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