unifies getting of workspace name in displayment (#4060)

This commit is contained in:
Dimitri Mitropoulos 2021-10-07 08:38:11 -04:00 committed by GitHub
parent 615287ccfc
commit e793e6f166
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 199 additions and 78 deletions

View File

@ -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);
});
});

View File

@ -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;
}

View File

@ -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<Workspace, 'name' | 'parentId'>) {
const newWorkspace = await db.duplicate(w, {
export async function duplicate(
workspace: Workspace,
{ name, parentId }: Pick<Workspace, 'name' | 'parentId'>,
) {
const newWorkspace = await db.duplicate(workspace, {
name,
parentId,
});

View File

@ -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 = <i className="fa fa-refresh fa-spin" />;
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 };
};

View File

@ -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<Props, State> {
render() {
const {
displayName,
activeWorkspaceName,
className,
activeWorkspace,
isLoading,
@ -157,9 +157,9 @@ export class WorkspaceDropdown extends PureComponent<Props, State> {
style={{
maxWidth: '400px',
}}
title={displayName}
title={activeWorkspaceName}
>
{displayName}
{activeWorkspaceName}
</div>
<i className="fa fa-caret-down space-left" />
{isLoading ? <i className="fa fa-refresh fa-spin space-left" /> : null}

View File

@ -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> = project => (
</option>
);
const WorkspaceDuplicateModalInternalWithRef: ForwardRefRenderFunction<Modal, InnerProps> = ({ workspace, apiSpec, onDone, hide, vcs }, ref) => {
const WorkspaceDuplicateModalInternalWithRef: ForwardRefRenderFunction<Modal, InnerProps> = ({ workspace, onDone, hide, vcs }, ref) => {
const dispatch = useDispatch();
const projects = useSelector(selectProjects);
@ -52,7 +49,7 @@ const WorkspaceDuplicateModalInternalWithRef: ForwardRefRenderFunction<Modal, In
const isLoggedIn = useSelector(selectIsLoggedIn);
const title = `Duplicate ${getWorkspaceLabel(workspace).singular}`;
const defaultWorkspaceName = getWorkspaceName(workspace, apiSpec);
const activeWorkspaceName = useSelector(selectActiveWorkspaceName);
const {
register,
@ -62,7 +59,7 @@ const WorkspaceDuplicateModalInternalWithRef: ForwardRefRenderFunction<Modal, In
errors,
} } = useForm<FormFields>({
defaultValues: {
newName: defaultWorkspaceName,
newName: activeWorkspaceName,
projectId: activeProject._id,
},
});

View File

@ -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<typeof mapStateToProps>;
interface Props extends ReduxProps {
clientCertificates: ClientCertificate[];
workspace: Workspace;
apiSpec: ApiSpec;
@ -87,7 +91,7 @@ interface State {
}
@autoBindMethodsForReact(AUTOBIND_CFG)
export class WorkspaceSettingsModal extends PureComponent<Props, State> {
export class UnconnectedWorkspaceSettingsModal extends PureComponent<Props, State> {
modal: Modal | null = null;
state: State = {
@ -127,8 +131,8 @@ export class WorkspaceSettingsModal extends PureComponent<Props, State> {
}
_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<Props, State> {
const {
clientCertificates,
workspace,
apiSpec,
activeWorkspaceName,
editorLineWrapping,
editorFontSize,
editorIndentSize,
@ -333,7 +337,7 @@ export class WorkspaceSettingsModal extends PureComponent<Props, State> {
type="text"
delay={500}
placeholder="Awesome API"
defaultValue={getWorkspaceName(workspace, apiSpec)}
defaultValue={activeWorkspaceName}
onChange={this._handleRename}
/>
</label>
@ -539,3 +543,9 @@ export class WorkspaceSettingsModal extends PureComponent<Props, State> {
);
}
}
const mapStateToProps = (state: RootState) => ({
activeWorkspaceName: selectActiveWorkspaceName(state),
});
export const WorkspaceSettingsModal = connect(mapStateToProps, null, null, { forwardRef: true })(UnconnectedWorkspaceSettingsModal);

View File

@ -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<Props> = ({ hideSettingsModal }) => {
hideSettingsModal();
}, [hideSettingsModal, activeWorkspace, dispatch]);
const activeWorkspaceName = useSelector(selectActiveWorkspaceName);
return (
<div>
<div className="no-margin-top">
@ -83,7 +85,7 @@ export const ImportExport: FC<Props> = ({ hideSettingsModal }) => {
<DropdownDivider>Choose Export Type</DropdownDivider>
{activeWorkspace && <DropdownItem onClick={showExportRequestsModal}>
<i className="fa fa-home" />
Export the "{activeWorkspace.name}" {getWorkspaceLabel(activeWorkspace).singular}
Export the "{activeWorkspaceName}" {getWorkspaceLabel(activeWorkspace).singular}
</DropdownItem>}
<DropdownItem onClick={handleExportAllToFile}>
<i className="fa fa-empty" />

View File

@ -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<Props> = ({
wrapperProps: {
activeApiSpec,
activeWorkspace,
activeWorkspaceName,
activeProject,
activeEnvironment,
settings,
@ -35,18 +36,15 @@ export const WorkspacePageHeader: FunctionComponent<Props> = ({
[activeWorkspace, handleActivityChange],
);
if (!activeWorkspace || !activeApiSpec || !activity) {
if (!activeWorkspace || !activeWorkspaceName || !activeApiSpec || !activity) {
return null;
}
const collection = isCollection(activeWorkspace);
const design = isDesign(activeWorkspace);
const workspace = (
<WorkspaceDropdown
displayName={collection ? activeWorkspace.name : activeApiSpec.fileName}
activeEnvironment={activeEnvironment}
activeWorkspace={activeWorkspace}
activeWorkspaceName={activeWorkspaceName}
activeApiSpec={activeApiSpec}
activeProject={activeProject}
hotKeyRegistry={settings.hotKeyRegistry}
@ -69,7 +67,7 @@ export const WorkspacePageHeader: FunctionComponent<Props> = ({
</Fragment>
}
gridCenter={
design && (
isDesign(activeWorkspace) && (
<ActivityToggle
activity={activity}
handleActivityChange={handleActivityChange}

View File

@ -52,7 +52,7 @@ import { Request, updateMimeType } from '../../models/request';
import { isRequestGroup, RequestGroup } from '../../models/request-group';
import { RequestMeta } from '../../models/request-meta';
import { Response } from '../../models/response';
import { isCollection, isWorkspace } from '../../models/workspace';
import { isWorkspace } from '../../models/workspace';
import { WorkspaceMeta } from '../../models/workspace-meta';
import * as network from '../../network/network';
import * as plugins from '../../plugins';
@ -97,6 +97,7 @@ import {
} from '../redux/modules/global';
import { importUri } from '../redux/modules/import';
import {
selectActiveApiSpec,
selectActiveCookieJar,
selectActiveEnvironment,
selectActiveGitRepository,
@ -113,6 +114,7 @@ import {
selectActiveWorkspace,
selectActiveWorkspaceClientCertificates,
selectActiveWorkspaceMeta,
selectActiveWorkspaceName,
selectEntitiesLists,
selectSettings,
selectSyncItems,
@ -1094,7 +1096,7 @@ class App extends PureComponent<AppProps, State> {
const {
activeWorkspace,
activeProject,
activeApiSpec,
activeWorkspaceName,
activeEnvironment,
activeRequest,
activity,
@ -1103,9 +1105,9 @@ class App extends PureComponent<AppProps, State> {
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,

View File

@ -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<Partial<Project>>({ _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);
});
});
});

View File

@ -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,