mirror of
https://github.com/Kong/insomnia
synced 2024-11-07 22:30:15 +00:00
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:
parent
de9cee96d3
commit
fc730b67e3
@ -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,
|
||||
},
|
||||
};
|
||||
|
@ -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', () => {
|
@ -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);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
@ -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);
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -96,7 +96,6 @@ export async function generateAuthorizationUrl() {
|
||||
export async function exchangeCodeForGitLabToken(input: {
|
||||
code: string;
|
||||
state: string;
|
||||
scope: string;
|
||||
}) {
|
||||
const { code, state } = input;
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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
|
||||
|
@ -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';
|
||||
|
@ -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);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
@ -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';
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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]);
|
||||
};
|
164
packages/insomnia/src/ui/hooks/use-app-commands.tsx
Normal file
164
packages/insomnia/src/ui/hooks/use-app-commands.tsx
Normal 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]);
|
||||
};
|
@ -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,
|
||||
);
|
||||
|
192
packages/insomnia/src/ui/import.ts
Normal file
192
packages/insomnia/src/ui/import.ts
Normal 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?.();
|
||||
};
|
@ -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,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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');
|
||||
|
@ -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?.();
|
||||
},
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
@ -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?.();
|
||||
};
|
@ -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);
|
||||
},
|
||||
});
|
||||
};
|
@ -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
|
||||
|
@ -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 => {
|
||||
|
@ -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} />
|
||||
)
|
||||
)
|
||||
)}
|
||||
|
@ -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
|
||||
}`
|
||||
)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user