From 9be7e05000a35b44e84fde8b05fb0240d660724b Mon Sep 17 00:00:00 2001 From: Jack Kavanagh Date: Mon, 3 Oct 2022 17:24:38 +0200 Subject: [PATCH] app tsx fc (#5235) * fc rebase * extract render listeners from app * organise global shortcuts * show logo in index.html * import listeners * add loading indicator and split shortcuts * Simplify GitVCS/VCS instance creation Co-authored-by: gatzjames --- .../ui/components/codemirror/code-editor.tsx | 4 +- .../dropdowns/environments-dropdown.tsx | 4 +- .../dropdowns/response-history-dropdown.tsx | 4 +- .../editors/body/graph-ql-editor.tsx | 4 +- .../graph-ql-explorer/graph-ql-explorer.tsx | 4 +- .../src/ui/components/keydown-binder.ts | 2 +- .../modals/request-switcher-modal.tsx | 4 +- .../panes/grpc-request-pane/index.tsx | 4 +- .../src/ui/components/request-url-bar.tsx | 4 +- .../ui/components/sidebar/sidebar-filter.tsx | 4 +- .../ui/components/viewers/response-viewer.tsx | 4 +- .../ui/components/websockets/action-bar.tsx | 4 +- .../src/ui/components/wrapper-debug.tsx | 149 +++- .../src/ui/components/wrapper-home.tsx | 4 +- .../insomnia/src/ui/components/wrapper.tsx | 10 +- .../insomnia/src/ui/containers/app-hooks.tsx | 143 +--- packages/insomnia/src/ui/containers/app.tsx | 722 ++++++------------ .../src/ui/hooks/use-document-title.ts | 33 + .../ui/hooks/use-global-keyboard-shortcuts.ts | 34 + packages/insomnia/src/ui/index.tsx | 26 +- packages/insomnia/src/ui/rendererListeners.ts | 98 +++ 21 files changed, 586 insertions(+), 679 deletions(-) create mode 100644 packages/insomnia/src/ui/hooks/use-document-title.ts create mode 100644 packages/insomnia/src/ui/hooks/use-global-keyboard-shortcuts.ts create mode 100644 packages/insomnia/src/ui/rendererListeners.ts diff --git a/packages/insomnia/src/ui/components/codemirror/code-editor.tsx b/packages/insomnia/src/ui/components/codemirror/code-editor.tsx index de82e3c60..75b35454f 100644 --- a/packages/insomnia/src/ui/components/codemirror/code-editor.tsx +++ b/packages/insomnia/src/ui/components/codemirror/code-editor.tsx @@ -29,7 +29,7 @@ import { selectSettings } from '../../redux/selectors'; import { Dropdown } from '../base/dropdown/dropdown'; import { DropdownButton } from '../base/dropdown/dropdown-button'; import { DropdownItem } from '../base/dropdown/dropdown-item'; -import { useGlobalKeyboardShortcuts } from '../keydown-binder'; +import { useDocBodyKeyboardShortcuts } from '../keydown-binder'; import { FilterHelpModal } from '../modals/filter-help-modal'; import { showModal } from '../modals/index'; import { isKeyCombinationInRegistry } from '../settings/shortcuts'; @@ -188,7 +188,7 @@ const CodeEditorFCWithRef: ForwardRefRenderFunction { const editorRef = useRef(null); - useGlobalKeyboardShortcuts({ + useDocBodyKeyboardShortcuts({ beautifyRequestBody: () => editorRef.current?._prettify(), }); diff --git a/packages/insomnia/src/ui/components/dropdowns/environments-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/environments-dropdown.tsx index 54cc0b9c3..c8b683280 100644 --- a/packages/insomnia/src/ui/components/dropdowns/environments-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/environments-dropdown.tsx @@ -10,7 +10,7 @@ import { DropdownButton } from '../base/dropdown/dropdown-button'; import { DropdownDivider } from '../base/dropdown/dropdown-divider'; import { DropdownHint } from '../base/dropdown/dropdown-hint'; import { DropdownItem } from '../base/dropdown/dropdown-item'; -import { useGlobalKeyboardShortcuts } from '../keydown-binder'; +import { useDocBodyKeyboardShortcuts } from '../keydown-binder'; import { showModal } from '../modals/index'; import { WorkspaceEnvironmentsEditModal } from '../modals/workspace-environments-edit-modal'; import { Tooltip } from '../tooltip'; @@ -39,7 +39,7 @@ export const EnvironmentsDropdown: FC = ({ dropdownRef.current?.toggle(true); }, []); - useGlobalKeyboardShortcuts({ + useDocBodyKeyboardShortcuts({ environment_showSwitchMenu: toggleSwitchMenu, }); diff --git a/packages/insomnia/src/ui/components/dropdowns/response-history-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/response-history-dropdown.tsx index 41edb48fd..d06d27aac 100644 --- a/packages/insomnia/src/ui/components/dropdowns/response-history-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/response-history-dropdown.tsx @@ -13,7 +13,7 @@ import { DropdownButton } from '../base/dropdown/dropdown-button'; import { DropdownDivider } from '../base/dropdown/dropdown-divider'; import { DropdownItem } from '../base/dropdown/dropdown-item'; import { PromptButton } from '../base/prompt-button'; -import { useGlobalKeyboardShortcuts } from '../keydown-binder'; +import { useDocBodyKeyboardShortcuts } from '../keydown-binder'; import { SizeTag } from '../tags/size-tag'; import { StatusTag } from '../tags/status-tag'; import { TimeTag } from '../tags/time-tag'; @@ -163,7 +163,7 @@ export const ResponseHistoryDropdown = dropdownRef.current?.toggle(true), }); diff --git a/packages/insomnia/src/ui/components/editors/body/graph-ql-editor.tsx b/packages/insomnia/src/ui/components/editors/body/graph-ql-editor.tsx index 3ff084a2b..09a72b27b 100644 --- a/packages/insomnia/src/ui/components/editors/body/graph-ql-editor.tsx +++ b/packages/insomnia/src/ui/components/editors/body/graph-ql-editor.tsx @@ -32,7 +32,7 @@ import { CodeEditor } from '../../codemirror/code-editor'; import { GraphQLExplorer } from '../../graph-ql-explorer/graph-ql-explorer'; import { ActiveReference } from '../../graph-ql-explorer/graph-ql-types'; import { HelpTooltip } from '../../help-tooltip'; -import { useGlobalKeyboardShortcuts } from '../../keydown-binder'; +import { useDocBodyKeyboardShortcuts } from '../../keydown-binder'; import { TimeFromNow } from '../../time-from-now'; const explorerContainer = document.querySelector('#graphql-explorer-container'); @@ -317,7 +317,7 @@ export const GraphQLEditor: FC = ({ } }; - useGlobalKeyboardShortcuts({ + useDocBodyKeyboardShortcuts({ beautifyRequestBody: _handlePrettify, }); diff --git a/packages/insomnia/src/ui/components/graph-ql-explorer/graph-ql-explorer.tsx b/packages/insomnia/src/ui/components/graph-ql-explorer/graph-ql-explorer.tsx index 488c5a21a..5d4d787ed 100644 --- a/packages/insomnia/src/ui/components/graph-ql-explorer/graph-ql-explorer.tsx +++ b/packages/insomnia/src/ui/components/graph-ql-explorer/graph-ql-explorer.tsx @@ -3,7 +3,7 @@ import { GraphQLEnumType, GraphQLField, GraphQLNamedType, GraphQLSchema, GraphQL import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; import { DebouncedInput } from '../base/debounced-input'; -import { useGlobalKeyboardShortcuts } from '../keydown-binder'; +import { useDocBodyKeyboardShortcuts } from '../keydown-binder'; import { GraphQLExplorerEnum } from './graph-ql-explorer-enum'; import { GraphQLExplorerField } from './graph-ql-explorer-field'; import { GraphQLExplorerSchema } from './graph-ql-explorer-schema'; @@ -131,7 +131,7 @@ export const GraphQLExplorer: FC = ({ schema, handleClose, visible, refer }); }; - useGlobalKeyboardShortcuts({ + useDocBodyKeyboardShortcuts({ graphql_explorer_focus_filter: () => { setState(state => ({ ...state, diff --git a/packages/insomnia/src/ui/components/keydown-binder.ts b/packages/insomnia/src/ui/components/keydown-binder.ts index 6af9fbb24..e9445784a 100644 --- a/packages/insomnia/src/ui/components/keydown-binder.ts +++ b/packages/insomnia/src/ui/components/keydown-binder.ts @@ -36,7 +36,7 @@ export function useKeyboardShortcuts(getTarget: () => HTMLElement, listeners: { }, [hotKeyRegistry, listeners, getTarget]); } -export function useGlobalKeyboardShortcuts(listeners: { [key in KeyboardShortcut]?: (event: KeyboardEvent) => any }) { +export function useDocBodyKeyboardShortcuts(listeners: { [key in KeyboardShortcut]?: (event: KeyboardEvent) => any }) { useKeyboardShortcuts(() => document.body, listeners); } diff --git a/packages/insomnia/src/ui/components/modals/request-switcher-modal.tsx b/packages/insomnia/src/ui/components/modals/request-switcher-modal.tsx index 1002a666c..49c31fd71 100644 --- a/packages/insomnia/src/ui/components/modals/request-switcher-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/request-switcher-modal.tsx @@ -19,7 +19,7 @@ import { Highlight } from '../base/highlight'; import { Modal, ModalHandle, ModalProps } from '../base/modal'; import { ModalBody } from '../base/modal-body'; import { ModalHeader } from '../base/modal-header'; -import { createKeybindingsHandler, useGlobalKeyboardShortcuts } from '../keydown-binder'; +import { createKeybindingsHandler, useDocBodyKeyboardShortcuts } from '../keydown-binder'; import { GrpcTag } from '../tags/grpc-tag'; import { MethodTag } from '../tags/method-tag'; import { WebSocketTag } from '../tags/websocket-tag'; @@ -294,7 +294,7 @@ export const RequestSwitcherModal = forwardRef { if (state.isModalVisible) { setState(state => ({ diff --git a/packages/insomnia/src/ui/components/panes/grpc-request-pane/index.tsx b/packages/insomnia/src/ui/components/panes/grpc-request-pane/index.tsx index 56ee74632..5983ffdb3 100644 --- a/packages/insomnia/src/ui/components/panes/grpc-request-pane/index.tsx +++ b/packages/insomnia/src/ui/components/panes/grpc-request-pane/index.tsx @@ -16,7 +16,7 @@ import { OneLineEditor } from '../../codemirror/one-line-editor'; import { GrpcMethodDropdown } from '../../dropdowns/grpc-method-dropdown/grpc-method-dropdown'; import { ErrorBoundary } from '../../error-boundary'; import { KeyValueEditor } from '../../key-value-editor/key-value-editor'; -import { useGlobalKeyboardShortcuts } from '../../keydown-binder'; +import { useDocBodyKeyboardShortcuts } from '../../keydown-binder'; import { GrpcTabbedMessages } from '../../viewers/grpc-tabbed-messages'; import { EmptyStatePane } from '../empty-state-pane'; import { Pane, PaneBody, PaneHeader } from '../pane'; @@ -78,7 +78,7 @@ export const GrpcRequestPane: FunctionComponent = ({ } }, [method, running, start]); - useGlobalKeyboardShortcuts({ + useDocBodyKeyboardShortcuts({ request_send: handleRequestSend, }); diff --git a/packages/insomnia/src/ui/components/request-url-bar.tsx b/packages/insomnia/src/ui/components/request-url-bar.tsx index 2d58eb2ed..f9dec3bf9 100644 --- a/packages/insomnia/src/ui/components/request-url-bar.tsx +++ b/packages/insomnia/src/ui/components/request-url-bar.tsx @@ -24,7 +24,7 @@ import { DropdownItem } from './base/dropdown/dropdown-item'; import { PromptButton } from './base/prompt-button'; import { OneLineEditor } from './codemirror/one-line-editor'; import { MethodDropdown } from './dropdowns/method-dropdown'; -import { createKeybindingsHandler, useGlobalKeyboardShortcuts } from './keydown-binder'; +import { createKeybindingsHandler, useDocBodyKeyboardShortcuts } from './keydown-binder'; import { GenerateCodeModal } from './modals/generate-code-modal'; import { showAlert, showModal, showPrompt } from './modals/index'; import { RequestRenderErrorModal } from './modals/request-render-error-modal'; @@ -275,7 +275,7 @@ export const RequestUrlBar = forwardRef(({ }, [request._id]); const handleClearDownloadLocation = () => updateRequestMetaByParentId(request._id, { downloadPath: null }); - useGlobalKeyboardShortcuts({ + useDocBodyKeyboardShortcuts({ request_focusUrl: () => { inputRef.current?.focus(); inputRef.current?.selectAll(); diff --git a/packages/insomnia/src/ui/components/sidebar/sidebar-filter.tsx b/packages/insomnia/src/ui/components/sidebar/sidebar-filter.tsx index 6277e84cd..b1e4b8064 100644 --- a/packages/insomnia/src/ui/components/sidebar/sidebar-filter.tsx +++ b/packages/insomnia/src/ui/components/sidebar/sidebar-filter.tsx @@ -7,7 +7,7 @@ import { sortMethodMap } from '../../../common/sorting'; import * as models from '../../../models'; import { isRequestGroup } from '../../../models/request-group'; import { selectActiveWorkspace, selectActiveWorkspaceMeta } from '../../redux/selectors'; -import { useGlobalKeyboardShortcuts } from '../keydown-binder'; +import { useDocBodyKeyboardShortcuts } from '../keydown-binder'; import { SidebarCreateDropdown } from './sidebar-create-dropdown'; import { SidebarSortDropdown } from './sidebar-sort-dropdown'; @@ -36,7 +36,7 @@ export const SidebarFilter: FC = ({ filter }) => { } }, [activeWorkspaceMeta]); - useGlobalKeyboardShortcuts({ + useDocBodyKeyboardShortcuts({ sidebar_focusFilter: () => { inputRef.current?.focus(); }, diff --git a/packages/insomnia/src/ui/components/viewers/response-viewer.tsx b/packages/insomnia/src/ui/components/viewers/response-viewer.tsx index d954f4f28..471d5d18e 100644 --- a/packages/insomnia/src/ui/components/viewers/response-viewer.tsx +++ b/packages/insomnia/src/ui/components/viewers/response-viewer.tsx @@ -16,7 +16,7 @@ import { import { clickLink } from '../../../common/electron-helpers'; import { xmlDecode } from '../../../common/misc'; import { CodeEditor, UnconnectedCodeEditor } from '../codemirror/code-editor'; -import { useGlobalKeyboardShortcuts } from '../keydown-binder'; +import { useDocBodyKeyboardShortcuts } from '../keydown-binder'; import { ResponseCSVViewer } from './response-csv-viewer'; import { ResponseErrorViewer } from './response-error-viewer'; import { ResponseMultipartViewer } from './response-multipart-viewer'; @@ -122,7 +122,7 @@ export const ResponseViewer = ({ ); }; - useGlobalKeyboardShortcuts({ + useDocBodyKeyboardShortcuts({ response_focus: () => { if (!_isViewSelectable()) { return; diff --git a/packages/insomnia/src/ui/components/websockets/action-bar.tsx b/packages/insomnia/src/ui/components/websockets/action-bar.tsx index f5522f0fd..5813a7656 100644 --- a/packages/insomnia/src/ui/components/websockets/action-bar.tsx +++ b/packages/insomnia/src/ui/components/websockets/action-bar.tsx @@ -7,7 +7,7 @@ import * as models from '../../../models'; import { WebSocketRequest } from '../../../models/websocket-request'; import { ReadyState } from '../../context/websocket-client/use-ws-ready-state'; import { OneLineEditor } from '../codemirror/one-line-editor'; -import { useGlobalKeyboardShortcuts } from '../keydown-binder'; +import { useDocBodyKeyboardShortcuts } from '../keydown-binder'; import { showAlert, showModal } from '../modals'; import { RequestRenderErrorModal } from '../modals/request-render-error-modal'; @@ -118,7 +118,7 @@ export const WebSocketActionBar: FC = ({ request, workspaceId, e } }, [environmentId, isOpen, request, workspaceId]); - useGlobalKeyboardShortcuts({ + useDocBodyKeyboardShortcuts({ request_send: () => handleSubmit(), request_focusUrl: () => { editorRef.current?.focus(); diff --git a/packages/insomnia/src/ui/components/wrapper-debug.tsx b/packages/insomnia/src/ui/components/wrapper-debug.tsx index 0b52cf2be..4ca9a4a38 100644 --- a/packages/insomnia/src/ui/components/wrapper-debug.tsx +++ b/packages/insomnia/src/ui/components/wrapper-debug.tsx @@ -1,16 +1,24 @@ import React, { FC, Fragment, ReactNode, useEffect } from 'react'; import { useSelector } from 'react-redux'; +import { SegmentEvent, trackSegmentEvent } from '../../common/analytics'; +import * as models from '../../models'; import { isGrpcRequest } from '../../models/grpc-request'; +import { getByParentId as getGrpcRequestMetaByParentId } from '../../models/grpc-request-meta'; +import * as requestOperations from '../../models/helpers/request-operations'; import { isRemoteProject } from '../../models/project'; +import { getByParentId as getRequestMetaByParentId } from '../../models/request-meta'; import { isWebSocketRequest } from '../../models/websocket-request'; import { isCollection, isDesign } from '../../models/workspace'; import { VCS } from '../../sync/vcs/vcs'; +import { updateRequestMetaByParentId } from '../hooks/create-request'; +import { createRequestGroup } from '../hooks/create-request-group'; import { selectActiveEnvironment, selectActiveProject, selectActiveRequest, selectActiveWorkspace, + selectActiveWorkspaceMeta, selectIsLoggedIn, selectSettings, } from '../redux/selectors'; @@ -18,7 +26,15 @@ import { selectSidebarFilter } from '../redux/sidebar-selectors'; import { EnvironmentsDropdown } from './dropdowns/environments-dropdown'; import { SyncDropdown } from './dropdowns/sync-dropdown'; import { ErrorBoundary } from './error-boundary'; -import { showCookiesModal } from './modals/cookies-modal'; +import { useDocBodyKeyboardShortcuts } from './keydown-binder'; +import { showModal } from './modals'; +import { AskModal } from './modals/ask-modal'; +import { CookiesModal, showCookiesModal } from './modals/cookies-modal'; +import { GenerateCodeModal } from './modals/generate-code-modal'; +import { PromptModal } from './modals/prompt-modal'; +import { RequestSettingsModal } from './modals/request-settings-modal'; +import { RequestSwitcherModal } from './modals/request-switcher-modal'; +import { WorkspaceEnvironmentsEditModal } from './modals/workspace-environments-edit-modal'; import { PageLayout } from './page-layout'; import { GrpcRequestPane } from './panes/grpc-request-pane'; import { GrpcResponsePane } from './panes/grpc-response-pane'; @@ -37,7 +53,6 @@ interface Props { handleActivityChange: HandleActivityChange; handleSetActiveEnvironment: (id: string | null) => void; handleImport: Function; - handleSetResponseFilter: (filter: string) => void; vcs: VCS | null; } export const WrapperDebug: FC = ({ @@ -45,7 +60,6 @@ export const WrapperDebug: FC = ({ handleActivityChange, handleSetActiveEnvironment, handleImport, - handleSetResponseFilter, vcs, }) => { const activeProject = useSelector(selectActiveProject); @@ -56,15 +70,142 @@ export const WrapperDebug: FC = ({ const activeWorkspace = useSelector(selectActiveWorkspace); const settings = useSelector(selectSettings); const sidebarFilter = useSelector(selectSidebarFilter); + const activeWorkspaceMeta = useSelector(selectActiveWorkspaceMeta); const isTeamSync = isLoggedIn && activeWorkspace && isCollection(activeWorkspace) && isRemoteProject(activeProject) && vcs; - + useDocBodyKeyboardShortcuts({ + request_togglePin: + async () => { + if (activeRequest) { + const meta = isGrpcRequest(activeRequest) ? await getGrpcRequestMetaByParentId(activeRequest._id) : await getRequestMetaByParentId(activeRequest._id); + updateRequestMetaByParentId(activeRequest._id, { pinned: !meta?.pinned }); + } + }, + request_showSettings: + () => { + if (activeRequest) { + showModal(RequestSettingsModal, { request: activeRequest }); + } + }, + request_showDelete: + () => { + if (activeRequest) { + showModal(AskModal, { + title: 'Delete Request?', + message: `Really delete ${activeRequest.name}?`, + onDone: async (confirmed: boolean) => { + if (confirmed) { + await requestOperations.remove(activeRequest); + models.stats.incrementDeletedRequests(); + } + }, + }); + } + }, + request_showDuplicate: + () => { + if (activeRequest) { + showModal(PromptModal, { + title: 'Duplicate Request', + defaultValue: activeRequest.name, + submitName: 'Create', + label: 'New Name', + selectText: true, + onComplete: async (name: string) => { + const newRequest = await requestOperations.duplicate(activeRequest, { + name, + }); + if (activeWorkspaceMeta) { + await models.workspaceMeta.update(activeWorkspaceMeta, { activeRequestId: newRequest._id }); + } + await updateRequestMetaByParentId(newRequest._id, { + lastActive: Date.now(), + }); + models.stats.incrementCreatedRequests(); + }, + }); + } + }, + request_createHTTP: + async () => { + if (activeWorkspace) { + const parentId = activeRequest ? activeRequest.parentId : activeWorkspace._id; + const request = await models.request.create({ + parentId, + name: 'New Request', + }); + if (activeWorkspaceMeta) { + await models.workspaceMeta.update(activeWorkspaceMeta, { activeRequestId: request._id }); + } + await updateRequestMetaByParentId(request._id, { + lastActive: Date.now(), + }); + models.stats.incrementCreatedRequests(); + trackSegmentEvent(SegmentEvent.requestCreate, { requestType: 'HTTP' }); + } + }, + request_showCreateFolder: + () => { + if (activeWorkspace) { + createRequestGroup(activeRequest ? activeRequest.parentId : activeWorkspace._id); + } + }, + request_showRecent: + () => showModal(RequestSwitcherModal, { + disableInput: true, + maxRequests: 10, + maxWorkspaces: 0, + selectOnKeyup: true, + title: 'Recent Requests', + hideNeverActiveRequests: true, + // Add an open delay so the dialog won't show for quick presses + openDelay: 150, + }), + request_quickSwitch: + () => showModal(RequestSwitcherModal), + environment_showEditor: + () => showModal(WorkspaceEnvironmentsEditModal, activeWorkspace), + showCookiesEditor: + () => showModal(CookiesModal), + request_showGenerateCodeEditor: + () => showModal(GenerateCodeModal, activeRequest), + }); // Close all websocket connections when the active environment changes useEffect(() => { return () => { window.main.webSocket.closeAll(); }; }, [activeEnvironment?._id]); + + async function handleSetResponseFilter(responseFilter: string) { + if (!activeRequest) { + return; + } + const requestId = activeRequest._id; + await updateRequestMetaByParentId(requestId, { responseFilter }); + + const meta = await models.requestMeta.getByParentId(requestId); + if (!meta) { + return; + } + const responseFilterHistory = meta.responseFilterHistory.slice(0, 10); + + // Already in history? + if (responseFilterHistory.includes(responseFilter)) { + return; + } + + // Blank? + if (!responseFilter) { + return; + } + + responseFilterHistory.unshift(responseFilter); + await updateRequestMetaByParentId(requestId, { + responseFilterHistory, + }); + } + return ( = (({ vcs }) => { setFilter(event.currentTarget.value); }, []); - useGlobalKeyboardShortcuts({ + useDocBodyKeyboardShortcuts({ documents_filter: () => inputRef.current?.focus(), }); diff --git a/packages/insomnia/src/ui/components/wrapper.tsx b/packages/insomnia/src/ui/components/wrapper.tsx index cc09d4929..e46ad8acc 100644 --- a/packages/insomnia/src/ui/components/wrapper.tsx +++ b/packages/insomnia/src/ui/components/wrapper.tsx @@ -131,7 +131,6 @@ const ActivityRouter = () => { const spectral = initializeSpectral(); export type Props = ReturnType & ReturnType & { - handleSetResponseFilter: Function; vcs: VCS | null; gitVCS: GitVCS | null; }; @@ -244,12 +243,6 @@ export class WrapperClass extends PureComponent { }, 1000); } - _handleSetResponseFilter(filter: string) { - const activeRequest = this.props.activeRequest; - const activeRequestId = activeRequest ? activeRequest._id : 'n/a'; - this.props.handleSetResponseFilter(activeRequestId, filter); - } - _handleGitBranchChanged(branch: string) { this.setState({ activeGitBranch: branch || 'no-vcs', @@ -343,7 +336,7 @@ export class WrapperClass extends PureComponent { registerModal(instance, 'SettingsModal')} /> registerModal(instance, 'ResponseDebugModal')} /> - registerModal(instance, 'RequestSwitcherModal')}/> + registerModal(instance, 'RequestSwitcherModal')} /> { handleActivityChange={this._handleWorkspaceActivityChange} handleSetActiveEnvironment={this._handleSetActiveEnvironment} handleImport={this._handleImport} - handleSetResponseFilter={this._handleSetResponseFilter} vcs={vcs} /> diff --git a/packages/insomnia/src/ui/containers/app-hooks.tsx b/packages/insomnia/src/ui/containers/app-hooks.tsx index cda15850f..5c34996b5 100644 --- a/packages/insomnia/src/ui/containers/app-hooks.tsx +++ b/packages/insomnia/src/ui/containers/app-hooks.tsx @@ -1,151 +1,12 @@ import { FC } from 'react'; -import { useSelector } from 'react-redux'; -import { SegmentEvent, trackSegmentEvent } from '../../common/analytics'; -import * as models from '../../models'; -import { isGrpcRequest } from '../../models/grpc-request'; -import { getByParentId as getGrpcRequestMetaByParentId } from '../../models/grpc-request-meta'; -import * as requestOperations from '../../models/helpers/request-operations'; -import { getByParentId as getRequestMetaByParentId } from '../../models/request-meta'; -import * as plugins from '../../plugins'; -import { useGlobalKeyboardShortcuts } from '../components/keydown-binder'; -import { showModal } from '../components/modals'; -import { AskModal } from '../components/modals/ask-modal'; -import { CookiesModal } from '../components/modals/cookies-modal'; -import { GenerateCodeModal } from '../components/modals/generate-code-modal'; -import { PromptModal } from '../components/modals/prompt-modal'; -import { RequestSettingsModal } from '../components/modals/request-settings-modal'; -import { RequestSwitcherModal } from '../components/modals/request-switcher-modal'; -import { SettingsModal, TAB_INDEX_SHORTCUTS } from '../components/modals/settings-modal'; -import { WorkspaceEnvironmentsEditModal } from '../components/modals/workspace-environments-edit-modal'; -import { WorkspaceSettingsModal } from '../components/modals/workspace-settings-modal'; -import { updateRequestMetaByParentId } from '../hooks/create-request'; -import { createRequestGroup } from '../hooks/create-request-group'; +import { useGlobalKeyboardShortcuts } from '../hooks/use-global-keyboard-shortcuts'; import { useSettingsSideEffects } from '../hooks/use-settings-side-effects'; import { useSyncMigration } from '../hooks/use-sync-migration'; -import { selectActiveRequest, selectActiveWorkspace, selectActiveWorkspaceMeta, selectSettings } from '../redux/selectors'; export const AppHooks: FC = () => { useSyncMigration(); useSettingsSideEffects(); - const activeRequest = useSelector(selectActiveRequest); - const activeWorkspace = useSelector(selectActiveWorkspace); - const activeWorkspaceMeta = useSelector(selectActiveWorkspaceMeta); - const settings = useSelector(selectSettings); - - useGlobalKeyboardShortcuts({ - preferences_showGeneral: - () => showModal(SettingsModal), - preferences_showKeyboardShortcuts: - () => showModal(SettingsModal, TAB_INDEX_SHORTCUTS), - request_showRecent: - () => showModal(RequestSwitcherModal, { - disableInput: true, - maxRequests: 10, - maxWorkspaces: 0, - selectOnKeyup: true, - title: 'Recent Requests', - hideNeverActiveRequests: true, - // Add an open delay so the dialog won't show for quick presses - openDelay: 150, - }), - workspace_showSettings: - () => showModal(WorkspaceSettingsModal, activeWorkspace), - request_showSettings: - () => { - if (activeRequest) { - showModal(RequestSettingsModal, { request: activeRequest }); - } - }, - request_quickSwitch: - () => showModal(RequestSwitcherModal), - environment_showEditor: - () => showModal(WorkspaceEnvironmentsEditModal, activeWorkspace), - showCookiesEditor: - () => showModal(CookiesModal), - request_createHTTP: - async () => { - if (activeWorkspace) { - const parentId = activeRequest ? activeRequest.parentId : activeWorkspace._id; - const request = await models.request.create({ - parentId, - name: 'New Request', - }); - if (activeWorkspaceMeta) { - await models.workspaceMeta.update(activeWorkspaceMeta, { activeRequestId: request._id }); - } - await updateRequestMetaByParentId(request._id, { - lastActive: Date.now(), - }); - models.stats.incrementCreatedRequests(); - trackSegmentEvent(SegmentEvent.requestCreate, { requestType: 'HTTP' }); - } - }, - request_showDelete: - () => { - if (activeRequest) { - showModal(AskModal, { - title: 'Delete Request?', - message: `Really delete ${activeRequest.name}?`, - onDone: async (confirmed: boolean) => { - if (confirmed) { - await requestOperations.remove(activeRequest); - models.stats.incrementDeletedRequests(); - } - }, - }); - } - }, - request_showCreateFolder: - () => { - if (activeWorkspace) { - createRequestGroup(activeRequest ? activeRequest.parentId : activeWorkspace._id); - } - }, - request_showGenerateCodeEditor: - () => showModal(GenerateCodeModal, activeRequest), - request_showDuplicate: - () => { - if (activeRequest) { - showModal(PromptModal, { - title: 'Duplicate Request', - defaultValue: activeRequest.name, - submitName: 'Create', - label: 'New Name', - selectText: true, - onComplete: async (name: string) => { - const newRequest = await requestOperations.duplicate(activeRequest, { - name, - }); - if (activeWorkspaceMeta) { - await models.workspaceMeta.update(activeWorkspaceMeta, { activeRequestId: newRequest._id }); - } - await updateRequestMetaByParentId(newRequest._id, { - lastActive: Date.now(), - }); - models.stats.incrementCreatedRequests(); - }, - }); - } - }, - request_togglePin: - async () => { - if (activeRequest) { - const meta = isGrpcRequest(activeRequest) ? await getGrpcRequestMetaByParentId(activeRequest._id) : await getRequestMetaByParentId(activeRequest._id); - updateRequestMetaByParentId(activeRequest._id, { pinned: !meta?.pinned }); - } - }, - plugin_reload: - () => plugins.reloadPlugins(), - environment_showVariableSourceAndValue: - () => models.settings.update(settings, { showVariableSourceAndValue: !settings.showVariableSourceAndValue }), - sidebar_toggle: - () => { - if (activeWorkspaceMeta) { - models.workspaceMeta.update(activeWorkspaceMeta, { sidebarHidden: !activeWorkspaceMeta.sidebarHidden }); - } - }, - }); - + useGlobalKeyboardShortcuts(); return null; }; diff --git a/packages/insomnia/src/ui/containers/app.tsx b/packages/insomnia/src/ui/containers/app.tsx index bd9cb026a..3fd592578 100644 --- a/packages/insomnia/src/ui/containers/app.tsx +++ b/packages/insomnia/src/ui/containers/app.tsx @@ -1,27 +1,16 @@ -import { autoBindMethodsForReact } from 'class-autobind-decorator'; import { ipcRenderer } from 'electron'; -import * as path from 'path'; -import React, { PureComponent } from 'react'; -import { connect } from 'react-redux'; -import { Action, bindActionCreators, Dispatch } from 'redux'; +import path from 'path'; +import React, { useEffect, useRef, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; import { parse as urlParse } from 'url'; -import { - ACTIVITY_HOME, - AUTOBIND_CFG, - getProductName, - isDevelopment, -} from '../../common/constants'; -import { type ChangeBufferEvent, database as db } from '../../common/database'; +import { database as db } from '../../common/database'; import { getDataDirectory } from '../../common/electron-helpers'; import { generateId, } from '../../common/misc'; import * as models from '../../models'; -import { isNotDefaultProject } from '../../models/project'; -import { type RequestGroupMeta } from '../../models/request-group-meta'; -import { isWorkspace } from '../../models/workspace'; -import * as plugins from '../../plugins'; +import { GitRepository } from '../../models/git-repository'; import * as themes from '../../plugins/misc'; import { fsClient } from '../../sync/git/fs-client'; import { GIT_CLONE_DIR, GIT_INSOMNIA_DIR, GIT_INTERNAL_DIR, GitVCS } from '../../sync/git/git-vcs'; @@ -30,35 +19,26 @@ import { routableFSClient } from '../../sync/git/routable-fs-client'; import FileSystemDriver from '../../sync/store/drivers/file-system-driver'; import { type MergeConflict } from '../../sync/types'; import { VCS } from '../../sync/vcs/vcs'; -import * as templating from '../../templating/index'; import { ErrorBoundary } from '../components/error-boundary'; -import { AskModal } from '../components/modals/ask-modal'; -import { showAlert, showModal } from '../components/modals/index'; -import { showSelectModal } from '../components/modals/select-modal'; -import { SettingsModal, TAB_INDEX_SHORTCUTS } from '../components/modals/settings-modal'; +import { AlertModal } from '../components/modals/alert-modal'; +import { showModal } from '../components/modals/index'; import { SyncMergeModal } from '../components/modals/sync-merge-modal'; import { Toast } from '../components/toast'; -import { Wrapper } from '../components/wrapper'; +import { type WrapperClass, Wrapper } from '../components/wrapper'; import withDragDropContext from '../context/app/drag-drop-context'; import { GrpcProvider } from '../context/grpc'; import { NunjucksEnabledProvider } from '../context/nunjucks/nunjucks-enabled-context'; -import { updateRequestMetaByParentId } from '../hooks/create-request'; -import { RootState } from '../redux/modules'; import { newCommand, } from '../redux/modules/global'; import { importUri } from '../redux/modules/import'; import { - selectActiveActivity, selectActiveApiSpec, selectActiveCookieJar, - selectActiveEnvironment, selectActiveGitRepository, selectActiveProject, - selectActiveRequest, selectActiveWorkspace, selectActiveWorkspaceMeta, - selectActiveWorkspaceName, selectEnvironments, selectIsFinishedBooting, selectIsLoggedIn, @@ -66,231 +46,31 @@ import { } from '../redux/selectors'; import { AppHooks } from './app-hooks'; -export type AppProps = ReturnType & ReturnType; - interface State { - vcs: VCS | null; - gitVCS: GitVCS | null; isMigratingChildren: boolean; } -@autoBindMethodsForReact(AUTOBIND_CFG) -class App extends PureComponent { - private _updateVCSLock: any; - private _responseFilterHistorySaveTimeout: NodeJS.Timeout | null = null; - - constructor(props: AppProps) { - super(props); - - this.state = { - vcs: null, - gitVCS: null, - isMigratingChildren: false, - }; - - this._updateVCSLock = null; - } - - static async _updateRequestGroupMetaByParentId(requestGroupId: string, patch: Partial) { - const requestGroupMeta = await models.requestGroupMeta.getByParentId(requestGroupId); - - if (requestGroupMeta) { - await models.requestGroupMeta.update(requestGroupMeta, patch); - } else { - const newPatch = Object.assign( - { - parentId: requestGroupId, - }, - patch, - ); - await models.requestGroupMeta.create(newPatch); - } - } - - async _handleSetResponseFilter(requestId: string, responseFilter: string) { - await updateRequestMetaByParentId(requestId, { - responseFilter, - }); - - if (this._responseFilterHistorySaveTimeout !== null) { - clearTimeout(this._responseFilterHistorySaveTimeout); - } - this._responseFilterHistorySaveTimeout = setTimeout(async () => { - const meta = await models.requestMeta.getByParentId(requestId); - // @ts-expect-error -- TSCONVERSION meta can be null - const responseFilterHistory = meta.responseFilterHistory.slice(0, 10); - - // Already in history? - if (responseFilterHistory.includes(responseFilter)) { - return; - } - - // Blank? - if (!responseFilter) { - return; - } - - responseFilterHistory.unshift(responseFilter); - await updateRequestMetaByParentId(requestId, { - responseFilterHistory, - }); - }, 2000); - } - - static _handleShowSettingsModal(tabIndex?: number) { - showModal(SettingsModal, tabIndex); - } - - async _handleReloadPlugins() { - const { settings } = this.props; - await plugins.reloadPlugins(); - await themes.applyColorScheme(settings); - templating.reload(); - console.log('[plugins] reloaded'); - } - - /** - * Update document.title to be "Project - Workspace (Environment) – Request" when not home - * @private - */ - _updateDocumentTitle() { - const { - activeWorkspace, - activeProject, - activeWorkspaceName, - activeEnvironment, - activeRequest, - activeActivity, - } = this.props; - let title; - - if (activeActivity === ACTIVITY_HOME) { - title = getProductName(); - } else if (activeWorkspace && activeWorkspaceName) { - title = activeProject.name; - title += ` - ${activeWorkspaceName}`; - - if (activeEnvironment) { - title += ` (${activeEnvironment.name})`; - } - - if (activeRequest) { - title += ` – ${activeRequest.name}`; - } - } - - document.title = title || getProductName(); - } - - componentDidUpdate(prevProps: AppProps) { - this._updateDocumentTitle(); - - this._ensureWorkspaceChildren(); - - // Check on VCS things - const { activeWorkspace, activeProject, activeGitRepository } = this.props; - const changingWorkspace = prevProps.activeWorkspace?._id !== activeWorkspace?._id; - - // Update VCS if needed - if (changingWorkspace) { - this._updateVCS(); - } - - // Update Git VCS if needed - const changingProject = prevProps.activeProject?._id !== activeProject?._id; - const changingGit = prevProps.activeGitRepository?._id !== activeGitRepository?._id; - - if (changingWorkspace || changingProject || changingGit) { - this._updateGitVCS(); - } - } - - async _updateGitVCS() { - const { activeGitRepository, activeWorkspace, activeProject } = this.props; - - // Get the vcs and set it to null in the state while we update it - let gitVCS = this.state.gitVCS; - this.setState({ - gitVCS: null, - }); - - if (!gitVCS) { - gitVCS = new GitVCS(); - } - - if (activeWorkspace && activeGitRepository) { - // Create FS client - const baseDir = path.join( - getDataDirectory(), - `version-control/git/${activeGitRepository._id}`, - ); - - /** All app data is stored within a namespaced GIT_INSOMNIA_DIR directory at the root of the repository and is read/written from the local NeDB database */ - const neDbClient = NeDBClient.createClient(activeWorkspace._id, activeProject._id); - - /** All git metadata in the GIT_INTERNAL_DIR directory is stored in a git/ directory on the filesystem */ - const gitDataClient = fsClient(baseDir); - - /** All data outside the directories listed below will be stored in an 'other' directory. This is so we can support files that exist outside the ones the app is specifically in charge of. */ - const otherDatClient = fsClient(path.join(baseDir, 'other')); - - /** The routable FS client directs isomorphic-git to read/write from the database or from the correct directory on the file system while performing git operations. */ - const routableFS = routableFSClient(otherDatClient, { - [GIT_INSOMNIA_DIR]: neDbClient, - [GIT_INTERNAL_DIR]: gitDataClient, - }); - // Init VCS - const { credentials, uri } = activeGitRepository; - - if (activeGitRepository.needsFullClone) { - await models.gitRepository.update(activeGitRepository, { - needsFullClone: false, - }); - await gitVCS.initFromClone({ - url: uri, - gitCredentials: credentials, - directory: GIT_CLONE_DIR, - fs: routableFS, - gitDirectory: GIT_INTERNAL_DIR, - }); - } else { - await gitVCS.init({ - directory: GIT_CLONE_DIR, - fs: routableFS, - gitDirectory: GIT_INTERNAL_DIR, - }); - } - - // Configure basic info - const { author, uri: gitUri } = activeGitRepository; - await gitVCS.setAuthor(author.name, author.email); - await gitVCS.addRemote(gitUri); - } else { - // Create new one to un-initialize it - gitVCS = new GitVCS(); - } - - this.setState({ - gitVCS, - }); - } - - async _updateVCS() { - const { activeWorkspace } = this.props; +function useVCS({ + workspaceId, +}: { + workspaceId?: string; +}) { + const vcsInstanceRef = useRef(null); + const [vcs, setVCS] = useState(null); + const updateVCSLock = useRef(false); + // Update VCS when the active workspace changes + useEffect(() => { const lock = generateId(); - this._updateVCSLock = lock; + updateVCSLock.current = lock; - // Get the vcs and set it to null in the state while we update it - let vcs = this.state.vcs; - this.setState({ - vcs: null, - }); + // Set vcs to null while we update it + setVCS(null); - if (!vcs) { + if (!vcsInstanceRef.current) { const driver = FileSystemDriver.create(getDataDirectory()); - vcs = new VCS(driver, async conflicts => { + vcsInstanceRef.current = new VCS(driver, async conflicts => { return new Promise(resolve => { showModal(SyncMergeModal, { conflicts, @@ -300,170 +80,142 @@ class App extends PureComponent { }); } - if (activeWorkspace) { - await vcs.switchProject(activeWorkspace._id); + if (workspaceId) { + vcsInstanceRef.current.switchProject(workspaceId); } else { - vcs.clearBackendProject(); + vcsInstanceRef.current.clearBackendProject(); } // Prevent a potential race-condition when _updateVCS() gets called for different workspaces in rapid succession - if (this._updateVCSLock === lock) { - this.setState({ - vcs, - }); + if (updateVCSLock.current === lock) { + setVCS(vcsInstanceRef.current); } - } + }, [workspaceId]); - async listenforWorkspaceDelete(changes: ChangeBufferEvent[]) { - for (const change of changes) { - const [type, doc] = change; - const { vcs } = this.state; + return vcs; +} - // Delete VCS project if workspace deleted - if (vcs && isWorkspace(doc) && type === db.CHANGE_REMOVE) { - await vcs.removeBackendProjectsForRoot(doc._id); +function useGitVCS({ + workspaceId, + projectId, + gitRepository, +}: { + workspaceId?: string; + projectId: string; + gitRepository?: GitRepository | null; +}) { + const gitVCSInstanceRef = useRef(null); + const [gitVCS, setGitVCS] = useState(null); + + useEffect(() => { + let isMounted = true; + + // Set the instance to null in the state while we update it + if (gitVCSInstanceRef.current) { + setGitVCS(null); + } + + if (!gitVCSInstanceRef.current) { + gitVCSInstanceRef.current = new GitVCS(); + } + + async function update() { + if (workspaceId && gitRepository && gitVCSInstanceRef.current) { + // Create FS client + const baseDir = path.join( + getDataDirectory(), + `version-control/git/${gitRepository._id}`, + ); + + /** All app data is stored within a namespaced GIT_INSOMNIA_DIR directory at the root of the repository and is read/written from the local NeDB database */ + const neDbClient = NeDBClient.createClient(workspaceId, projectId); + + /** All git metadata in the GIT_INTERNAL_DIR directory is stored in a git/ directory on the filesystem */ + const gitDataClient = fsClient(baseDir); + + /** All data outside the directories listed below will be stored in an 'other' directory. This is so we can support files that exist outside the ones the app is specifically in charge of. */ + const otherDatClient = fsClient(path.join(baseDir, 'other')); + + /** The routable FS client directs isomorphic-git to read/write from the database or from the correct directory on the file system while performing git operations. */ + const routableFS = routableFSClient(otherDatClient, { + [GIT_INSOMNIA_DIR]: neDbClient, + [GIT_INTERNAL_DIR]: gitDataClient, + }); + // Init VCS + const { credentials, uri } = gitRepository; + if (gitRepository.needsFullClone) { + await models.gitRepository.update(gitRepository, { + needsFullClone: false, + }); + await gitVCSInstanceRef.current.initFromClone({ + url: uri, + gitCredentials: credentials, + directory: GIT_CLONE_DIR, + fs: routableFS, + gitDirectory: GIT_INTERNAL_DIR, + }); + } else { + await gitVCSInstanceRef.current.init({ + directory: GIT_CLONE_DIR, + fs: routableFS, + gitDirectory: GIT_INTERNAL_DIR, + }); + } + + // Configure basic info + const { author, uri: gitUri } = gitRepository; + await gitVCSInstanceRef.current.setAuthor(author.name, author.email); + await gitVCSInstanceRef.current.addRemote(gitUri); + } else { + // Create new one to un-initialize it + gitVCSInstanceRef.current = new GitVCS(); } - } - } - async componentDidMount() { - // Update title - this._updateDocumentTitle(); - - // Update VCS - await this._updateVCS(); - await this._updateGitVCS(); - db.onChange(this.listenforWorkspaceDelete); - ipcRenderer.on('toggle-preferences', () => { - App._handleShowSettingsModal(); - }); - - if (isDevelopment()) { - ipcRenderer.on('clear-model', () => { - const options = models - .types() - .filter(t => t !== models.settings.type) // don't clear settings - .map(t => ({ name: t, value: t })); - - showSelectModal({ - title: 'Clear a model', - message: 'Select a model to clear; this operation cannot be undone.', - value: options[0].value, - options, - onDone: async type => { - if (type) { - const bufferId = await db.bufferChanges(); - console.log(`[developer] clearing all "${type}" entities`); - const allEntities = await db.all(type); - const filteredEntites = allEntities - .filter(isNotDefaultProject); // don't clear the default project - await db.batchModifyDocs({ remove: filteredEntites }); - db.flushChanges(bufferId); - } - }, - }); - }); - - ipcRenderer.on('clear-all-models', () => { - showModal(AskModal, { - title: 'Clear all models', - message: 'Are you sure you want to clear all models? This operation cannot be undone.', - yesText: 'Yes', - noText: 'No', - onDone: async (yes: boolean) => { - if (yes) { - const bufferId = await db.bufferChanges(); - const promises = models - .types() - .filter(t => t !== models.settings.type) // don't clear settings - .reverse().map(async type => { - console.log(`[developer] clearing all "${type}" entities`); - const allEntities = await db.all(type); - const filteredEntites = allEntities - .filter(isNotDefaultProject); // don't clear the default project - await db.batchModifyDocs({ remove: filteredEntites }); - }); - await Promise.all(promises); - db.flushChanges(bufferId); - } - }, - }); - }); + isMounted && setGitVCS(gitVCSInstanceRef.current); } - ipcRenderer.on('reload-plugins', this._handleReloadPlugins); - ipcRenderer.on('toggle-preferences-shortcuts', () => { - App._handleShowSettingsModal(TAB_INDEX_SHORTCUTS); - }); - ipcRenderer.on('run-command', (_, commandUri) => { - const parsed = urlParse(commandUri, true); - const command = `${parsed.hostname}${parsed.pathname}`; - const args = JSON.parse(JSON.stringify(parsed.query)); - args.workspaceId = args.workspaceId || this.props.activeWorkspace?._id; - this.props.handleCommand(command, args); - }); - // NOTE: This is required for "drop" event to trigger. - document.addEventListener( - 'dragover', - event => { - event.preventDefault(); - }, - false, - ); - document.addEventListener( - 'drop', - async event => { - event.preventDefault(); - const { activeWorkspace, handleImportUri } = this.props; + update(); - if (!activeWorkspace) { - return; - } + return () => { + isMounted = false; + }; + }, [workspaceId, projectId, gitRepository]); - // @ts-expect-error -- TSCONVERSION - if (event.dataTransfer.files.length === 0) { - console.log('[drag] Ignored drop event because no files present'); - return; - } + return gitVCS; +} - // @ts-expect-error -- TSCONVERSION - const file = event.dataTransfer.files[0]; - const { path } = file; - const uri = `file://${path}`; - await showAlert({ - title: 'Confirm Data Import', - message: ( - - Import {path}? - - ), - addCancel: true, - }); - handleImportUri(uri, { workspaceId: activeWorkspace?._id }); - }, - false, - ); +const App = () => { + const [state, setState] = useState({ + isMigratingChildren: false, + }); - // Give it a bit before letting the backend know it's ready - setTimeout(() => ipcRenderer.send('window-ready'), 500); - window - .matchMedia('(prefers-color-scheme: dark)') - .addListener(async () => themes.applyColorScheme(this.props.settings)); - } + const activeProject = useSelector(selectActiveProject); + const activeCookieJar = useSelector(selectActiveCookieJar); + const activeApiSpec = useSelector(selectActiveApiSpec); + const activeWorkspace = useSelector(selectActiveWorkspace); + const activeGitRepository = useSelector(selectActiveGitRepository); + const activeWorkspaceMeta = useSelector(selectActiveWorkspaceMeta); + const environments = useSelector(selectEnvironments); + const isLoggedIn = useSelector(selectIsLoggedIn); + const isFinishedBooting = useSelector(selectIsFinishedBooting); + const settings = useSelector(selectSettings); + const dispatch = useDispatch(); + const handleCommand = dispatch(newCommand); + const handleImportUri = dispatch(importUri); + const vcs = useVCS({ + workspaceId: activeWorkspace?._id, + }); - componentWillUnmount() { - db.offChange(this.listenforWorkspaceDelete); - } + const gitVCS = useGitVCS({ + workspaceId: activeWorkspace?._id, + projectId: activeProject?._id, + gitRepository: activeGitRepository, + }); - async _ensureWorkspaceChildren() { - const { - activeWorkspace, - activeWorkspaceMeta, - activeCookieJar, - environments, - activeApiSpec, - } = this.props; + const wrapperRef = useRef(null); + // Ensure Children: Make sure cookies, env, and meta models are created under this workspace + useEffect(() => { if (!activeWorkspace) { return; } @@ -476,106 +228,120 @@ class App extends PureComponent { } // We already started migrating. Let it finish. - if (this.state.isMigratingChildren) { + if (state.isMigratingChildren) { return; } - // Prevent rendering of everything - this.setState( - { - isMigratingChildren: true, - }, - async () => { + // Prevent rendering of everything until we check the workspace has cookies, env, and meta + setState(state => ({ ...state, isMigratingChildren: true })); + async function update() { + if (activeWorkspace) { const flushId = await db.bufferChanges(); await models.workspace.ensureChildren(activeWorkspace); await db.flushChanges(flushId); - this.setState({ - isMigratingChildren: false, - }); + setState(state => ({ ...state, isMigratingChildren: false })); + } + } + update(); + }, [activeApiSpec, activeCookieJar, activeWorkspace, activeWorkspaceMeta, environments, state.isMigratingChildren]); + + // Give it a bit before letting the backend know it's ready + useEffect(() => { + setTimeout(() => ipcRenderer.send('window-ready'), 500); + }, []); + + // Handle Application Commands + useEffect(() => { + ipcRenderer.on('run-command', (_, commandUri) => { + const parsed = urlParse(commandUri, true); + const command = `${parsed.hostname}${parsed.pathname}`; + const args = JSON.parse(JSON.stringify(parsed.query)); + args.workspaceId = args.workspaceId || activeWorkspace?._id; + handleCommand(command, args); + }); + }, [activeWorkspace?._id, handleCommand]); + + // Handle System Theme change + useEffect(() => { + const matches = window.matchMedia('(prefers-color-scheme: dark)'); + matches.addEventListener('change', () => themes.applyColorScheme(settings)); + return () => { + matches.removeEventListener('change', () => themes.applyColorScheme(settings)); + }; + }); + + // Global Drag and Drop for importing files + useEffect(() => { + // NOTE: This is required for "drop" event to trigger. + document.addEventListener( + 'dragover', + event => { + event.preventDefault(); }, + false, ); - } - - // eslint-disable-next-line camelcase - UNSAFE_componentWillReceiveProps() { - this._ensureWorkspaceChildren(); - } - - // eslint-disable-next-line camelcase - UNSAFE_componentWillMount() { - this._ensureWorkspaceChildren(); - } - - render() { - if (this.state.isMigratingChildren) { - console.log('[app] Waiting for migration to complete'); - return null; - } - - if (!this.props.isFinishedBooting) { - console.log('[app] Waiting to finish booting'); - return null; - } - - const { activeWorkspace, isLoggedIn } = this.props; - const { - gitVCS, - vcs, - } = this.state; - const uniquenessKey = `${isLoggedIn}::${activeWorkspace?._id || 'n/a'}`; - return ( - - - - -
- - - - - - - -
-
-
+ document.addEventListener( + 'drop', + async event => { + event.preventDefault(); + if (!activeWorkspace) { + return; + } + const files = event.dataTransfer?.files || []; + if (files.length === 0) { + console.log('[drag] Ignored drop event because no files present'); + return; + } + const file = files[0]; + if (!file?.path) { + return; + } + await showModal(AlertModal, { + title: 'Confirm Data Import', + message: ( + + Import {file.path}? + + ), + addCancel: true, + }); + handleImportUri(`file://${file.path}`, { workspaceId: activeWorkspace?._id }); + }, + false, ); + }); + + if (state.isMigratingChildren) { + console.log('[app] Waiting for migration to complete'); + return null; } -} -const mapStateToProps = (state: RootState) => ({ - activeActivity: selectActiveActivity(state), - activeProject: selectActiveProject(state), - activeApiSpec: selectActiveApiSpec(state), - activeWorkspaceName: selectActiveWorkspaceName(state), - activeCookieJar: selectActiveCookieJar(state), - activeEnvironment: selectActiveEnvironment(state), - activeGitRepository: selectActiveGitRepository(state), - activeRequest: selectActiveRequest(state), - activeWorkspace: selectActiveWorkspace(state), - activeWorkspaceMeta: selectActiveWorkspaceMeta(state), - environments: selectEnvironments(state), - isLoggedIn: selectIsLoggedIn(state), - isFinishedBooting: selectIsFinishedBooting(state), - settings: selectSettings(state), -}); + if (!isFinishedBooting) { + console.log('[app] Waiting to finish booting'); + return null; + } -const mapDispatchToProps = (dispatch: Dispatch>) => { - const { - importUri: handleImportUri, - newCommand: handleCommand, - } = bindActionCreators({ - importUri, - newCommand, - }, dispatch); - return { - handleCommand, - handleImportUri, - }; + const uniquenessKey = `${isLoggedIn}::${activeWorkspace?._id || 'n/a'}`; + return ( + + + +
+ + + + + + + +
+
+
+ ); }; -export default connect(mapStateToProps, mapDispatchToProps)(withDragDropContext(App)); +export default withDragDropContext(App); diff --git a/packages/insomnia/src/ui/hooks/use-document-title.ts b/packages/insomnia/src/ui/hooks/use-document-title.ts new file mode 100644 index 000000000..62b9cd5e2 --- /dev/null +++ b/packages/insomnia/src/ui/hooks/use-document-title.ts @@ -0,0 +1,33 @@ +import { useEffect } from 'react'; +import { useSelector } from 'react-redux'; + +import { ACTIVITY_HOME, getProductName } from '../../common/constants'; +import { selectActiveActivity, selectActiveEnvironment, selectActiveProject, selectActiveRequest, selectActiveWorkspace, selectActiveWorkspaceName } from '../redux/selectors'; + +export const useDocumentTitle = () => { + const activeActivity = useSelector(selectActiveActivity); + const activeProject = useSelector(selectActiveProject); + const activeWorkspaceName = useSelector(selectActiveWorkspaceName); + const activeWorkspace = useSelector(selectActiveWorkspace); + + const activeEnvironment = useSelector(selectActiveEnvironment); + const activeRequest = useSelector(selectActiveRequest); + // Update document title + useEffect(() => { + let title; + if (activeActivity === ACTIVITY_HOME) { + title = getProductName(); + } else if (activeWorkspace && activeWorkspaceName) { + title = activeProject.name; + title += ` - ${activeWorkspaceName}`; + if (activeEnvironment) { + title += ` (${activeEnvironment.name})`; + } + if (activeRequest) { + title += ` – ${activeRequest.name}`; + } + } + document.title = title || getProductName(); + }, [activeActivity, activeEnvironment, activeProject.name, activeRequest, activeWorkspace, activeWorkspaceName]); + +}; diff --git a/packages/insomnia/src/ui/hooks/use-global-keyboard-shortcuts.ts b/packages/insomnia/src/ui/hooks/use-global-keyboard-shortcuts.ts new file mode 100644 index 000000000..29486af81 --- /dev/null +++ b/packages/insomnia/src/ui/hooks/use-global-keyboard-shortcuts.ts @@ -0,0 +1,34 @@ +import { useSelector } from 'react-redux'; + +import * as models from '../../models'; +import * as plugins from '../../plugins'; +import { useDocBodyKeyboardShortcuts } from '../components/keydown-binder'; +import { showModal } from '../components/modals'; +import { SettingsModal, TAB_INDEX_SHORTCUTS } from '../components/modals/settings-modal'; +import { WorkspaceSettingsModal } from '../components/modals/workspace-settings-modal'; +import { selectActiveWorkspace, selectActiveWorkspaceMeta, selectSettings } from '../redux/selectors'; + +export const useGlobalKeyboardShortcuts = () => { + const activeWorkspace = useSelector(selectActiveWorkspace); + const activeWorkspaceMeta = useSelector(selectActiveWorkspaceMeta); + const settings = useSelector(selectSettings); + + useDocBodyKeyboardShortcuts({ + workspace_showSettings: + () => showModal(WorkspaceSettingsModal, activeWorkspace), + plugin_reload: + () => plugins.reloadPlugins(), + environment_showVariableSourceAndValue: + () => models.settings.update(settings, { showVariableSourceAndValue: !settings.showVariableSourceAndValue }), + preferences_showGeneral: + () => showModal(SettingsModal), + preferences_showKeyboardShortcuts: + () => showModal(SettingsModal, TAB_INDEX_SHORTCUTS), + sidebar_toggle: + () => { + if (activeWorkspaceMeta) { + models.workspaceMeta.update(activeWorkspaceMeta, { sidebarHidden: !activeWorkspaceMeta.sidebarHidden }); + } + }, + }); +}; diff --git a/packages/insomnia/src/ui/index.tsx b/packages/insomnia/src/ui/index.tsx index aacb0093c..ca469ed44 100644 --- a/packages/insomnia/src/ui/index.tsx +++ b/packages/insomnia/src/ui/index.tsx @@ -1,11 +1,10 @@ // eslint-disable-next-line simple-import-sort/imports -import { ipcRenderer } from 'electron'; import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; - +import './rendererListeners'; import { getProductName, isDevelopment } from '../common/constants'; -import { database as db } from '../common/database'; +import { database } from '../common/database'; import { initializeLogging } from '../common/log'; import * as models from '../models'; import { initNewOAuthSession } from '../network/o-auth-2/misc'; @@ -25,7 +24,7 @@ document.body.setAttribute('data-platform', process.platform); document.title = getProductName(); (async function() { - await db.initClient(); + await database.initClient(); await initPlugins(); @@ -59,22 +58,5 @@ if (isDevelopment()) { // @ts-expect-error -- TSCONVERSION needs window augmentation window.models = models; // @ts-expect-error -- TSCONVERSION needs window augmentation - window.db = db; + window.db = database; } - -function showUpdateNotification() { - console.log('[app] Update Available'); - // eslint-disable-next-line no-new - new window.Notification('Insomnia Update Ready', { - body: 'Relaunch the app for it to take effect', - silent: true, - // @ts-expect-error -- TSCONVERSION - sticky: true, - }); -} - -ipcRenderer.on('update-available', () => { - // Give it a few seconds before showing this. Sometimes, when - // you relaunch too soon it doesn't work the first time. - setTimeout(showUpdateNotification, 1000 * 10); -}); diff --git a/packages/insomnia/src/ui/rendererListeners.ts b/packages/insomnia/src/ui/rendererListeners.ts new file mode 100644 index 000000000..d2e41fd20 --- /dev/null +++ b/packages/insomnia/src/ui/rendererListeners.ts @@ -0,0 +1,98 @@ + +import { ipcRenderer } from 'electron'; + +import { isDevelopment } from '../common/constants'; +import { database } from '../common/database'; +import * as models from '../models'; +import { isNotDefaultProject } from '../models/project'; +import * as plugins from '../plugins'; +import * as themes from '../plugins/misc'; +import * as templating from '../templating'; +import { showModal } from './components/modals'; +import { AskModal } from './components/modals/ask-modal'; +import { SelectModal } from './components/modals/select-modal'; +import { SettingsModal, TAB_INDEX_SHORTCUTS } from './components/modals/settings-modal'; + +ipcRenderer.on('update-available', () => { + // Give it a few seconds before showing this. Sometimes, when + // you relaunch too soon it doesn't work the first time. + setTimeout(() => { + console.log('[app] Update Available'); + // eslint-disable-next-line no-new + new window.Notification('Insomnia Update Ready', { + body: 'Relaunch the app for it to take effect', + silent: true, + // @ts-expect-error -- TSCONVERSION + sticky: true, + }); + }, 1000 * 10); +}); + +ipcRenderer.on('toggle-preferences', () => { + showModal(SettingsModal); +}); + +if (isDevelopment()) { + ipcRenderer.on('clear-model', () => { + const options = models + .types() + .filter(t => t !== models.settings.type) // don't clear settings + .map(t => ({ name: t, value: t })); + + showModal(SelectModal, { + title: 'Clear a model', + message: 'Select a model to clear; this operation cannot be undone.', + value: options[0].value, + options, + onDone: async (type: string | null) => { + if (type) { + const bufferId = await database.bufferChanges(); + console.log(`[developer] clearing all "${type}" entities`); + const allEntities = await database.all(type); + const filteredEntites = allEntities + .filter(isNotDefaultProject); // don't clear the default project + await database.batchModifyDocs({ remove: filteredEntites }); + database.flushChanges(bufferId); + } + }, + }); + }); + + ipcRenderer.on('clear-all-models', () => { + showModal(AskModal, { + title: 'Clear all models', + message: 'Are you sure you want to clear all models? This operation cannot be undone.', + yesText: 'Yes', + noText: 'No', + onDone: async (yes: boolean) => { + if (yes) { + const bufferId = await database.bufferChanges(); + const promises = models + .types() + .filter(t => t !== models.settings.type) // don't clear settings + .reverse().map(async type => { + console.log(`[developer] clearing all "${type}" entities`); + const allEntities = await database.all(type); + const filteredEntites = allEntities + .filter(isNotDefaultProject); // don't clear the default project + await database.batchModifyDocs({ remove: filteredEntites }); + }); + await Promise.all(promises); + database.flushChanges(bufferId); + } + }, + }); + }); +} + +ipcRenderer.on('reload-plugins', async () => { + const settings = await models.settings.getOrCreate(); + await plugins.reloadPlugins(); + await themes.applyColorScheme(settings); + templating.reload(); + console.log('[plugins] reloaded'); +}); + +ipcRenderer.on('toggle-preferences-shortcuts', () => { + showModal(SettingsModal, TAB_INDEX_SHORTCUTS); +});