From e793e6f166b7c7a62ec0867a535d6eb7ebf44d97 Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Thu, 7 Oct 2021 08:38:11 -0400 Subject: [PATCH] unifies getting of workspace name in displayment (#4060) --- .../__tests__/get-workspace-name.test.ts | 19 ---- .../app/models/helpers/get-workspace-name.ts | 6 -- .../models/helpers/workspace-operations.ts | 15 +-- .../dropdowns/workspace-card-dropdown.tsx | 17 ++-- .../dropdowns/workspace-dropdown.tsx | 8 +- .../modals/workspace-duplicate-modal.tsx | 11 +-- .../modals/workspace-settings-modal.tsx | 24 +++-- .../ui/components/settings/import-export.tsx | 6 +- .../ui/components/workspace-page-header.tsx | 12 +-- .../insomnia-app/app/ui/containers/app.tsx | 14 ++- .../app/ui/redux/__tests__/selectors.test.ts | 98 ++++++++++++++++++- .../insomnia-app/app/ui/redux/selectors.ts | 47 +++++++-- 12 files changed, 199 insertions(+), 78 deletions(-) delete mode 100644 packages/insomnia-app/app/models/helpers/__tests__/get-workspace-name.test.ts delete mode 100644 packages/insomnia-app/app/models/helpers/get-workspace-name.ts diff --git a/packages/insomnia-app/app/models/helpers/__tests__/get-workspace-name.test.ts b/packages/insomnia-app/app/models/helpers/__tests__/get-workspace-name.test.ts deleted file mode 100644 index 89a706e6bf..0000000000 --- a/packages/insomnia-app/app/models/helpers/__tests__/get-workspace-name.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as models from '../../../models'; -import { WorkspaceScopeKeys } from '../../workspace'; -import getWorkspaceName from '../get-workspace-name'; - -describe('getWorkspaceName', () => { - it('returns workspace name', () => { - const w = models.workspace.init(); - const s = models.apiSpec.init(); - w.scope = WorkspaceScopeKeys.collection; - expect(getWorkspaceName(w, s)).toBe(w.name); - }); - - it('returns api spec name', () => { - const w = models.workspace.init(); - const s = models.apiSpec.init(); - w.scope = WorkspaceScopeKeys.design; - expect(getWorkspaceName(w, s)).toBe(s.fileName); - }); -}); diff --git a/packages/insomnia-app/app/models/helpers/get-workspace-name.ts b/packages/insomnia-app/app/models/helpers/get-workspace-name.ts deleted file mode 100644 index 55db6f9a6a..0000000000 --- a/packages/insomnia-app/app/models/helpers/get-workspace-name.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { ApiSpec } from '../api-spec'; -import { isDesign, Workspace } from '../workspace'; - -export default function getWorkspaceName(w: Workspace, s: ApiSpec) { - return isDesign(w) ? s.fileName : w.name; -} diff --git a/packages/insomnia-app/app/models/helpers/workspace-operations.ts b/packages/insomnia-app/app/models/helpers/workspace-operations.ts index e9f3c54d63..074b6f1bee 100644 --- a/packages/insomnia-app/app/models/helpers/workspace-operations.ts +++ b/packages/insomnia-app/app/models/helpers/workspace-operations.ts @@ -3,20 +3,23 @@ import type { ApiSpec } from '../api-spec'; import * as models from '../index'; import { isDesign, Workspace } from '../workspace'; -export async function rename(w: Workspace, s: ApiSpec, name: string) { - if (isDesign(w)) { - await models.apiSpec.update(s, { +export async function rename(workspace: Workspace, apiSpec: ApiSpec, name: string) { + if (isDesign(workspace)) { + await models.apiSpec.update(apiSpec, { fileName: name, }); } else { - await models.workspace.update(w, { + await models.workspace.update(workspace, { name, }); } } -export async function duplicate(w: Workspace, { name, parentId }: Pick) { - const newWorkspace = await db.duplicate(w, { +export async function duplicate( + workspace: Workspace, + { name, parentId }: Pick, +) { + const newWorkspace = await db.duplicate(workspace, { name, parentId, }); diff --git a/packages/insomnia-app/app/ui/components/dropdowns/workspace-card-dropdown.tsx b/packages/insomnia-app/app/ui/components/dropdowns/workspace-card-dropdown.tsx index 3050303b28..fbcd3054ae 100644 --- a/packages/insomnia-app/app/ui/components/dropdowns/workspace-card-dropdown.tsx +++ b/packages/insomnia-app/app/ui/components/dropdowns/workspace-card-dropdown.tsx @@ -1,12 +1,12 @@ import { SvgIcon } from 'insomnia-components'; import React, { FC, useCallback, useState } from 'react'; +import { useSelector } from 'react-redux'; import { parseApiSpec } from '../../../common/api-specs'; import { getWorkspaceLabel } from '../../../common/get-workspace-label'; import { RENDER_PURPOSE_NO_RENDER } from '../../../common/render'; import * as models from '../../../models'; import type { ApiSpec } from '../../../models/api-spec'; -import getWorkspaceName from '../../../models/helpers/get-workspace-name'; import * as workspaceOperations from '../../../models/helpers/workspace-operations'; import { Project } from '../../../models/project'; import type { Workspace } from '../../../models/workspace'; @@ -15,6 +15,7 @@ import type { DocumentAction } from '../../../plugins'; import { getDocumentActions } from '../../../plugins'; import * as pluginContexts from '../../../plugins/context'; import { useLoadingRecord } from '../../hooks/use-loading-record'; +import { selectActiveWorkspaceName } from '../../redux/selectors'; import { Dropdown } from '../base/dropdown/dropdown'; import { DropdownButton } from '../base/dropdown/dropdown-button'; import { DropdownDivider } from '../base/dropdown/dropdown-divider'; @@ -33,13 +34,15 @@ const spinner = ; const useWorkspaceHandlers = ({ workspace, apiSpec }: Props) => { const handleDuplicate = useCallback(() => { - showWorkspaceDuplicateModal({ workspace, apiSpec }); - }, [apiSpec, workspace]); + showWorkspaceDuplicateModal({ workspace }); + }, [workspace]); + + const activeWorkspaceName = useSelector(selectActiveWorkspaceName); const handleRename = useCallback(() => { showPrompt({ title: `Rename ${getWorkspaceLabel(workspace).singular}`, - defaultValue: getWorkspaceName(workspace, apiSpec), + defaultValue: activeWorkspaceName, submitName: 'Rename', selectText: true, label: 'Name', @@ -47,13 +50,13 @@ const useWorkspaceHandlers = ({ workspace, apiSpec }: Props) => { await workspaceOperations.rename(workspace, apiSpec, name); }, }); - }, [apiSpec, workspace]); + }, [apiSpec, workspace, activeWorkspaceName]); const handleDelete = useCallback(() => { const label = getWorkspaceLabel(workspace); showModal(AskModal, { title: `Delete ${label.singular}`, - message: `Do you really want to delete "${getWorkspaceName(workspace, apiSpec)}"?`, + message: `Do you really want to delete "${activeWorkspaceName}"?`, yesText: 'Yes', noText: 'Cancel', onDone: async (isYes: boolean) => { @@ -65,7 +68,7 @@ const useWorkspaceHandlers = ({ workspace, apiSpec }: Props) => { await models.workspace.remove(workspace); }, }); - }, [apiSpec, workspace]); + }, [workspace, activeWorkspaceName]); return { handleDelete, handleDuplicate, handleRename }; }; diff --git a/packages/insomnia-app/app/ui/components/dropdowns/workspace-dropdown.tsx b/packages/insomnia-app/app/ui/components/dropdowns/workspace-dropdown.tsx index 4bf8d8929f..a13a70f475 100644 --- a/packages/insomnia-app/app/ui/components/dropdowns/workspace-dropdown.tsx +++ b/packages/insomnia-app/app/ui/components/dropdowns/workspace-dropdown.tsx @@ -30,9 +30,9 @@ import { SettingsModal, TAB_INDEX_EXPORT } from '../modals/settings-modal'; import { WorkspaceSettingsModal } from '../modals/workspace-settings-modal'; interface Props { - displayName: string; activeEnvironment: Environment | null; activeWorkspace: Workspace; + activeWorkspaceName: string; activeApiSpec: ApiSpec; activeProject: Project; hotKeyRegistry: HotKeyRegistry; @@ -131,7 +131,7 @@ export class WorkspaceDropdown extends PureComponent { render() { const { - displayName, + activeWorkspaceName, className, activeWorkspace, isLoading, @@ -157,9 +157,9 @@ export class WorkspaceDropdown extends PureComponent { style={{ maxWidth: '400px', }} - title={displayName} + title={activeWorkspaceName} > - {displayName} + {activeWorkspaceName} {isLoading ? : null} diff --git a/packages/insomnia-app/app/ui/components/modals/workspace-duplicate-modal.tsx b/packages/insomnia-app/app/ui/components/modals/workspace-duplicate-modal.tsx index ca7c9cb80e..5728d9226a 100644 --- a/packages/insomnia-app/app/ui/components/modals/workspace-duplicate-modal.tsx +++ b/packages/insomnia-app/app/ui/components/modals/workspace-duplicate-modal.tsx @@ -8,15 +8,13 @@ import { AUTOBIND_CFG } from '../../../common/constants'; import { getWorkspaceLabel } from '../../../common/get-workspace-label'; import { strings } from '../../../common/strings'; import * as models from '../../../models'; -import { ApiSpec } from '../../../models/api-spec'; -import getWorkspaceName from '../../../models/helpers/get-workspace-name'; import * as workspaceOperations from '../../../models/helpers/workspace-operations'; import { isDefaultProject, isLocalProject, isRemoteProject, Project } from '../../../models/project'; import { Workspace } from '../../../models/workspace'; import { initializeLocalBackendProjectAndMarkForSync } from '../../../sync/vcs/initialize-backend-project'; import { VCS } from '../../../sync/vcs/vcs'; import { activateWorkspace } from '../../redux/modules/workspace'; -import { selectActiveProject, selectIsLoggedIn, selectProjects } from '../../redux/selectors'; +import { selectActiveProject, selectActiveWorkspaceName, selectIsLoggedIn, selectProjects } from '../../redux/selectors'; import { Modal } from '../base/modal'; import { ModalBody } from '../base/modal-body'; import { ModalFooter } from '../base/modal-footer'; @@ -25,7 +23,6 @@ import { showModal } from '.'; interface Options { workspace: Workspace; - apiSpec: ApiSpec; onDone?: () => void; } @@ -44,7 +41,7 @@ const ProjectOption: FC = project => ( ); -const WorkspaceDuplicateModalInternalWithRef: ForwardRefRenderFunction = ({ workspace, apiSpec, onDone, hide, vcs }, ref) => { +const WorkspaceDuplicateModalInternalWithRef: ForwardRefRenderFunction = ({ workspace, onDone, hide, vcs }, ref) => { const dispatch = useDispatch(); const projects = useSelector(selectProjects); @@ -52,7 +49,7 @@ const WorkspaceDuplicateModalInternalWithRef: ForwardRefRenderFunction({ defaultValues: { - newName: defaultWorkspaceName, + newName: activeWorkspaceName, projectId: activeProject._id, }, }); diff --git a/packages/insomnia-app/app/ui/components/modals/workspace-settings-modal.tsx b/packages/insomnia-app/app/ui/components/modals/workspace-settings-modal.tsx index 3b01a2a87e..f611867b85 100644 --- a/packages/insomnia-app/app/ui/components/modals/workspace-settings-modal.tsx +++ b/packages/insomnia-app/app/ui/components/modals/workspace-settings-modal.tsx @@ -1,5 +1,6 @@ import { autoBindMethodsForReact } from 'class-autobind-decorator'; import React, { FC, PureComponent, ReactNode } from 'react'; +import { connect } from 'react-redux'; import { Tab, TabList, TabPanel, Tabs } from 'react-tabs'; import styled from 'styled-components'; @@ -8,10 +9,11 @@ import { getWorkspaceLabel } from '../../../common/get-workspace-label'; import { HandleGetRenderContext, HandleRender } from '../../../common/render'; import type { ApiSpec } from '../../../models/api-spec'; import type { ClientCertificate } from '../../../models/client-certificate'; -import getWorkspaceName from '../../../models/helpers/get-workspace-name'; import * as workspaceOperations from '../../../models/helpers/workspace-operations'; import * as models from '../../../models/index'; import type { Workspace } from '../../../models/workspace'; +import { RootState } from '../../redux/modules'; +import { selectActiveWorkspaceName } from '../../redux/selectors'; import { DebouncedInput } from '../base/debounced-input'; import { FileInputButton } from '../base/file-input-button'; import { Modal } from '../base/modal'; @@ -58,7 +60,9 @@ const CertificateField: FC<{ ); }; -interface Props { +type ReduxProps = ReturnType; + +interface Props extends ReduxProps { clientCertificates: ClientCertificate[]; workspace: Workspace; apiSpec: ApiSpec; @@ -87,7 +91,7 @@ interface State { } @autoBindMethodsForReact(AUTOBIND_CFG) -export class WorkspaceSettingsModal extends PureComponent { +export class UnconnectedWorkspaceSettingsModal extends PureComponent { modal: Modal | null = null; state: State = { @@ -127,8 +131,8 @@ export class WorkspaceSettingsModal extends PureComponent { } _handleDuplicateWorkspace() { - const { workspace, apiSpec } = this.props; - showWorkspaceDuplicateModal({ workspace, apiSpec, onDone: this.hide }); + const { workspace } = this.props; + showWorkspaceDuplicateModal({ workspace, onDone: this.hide }); } _handleToggleCertificateForm() { @@ -292,7 +296,7 @@ export class WorkspaceSettingsModal extends PureComponent { const { clientCertificates, workspace, - apiSpec, + activeWorkspaceName, editorLineWrapping, editorFontSize, editorIndentSize, @@ -333,7 +337,7 @@ export class WorkspaceSettingsModal extends PureComponent { type="text" delay={500} placeholder="Awesome API" - defaultValue={getWorkspaceName(workspace, apiSpec)} + defaultValue={activeWorkspaceName} onChange={this._handleRename} /> @@ -539,3 +543,9 @@ export class WorkspaceSettingsModal extends PureComponent { ); } } + +const mapStateToProps = (state: RootState) => ({ + activeWorkspaceName: selectActiveWorkspaceName(state), +}); + +export const WorkspaceSettingsModal = connect(mapStateToProps, null, null, { forwardRef: true })(UnconnectedWorkspaceSettingsModal); diff --git a/packages/insomnia-app/app/ui/components/settings/import-export.tsx b/packages/insomnia-app/app/ui/components/settings/import-export.tsx index 7933cc8dd1..3182747571 100644 --- a/packages/insomnia-app/app/ui/components/settings/import-export.tsx +++ b/packages/insomnia-app/app/ui/components/settings/import-export.tsx @@ -8,7 +8,7 @@ import { getWorkspaceLabel } from '../../../common/get-workspace-label'; import { strings } from '../../../common/strings'; import { exportAllToFile } from '../../redux/modules/global'; import { importClipBoard, importFile, importUri } from '../../redux/modules/import'; -import { selectActiveProjectName, selectActiveWorkspace } from '../../redux/selectors'; +import { selectActiveProjectName, selectActiveWorkspace, selectActiveWorkspaceName } from '../../redux/selectors'; import { Dropdown } from '../base/dropdown/dropdown'; import { DropdownButton } from '../base/dropdown/dropdown-button'; import { DropdownDivider } from '../base/dropdown/dropdown-divider'; @@ -64,6 +64,8 @@ export const ImportExport: FC = ({ hideSettingsModal }) => { hideSettingsModal(); }, [hideSettingsModal, activeWorkspace, dispatch]); + const activeWorkspaceName = useSelector(selectActiveWorkspaceName); + return (
@@ -83,7 +85,7 @@ export const ImportExport: FC = ({ hideSettingsModal }) => { Choose Export Type {activeWorkspace && - Export the "{activeWorkspace.name}" {getWorkspaceLabel(activeWorkspace).singular} + Export the "{activeWorkspaceName}" {getWorkspaceLabel(activeWorkspace).singular} } diff --git a/packages/insomnia-app/app/ui/components/workspace-page-header.tsx b/packages/insomnia-app/app/ui/components/workspace-page-header.tsx index 44d29104c5..f685a3ae79 100644 --- a/packages/insomnia-app/app/ui/components/workspace-page-header.tsx +++ b/packages/insomnia-app/app/ui/components/workspace-page-header.tsx @@ -3,7 +3,7 @@ import React, { Fragment, FunctionComponent, ReactNode, useCallback } from 'reac import { ACTIVITY_HOME, GlobalActivity } from '../../common/constants'; import { strings } from '../../common/strings'; -import { isCollection, isDesign } from '../../models/workspace'; +import { isDesign } from '../../models/workspace'; import coreLogo from '../images/insomnia-core-logo.png'; import { ActivityToggle } from './activity-toggle'; import SettingsButton from './buttons/settings-button'; @@ -23,6 +23,7 @@ export const WorkspacePageHeader: FunctionComponent = ({ wrapperProps: { activeApiSpec, activeWorkspace, + activeWorkspaceName, activeProject, activeEnvironment, settings, @@ -35,18 +36,15 @@ export const WorkspacePageHeader: FunctionComponent = ({ [activeWorkspace, handleActivityChange], ); - if (!activeWorkspace || !activeApiSpec || !activity) { + if (!activeWorkspace || !activeWorkspaceName || !activeApiSpec || !activity) { return null; } - const collection = isCollection(activeWorkspace); - const design = isDesign(activeWorkspace); - const workspace = ( = ({ } gridCenter={ - design && ( + isDesign(activeWorkspace) && ( { const { activeWorkspace, activeProject, - activeApiSpec, + activeWorkspaceName, activeEnvironment, activeRequest, activity, @@ -1103,9 +1105,9 @@ class App extends PureComponent { if (activity === ACTIVITY_HOME || activity === ACTIVITY_MIGRATION) { title = getAppName(); - } else if (activeWorkspace && activeApiSpec) { + } else if (activeWorkspace && activeWorkspaceName) { title = activeProject.name; - title += ` - ${isCollection(activeWorkspace) ? activeWorkspace.name : activeApiSpec.fileName}`; + title += ` - ${activeWorkspaceName}`; if (activeEnvironment) { title += ` (${activeEnvironment.name})`; @@ -1590,6 +1592,7 @@ function mapStateToProps(state: RootState) { const workspaces = selectWorkspacesForActiveProject(state); const activeWorkspaceMeta = selectActiveWorkspaceMeta(state); const activeWorkspace = selectActiveWorkspace(state); + const activeWorkspaceName = selectActiveWorkspaceName(state); const activeWorkspaceClientCertificates = selectActiveWorkspaceClientCertificates(state); const activeGitRepository = selectActiveGitRepository(state); @@ -1631,7 +1634,7 @@ function mapStateToProps(state: RootState) { const syncItems = selectSyncItems(state); // Api spec stuff - const activeApiSpec = apiSpecs.find(s => s.parentId === activeWorkspace?._id); + const activeApiSpec = selectActiveApiSpec(state); // Test stuff const activeUnitTests = selectActiveUnitTests(state); @@ -1643,6 +1646,7 @@ function mapStateToProps(state: RootState) { activity: activeActivity, activeProject, activeApiSpec, + activeWorkspaceName, activeCookieJar, activeEnvironment, activeGitRepository, diff --git a/packages/insomnia-app/app/ui/redux/__tests__/selectors.test.ts b/packages/insomnia-app/app/ui/redux/__tests__/selectors.test.ts index efaa1e9799..22d0ded68a 100644 --- a/packages/insomnia-app/app/ui/redux/__tests__/selectors.test.ts +++ b/packages/insomnia-app/app/ui/redux/__tests__/selectors.test.ts @@ -1,8 +1,10 @@ import { globalBeforeEach } from '../../../__jest__/before-each'; import { reduxStateForTest } from '../../../__jest__/redux-state-for-test'; +import { ACTIVITY_DEBUG } from '../../../common/constants'; import * as models from '../../../models'; import { DEFAULT_PROJECT_ID, Project } from '../../../models/project'; -import { selectActiveProject } from '../selectors'; +import { WorkspaceScopeKeys } from '../../../models/workspace'; +import { selectActiveApiSpec, selectActiveProject, selectActiveWorkspaceName } from '../selectors'; describe('selectors', () => { beforeEach(globalBeforeEach); @@ -44,4 +46,98 @@ describe('selectors', () => { expect(project).toStrictEqual(expect.objectContaining>({ _id: DEFAULT_PROJECT_ID })); }); }); + + describe('selectActiveApiSpec', () => { + it('will return undefined when there is not an active workspace', async () => { + const state = await reduxStateForTest({ + activeWorkspaceId: null, + }); + + expect(selectActiveApiSpec(state)).toBe(undefined); + }); + + it('will return throw when there is not an active apiSpec', async () => { + const workspace = await models.workspace.create({ + name: 'workspace.name', + scope: WorkspaceScopeKeys.design, + }); + + const state = await reduxStateForTest({ + activeActivity: ACTIVITY_DEBUG, + activeWorkspaceId: workspace._id, + }); + state.entities.apiSpecs = {}; + + const execute = () => selectActiveApiSpec(state); + expect(execute).toThrowError(`an api spec not found for the workspace ${workspace._id} (workspace.name)`); + }); + + it('will return the apiSpec for a given workspace', async () => { + const workspace = await models.workspace.create({ + name: 'workspace.name', + scope: WorkspaceScopeKeys.design, + }); + const spec = await models.apiSpec.updateOrCreateForParentId( + workspace._id, + { fileName: 'apiSpec.fileName' }, + ); + + const state = await reduxStateForTest({ + activeActivity: ACTIVITY_DEBUG, + activeWorkspaceId: workspace._id, + }); + + expect(selectActiveApiSpec(state)).toEqual(spec); + }); + }); + + describe('selectActiveWorkspaceName', () => { + it('returns workspace name for collections', async () => { + const workspace = await models.workspace.create({ + name: 'workspace.name', + scope: WorkspaceScopeKeys.collection, + }); + // even though this shouldn't technically happen, we want to make sure the selector still makes the right decision (and ignores the api spec for collections) + await models.apiSpec.updateOrCreateForParentId( + workspace._id, + { fileName: 'apiSpec.fileName' }, + ); + const state = await reduxStateForTest({ + activeActivity: ACTIVITY_DEBUG, + activeWorkspaceId: workspace._id, + }); + + expect(selectActiveWorkspaceName(state)).toBe('workspace.name'); + }); + + it('returns api spec name for design documents', async () => { + const workspace = await models.workspace.create({ + name: 'workspace.name', + scope: WorkspaceScopeKeys.design, + }); + await models.apiSpec.updateOrCreateForParentId( + workspace._id, + { fileName: 'apiSpec.fileName' }, + ); + const state = await reduxStateForTest({ + activeActivity: ACTIVITY_DEBUG, + activeWorkspaceId: workspace._id, + }); + + expect(selectActiveWorkspaceName(state)).toBe('apiSpec.fileName'); + }); + + it('returns undefined when there is not an active workspace', async () => { + await models.workspace.create({ + name: 'workspace.name', + scope: WorkspaceScopeKeys.collection, + }); + const state = await reduxStateForTest({ + activeActivity: ACTIVITY_DEBUG, + activeWorkspaceId: null, + }); + + expect(selectActiveWorkspaceName(state)).toBe(undefined); + }); + }); }); diff --git a/packages/insomnia-app/app/ui/redux/selectors.ts b/packages/insomnia-app/app/ui/redux/selectors.ts index 6856f259f2..cfff124437 100644 --- a/packages/insomnia-app/app/ui/redux/selectors.ts +++ b/packages/insomnia-app/app/ui/redux/selectors.ts @@ -10,6 +10,7 @@ import { DEFAULT_PROJECT_ID, isRemoteProject } from '../../models/project'; import { isRequest, Request } from '../../models/request'; import { isRequestGroup, RequestGroup } from '../../models/request-group'; import { UnitTestResult } from '../../models/unit-test-result'; +import { isCollection } from '../../models/workspace'; import { RootState } from './modules'; type EntitiesLists = { @@ -100,15 +101,10 @@ export const selectAllWorkspaces = createSelector( entities => entities.workspaces, ); -export const selectAllApiSpecs = createSelector( - selectEntitiesLists, - entities => entities.apiSpecs, -); - export const selectWorkspacesForActiveProject = createSelector( selectAllWorkspaces, selectActiveProject, - (workspaces, activeProject) => workspaces.filter(w => w.parentId === activeProject._id), + (workspaces, activeProject) => workspaces.filter(workspace => workspace.parentId === activeProject._id), ); export const selectActiveWorkspace = createSelector( @@ -118,7 +114,7 @@ export const selectActiveWorkspace = createSelector( (workspaces, activeWorkspaceId, activeActivity) => { // Only return an active workspace if we're in an activity if (activeActivity && isWorkspaceActivity(activeActivity)) { - const workspace = workspaces.find(w => w._id === activeWorkspaceId); + const workspace = workspaces.find(workspace => workspace._id === activeWorkspaceId); return workspace; } @@ -135,6 +131,43 @@ export const selectActiveWorkspaceMeta = createSelector( }, ); +export const selectAllApiSpecs = createSelector( + selectEntitiesLists, + entities => entities.apiSpecs, +); + +export const selectActiveApiSpec = createSelector( + selectAllApiSpecs, + selectActiveWorkspace, + (apiSpecs, activeWorkspace) => { + if (!activeWorkspace) { + // There should never be an active api spec without an active workspace + return undefined; + } + const activeSpec = apiSpecs.find(apiSpec => apiSpec.parentId === activeWorkspace._id); + + if (!activeSpec) { + // This case should never be reached; an api spec should always exist for a given workspace. + throw new Error(`an api spec not found for the workspace ${activeWorkspace._id} (${activeWorkspace.name})`); + } + + return activeSpec; + } +); + +export const selectActiveWorkspaceName = createSelector( + selectActiveWorkspace, + selectActiveApiSpec, + (activeWorkspace, activeApiSpec) => { + if (!activeWorkspace) { + // see above, but since the selectActiveWorkspace selector really can return undefined, we need to handle it here. + return undefined; + } + + return isCollection(activeWorkspace) ? activeWorkspace.name : activeApiSpec?.fileName; + } +); + export const selectActiveEnvironment = createSelector( selectActiveWorkspaceMeta, selectEntitiesLists,