diff --git a/packages/insomnia-app/app/common/export.ts b/packages/insomnia-app/app/common/export.ts index 5b6e764b96..42f36b718b 100644 --- a/packages/insomnia-app/app/common/export.ts +++ b/packages/insomnia-app/app/common/export.ts @@ -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; diff --git a/packages/insomnia-app/app/common/import.ts b/packages/insomnia-app/app/common/import.ts index 9afeac27cb..356614fcdf 100644 --- a/packages/insomnia-app/app/common/import.ts +++ b/packages/insomnia-app/app/common/import.ts @@ -39,6 +39,7 @@ interface ConvertResult { export interface ImportRawConfig { getWorkspaceId: ImportToWorkspacePrompt; + getSpaceId?: () => Promise; 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, +) { + 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) => ( id === 'openapi3' || id === 'swagger2' ); diff --git a/packages/insomnia-app/app/ui/components/modals/select-modal.tsx b/packages/insomnia-app/app/ui/components/modals/select-modal.tsx index 9a7168ae14..85c2250fb1 100644 --- a/packages/insomnia-app/app/ui/components/modals/select-modal.tsx +++ b/packages/insomnia-app/app/ui/components/modals/select-modal.tsx @@ -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; + onDone?: (selectedValue: string | null) => void | Promise; 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 ( - + {title || 'Confirm?'}

{message}

@@ -88,3 +93,5 @@ export class SelectModal extends PureComponent<{}, SelectModalShowOptions> { ); } } + +export const showSelectModal = (opts: SelectModalShowOptions) => showModal(SelectModal, opts); diff --git a/packages/insomnia-app/app/ui/components/panes/placeholder-request-pane.tsx b/packages/insomnia-app/app/ui/components/panes/placeholder-request-pane.tsx index 1da9abfdea..e17567c503 100644 --- a/packages/insomnia-app/app/ui/components/panes/placeholder-request-pane.tsx +++ b/packages/insomnia-app/app/ui/components/panes/placeholder-request-pane.tsx @@ -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 = ({ +const PlaceholderRequestPane: FC = ({ hotKeyRegistry, - handleImportFile, handleCreateRequest, -}) => ( - - - -
- - - - - - - - - - - - - - - -
New Request - - - -
Switch Requests - - - -
Edit Environments - - - -
+}) => { + const dispatch = useDispatch(); + const workspaceId = useSelector(selectActiveWorkspace)?._id; + const handleImportFile = useCallback(() => dispatch(importFile({ workspaceId })), [workspaceId, dispatch]); -
- {/* @ts-expect-error -- TSCONVERSION event not used */} - - + return ( + + + +
+ + + + + + + + + + + + + + + +
New Request + + + +
Switch Requests + + + +
Edit Environments + + + +
+ +
+ + +
-
- - -); + + + ); +}; export default PlaceholderRequestPane; diff --git a/packages/insomnia-app/app/ui/components/panes/request-pane.tsx b/packages/insomnia-app/app/ui/components/panes/request-pane.tsx index 4a2fc8ef48..8fa15fc08c 100644 --- a/packages/insomnia-app/app/ui/components/panes/request-pane.tsx +++ b/packages/insomnia-app/app/ui/components/panes/request-pane.tsx @@ -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; handleImport: Function; - handleImportFile: HandleImportFileCallback; workspace: Workspace; settings: Settings; isVariableUncovered: boolean; @@ -132,7 +130,6 @@ class RequestPane extends PureComponent { handleGenerateCode, handleGetRenderContext, handleImport, - handleImportFile, handleCreateRequest, handleRender, handleSend, @@ -161,7 +158,6 @@ class RequestPane extends PureComponent { return ( ); diff --git a/packages/insomnia-app/app/ui/components/settings/import-export.tsx b/packages/insomnia-app/app/ui/components/settings/import-export.tsx index 0f2a23018c..3fc9203577 100644 --- a/packages/insomnia-app/app/ui/components/settings/import-export.tsx +++ b/packages/insomnia-app/app/ui/components/settings/import-export.tsx @@ -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; diff --git a/packages/insomnia-app/app/ui/components/wrapper-debug.tsx b/packages/insomnia-app/app/ui/components/wrapper-debug.tsx index fba421c304..272ccf50f9 100644 --- a/packages/insomnia-app/app/ui/components/wrapper-debug.tsx +++ b/packages/insomnia-app/app/ui/components/wrapper-debug.tsx @@ -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) => Promise; handleForceUpdateRequestHeaders: (r: Request, headers: RequestHeader[]) => Promise; handleImport: Function; - handleImportFile: HandleImportFileCallback; handleRequestCreate: () => void; handleRequestGroupCreate: () => void; handleSendAndDownloadRequestWithActiveEnvironment: (filepath?: string) => Promise; @@ -196,7 +195,6 @@ class WrapperDebug extends PureComponent { handleForceUpdateRequest, handleForceUpdateRequestHeaders, handleImport, - handleImportFile, handleSendAndDownloadRequestWithActiveEnvironment, handleSendRequestWithActiveEnvironment, handleUpdateRequestAuthentication, @@ -261,7 +259,6 @@ class WrapperDebug extends PureComponent { handleGenerateCode={handleGenerateCodeForActiveRequest} handleGetRenderContext={handleGetRenderContext} handleImport={handleImport} - handleImportFile={handleImportFile} handleRender={handleRender} handleSend={handleSendRequestWithActiveEnvironment} handleSendAndDownload={handleSendAndDownloadRequestWithActiveEnvironment} diff --git a/packages/insomnia-app/app/ui/components/wrapper-home.tsx b/packages/insomnia-app/app/ui/components/wrapper-home.tsx index 72df63fa70..7684b6ca5a 100644 --- a/packages/insomnia-app/app/ui/components/wrapper-home.tsx +++ b/packages/insomnia-app/app/ui/components/wrapper-home.tsx @@ -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 { wrapperProps: WrapperProps; - handleImportFile: HandleImportFileCallback; - handleImportUri: HandleImportUriCallback; - handleImportClipboard: HandleImportClipboardCallback; } interface State { @@ -116,13 +106,13 @@ class WrapperHome extends PureComponent { _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 { 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 { } } -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); diff --git a/packages/insomnia-app/app/ui/components/wrapper-onboarding.tsx b/packages/insomnia-app/app/ui/components/wrapper-onboarding.tsx index fbc83bbfbf..62f56b00ac 100644 --- a/packages/insomnia-app/app/ui/components/wrapper-onboarding.tsx +++ b/packages/insomnia-app/app/ui/components/wrapper-onboarding.tsx @@ -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; + +interface Props extends ReduxProps { wrapperProps: WrapperProps; - handleImportFile: HandleImportFileCallback; - handleImportUri: HandleImportUriCallback; } interface State { @@ -64,9 +67,8 @@ class WrapperOnboarding extends PureComponent { } _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 { 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 { } } -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); diff --git a/packages/insomnia-app/app/ui/components/wrapper.tsx b/packages/insomnia-app/app/ui/components/wrapper.tsx index a73394fcc5..1cebebc591 100644 --- a/packages/insomnia-app/app/ui/components/wrapper.tsx +++ b/packages/insomnia-app/app/ui/components/wrapper.tsx @@ -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 { 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 { {(activity === ACTIVITY_HOME || !activeWorkspace) && ( )} @@ -792,7 +772,6 @@ class Wrapper extends PureComponent { 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 { {activity === ACTIVITY_ANALYTICS && } {(activity === ACTIVITY_ONBOARDING || activity === null) && ( - + )} diff --git a/packages/insomnia-app/app/ui/containers/app.tsx b/packages/insomnia-app/app/ui/containers/app.tsx index 2de73fa3c6..08c9e45084 100644 --- a/packages/insomnia-app/app/ui/containers/app.tsx +++ b/packages/insomnia-app/app/ui/containers/app.tsx @@ -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 & ReturnType; @@ -1372,7 +1370,7 @@ class App extends PureComponent { '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 { ), 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>) => { 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>) => { 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. diff --git a/packages/insomnia-app/app/ui/redux/modules/__tests__/helpers.test.ts b/packages/insomnia-app/app/ui/redux/modules/__tests__/helpers.test.ts index 976b85e97c..d87a0a3eef 100644 --- a/packages/insomnia-app/app/ui/redux/modules/__tests__/helpers.test.ts +++ b/packages/insomnia-app/app/ui/redux/modules/__tests__/helpers.test.ts @@ -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); }); diff --git a/packages/insomnia-app/app/ui/redux/modules/global.tsx b/packages/insomnia-app/app/ui/redux/modules/global.tsx index 61765dd17c..feecc48606 100644 --- a/packages/insomnia-app/app/ui/redux/modules/global.tsx +++ b/packages/insomnia-app/app/ui/redux/modules/global.tsx @@ -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'; diff --git a/packages/insomnia-app/app/ui/redux/modules/helpers.ts b/packages/insomnia-app/app/ui/redux/modules/helpers.ts index 7eab7e500b..62a457014e 100644 --- a/packages/insomnia-app/app/ui/redux/modules/helpers.ts +++ b/packages/insomnia-app/app/ui/redux/modules/helpers.ts @@ -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; +export enum ForceToWorkspace { + new = 'new', + current = 'current' +} export type ImportToWorkspacePrompt = () => null | string | Promise; 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; +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); + }, + }); + }); + }; +} diff --git a/packages/insomnia-app/app/ui/redux/modules/import.ts b/packages/insomnia-app/app/ui/redux/modules/import.ts new file mode 100644 index 0000000000..5c885c687c --- /dev/null +++ b/packages/insomnia-app/app/ui/redux/modules/import.ts @@ -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 => 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 => 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 => 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()); + } +};