mirror of
https://github.com/Kong/insomnia.git
synced 2026-10-27 17:19:42 -04:00
Refactor/wrapper-tsx-fc (#5242)
* wrapper-fc squashed * undrill setFilter * extract hooks * eliminate wrapper.tsx
This commit is contained in:
1 parent
edad158d65
commit
898d24c4ba
19 files changed
+719
-867
No files matched your search
@@ -5,7 +5,7 @@ import styled from 'styled-components';
|
||||
import type { GlobalActivity } from '../../../common/constants';
|
||||
import { ACTIVITY_DEBUG, ACTIVITY_SPEC, ACTIVITY_UNIT_TEST } from '../../../common/constants';
|
||||
import { isDesign, Workspace } from '../../../models/workspace';
|
||||
import { HandleActivityChange } from '../wrapper';
|
||||
import { HandleActivityChange } from '../workspace-page-header';
|
||||
|
||||
const StyledNav = styled.nav({
|
||||
display: 'flex',
|
||||
|
||||
@@ -2,9 +2,10 @@ import { EnvironmentHighlightColorStyle } from 'insomnia-common';
|
||||
import React, { FC, useCallback, useRef } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import * as models from '../../../models';
|
||||
import type { Environment } from '../../../models/environment';
|
||||
import type { Workspace } from '../../../models/workspace';
|
||||
import { selectEnvironments, selectHotKeyRegistry } from '../../redux/selectors';
|
||||
import { selectActiveWorkspaceMeta, selectEnvironments, selectHotKeyRegistry } from '../../redux/selectors';
|
||||
import { type DropdownHandle, Dropdown } from '../base/dropdown/dropdown';
|
||||
import { DropdownButton } from '../base/dropdown/dropdown-button';
|
||||
import { DropdownDivider } from '../base/dropdown/dropdown-divider';
|
||||
@@ -18,18 +19,17 @@ import { Tooltip } from '../tooltip';
|
||||
interface Props {
|
||||
activeEnvironment?: Environment | null;
|
||||
environmentHighlightColorStyle: EnvironmentHighlightColorStyle;
|
||||
handleSetActiveEnvironment: (id: string | null) => void;
|
||||
workspace: Workspace;
|
||||
}
|
||||
|
||||
export const EnvironmentsDropdown: FC<Props> = ({
|
||||
activeEnvironment,
|
||||
environmentHighlightColorStyle,
|
||||
handleSetActiveEnvironment,
|
||||
workspace,
|
||||
}) => {
|
||||
const environments = useSelector(selectEnvironments);
|
||||
const hotKeyRegistry = useSelector(selectHotKeyRegistry);
|
||||
const activeWorkspaceMeta = useSelector(selectActiveWorkspaceMeta);
|
||||
const dropdownRef = useRef<DropdownHandle>(null);
|
||||
const handleShowEnvironmentModal = useCallback(() => {
|
||||
showModal(WorkspaceEnvironmentsEditModal, workspace);
|
||||
@@ -48,7 +48,7 @@ export const EnvironmentsDropdown: FC<Props> = ({
|
||||
const subEnvironments = environments
|
||||
.filter(environment => environment.parentId === (baseEnvironment && baseEnvironment._id))
|
||||
.sort((e1, e2) => e1.metaSortKey - e2.metaSortKey);
|
||||
const description = (!activeEnvironment || activeEnvironment === baseEnvironment) ? 'No Environment' : activeEnvironment.name;
|
||||
const description = (!activeEnvironment || activeEnvironment === baseEnvironment) ? 'No Environment' : activeEnvironment.name;
|
||||
|
||||
return (
|
||||
<Dropdown ref={dropdownRef}>
|
||||
@@ -82,8 +82,11 @@ export const EnvironmentsDropdown: FC<Props> = ({
|
||||
{subEnvironments.map(environment => (
|
||||
<DropdownItem
|
||||
key={environment._id}
|
||||
value={environment._id}
|
||||
onClick={handleSetActiveEnvironment}
|
||||
onClick={() => {
|
||||
if (activeWorkspaceMeta) {
|
||||
models.workspaceMeta.update(activeWorkspaceMeta, { activeEnvironmentId: environment._id });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<i
|
||||
className="fa fa-random"
|
||||
@@ -95,7 +98,13 @@ export const EnvironmentsDropdown: FC<Props> = ({
|
||||
</DropdownItem>
|
||||
))}
|
||||
|
||||
<DropdownItem value={null} onClick={handleSetActiveEnvironment}>
|
||||
<DropdownItem
|
||||
onClick={() => {
|
||||
if (activeWorkspaceMeta) {
|
||||
models.workspaceMeta.update(activeWorkspaceMeta, { activeEnvironmentId: null });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<i className="fa fa-empty" /> No Environment
|
||||
</DropdownItem>
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ import { GitLogModal } from '../modals/git-log-modal';
|
||||
import { GitStagingModal } from '../modals/git-staging-modal';
|
||||
|
||||
type Props = ReturnType<typeof mapDispatchToProps> & {
|
||||
handleGitBranchChanged: (branch: string) => void;
|
||||
workspace: Workspace;
|
||||
vcs: GitVCS;
|
||||
gitRepository: GitRepository | null;
|
||||
@@ -64,7 +63,7 @@ class GitSyncDropdown extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
async _refreshState(otherState?: Record<string, any>) {
|
||||
const { vcs, workspace, handleGitBranchChanged } = this.props;
|
||||
const { vcs, workspace } = this.props;
|
||||
const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId(workspace._id);
|
||||
|
||||
// Clear cached items and return if no state
|
||||
@@ -101,7 +100,6 @@ class GitSyncDropdown extends PureComponent<Props, State> {
|
||||
cachedGitLastAuthor,
|
||||
cachedGitLastCommitTime,
|
||||
});
|
||||
handleGitBranchChanged(branch);
|
||||
}
|
||||
|
||||
async _handleOpen() {
|
||||
|
||||
@@ -20,7 +20,6 @@ import { PromptButton } from '../base/prompt-button';
|
||||
type Props = ReturnType<typeof mapDispatchToProps> & {
|
||||
vcs: GitVCS;
|
||||
gitRepository: GitRepository;
|
||||
handleGitBranchChanged: (branch: string) => void;
|
||||
};
|
||||
|
||||
interface State {
|
||||
@@ -75,7 +74,7 @@ export class GitBranchesModalClass extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
async _refreshState(newState?: Record<string, any>) {
|
||||
const { vcs, handleGitBranchChanged } = this.props;
|
||||
const { vcs } = this.props;
|
||||
const branch = await vcs.getBranch();
|
||||
const branches = await vcs.listBranches();
|
||||
const remoteBranches = await vcs.listRemoteBranches();
|
||||
@@ -85,7 +84,6 @@ export class GitBranchesModalClass extends PureComponent<Props, State> {
|
||||
remoteBranches,
|
||||
...newState,
|
||||
});
|
||||
handleGitBranchChanged(branch);
|
||||
}
|
||||
|
||||
_handleClearError() {
|
||||
|
||||
@@ -52,7 +52,6 @@ const TabPanelBody = styled.div({
|
||||
|
||||
interface Props {
|
||||
environmentId: string;
|
||||
handleImport: Function;
|
||||
request?: Request | null;
|
||||
settings: Settings;
|
||||
workspace: Workspace;
|
||||
@@ -60,7 +59,6 @@ interface Props {
|
||||
|
||||
export const RequestPane: FC<Props> = ({
|
||||
environmentId,
|
||||
handleImport,
|
||||
request,
|
||||
settings,
|
||||
workspace,
|
||||
@@ -172,7 +170,6 @@ export const RequestPane: FC<Props> = ({
|
||||
uniquenessKey={uniqueKey}
|
||||
onUrlChange={updateRequestUrl}
|
||||
handleAutocompleteUrls={autocompleteUrls}
|
||||
handleImport={handleImport}
|
||||
nunjucksPowerUserMode={settings.nunjucksPowerUserMode}
|
||||
request={request}
|
||||
/>
|
||||
|
||||
@@ -13,6 +13,7 @@ import * as models from '../../../models';
|
||||
import type { Request } from '../../../models/request';
|
||||
import type { Response } from '../../../models/response';
|
||||
import { cancelRequestById } from '../../../network/network';
|
||||
import { updateRequestMetaByParentId } from '../../hooks/create-request';
|
||||
import { selectActiveResponse, selectLoadStartTime, selectResponseFilter, selectResponseFilterHistory, selectResponsePreviewMode, selectSettings } from '../../redux/selectors';
|
||||
import { Button } from '../base/button';
|
||||
import { PreviewModeDropdown } from '../dropdowns/preview-mode-dropdown';
|
||||
@@ -32,11 +33,9 @@ import { Pane, paneBodyClasses, PaneHeader } from './pane';
|
||||
import { PlaceholderResponsePane } from './placeholder-response-pane';
|
||||
|
||||
interface Props {
|
||||
handleSetFilter: (filter: string) => void;
|
||||
request?: Request | null;
|
||||
}
|
||||
export const ResponsePane: FC<Props> = ({
|
||||
handleSetFilter,
|
||||
request,
|
||||
}) => {
|
||||
const response = useSelector(selectActiveResponse) as Response | null;
|
||||
@@ -45,7 +44,24 @@ export const ResponsePane: FC<Props> = ({
|
||||
const settings = useSelector(selectSettings);
|
||||
const loadStartTime = useSelector(selectLoadStartTime);
|
||||
const previewMode = useSelector(selectResponsePreviewMode);
|
||||
|
||||
const handleSetFilter = async (responseFilter: string) => {
|
||||
if (!response) {
|
||||
return;
|
||||
}
|
||||
const requestId = response.parentId;
|
||||
await updateRequestMetaByParentId(requestId, { responseFilter });
|
||||
const meta = await models.requestMeta.getByParentId(requestId);
|
||||
if (!meta) {
|
||||
return;
|
||||
}
|
||||
const responseFilterHistory = meta.responseFilterHistory.slice(0, 10);
|
||||
// Already in history or empty?
|
||||
if (!responseFilter || responseFilterHistory.includes(responseFilter)) {
|
||||
return;
|
||||
}
|
||||
responseFilterHistory.unshift(responseFilter);
|
||||
updateRequestMetaByParentId(requestId, { responseFilterHistory });
|
||||
};
|
||||
const handleGetResponseBody = useCallback(() => {
|
||||
if (!response) {
|
||||
return null;
|
||||
@@ -58,7 +74,6 @@ export const ResponsePane: FC<Props> = ({
|
||||
clipboard.writeText(bodyBuffer.toString('utf8'));
|
||||
}
|
||||
}, [handleGetResponseBody]);
|
||||
|
||||
const handleDownloadResponseBody = useCallback(async (prettify: boolean) => {
|
||||
if (!response || !request) {
|
||||
console.warn('Nothing to download');
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { SaveDialogOptions } from 'electron';
|
||||
import fs from 'fs';
|
||||
import * as importers from 'insomnia-importers';
|
||||
import { extension as mimeExtension } from 'mime-types';
|
||||
import path from 'path';
|
||||
import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react';
|
||||
@@ -7,15 +8,16 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useInterval } from 'react-use';
|
||||
|
||||
import { SegmentEvent, trackSegmentEvent } from '../../common/analytics';
|
||||
import { database } from '../../common/database';
|
||||
import { getContentDispositionHeader } from '../../common/misc';
|
||||
import * as models from '../../models';
|
||||
import { update } from '../../models/helpers/request-operations';
|
||||
import type { Request } from '../../models/request';
|
||||
import { isRequest, Request } from '../../models/request';
|
||||
import * as network from '../../network/network';
|
||||
import { updateRequestMetaByParentId } from '../hooks/create-request';
|
||||
import { useTimeoutWhen } from '../hooks/useTimeoutWhen';
|
||||
import { loadRequestStart, loadRequestStop } from '../redux/modules/global';
|
||||
import { selectActiveEnvironment, selectHotKeyRegistry, selectResponseDownloadPath, selectSettings } from '../redux/selectors';
|
||||
import { selectActiveEnvironment, selectActiveRequest, selectHotKeyRegistry, selectResponseDownloadPath, selectSettings } from '../redux/selectors';
|
||||
import { type DropdownHandle, Dropdown } from './base/dropdown/dropdown';
|
||||
import { DropdownButton } from './base/dropdown/dropdown-button';
|
||||
import { DropdownDivider } from './base/dropdown/dropdown-divider';
|
||||
@@ -31,7 +33,6 @@ import { RequestRenderErrorModal } from './modals/request-render-error-modal';
|
||||
|
||||
interface Props {
|
||||
handleAutocompleteUrls: () => Promise<string[]>;
|
||||
handleImport: Function;
|
||||
nunjucksPowerUserMode: boolean;
|
||||
onUrlChange: (r: Request, url: string) => Promise<Request>;
|
||||
request: Request;
|
||||
@@ -44,7 +45,6 @@ export interface RequestUrlBarHandle {
|
||||
|
||||
export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
|
||||
handleAutocompleteUrls,
|
||||
handleImport,
|
||||
onUrlChange,
|
||||
request,
|
||||
uniquenessKey,
|
||||
@@ -52,6 +52,7 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
|
||||
const downloadPath = useSelector(selectResponseDownloadPath);
|
||||
const hotKeyRegistry = useSelector(selectHotKeyRegistry);
|
||||
const activeEnvironment = useSelector(selectActiveEnvironment);
|
||||
const activeRequest = useSelector(selectActiveRequest);
|
||||
const settings = useSelector(selectSettings);
|
||||
const dispatch = useDispatch();
|
||||
const methodDropdownRef = useRef<DropdownHandle>(null);
|
||||
@@ -302,6 +303,36 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
|
||||
});
|
||||
|
||||
const [lastPastedText, setLastPastedText] = useState<string>();
|
||||
const handleImport = useCallback(async (text: string) => {
|
||||
// Allow user to paste any import file into the url. If it results in
|
||||
// only one item, it will overwrite the current request.
|
||||
try {
|
||||
const { data } = await importers.convert(text);
|
||||
const { resources } = data;
|
||||
const r = resources[0];
|
||||
if (r && r._type === 'request' && activeRequest && isRequest(activeRequest)) {
|
||||
// Only pull fields that we want to update
|
||||
return database.update({
|
||||
...activeRequest,
|
||||
modified: Date.now(),
|
||||
url: r.url,
|
||||
method: r.method,
|
||||
headers: r.headers,
|
||||
body: r.body,
|
||||
authentication: r.authentication,
|
||||
parameters: r.parameters,
|
||||
},
|
||||
// Pass true to indicate that this is an import
|
||||
true
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// Import failed, that's alright
|
||||
console.error(error);
|
||||
}
|
||||
return null;
|
||||
}, [activeRequest]);
|
||||
|
||||
const handleUrlChange = useCallback(async (url: string) => {
|
||||
const pastedText = lastPastedText;
|
||||
// If no pasted text in the queue, just fire the regular change handler
|
||||
|
||||
@@ -1,37 +1,113 @@
|
||||
import React, { FunctionComponent, ReactNode, useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { ACTIVITY_HOME } from '../../common/constants';
|
||||
import { selectActiveActivity, selectActiveApiSpec, selectActiveProjectName, selectActiveWorkspace } from '../redux/selectors';
|
||||
import { ACTIVITY_DEBUG, ACTIVITY_HOME, ACTIVITY_SPEC, ACTIVITY_UNIT_TEST, GlobalActivity } from '../../common/constants';
|
||||
import { importRaw } from '../../common/import';
|
||||
import { initializeSpectral, isLintError } from '../../common/spectral';
|
||||
import * as models from '../../models';
|
||||
import { isRemoteProject } from '../../models/project';
|
||||
import { isCollection, isDesign } from '../../models/workspace';
|
||||
import { useGitVCS } from '../hooks/use-git-vcs';
|
||||
import { useVCS } from '../hooks/use-vcs';
|
||||
import { setActiveActivity } from '../redux/modules/global';
|
||||
import { selectActiveActivity, selectActiveApiSpec, selectActiveGitRepository, selectActiveProject, selectActiveProjectName, selectActiveWorkspace, selectIsLoggedIn } from '../redux/selectors';
|
||||
import { ActivityToggle } from './activity-toggle/activity-toggle';
|
||||
import { AppHeader } from './app-header';
|
||||
import { DropdownButton } from './base/dropdown/dropdown-button';
|
||||
import GitSyncDropdown from './dropdowns/git-sync-dropdown';
|
||||
import { SyncDropdown } from './dropdowns/sync-dropdown';
|
||||
import { WorkspaceDropdown } from './dropdowns/workspace-dropdown';
|
||||
import { HandleActivityChange } from './wrapper';
|
||||
import { showModal } from './modals';
|
||||
import { AlertModal } from './modals/alert-modal';
|
||||
|
||||
interface Props {
|
||||
handleActivityChange: HandleActivityChange;
|
||||
gridRight: ReactNode;
|
||||
}
|
||||
export type HandleActivityChange = (options: {
|
||||
workspaceId?: string;
|
||||
nextActivity: GlobalActivity;
|
||||
}) => Promise<void>;
|
||||
|
||||
export const WorkspacePageHeader: FunctionComponent<Props> = ({
|
||||
gridRight,
|
||||
handleActivityChange,
|
||||
}) => {
|
||||
// TODO(jackkav): find the right place to initializeSpectral
|
||||
const spectral = initializeSpectral();
|
||||
export const WorkspacePageHeader: FC = () => {
|
||||
const activeApiSpec = useSelector(selectActiveApiSpec);
|
||||
const activeWorkspace = useSelector(selectActiveWorkspace);
|
||||
const activity = useSelector(selectActiveActivity);
|
||||
const activeActivity = useSelector(selectActiveActivity);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleActivityChange = useCallback(async ({ workspaceId, nextActivity }) => {
|
||||
// Remember last activity on workspace for later, but only if it isn't HOME
|
||||
if (workspaceId && nextActivity !== ACTIVITY_HOME) {
|
||||
await models.workspaceMeta.updateByParentId(workspaceId, {
|
||||
activeActivity: nextActivity,
|
||||
});
|
||||
}
|
||||
const editingASpec = activeActivity === ACTIVITY_SPEC;
|
||||
if (!editingASpec) {
|
||||
dispatch(setActiveActivity(nextActivity));
|
||||
return;
|
||||
}
|
||||
if (!activeApiSpec || !workspaceId) {
|
||||
return;
|
||||
}
|
||||
const goingToDebugOrTest = nextActivity === ACTIVITY_DEBUG || nextActivity === ACTIVITY_UNIT_TEST;
|
||||
// If editing a spec and not going to debug or test, don't regenerate anything
|
||||
if (editingASpec && !goingToDebugOrTest) {
|
||||
dispatch(setActiveActivity(nextActivity));
|
||||
return;
|
||||
}
|
||||
// Handle switching away from the spec design activity. For this, we want to generate
|
||||
// requests that can be accessed from debug or test.
|
||||
// If there are errors in the spec, show the user a warning first
|
||||
const results = (await spectral.run(activeApiSpec.contents)).filter(isLintError);
|
||||
if (activeApiSpec.contents && results && results.length) {
|
||||
showModal(AlertModal, {
|
||||
title: 'Error Generating Configuration',
|
||||
message: 'Some requests may not be available due to errors found in the specification. We recommend fixing errors before proceeding.',
|
||||
okLabel: 'Proceed',
|
||||
addCancel: true,
|
||||
onConfirm: () => {
|
||||
dispatch(setActiveActivity(nextActivity));
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO(gatzjames): make import on navigation optional
|
||||
// Delaying generation so design to debug mode is smooth
|
||||
dispatch(setActiveActivity(nextActivity));
|
||||
setTimeout(() => {
|
||||
importRaw(activeApiSpec.contents, {
|
||||
getWorkspaceId: () => Promise.resolve(workspaceId),
|
||||
enableDiffBasedPatching: true,
|
||||
enableDiffDeep: true,
|
||||
bypassDiffProps: {
|
||||
url: true,
|
||||
},
|
||||
});
|
||||
}, 1000);
|
||||
}, [activeActivity, activeApiSpec, dispatch]);
|
||||
|
||||
const homeCallback = useCallback(() => {
|
||||
handleActivityChange({ workspaceId: activeWorkspace?._id, nextActivity: ACTIVITY_HOME });
|
||||
}, [activeWorkspace, handleActivityChange]);
|
||||
const activeProjectName = useSelector(selectActiveProjectName);
|
||||
const activeProject = useSelector(selectActiveProject);
|
||||
const isLoggedIn = useSelector(selectIsLoggedIn);
|
||||
const activeGitRepository = useSelector(selectActiveGitRepository);
|
||||
|
||||
if (!activeWorkspace || !activeApiSpec || !activity) {
|
||||
const vcs = useVCS({
|
||||
workspaceId: activeWorkspace?._id,
|
||||
});
|
||||
|
||||
const gitVCS = useGitVCS({
|
||||
workspaceId: activeWorkspace?._id,
|
||||
projectId: activeProject?._id,
|
||||
gitRepository: activeGitRepository,
|
||||
});
|
||||
const isTeamSync = isLoggedIn && activeWorkspace && isCollection(activeWorkspace) && isRemoteProject(activeProject) && vcs;
|
||||
const isGitSync = activeWorkspace && gitVCS && isDesign(activeWorkspace);
|
||||
if (!activeWorkspace || !activeApiSpec || !activeActivity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const crumbs = [
|
||||
{ id: 'project', node: activeProjectName, onClick: homeCallback },
|
||||
{ id: 'project', node: activeProjectName, onClick: () => handleActivityChange({ workspaceId: activeWorkspace?._id, nextActivity: ACTIVITY_HOME }) },
|
||||
{ id: 'workspace', node: <WorkspaceDropdown key="workspace-dd" /> },
|
||||
];
|
||||
|
||||
@@ -41,11 +117,25 @@ export const WorkspacePageHeader: FunctionComponent<Props> = ({
|
||||
gridCenter={
|
||||
<ActivityToggle
|
||||
workspace={activeWorkspace}
|
||||
activity={activity}
|
||||
activity={activeActivity}
|
||||
handleActivityChange={handleActivityChange}
|
||||
/>
|
||||
}
|
||||
gridRight={gridRight}
|
||||
gridRight={isTeamSync ? <SyncDropdown
|
||||
workspace={activeWorkspace}
|
||||
project={activeProject}
|
||||
vcs={vcs}
|
||||
/> : isGitSync ? <GitSyncDropdown
|
||||
className="margin-left"
|
||||
workspace={activeWorkspace}
|
||||
gitRepository={activeGitRepository}
|
||||
vcs={gitVCS}
|
||||
renderDropdownButton={children => (
|
||||
<DropdownButton className="btn--clicky-small btn-sync">
|
||||
{children}
|
||||
</DropdownButton>
|
||||
)}
|
||||
/> : null}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC, Fragment, ReactNode, useEffect } from 'react';
|
||||
import React, { FC, Fragment, useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { SegmentEvent, trackSegmentEvent } from '../../common/analytics';
|
||||
@@ -6,25 +6,19 @@ 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';
|
||||
import { selectSidebarFilter } from '../redux/sidebar-selectors';
|
||||
import { EnvironmentsDropdown } from './dropdowns/environments-dropdown';
|
||||
import { SyncDropdown } from './dropdowns/sync-dropdown';
|
||||
import { ErrorBoundary } from './error-boundary';
|
||||
import { useDocBodyKeyboardShortcuts } from './keydown-binder';
|
||||
import { showModal } from './modals';
|
||||
@@ -46,25 +40,8 @@ import { SidebarFilter } from './sidebar/sidebar-filter';
|
||||
import { WebSocketRequestPane } from './websockets/websocket-request-pane';
|
||||
import { WebSocketResponsePane } from './websockets/websocket-response-pane';
|
||||
import { WorkspacePageHeader } from './workspace-page-header';
|
||||
import type { HandleActivityChange } from './wrapper';
|
||||
|
||||
interface Props {
|
||||
gitSyncDropdown: ReactNode;
|
||||
handleActivityChange: HandleActivityChange;
|
||||
handleSetActiveEnvironment: (id: string | null) => void;
|
||||
handleImport: Function;
|
||||
vcs: VCS | null;
|
||||
}
|
||||
export const WrapperDebug: FC<Props> = ({
|
||||
gitSyncDropdown,
|
||||
handleActivityChange,
|
||||
handleSetActiveEnvironment,
|
||||
handleImport,
|
||||
vcs,
|
||||
}) => {
|
||||
const activeProject = useSelector(selectActiveProject);
|
||||
const isLoggedIn = useSelector(selectIsLoggedIn);
|
||||
|
||||
export const WrapperDebug: FC = () => {
|
||||
const activeEnvironment = useSelector(selectActiveEnvironment);
|
||||
const activeRequest = useSelector(selectActiveRequest);
|
||||
const activeWorkspace = useSelector(selectActiveWorkspace);
|
||||
@@ -72,7 +49,6 @@ export const WrapperDebug: FC<Props> = ({
|
||||
const sidebarFilter = useSelector(selectSidebarFilter);
|
||||
const activeWorkspaceMeta = useSelector(selectActiveWorkspaceMeta);
|
||||
|
||||
const isTeamSync = isLoggedIn && activeWorkspace && isCollection(activeWorkspace) && isRemoteProject(activeProject) && vcs;
|
||||
useDocBodyKeyboardShortcuts({
|
||||
request_togglePin:
|
||||
async () => {
|
||||
@@ -177,53 +153,16 @@ export const WrapperDebug: FC<Props> = ({
|
||||
};
|
||||
}, [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 (
|
||||
<PageLayout
|
||||
renderPageHeader={activeWorkspace ?
|
||||
<WorkspacePageHeader
|
||||
handleActivityChange={handleActivityChange}
|
||||
gridRight={isTeamSync ? <SyncDropdown
|
||||
workspace={activeWorkspace}
|
||||
project={activeProject}
|
||||
vcs={vcs}
|
||||
/> : isDesign(activeWorkspace) ? gitSyncDropdown : null}
|
||||
/>
|
||||
<WorkspacePageHeader />
|
||||
: null}
|
||||
renderPageSidebar={activeWorkspace ? <Fragment>
|
||||
<div className="sidebar__menu">
|
||||
<EnvironmentsDropdown
|
||||
activeEnvironment={activeEnvironment}
|
||||
environmentHighlightColorStyle={settings.environmentHighlightColorStyle}
|
||||
handleSetActiveEnvironment={handleSetActiveEnvironment}
|
||||
workspace={activeWorkspace}
|
||||
/>
|
||||
<button className="btn btn--super-compact" onClick={showCookiesModal}>
|
||||
@@ -263,7 +202,6 @@ export const WrapperDebug: FC<Props> = ({
|
||||
) : (
|
||||
<RequestPane
|
||||
environmentId={activeEnvironment ? activeEnvironment._id : ''}
|
||||
handleImport={handleImport}
|
||||
request={activeRequest}
|
||||
settings={settings}
|
||||
workspace={activeWorkspace}
|
||||
@@ -278,19 +216,12 @@ export const WrapperDebug: FC<Props> = ({
|
||||
<ErrorBoundary showAlert>
|
||||
{activeRequest && (
|
||||
isGrpcRequest(activeRequest) ? (
|
||||
<GrpcResponsePane
|
||||
activeRequest={activeRequest}
|
||||
/>
|
||||
<GrpcResponsePane activeRequest={activeRequest} />
|
||||
) : (
|
||||
isWebSocketRequest(activeRequest) ? (
|
||||
<WebSocketResponsePane
|
||||
requestId={activeRequest._id}
|
||||
/>
|
||||
<WebSocketResponsePane requestId={activeRequest._id} />
|
||||
) : (
|
||||
<ResponsePane
|
||||
handleSetFilter={handleSetResponseFilter}
|
||||
request={activeRequest}
|
||||
/>
|
||||
<ResponsePane request={activeRequest} />
|
||||
)
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IRuleResult } from '@stoplight/spectral-core';
|
||||
import { Notice, NoticeTable } from 'insomnia-components';
|
||||
import React, { createRef, FC, ReactNode, RefObject, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { createRef, FC, RefObject, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
import SwaggerUI from 'swagger-ui-react';
|
||||
@@ -19,7 +19,6 @@ import { ErrorBoundary } from './error-boundary';
|
||||
import { PageLayout } from './page-layout';
|
||||
import { SpecEditorSidebar } from './spec-editor/spec-editor-sidebar';
|
||||
import { WorkspacePageHeader } from './workspace-page-header';
|
||||
import type { HandleActivityChange } from './wrapper';
|
||||
|
||||
const EmptySpaceHelper = styled.div({
|
||||
...superFaint,
|
||||
@@ -30,6 +29,7 @@ const EmptySpaceHelper = styled.div({
|
||||
textAlign: 'center',
|
||||
});
|
||||
|
||||
// TODO(jackkav): find the right place to do this
|
||||
const spectral = initializeSpectral();
|
||||
|
||||
interface LintMessage extends Notice {
|
||||
@@ -48,7 +48,7 @@ const RenderEditor: FC<{ editor: RefObject<UnconnectedCodeEditor> }> = ({ editor
|
||||
return;
|
||||
}
|
||||
|
||||
await database.update({ ...activeApiSpec, modified: Date.now(), contents: value }, true);
|
||||
await database.update({ ...activeApiSpec, modified: Date.now(), contents: value }, true);
|
||||
}, [activeApiSpec]);
|
||||
|
||||
const uniquenessKey = `::${activeApiSpec?._id}::${gitVersion}::${syncVersion}`;
|
||||
@@ -68,7 +68,7 @@ const RenderEditor: FC<{ editor: RefObject<UnconnectedCodeEditor> }> = ({ editor
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
const update = async () => {
|
||||
// Lint only if spec has content
|
||||
// Lint only if spec has content
|
||||
if (contents && contents.length !== 0) {
|
||||
const results: LintMessage[] = (await spectral.run(contents))
|
||||
.filter(isLintError)
|
||||
@@ -233,24 +233,13 @@ const RenderPageSidebar: FC<{ editor: RefObject<UnconnectedCodeEditor> }> = ({ e
|
||||
);
|
||||
};
|
||||
|
||||
interface Props {
|
||||
gitSyncDropdown: ReactNode;
|
||||
handleActivityChange: HandleActivityChange;
|
||||
}
|
||||
|
||||
export const WrapperDesign: FC<Props> = ({
|
||||
gitSyncDropdown,
|
||||
handleActivityChange,
|
||||
}) => {
|
||||
export const WrapperDesign: FC = () => {
|
||||
const editor = createRef<UnconnectedCodeEditor>();
|
||||
|
||||
return (
|
||||
<PageLayout
|
||||
renderPageHeader={(
|
||||
<WorkspacePageHeader
|
||||
handleActivityChange={handleActivityChange}
|
||||
gridRight={gitSyncDropdown}
|
||||
/>
|
||||
<WorkspacePageHeader />
|
||||
)}
|
||||
renderPaneOne={<RenderEditor editor={editor} />}
|
||||
renderPaneTwo={<RenderPreview />}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from 'insomnia-components';
|
||||
import { generate, runTests, Test } from 'insomnia-testing';
|
||||
import { isEmpty } from 'ramda';
|
||||
import React, { FC, ReactNode, useCallback, useState } from 'react';
|
||||
import React, { FC, useCallback, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
|
||||
@@ -34,7 +34,6 @@ import { EmptyStatePane } from './panes/empty-state-pane';
|
||||
import type { Child } from './sidebar/sidebar-children';
|
||||
import { UnitTestEditable } from './unit-test-editable';
|
||||
import { WorkspacePageHeader } from './workspace-page-header';
|
||||
import type { HandleActivityChange } from './wrapper';
|
||||
|
||||
const HeaderButton = styled(Button)({
|
||||
'&&': {
|
||||
@@ -42,15 +41,7 @@ const HeaderButton = styled(Button)({
|
||||
},
|
||||
});
|
||||
|
||||
interface Props {
|
||||
gitSyncDropdown: ReactNode;
|
||||
handleActivityChange: HandleActivityChange;
|
||||
}
|
||||
|
||||
const WrapperUnitTest: FC<Props> = ({
|
||||
gitSyncDropdown,
|
||||
handleActivityChange,
|
||||
}) => {
|
||||
const WrapperUnitTest: FC = () => {
|
||||
const [testsRunning, setTestsRunning] = useState<UnitTest[] | null>(null);
|
||||
const [resultsError, setResultsError] = useState<string | null>(null);
|
||||
const lintOptions = {
|
||||
@@ -421,10 +412,7 @@ const WrapperUnitTest: FC<Props> = ({
|
||||
<TestRunStatus testsRunning={testsRunning} resultsError={resultsError} />
|
||||
}
|
||||
renderPageHeader={
|
||||
<WorkspacePageHeader
|
||||
handleActivityChange={handleActivityChange}
|
||||
gridRight={gitSyncDropdown}
|
||||
/>
|
||||
<WorkspacePageHeader />
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -432,9 +420,9 @@ const WrapperUnitTest: FC<Props> = ({
|
||||
|
||||
export default WrapperUnitTest;
|
||||
|
||||
interface TestRunStatusProps{
|
||||
testsRunning:UnitTest[] | null;
|
||||
resultsError:string | null;
|
||||
interface TestRunStatusProps {
|
||||
testsRunning: UnitTest[] | null;
|
||||
resultsError: string | null;
|
||||
}
|
||||
const TestRunStatus: FC<TestRunStatusProps> = ({ testsRunning, resultsError }) => {
|
||||
const activeUnitTestResult = useSelector(selectActiveUnitTestResult);
|
||||
|
||||
@@ -1,465 +0,0 @@
|
||||
import { autoBindMethodsForReact } from 'class-autobind-decorator';
|
||||
import * as importers from 'insomnia-importers';
|
||||
import React, { FC, Fragment, lazy, PureComponent, Suspense } from 'react';
|
||||
import { connect, useSelector } from 'react-redux';
|
||||
import { Route, Routes, useNavigate } from 'react-router-dom';
|
||||
import { AnyAction, bindActionCreators, Dispatch } from 'redux';
|
||||
|
||||
import type { GlobalActivity } from '../../common/constants';
|
||||
import {
|
||||
ACTIVITY_DEBUG,
|
||||
ACTIVITY_HOME,
|
||||
ACTIVITY_SPEC,
|
||||
ACTIVITY_UNIT_TEST,
|
||||
AUTOBIND_CFG,
|
||||
} from '../../common/constants';
|
||||
import { database } from '../../common/database';
|
||||
import { importRaw } from '../../common/import';
|
||||
import { initializeSpectral, isLintError } from '../../common/spectral';
|
||||
import * as models from '../../models/index';
|
||||
import {
|
||||
isRequest,
|
||||
} from '../../models/request';
|
||||
import { GitVCS } from '../../sync/git/git-vcs';
|
||||
import { VCS } from '../../sync/vcs/vcs';
|
||||
import { CookieModifyModal } from '../components/modals/cookie-modify-modal';
|
||||
import { GrpcDispatchModalWrapper } from '../context/grpc';
|
||||
import { RootState } from '../redux/modules';
|
||||
import { setActiveActivity } from '../redux/modules/global';
|
||||
import { selectActiveActivity, selectActiveApiSpec, selectActiveCookieJar, selectActiveEnvironment, selectActiveGitRepository, selectActiveRequest, selectActiveResponse, selectActiveWorkspace, selectActiveWorkspaceMeta, selectSettings } from '../redux/selectors';
|
||||
import { DropdownButton } from './base/dropdown/dropdown-button';
|
||||
import GitSyncDropdown from './dropdowns/git-sync-dropdown';
|
||||
import { ErrorBoundary } from './error-boundary';
|
||||
import { AddKeyCombinationModal } from './modals/add-key-combination-modal';
|
||||
import { AlertModal } from './modals/alert-modal';
|
||||
import { AnalyticsModal } from './modals/analytics-modal';
|
||||
import { AskModal } from './modals/ask-modal';
|
||||
import { CodePromptModal } from './modals/code-prompt-modal';
|
||||
import { CookiesModalFC } from './modals/cookies-modal';
|
||||
import { EnvironmentEditModal } from './modals/environment-edit-modal';
|
||||
import { ErrorModal } from './modals/error-modal';
|
||||
import { ExportRequestsModal } from './modals/export-requests-modal';
|
||||
import { FilterHelpModal } from './modals/filter-help-modal';
|
||||
import { GenerateCodeModal } from './modals/generate-code-modal';
|
||||
import { GenerateConfigModal } from './modals/generate-config-modal';
|
||||
import { GitBranchesModal } from './modals/git-branches-modal';
|
||||
import { GitLogModal } from './modals/git-log-modal';
|
||||
import { GitRepositorySettingsModal } from './modals/git-repository-settings-modal';
|
||||
import { GitStagingModal } from './modals/git-staging-modal';
|
||||
import { registerModal, showModal } from './modals/index';
|
||||
import { LoginModal } from './modals/login-modal';
|
||||
import { NunjucksModal } from './modals/nunjucks-modal';
|
||||
import ProjectSettingsModal from './modals/project-settings-modal';
|
||||
import { PromptModal } from './modals/prompt-modal';
|
||||
import ProtoFilesModal from './modals/proto-files-modal';
|
||||
import { RequestGroupSettingsModal } from './modals/request-group-settings-modal';
|
||||
import { RequestRenderErrorModal } from './modals/request-render-error-modal';
|
||||
import { RequestSettingsModal } from './modals/request-settings-modal';
|
||||
import { RequestSwitcherModal } from './modals/request-switcher-modal';
|
||||
import { ResponseDebugModal } from './modals/response-debug-modal';
|
||||
import { SelectModal } from './modals/select-modal';
|
||||
import { SettingsModal } from './modals/settings-modal';
|
||||
import { SyncBranchesModal } from './modals/sync-branches-modal';
|
||||
import { SyncDeleteModal } from './modals/sync-delete-modal';
|
||||
import { SyncHistoryModal } from './modals/sync-history-modal';
|
||||
import { SyncMergeModal } from './modals/sync-merge-modal';
|
||||
import { SyncStagingModal } from './modals/sync-staging-modal';
|
||||
import { WorkspaceDuplicateModal } from './modals/workspace-duplicate-modal';
|
||||
import { WorkspaceEnvironmentsEditModal } from './modals/workspace-environments-edit-modal';
|
||||
import { WorkspaceSettingsModal } from './modals/workspace-settings-modal';
|
||||
import { WrapperModal } from './modals/wrapper-modal';
|
||||
|
||||
const lazyWithPreload = <T extends FC<any>>(
|
||||
importFn: () => Promise<{ default: T }>
|
||||
) => {
|
||||
const LazyComponent = lazy(importFn);
|
||||
const preload = () => importFn();
|
||||
|
||||
return [LazyComponent, preload] as const;
|
||||
};
|
||||
|
||||
const [WrapperHome, preloadWrapperHome] = lazyWithPreload(
|
||||
() => import('./wrapper-home')
|
||||
);
|
||||
const [WrapperDebug, preloadWrapperDebug] = lazyWithPreload(
|
||||
() => import('./wrapper-debug')
|
||||
);
|
||||
const [WrapperDesign, preloadWrapperDesign] = lazyWithPreload(
|
||||
() => import('./wrapper-design')
|
||||
);
|
||||
const [WrapperUnitTest, preloadWrapperUnitTest] = lazyWithPreload(
|
||||
() => import('./wrapper-unit-test')
|
||||
);
|
||||
|
||||
preloadWrapperHome();
|
||||
preloadWrapperDebug();
|
||||
preloadWrapperDesign();
|
||||
preloadWrapperUnitTest();
|
||||
|
||||
const LoadingIndicator = () => (<div
|
||||
id="app-loading-indicator"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: '0',
|
||||
left: '0',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<img src="./ui/images/insomnia-logo.svg" alt="Insomnia" />
|
||||
</div>);
|
||||
|
||||
const ActivityRouter = () => {
|
||||
const selectedActivity = useSelector(selectActiveActivity);
|
||||
const activeWorkspace = useSelector(selectActiveWorkspace);
|
||||
// If there is no active workspace, we want to navigate to home no matter what the previous activity was
|
||||
const activity = activeWorkspace ? selectedActivity : ACTIVITY_HOME;
|
||||
const navigate = useNavigate();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (activity) {
|
||||
navigate(activity);
|
||||
}
|
||||
}, [activity, navigate]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const spectral = initializeSpectral();
|
||||
|
||||
export type Props = ReturnType<typeof mapStateToProps> & ReturnType<typeof mapDispatchToProps> & {
|
||||
vcs: VCS | null;
|
||||
gitVCS: GitVCS | null;
|
||||
};
|
||||
|
||||
export type HandleActivityChange = (options: {
|
||||
workspaceId?: string;
|
||||
nextActivity: GlobalActivity;
|
||||
}) => Promise<void>;
|
||||
|
||||
interface State {
|
||||
activeGitBranch: string;
|
||||
}
|
||||
|
||||
@autoBindMethodsForReact(AUTOBIND_CFG)
|
||||
export class WrapperClass extends PureComponent<Props, State> {
|
||||
state: State = {
|
||||
activeGitBranch: 'no-vcs',
|
||||
};
|
||||
|
||||
async _handleImport(text: string) {
|
||||
const { activeRequest } = this.props;
|
||||
|
||||
// Allow user to paste any import file into the url. If it results in
|
||||
// only one item, it will overwrite the current request.
|
||||
try {
|
||||
const { data } = await importers.convert(text);
|
||||
const { resources } = data;
|
||||
const r = resources[0];
|
||||
|
||||
if (r && r._type === 'request' && activeRequest && isRequest(activeRequest)) {
|
||||
// Only pull fields that we want to update
|
||||
return database.update({
|
||||
...activeRequest,
|
||||
modified: Date.now(),
|
||||
url: r.url,
|
||||
method: r.method,
|
||||
headers: r.headers,
|
||||
body: r.body,
|
||||
authentication: r.authentication,
|
||||
parameters: r.parameters,
|
||||
},
|
||||
// Pass true to indicate that this is an import
|
||||
true
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// Import failed, that's alright
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async _handleWorkspaceActivityChange({ workspaceId, nextActivity }: Parameters<HandleActivityChange>[0]): ReturnType<HandleActivityChange> {
|
||||
const { activeActivity, activeApiSpec, handleSetActiveActivity } = this.props;
|
||||
|
||||
// Remember last activity on workspace for later, but only if it isn't HOME
|
||||
if (workspaceId && nextActivity !== ACTIVITY_HOME) {
|
||||
await models.workspaceMeta.updateByParentId(workspaceId, {
|
||||
activeActivity: nextActivity,
|
||||
});
|
||||
}
|
||||
|
||||
const editingASpec = activeActivity === ACTIVITY_SPEC;
|
||||
|
||||
if (!editingASpec) {
|
||||
handleSetActiveActivity(nextActivity);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!activeApiSpec || !workspaceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const goingToDebugOrTest = nextActivity === ACTIVITY_DEBUG || nextActivity === ACTIVITY_UNIT_TEST;
|
||||
|
||||
// If editing a spec and not going to debug or test, don't regenerate anything
|
||||
if (editingASpec && !goingToDebugOrTest) {
|
||||
handleSetActiveActivity(nextActivity);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle switching away from the spec design activity. For this, we want to generate
|
||||
// requests that can be accessed from debug or test.
|
||||
// If there are errors in the spec, show the user a warning first
|
||||
const results = (await spectral.run(activeApiSpec.contents)).filter(isLintError);
|
||||
if (activeApiSpec.contents && results && results.length) {
|
||||
showModal(AlertModal, {
|
||||
title: 'Error Generating Configuration',
|
||||
message: 'Some requests may not be available due to errors found in the specification. We recommend fixing errors before proceeding.',
|
||||
okLabel: 'Proceed',
|
||||
addCancel: true,
|
||||
onConfirm: () => {
|
||||
handleSetActiveActivity(nextActivity);
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Delaying generation so design to debug mode is smooth
|
||||
handleSetActiveActivity(nextActivity);
|
||||
setTimeout(() => {
|
||||
importRaw(activeApiSpec.contents, {
|
||||
getWorkspaceId: () => Promise.resolve(workspaceId),
|
||||
enableDiffBasedPatching: true,
|
||||
enableDiffDeep: true,
|
||||
bypassDiffProps: {
|
||||
url: true,
|
||||
},
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
_handleGitBranchChanged(branch: string) {
|
||||
this.setState({
|
||||
activeGitBranch: branch || 'no-vcs',
|
||||
});
|
||||
}
|
||||
|
||||
async _handleSetActiveEnvironment(activeEnvironmentId: string | null) {
|
||||
if (this.props.activeWorkspaceMeta) {
|
||||
await models.workspaceMeta.update(this.props.activeWorkspaceMeta, { activeEnvironmentId });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
activeCookieJar,
|
||||
activeEnvironment,
|
||||
activeGitRepository,
|
||||
activeWorkspace,
|
||||
activeApiSpec,
|
||||
gitVCS,
|
||||
vcs,
|
||||
} = this.props;
|
||||
|
||||
// Setup git sync dropdown for use in Design/Debug pages
|
||||
let gitSyncDropdown: JSX.Element | null = null;
|
||||
|
||||
if (activeWorkspace && gitVCS) {
|
||||
gitSyncDropdown = (
|
||||
<GitSyncDropdown
|
||||
className="margin-left"
|
||||
workspace={activeWorkspace}
|
||||
gitRepository={activeGitRepository}
|
||||
vcs={gitVCS}
|
||||
handleGitBranchChanged={this._handleGitBranchChanged}
|
||||
renderDropdownButton={children => (
|
||||
<DropdownButton className="btn--clicky-small btn-sync">
|
||||
{children}
|
||||
</DropdownButton>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div key="modals" className="modals">
|
||||
<ErrorBoundary showAlert>
|
||||
<AnalyticsModal />
|
||||
<AlertModal ref={registerModal} />
|
||||
<ErrorModal ref={registerModal} />
|
||||
<PromptModal ref={registerModal} />
|
||||
<WrapperModal ref={registerModal} />
|
||||
<LoginModal ref={registerModal} />
|
||||
<AskModal ref={registerModal} />
|
||||
<SelectModal ref={registerModal} />
|
||||
<FilterHelpModal ref={registerModal} />
|
||||
<RequestRenderErrorModal ref={registerModal} />
|
||||
<GenerateConfigModal ref={registerModal} />
|
||||
<ProjectSettingsModal ref={registerModal} />
|
||||
<WorkspaceDuplicateModal ref={registerModal} vcs={vcs || undefined} />
|
||||
<CodePromptModal ref={registerModal} />
|
||||
<RequestSettingsModal ref={instance => registerModal(instance, 'RequestSettingsModal')} />
|
||||
<RequestGroupSettingsModal ref={registerModal} />
|
||||
|
||||
{activeWorkspace ? <>
|
||||
{/* TODO: Figure out why cookieJar is sometimes null */}
|
||||
{activeCookieJar ? <>
|
||||
<CookiesModalFC
|
||||
ref={registerModal}
|
||||
/>
|
||||
<CookieModifyModal ref={registerModal} />
|
||||
</> : null}
|
||||
|
||||
<NunjucksModal
|
||||
ref={registerModal}
|
||||
workspace={activeWorkspace}
|
||||
/>
|
||||
|
||||
{activeApiSpec ? <WorkspaceSettingsModal
|
||||
ref={registerModal}
|
||||
workspace={activeWorkspace}
|
||||
apiSpec={activeApiSpec}
|
||||
/> : null}
|
||||
</> : null}
|
||||
|
||||
<GenerateCodeModal
|
||||
ref={registerModal}
|
||||
environmentId={activeEnvironment ? activeEnvironment._id : 'n/a'}
|
||||
/>
|
||||
|
||||
<SettingsModal ref={instance => registerModal(instance, 'SettingsModal')} />
|
||||
<ResponseDebugModal ref={instance => registerModal(instance, 'ResponseDebugModal')} />
|
||||
|
||||
<RequestSwitcherModal ref={instance => registerModal(instance, 'RequestSwitcherModal')} />
|
||||
|
||||
<EnvironmentEditModal
|
||||
ref={registerModal}
|
||||
onChange={models.requestGroup.update}
|
||||
/>
|
||||
|
||||
<GitRepositorySettingsModal ref={registerModal} />
|
||||
|
||||
{activeWorkspace && gitVCS ? (
|
||||
<Fragment>
|
||||
<GitStagingModal ref={registerModal} workspace={activeWorkspace} vcs={gitVCS} gitRepository={activeGitRepository} />
|
||||
<GitLogModal ref={registerModal} vcs={gitVCS} />
|
||||
{activeGitRepository !== null && (
|
||||
<GitBranchesModal
|
||||
ref={registerModal}
|
||||
vcs={gitVCS}
|
||||
gitRepository={activeGitRepository}
|
||||
handleGitBranchChanged={this._handleGitBranchChanged}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
) : null}
|
||||
|
||||
{activeWorkspace && vcs ? (
|
||||
<Fragment>
|
||||
<SyncStagingModal ref={registerModal} vcs={vcs} />
|
||||
<SyncMergeModal ref={registerModal} vcs={vcs} />
|
||||
<SyncBranchesModal ref={registerModal} vcs={vcs} />
|
||||
<SyncDeleteModal ref={registerModal} vcs={vcs} />
|
||||
<SyncHistoryModal ref={registerModal} vcs={vcs} />
|
||||
</Fragment>
|
||||
) : null}
|
||||
|
||||
<WorkspaceEnvironmentsEditModal
|
||||
ref={registerModal}
|
||||
handleSetActiveEnvironment={this._handleSetActiveEnvironment}
|
||||
activeEnvironmentId={activeEnvironment ? activeEnvironment._id : null}
|
||||
/>
|
||||
|
||||
<AddKeyCombinationModal ref={instance => registerModal(instance, 'AddKeyCombinationModal')} />
|
||||
<ExportRequestsModal ref={registerModal} />
|
||||
|
||||
<GrpcDispatchModalWrapper>
|
||||
{dispatch => (
|
||||
<ProtoFilesModal
|
||||
ref={registerModal}
|
||||
grpcDispatch={dispatch}
|
||||
/>
|
||||
)}
|
||||
</GrpcDispatchModalWrapper>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
<Routes>
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
<Suspense fallback={<LoadingIndicator />}>
|
||||
<WrapperHome
|
||||
vcs={vcs}
|
||||
/>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={ACTIVITY_UNIT_TEST}
|
||||
element={
|
||||
<Suspense fallback={<LoadingIndicator />}>
|
||||
<WrapperUnitTest
|
||||
gitSyncDropdown={gitSyncDropdown}
|
||||
handleActivityChange={this._handleWorkspaceActivityChange}
|
||||
/>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={ACTIVITY_SPEC}
|
||||
element={
|
||||
<Suspense fallback={<LoadingIndicator />}>
|
||||
<WrapperDesign
|
||||
gitSyncDropdown={gitSyncDropdown}
|
||||
handleActivityChange={this._handleWorkspaceActivityChange}
|
||||
/>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={ACTIVITY_DEBUG}
|
||||
element={
|
||||
<Suspense fallback={<LoadingIndicator />}>
|
||||
<WrapperDebug
|
||||
gitSyncDropdown={gitSyncDropdown}
|
||||
handleActivityChange={this._handleWorkspaceActivityChange}
|
||||
handleSetActiveEnvironment={this._handleSetActiveEnvironment}
|
||||
handleImport={this._handleImport}
|
||||
vcs={vcs}
|
||||
/>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
<ActivityRouter />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
const mapStateToProps = (state: RootState) => ({
|
||||
activeActivity: selectActiveActivity(state),
|
||||
activeRequest: selectActiveRequest(state),
|
||||
activeCookieJar: selectActiveCookieJar(state),
|
||||
activeEnvironment: selectActiveEnvironment(state),
|
||||
activeGitRepository: selectActiveGitRepository(state),
|
||||
activeWorkspace: selectActiveWorkspace(state),
|
||||
activeWorkspaceMeta: selectActiveWorkspaceMeta(state),
|
||||
activeApiSpec: selectActiveApiSpec(state),
|
||||
activeResponse: selectActiveResponse(state),
|
||||
settings: selectSettings(state),
|
||||
});
|
||||
const mapDispatchToProps = (dispatch: Dispatch<AnyAction>) => {
|
||||
const bound = bindActionCreators({ setActiveActivity }, dispatch);
|
||||
return {
|
||||
handleSetActiveActivity: bound.setActiveActivity,
|
||||
};
|
||||
};
|
||||
|
||||
export const Wrapper = connect(mapStateToProps, mapDispatchToProps, null, { forwardRef: true })(WrapperClass);
|
||||
@@ -1,12 +1,24 @@
|
||||
import { FC } from 'react';
|
||||
import { ipcRenderer } from 'electron/renderer';
|
||||
import { FC, useEffect } from 'react';
|
||||
|
||||
import { useAppCommands } from '../hooks/use-app-commands';
|
||||
import { useDragAndDropImportFile } from '../hooks/use-drag-and-drop-import-file';
|
||||
import { useGlobalKeyboardShortcuts } from '../hooks/use-global-keyboard-shortcuts';
|
||||
import { useSettingsSideEffects } from '../hooks/use-settings-side-effects';
|
||||
import { useSyncMigration } from '../hooks/use-sync-migration';
|
||||
import { useThemeChange } from '../hooks/use-theme-change';
|
||||
|
||||
export const AppHooks: FC = () => {
|
||||
useSyncMigration();
|
||||
useSettingsSideEffects();
|
||||
useGlobalKeyboardShortcuts();
|
||||
useDragAndDropImportFile();
|
||||
useThemeChange();
|
||||
useAppCommands();
|
||||
// Give it a bit before letting the backend know it's ready
|
||||
useEffect(() => {
|
||||
setTimeout(() => ipcRenderer.send('window-ready'), 500);
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -1,40 +1,66 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
import path from 'path';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { parse as urlParse } from 'url';
|
||||
import React, { FC, Fragment, lazy, Suspense, useEffect, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Route, Routes, useNavigate } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
ACTIVITY_DEBUG,
|
||||
ACTIVITY_HOME,
|
||||
ACTIVITY_SPEC,
|
||||
ACTIVITY_UNIT_TEST,
|
||||
} from '../../common/constants';
|
||||
import { database as db } from '../../common/database';
|
||||
import { getDataDirectory } from '../../common/electron-helpers';
|
||||
import {
|
||||
generateId,
|
||||
} from '../../common/misc';
|
||||
import * as models from '../../models';
|
||||
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';
|
||||
import { NeDBClient } from '../../sync/git/ne-db-client';
|
||||
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 { ErrorBoundary } from '../components/error-boundary';
|
||||
import { AddKeyCombinationModal } from '../components/modals/add-key-combination-modal';
|
||||
import { AlertModal } from '../components/modals/alert-modal';
|
||||
import { showModal } from '../components/modals/index';
|
||||
import { AnalyticsModal } from '../components/modals/analytics-modal';
|
||||
import { AskModal } from '../components/modals/ask-modal';
|
||||
import { CodePromptModal } from '../components/modals/code-prompt-modal';
|
||||
import { CookieModifyModal } from '../components/modals/cookie-modify-modal';
|
||||
import { CookiesModalFC } from '../components/modals/cookies-modal';
|
||||
import { EnvironmentEditModal } from '../components/modals/environment-edit-modal';
|
||||
import { ErrorModal } from '../components/modals/error-modal';
|
||||
import { ExportRequestsModal } from '../components/modals/export-requests-modal';
|
||||
import { FilterHelpModal } from '../components/modals/filter-help-modal';
|
||||
import { GenerateCodeModal } from '../components/modals/generate-code-modal';
|
||||
import { GenerateConfigModal } from '../components/modals/generate-config-modal';
|
||||
import { GitBranchesModal } from '../components/modals/git-branches-modal';
|
||||
import { GitLogModal } from '../components/modals/git-log-modal';
|
||||
import { GitRepositorySettingsModal } from '../components/modals/git-repository-settings-modal';
|
||||
import { GitStagingModal } from '../components/modals/git-staging-modal';
|
||||
import { registerModal } from '../components/modals/index';
|
||||
import { LoginModal } from '../components/modals/login-modal';
|
||||
import { NunjucksModal } from '../components/modals/nunjucks-modal';
|
||||
import ProjectSettingsModal from '../components/modals/project-settings-modal';
|
||||
import { PromptModal } from '../components/modals/prompt-modal';
|
||||
import ProtoFilesModal from '../components/modals/proto-files-modal';
|
||||
import { RequestGroupSettingsModal } from '../components/modals/request-group-settings-modal';
|
||||
import { RequestRenderErrorModal } from '../components/modals/request-render-error-modal';
|
||||
import { RequestSettingsModal } from '../components/modals/request-settings-modal';
|
||||
import { RequestSwitcherModal } from '../components/modals/request-switcher-modal';
|
||||
import { ResponseDebugModal } from '../components/modals/response-debug-modal';
|
||||
import { SelectModal } from '../components/modals/select-modal';
|
||||
import { SettingsModal } from '../components/modals/settings-modal';
|
||||
import { SyncBranchesModal } from '../components/modals/sync-branches-modal';
|
||||
import { SyncDeleteModal } from '../components/modals/sync-delete-modal';
|
||||
import { SyncHistoryModal } from '../components/modals/sync-history-modal';
|
||||
import { SyncMergeModal } from '../components/modals/sync-merge-modal';
|
||||
import { SyncStagingModal } from '../components/modals/sync-staging-modal';
|
||||
import { WorkspaceDuplicateModal } from '../components/modals/workspace-duplicate-modal';
|
||||
import { WorkspaceEnvironmentsEditModal } from '../components/modals/workspace-environments-edit-modal';
|
||||
import { WorkspaceSettingsModal } from '../components/modals/workspace-settings-modal';
|
||||
import { WrapperModal } from '../components/modals/wrapper-modal';
|
||||
import { Toast } from '../components/toast';
|
||||
import { type WrapperClass, Wrapper } from '../components/wrapper';
|
||||
import withDragDropContext from '../context/app/drag-drop-context';
|
||||
import { GrpcProvider } from '../context/grpc';
|
||||
import { GrpcDispatchModalWrapper, GrpcProvider } from '../context/grpc';
|
||||
import { NunjucksEnabledProvider } from '../context/nunjucks/nunjucks-enabled-context';
|
||||
import { useGitVCS } from '../hooks/use-git-vcs';
|
||||
import { useVCS } from '../hooks/use-vcs';
|
||||
import {
|
||||
newCommand,
|
||||
} from '../redux/modules/global';
|
||||
import { importUri } from '../redux/modules/import';
|
||||
import {
|
||||
selectActiveActivity,
|
||||
selectActiveApiSpec,
|
||||
selectActiveCookieJar,
|
||||
selectActiveEnvironment,
|
||||
selectActiveGitRepository,
|
||||
selectActiveProject,
|
||||
selectActiveWorkspace,
|
||||
@@ -42,166 +68,88 @@ import {
|
||||
selectEnvironments,
|
||||
selectIsFinishedBooting,
|
||||
selectIsLoggedIn,
|
||||
selectSettings,
|
||||
} from '../redux/selectors';
|
||||
import { AppHooks } from './app-hooks';
|
||||
|
||||
const lazyWithPreload = <T extends FC<any>>(
|
||||
importFn: () => Promise<{ default: T }>
|
||||
) => {
|
||||
const LazyComponent = lazy(importFn);
|
||||
const preload = () => importFn();
|
||||
|
||||
return [LazyComponent, preload] as const;
|
||||
};
|
||||
|
||||
const [WrapperHome, preloadWrapperHome] = lazyWithPreload(
|
||||
() => import('../components/wrapper-home')
|
||||
);
|
||||
const [WrapperDebug, preloadWrapperDebug] = lazyWithPreload(
|
||||
() => import('../components/wrapper-debug')
|
||||
);
|
||||
const [WrapperDesign, preloadWrapperDesign] = lazyWithPreload(
|
||||
() => import('../components/wrapper-design')
|
||||
);
|
||||
const [WrapperUnitTest, preloadWrapperUnitTest] = lazyWithPreload(
|
||||
() => import('../components/wrapper-unit-test')
|
||||
);
|
||||
|
||||
preloadWrapperHome();
|
||||
preloadWrapperDebug();
|
||||
preloadWrapperDesign();
|
||||
preloadWrapperUnitTest();
|
||||
|
||||
const LoadingIndicator = () => (<div
|
||||
id="app-loading-indicator"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: '0',
|
||||
left: '0',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<img src="./ui/images/insomnia-logo.svg" alt="Insomnia" />
|
||||
</div>);
|
||||
|
||||
const ActivityRouter = () => {
|
||||
const selectedActivity = useSelector(selectActiveActivity);
|
||||
const activeWorkspace = useSelector(selectActiveWorkspace);
|
||||
// If there is no active workspace, we want to navigate to home no matter what the previous activity was
|
||||
const activity = activeWorkspace ? selectedActivity : ACTIVITY_HOME;
|
||||
const navigate = useNavigate();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (activity) {
|
||||
navigate(activity);
|
||||
}
|
||||
}, [activity, navigate]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
interface State {
|
||||
isMigratingChildren: boolean;
|
||||
}
|
||||
|
||||
function useVCS({
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId?: string;
|
||||
}) {
|
||||
const vcsInstanceRef = useRef<VCS | null>(null);
|
||||
const [vcs, setVCS] = useState<VCS | null>(null);
|
||||
const updateVCSLock = useRef<boolean | string>(false);
|
||||
|
||||
// Update VCS when the active workspace changes
|
||||
useEffect(() => {
|
||||
const lock = generateId();
|
||||
updateVCSLock.current = lock;
|
||||
|
||||
// Set vcs to null while we update it
|
||||
setVCS(null);
|
||||
|
||||
if (!vcsInstanceRef.current) {
|
||||
const driver = FileSystemDriver.create(getDataDirectory());
|
||||
|
||||
vcsInstanceRef.current = new VCS(driver, async conflicts => {
|
||||
return new Promise(resolve => {
|
||||
showModal(SyncMergeModal, {
|
||||
conflicts,
|
||||
handleDone: (conflicts: MergeConflict[]) => resolve(conflicts),
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (workspaceId) {
|
||||
vcsInstanceRef.current.switchProject(workspaceId);
|
||||
} else {
|
||||
vcsInstanceRef.current.clearBackendProject();
|
||||
}
|
||||
|
||||
// Prevent a potential race-condition when _updateVCS() gets called for different workspaces in rapid succession
|
||||
if (updateVCSLock.current === lock) {
|
||||
setVCS(vcsInstanceRef.current);
|
||||
}
|
||||
}, [workspaceId]);
|
||||
|
||||
return vcs;
|
||||
}
|
||||
|
||||
function useGitVCS({
|
||||
workspaceId,
|
||||
projectId,
|
||||
gitRepository,
|
||||
}: {
|
||||
workspaceId?: string;
|
||||
projectId: string;
|
||||
gitRepository?: GitRepository | null;
|
||||
}) {
|
||||
const gitVCSInstanceRef = useRef<GitVCS | null>(null);
|
||||
const [gitVCS, setGitVCS] = useState<GitVCS | null>(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();
|
||||
}
|
||||
|
||||
isMounted && setGitVCS(gitVCSInstanceRef.current);
|
||||
}
|
||||
|
||||
update();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [workspaceId, projectId, gitRepository]);
|
||||
|
||||
return gitVCS;
|
||||
}
|
||||
|
||||
const App = () => {
|
||||
const [state, setState] = useState<State>({
|
||||
isMigratingChildren: false,
|
||||
});
|
||||
|
||||
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 activeGitRepository = useSelector(selectActiveGitRepository);
|
||||
const activeEnvironment = useSelector(selectActiveEnvironment);
|
||||
const activeProject = useSelector(selectActiveProject);
|
||||
|
||||
const vcs = useVCS({
|
||||
workspaceId: activeWorkspace?._id,
|
||||
});
|
||||
@@ -212,26 +160,25 @@ const App = () => {
|
||||
gitRepository: activeGitRepository,
|
||||
});
|
||||
|
||||
const wrapperRef = useRef<WrapperClass | null>(null);
|
||||
|
||||
async function handleSetActiveEnvironment(activeEnvironmentId: string | null) {
|
||||
if (activeWorkspaceMeta) {
|
||||
await models.workspaceMeta.update(activeWorkspaceMeta, { activeEnvironmentId });
|
||||
}
|
||||
}
|
||||
// Ensure Children: Make sure cookies, env, and meta models are created under this workspace
|
||||
useEffect(() => {
|
||||
if (!activeWorkspace) {
|
||||
return;
|
||||
}
|
||||
|
||||
const baseEnvironments = environments.filter(environment => environment.parentId === activeWorkspace._id);
|
||||
|
||||
// Nothing to do
|
||||
if (baseEnvironments.length && activeCookieJar && activeApiSpec && activeWorkspaceMeta) {
|
||||
const workspaceHasChildren = baseEnvironments.length && activeCookieJar && activeApiSpec && activeWorkspaceMeta;
|
||||
if (workspaceHasChildren) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We already started migrating. Let it finish.
|
||||
if (state.isMigratingChildren) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent rendering of everything until we check the workspace has cookies, env, and meta
|
||||
setState(state => ({ ...state, isMigratingChildren: true }));
|
||||
async function update() {
|
||||
@@ -245,72 +192,6 @@ const App = () => {
|
||||
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,
|
||||
);
|
||||
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: (
|
||||
<span>
|
||||
Import <code>{file.path}</code>?
|
||||
</span>
|
||||
),
|
||||
addCancel: true,
|
||||
});
|
||||
handleImportUri(`file://${file.path}`, { workspaceId: activeWorkspace?._id });
|
||||
},
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
if (state.isMigratingChildren) {
|
||||
console.log('[app] Waiting for migration to complete');
|
||||
return null;
|
||||
@@ -328,11 +209,144 @@ const App = () => {
|
||||
<AppHooks />
|
||||
<div className="app" key={uniquenessKey}>
|
||||
<ErrorBoundary showAlert>
|
||||
<Wrapper
|
||||
ref={wrapperRef}
|
||||
vcs={vcs}
|
||||
gitVCS={gitVCS}
|
||||
/>
|
||||
<div key="modals" className="modals">
|
||||
<ErrorBoundary showAlert>
|
||||
<AnalyticsModal />
|
||||
<AlertModal ref={registerModal} />
|
||||
<ErrorModal ref={registerModal} />
|
||||
<PromptModal ref={registerModal} />
|
||||
<WrapperModal ref={registerModal} />
|
||||
<LoginModal ref={registerModal} />
|
||||
<AskModal ref={registerModal} />
|
||||
<SelectModal ref={registerModal} />
|
||||
<FilterHelpModal ref={registerModal} />
|
||||
<RequestRenderErrorModal ref={registerModal} />
|
||||
<GenerateConfigModal ref={registerModal} />
|
||||
<ProjectSettingsModal ref={registerModal} />
|
||||
<WorkspaceDuplicateModal ref={registerModal} vcs={vcs || undefined} />
|
||||
<CodePromptModal ref={registerModal} />
|
||||
<RequestSettingsModal ref={instance => registerModal(instance, 'RequestSettingsModal')} />
|
||||
<RequestGroupSettingsModal ref={registerModal} />
|
||||
|
||||
{activeWorkspace ? <>
|
||||
{/* TODO: Figure out why cookieJar is sometimes null */}
|
||||
{activeCookieJar ? <>
|
||||
<CookiesModalFC
|
||||
ref={registerModal}
|
||||
/>
|
||||
<CookieModifyModal ref={registerModal} />
|
||||
</> : null}
|
||||
|
||||
<NunjucksModal
|
||||
ref={registerModal}
|
||||
workspace={activeWorkspace}
|
||||
/>
|
||||
|
||||
{activeApiSpec ? <WorkspaceSettingsModal
|
||||
ref={registerModal}
|
||||
workspace={activeWorkspace}
|
||||
apiSpec={activeApiSpec}
|
||||
/> : null}
|
||||
</> : null}
|
||||
|
||||
<GenerateCodeModal
|
||||
ref={registerModal}
|
||||
environmentId={activeEnvironment ? activeEnvironment._id : 'n/a'}
|
||||
/>
|
||||
|
||||
<SettingsModal ref={instance => registerModal(instance, 'SettingsModal')} />
|
||||
<ResponseDebugModal ref={instance => registerModal(instance, 'ResponseDebugModal')} />
|
||||
|
||||
<RequestSwitcherModal ref={instance => registerModal(instance, 'RequestSwitcherModal')} />
|
||||
|
||||
<EnvironmentEditModal
|
||||
ref={registerModal}
|
||||
onChange={models.requestGroup.update}
|
||||
/>
|
||||
|
||||
<GitRepositorySettingsModal ref={registerModal} />
|
||||
|
||||
{activeWorkspace && gitVCS ? (
|
||||
<Fragment>
|
||||
<GitStagingModal ref={registerModal} workspace={activeWorkspace} vcs={gitVCS} gitRepository={activeGitRepository} />
|
||||
<GitLogModal ref={registerModal} vcs={gitVCS} />
|
||||
{activeGitRepository !== null && (
|
||||
<GitBranchesModal
|
||||
ref={registerModal}
|
||||
vcs={gitVCS}
|
||||
gitRepository={activeGitRepository}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
) : null}
|
||||
|
||||
{activeWorkspace && vcs ? (
|
||||
<Fragment>
|
||||
<SyncStagingModal ref={registerModal} vcs={vcs} />
|
||||
<SyncMergeModal ref={registerModal} vcs={vcs} />
|
||||
<SyncBranchesModal ref={registerModal} vcs={vcs} />
|
||||
<SyncDeleteModal ref={registerModal} vcs={vcs} />
|
||||
<SyncHistoryModal ref={registerModal} vcs={vcs} />
|
||||
</Fragment>
|
||||
) : null}
|
||||
|
||||
<WorkspaceEnvironmentsEditModal
|
||||
ref={registerModal}
|
||||
handleSetActiveEnvironment={handleSetActiveEnvironment}
|
||||
activeEnvironmentId={activeEnvironment ? activeEnvironment._id : null}
|
||||
/>
|
||||
|
||||
<AddKeyCombinationModal ref={instance => registerModal(instance, 'AddKeyCombinationModal')} />
|
||||
<ExportRequestsModal ref={registerModal} />
|
||||
|
||||
<GrpcDispatchModalWrapper>
|
||||
{dispatch => (
|
||||
<ProtoFilesModal
|
||||
ref={registerModal}
|
||||
grpcDispatch={dispatch}
|
||||
/>
|
||||
)}
|
||||
</GrpcDispatchModalWrapper>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
<Routes>
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
<Suspense fallback={<LoadingIndicator />}>
|
||||
<WrapperHome
|
||||
vcs={vcs}
|
||||
/>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={ACTIVITY_UNIT_TEST}
|
||||
element={
|
||||
<Suspense fallback={<LoadingIndicator />}>
|
||||
<WrapperUnitTest />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={ACTIVITY_SPEC}
|
||||
element={
|
||||
<Suspense fallback={<LoadingIndicator />}>
|
||||
<WrapperDesign />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={ACTIVITY_DEBUG}
|
||||
element={
|
||||
<Suspense fallback={<LoadingIndicator />}>
|
||||
<WrapperDebug />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
<ActivityRouter />
|
||||
</ErrorBoundary>
|
||||
|
||||
<ErrorBoundary showAlert>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { parse } from 'url';
|
||||
|
||||
import { newCommand } from '../redux/modules/global';
|
||||
import { selectActiveWorkspace } from '../redux/selectors';
|
||||
|
||||
export const useAppCommands = () => {
|
||||
const dispatch = useDispatch();
|
||||
const handleCommand = dispatch(newCommand);
|
||||
const activeWorkspace = useSelector(selectActiveWorkspace);
|
||||
|
||||
// Handle Application Commands
|
||||
useEffect(() => {
|
||||
ipcRenderer.on('run-command', (_, commandUri) => {
|
||||
const parsed = parse(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]);
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { showModal } from '../components/modals';
|
||||
import { AlertModal } from '../components/modals/alert-modal';
|
||||
import { importUri } from '../redux/modules/import';
|
||||
import { selectActiveWorkspace } from '../redux/selectors';
|
||||
|
||||
export const useDragAndDropImportFile = () => {
|
||||
const activeWorkspace = useSelector(selectActiveWorkspace);
|
||||
const dispatch = useDispatch();
|
||||
const handleImportUri = dispatch(importUri);
|
||||
|
||||
// Global Drag and Drop for importing files
|
||||
useEffect(() => {
|
||||
// NOTE: This is required for "drop" event to trigger.
|
||||
document.addEventListener(
|
||||
'dragover',
|
||||
event => {
|
||||
event.preventDefault();
|
||||
},
|
||||
false,
|
||||
);
|
||||
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: (
|
||||
<span>
|
||||
Import <code>{file.path}</code>?
|
||||
</span>
|
||||
),
|
||||
addCancel: true,
|
||||
});
|
||||
handleImportUri(`file://${file.path}`, { workspaceId: activeWorkspace?._id });
|
||||
},
|
||||
false,
|
||||
);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
import path from 'path';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import * as models from '../../models';
|
||||
import { GitRepository } from '../../models/git-repository';
|
||||
import { fsClient } from '../../sync/git/fs-client';
|
||||
import { GIT_CLONE_DIR, GIT_INSOMNIA_DIR, GIT_INTERNAL_DIR, GitVCS } from '../../sync/git/git-vcs';
|
||||
import { NeDBClient } from '../../sync/git/ne-db-client';
|
||||
import { routableFSClient } from '../../sync/git/routable-fs-client';
|
||||
export function useGitVCS({
|
||||
workspaceId,
|
||||
projectId,
|
||||
gitRepository,
|
||||
}: {
|
||||
workspaceId?: string;
|
||||
projectId: string;
|
||||
gitRepository?: GitRepository | null;
|
||||
}) {
|
||||
const gitVCSInstanceRef = useRef<GitVCS | null>(null);
|
||||
const [gitVCS, setGitVCS] = useState<GitVCS | null>(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(
|
||||
process.env['INSOMNIA_DATA_PATH'] || window.app.getPath('userData'),
|
||||
`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();
|
||||
}
|
||||
|
||||
isMounted && setGitVCS(gitVCSInstanceRef.current);
|
||||
}
|
||||
|
||||
update();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [workspaceId, projectId, gitRepository]);
|
||||
|
||||
return gitVCS;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import * as themes from '../../plugins/misc';
|
||||
import { selectSettings } from '../redux/selectors';
|
||||
|
||||
export const useThemeChange = () => {
|
||||
const settings = useSelector(selectSettings);
|
||||
// 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));
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { getDataDirectory } from '../../common/electron-helpers';
|
||||
import { generateId } from '../../common/misc';
|
||||
import FileSystemDriver from '../../sync/store/drivers/file-system-driver';
|
||||
import { type MergeConflict } from '../../sync/types';
|
||||
import { VCS } from '../../sync/vcs/vcs';
|
||||
import { showModal } from '../components/modals/index';
|
||||
import { SyncMergeModal } from '../components/modals/sync-merge-modal';
|
||||
export function useVCS({
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId?: string;
|
||||
}) {
|
||||
const vcsInstanceRef = useRef<VCS | null>(null);
|
||||
const [vcs, setVCS] = useState<VCS | null>(null);
|
||||
const updateVCSLock = useRef<boolean | string>(false);
|
||||
|
||||
// Update VCS when the active workspace changes
|
||||
useEffect(() => {
|
||||
const lock = generateId();
|
||||
updateVCSLock.current = lock;
|
||||
|
||||
// Set vcs to null while we update it
|
||||
setVCS(null);
|
||||
|
||||
if (!vcsInstanceRef.current) {
|
||||
const driver = FileSystemDriver.create(getDataDirectory());
|
||||
|
||||
vcsInstanceRef.current = new VCS(driver, async conflicts => {
|
||||
return new Promise(resolve => {
|
||||
showModal(SyncMergeModal, {
|
||||
conflicts,
|
||||
handleDone: (conflicts: MergeConflict[]) => resolve(conflicts),
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (workspaceId) {
|
||||
vcsInstanceRef.current.switchProject(workspaceId);
|
||||
} else {
|
||||
vcsInstanceRef.current.clearBackendProject();
|
||||
}
|
||||
|
||||
// Prevent a potential race-condition when _updateVCS() gets called for different workspaces in rapid succession
|
||||
if (updateVCSLock.current === lock) {
|
||||
setVCS(vcsInstanceRef.current);
|
||||
}
|
||||
}, [workspaceId]);
|
||||
|
||||
return vcs;
|
||||
}
|
||||
Reference in new issue
Block a user