From 8fa8b9beb8799c9bbb8a2c84d1180e8a7e666317 Mon Sep 17 00:00:00 2001 From: Jack Kavanagh Date: Fri, 18 Aug 2023 11:19:35 +0200 Subject: [PATCH] fix export from outside workspace (#6307) * add test * simplify export requests modal logic * fix export * fix type check * fix test --- .../tests/smoke/app.test.ts | 8 + .../dropdowns/workspace-card-dropdown.tsx | 1 + .../dropdowns/workspace-dropdown.tsx | 1 + .../modals/export-requests-modal.tsx | 189 +++++++----------- .../ui/components/settings/import-export.tsx | 3 +- 5 files changed, 87 insertions(+), 115 deletions(-) diff --git a/packages/insomnia-smoke-test/tests/smoke/app.test.ts b/packages/insomnia-smoke-test/tests/smoke/app.test.ts index d45cb08866..c0827ce1f9 100644 --- a/packages/insomnia-smoke-test/tests/smoke/app.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/app.test.ts @@ -19,6 +19,14 @@ test('can send requests', async ({ app, page }) => { await page.getByText('Clipboard').click(); await page.getByRole('button', { name: 'Scan' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); + + await page.locator('.card-menu').click(); + await page.getByRole('menuitem', { name: 'Export' }).click(); + await page.getByRole('dialog').getByRole('checkbox').nth(1).uncheck(); + await page.getByRole('button', { name: 'Export' }).click(); + await page.getByText('Which format would you like to export as?').click(); + await page.locator('.app').press('Escape'); + await page.getByText('CollectionSmoke testsjust now').click(); const curl = 'curl --request POST --url http://mockbin.org/status/200'; diff --git a/packages/insomnia/src/ui/components/dropdowns/workspace-card-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/workspace-card-dropdown.tsx index d8283d0a6a..fc8f63595e 100644 --- a/packages/insomnia/src/ui/components/dropdowns/workspace-card-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/workspace-card-dropdown.tsx @@ -217,6 +217,7 @@ export const WorkspaceCardDropdown: FC = props => { )} {isExportModalOpen && ( setIsExportModalOpen(false)} /> )} diff --git a/packages/insomnia/src/ui/components/dropdowns/workspace-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/workspace-dropdown.tsx index 87679fdd05..885fd1ca25 100644 --- a/packages/insomnia/src/ui/components/dropdowns/workspace-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/workspace-dropdown.tsx @@ -275,6 +275,7 @@ export const WorkspaceDropdown: FC = () => { )} {isExportModalOpen && ( setIsExportModalOpen(false)} /> )} diff --git a/packages/insomnia/src/ui/components/modals/export-requests-modal.tsx b/packages/insomnia/src/ui/components/modals/export-requests-modal.tsx index 7f82df022a..c6b045b630 100644 --- a/packages/insomnia/src/ui/components/modals/export-requests-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/export-requests-modal.tsx @@ -1,14 +1,15 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { OverlayContainer } from 'react-aria'; -import { useRouteLoaderData } from 'react-router-dom'; +import { useFetcher, useParams } from 'react-router-dom'; import { exportRequestsToFile } from '../../../common/export'; import * as models from '../../../models'; import { GrpcRequest, isGrpcRequest } from '../../../models/grpc-request'; import { isRequest, Request } from '../../../models/request'; -import { isRequestGroup, RequestGroup } from '../../../models/request-group'; +import { RequestGroup } from '../../../models/request-group'; import { isWebSocketRequest, WebSocketRequest } from '../../../models/websocket-request'; -import { WorkspaceLoaderData } from '../../routes/workspace'; +import { Workspace } from '../../../models/workspace'; +import { Child, WorkspaceLoaderData } from '../../routes/workspace'; import { Modal, type ModalHandle, ModalProps } from '../base/modal'; import { ModalBody } from '../base/modal-body'; import { ModalFooter } from '../base/modal-footer'; @@ -26,59 +27,56 @@ export interface Node { export interface State { treeRoot: Node | null; } -export interface ExportRequestsModalHandle { - show: () => void; - hide: () => void; -} -export const ExportRequestsModal = ({ onHide }: ModalProps) => { +export const ExportRequestsModal = ({ workspace, onHide }: { workspace: Workspace } & ModalProps) => { const modalRef = useRef(null); - const workspaceData = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData; - const requestTree = workspaceData?.requestTree || []; + const { organizationId, projectId } = useParams() as { organizationId: string; projectId: string }; + const workspaceFetcher = useFetcher(); + const [state, setState] = useState(); - const createNode = useCallback((item: Record): Node => { - const children: Node[] = item.children.map((child: Record) => createNode(child)); - let totalRequests = children - .map(child => child.totalRequests) - .reduce((acc, totalRequests) => acc + totalRequests, 0); - const docIsRequest = isRequest(item.doc) || isWebSocketRequest(item.doc) || isGrpcRequest(item.doc); - if (docIsRequest) { - totalRequests++; + useEffect(() => { + const isIdleAndUninitialized = workspaceFetcher.state === 'idle' && !workspaceFetcher.data; + if (isIdleAndUninitialized) { + workspaceFetcher.load(`/organization/${organizationId}/project/${projectId}/workspace/${workspace._id}`); } - return { - doc: item.doc, - collapsed: false, - children: children, - totalRequests: totalRequests, - selectedRequests: totalRequests, // Default select all + }, [organizationId, projectId, workspaceFetcher, workspace._id]); + const workspaceLoaderData = workspaceFetcher?.data as WorkspaceLoaderData; + + useEffect(() => { + const createTreeNode = (child: Child): Node => { + const docIsRequest = isRequest(child.doc) || isWebSocketRequest(child.doc) || isGrpcRequest(child.doc); + const children = child.children.map((child: Child) => createTreeNode(child)); + const totalRequests = +docIsRequest + children.reduce((acc, { totalRequests }) => acc + totalRequests, 0); + return { + doc: child.doc, + collapsed: false, + children, + totalRequests: totalRequests, + selectedRequests: totalRequests, // Default select all + }; }; - }, []); + const requestTree = workspaceLoaderData?.requestTree || []; + const children: Node[] = requestTree.map(child => createTreeNode(child)); + setState({ + treeRoot: { + doc: { + ...models.requestGroup.init(), + _id: 'all', + type: models.requestGroup.type, + name: 'All requests', + parentId: '', + modified: 0, + created: 0, + isPrivate: false, + }, + collapsed: false, + children: children, + totalRequests: children.map(child => child.totalRequests).reduce((acc, totalRequests) => acc + totalRequests, 0), + selectedRequests: children.map(child => child.totalRequests).reduce((acc, totalRequests) => acc + totalRequests, 0), // Default select all + }, + }); + }, [workspaceLoaderData?.requestTree]); - const children: Node[] = requestTree.map(child => createNode(child)); - const totalRequests = children - .map(child => child.totalRequests) - .reduce((acc, totalRequests) => acc + totalRequests, 0); - - // @ts-expect-error -- TSCONVERSION missing property - const rootFolder: RequestGroup = { - ...models.requestGroup.init(), - _id: 'all', - type: models.requestGroup.type, - name: 'All requests', - parentId: '', - modified: 0, - created: 0, - }; - - const [state, setState] = useState({ - treeRoot: { - doc: rootFolder, - collapsed: false, - children: children, - totalRequests: totalRequests, - selectedRequests: totalRequests, // Default select all - }, - }); useEffect(() => { modalRef.current?.show(); }, []); @@ -87,95 +85,53 @@ export const ExportRequestsModal = ({ onHide }: ModalProps) => { if (docIsRequest && node.selectedRequests === node.totalRequests) { return [node.doc._id]; } - const requestIds: string[] = []; - for (const child of node.children) { - const reqIds = getSelectedRequestIds(child); - requestIds.push(...reqIds); - } - return requestIds; + return node.children.map(child => getSelectedRequestIds(child)).reduce((acc, reqIds) => [...acc, ...reqIds], []); }; const setItemSelected = (node: Node, isSelected: boolean, id?: string) => { - if (id == null || node.doc._id === id) { + if (id === null || node.doc._id === id) { // Switch the flags of all children in this subtree. - for (const child of node.children) { - setItemSelected(child, isSelected); - } + node.children.forEach(child => setItemSelected(child, isSelected)); node.selectedRequests = isSelected ? node.totalRequests : 0; return true; } for (const child of node.children) { const found = setItemSelected(child, isSelected, id); if (found) { - node.selectedRequests = node.children - .map(ch => ch.selectedRequests) - .reduce((acc, selected) => acc + selected, 0); + node.selectedRequests = node.children.map(ch => ch.selectedRequests).reduce((acc, selected) => acc + selected, 0); return true; } } return false; }; - const handleSetItemSelected = (itemId: string, isSelected: boolean) => { - const { treeRoot } = state; - if (treeRoot == null) { - return; - } - const found = setItemSelected(treeRoot, isSelected, itemId); - if (!found) { - return; - } - setState({ treeRoot: { ...treeRoot } }); - }; - const handleSetRequestGroupCollapsed = (requestGroupId: string, isCollapsed: boolean) => { - const { treeRoot } = state; - if (treeRoot == null) { - return; - } - const found = setRequestGroupCollapsed(treeRoot, isCollapsed, requestGroupId); - if (!found) { - return; - } - setState({ treeRoot: { ...treeRoot } }); - }; - const setRequestGroupCollapsed = (node: Node, isCollapsed: boolean, requestGroupId: string) => { - if (!isRequestGroup(node.doc)) { - return false; - } + + const setRequestGroupCollapsed = (node: Node, isCollapsed: boolean, requestGroupId: string): boolean => { if (node.doc._id === requestGroupId) { node.collapsed = isCollapsed; return true; } - for (const child of node.children) { - const found = setRequestGroupCollapsed(child, isCollapsed, requestGroupId); - if (found) { - return true; - } - } - return false; - }; - const handleExport = () => { - const { treeRoot } = state; - if (treeRoot == null || treeRoot.selectedRequests === 0) { - return; - } - const exportedRequestIds = getSelectedRequestIds(treeRoot); - - exportRequestsToFile(exportedRequestIds); - modalRef.current?.hide(); + return !!node.children.find(child => setRequestGroupCollapsed(child, isCollapsed, requestGroupId)); }; - const { treeRoot } = state; - const isExportDisabled = treeRoot != null ? treeRoot.selectedRequests === 0 : false; + const isExportDisabled = state?.treeRoot?.selectedRequests === 0 || false; return ( - + e.stopPropagation()}> Select Requests to Export
{ + if (state?.treeRoot && setRequestGroupCollapsed(state?.treeRoot, isCollapsed, requestGroupId)) { + setState({ treeRoot: state?.treeRoot }); + } + }} + handleSetItemSelected={(itemId: string, isSelected: boolean) => { + if (state?.treeRoot && setItemSelected(state?.treeRoot, isSelected, itemId)) { + setState({ treeRoot: state?.treeRoot }); + } + }} />
@@ -186,7 +142,12 @@ export const ExportRequestsModal = ({ onHide }: ModalProps) => {