fix export from outside workspace (#6307)

* add test

* simplify export requests modal logic

* fix export

* fix type check

* fix test
This commit is contained in:
Jack Kavanagh
2023-08-18 11:19:35 +02:00
committed by GitHub
parent 5ecc3aba4a
commit 8fa8b9beb8
5 changed files with 87 additions and 115 deletions

View File

@@ -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';

View File

@@ -217,6 +217,7 @@ export const WorkspaceCardDropdown: FC<Props> = props => {
)}
{isExportModalOpen && (
<ExportRequestsModal
workspace={workspace}
onHide={() => setIsExportModalOpen(false)}
/>
)}

View File

@@ -275,6 +275,7 @@ export const WorkspaceDropdown: FC = () => {
)}
{isExportModalOpen && (
<ExportRequestsModal
workspace={activeWorkspace}
onHide={() => setIsExportModalOpen(false)}
/>
)}

View File

@@ -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<ModalHandle>(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<State>();
const createNode = useCallback((item: Record<string, any>): Node => {
const children: Node[] = item.children.map((child: Record<string, any>) => 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<State>({
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 (
<OverlayContainer>
<OverlayContainer onClick={e => e.stopPropagation()}>
<Modal ref={modalRef} tall onHide={onHide}>
<ModalHeader>Select Requests to Export</ModalHeader>
<ModalBody>
<div className="requests-tree">
<Tree
root={treeRoot}
handleSetRequestGroupCollapsed={handleSetRequestGroupCollapsed}
handleSetItemSelected={handleSetItemSelected}
root={state?.treeRoot}
handleSetRequestGroupCollapsed={(requestGroupId: string, isCollapsed: boolean) => {
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 });
}
}}
/>
</div>
</ModalBody>
@@ -186,7 +142,12 @@ export const ExportRequestsModal = ({ onHide }: ModalProps) => {
</button>
<button
className="btn"
onClick={handleExport}
onClick={() => {
if (state?.treeRoot && state?.treeRoot.selectedRequests > 0) {
exportRequestsToFile(getSelectedRequestIds(state?.treeRoot));
modalRef.current?.hide();
}
}}
disabled={isExportDisabled}
>
Export

View File

@@ -115,8 +115,9 @@ export const ImportExport: FC<Props> = ({ hideSettingsModal }) => {
defaultWorkspaceId={workspaceId}
/>
)}
{isExportModalOpen && (
{isExportModalOpen && workspaceData?.activeWorkspace && (
<ExportRequestsModal
workspace={workspaceData.activeWorkspace}
onHide={() => setIsExportModalOpen(false)}
/>
)}