diff --git a/src/management-system-v2/components/modeler-toolbar.tsx b/src/management-system-v2/components/modeler-toolbar.tsx index 5a789d809..5f31da8ea 100644 --- a/src/management-system-v2/components/modeler-toolbar.tsx +++ b/src/management-system-v2/components/modeler-toolbar.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import type ElementRegistry from 'diagram-js/lib/core/ElementRegistry'; @@ -20,13 +20,12 @@ import { SvgXML, SvgShare } from '@/components/svg'; import PropertiesPanel from './properties-panel'; import useModelerStateStore from '@/lib/use-modeler-state-store'; -import { useParams } from 'next/navigation'; +import { useParams, useSearchParams } from 'next/navigation'; import ProcessExportModal from './process-export'; import { createNewProcessVersion } from '@/lib/helpers'; import VersionCreationButton from './version-creation-button'; -import { useGetAsset } from '@/lib/fetch-data'; type ModelerToolbarProps = { onOpenXmlEditor: () => void; @@ -45,14 +44,6 @@ const ModelerToolbar: React.FC = ({ onOpenXmlEditor }) => { // const [index, setIndex] = useState(0); const { processId } = useParams(); - const { - isSuccess, - data: processData, - refetch: refetchProcess, - } = useGetAsset('/process/{definitionId}', { - params: { path: { definitionId: processId as string } }, - }); - let selectedElement; if (modeler) { @@ -75,7 +66,6 @@ const ModelerToolbar: React.FC = ({ onOpenXmlEditor }) => { values.versionName, values.versionDescription, ); - refetchProcess(); } }; const handlePropertiesPanelToggle = () => { @@ -86,7 +76,7 @@ const ModelerToolbar: React.FC = ({ onOpenXmlEditor }) => { setShowProcessExportModal(!showProcessExportModal); }; - const selectedVersion = useModelerStateStore((state) => state.selectedVersion); + const selectedVersion = useSearchParams().get('version'); return ( <> @@ -140,9 +130,12 @@ const ModelerToolbar: React.FC = ({ onOpenXmlEditor }) => { )} */} setShowProcessExportModal(false)} - processVersion={selectedVersion || undefined} /> ); diff --git a/src/management-system-v2/components/process-export.tsx b/src/management-system-v2/components/process-export.tsx index 58435af5b..85132c5c9 100644 --- a/src/management-system-v2/components/process-export.tsx +++ b/src/management-system-v2/components/process-export.tsx @@ -3,7 +3,7 @@ import React, { useState } from 'react'; import { Modal, Checkbox } from 'antd'; import type { CheckboxValueType } from 'antd/es/checkbox/Group'; -import { exportBpmn, exportPDF, exportSVG } from '@/lib/process-export'; +import { exportProcesses, exportType } from '@/lib/process-export'; const exportTypeOptions = [ { label: 'BPMN', value: 'bpmn' }, @@ -12,16 +12,11 @@ const exportTypeOptions = [ ]; type ProcessExportModalProps = { - processId?: string; // the id of the process to export; also used to decide if the modal should be opened + processes: { definitionId: string; processVersion?: number | string }[]; // the processes to export; also used to decide if the modal should be opened onClose: () => void; - processVersion?: number | string; }; -const ProcessExportModal: React.FC = ({ - processId, - onClose, - processVersion, -}) => { +const ProcessExportModal: React.FC = ({ processes = [], onClose }) => { const [selectedTypes, setSelectedTypes] = useState([]); const handleTypeSelectionChange = (checkedValues: CheckboxValueType[]) => { @@ -31,19 +26,7 @@ const ProcessExportModal: React.FC = ({ }; const handleProcessExport = async () => { - switch (selectedTypes[0]) { - case 'bpmn': - await exportBpmn(processId!, processVersion); - break; - case 'pdf': - await exportPDF(processId!, processVersion); - break; - case 'svg': - await exportSVG(processId!, processVersion); - break; - default: - throw 'Unexpected value for process export!'; - } + await exportProcesses(processes, selectedTypes[0] as exportType); onClose(); }; @@ -52,7 +35,7 @@ const ProcessExportModal: React.FC = ({ <> >; isLoading?: boolean; - onExportProcess: Dispatch>; + onExportProcess: Dispatch>; }>; const ColumnHeader = [ @@ -113,7 +113,7 @@ const ProcessList: FC = ({ { - onExportProcess(record.definitionId); + onExportProcess([record.definitionId]); }} /> diff --git a/src/management-system-v2/components/processes.tsx b/src/management-system-v2/components/processes.tsx index 5085edeee..323febc5c 100644 --- a/src/management-system-v2/components/processes.tsx +++ b/src/management-system-v2/components/processes.tsx @@ -59,7 +59,7 @@ const Processes: FC = () => { const [iconView, setIconView] = useState(prefs['icon-view-in-process-list']); - const [exportProcessId, setExportProcessId] = useState(); + const [exportProcessIds, setExportProcessIds] = useState([]); const actionBar = ( <> @@ -72,7 +72,7 @@ const Processes: FC = () => { { - setExportProcessId(selectedRowKeys[0].toString()); + setExportProcessIds(selectedRowKeys as string[]); }} /> @@ -180,7 +180,7 @@ const Processes: FC = () => { selection={selectedRowKeys} setSelection={setSelectedRowKeys} isLoading={isLoading} - onExportProcess={setExportProcessId} + onExportProcess={setExportProcessIds} /> )} @@ -188,8 +188,8 @@ const Processes: FC = () => { setExportProcessId(undefined)} + processes={exportProcessIds.map((definitionId) => ({ definitionId }))} + onClose={() => setExportProcessIds([])} /> ); diff --git a/src/management-system-v2/lib/process-export.ts b/src/management-system-v2/lib/process-export.ts index b7c096662..c3433d967 100644 --- a/src/management-system-v2/lib/process-export.ts +++ b/src/management-system-v2/lib/process-export.ts @@ -1,5 +1,6 @@ import { v4 } from 'uuid'; import { jsPDF } from 'jspdf'; +import jsZip from 'jszip'; import 'svg2pdf.js'; import { fetchProcessVersionBpmn, fetchProcess } from './process-queries'; @@ -28,12 +29,59 @@ async function getProcessData(processId: string, processVersion?: string | numbe return data; } -export async function exportBpmn(processId: string, processVersion?: string | number) { +export type exportType = 'bpmn' | 'svg' | 'pdf'; + +export async function exportProcesses( + processes: { definitionId: string; processVersion?: number | string }[], + type: exportType, +) { + if (processes.length === 1) { + // generate the file and download it + switch (type) { + case 'bpmn': + await exportBpmn(exportFile, processes[0].definitionId, processes[0].processVersion); + break; + case 'svg': + await exportSVG(exportFile, processes[0].definitionId, processes[0].processVersion); + break; + case 'pdf': + await exportPDF(exportFile, processes[0].definitionId, processes[0].processVersion); + break; + } + } else if (processes.length > 1) { + const zip = new jsZip(); + + for (const { definitionId, processVersion } of processes) { + // add the file to the zip archive + switch (type) { + case 'bpmn': + await exportBpmn(zip.file.bind(zip), definitionId, processVersion); + break; + case 'svg': + await exportSVG(zip.file.bind(zip), definitionId, processVersion); + break; + case 'pdf': + await exportPDF(zip.file.bind(zip), definitionId, processVersion); + break; + } + } + // download the zip archive to the users device + exportFile('PROCEED_Multiple-Processes_bpmn.zip', await zip.generateAsync({ type: 'blob' })); + } else { + throw new Error('Tried exporting without specifying the processes to export!'); + } +} + +export async function exportBpmn( + dataHandler: (fileName: string, data: Blob) => void, + processId: string, + processVersion?: string | number, +) { const process = await getProcessData(processId, processVersion); const bpmnBlob = new Blob([process.bpmn!], { type: 'application/xml' }); - exportFile(`${process.definitionName}.bpmn`, bpmnBlob); + dataHandler(`${process.definitionName}.bpmn`, bpmnBlob); } async function getSVGFromBPMN(bpmn: string) { @@ -54,7 +102,11 @@ async function getSVGFromBPMN(bpmn: string) { return svg; } -export async function exportSVG(processId: string, processVersion?: string | number) { +export async function exportSVG( + dataHandler: (fileName: string, data: Blob) => void, + processId: string, + processVersion?: string | number, +) { const process = await getProcessData(processId, processVersion); const svg = await getSVGFromBPMN(process.bpmn!); @@ -63,10 +115,14 @@ export async function exportSVG(processId: string, processVersion?: string | num type: 'image/svg+xml', }); - exportFile(`${process.definitionName}.svg`, svgBlob); + dataHandler(`${process.definitionName}.svg`, svgBlob); } -export async function exportPDF(processId: string, processVersion?: string | number) { +export async function exportPDF( + dataHandler: (fileName: string, data: Blob) => void, + processId: string, + processVersion?: string | number, +) { const process = await getProcessData(processId, processVersion); const svg = await getSVGFromBPMN(process.bpmn!); @@ -107,10 +163,16 @@ export async function exportPDF(processId: string, processVersion?: string | num height: pageHeight, }); - await doc.save(`${process.definitionName}.pdf`); + dataHandler(`${process.definitionName}.pdf`, await doc.output('blob')); } -function exportFile(processName: string, data: Blob) { +/** + * Downloads the file to export on the users device + * + * @param fileName + * @param data + */ +function exportFile(fileName: string, data: Blob) { const objectURL = URL.createObjectURL(data); // Creating Anchor Element to trigger download feature @@ -118,7 +180,7 @@ function exportFile(processName: string, data: Blob) { // Setting anchor tag properties aLink.style.display = 'none'; - aLink.download = processName; + aLink.download = fileName; aLink.href = objectURL; // Setting anchor tag to DOM @@ -126,6 +188,6 @@ function exportFile(processName: string, data: Blob) { aLink.click(); document.body.removeChild(aLink); - // Release Object URL, so browser dont keep reference + // Release Object URL, so the browser doesn't keep the reference URL.revokeObjectURL(objectURL); } diff --git a/src/management-system-v2/package.json b/src/management-system-v2/package.json index 578702ab6..2c223c584 100644 --- a/src/management-system-v2/package.json +++ b/src/management-system-v2/package.json @@ -20,25 +20,26 @@ "singleQuote": true }, "dependencies": { - "jspdf": "^2.5.1", - "svg2pdf.js": "^2.2.2", - "uuid": "^9.0.0", - "@monaco-editor/react": "4.5.2", + "@casl/ability": "6.5.0", + "@monaco-editor/react": "^4.5.2", + "@proceed/bpmn-helper": "1.0.0", "@tanstack/react-query": "4.35.7", "antd": "5.9.4", "bpmn-js": "13.2.0", "bpmn-js-differ": "2.0.2", "classnames": "2.3.2", + "fuse.js": "6.6.2", + "immer": "10.0.3", + "jspdf": "^2.5.1", + "jszip": "^3.10.1", "monaco-editor": "0.43.0", "next": "13.5.4", + "openapi-fetch": "0.7.8", "react": "18.2.0", "react-dom": "18.2.0", - "zustand": "4.4.2", - "immer": "10.0.3", - "fuse.js": "6.6.2", - "@casl/ability": "6.5.0", - "openapi-fetch": "0.7.8", - "@proceed/bpmn-helper": "1.0.0" + "svg2pdf.js": "^2.2.2", + "uuid": "^9.0.0", + "zustand": "4.4.2" }, "devDependencies": { "@tanstack/eslint-plugin-query": "4.36.0", diff --git a/yarn.lock b/yarn.lock index 23f9f3d1a..8cd6a6934 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1945,19 +1945,19 @@ resolved "https://registry.yarnpkg.com/@mdi/font/-/font-6.9.96.tgz#c68da7e0895885dd09e60dc08c5ecc0d77f67efb" integrity sha512-z3QVZStyHVwkDsFR7A7F2PIvZJPWgdSFw4BEEy2Gc9HUN5NfK9mGbjgaYClRcbMWiYEV45srmiYtczmBtCqR8w== -"@monaco-editor/loader@^1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.3.3.tgz#7f1742bd3cc21c0362a46a4056317f6e5215cfca" - integrity sha512-6KKF4CTzcJiS8BJwtxtfyYt9shBiEv32ateQ9T4UVogwn4HM/uPo9iJd2Dmbkpz8CM6Y0PDUpjnZzCwC+eYo2Q== +"@monaco-editor/loader@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.4.0.tgz#f08227057331ec890fa1e903912a5b711a2ad558" + integrity sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg== dependencies: state-local "^1.0.6" -"@monaco-editor/react@4.5.2": - version "4.5.2" - resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.5.2.tgz#e8cc802203f729b423a998ea6fcb466604d61258" - integrity sha512-emcWu6vg1OpXPiYll4aPOaXe8bwYB4UaaNTwtArFLgMoNGBzRZb2Xn0Bra2HMIFM7QLgs7fCGunHO5LkfT2LBA== +"@monaco-editor/react@^4.5.2": + version "4.6.0" + resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.6.0.tgz#bcc68671e358a21c3814566b865a54b191e24119" + integrity sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw== dependencies: - "@monaco-editor/loader" "^1.3.3" + "@monaco-editor/loader" "^1.4.0" "@mrmlnc/readdir-enhanced@^2.2.1": version "2.2.1" @@ -13442,7 +13442,7 @@ jsprim@^1.2.2: object.assign "^4.1.4" object.values "^1.1.6" -jszip@^3.0.0, jszip@^3.1.0, jszip@^3.4.0: +jszip@^3.0.0, jszip@^3.1.0, jszip@^3.10.1, jszip@^3.4.0: version "3.10.1" resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2" integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==