diff --git a/src/management-system-v2/components/process-export.tsx b/src/management-system-v2/components/process-export.tsx index e8f221ff1..bc2dbff74 100644 --- a/src/management-system-v2/components/process-export.tsx +++ b/src/management-system-v2/components/process-export.tsx @@ -14,8 +14,8 @@ const exportTypeOptions = [ const exportSubOptions = { bpmn: [{ label: 'Export with Artefacts', value: 'artefacts' }], - pdf: [], - svg: [], + pdf: [{ label: 'Export with collapsed subprocesses', value: 'subprocesses' }], + svg: [{ label: 'Export with collapsed subprocesses', value: 'subprocesses' }], }; type ProcessExportModalProps = { @@ -56,6 +56,7 @@ const ProcessExportModal: React.FC = ({ processes = [], { type: selectedType!, artefacts: selectedOptions.some((el) => el === 'artefacts'), + subprocesses: selectedOptions.some((el) => el === 'subprocesses'), }, processes, ); diff --git a/src/management-system-v2/lib/process-export/export-preparation.ts b/src/management-system-v2/lib/process-export/export-preparation.ts index 504db3c2d..fc96f9362 100644 --- a/src/management-system-v2/lib/process-export/export-preparation.ts +++ b/src/management-system-v2/lib/process-export/export-preparation.ts @@ -9,6 +9,10 @@ import { getAllUserTaskFileNamesAndUserTaskIdsMapping, getAllBpmnFlowElements, getMetaDataFromElement, + getElementsByTagName, + toBpmnObject, + getElementDI, + getDefinitionsVersionInformation, } from '@proceed/bpmn-helper'; /** @@ -17,6 +21,7 @@ import { export type ProcessExportOptions = { type: 'bpmn' | 'svg' | 'pdf'; artefacts: boolean; // if artefacts like images or user task html should be included in the export + subprocesses: boolean; // if collapsed subprocesses should be exported as well (svg, pdf) }; /** @@ -32,10 +37,9 @@ export type ProcessExportData = { definitionName: string; versions: { [version: string]: { + name?: string; bpmn: string; - artefactsToExport: { - userTaskFiles: string[]; - }; + subprocesses: { id: string; name: string }[]; }; }; userTasks: { @@ -97,6 +101,22 @@ function getImagesReferencedByHtml(html: string) { } } +/** + * Returns the ids of all subprocesses in the given bpmn that are not expanded + * + * @param bpmn + */ +async function getCollapsedSubprocessIds(bpmn: string) { + const definitions = await toBpmnObject(bpmn); + const subprocesses = getElementsByTagName(definitions, 'bpmn:SubProcess'); + + const collapsedSubprocesses = subprocesses + .filter((subprocess) => !getElementDI(subprocess, definitions).isExpanded) + .map(({ id, name }) => ({ id, name })); + + return collapsedSubprocesses; +} + /** * Internal export data representation for easier referencing */ @@ -105,10 +125,9 @@ type ExportMap = { definitionName: string; versions: { [version: string]: { + name?: string; bpmn: string; - artefactsToExport: { - userTaskFiles: string[]; - }; + subprocesses: { id: string; name: string }[]; }; }; userTasks: { @@ -151,20 +170,30 @@ export async function prepareExport( // prevent (unlikely) situations where a version might be referenced once by number and once by string const versionName = processVersion ? `${processVersion}` : 'latest'; + const versionBpmn = await getVersionBpmn(definitionId, processVersion); + const versionInformation = await getDefinitionsVersionInformation(versionBpmn); + exportData[definitionId] = { definitionName: process.definitionName, versions: { [versionName]: { - bpmn: await getVersionBpmn(definitionId, processVersion), - artefactsToExport: { - userTaskFiles: [], - }, + name: versionInformation.name, + bpmn: versionBpmn, + subprocesses: [], }, }, userTasks: [], images: [], }; + // get the ids of all collapsed subprocesses so they can be used later during export + if (options.subprocesses) { + for (const [version, { bpmn }] of Object.entries(exportData[definitionId].versions)) { + exportData[definitionId].versions[version].subprocesses = + await getCollapsedSubprocessIds(bpmn); + } + } + // fetch data for additional artefacts if requested in the options if (options.artefacts) { const allRequiredUserTaskFiles: Set = new Set(); @@ -175,8 +204,6 @@ export async function prepareExport( const versionUserTasks = Object.keys( await getAllUserTaskFileNamesAndUserTaskIdsMapping(bpmn), ); - exportData[definitionId].versions[version].artefactsToExport.userTaskFiles = - versionUserTasks; for (const filename of versionUserTasks) allRequiredUserTaskFiles.add(filename); } diff --git a/src/management-system-v2/lib/process-export/index.ts b/src/management-system-v2/lib/process-export/index.ts index 5f36df06b..eb1661f0e 100644 --- a/src/management-system-v2/lib/process-export/index.ts +++ b/src/management-system-v2/lib/process-export/index.ts @@ -37,12 +37,12 @@ function downloadFile(filename: string, data: Blob) { } /** - * Converts the bpmn into an svg image of the process + * Converts the bpmn into an svg image of the process or of subprocess contained inside the process * * @param bpmn * @returns the svg image as a string */ -async function getSVGFromBPMN(bpmn: string) { +async function getSVGFromBPMN(bpmn: string, subprocessId?: string) { const Viewer = (await import('bpmn-js/lib/Viewer')).default; //Creating temporary element for BPMN Viewer @@ -55,6 +55,16 @@ async function getSVGFromBPMN(bpmn: string) { //Create a viewer to transform the bpmn into an svg const viewer = new Viewer({ container: '#' + viewerElement.id }); await viewer.importXML(bpmn); + + const canvas = viewer.get('canvas') as any; + + // target the correct plane (root process or the specified subprocess) + if (subprocessId) { + canvas.setRootElement(canvas.findRoot(`${subprocessId}_plane`)); + } + + canvas.zoom('fit-viewport', 'auto'); + const { svg } = await viewer.saveSVG(); return svg; @@ -69,36 +79,47 @@ async function pdfExport(processData: ProcessExportData, zip?: jsZip | null) { }); doc.deletePage(1); - for (const versionData of Object.values(processData.versions)) { - // get the svg so we can display the process as a vector graphic inside the pdf - const svg = await getSVGFromBPMN(versionData.bpmn); - const parser = new DOMParser(); - const svgDOM = parser.parseFromString(svg, 'image/svg+xml'); - - // get image dimensions - let svgWidth = parseFloat(svg.split('width="')[1].split('"')[0]); - let svgHeight = 20 + parseFloat(svg.split('height="')[1].split('"')[0]); - - // adding a new page, second parameter orientation: p - portrait, l - landscape - doc.addPage([svgWidth, svgHeight], svgHeight > svgWidth ? 'p' : 'l'); - - //Getting PDF Documents width and height - const pageWidth = doc.internal.pageSize.getWidth() - 10; - const pageHeight = doc.internal.pageSize.getHeight() - 10; - - //Setting pdf font size - doc.setFontSize(15); - - //Adding Header to the Pdf - // TODO: make sure that the text fits both in landscape as well as in portrait mode - doc.text(`Process: ${processData.definitionName} \n`, 10, 15); + // add all versions of the process into the same pdf + for (const [version, versionData] of Object.entries(processData.versions)) { + // add all collapsed subprocesses (if requested) + for (const { id: subprocessId, name: subprocessName } of versionData.subprocesses + .concat([{ id: '', name: 'root process' }]) // handle the root process like another collapsed subprocess + // ensure the correct order of elements being added + .reverse()) { + // get the svg so we can display the process as a vector graphic inside the pdf + const svg = await getSVGFromBPMN(versionData.bpmn, subprocessId); + const parser = new DOMParser(); + const svgDOM = parser.parseFromString(svg, 'image/svg+xml'); + + // get image dimensions + let svgWidth = parseFloat(svg.split('width="')[1].split('"')[0]); + let svgHeight = 20 + parseFloat(svg.split('height="')[1].split('"')[0]); + + // adding a new page, second parameter orientation: p - portrait, l - landscape + doc.addPage([svgWidth, svgHeight], svgHeight > svgWidth ? 'p' : 'l'); + + //Getting PDF Documents width and height + const pageWidth = doc.internal.pageSize.getWidth() - 10; + const pageHeight = doc.internal.pageSize.getHeight() - 10; + + //Setting pdf font size + doc.setFontSize(15); + + //Adding Header to the Pdf + // TODO: make sure that the text fits both in landscape as well as in portrait mode + if (subprocessId) { + doc.text(`Subprocess: ${subprocessName || subprocessId} \n`, 10, 15); + } else { + doc.text(`Version: ${versionData.name || version} \n`, 10, 15); + } - await doc.svg(svgDOM.children[0], { - x: 0, - y: 10, - width: pageWidth, - height: pageHeight, - }); + await doc.svg(svgDOM.children[0], { + x: 0, + y: 10, + width: pageWidth, + height: pageHeight, + }); + } } if (zip) { @@ -109,20 +130,31 @@ async function pdfExport(processData: ProcessExportData, zip?: jsZip | null) { } async function svgExport(processData: ProcessExportData, zipFolder?: jsZip | null) { + // export all versions of the process as separate files for (const [versionName, versionData] of Object.entries(processData.versions)) { - const svg = await getSVGFromBPMN(versionData.bpmn!); - - const svgBlob = new Blob([svg], { - type: 'image/svg+xml', - }); + // export all collapsed subprocesses as separate files (if requested) + for (const { id: subprocessId, name: subprocessName } of versionData.subprocesses.concat([ + { id: '', name: 'root process' }, // handle the root process like another collapsed subprocess + ])) { + const svg = await getSVGFromBPMN(versionData.bpmn!, subprocessId); + + const svgBlob = new Blob([svg], { + type: 'image/svg+xml', + }); + + // a) if we output into a zip folder that uses the process name use the version name as the filename + // b) if we output as a single file use the process name as the file name + let filename = zipFolder ? versionData.name || versionName : processData.definitionName; + + if (subprocessId) { + filename = `subprocess_${subprocessName || subprocessId}`; + } - // a) if we output into a zip folder that uses the process name use the version name as the filename - // b) if we output as a single file use the process name as the file name - const filename = zipFolder ? versionName : processData.definitionName; - if (zipFolder) { - zipFolder.file(`${filename}.svg`, svgBlob); - } else { - downloadFile(`${filename}.svg`, svgBlob); + if (zipFolder) { + zipFolder.file(`${filename}.svg`, svgBlob); + } else { + downloadFile(`${filename}.svg`, svgBlob); + } } } } @@ -177,8 +209,10 @@ export async function exportProcesses(options: ProcessExportOptions, processes: if (!needsZip && options.type !== 'pdf') { const hasMulitpleVersions = Object.keys(exportData[0].versions).length > 1; const hasArtefacts = !!exportData[0].userTasks.length || !!exportData[0].images.length; + // this becomes relevant if there is only one version (otherwise hasMultipleVersions will lead to needsZip being true anyway) + const withSubprocesses = Object.values(exportData[0].versions)[0].subprocesses.length > 0; - needsZip = needsZip || hasMulitpleVersions || hasArtefacts; + needsZip = needsZip || hasMulitpleVersions || hasArtefacts || withSubprocesses; } const zip = needsZip ? new jsZip() : undefined; @@ -187,7 +221,7 @@ export async function exportProcesses(options: ProcessExportOptions, processes: if (options.type === 'pdf') { await pdfExport(processData, zip); } else { - const folder = needsZip ? zip!.folder(processData.definitionName) : undefined; + const folder = zip?.folder(processData.definitionName); if (options.type === 'bpmn') await bpmnExport(processData, folder); if (options.type === 'svg') await svgExport(processData, folder); }