Remove/redux-loading (#5381)

* removes 4 redux actions

* fix lint

* fix tests

* fix tests (actually)

* grumble

* remove unused const

* request timer still needs state from somewhere

* remove redux from import logic

* simplfied import files

* simplfy imports again

* move to ui root

* remove unused

* drag and drop import

* remove console log

* fix lint

* key loading state on request id

* fix loading first state
This commit is contained in:
Jack Kavanagh 2022-11-16 09:56:58 +00:00 committed by GitHub
parent de9cee96d3
commit fc730b67e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 959 additions and 1243 deletions

View File

@ -39,9 +39,7 @@ export const reduxStateForTest = async (global: Partial<GlobalState> = {}): Prom
activeActivity: ACTIVITY_HOME,
activeProjectId: DEFAULT_PROJECT_ID,
dashboardSortOrder: 'modified-desc',
isLoading: false,
isLoggedIn: false,
loadingRequestIds: {},
...global,
},
};

View File

@ -1,9 +1,9 @@
import { describe, expect, it, jest } from '@jest/globals';
import * as modals from '../../../components/modals';
import { askToImportIntoWorkspace, ForceToWorkspace } from '../helpers';
import * as modals from '../../ui/components/modals';
import { askToImportIntoWorkspace, ForceToWorkspace } from '../import';
jest.mock('../../../components/modals');
jest.mock('../../ui/components/modals');
describe('askToImportIntoWorkspace', () => {
it('should return null if no active workspace', () => {

View File

@ -1,11 +1,18 @@
import clone from 'clone';
import { format } from 'date-fns';
import fs from 'fs';
import { NoParamCallback } from 'fs-extra';
import { Insomnia4Data } from 'insomnia-importers';
import path from 'path';
import React from 'react';
import { unreachableCase } from 'ts-assert-unreachable';
import YAML from 'yaml';
import { isApiSpec } from '../models/api-spec';
import { isCookieJar } from '../models/cookie-jar';
import { isEnvironment } from '../models/environment';
import { Environment, isEnvironment } from '../models/environment';
import { isGrpcRequest } from '../models/grpc-request';
import * as requestOperations from '../models/helpers/request-operations';
import type { BaseModel } from '../models/index';
import * as models from '../models/index';
import { isProtoDirectory } from '../models/proto-directory';
@ -19,6 +26,9 @@ import { isWebSocketRequest } from '../models/websocket-request';
import { isWorkspace, Workspace } from '../models/workspace';
import { resetKeys } from '../sync/ignore-keys';
import { SegmentEvent, trackSegmentEvent } from '../ui/analytics';
import { showAlert, showError, showModal } from '../ui/components/modals';
import { AskModal } from '../ui/components/modals/ask-modal';
import { SelectModal } from '../ui/components/modals/select-modal';
import {
EXPORT_TYPE_API_SPEC,
EXPORT_TYPE_COOKIE_JAR,
@ -35,8 +45,9 @@ import {
EXPORT_TYPE_WORKSPACE,
getAppVersion,
} from './constants';
import { database as db } from './database';
import { database, database as db } from './database';
import * as har from './har';
import { strings } from './strings';
const EXPORT_FORMAT = 4;
@ -270,3 +281,243 @@ export async function exportRequestsData(
throw new Error(`Invalid export format ${format}. Must be "json" or "yaml"`);
}
}
const VALUE_JSON = 'json';
const VALUE_YAML = 'yaml';
const VALUE_HAR = 'har';
export type SelectedFormat =
| typeof VALUE_HAR
| typeof VALUE_JSON
| typeof VALUE_YAML
;
const showSelectExportTypeModal = ({ onDone }: {
onDone: (selectedFormat: SelectedFormat) => Promise<void>;
}) => {
const options = [
{
name: 'Insomnia v4 (JSON)',
value: VALUE_JSON,
},
{
name: 'Insomnia v4 (YAML)',
value: VALUE_YAML,
},
{
name: 'HAR HTTP Archive Format',
value: VALUE_HAR,
},
];
const lastFormat = window.localStorage.getItem('insomnia.lastExportFormat');
const defaultValue = options.find(({ value }) => value === lastFormat) ? lastFormat : VALUE_JSON;
showModal(SelectModal, {
title: 'Select Export Type',
value: defaultValue,
options,
message: 'Which format would you like to export as?',
onDone: async selectedFormat => {
if (selectedFormat) {
window.localStorage.setItem('insomnia.lastExportFormat', selectedFormat);
await onDone(selectedFormat as SelectedFormat);
}
},
});
};
const showExportPrivateEnvironmentsModal = async (privateEnvNames: string) => {
return new Promise<boolean>(resolve => {
showModal(AskModal, {
title: 'Export Private Environments?',
message: `Do you want to include private environments (${privateEnvNames}) in your export?`,
onDone: async (isYes: boolean) => {
if (isYes) {
resolve(true);
} else {
resolve(false);
}
},
});
});
};
const showSaveExportedFileDialog = async ({
exportedFileNamePrefix,
selectedFormat,
}: {
exportedFileNamePrefix: string;
selectedFormat: SelectedFormat;
}) => {
const date = format(Date.now(), 'yyyy-MM-dd');
const name = exportedFileNamePrefix.replace(/ /g, '-');
const lastDir = window.localStorage.getItem('insomnia.lastExportPath');
const dir = lastDir || window.app.getPath('desktop');
const options = {
title: 'Export Insomnia Data',
buttonLabel: 'Export',
defaultPath: `${path.join(dir, `${name}_${date}`)}.${selectedFormat}`,
};
const { filePath } = await window.dialog.showSaveDialog(options);
return filePath || null;
};
const writeExportedFileToFileSystem = (filename: string, jsonData: string, onDone: NoParamCallback) => {
// Remember last exported path
window.localStorage.setItem('insomnia.lastExportPath', path.dirname(filename));
fs.writeFile(filename, jsonData, {}, onDone);
};
export const exportAllToFile = (activeProjectName: string, workspacesForActiveProject: Workspace[]) => {
if (!workspacesForActiveProject.length) {
showAlert({
title: 'Cannot export',
message: <>There are no workspaces to export in the <strong>{activeProjectName}</strong> {strings.project.singular.toLowerCase()}.</>,
});
return;
}
showSelectExportTypeModal({
onDone: async selectedFormat => {
// Check if we want to export private environments.
const environments = await models.environment.all();
let exportPrivateEnvironments = false;
const privateEnvironments = environments.filter(environment => environment.isPrivate);
if (privateEnvironments.length) {
const names = privateEnvironments.map(environment => environment.name).join(', ');
exportPrivateEnvironments = await showExportPrivateEnvironmentsModal(names);
}
const fileName = await showSaveExportedFileDialog({
exportedFileNamePrefix: 'Insomnia-All',
selectedFormat,
});
if (!fileName) {
return;
}
let stringifiedExport;
try {
switch (selectedFormat) {
case VALUE_HAR:
stringifiedExport = await exportWorkspacesHAR(workspacesForActiveProject, exportPrivateEnvironments);
break;
case VALUE_YAML:
stringifiedExport = await exportWorkspacesData(workspacesForActiveProject, exportPrivateEnvironments, 'yaml');
break;
case VALUE_JSON:
stringifiedExport = await exportWorkspacesData(workspacesForActiveProject, exportPrivateEnvironments, 'json');
break;
default:
unreachableCase(selectedFormat, `selected export format "${selectedFormat}" is invalid`);
}
} catch (err) {
showError({
title: 'Export Failed',
error: err,
message: 'Export failed due to an unexpected error',
});
return;
}
writeExportedFileToFileSystem(fileName, stringifiedExport, err => {
if (err) {
console.warn('Export failed', err);
}
});
},
});
};
export const exportRequestsToFile = (requestIds: string[]) => {
showSelectExportTypeModal({
onDone: async selectedFormat => {
const requests: BaseModel[] = [];
const privateEnvironments: Environment[] = [];
const workspaceLookup: any = {};
for (const requestId of requestIds) {
const request = await requestOperations.getById(requestId);
if (request == null) {
continue;
}
requests.push(request);
const ancestors = await database.withAncestors(request, [
models.workspace.type,
models.requestGroup.type,
]);
const workspace = ancestors.find(isWorkspace);
if (workspace == null || workspaceLookup.hasOwnProperty(workspace._id)) {
continue;
}
workspaceLookup[workspace._id] = true;
const descendants = await database.withDescendants(workspace);
const privateEnvs = descendants.filter(isEnvironment).filter(
descendant => descendant.isPrivate,
);
privateEnvironments.push(...privateEnvs);
}
let exportPrivateEnvironments = false;
if (privateEnvironments.length) {
const names = privateEnvironments.map(privateEnvironment => privateEnvironment.name).join(', ');
exportPrivateEnvironments = await showExportPrivateEnvironmentsModal(names);
}
const fileName = await showSaveExportedFileDialog({
exportedFileNamePrefix: 'Insomnia',
selectedFormat,
});
if (!fileName) {
return;
}
let stringifiedExport;
try {
switch (selectedFormat) {
case VALUE_HAR:
stringifiedExport = await exportRequestsHAR(requests, exportPrivateEnvironments);
break;
case VALUE_YAML:
stringifiedExport = await exportRequestsData(requests, exportPrivateEnvironments, 'yaml');
break;
case VALUE_JSON:
stringifiedExport = await exportRequestsData(requests, exportPrivateEnvironments, 'json');
break;
default:
unreachableCase(selectedFormat, `selected export format "${selectedFormat}" is invalid`);
}
} catch (err) {
showError({
title: 'Export Failed',
error: err,
message: 'Export failed due to an unexpected error',
});
return;
}
writeExportedFileToFileSystem(fileName, stringifiedExport, err => {
if (err) {
console.warn('Export failed', err);
}
});
},
});
};

View File

@ -4,12 +4,14 @@ import { convert, ConvertResultType } from 'insomnia-importers';
import type { ApiSpec } from '../models/api-spec';
import type { BaseModel } from '../models/index';
import * as models from '../models/index';
import { Project } from '../models/project';
import { isRequest } from '../models/request';
import { isWorkspace, Workspace } from '../models/workspace';
import { isWorkspace, Workspace, WorkspaceScope, WorkspaceScopeKeys } from '../models/workspace';
import { SegmentEvent, trackSegmentEvent } from '../ui/analytics';
import { AlertModal } from '../ui/components/modals/alert-modal';
import { AskModal } from '../ui/components/modals/ask-modal';
import { showError, showModal } from '../ui/components/modals/index';
import { ImportToWorkspacePrompt, SetWorkspaceScopePrompt } from '../ui/redux/modules/helpers';
import { showSelectModal } from '../ui/components/modals/select-modal';
import {
BASE_ENVIRONMENT_ID_KEY,
CONTENT_TYPE_GRAPHQL,
@ -265,8 +267,26 @@ export async function importRaw(
} else {
// If workspace, check and set the scope and parentId while importing a new workspace
if (isWorkspace(model)) {
await updateWorkspaceScope(resource as Workspace, resultsType, getWorkspaceScope);
// Set the workspace scope if creating a new workspace during import
// IF is creating a new workspace
// AND imported resource has no preset scope property OR scope is null
// AND we have a function to get scope
if ((!resource.hasOwnProperty('scope') || resource.scope === null) && getWorkspaceScope) {
const workspaceName = resource.name;
let specName;
// If is from insomnia v4 and the spec has contents, add to the name when prompting
if (isInsomniaV4Import(resultsType)) {
const spec: ApiSpec | null = await models.apiSpec.getByParentId(resource._id);
if (spec && spec.contents.trim()) {
specName = spec.fileName;
}
}
const nameToPrompt = specName ? `${specName} / ${workspaceName}` : workspaceName;
resource.scope = await getWorkspaceScope(nameToPrompt);
}
// If the workspace doesn't have a name, update the default name based on it's scope
if (!resource.name) {
const name =
@ -277,7 +297,10 @@ export async function importRaw(
resource.name = name;
}
await createWorkspaceInProject(resource as Workspace, getProjectId);
if (getProjectId) {
// Set the workspace parent if creating a new workspace during import
resource.parentId = await getProjectId();
}
}
newDoc = await db.docCreate(model.type, resource);
@ -324,43 +347,6 @@ export async function importRaw(
return importRequest;
}
async function updateWorkspaceScope(
resource: Workspace,
resultType: ConvertResultType,
getWorkspaceScope?: SetWorkspaceScopePrompt,
) {
// Set the workspace scope if creating a new workspace during import
// IF is creating a new workspace
// AND imported resource has no preset scope property OR scope is null
// AND we have a function to get scope
if ((!resource.hasOwnProperty('scope') || resource.scope === null) && getWorkspaceScope) {
const workspaceName = resource.name;
let specName;
// If is from insomnia v4 and the spec has contents, add to the name when prompting
if (isInsomniaV4Import(resultType)) {
const spec: ApiSpec | null = await models.apiSpec.getByParentId(resource._id);
if (spec && spec.contents.trim()) {
specName = spec.fileName;
}
}
const nameToPrompt = specName ? `${specName} / ${workspaceName}` : workspaceName;
resource.scope = await getWorkspaceScope(nameToPrompt);
}
}
async function createWorkspaceInProject(
resource: Workspace,
getProjectId?: () => Promise<string>,
) {
if (getProjectId) {
// Set the workspace parent if creating a new workspace during import
resource.parentId = await getProjectId();
}
}
export const isApiSpecImport = ({ id }: Pick<ConvertResultType, 'id'>) => (
id === 'openapi3' || id === 'swagger2'
);
@ -368,3 +354,134 @@ export const isApiSpecImport = ({ id }: Pick<ConvertResultType, 'id'>) => (
export const isInsomniaV4Import = ({ id }: Pick<ConvertResultType, 'id'>) => (
id === 'insomnia-4'
);
export enum ForceToWorkspace {
new = 'new',
current = 'current',
existing = 'existing'
}
export type SelectExistingWorkspacePrompt = Promise<string | null>;
// Returning null instead of a string will create a new workspace
export type ImportToWorkspacePrompt = () => null | string | Promise<null | string>;
export function askToImportIntoWorkspace({ workspaceId, forceToWorkspace, activeProjectWorkspaces }: { workspaceId?: string; forceToWorkspace?: ForceToWorkspace; activeProjectWorkspaces?: Workspace[] }): ImportToWorkspacePrompt {
return function() {
switch (forceToWorkspace) {
case ForceToWorkspace.new: {
return null;
}
case ForceToWorkspace.current: {
if (!workspaceId) {
return null;
}
return workspaceId;
}
case ForceToWorkspace.existing: {
// Return null if there are no available workspaces to chose from.
if (activeProjectWorkspaces?.length) {
return new Promise(async resolve => {
showModal(AskModal, {
title: 'Import',
message: `Do you want to import into an existing ${strings.workspace.singular.toLowerCase()} or a new one?`,
yesText: 'Existing',
noText: 'New',
onDone: async (yes: boolean) => {
if (!yes) {
return resolve(null);
}
const options = activeProjectWorkspaces.map(workspace => ({ name: workspace.name, value: workspace._id }));
showSelectModal({
title: 'Import',
message: `Select a ${strings.workspace.singular.toLowerCase()} to import into`,
options,
value: options[0]?.value,
noEscape: true,
onDone: workspaceId => {
resolve(workspaceId);
},
});
},
});
});
}
}
default: {
if (!workspaceId) {
return null;
}
return new Promise(resolve => {
showModal(AskModal, {
title: 'Import',
message: 'Do you want to import into the current workspace or a new one?',
yesText: 'Current',
noText: 'New Workspace',
onDone: async (yes: boolean) => {
resolve(yes ? workspaceId : null);
},
});
});
}
}
};
}
export type SetWorkspaceScopePrompt = (name?: string) => WorkspaceScope | Promise<WorkspaceScope>;
export function askToSetWorkspaceScope(scope?: WorkspaceScope): SetWorkspaceScopePrompt {
return name => {
switch (scope) {
case WorkspaceScopeKeys.collection:
case WorkspaceScopeKeys.design:
return scope;
default:
return new Promise(resolve => {
const message = name
? `How would you like to import "${name}"?`
: 'Do you want to import as a Request Collection or a Design Document?';
showModal(AskModal, {
title: 'Import As',
message,
noText: 'Request Collection',
yesText: 'Design Document',
onDone: async (yes: boolean) => {
resolve(yes ? WorkspaceScopeKeys.design : WorkspaceScopeKeys.collection);
},
});
});
}
};
}
export type SetProjectIdPrompt = () => Promise<string>;
export function askToImportIntoProject({ projects, activeProject }: { projects?: Project[]; activeProject?: Project }): SetProjectIdPrompt {
return function() {
return new Promise(resolve => {
// If only one project exists, return that
if (projects?.length === 1) {
return resolve(projects[0]._id);
}
const options = projects?.map(project => ({ name: project.name, value: project._id })) || [];
const defaultValue = activeProject?._id || null;
showSelectModal({
title: 'Import',
message: `Select a ${strings.project.singular.toLowerCase()} to import into`,
options,
value: defaultValue,
noEscape: true,
onDone: selectedProjectId => {
// @ts-expect-error onDone can send null as an argument; why/how?
resolve(selectedProjectId);
},
});
});
};
}

View File

@ -96,7 +96,6 @@ export async function generateAuthorizationUrl() {
export async function exchangeCodeForGitLabToken(input: {
code: string;
state: string;
scope: string;
}) {
const { code, state } = input;

View File

@ -10,7 +10,6 @@ export interface CrumbProps {
export interface BreadcrumbProps {
crumbs: CrumbProps[];
className?: string;
isLoading?: boolean;
}
const StyledBreadcrumb = styled.ul`
@ -61,13 +60,10 @@ const Crumb: FC<CrumbProps> = ({ id, node, onClick }) => (
</li>
);
export const Breadcrumb: FC<BreadcrumbProps> = ({ crumbs, className, isLoading }) => (
export const Breadcrumb: FC<BreadcrumbProps> = ({ crumbs, className }) => (
<Fragment>
<StyledBreadcrumb className={className}>
{crumbs.map(Crumb)}
</StyledBreadcrumb>
{isLoading ? (
<i className="fa fa-refresh fa-spin space-left" />
) : null}
</Fragment>
);

View File

@ -11,7 +11,6 @@ import { isDesign, Workspace } from '../../../models/workspace';
import type { WorkspaceAction } from '../../../plugins';
import { ConfigGenerator, getConfigGenerators, getWorkspaceActions } from '../../../plugins';
import * as pluginContexts from '../../../plugins/context';
import { selectIsLoading } from '../../redux/modules/global';
import { selectActiveApiSpec, selectActiveEnvironment, selectActiveProject, selectActiveWorkspace, selectActiveWorkspaceName, selectSettings } from '../../redux/selectors';
import { type DropdownHandle, Dropdown } from '../base/dropdown/dropdown';
import { DropdownButton } from '../base/dropdown/dropdown-button';
@ -29,7 +28,6 @@ export const WorkspaceDropdown: FC = () => {
const activeWorkspaceName = useSelector(selectActiveWorkspaceName);
const activeApiSpec = useSelector(selectActiveApiSpec);
const activeProject = useSelector(selectActiveProject);
const isLoading = useSelector(selectIsLoading);
const settings = useSelector(selectSettings);
const { hotKeyRegistry } = settings;
const [actionPlugins, setActionPlugins] = useState<WorkspaceAction[]>([]);
@ -117,7 +115,6 @@ export const WorkspaceDropdown: FC = () => {
{activeWorkspaceName}
</div>
<i className="fa fa-caret-down space-left" />
{isLoading ? <i className="fa fa-refresh fa-spin space-left" /> : null}
</DropdownButton>
<DropdownItem onClick={handleShowWorkspaceSettings}>
<i className="fa fa-wrench" /> {getWorkspaceLabel(activeWorkspace).singular} Settings

View File

@ -1,12 +1,12 @@
import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { exportRequestsToFile } from '../../../common/export';
import * as models from '../../../models';
import { GrpcRequest, isGrpcRequest } from '../../../models/grpc-request';
import { isRequest, Request } from '../../../models/request';
import { isRequestGroup, RequestGroup } from '../../../models/request-group';
import { isWebSocketRequest, WebSocketRequest } from '../../../models/websocket-request';
import { exportRequestsToFile } from '../../redux/modules/global';
import { selectSidebarChildren } from '../../redux/sidebar-selectors';
import { type ModalHandle, Modal, ModalProps } from '../base/modal';
import { ModalBody } from '../base/modal-body';

View File

@ -1,23 +1,19 @@
import { AxiosResponse } from 'axios';
import type { GraphQLError } from 'graphql';
import React, { MouseEvent, useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useInterval, useLocalStorage } from 'react-use';
import styled from 'styled-components';
import { GitRepository } from '../../../../models/git-repository';
import { axiosRequest } from '../../../../network/axios-request';
import {
exchangeCodeForToken,
generateAuthorizationUrl,
GITHUB_GRAPHQL_API_URL,
signOut,
} from '../../../../sync/git/github-oauth-provider';
import {
COMMAND_GITHUB_OAUTH_AUTHENTICATE,
newCommand,
} from '../../../redux/modules/global';
import { Button } from '../../themed-button';
import { showAlert } from '..';
import { showAlert, showError } from '..';
interface Props {
uri?: string;
@ -329,7 +325,6 @@ const GitHubSignInForm = ({ token }: GitHubSignInFormProps) => {
const [error, setError] = useState('');
const [authUrl, setAuthUrl] = useState(() => generateAuthorizationUrl());
const [isAuthenticating, setIsAuthenticating] = useState(false);
const dispatch = useDispatch();
// When we get a new token we reset the authenticating flag and auth url. This happens because we can use the generated url for only one authorization flow.
useEffect(() => {
@ -375,12 +370,16 @@ const GitHubSignInForm = ({ token }: GitHubSignInFormProps) => {
return;
}
const command = newCommand(COMMAND_GITHUB_OAUTH_AUTHENTICATE, {
exchangeCodeForToken({
code,
state,
}).catch((error: Error) => {
showError({
error,
title: 'Error authorizing GitHub',
message: error.message,
});
});
command(dispatch);
}
}}
>

View File

@ -1,23 +1,19 @@
import axios from 'axios';
import React, { useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useInterval, useLocalStorage } from 'react-use';
import styled from 'styled-components';
import { GitRepository } from '../../../../models/git-repository';
import { axiosRequest } from '../../../../network/axios-request';
import {
exchangeCodeForGitLabToken,
generateAuthorizationUrl,
getGitLabOauthApiURL,
refreshToken,
signOut,
} from '../../../../sync/git/gitlab-oauth-provider';
import {
COMMAND_GITLAB_OAUTH_AUTHENTICATE,
newCommand,
} from '../../../redux/modules/global';
import { Button } from '../../themed-button';
import { showAlert } from '..';
import { showAlert, showError } from '..';
interface Props {
uri?: string;
@ -281,7 +277,6 @@ interface GitLabSignInFormProps {
const GitLabSignInForm = ({ token }: GitLabSignInFormProps) => {
const [authUrl, setAuthUrl] = useState('');
const [isAuthenticating, setIsAuthenticating] = useState(false);
const dispatch = useDispatch();
// When we get a new token we reset the authenticating flag and auth url. This happens because we can use the generated url for only one authorization flow.
useEffect(() => {
@ -323,12 +318,16 @@ const GitLabSignInForm = ({ token }: GitLabSignInFormProps) => {
const state = parsedURL.searchParams.get('state');
if (typeof code === 'string' && typeof state === 'string') {
const command = newCommand(COMMAND_GITLAB_OAUTH_AUTHENTICATE, {
exchangeCodeForGitLabToken({
code,
state,
}).catch((error: Error) => {
showError({
error,
title: 'Error authorizing GitLab',
message: error.message,
});
});
command(dispatch);
}
}
}}

View File

@ -1,9 +1,9 @@
import React, { FC, useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { ForceToWorkspace } from '../../../common/import';
import { createRequest } from '../../hooks/create-request';
import { ForceToWorkspace } from '../../redux/modules/helpers';
import { importFile } from '../../redux/modules/import';
import { importFile } from '../../import';
import { selectActiveWorkspace, selectSettings } from '../../redux/selectors';
import { Hotkey } from '../hotkey';
import { Pane, PaneBody, PaneHeader } from './pane';

View File

@ -62,6 +62,7 @@ interface Props {
request?: Request | null;
settings: Settings;
workspace: Workspace;
setLoading: (l: boolean) => void;
}
export const RequestPane: FC<Props> = ({
@ -69,6 +70,7 @@ export const RequestPane: FC<Props> = ({
request,
settings,
workspace,
setLoading,
}) => {
const updateRequestUrl = (request: Request, url: string) => {
@ -179,6 +181,7 @@ export const RequestPane: FC<Props> = ({
handleAutocompleteUrls={autocompleteUrls}
nunjucksPowerUserMode={settings.nunjucksPowerUserMode}
request={request}
setLoading={setLoading}
/>
</ErrorBoundary>
</PaneHeader>

View File

@ -12,7 +12,7 @@ import type { Response } from '../../../models/response';
import { cancelRequestById } from '../../../network/network';
import { jsonPrettify } from '../../../utils/prettify/json';
import { updateRequestMetaByParentId } from '../../hooks/create-request';
import { selectActiveResponse, selectLoadStartTime, selectResponseFilter, selectResponseFilterHistory, selectResponsePreviewMode, selectSettings } from '../../redux/selectors';
import { selectActiveResponse, selectResponseFilter, selectResponseFilterHistory, selectResponsePreviewMode, selectSettings } from '../../redux/selectors';
import { PanelContainer, TabItem, Tabs } from '../base/tabs';
import { PreviewModeDropdown } from '../dropdowns/preview-mode-dropdown';
import { ResponseHistoryDropdown } from '../dropdowns/response-history-dropdown';
@ -32,15 +32,16 @@ import { PlaceholderResponsePane } from './placeholder-response-pane';
interface Props {
request?: Request | null;
runningRequests: Record<string, number>;
}
export const ResponsePane: FC<Props> = ({
request,
runningRequests,
}) => {
const response = useSelector(selectActiveResponse) as Response | null;
const filterHistory = useSelector(selectResponseFilterHistory);
const filter = useSelector(selectResponseFilter);
const settings = useSelector(selectSettings);
const loadStartTime = useSelector(selectLoadStartTime);
const previewMode = useSelector(selectResponsePreviewMode);
const handleSetFilter = async (responseFilter: string) => {
if (!response) {
@ -123,12 +124,13 @@ export const ResponsePane: FC<Props> = ({
return <BlankPane type="response" />;
}
// If there is no previous response, show placeholder for loading indicator
if (!response) {
return (
<PlaceholderResponsePane>
<ResponseTimer
handleCancel={() => cancelRequestById(request._id)}
loadStartTime={loadStartTime}
loadStartTime={runningRequests[request._id]}
/>
</PlaceholderResponsePane>
);
@ -229,7 +231,7 @@ export const ResponsePane: FC<Props> = ({
<ErrorBoundary errorClassName="font-error pad text-center">
<ResponseTimer
handleCancel={() => cancelRequestById(request._id)}
loadStartTime={loadStartTime}
loadStartTime={runningRequests[request._id]}
/>
</ErrorBoundary>
</Pane>

View File

@ -4,7 +4,7 @@ import * as importers from 'insomnia-importers';
import { extension as mimeExtension } from 'mime-types';
import path from 'path';
import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useSelector } from 'react-redux';
import { useInterval } from 'react-use';
import { database } from '../../common/database';
@ -16,7 +16,6 @@ import * as network from '../../network/network';
import { SegmentEvent, trackSegmentEvent } from '../analytics';
import { updateRequestMetaByParentId } from '../hooks/create-request';
import { useTimeoutWhen } from '../hooks/useTimeoutWhen';
import { loadRequestStart, loadRequestStop } from '../redux/modules/global';
import { selectActiveEnvironment, selectActiveRequest, selectHotKeyRegistry, selectResponseDownloadPath, selectSettings } from '../redux/selectors';
import { type DropdownHandle, Dropdown } from './base/dropdown/dropdown';
import { DropdownButton } from './base/dropdown/dropdown-button';
@ -37,6 +36,7 @@ interface Props {
onUrlChange: (r: Request, url: string) => Promise<Request>;
request: Request;
uniquenessKey: string;
setLoading: (l: boolean) => void;
}
export interface RequestUrlBarHandle {
@ -48,13 +48,13 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
onUrlChange,
request,
uniquenessKey,
setLoading,
}, ref) => {
const downloadPath = useSelector(selectResponseDownloadPath);
const hotKeyRegistry = useSelector(selectHotKeyRegistry);
const activeEnvironment = useSelector(selectActiveEnvironment);
const activeRequest = useSelector(selectActiveRequest);
const settings = useSelector(selectSettings);
const dispatch = useDispatch();
const methodDropdownRef = useRef<DropdownHandle>(null);
const dropdownRef = useRef<DropdownHandle>(null);
const inputRef = useRef<OneLineEditorHandle>(null);
@ -106,9 +106,7 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
authenticationType: request.authentication?.type,
mimeType: request.body.mimeType,
});
// Start loading
dispatch(loadRequestStart(request._id));
setLoading(true);
try {
const responsePatch = await network.send(request._id, activeEnvironment?._id);
const headers = responsePatch.headers || [];
@ -175,10 +173,9 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
} finally {
// Unset active response because we just made a new one
await updateRequestMetaByParentId(request._id, { activeResponseId: null });
// Stop loading
dispatch(loadRequestStop(request._id));
setLoading(false);
}
}, [activeEnvironment, dispatch, request, settings.maxHistoryResponses, settings.preferredHttpVersion]);
}, [activeEnvironment?._id, request, setLoading, settings.maxHistoryResponses, settings.preferredHttpVersion]);
const handleSend = useCallback(async () => {
if (!request) {
@ -191,7 +188,7 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
authenticationType: request.authentication?.type,
mimeType: request.body.mimeType,
});
dispatch(loadRequestStart(request._id));
setLoading(true);
try {
const responsePatch = await network.send(request._id, activeEnvironment?._id);
await models.response.create(responsePatch, settings.maxHistoryResponses);
@ -217,9 +214,8 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
}
// Unset active response because we just made a new one
await updateRequestMetaByParentId(request._id, { activeResponseId: null });
// Stop loading
dispatch(loadRequestStop(request._id));
}, [activeEnvironment, dispatch, request, settings.maxHistoryResponses, settings.preferredHttpVersion]);
setLoading(false);
}, [activeEnvironment?._id, request, setLoading, settings.maxHistoryResponses, settings.preferredHttpVersion]);
const send = useCallback(() => {
setCurrentTimeout(undefined);

View File

@ -1,17 +1,17 @@
import { importers } from 'insomnia-importers';
import React, { FC, useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useSelector } from 'react-redux';
import { useRevalidator } from 'react-router-dom';
import { getProductName } from '../../../common/constants';
import { docsImportExport } from '../../../common/documentation';
import { exportAllToFile } from '../../../common/export';
import { getWorkspaceLabel } from '../../../common/get-workspace-label';
import { ForceToWorkspace } from '../../../common/import';
import { strings } from '../../../common/strings';
import { isRequestGroup } from '../../../models/request-group';
import { exportAllToFile } from '../../redux/modules/global';
import { ForceToWorkspace } from '../../redux/modules/helpers';
import { importClipBoard, importFile, importUri } from '../../redux/modules/import';
import { selectActiveProjectName, selectActiveWorkspace, selectActiveWorkspaceName, selectWorkspaceRequestsAndRequestGroups, selectWorkspacesForActiveProject } from '../../redux/selectors';
import { importClipBoard, importFile, importUri } from '../../import';
import { selectActiveProject, selectActiveProjectName, selectActiveWorkspace, selectActiveWorkspaceName, selectProjects, selectWorkspaceRequestsAndRequestGroups, selectWorkspacesForActiveProject, selectWorkspacesWithResolvedNameForActiveProject } from '../../redux/selectors';
import { Dropdown } from '../base/dropdown/dropdown';
import { DropdownButton } from '../base/dropdown/dropdown-button';
import { DropdownDivider } from '../base/dropdown/dropdown-divider';
@ -26,9 +26,11 @@ interface Props {
}
export const ImportExport: FC<Props> = ({ hideSettingsModal }) => {
const dispatch = useDispatch();
const projectName = useSelector(selectActiveProjectName) ?? getProductName();
const activeWorkspace = useSelector(selectActiveWorkspace);
const activeProjectWorkspaces = useSelector(selectWorkspacesWithResolvedNameForActiveProject);
const activeProject = useSelector(selectActiveProject);
const projects = useSelector(selectProjects);
const forceToWorkspace = activeWorkspace?._id ? ForceToWorkspace.current : ForceToWorkspace.existing;
const workspacesForActiveProject = useSelector(selectWorkspacesForActiveProject);
const workspaceRequestsAndRequestGroups = useSelector(selectWorkspaceRequestsAndRequestGroups);
@ -44,12 +46,17 @@ export const ImportExport: FC<Props> = ({ hideSettingsModal }) => {
placeholder: 'https://website.com/insomnia-import.json',
onComplete: (uri: string) => {
window.localStorage.setItem('insomnia.lastUsedImportUri', uri);
dispatch(importUri(uri, { workspaceId: activeWorkspace?._id, forceToWorkspace, onComplete: revalidate }));
importUri(uri, {
activeProjectWorkspaces,
activeProject,
projects,
workspaceId: activeWorkspace?._id,
forceToWorkspace, onComplete: revalidate });
hideSettingsModal();
},
...defaultValue,
});
}, [dispatch, activeWorkspace?._id, forceToWorkspace, revalidate, hideSettingsModal]);
}, [activeProjectWorkspaces, activeProject, projects, activeWorkspace?._id, forceToWorkspace, revalidate, hideSettingsModal]);
const showExportRequestsModal = useCallback(() => {
if (!workspaceRequestsAndRequestGroups.filter(r => !isRequestGroup(r)).length) {
@ -69,14 +76,24 @@ export const ImportExport: FC<Props> = ({ hideSettingsModal }) => {
}, [hideSettingsModal, projectName, workspacesForActiveProject]);
const handleImportFile = useCallback(() => {
dispatch(importFile({ workspaceId: activeWorkspace?._id, forceToWorkspace, onComplete: revalidate }));
importFile({
activeProjectWorkspaces,
activeProject,
projects,
workspaceId: activeWorkspace?._id,
forceToWorkspace, onComplete: revalidate });
hideSettingsModal();
}, [dispatch, activeWorkspace?._id, forceToWorkspace, revalidate, hideSettingsModal]);
}, [activeProjectWorkspaces, activeProject, projects, activeWorkspace?._id, forceToWorkspace, revalidate, hideSettingsModal]);
const handleImportClipBoard = useCallback(() => {
dispatch(importClipBoard({ workspaceId: activeWorkspace?._id, forceToWorkspace, onComplete: revalidate }));
importClipBoard({
activeProjectWorkspaces,
activeProject,
projects,
workspaceId: activeWorkspace?._id,
forceToWorkspace, onComplete: revalidate });
hideSettingsModal();
}, [dispatch, activeWorkspace?._id, forceToWorkspace, revalidate, hideSettingsModal]);
}, [activeProjectWorkspaces, activeProject, projects, activeWorkspace?._id, forceToWorkspace, revalidate, hideSettingsModal]);
const activeWorkspaceName = useSelector(selectActiveWorkspaceName);

View File

@ -1,24 +0,0 @@
import { IpcRendererEvent } from 'electron/renderer';
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { parse } from 'url';
import { newCommand } from '../redux/modules/global';
import { selectActiveWorkspace } from '../redux/selectors';
export const useAppCommands = () => {
const dispatch = useDispatch();
const activeWorkspace = useSelector(selectActiveWorkspace);
// Handle Application Commands
useEffect(() => {
return window.main.on('shell:open', (_: IpcRendererEvent, url: string) => {
console.log('[renderer] Received Deep Link URL', url);
const parsed = parse(url, true);
const command = `${parsed.hostname}${parsed.pathname}`;
const args = JSON.parse(JSON.stringify(parsed.query));
args.workspaceId = args.workspaceId || activeWorkspace?._id;
newCommand(command, args)(dispatch);
});
}, [activeWorkspace?._id, dispatch]);
};

View File

@ -0,0 +1,164 @@
import { IpcRendererEvent } from 'electron/renderer';
import { useEffect } from 'react';
import React from 'react';
import { useSelector } from 'react-redux';
import { askToImportIntoProject, askToImportIntoWorkspace, askToSetWorkspaceScope, importUri } from '../../common/import';
import * as models from '../../models';
import { reloadPlugins } from '../../plugins';
import { createPlugin } from '../../plugins/create';
import { setTheme } from '../../plugins/misc';
import { exchangeCodeForToken } from '../../sync/git/github-oauth-provider';
import { exchangeCodeForGitLabToken } from '../../sync/git/gitlab-oauth-provider';
import { submitAuthCode } from '../auth-session-provider';
import { showError, showModal } from '../components/modals';
import { AlertModal } from '../components/modals/alert-modal';
import { AskModal } from '../components/modals/ask-modal';
import { LoginModal } from '../components/modals/login-modal';
import {
SettingsModal,
TAB_INDEX_PLUGINS,
TAB_INDEX_THEMES,
} from '../components/modals/settings-modal';
import { selectActiveProject, selectActiveWorkspace, selectProjects, selectWorkspacesWithResolvedNameForActiveProject } from '../redux/selectors';
export const useAppCommands = () => {
const activeWorkspace = useSelector(selectActiveWorkspace);
const activeProject = useSelector(selectActiveProject);
const activeProjectWorkspaces = useSelector(selectWorkspacesWithResolvedNameForActiveProject);
const projects = useSelector(selectProjects);
useEffect(() => {
return window.main.on('shell:open', async (_: IpcRendererEvent, url: string) => {
const urlWithoutParams = url.substring(0, url.indexOf('?'));
const params = Object.fromEntries(new URL(url).searchParams);
switch (urlWithoutParams) {
case 'insomnia://app/alert':
showModal(AlertModal, {
title: params.title,
message: params.message,
});
break;
case 'insomnia://app/auth/login':
showModal(LoginModal, {
title: params.title,
message: params.message,
reauth: true,
});
break;
case 'insomnia://app/import':
showModal(AlertModal, {
title: 'Confirm Data Import',
message: (
<span>
Do you really want to import {params.name && (<><code>{params.name}</code> from</>)} <code>{params.uri}</code>?
</span>
),
addCancel: true,
onConfirm: async () => {
const activeWorkspaceId = activeWorkspace?._id;
importUri(params.uri, {
getWorkspaceScope: askToSetWorkspaceScope(),
getWorkspaceId: askToImportIntoWorkspace({ workspaceId: params.workspaceId || activeWorkspaceId, activeProjectWorkspaces }),
// Currently, just return the active project instead of prompting for which project to import into
getProjectId: askToImportIntoProject({ projects, activeProject }),
});
},
});
break;
case 'insomnia://plugins/install':
showModal(AskModal, {
title: 'Plugin Install',
message: (
<>
Do you want to install <code>{params.name}</code>?
</>
),
yesText: 'Install',
noText: 'Cancel',
onDone: async (isYes: boolean) => {
if (isYes) {
try {
await window.main.installPlugin(params.name);
showModal(SettingsModal, { tab: TAB_INDEX_PLUGINS });
} catch (err) {
showError({
title: 'Plugin Install',
message: 'Failed to install plugin',
error: err.message,
});
}
}
},
});
break;
case 'insomnia://plugins/theme':
const parsedTheme = JSON.parse(decodeURIComponent(params.theme));
showModal(AskModal, {
title: 'Install Theme',
message: (
<>
Do you want to install <code>{parsedTheme.displayName}</code>?
</>
),
yesText: 'Install',
noText: 'Cancel',
onDone: async (isYes: boolean) => {
if (isYes) {
const mainJsContent = `module.exports.themes = [${JSON.stringify(
parsedTheme,
null,
2,
)}];`;
await createPlugin(`theme-${parsedTheme.name}`, '0.0.1', mainJsContent);
const settings = await models.settings.getOrCreate();
await models.settings.update(settings, {
theme: parsedTheme.name,
});
await reloadPlugins();
await setTheme(parsedTheme.name);
showModal(SettingsModal, { tab: TAB_INDEX_THEMES });
}
},
});
break;
case 'insomnia://oauth/github/authenticate': {
const { code, state } = params;
await exchangeCodeForToken({ code, state }).catch((error: Error) => {
showError({
error,
title: 'Error authorizing GitHub',
message: error.message,
});
});
break;
}
case 'insomnia://oauth/gitlab/authenticate': {
const { code, state } = params;
await exchangeCodeForGitLabToken({ code, state }).catch((error: Error) => {
showError({
error,
title: 'Error authorizing GitLab',
message: error.message,
});
});
break;
}
case 'insomnia://app/auth/finish': {
submitAuthCode(params.box);
break;
}
default: {
console.log(`Unknown deep link: ${url}`);
}
}
});
}, [activeProject, activeProjectWorkspaces, activeWorkspace?._id, projects]);
};

View File

@ -1,15 +1,16 @@
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useSelector } from 'react-redux';
import { showModal } from '../components/modals';
import { AlertModal } from '../components/modals/alert-modal';
import { importUri } from '../redux/modules/import';
import { selectActiveWorkspace } from '../redux/selectors';
import { importUri } from '../import';
import { selectActiveProject, selectActiveWorkspace, selectProjects, selectWorkspacesWithResolvedNameForActiveProject } from '../redux/selectors';
export const useDragAndDropImportFile = () => {
const activeWorkspace = useSelector(selectActiveWorkspace);
const dispatch = useDispatch();
const handleImportUri = dispatch(importUri);
const activeProjectWorkspaces = useSelector(selectWorkspacesWithResolvedNameForActiveProject);
const activeProject = useSelector(selectActiveProject);
const projects = useSelector(selectProjects);
// Global Drag and Drop for importing files
useEffect(() => {
@ -46,7 +47,12 @@ export const useDragAndDropImportFile = () => {
),
addCancel: true,
});
handleImportUri(`file://${file.path}`, { workspaceId: activeWorkspace?._id });
importUri(`file://${file.path}`, {
activeProjectWorkspaces,
activeProject,
projects,
workspaceId: activeWorkspace?._id,
});
},
false,
);

View File

@ -0,0 +1,192 @@
import electron, { OpenDialogOptions } from 'electron';
import {
askToImportIntoProject,
askToImportIntoWorkspace,
askToSetWorkspaceScope,
ForceToWorkspace,
importRaw,
importUri as _importUri,
} from '../common/import';
import * as models from '../models';
import { DEFAULT_PROJECT_ID, Project } from '../models/project';
import { Workspace, WorkspaceScope } from '../models/workspace';
import { showError, showModal } from './components/modals';
import { AlertModal } from './components/modals/alert-modal';
export interface ImportOptions {
workspaceId?: string;
forceToProject?: 'active' | 'prompt';
forceToWorkspace?: ForceToWorkspace;
forceToScope?: WorkspaceScope;
onComplete?: () => void;
activeProject?: Project;
activeProjectWorkspaces?: Workspace[];
projects?: Project[];
}
export const importFile = async (
{
forceToScope,
forceToWorkspace,
workspaceId,
forceToProject,
activeProject,
activeProjectWorkspaces,
projects,
onComplete,
}: ImportOptions = {},
) => {
const openDialogOptions: OpenDialogOptions = {
title: 'Import Insomnia Data',
buttonLabel: 'Import',
properties: ['openFile'],
filters: [
// @ts-expect-error https://github.com/electron/electron/pull/29322
{
extensions: [
'',
'sh',
'txt',
'json',
'har',
'curl',
'bash',
'shell',
'yaml',
'yml',
'wsdl',
],
},
],
};
const { canceled, filePaths } = await window.dialog.showOpenDialog(openDialogOptions);
if (canceled) {
// It was cancelled, so let's bail out
return;
}
// Let's import all the files!
for (const filePath of filePaths) {
try {
const uri = `file://${filePath}`;
const config = {
getWorkspaceScope: askToSetWorkspaceScope(forceToScope),
getWorkspaceId: askToImportIntoWorkspace({ workspaceId, forceToWorkspace, activeProjectWorkspaces }),
// Currently, just return the active project instead of prompting for which project to import into
getProjectId: forceToProject === 'prompt' ? askToImportIntoProject({ projects, activeProject }) : () => Promise.resolve(activeProject?._id || DEFAULT_PROJECT_ID),
};
const { error, summary } = await _importUri(uri, config);
if (!error) {
models.stats.incrementRequestStats({ createdRequests: summary[models.request.type].length + summary[models.grpcRequest.type].length });
}
if (error) {
showError({
title: 'Import Failed',
message: 'The file does not contain a valid specification.',
error,
});
}
} catch (err) {
showModal(AlertModal, {
title: 'Import Failed',
message: err + '',
});
}
}
onComplete?.();
};
export const importClipBoard = async ({
forceToScope,
forceToWorkspace,
workspaceId,
forceToProject,
activeProject,
activeProjectWorkspaces,
projects,
onComplete,
}: ImportOptions = {},
) => {
const schema = electron.clipboard.readText();
if (!schema) {
showModal(AlertModal, {
title: 'Import Failed',
message: 'Your clipboard appears to be empty.',
});
return;
}
// Let's import all the paths!
try {
const config = {
getWorkspaceScope: askToSetWorkspaceScope(forceToScope),
getWorkspaceId: askToImportIntoWorkspace({ workspaceId, forceToWorkspace, activeProjectWorkspaces }),
// Currently, just return the active project instead of prompting for which project to import into
getProjectId: forceToProject === 'prompt' ? askToImportIntoProject({ projects, activeProject }) : () => Promise.resolve(activeProject?._id || DEFAULT_PROJECT_ID),
};
const { error, summary } = await importRaw(schema, config);
if (!error) {
models.stats.incrementRequestStats({ createdRequests: summary[models.request.type].length + summary[models.grpcRequest.type].length });
}
if (error) {
showError({
title: 'Import Failed',
message: 'Your clipboard does not contain a valid specification.',
error,
});
}
} catch (err) {
showModal(AlertModal, {
title: 'Import Failed',
message: 'Your clipboard does not contain a valid specification.',
});
}
onComplete?.();
};
export const importUri = async (
uri: string,
{
forceToScope,
forceToWorkspace,
workspaceId,
forceToProject,
activeProject,
activeProjectWorkspaces,
projects,
onComplete,
}: ImportOptions = {},
) => {
try {
const config = {
getWorkspaceScope: askToSetWorkspaceScope(forceToScope),
getWorkspaceId: askToImportIntoWorkspace({ workspaceId, forceToWorkspace, activeProjectWorkspaces }),
// Currently, just return the active project instead of prompting for which project to import into
getProjectId: forceToProject === 'prompt' ? askToImportIntoProject({ projects, activeProject }) : () => Promise.resolve(activeProject?._id || DEFAULT_PROJECT_ID),
};
const { error, summary } = await _importUri(uri, config);
if (!error) {
models.stats.incrementRequestStats({ createdRequests: summary[models.request.type].length + summary[models.grpcRequest.type].length });
}
if (error) {
showError({
title: 'Import Failed',
message: 'The URI does not contain a valid specification.',
error,
});
}
} catch (err) {
showModal(AlertModal, {
title: 'Import Failed',
message: err + '',
});
}
onComplete?.();
};

View File

@ -1,5 +1,5 @@
import { createBuilder } from '@develohpanda/fluent-builder';
import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals';
import { beforeEach, describe, expect, it, jest } from '@jest/globals';
import { PromiseFsClient } from 'isomorphic-git';
import { mocked } from 'jest-mock';
import path from 'path';
@ -25,7 +25,7 @@ import {
} from '../../../../test-utils';
import { SegmentEvent, trackSegmentEvent } from '../../../analytics';
import { cloneGitRepository, setupGitRepository } from '../git';
import { LOAD_START, LOAD_STOP, SET_ACTIVE_ACTIVITY, SET_ACTIVE_PROJECT, SET_ACTIVE_WORKSPACE } from '../global';
import { SET_ACTIVE_ACTIVITY, SET_ACTIVE_PROJECT, SET_ACTIVE_WORKSPACE } from '../global';
jest.mock('../../../components/modals');
jest.mock('../../../../sync/git/shallow-clone');
@ -46,18 +46,6 @@ describe('git', () => {
gitRepoBuilder.reset();
});
// Check loading events
afterEach(() => {
const actions = store.getActions();
// Should always contain one LOAD_START and one LOAD_END
expect(actions.filter(({ type }) => type === LOAD_START)).toHaveLength(1);
expect(actions.filter(({ type }) => type === LOAD_STOP)).toHaveLength(1);
// LOAD_START should never be before LOAD_STOP
const startActionIndex = actions.findIndex(({ type }) => type === LOAD_START);
const stopActionIndex = actions.findIndex(({ type }) => type === LOAD_STOP);
expect(stopActionIndex).toBeGreaterThan(startActionIndex);
});
describe('cloneGitRepository', () => {
const dispatchCloneAndSubmitSettings = async (memClient: PromiseFsClient, uri?: string) => {
const createFsClientMock = jest.fn().mockReturnValue(memClient);
@ -113,12 +101,6 @@ describe('git', () => {
expect(trackSegmentEvent).toHaveBeenCalledWith(SegmentEvent.documentCreate);
// Ensure activity is activated
expect(store.getActions()).toEqual([
{
type: LOAD_START,
},
{
type: LOAD_STOP,
},
{
type: SET_ACTIVE_PROJECT,
projectId: DEFAULT_PROJECT_ID,
@ -164,15 +146,6 @@ describe('git', () => {
const alertArgs = getAndClearShowAlertMockArgs();
expect(alertArgs.title).toBe('Clone Problem');
expect(alertArgs.message).toBe('Multiple workspaces found in repository; expected one.');
// Ensure activity is activated
expect(store.getActions()).toEqual([
{
type: LOAD_START,
},
{
type: LOAD_STOP,
},
]);
});
it('should fail if workspace already exists', async () => {
@ -194,15 +167,6 @@ describe('git', () => {
Workspace <strong>New Collection</strong> already exists. Please delete it before cloning.
</Fragment>,
);
// Ensure activity is activated
expect(store.getActions()).toEqual([
{
type: LOAD_START,
},
{
type: LOAD_STOP,
},
]);
});
it('should fail if exception during clone and not try again if uri ends with .git', async () => {
@ -213,14 +177,6 @@ describe('git', () => {
const alertArgs = getAndClearShowAlertMockArgs();
expect(alertArgs.title).toBe('Error Cloning Repository');
expect(alertArgs.message).toBe(err.message);
expect(store.getActions()).toEqual([
{
type: LOAD_START,
},
{
type: LOAD_STOP,
},
]);
expect(shallowClone).toHaveBeenCalledTimes(1);
});
@ -240,14 +196,6 @@ describe('git', () => {
expect(alertArgs.message).toBe(
`Failed to clone with original url (${uri}): ${firstError.message};\n\nAlso failed to clone with \`.git\` suffix added (${dotGitUri}): ${secondError.message}`,
);
expect(store.getActions()).toEqual([
{
type: LOAD_START,
},
{
type: LOAD_STOP,
},
]);
expect(shallowClone).toHaveBeenCalledTimes(2);
});
@ -355,15 +303,6 @@ describe('git', () => {
expect(meta?.gitRepositoryId).toBe(repoSettings._id);
const createdRepo = await models.gitRepository.getById(repoSettings._id);
expect(createdRepo?.needsFullClone).toBe(true);
// Ensure workspace is enabled
expect(store.getActions()).toEqual([
{
type: LOAD_START,
},
{
type: LOAD_STOP,
},
]);
});
});
@ -398,15 +337,6 @@ describe('git', () => {
expect(errorArgs.title).toBe('Error Cloning Repository');
expect(errorArgs.message).toBe(err.message);
expect(errorArgs.error).toBe(err);
// Ensure activity is activated
expect(store.getActions()).toEqual([
{
type: LOAD_START,
},
{
type: LOAD_STOP,
},
]);
});
it('should fail if workspace is found', async () => {
@ -426,15 +356,6 @@ describe('git', () => {
expect(alertArgs.message).toBe(
'This repository is already connected to Insomnia; try creating a clone from the dashboard instead.',
);
// Ensure activity is activated
expect(store.getActions()).toEqual([
{
type: LOAD_START,
},
{
type: LOAD_STOP,
},
]);
});
it('should setup if no .insomnia directory is found', async () => {
@ -447,15 +368,6 @@ describe('git', () => {
expect(wMeta?.gitRepositoryId).toBe(repo._id);
const createdRepo = await models.gitRepository.getById(repo._id);
expect(createdRepo?.needsFullClone).toBe(true);
// Ensure activity is activated
expect(store.getActions()).toEqual([
{
type: LOAD_START,
},
{
type: LOAD_STOP,
},
]);
});
it('should setup if empty .insomnia directory is found', async () => {
@ -469,15 +381,6 @@ describe('git', () => {
expect(wMeta?.gitRepositoryId).toBe(repo._id);
const createdRepo = await models.gitRepository.getById(repo._id);
expect(createdRepo?.needsFullClone).toBe(true);
// Ensure activity is activated
expect(store.getActions()).toEqual([
{
type: LOAD_START,
},
{
type: LOAD_STOP,
},
]);
});
it('should setup if empty .insomnia/workspace directory is found', async () => {
@ -492,15 +395,6 @@ describe('git', () => {
expect(wMeta?.gitRepositoryId).toBe(repo._id);
const createdRepo = await models.gitRepository.getById(repo._id);
expect(createdRepo?.needsFullClone).toBe(true);
// Ensure activity is activated
expect(store.getActions()).toEqual([
{
type: LOAD_START,
},
{
type: LOAD_STOP,
},
]);
});
});
});

View File

@ -15,8 +15,9 @@ import { Workspace, WorkspaceScope, WorkspaceScopeKeys } from '../../../../model
import { WorkspaceMeta } from '../../../../models/workspace-meta';
import { getAndClearShowPromptMockArgs } from '../../../../test-utils';
import { SegmentEvent, trackSegmentEvent } from '../../../analytics';
import { createWorkspace } from '../git';
import { SET_ACTIVE_ACTIVITY, SET_ACTIVE_PROJECT, SET_ACTIVE_WORKSPACE } from '../global';
import { activateWorkspace, createWorkspace } from '../workspace';
import { activateWorkspace } from '../workspace';
jest.mock('../../../components/modals');
jest.mock('../../../../ui/analytics');

View File

@ -9,18 +9,17 @@ import * as models from '../../../models';
import { BaseModel } from '../../../models';
import type { GitRepository } from '../../../models/git-repository';
import { createGitRepository } from '../../../models/helpers/git-repository-operations';
import { isWorkspace, Workspace, WorkspaceScopeKeys } from '../../../models/workspace';
import { isDesign, isWorkspace, Workspace, WorkspaceScope, WorkspaceScopeKeys } from '../../../models/workspace';
import { forceWorkspaceScopeToDesign } from '../../../sync/git/force-workspace-scope-to-design';
import { GIT_CLONE_DIR, GIT_INSOMNIA_DIR, GIT_INSOMNIA_DIR_NAME } from '../../../sync/git/git-vcs';
import { shallowClone } from '../../../sync/git/shallow-clone';
import { addDotGit, getOauth2FormatName, translateSSHtoHTTP } from '../../../sync/git/utils';
import { SegmentEvent, trackSegmentEvent, vcsSegmentEventProperties } from '../../analytics';
import { showAlert, showError, showModal } from '../../components/modals';
import { showAlert, showError, showModal, showPrompt } from '../../components/modals';
import { GitRepositorySettingsModal } from '../../components/modals/git-repository-settings-modal';
import { selectActiveProject } from '../selectors';
import { RootState } from '.';
import { loadStart, loadStop } from './global';
import { createWorkspace } from './workspace';
import { activateWorkspace } from './workspace';
export type UpdateGitRepositoryCallback = (arg0: { gitRepository: GitRepository }) => void;
@ -49,15 +48,13 @@ export type SetupGitRepositoryCallback = (arg0: {
* Setup a git repository against a document
* */
export const setupGitRepository: SetupGitRepositoryCallback = ({ createFsClient, workspace }) => {
return (dispatch: any) => {
return () => {
trackSegmentEvent(SegmentEvent.vcsSyncStart, vcsSegmentEventProperties('git', 'setup'));
showModal(GitRepositorySettingsModal, {
gitRepository: null,
onSubmitEdits: async (gitRepoPatch: Partial<GitRepository>) => {
const providerName = getOauth2FormatName(gitRepoPatch.credentials);
dispatch(loadStart());
try {
gitRepoPatch.needsFullClone = true;
const fsClient = createFsClient();
@ -97,8 +94,6 @@ export const setupGitRepository: SetupGitRepositoryCallback = ({ createFsClient,
trackSegmentEvent(SegmentEvent.vcsSyncComplete, { ...vcsSegmentEventProperties('git', 'setup'), providerName });
} catch (err) {
trackSegmentEvent(SegmentEvent.vcsSyncComplete, { ...vcsSegmentEventProperties('git', 'setup', err.message), providerName });
} finally {
dispatch(loadStop());
}
},
});
@ -131,6 +126,46 @@ const createWorkspaceWithGitRepo = (gitRepo: GitRepository) => {
);
};
type OnWorkspaceCreateCallback = (arg0: Workspace) => Promise<void> | void;
export const createWorkspace = ({ scope, onCreate }: {
scope: WorkspaceScope;
onCreate?: OnWorkspaceCreateCallback;
}) => {
return (dispatch: any, getState: any) => {
const activeProject = selectActiveProject(getState());
const design = isDesign({
scope,
});
const title = design ? 'Design Document' : 'Request Collection';
const defaultValue = design ? 'my-spec.yaml' : 'My Collection';
const segmentEvent = design ? SegmentEvent.documentCreate : SegmentEvent.collectionCreate;
showPrompt({
title: `Create New ${title}`,
submitName: 'Create',
placeholder: defaultValue,
defaultValue,
selectText: true,
onComplete: async name => {
const flushId = await db.bufferChanges();
const workspace = await models.workspace.create({
name,
scope,
parentId: activeProject._id,
});
await models.workspace.ensureChildren(workspace);
await db.flushChanges(flushId);
if (onCreate) {
await onCreate(workspace);
}
await dispatch(activateWorkspace({ workspace }));
trackSegmentEvent(segmentEvent);
},
});
};
};
const cloneProblem = (message: ReactNode) => {
showAlert({
title: 'Clone Problem',
@ -165,7 +200,6 @@ export const cloneGitRepository = ({ createFsClient, onComplete }: {
showModal(GitRepositorySettingsModal, {
gitRepository: null,
onSubmitEdits: async (repoSettingsPatch: any) => {
dispatch(loadStart());
repoSettingsPatch.needsFullClone = true;
repoSettingsPatch.uri = translateSSHtoHTTP(repoSettingsPatch.uri);
let fsClient = createFsClient();
@ -182,7 +216,6 @@ export const cloneGitRepository = ({ createFsClient, onComplete }: {
title: 'Error Cloning Repository',
message: originalUriError.message,
});
dispatch(loadStop());
trackSegmentEvent(SegmentEvent.vcsSyncComplete, { ...vcsSegmentEventProperties('git', 'clone', originalUriError.message), providerName });
return;
}
@ -202,7 +235,6 @@ export const cloneGitRepository = ({ createFsClient, onComplete }: {
title: 'Error Cloning Repository: failed to clone with and without `.git` suffix',
message: `Failed to clone with original url (${repoSettingsPatch.uri}): ${originalUriError.message};\n\nAlso failed to clone with \`.git\` suffix added (${dotGitUri}): ${dotGitError.message}`,
});
dispatch(loadStop());
trackSegmentEvent(SegmentEvent.vcsSyncComplete, { ...vcsSegmentEventProperties('git', 'clone', dotGitError.message), providerName });
return;
}
@ -211,7 +243,6 @@ export const cloneGitRepository = ({ createFsClient, onComplete }: {
// If no workspace exists, user should be prompted to create a document
if (!(await containsInsomniaWorkspaceDir(fsClient))) {
dispatch(noDocumentFound(repoSettingsPatch));
dispatch(loadStop());
trackSegmentEvent(SegmentEvent.vcsSyncComplete, { ...vcsSegmentEventProperties('git', 'clone', 'no directory found'), providerName });
return;
}
@ -221,14 +252,12 @@ export const cloneGitRepository = ({ createFsClient, onComplete }: {
if (workspaces.length === 0) {
dispatch(noDocumentFound(repoSettingsPatch));
dispatch(loadStop());
trackSegmentEvent(SegmentEvent.vcsSyncComplete, { ...vcsSegmentEventProperties('git', 'clone', 'no workspaces found'), providerName });
return;
}
if (workspaces.length > 1) {
cloneProblem('Multiple workspaces found in repository; expected one.');
dispatch(loadStop());
trackSegmentEvent(SegmentEvent.vcsSyncComplete, { ...vcsSegmentEventProperties('git', 'clone', 'multiple workspaces found'), providerName });
return;
}
@ -247,7 +276,6 @@ export const cloneGitRepository = ({ createFsClient, onComplete }: {
before cloning.
</>,
);
dispatch(loadStop());
trackSegmentEvent(SegmentEvent.vcsSyncComplete, { ...vcsSegmentEventProperties('git', 'clone', 'workspace already exists'), providerName });
return;
}
@ -290,7 +318,6 @@ export const cloneGitRepository = ({ createFsClient, onComplete }: {
// Flush DB changes
await db.flushChanges(bufferId);
dispatch(loadStop());
trackSegmentEvent(SegmentEvent.vcsSyncComplete, { ...vcsSegmentEventProperties('git', 'clone'), providerName });
onComplete?.();
},

View File

@ -1,70 +1,19 @@
import { format } from 'date-fns';
import fs, { NoParamCallback } from 'fs';
import path from 'path';
import React, { Fragment } from 'react';
import { combineReducers, Dispatch } from 'redux';
import { unreachableCase } from 'ts-assert-unreachable';
import { combineReducers } from 'redux';
import type { DashboardSortOrder, GlobalActivity } from '../../../common/constants';
import {
ACTIVITY_HOME,
isValidActivity,
} from '../../../common/constants';
import { database } from '../../../common/database';
import {
exportRequestsData,
exportRequestsHAR,
exportWorkspacesData,
exportWorkspacesHAR,
} from '../../../common/export';
import { strings } from '../../../common/strings';
import * as models from '../../../models';
import { Environment, isEnvironment } from '../../../models/environment';
import { GrpcRequest } from '../../../models/grpc-request';
import * as requestOperations from '../../../models/helpers/request-operations';
import { DEFAULT_PROJECT_ID } from '../../../models/project';
import { Request } from '../../../models/request';
import { WebSocketRequest } from '../../../models/websocket-request';
import { isWorkspace, Workspace } from '../../../models/workspace';
import { reloadPlugins } from '../../../plugins';
import { createPlugin } from '../../../plugins/create';
import { setTheme } from '../../../plugins/misc';
import { exchangeCodeForToken } from '../../../sync/git/github-oauth-provider';
import { exchangeCodeForGitLabToken } from '../../../sync/git/gitlab-oauth-provider';
import { AskModal } from '../../../ui/components/modals/ask-modal';
import { trackPageView } from '../../analytics';
import { submitAuthCode } from '../../auth-session-provider';
import { AlertModal } from '../../components/modals/alert-modal';
import { showAlert, showError, showModal } from '../../components/modals/index';
import { LoginModal } from '../../components/modals/login-modal';
import { SelectModal } from '../../components/modals/select-modal';
import {
SettingsModal,
TAB_INDEX_PLUGINS,
TAB_INDEX_THEMES,
} from '../../components/modals/settings-modal';
import { RootState } from '.';
import { importUri } from './import';
export const LOCALSTORAGE_PREFIX = 'insomnia::meta';
const LOGIN_STATE_CHANGE = 'global/login-state-change';
export const LOAD_START = 'global/load-start';
export const LOAD_STOP = 'global/load-stop';
const LOAD_REQUEST_START = 'global/load-request-start';
const LOAD_REQUEST_STOP = 'global/load-request-stop';
export const SET_ACTIVE_PROJECT = 'global/activate-project';
export const SET_DASHBOARD_SORT_ORDER = 'global/dashboard-sort-order';
export const SET_ACTIVE_WORKSPACE = 'global/activate-workspace';
export const SET_ACTIVE_ACTIVITY = 'global/activate-activity';
export const SET_IS_FINISHED_BOOTING = 'global/is-finished-booting';
const COMMAND_ALERT = 'app/alert';
const COMMAND_LOGIN = 'app/auth/login';
const COMMAND_IMPORT_URI = 'app/import';
const COMMAND_PLUGIN_INSTALL = 'plugins/install';
const COMMAND_PLUGIN_THEME = 'plugins/theme';
export const COMMAND_GITHUB_OAUTH_AUTHENTICATE = 'oauth/github/authenticate';
export const COMMAND_GITLAB_OAUTH_AUTHENTICATE = 'oauth/gitlab/authenticate';
export const COMMAND_FINISH_AUTHENTICATION = 'app/auth/finish';
// ~~~~~~~~ //
// REDUCERS //
@ -109,36 +58,6 @@ function activeWorkspaceReducer(state: string | null = null, action: any) {
}
}
function loadingReducer(state = false, action: any) {
switch (action.type) {
case LOAD_START:
return true;
case LOAD_STOP:
return false;
default:
return state;
}
}
function loadingRequestsReducer(state: Record<string, number> = {}, action: any) {
switch (action.type) {
case LOAD_REQUEST_START:
return Object.assign({}, state, {
[action.requestId]: action.time,
});
case LOAD_REQUEST_STOP:
return Object.assign({}, state, {
[action.requestId]: -1,
});
default:
return state;
}
}
function loginStateChangeReducer(state = false, action: any) {
switch (action.type) {
case LOGIN_STATE_CHANGE:
@ -150,191 +69,36 @@ function loginStateChangeReducer(state = false, action: any) {
}
export interface GlobalState {
isLoading: boolean;
activeProjectId: string;
dashboardSortOrder: DashboardSortOrder;
activeWorkspaceId: string | null;
activeActivity: GlobalActivity | null;
isLoggedIn: boolean;
loadingRequestIds: Record<string, number>;
}
export const reducer = combineReducers<GlobalState>({
isLoading: loadingReducer,
dashboardSortOrder: dashboardSortOrderReducer,
loadingRequestIds: loadingRequestsReducer,
activeProjectId: activeProjectReducer,
activeWorkspaceId: activeWorkspaceReducer,
activeActivity: activeActivityReducer,
isLoggedIn: loginStateChangeReducer,
});
export const selectIsLoading = (state: RootState) => state.global.isLoading;
// ~~~~~~~ //
// ACTIONS //
// ~~~~~~~ //
export const newCommand = (command: string, args: any) => async (dispatch: Dispatch<any>) => {
switch (command) {
case COMMAND_ALERT:
showModal(AlertModal, {
title: args.title,
message: args.message,
});
break;
case COMMAND_LOGIN:
showModal(LoginModal, {
title: args.title,
message: args.message,
reauth: true,
});
break;
case COMMAND_IMPORT_URI:
await showModal(AlertModal, {
title: 'Confirm Data Import',
message: (
<span>
Do you really want to import {args.name && (<><code>{args.name}</code> from</>)} <code>{args.uri}</code>?
</span>
),
addCancel: true,
});
dispatch(importUri(args.uri, { workspaceId: args.workspaceId, forceToProject: 'prompt' }));
break;
case COMMAND_PLUGIN_INSTALL:
showModal(AskModal, {
title: 'Plugin Install',
message: (
<Fragment>
Do you want to install <code>{args.name}</code>?
</Fragment>
),
yesText: 'Install',
noText: 'Cancel',
onDone: async (isYes: boolean) => {
if (!isYes) {
return;
}
try {
await window.main.installPlugin(args.name);
showModal(SettingsModal, { tab: TAB_INDEX_PLUGINS });
} catch (err) {
showError({
title: 'Plugin Install',
message: 'Failed to install plugin',
error: err.message,
});
}
},
});
break;
case COMMAND_PLUGIN_THEME:
const parsedTheme = JSON.parse(decodeURIComponent(args.theme));
showModal(AskModal, {
title: 'Install Theme',
message: (
<Fragment>
Do you want to install <code>{parsedTheme.displayName}</code>?
</Fragment>
),
yesText: 'Install',
noText: 'Cancel',
onDone: async (isYes: boolean) => {
if (!isYes) {
return;
}
const mainJsContent = `module.exports.themes = [${JSON.stringify(
parsedTheme,
null,
2,
)}];`;
await createPlugin(`theme-${parsedTheme.name}`, '0.0.1', mainJsContent);
const settings = await models.settings.getOrCreate();
await models.settings.update(settings, {
theme: parsedTheme.name,
});
await reloadPlugins();
await setTheme(parsedTheme.name);
showModal(SettingsModal, { tab: TAB_INDEX_THEMES });
},
});
break;
case COMMAND_GITHUB_OAUTH_AUTHENTICATE: {
await exchangeCodeForToken(args).catch((error: Error) => {
showError({
error,
title: 'Error authorizing GitHub',
message: error.message,
});
});
break;
}
case COMMAND_GITLAB_OAUTH_AUTHENTICATE: {
await exchangeCodeForGitLabToken(args).catch((error: Error) => {
showError({
error,
title: 'Error authorizing GitLab',
message: error.message,
});
});
break;
}
case COMMAND_FINISH_AUTHENTICATION: {
submitAuthCode(args.box);
break;
}
case null:
break;
default: {
console.log(`Unknown command: ${command}`);
}
}
};
export const loadStart = () => ({
type: LOAD_START,
});
export const loadStop = () => ({
type: LOAD_STOP,
});
export const loadRequestStart = (requestId: string) => ({
type: LOAD_REQUEST_START,
requestId,
time: Date.now(),
});
export const loginStateChange = (loggedIn: boolean) => ({
type: LOGIN_STATE_CHANGE,
loggedIn,
});
export const loadRequestStop = (requestId: string) => ({
type: LOAD_REQUEST_STOP,
requestId,
});
/*
Go to an explicit activity
*/
export const setActiveActivity = (activity: GlobalActivity) => {
activity = _normalizeActivity(activity);
if (window.localStorage.getItem(`${LOCALSTORAGE_PREFIX}::activity`) !== activity) {
window.localStorage.setItem(`${LOCALSTORAGE_PREFIX}::activity`, JSON.stringify(activity));
trackPageView(activity);
}
activity = isValidActivity(activity) ? activity : ACTIVITY_HOME;
window.localStorage.setItem(`${LOCALSTORAGE_PREFIX}::activity`, JSON.stringify(activity));
trackPageView(activity);
return {
type: SET_ACTIVE_ACTIVITY,
activity,
@ -369,254 +133,3 @@ export const setActiveWorkspace = (workspaceId: string | null) => {
workspaceId,
};
};
const VALUE_JSON = 'json';
const VALUE_YAML = 'yaml';
const VALUE_HAR = 'har';
export type SelectedFormat =
| typeof VALUE_HAR
| typeof VALUE_JSON
| typeof VALUE_YAML
;
const showSelectExportTypeModal = ({ onDone }: {
onDone: (selectedFormat: SelectedFormat) => Promise<void>;
}) => {
const options = [
{
name: 'Insomnia v4 (JSON)',
value: VALUE_JSON,
},
{
name: 'Insomnia v4 (YAML)',
value: VALUE_YAML,
},
{
name: 'HAR HTTP Archive Format',
value: VALUE_HAR,
},
];
const lastFormat = window.localStorage.getItem('insomnia.lastExportFormat');
const defaultValue = options.find(({ value }) => value === lastFormat) ? lastFormat : VALUE_JSON;
showModal(SelectModal, {
title: 'Select Export Type',
value: defaultValue,
options,
message: 'Which format would you like to export as?',
onDone: async selectedFormat => {
if (selectedFormat) {
window.localStorage.setItem('insomnia.lastExportFormat', selectedFormat);
await onDone(selectedFormat as SelectedFormat);
}
},
});
};
const showExportPrivateEnvironmentsModal = async (privateEnvNames: string) => {
return new Promise<boolean>(resolve => {
showModal(AskModal, {
title: 'Export Private Environments?',
message: `Do you want to include private environments (${privateEnvNames}) in your export?`,
onDone: async (isYes: boolean) => {
if (isYes) {
resolve(true);
} else {
resolve(false);
}
},
});
});
};
const showSaveExportedFileDialog = async ({
exportedFileNamePrefix,
selectedFormat,
}: {
exportedFileNamePrefix: string;
selectedFormat: SelectedFormat;
}) => {
const date = format(Date.now(), 'yyyy-MM-dd');
const name = exportedFileNamePrefix.replace(/ /g, '-');
const lastDir = window.localStorage.getItem('insomnia.lastExportPath');
const dir = lastDir || window.app.getPath('desktop');
const options = {
title: 'Export Insomnia Data',
buttonLabel: 'Export',
defaultPath: `${path.join(dir, `${name}_${date}`)}.${selectedFormat}`,
};
const { filePath } = await window.dialog.showSaveDialog(options);
return filePath || null;
};
const writeExportedFileToFileSystem = (filename: string, jsonData: string, onDone: NoParamCallback) => {
// Remember last exported path
window.localStorage.setItem('insomnia.lastExportPath', path.dirname(filename));
fs.writeFile(filename, jsonData, {}, onDone);
};
export const exportAllToFile = (activeProjectName: string, workspacesForActiveProject: Workspace[]) => {
if (!workspacesForActiveProject.length) {
showAlert({
title: 'Cannot export',
message: <>There are no workspaces to export in the <strong>{activeProjectName}</strong> {strings.project.singular.toLowerCase()}.</>,
});
return;
}
showSelectExportTypeModal({
onDone: async selectedFormat => {
// Check if we want to export private environments.
const environments = await models.environment.all();
let exportPrivateEnvironments = false;
const privateEnvironments = environments.filter(environment => environment.isPrivate);
if (privateEnvironments.length) {
const names = privateEnvironments.map(environment => environment.name).join(', ');
exportPrivateEnvironments = await showExportPrivateEnvironmentsModal(names);
}
const fileName = await showSaveExportedFileDialog({
exportedFileNamePrefix: 'Insomnia-All',
selectedFormat,
});
if (!fileName) {
return;
}
let stringifiedExport;
try {
switch (selectedFormat) {
case VALUE_HAR:
stringifiedExport = await exportWorkspacesHAR(workspacesForActiveProject, exportPrivateEnvironments);
break;
case VALUE_YAML:
stringifiedExport = await exportWorkspacesData(workspacesForActiveProject, exportPrivateEnvironments, 'yaml');
break;
case VALUE_JSON:
stringifiedExport = await exportWorkspacesData(workspacesForActiveProject, exportPrivateEnvironments, 'json');
break;
default:
unreachableCase(selectedFormat, `selected export format "${selectedFormat}" is invalid`);
}
} catch (err) {
showError({
title: 'Export Failed',
error: err,
message: 'Export failed due to an unexpected error',
});
return;
}
writeExportedFileToFileSystem(fileName, stringifiedExport, err => {
if (err) {
console.warn('Export failed', err);
}
});
},
});
};
export const exportRequestsToFile = (requestIds: string[]) => {
showSelectExportTypeModal({
onDone: async selectedFormat => {
const requests: (GrpcRequest | Request | WebSocketRequest)[] = [];
const privateEnvironments: Environment[] = [];
const workspaceLookup: any = {};
for (const requestId of requestIds) {
const request = await requestOperations.getById(requestId);
if (request == null) {
continue;
}
requests.push(request);
const ancestors = await database.withAncestors(request, [
models.workspace.type,
models.requestGroup.type,
]);
const workspace = ancestors.find(isWorkspace);
if (workspace == null || workspaceLookup.hasOwnProperty(workspace._id)) {
continue;
}
workspaceLookup[workspace._id] = true;
const descendants = await database.withDescendants(workspace);
const privateEnvs = descendants.filter(isEnvironment).filter(
descendant => descendant.isPrivate,
);
privateEnvironments.push(...privateEnvs);
}
let exportPrivateEnvironments = false;
if (privateEnvironments.length) {
const names = privateEnvironments.map(privateEnvironment => privateEnvironment.name).join(', ');
exportPrivateEnvironments = await showExportPrivateEnvironmentsModal(names);
}
const fileName = await showSaveExportedFileDialog({
exportedFileNamePrefix: 'Insomnia',
selectedFormat,
});
if (!fileName) {
return;
}
let stringifiedExport;
try {
switch (selectedFormat) {
case VALUE_HAR:
stringifiedExport = await exportRequestsHAR(requests, exportPrivateEnvironments);
break;
case VALUE_YAML:
stringifiedExport = await exportRequestsData(requests, exportPrivateEnvironments, 'yaml');
break;
case VALUE_JSON:
stringifiedExport = await exportRequestsData(requests, exportPrivateEnvironments, 'json');
break;
default:
unreachableCase(selectedFormat, `selected export format "${selectedFormat}" is invalid`);
}
} catch (err) {
showError({
title: 'Export Failed',
error: err,
message: 'Export failed due to an unexpected error',
});
return;
}
writeExportedFileToFileSystem(fileName, stringifiedExport, err => {
if (err) {
console.warn('Export failed', err);
}
});
},
});
};
function _normalizeActivity(activity: GlobalActivity): GlobalActivity {
if (isValidActivity(activity)) {
return activity;
}
const fallbackActivity = ACTIVITY_HOME;
console.log(`[app] invalid activity "${activity}"; navigating to ${fallbackActivity}`);
return fallbackActivity;
}

View File

@ -1,154 +0,0 @@
import { strings } from '../../../common/strings';
import { Project } from '../../../models/project';
import { Workspace, WorkspaceScope, WorkspaceScopeKeys } from '../../../models/workspace';
import { showModal } from '../../components/modals';
import { AskModal } from '../../components/modals/ask-modal';
import { showSelectModal } from '../../components/modals/select-modal';
export enum ForceToWorkspace {
new = 'new',
current = 'current',
existing = 'existing'
}
export type SelectExistingWorkspacePrompt = Promise<string | null>;
export function askToSelectExistingWorkspace(workspaces: Workspace[]): SelectExistingWorkspacePrompt {
return new Promise(resolve => {
const options = workspaces.map(workspace => ({ name: workspace.name, value: workspace._id }));
showSelectModal({
title: 'Import',
message: `Select a ${strings.workspace.singular.toLowerCase()} to import into`,
options,
value: options[0]?.value,
noEscape: true,
onDone: workspaceId => {
resolve(workspaceId);
},
});
});
}
async function askToImportIntoNewWorkspace(): Promise<boolean> {
return new Promise(resolve => {
showModal(AskModal, {
title: 'Import',
message: `Do you want to import into an existing ${strings.workspace.singular.toLowerCase()} or a new one?`,
yesText: 'Existing',
noText: 'New',
onDone: async (yes: boolean) => {
resolve(yes);
},
});
});
}
// Returning null instead of a string will create a new workspace
export type ImportToWorkspacePrompt = () => null | string | Promise<null | string>;
export function askToImportIntoWorkspace({ workspaceId, forceToWorkspace, activeProjectWorkspaces }: { workspaceId?: string; forceToWorkspace?: ForceToWorkspace; activeProjectWorkspaces: Workspace[] }): ImportToWorkspacePrompt {
return function() {
switch (forceToWorkspace) {
case ForceToWorkspace.new: {
return null;
}
case ForceToWorkspace.current: {
if (!workspaceId) {
return null;
}
return workspaceId;
}
case ForceToWorkspace.existing: {
// Return null if there are no available workspaces to chose from.
if (activeProjectWorkspaces.length === 0) {
return null;
}
return new Promise(async resolve => {
const yes = await askToImportIntoNewWorkspace();
if (yes) {
const workspaceId = await askToSelectExistingWorkspace(activeProjectWorkspaces);
resolve(workspaceId);
} else {
resolve(null);
}
});
}
default: {
if (!workspaceId) {
return null;
}
return new Promise(resolve => {
showModal(AskModal, {
title: 'Import',
message: 'Do you want to import into the current workspace or a new one?',
yesText: 'Current',
noText: 'New Workspace',
onDone: async (yes: boolean) => {
resolve(yes ? workspaceId : null);
},
});
});
}
}
};
}
export type SetWorkspaceScopePrompt = (name?: string) => WorkspaceScope | Promise<WorkspaceScope>;
export function askToSetWorkspaceScope(scope?: WorkspaceScope): SetWorkspaceScopePrompt {
return name => {
switch (scope) {
case WorkspaceScopeKeys.collection:
case WorkspaceScopeKeys.design:
return scope;
default:
return new Promise(resolve => {
const message = name
? `How would you like to import "${name}"?`
: 'Do you want to import as a Request Collection or a Design Document?';
showModal(AskModal, {
title: 'Import As',
message,
noText: 'Request Collection',
yesText: 'Design Document',
onDone: async (yes: boolean) => {
resolve(yes ? WorkspaceScopeKeys.design : WorkspaceScopeKeys.collection);
},
});
});
}
};
}
export type SetProjectIdPrompt = () => Promise<string>;
export function askToImportIntoProject({ projects, activeProject }: { projects: Project[]; activeProject: Project }): SetProjectIdPrompt {
return function() {
return new Promise(resolve => {
// If only one project exists, return that
if (projects.length === 1) {
return resolve(projects[0]._id);
}
const options = projects.map(project => ({ name: project.name, value: project._id }));
const defaultValue = activeProject._id;
showSelectModal({
title: 'Import',
message: `Select a ${strings.project.singular.toLowerCase()} to import into`,
options,
value: defaultValue,
noEscape: true,
onDone: selectedProjectId => {
// @ts-expect-error onDone can send null as an argument; why/how?
resolve(selectedProjectId);
},
});
});
};
}

View File

@ -1,179 +0,0 @@
import electron, { OpenDialogOptions } from 'electron';
import { AnyAction } from 'redux';
import { ThunkAction } from 'redux-thunk';
import {
importRaw,
ImportRawConfig,
ImportResult,
importUri as _importUri,
} from '../../../common/import';
import * as models from '../../../models';
import { Workspace, WorkspaceScope } from '../../../models/workspace';
import { showError, showModal } from '../../components/modals';
import { AlertModal } from '../../components/modals/alert-modal';
import { selectActiveProject, selectProjects, selectWorkspacesWithResolvedNameForActiveProject } from '../selectors';
import { RootState } from '.';
import { loadStart, loadStop } from './global';
import { askToImportIntoProject, askToImportIntoWorkspace, askToSetWorkspaceScope, ForceToWorkspace } from './helpers';
export interface ImportOptions {
workspaceId?: string;
forceToProject?: 'active' | 'prompt';
forceToWorkspace?: ForceToWorkspace;
forceToScope?: WorkspaceScope;
onComplete?: () => void;
}
const handleImportResult = (result: ImportResult, errorMessage: string) => {
const { error, summary } = result;
if (error) {
showError({
title: 'Import Failed',
message: errorMessage,
error,
});
return [];
}
models.stats.incrementRequestStats({
createdRequests: summary[models.request.type].length + summary[models.grpcRequest.type].length,
});
return (summary[models.workspace.type] as Workspace[]) || [];
};
const convertToRawConfig = ({
forceToScope,
forceToWorkspace,
workspaceId,
forceToProject,
}: ImportOptions,
state: RootState): ImportRawConfig => {
const activeProject = selectActiveProject(state);
const activeProjectWorkspaces = selectWorkspacesWithResolvedNameForActiveProject(state);
const projects = selectProjects(state);
return ({
getWorkspaceScope: askToSetWorkspaceScope(forceToScope),
getWorkspaceId: askToImportIntoWorkspace({ workspaceId, forceToWorkspace, activeProjectWorkspaces }),
// Currently, just return the active project instead of prompting for which project to import into
getProjectId: forceToProject === 'prompt' ? askToImportIntoProject({ projects, activeProject }) : () => Promise.resolve(activeProject._id),
});
};
export const importFile = (
options: ImportOptions = {},
): ThunkAction<void, RootState, void, AnyAction> => async (dispatch, getState) => {
dispatch(loadStart());
const openDialogOptions: OpenDialogOptions = {
title: 'Import Insomnia Data',
buttonLabel: 'Import',
properties: ['openFile'],
filters: [
// @ts-expect-error https://github.com/electron/electron/pull/29322
{
extensions: [
'',
'sh',
'txt',
'json',
'har',
'curl',
'bash',
'shell',
'yaml',
'yml',
'wsdl',
],
},
],
};
const { canceled, filePaths } = await window.dialog.showOpenDialog(openDialogOptions);
if (canceled) {
// It was cancelled, so let's bail out
dispatch(loadStop());
return;
}
// Let's import all the files!
for (const filePath of filePaths) {
try {
const uri = `file://${filePath}`;
const config = convertToRawConfig(options, getState());
const result = await _importUri(uri, config);
handleImportResult(result, 'The file does not contain a valid specification.');
} catch (err) {
showModal(AlertModal, {
title: 'Import Failed',
message: err + '',
});
} finally {
dispatch(loadStop());
}
}
options.onComplete?.();
};
export const readFromClipBoard = () => {
const schema = electron.clipboard.readText();
if (!schema) {
showModal(AlertModal, {
title: 'Import Failed',
message: 'Your clipboard appears to be empty.',
});
}
return schema;
};
export const importClipBoard = (
options: ImportOptions = {},
): ThunkAction<void, RootState, void, AnyAction> => async (dispatch, getState) => {
dispatch(loadStart());
const schema = readFromClipBoard();
if (!schema) {
return;
}
// Let's import all the paths!
try {
const config = convertToRawConfig(options, getState());
const result = await importRaw(schema, config);
handleImportResult(result, 'Your clipboard does not contain a valid specification.');
} catch (err) {
showModal(AlertModal, {
title: 'Import Failed',
message: 'Your clipboard does not contain a valid specification.',
});
} finally {
dispatch(loadStop());
}
options.onComplete?.();
};
export const importUri = (
uri: string,
options: ImportOptions = {},
): ThunkAction<void, RootState, void, AnyAction> => async (dispatch, getState) => {
dispatch(loadStart());
try {
const config = convertToRawConfig(options, getState());
const result = await _importUri(uri, config);
handleImportResult(result, 'The URI does not contain a valid specification.');
} catch (err) {
showModal(AlertModal, {
title: 'Import Failed',
message: err + '',
});
} finally {
dispatch(loadStop());
}
options.onComplete?.();
};

View File

@ -1,48 +0,0 @@
import { ACTIVITY_HOME } from '../../../common/constants';
import { strings } from '../../../common/strings';
import * as models from '../../../models';
import { DEFAULT_PROJECT_ID, isRemoteProject, Project } from '../../../models/project';
import { SegmentEvent, trackSegmentEvent } from '../../analytics';
import { showAlert, showPrompt } from '../../components/modals';
import { setActiveActivity, setActiveProject } from './global';
export const createProject = () => (dispatch: any) => {
const defaultValue = `My ${strings.project.singular}`;
showPrompt({
title: `Create New ${strings.project.singular}`,
submitName: 'Create',
cancelable: true,
placeholder: defaultValue,
defaultValue,
selectText: true,
onComplete: async name => {
const project = await models.project.create({ name });
dispatch(setActiveProject(project._id));
dispatch(setActiveActivity(ACTIVITY_HOME));
trackSegmentEvent(SegmentEvent.projectLocalCreate);
},
});
};
export const removeProject = (project: Project) => (dispatch: any) => {
const message = isRemoteProject(project)
? `Deleting a ${strings.remoteProject.singular.toLowerCase()} ${strings.project.singular.toLowerCase()} will delete all local copies and changes of ${strings.document.plural.toLowerCase()} and ${strings.collection.plural.toLowerCase()} within. All changes that are not synced will be lost. The ${strings.remoteProject.singular.toLowerCase()} ${strings.project.singular.toLowerCase()} will continue to exist remotely. Deleting this ${strings.project.singular.toLowerCase()} locally cannot be undone. Are you sure you want to delete ${project.name}?`
: `Deleting a ${strings.project.singular.toLowerCase()} will delete all ${strings.document.plural.toLowerCase()} and ${strings.collection.plural.toLowerCase()} within. This cannot be undone. Are you sure you want to delete ${project.name}?`;
showAlert({
title: `Delete ${strings.project.singular}`,
message,
addCancel: true,
okLabel: 'Delete',
onConfirm: async () => {
await models.stats.incrementDeletedRequestsForDescendents(project);
await models.project.remove(project);
// Show default project
dispatch(setActiveProject(DEFAULT_PROJECT_ID));
// Show home in case not already on home
dispatch(setActiveActivity(ACTIVITY_HOME));
trackSegmentEvent(SegmentEvent.projectLocalDelete);
},
});
};

View File

@ -2,77 +2,12 @@ import { Dispatch } from 'redux';
import { RequireExactlyOne } from 'type-fest';
import { ACTIVITY_DEBUG, ACTIVITY_SPEC, GlobalActivity, isCollectionActivity, isDesignActivity } from '../../../common/constants';
import { database } from '../../../common/database';
import * as models from '../../../models';
import { isCollection, isDesign, Workspace, WorkspaceScope } from '../../../models/workspace';
import { SegmentEvent, trackSegmentEvent } from '../../analytics';
import { showPrompt } from '../../components/modals';
import { selectActiveActivity, selectActiveProject, selectWorkspaces } from '../selectors';
import { isCollection, isDesign, Workspace } from '../../../models/workspace';
import { selectActiveActivity, selectWorkspaces } from '../selectors';
import { RootState } from '.';
import { setActiveActivity, setActiveProject, setActiveWorkspace } from './global';
type OnWorkspaceCreateCallback = (arg0: Workspace) => Promise<void> | void;
const createWorkspaceAndChildren = async (patch: Partial<Workspace>) => {
const flushId = await database.bufferChanges();
const workspace = await models.workspace.create(patch);
await models.workspace.ensureChildren(workspace);
await database.flushChanges(flushId);
return workspace;
};
const actuallyCreate = (patch: Partial<Workspace>, onCreate?: OnWorkspaceCreateCallback) => {
return async (dispatch: any) => {
const workspace = await createWorkspaceAndChildren(patch);
if (onCreate) {
await onCreate(workspace);
}
trackSegmentEvent(SegmentEvent.collectionCreate);
await dispatch(activateWorkspace({ workspace }));
};
};
export const createWorkspace = ({ scope, onCreate }: {
scope: WorkspaceScope;
onCreate?: OnWorkspaceCreateCallback;
}) => {
return (dispatch: any, getState: any) => {
const activeProject = selectActiveProject(getState());
const design = isDesign({
scope,
});
const title = design ? 'Design Document' : 'Request Collection';
const defaultValue = design ? 'my-spec.yaml' : 'My Collection';
const segmentEvent = design ? SegmentEvent.documentCreate : SegmentEvent.collectionCreate;
showPrompt({
title: `Create New ${title}`,
submitName: 'Create',
placeholder: defaultValue,
defaultValue,
selectText: true,
onComplete: async name => {
await dispatch(
actuallyCreate(
{
name,
scope,
parentId: activeProject._id,
},
onCreate,
),
);
trackSegmentEvent(segmentEvent);
},
});
};
};
export const activateWorkspace = ({ workspace, workspaceId }: RequireExactlyOne<{workspace: Workspace; workspaceId: string}>) => {
return async (dispatch: Dispatch, getState: () => RootState) => {
// If we have no workspace but we do have an id, search for it

View File

@ -379,17 +379,6 @@ export const selectActiveOAuth2Token = createSelector(
},
);
export const selectLoadingRequestIds = createSelector(
selectGlobal,
global => global.loadingRequestIds,
);
export const selectLoadStartTime = createSelector(
selectLoadingRequestIds,
selectActiveRequest,
(loadingRequestIds, activeRequest) => loadingRequestIds[activeRequest ? activeRequest._id : 'n/a'] || -1
);
export const selectUnseenWorkspaces = createSelector(
selectEntitiesLists,
entities => {

View File

@ -1,4 +1,5 @@
import React, { FC, Fragment, useEffect } from 'react';
import { invariant } from '@remix-run/router';
import React, { FC, Fragment, useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import * as models from '../../models';
@ -48,6 +49,14 @@ export const Debug: FC = () => {
const settings = useSelector(selectSettings);
const sidebarFilter = useSelector(selectSidebarFilter);
const activeWorkspaceMeta = useSelector(selectActiveWorkspaceMeta);
const [runningRequests, setRunningRequests] = useState({});
const setLoading = (isLoading: boolean) => {
invariant(activeRequest, 'No active request');
setRunningRequests({
...runningRequests,
[activeRequest._id]: isLoading ? Date.now() : 0,
});
};
useDocBodyKeyboardShortcuts({
request_togglePin:
@ -205,6 +214,7 @@ export const Debug: FC = () => {
request={activeRequest}
settings={settings}
workspace={activeWorkspace}
setLoading={setLoading}
/>
)
)
@ -221,7 +231,7 @@ export const Debug: FC = () => {
isWebSocketRequest(activeRequest) ? (
<WebSocketResponsePane requestId={activeRequest._id} />
) : (
<ResponsePane request={activeRequest} />
<ResponsePane request={activeRequest} runningRequests={runningRequests} />
)
)
)}

View File

@ -20,6 +20,7 @@ import {
ACTIVITY_SPEC,
DashboardSortOrder,
} from '../../common/constants';
import { ForceToWorkspace } from '../../common/import';
import { fuzzyMatchAll, isNotNullOrUndefined } from '../../common/misc';
import { descendingNumberSort, sortMethodMap } from '../../common/sorting';
import { strings } from '../../common/strings';
@ -43,13 +44,12 @@ import { EmptyStatePane } from '../components/panes/project-empty-state-pane';
import { SidebarLayout } from '../components/sidebar-layout';
import { Button } from '../components/themed-button';
import { WorkspaceCard } from '../components/workspace-card';
import { cloneGitRepository } from '../redux/modules/git';
import { ForceToWorkspace } from '../redux/modules/helpers';
import {
importClipBoard,
importFile,
importUri,
} from '../redux/modules/import';
} from '../import';
import { cloneGitRepository } from '../redux/modules/git';
const CreateButton = styled(Button).attrs({
variant: 'outlined',
@ -299,10 +299,9 @@ const OrganizationProjectsSidebar: FC<{
return (
<li key={proj._id} className="sidebar__row">
<div
className={`sidebar__item sidebar__item--request ${
activeProject._id === proj._id
? 'sidebar__item--active'
: ''
className={`sidebar__item sidebar__item--request ${activeProject._id === proj._id
? 'sidebar__item--active'
: ''
}`}
>
<button
@ -450,8 +449,8 @@ export const loader: LoaderFunction = async ({
const hasUnsavedChanges = Boolean(
isDesign(workspace) &&
workspaceMeta?.cachedGitLastCommitTime &&
apiSpec.modified > workspaceMeta?.cachedGitLastCommitTime
workspaceMeta?.cachedGitLastCommitTime &&
apiSpec.modified > workspaceMeta?.cachedGitLastCommitTime
);
return {
@ -466,7 +465,10 @@ export const loader: LoaderFunction = async ({
name: isDesign(workspace) ? apiSpec.fileName : workspace.name,
apiSpec,
specFormatVersion,
workspace,
workspace: {
...workspace,
name: isDesign(workspace) ? apiSpec.fileName : workspace.name,
},
};
};
@ -554,12 +556,12 @@ export const loader: LoaderFunction = async ({
const ProjectRoute: FC = () => {
const { workspaces, activeProject, projects, organization } = useLoaderData() as LoaderData;
const { organizationId } = useParams() as {organizationId: string};
const { organizationId } = useParams() as { organizationId: string };
const [searchParams] = useSearchParams();
const dispatch = useDispatch();
const fetcher = useFetcher();
const { revalidate } = useRevalidator();
const submit = useSubmit();
const navigate = useNavigate();
const filter = searchParams.get('filter') || '';
@ -620,20 +622,36 @@ const ProjectRoute: FC = () => {
cancelable: true,
placeholder: 'https://website.com/insomnia-import.json',
onComplete: uri => {
dispatch(
importUri(uri, { forceToWorkspace: ForceToWorkspace.existing, onComplete: revalidate })
);
importUri(uri, {
activeProjectWorkspaces: workspaces.map(w => w.workspace),
activeProject,
projects,
forceToWorkspace: ForceToWorkspace.existing,
onComplete: revalidate,
});
},
});
}, [dispatch, revalidate]);
}, [activeProject, projects, revalidate, workspaces]);
const importFromClipboard = useCallback(() => {
dispatch(importClipBoard({ forceToWorkspace: ForceToWorkspace.existing, onComplete: revalidate }));
}, [dispatch, revalidate]);
importClipBoard({
activeProjectWorkspaces: workspaces.map(w => w.workspace),
activeProject,
projects,
forceToWorkspace: ForceToWorkspace.existing,
onComplete: revalidate,
});
}, [activeProject, projects, revalidate, workspaces]);
const importFromFile = useCallback(() => {
dispatch(importFile({ forceToWorkspace: ForceToWorkspace.existing, onComplete: revalidate }));
}, [dispatch, revalidate]);
importFile({
activeProjectWorkspaces: workspaces.map(w => w.workspace),
activeProject,
projects,
forceToWorkspace: ForceToWorkspace.existing,
onComplete: revalidate,
});
}, [activeProject, projects, revalidate, workspaces]);
const importFromGit = useCallback(() => {
dispatch(cloneGitRepository({ createFsClient: MemClient.createClient, onComplete: revalidate }));
@ -729,12 +747,10 @@ const ProjectRoute: FC = () => {
activeProject={activeProject}
onSelect={() =>
navigate(
`/organization/${organizationId}/project/${
activeProject._id
}/workspace/${workspace.workspace._id}/${
workspace.workspace.scope === 'design'
? ACTIVITY_SPEC
: ACTIVITY_DEBUG
`/organization/${organizationId}/project/${activeProject._id
}/workspace/${workspace.workspace._id}/${workspace.workspace.scope === 'design'
? ACTIVITY_SPEC
: ACTIVITY_DEBUG
}`
)
}