Import into the active space (#3795)

This commit is contained in:
Opender Singh
2021-07-14 12:10:49 +12:00
committed by GitHub
parent ca0f6eb9d0
commit d241093ec2
15 changed files with 354 additions and 301 deletions

View File

@@ -31,6 +31,7 @@ import { isCookieJar } from '../models/cookie-jar';
import { isEnvironment } from '../models/environment';
import { isUnitTestSuite } from '../models/unit-test-suite';
import { isUnitTest } from '../models/unit-test';
import { resetKeys } from '../sync/vcs/ignore-keys';
const EXPORT_FORMAT = 4;
@@ -210,6 +211,8 @@ export async function exportRequestsData(
if (isWorkspace(d)) {
// @ts-expect-error -- TSCONVERSION maybe this needs to be added to the upstream type?
d._type = EXPORT_TYPE_WORKSPACE;
// reset the parentId of a workspace
resetKeys(d);
} else if (isCookieJar(d)) {
// @ts-expect-error -- TSCONVERSION maybe this needs to be added to the upstream type?
d._type = EXPORT_TYPE_COOKIE_JAR;

View File

@@ -39,6 +39,7 @@ interface ConvertResult {
export interface ImportRawConfig {
getWorkspaceId: ImportToWorkspacePrompt;
getSpaceId?: () => Promise<string | null>;
getWorkspaceScope?: SetWorkspaceScopePrompt;
enableDiffBasedPatching?: boolean;
enableDiffDeep?: boolean;
@@ -112,6 +113,7 @@ export async function importRaw(
{
getWorkspaceId,
getWorkspaceScope,
getSpaceId,
enableDiffBasedPatching,
enableDiffDeep,
bypassDiffProps,
@@ -246,15 +248,18 @@ export async function importRaw(
updateDoc.url = resource.url;
}
// If workspace, don't overwrite the existing scope
// If workspace preserve the scope and parentId of the existing workspace while importing
if (isWorkspace(model)) {
(updateDoc as Workspace).scope = (existingDoc as Workspace).scope;
(updateDoc as Workspace).parentId = (existingDoc as Workspace).parentId;
}
newDoc = await db.docUpdate(existingDoc, updateDoc);
} 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);
await createWorkspaceInSpace(resource as Workspace, getSpaceId);
}
newDoc = await db.docCreate(model.type, resource);
@@ -306,7 +311,7 @@ async function updateWorkspaceScope(
resultType: ConvertResultType,
getWorkspaceScope?: SetWorkspaceScopePrompt,
) {
// Set the workspace scope if creating a new workspace
// 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
@@ -328,6 +333,17 @@ async function updateWorkspaceScope(
}
}
async function createWorkspaceInSpace(
resource: Workspace,
getSpaceId?: () => Promise<string | null>,
) {
if (getSpaceId) {
// Set the workspace parent if creating a new workspace during import
// @ts-expect-error workspace parent can be null or string
resource.parentId = await getSpaceId();
}
}
export const isApiSpecImport = ({ id }: Pick<ConvertResultType, 'id'>) => (
id === 'openapi3' || id === 'swagger2'
);

View File

@@ -5,17 +5,19 @@ import ModalHeader from '../base/modal-header';
import ModalFooter from '../base/modal-footer';
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import { AUTOBIND_CFG } from '../../../common/constants';
import { showModal } from '.';
export interface SelectModalShowOptions {
message: string | null;
onCancel?: () => void;
onDone?: (selectedValue: string | null) => Promise<void>;
onDone?: (selectedValue: string | null) => void | Promise<void>;
options: {
name: string;
value: string;
}[];
title: string | null;
value: string | null;
noEscape?: boolean;
}
const initialState: SelectModalShowOptions = {
@@ -47,6 +49,7 @@ export class SelectModal extends PureComponent<{}, SelectModalShowOptions> {
options,
title,
value,
noEscape,
}: SelectModalShowOptions = initialState) {
this.setState({
message,
@@ -55,6 +58,7 @@ export class SelectModal extends PureComponent<{}, SelectModalShowOptions> {
options,
title,
value,
noEscape,
});
this.modal.current?.show();
setTimeout(() => {
@@ -63,9 +67,10 @@ export class SelectModal extends PureComponent<{}, SelectModalShowOptions> {
}
render() {
const { message, title, options, value, onCancel } = this.state;
const { message, title, options, value, onCancel, noEscape } = this.state;
return (
<Modal ref={this.modal} onCancel={onCancel}>
<Modal ref={this.modal} onCancel={onCancel} noEscape={noEscape}>
<ModalHeader>{title || 'Confirm?'}</ModalHeader>
<ModalBody className="wide pad">
<p>{message}</p>
@@ -88,3 +93,5 @@ export class SelectModal extends PureComponent<{}, SelectModalShowOptions> {
);
}
}
export const showSelectModal = (opts: SelectModalShowOptions) => showModal(SelectModal, opts);

View File

@@ -1,75 +1,77 @@
import React, { FunctionComponent } from 'react';
import React, { FC, useCallback } from 'react';
import Hotkey from '../hotkey';
import { hotKeyRefs } from '../../../common/hotkeys';
import * as hotkeys from '../../../common/hotkeys';
import { Pane, PaneBody, PaneHeader } from './pane';
import type { HandleImportFileCallback } from '../wrapper';
import { useDispatch, useSelector } from 'react-redux';
import { importFile } from '../../redux/modules/import';
import { selectActiveWorkspace } from '../../redux/selectors';
interface Props {
hotKeyRegistry: hotkeys.HotKeyRegistry;
handleImportFile: HandleImportFileCallback;
handleCreateRequest: () => void;
}
const PlaceholderRequestPane: FunctionComponent<Props> = ({
const PlaceholderRequestPane: FC<Props> = ({
hotKeyRegistry,
handleImportFile,
handleCreateRequest,
}) => (
<Pane type="request">
<PaneHeader />
<PaneBody placeholder>
<div>
<table className="table--fancy">
<tbody>
<tr>
<td>New Request</td>
<td className="text-right">
<code>
<Hotkey
keyBindings={hotKeyRegistry[hotKeyRefs.REQUEST_SHOW_CREATE.id]}
useFallbackMessage
/>
</code>
</td>
</tr>
<tr>
<td>Switch Requests</td>
<td className="text-right">
<code>
<Hotkey
keyBindings={hotKeyRegistry[hotKeyRefs.REQUEST_QUICK_SWITCH.id]}
useFallbackMessage
/>
</code>
</td>
</tr>
<tr>
<td>Edit Environments</td>
<td className="text-right">
<code>
<Hotkey
keyBindings={hotKeyRegistry[hotKeyRefs.ENVIRONMENT_SHOW_EDITOR.id]}
useFallbackMessage
/>
</code>
</td>
</tr>
</tbody>
</table>
}) => {
const dispatch = useDispatch();
const workspaceId = useSelector(selectActiveWorkspace)?._id;
const handleImportFile = useCallback(() => dispatch(importFile({ workspaceId })), [workspaceId, dispatch]);
<div className="text-center pane__body--placeholder__cta">
{/* @ts-expect-error -- TSCONVERSION event not used */}
<button className="btn inline-block btn--clicky" onClick={handleImportFile}>
Import from File
</button>
<button className="btn inline-block btn--clicky" onClick={handleCreateRequest}>
New Request
</button>
return (
<Pane type="request">
<PaneHeader />
<PaneBody placeholder>
<div>
<table className="table--fancy">
<tbody>
<tr>
<td>New Request</td>
<td className="text-right">
<code>
<Hotkey
keyBindings={hotKeyRegistry[hotKeyRefs.REQUEST_SHOW_CREATE.id]}
useFallbackMessage />
</code>
</td>
</tr>
<tr>
<td>Switch Requests</td>
<td className="text-right">
<code>
<Hotkey
keyBindings={hotKeyRegistry[hotKeyRefs.REQUEST_QUICK_SWITCH.id]}
useFallbackMessage />
</code>
</td>
</tr>
<tr>
<td>Edit Environments</td>
<td className="text-right">
<code>
<Hotkey
keyBindings={hotKeyRegistry[hotKeyRefs.ENVIRONMENT_SHOW_EDITOR.id]}
useFallbackMessage />
</code>
</td>
</tr>
</tbody>
</table>
<div className="text-center pane__body--placeholder__cta">
<button className="btn inline-block btn--clicky" onClick={handleImportFile}>
Import from File
</button>
<button className="btn inline-block btn--clicky" onClick={handleCreateRequest}>
New Request
</button>
</div>
</div>
</div>
</PaneBody>
</Pane>
);
</PaneBody>
</Pane>
);
};
export default PlaceholderRequestPane;

View File

@@ -30,7 +30,6 @@ import PlaceholderRequestPane from './placeholder-request-pane';
import { Pane, paneBodyClasses, PaneHeader } from './pane';
import classnames from 'classnames';
import { queryAllWorkspaceUrls } from '../../../models/helpers/query-all-workspace-urls';
import type { HandleImportFileCallback } from '../wrapper';
import { HandleGetRenderContext, HandleRender } from '../../../common/render';
interface Props {
@@ -55,7 +54,6 @@ interface Props {
updateSettingsUseBulkHeaderEditor: Function;
updateSettingsUseBulkParametersEditor: (useBulkParametersEditor: boolean) => Promise<Settings>;
handleImport: Function;
handleImportFile: HandleImportFileCallback;
workspace: Workspace;
settings: Settings;
isVariableUncovered: boolean;
@@ -132,7 +130,6 @@ class RequestPane extends PureComponent<Props> {
handleGenerateCode,
handleGetRenderContext,
handleImport,
handleImportFile,
handleCreateRequest,
handleRender,
handleSend,
@@ -161,7 +158,6 @@ class RequestPane extends PureComponent<Props> {
return (
<PlaceholderRequestPane
hotKeyRegistry={hotKeyRegistry}
handleImportFile={handleImportFile}
handleCreateRequest={handleCreateRequest}
/>
);

View File

@@ -7,9 +7,10 @@ import { strings } from '../../../common/strings';
import { useDispatch, useSelector } from 'react-redux';
import { selectActiveSpaceName, selectActiveWorkspace } from '../../redux/selectors';
import ExportRequestsModal from '../modals/export-requests-modal';
import { exportAllToFile, importClipBoard, importFile, importUri } from '../../redux/modules/global';
import { exportAllToFile } from '../../redux/modules/global';
import { getAppName } from '../../../common/constants';
import { getWorkspaceLabel } from '../../../common/get-workspace-label';
import { importClipBoard, importFile, importUri } from '../../redux/modules/import';
interface Props {
hideSettingsModal: () => void;

View File

@@ -1,7 +1,7 @@
import React, { Fragment, PureComponent, ReactNode } from 'react';
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import PageLayout from './page-layout';
import type { HandleImportFileCallback, WrapperProps } from './wrapper';
import type { WrapperProps } from './wrapper';
import RequestPane from './panes/request-pane';
import ErrorBoundary from './error-boundary';
import ResponsePane from './panes/response-pane';
@@ -30,7 +30,6 @@ interface Props {
handleForceUpdateRequest: (r: Request, patch: Partial<Request>) => Promise<Request>;
handleForceUpdateRequestHeaders: (r: Request, headers: RequestHeader[]) => Promise<Request>;
handleImport: Function;
handleImportFile: HandleImportFileCallback;
handleRequestCreate: () => void;
handleRequestGroupCreate: () => void;
handleSendAndDownloadRequestWithActiveEnvironment: (filepath?: string) => Promise<void>;
@@ -196,7 +195,6 @@ class WrapperDebug extends PureComponent<Props> {
handleForceUpdateRequest,
handleForceUpdateRequestHeaders,
handleImport,
handleImportFile,
handleSendAndDownloadRequestWithActiveEnvironment,
handleSendRequestWithActiveEnvironment,
handleUpdateRequestAuthentication,
@@ -261,7 +259,6 @@ class WrapperDebug extends PureComponent<Props> {
handleGenerateCode={handleGenerateCodeForActiveRequest}
handleGetRenderContext={handleGetRenderContext}
handleImport={handleImport}
handleImportFile={handleImportFile}
handleRender={handleRender}
handleSend={handleSendRequestWithActiveEnvironment}
handleSendAndDownload={handleSendAndDownloadRequestWithActiveEnvironment}

View File

@@ -30,14 +30,10 @@ import TimeFromNow from './time-from-now';
import Highlight from './base/highlight';
import { fuzzyMatchAll, isNotNullOrUndefined } from '../../common/misc';
import type {
HandleImportClipboardCallback,
HandleImportFileCallback,
HandleImportUriCallback,
WrapperProps,
} from './wrapper';
import Notice from './notice';
import PageLayout from './page-layout';
import { ForceToWorkspaceKeys } from '../redux/modules/helpers';
import coreLogo from '../images/insomnia-core-logo.png';
import { parseApiSpec, ParsedApiSpec } from '../../common/api-specs';
import { RemoteWorkspacesDropdown } from './dropdowns/remote-workspaces-dropdown';
@@ -52,22 +48,16 @@ import { cloneGitRepository } from '../redux/modules/git';
import { MemClient } from '../../sync/git/mem-client';
import { SpaceDropdown } from './dropdowns/space-dropdown';
import { initializeLocalProjectAndMarkForSync } from '../../sync/vcs/initialize-project';
import { importClipBoard, importFile, importUri } from '../redux/modules/import';
import { ForceToWorkspace } from '../redux/modules/helpers';
interface RenderedCard {
card: ReactNode;
lastModifiedTimestamp?: number | null;
}
interface ReduxDispatchProps {
handleCreateWorkspace: typeof createWorkspace;
handleGitCloneWorkspace: typeof cloneGitRepository;
}
interface Props extends ReduxDispatchProps {
interface Props extends ReturnType<typeof mapDispatchToProps> {
wrapperProps: WrapperProps;
handleImportFile: HandleImportFileCallback;
handleImportUri: HandleImportUriCallback;
handleImportClipboard: HandleImportClipboardCallback;
}
interface State {
@@ -116,13 +106,13 @@ class WrapperHome extends PureComponent<Props, State> {
_handleImportFile() {
this.props.handleImportFile({
forceToWorkspace: ForceToWorkspaceKeys.new,
forceToWorkspace: ForceToWorkspace.new,
});
}
_handleImportClipBoard() {
this.props.handleImportClipboard({
forceToWorkspace: ForceToWorkspaceKeys.new,
forceToWorkspace: ForceToWorkspace.new,
});
}
@@ -134,7 +124,7 @@ class WrapperHome extends PureComponent<Props, State> {
placeholder: 'https://website.com/insomnia-import.json',
onComplete: uri => {
this.props.handleImportUri(uri, {
forceToWorkspace: ForceToWorkspaceKeys.new,
forceToWorkspace: ForceToWorkspace.new,
});
},
});
@@ -421,9 +411,22 @@ class WrapperHome extends PureComponent<Props, State> {
}
}
const mapDispatchToProps = (dispatch): ReduxDispatchProps => ({
handleCreateWorkspace: bindActionCreators(createWorkspace, dispatch),
handleGitCloneWorkspace: bindActionCreators(cloneGitRepository, dispatch),
});
const mapDispatchToProps = (dispatch) => {
const bound = bindActionCreators({
createWorkspace,
cloneGitRepository,
importFile,
importClipBoard,
importUri,
}, dispatch);
return ({
handleCreateWorkspace: bound.createWorkspace,
handleGitCloneWorkspace: bound.cloneGitRepository,
handleImportFile: bound.importFile,
handleImportUri: bound.importUri,
handleImportClipboard: bound.importClipBoard,
});
};
export default connect(null, mapDispatchToProps)(WrapperHome);

View File

@@ -4,17 +4,20 @@ import 'swagger-ui-react/swagger-ui.css';
import { showPrompt } from './modals';
import type { BaseModel } from '../../models';
import { AUTOBIND_CFG, getAppLongName, getAppName, getAppSynopsis } from '../../common/constants';
import type { HandleImportFileCallback, HandleImportUriCallback, WrapperProps } from './wrapper';
import type { WrapperProps } from './wrapper';
import { database as db } from '../../common/database';
import { ForceToWorkspaceKeys } from '../redux/modules/helpers';
import OnboardingContainer from './onboarding-container';
import { isWorkspace, WorkspaceScopeKeys } from '../../models/workspace';
import Analytics from './analytics';
import { bindActionCreators } from 'redux';
import { importFile, importUri } from '../redux/modules/import';
import { connect } from 'react-redux';
import { ForceToWorkspace } from '../redux/modules/helpers';
interface Props {
type ReduxProps = ReturnType<typeof mapDispatchToProps>;
interface Props extends ReduxProps {
wrapperProps: WrapperProps;
handleImportFile: HandleImportFileCallback;
handleImportUri: HandleImportUriCallback;
}
interface State {
@@ -64,9 +67,8 @@ class WrapperOnboarding extends PureComponent<Props, State> {
}
_handleImportFile() {
const { handleImportFile } = this.props;
handleImportFile({
forceToWorkspace: ForceToWorkspaceKeys.new,
this.props.handleImportFile({
forceToWorkspace: ForceToWorkspace.new,
forceToScope: WorkspaceScopeKeys.design,
});
}
@@ -80,7 +82,7 @@ class WrapperOnboarding extends PureComponent<Props, State> {
label: 'URI to Import',
onComplete: value => {
handleImportUri(value, {
forceToWorkspace: ForceToWorkspaceKeys.new,
forceToWorkspace: ForceToWorkspace.new,
forceToScope: WorkspaceScopeKeys.design,
});
},
@@ -160,4 +162,16 @@ class WrapperOnboarding extends PureComponent<Props, State> {
}
}
export default WrapperOnboarding;
const mapDispatchToProps = (dispatch) => {
const bound = bindActionCreators({
importFile,
importUri,
}, dispatch);
return ({
handleImportFile: bound.importFile,
handleImportUri: bound.importUri,
});
};
export default connect(null, mapDispatchToProps)(WrapperOnboarding);

View File

@@ -79,7 +79,6 @@ import type { GlobalActivity } from '../../common/constants';
import ProtoFilesModal from './modals/proto-files-modal';
import { GrpcDispatchModalWrapper } from '../context/grpc';
import WrapperMigration from './wrapper-migration';
import type { ImportOptions } from '../redux/modules/global';
import WrapperAnalytics from './wrapper-analytics';
import { HandleGetRenderContext, HandleRender } from '../../common/render';
import { RequestGroup } from '../../models/request-group';
@@ -134,10 +133,6 @@ export type WrapperProps = AppProps & {
gitVCS: GitVCS | null;
}
export type HandleImportFileCallback = (options?: ImportOptions) => void;
export type HandleImportClipboardCallback = (options?: ImportOptions) => void;
export type HandleImportUriCallback = (uri: string, options?: ImportOptions) => void;
interface State {
forceRefreshKey: number;
activeGitBranch: string;
@@ -302,18 +297,6 @@ class Wrapper extends PureComponent<WrapperProps, State> {
return models.settings.update(this.props.settings, { useBulkParametersEditor });
}
_handleImportFile(options?: ImportOptions) {
this.props.handleImportFileToWorkspace({ workspaceId: this.props.activeWorkspace?._id, ...options });
}
_handleImportUri(uri: string, options?: ImportOptions) {
this.props.handleImportUriToWorkspace(uri, { workspaceId: this.props.activeWorkspace?._id, ...options });
}
_handleImportClipBoard(options?: ImportOptions) {
this.props.handleImportClipBoardToWorkspace({ workspaceId: this.props.activeWorkspace?._id, ...options });
}
_handleSetActiveResponse(responseId: string | null) {
if (!this.props.activeRequest) {
console.warn('Tried to set active response when request not active');
@@ -756,9 +739,6 @@ class Wrapper extends PureComponent<WrapperProps, State> {
{(activity === ACTIVITY_HOME || !activeWorkspace) && (
<WrapperHome
wrapperProps={this.props}
handleImportFile={this._handleImportFile}
handleImportUri={this._handleImportUri}
handleImportClipboard={this._handleImportClipBoard}
/>
)}
@@ -792,7 +772,6 @@ class Wrapper extends PureComponent<WrapperProps, State> {
handleForceUpdateRequest={this._handleForceUpdateRequest}
handleForceUpdateRequestHeaders={this._handleForceUpdateRequestHeaders}
handleImport={this._handleImport}
handleImportFile={this._handleImportFile}
handleRequestCreate={this._handleCreateRequestInWorkspace}
handleRequestGroupCreate={this._handleCreateRequestGroupInWorkspace}
handleSendAndDownloadRequestWithActiveEnvironment={
@@ -827,11 +806,7 @@ class Wrapper extends PureComponent<WrapperProps, State> {
{activity === ACTIVITY_ANALYTICS && <WrapperAnalytics wrapperProps={this.props} />}
{(activity === ACTIVITY_ONBOARDING || activity === null) && (
<WrapperOnboarding
wrapperProps={this.props}
handleImportFile={this._handleImportFile}
handleImportUri={this._handleImportUri}
/>
<WrapperOnboarding wrapperProps={this.props} />
)}
</Fragment>
</Fragment>

View File

@@ -32,15 +32,12 @@ import CookiesModal from '../components/modals/cookies-modal';
import RequestSwitcherModal from '../components/modals/request-switcher-modal';
import SettingsModal, { TAB_INDEX_SHORTCUTS } from '../components/modals/settings-modal';
import {
importUri,
loadRequestStart,
loadRequestStop,
newCommand,
setActiveWorkspace,
setActiveActivity,
goToNextActivity,
importFile,
importClipBoard,
exportRequestsToFile,
} from '../redux/modules/global';
import { initialize } from '../redux/modules/entities';
@@ -124,6 +121,7 @@ import { WorkspaceMeta } from '../../models/workspace-meta';
import { Response } from '../../models/response';
import { RenderContextAndKeys } from '../../common/render';
import { RootState } from '../redux/modules';
import { importUri } from '../redux/modules/import';
export type AppProps = ReturnType<typeof mapStateToProps> & ReturnType<typeof mapDispatchToProps>;
@@ -1372,7 +1370,7 @@ class App extends PureComponent<AppProps, State> {
'drop',
async e => {
e.preventDefault();
const { activeWorkspace, handleImportUriToWorkspace } = this.props;
const { activeWorkspace, handleImportUri } = this.props;
if (!activeWorkspace) {
return;
@@ -1397,7 +1395,7 @@ class App extends PureComponent<AppProps, State> {
),
addCancel: true,
});
handleImportUriToWorkspace(uri, { workspaceId: activeWorkspace?._id });
handleImportUri(uri, { workspaceId: activeWorkspace?._id });
},
false,
);
@@ -1755,15 +1753,13 @@ function mapStateToProps(state: RootState) {
const mapDispatchToProps = (dispatch: Dispatch<Action<any>>) => {
const {
importUri: handleImportUriToWorkspace,
importUri: handleImportUri,
loadRequestStart: handleStartLoading,
loadRequestStop: handleStopLoading,
setActiveWorkspace: handleSetActiveWorkspace,
newCommand: handleCommand,
setActiveActivity: handleSetActiveActivity,
goToNextActivity: handleGoToNextActivity,
importFile: handleImportFileToWorkspace,
importClipBoard: handleImportClipBoardToWorkspace,
exportRequestsToFile: handleExportRequestsToFile,
initialize: handleInitializeEntities,
} = bindActionCreators({
@@ -1774,21 +1770,17 @@ const mapDispatchToProps = (dispatch: Dispatch<Action<any>>) => {
setActiveWorkspace,
setActiveActivity,
goToNextActivity,
importFile,
importClipBoard,
exportRequestsToFile,
initialize,
}, dispatch);
return {
handleCommand,
handleImportUriToWorkspace,
handleImportUri,
handleSetActiveWorkspace,
handleSetActiveActivity,
handleStartLoading,
handleStopLoading,
handleGoToNextActivity,
handleImportFileToWorkspace,
handleImportClipBoardToWorkspace,
handleExportRequestsToFile,
handleInitializeEntities,
handleMoveDoc: _moveDoc, // TODO this doesn't use dispatch.. it's unclear why it needs to be here.

View File

@@ -1,22 +1,22 @@
import { askToImportIntoWorkspace, ForceToWorkspaceKeys } from '../helpers';
import { askToImportIntoWorkspace, ForceToWorkspace } from '../helpers';
import * as modals from '../../../components/modals';
jest.mock('../../../components/modals');
describe('askToImportIntoWorkspace', () => {
it('should return null if no active workspace', () => {
const func = askToImportIntoWorkspace({ workspaceId: undefined, forceToWorkspace: ForceToWorkspaceKeys.new });
const func = askToImportIntoWorkspace({ workspaceId: undefined, forceToWorkspace: ForceToWorkspace.new });
expect(func()).toBeNull();
});
it('should return null if forcing to a new workspace', () => {
const func = askToImportIntoWorkspace({ workspaceId: 'id', forceToWorkspace: ForceToWorkspaceKeys.new });
const func = askToImportIntoWorkspace({ workspaceId: 'id', forceToWorkspace: ForceToWorkspace.new });
expect(func()).toBeNull();
});
it('should return id if forcing to a current workspace', () => {
const currentWorkspaceId = 'currentId';
const func = askToImportIntoWorkspace({ workspaceId: currentWorkspaceId, forceToWorkspace: ForceToWorkspaceKeys.current });
const func = askToImportIntoWorkspace({ workspaceId: currentWorkspaceId, forceToWorkspace: ForceToWorkspace.current });
expect(func()).toBe(currentWorkspaceId);
});

View File

@@ -1,16 +1,10 @@
import electron, { OpenDialogOptions } from 'electron';
import electron from 'electron';
import React, { Fragment } from 'react';
import { combineReducers, Dispatch } from 'redux';
import fs, { NoParamCallback } from 'fs';
import path from 'path';
import AskModal from '../../../ui/components/modals/ask-modal';
import moment from 'moment';
import {
ImportRawConfig,
ImportResult,
importRaw,
importUri as _importUri,
} from '../../../common/import';
import {
exportRequestsData,
exportRequestsHAR,
@@ -31,13 +25,11 @@ import SettingsModal, {
TAB_INDEX_THEMES,
} from '../../components/modals/settings-modal';
import install from '../../../plugins/install';
import type { ForceToWorkspace } from './helpers';
import { askToImportIntoWorkspace, askToSetWorkspaceScope } from './helpers';
import { createPlugin } from '../../../plugins/create';
import { reloadPlugins } from '../../../plugins';
import { setTheme } from '../../../plugins/misc';
import type { GlobalActivity } from '../../../common/constants';
import { isWorkspace, Workspace, WorkspaceScope } from '../../../models/workspace';
import { isWorkspace } from '../../../models/workspace';
import {
ACTIVITY_DEBUG,
ACTIVITY_HOME,
@@ -55,6 +47,7 @@ import { Request } from '../../../models/request';
import { Environment, isEnvironment } from '../../../models/environment';
import { BASE_SPACE_ID } from '../../../models/space';
import { unreachableCase } from 'ts-assert-unreachable';
import { importUri } from './import';
export const LOCALSTORAGE_PREFIX = 'insomnia::meta';
const LOGIN_STATE_CHANGE = 'global/login-state-change';
@@ -197,7 +190,7 @@ export const newCommand = (command: string, args: any) => async (dispatch: Dispa
),
addCancel: true,
});
dispatch(importUri(args.uri, { workspaceId: args.workspaceId }));
dispatch(importUri(args.uri, { workspaceId: args.workspaceId, forceToSpace: 'prompt' }));
break;
case COMMAND_PLUGIN_INSTALL:
@@ -389,141 +382,6 @@ export const setActiveWorkspace = (workspaceId: string | null) => {
};
};
export interface ImportOptions {
workspaceId?: string
forceToWorkspace?: ForceToWorkspace;
forceToScope?: WorkspaceScope;
}
export const importFile = (
{ workspaceId, forceToScope, forceToWorkspace }: ImportOptions = {},
) => async (dispatch: Dispatch) => {
dispatch(loadStart());
const options: 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 electron.remote.dialog.showOpenDialog(options);
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 options: ImportRawConfig = {
getWorkspaceScope: askToSetWorkspaceScope(forceToScope),
getWorkspaceId: askToImportIntoWorkspace({ workspaceId, forceToWorkspace }),
};
const result = await _importUri(uri, options);
handleImportResult(result, 'The file does not contain a valid specification.');
} catch (err) {
showModal(AlertModal, {
title: 'Import Failed',
message: err + '',
});
} finally {
dispatch(loadStop());
}
}
};
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[]) || [];
};
export const importClipBoard = (
{ forceToScope, forceToWorkspace, workspaceId }: ImportOptions = {},
) => async (dispatch: Dispatch) => {
dispatch(loadStart());
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 options: ImportRawConfig = {
getWorkspaceScope: askToSetWorkspaceScope(forceToScope),
getWorkspaceId: askToImportIntoWorkspace({ workspaceId, forceToWorkspace }),
};
const result = await importRaw(schema, options);
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());
}
};
export const importUri = (
uri: string,
{ forceToScope, forceToWorkspace, workspaceId }: ImportOptions = {},
) => async (dispatch: Dispatch) => {
dispatch(loadStart());
try {
const options: ImportRawConfig = {
getWorkspaceScope: askToSetWorkspaceScope(forceToScope),
getWorkspaceId: askToImportIntoWorkspace({ workspaceId, forceToWorkspace }),
};
const result = await _importUri(uri, options);
handleImportResult(result, 'The URI does not contain a valid specification.');
} catch (err) {
showModal(AlertModal, {
title: 'Import Failed',
message: err + '',
});
} finally {
dispatch(loadStop());
}
};
const VALUE_JSON = 'json';
const VALUE_YAML = 'yaml';
const VALUE_HAR = 'har';

View File

@@ -1,14 +1,14 @@
import { showModal } from '../../components/modals';
import AskModal from '../../components/modals/ask-modal';
import { WorkspaceScope, WorkspaceScopeKeys } from '../../../models/workspace';
import { ValueOf } from 'type-fest';
import { showSelectModal } from '../../components/modals/select-modal';
import { BASE_SPACE_ID, Space } from '../../../models/space';
import { getAppName } from '../../../common/constants';
export const ForceToWorkspaceKeys = {
new: 'new',
current: 'current',
} as const;
export type ForceToWorkspace = ValueOf<typeof ForceToWorkspaceKeys>;
export enum ForceToWorkspace {
new = 'new',
current = 'current'
}
export type ImportToWorkspacePrompt = () => null | string | Promise<null | string>;
export function askToImportIntoWorkspace({ workspaceId, forceToWorkspace }: { workspaceId?: string; forceToWorkspace?: ForceToWorkspace; }): ImportToWorkspacePrompt {
@@ -18,10 +18,10 @@ export function askToImportIntoWorkspace({ workspaceId, forceToWorkspace }: { wo
}
switch (forceToWorkspace) {
case ForceToWorkspaceKeys.new:
case ForceToWorkspace.new:
return null;
case ForceToWorkspaceKeys.current:
case ForceToWorkspace.current:
return workspaceId;
default:
@@ -63,3 +63,30 @@ export function askToSetWorkspaceScope(scope?: WorkspaceScope): SetWorkspaceScop
}
};
}
export type SetSpaceIdPrompt = () => Promise<string | null>;
export function askToImportIntoSpace({ spaces, activeSpace }: { spaces: Space[]; activeSpace?: Space; }): SetSpaceIdPrompt {
return function() {
return new Promise(resolve => {
// If no spaces exist, return null (indicating no parent/space)
if (spaces.length === 0) {
return resolve(null);
}
const options = [{ name: getAppName(), value: BASE_SPACE_ID }, ...spaces.map(space => ({ name: space.name, value: space._id }))];
const defaultValue = activeSpace?._id || options[0].value;
showSelectModal({
title: 'Import',
message: 'Select a space to import into',
options,
value: defaultValue,
noEscape: true,
onDone: selectedSpaceId => {
resolve(selectedSpaceId === BASE_SPACE_ID ? null : selectedSpaceId);
},
});
});
};
}

View File

@@ -0,0 +1,162 @@
import electron, { OpenDialogOptions } from 'electron';
import { AnyAction } from 'redux';
import {
ImportRawConfig,
ImportResult,
importRaw,
importUri as _importUri,
} from '../../../common/import';
import { WorkspaceScope, Workspace } from '../../../models/workspace';
import { showModal, showError } from '../../components/modals';
import AlertModal from '../../components/modals/alert-modal';
import { loadStart, loadStop } from './global';
import { ForceToWorkspace, askToSetWorkspaceScope, askToImportIntoWorkspace, askToImportIntoSpace } from './helpers';
import * as models from '../../../models';
import { RootState } from '.';
import { selectActiveSpace, selectSpaces } from '../selectors';
import { ThunkAction } from 'redux-thunk';
export interface ImportOptions {
workspaceId?: string;
forceToSpace?: 'active' | 'prompt';
forceToWorkspace?: ForceToWorkspace;
forceToScope?: WorkspaceScope;
}
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,
forceToSpace,
}: ImportOptions,
state: RootState): ImportRawConfig => {
const activeSpace = selectActiveSpace(state);
const spaces = selectSpaces(state);
return ({
getWorkspaceScope: askToSetWorkspaceScope(forceToScope),
getWorkspaceId: askToImportIntoWorkspace({ workspaceId, forceToWorkspace }),
// Currently, just return the active space instead of prompting for which space to import into
getSpaceId: forceToSpace === 'prompt' ? askToImportIntoSpace({ spaces, activeSpace }) : () => Promise.resolve(activeSpace?._id || null),
});
};
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 electron.remote.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());
}
}
};
export const importClipBoard = (
options: ImportOptions = {},
): ThunkAction<void, RootState, void, AnyAction> => async (dispatch, getState) => {
dispatch(loadStart());
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 = 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());
}
};
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());
}
};