Skip to content

Commit

Permalink
Collapsed Sub-Process Export (new MS) (#122)
Browse files Browse the repository at this point in the history
* select box styling, table categories button added, functionality tba

* select bar layout wip

* added first version of a breadcrumb

* styling wip

* Breadcrumb version and process select

* fuzzy search, dropdown for columns, deselect for multirow

* add comment for fuzzy search

* changed colour breadcrumb, added latest changes to modeler (jj), Menu (sider) updated

* Adjusted Modeler height, added Preview-Viewer Co-authored-by: winniel24 <[email protected]> Co-authored-by: jjoderis <[email protected]> Co-authored-by: Lucas <[email protected]>

* Made previewer height adjustable

* Added export of a single process as bpmn, svg or pdf from the process modeler

* Added the process export modal to the processes view in addition to the process editor view

* Ran prettier

* Removed unnecessary code

* Added jszip as a dependency

* Added option to export multiple processes at once

* Added the option to export user task html alongside process bpmn

* Added an export preparation step to handle situations where a user selects an export with artefacts but there are no artefacts to export so a single file export is possible

* Added image export when artefacts are supposed to be exported with the bpmn

* Consolidated some logic

* Added some comments and fixed a broken import

* Removed unused code

* Added option to export collapsed subprocesses seperately for svg and pdf export

* Removed submodule that was accidentally added

---------

Co-authored-by: winnie <[email protected]>
Co-authored-by: LaMaLein <[email protected]>
Co-authored-by: LaMaLein <[email protected]>
  • Loading branch information
4 people authored Oct 19, 2023
1 parent 976156e commit d5da3c3
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 59 deletions.
5 changes: 3 additions & 2 deletions src/management-system-v2/components/process-export.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -56,6 +56,7 @@ const ProcessExportModal: React.FC<ProcessExportModalProps> = ({ processes = [],
{
type: selectedType!,
artefacts: selectedOptions.some((el) => el === 'artefacts'),
subprocesses: selectedOptions.some((el) => el === 'subprocesses'),
},
processes,
);
Expand Down
51 changes: 39 additions & 12 deletions src/management-system-v2/lib/process-export/export-preparation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import {
getAllUserTaskFileNamesAndUserTaskIdsMapping,
getAllBpmnFlowElements,
getMetaDataFromElement,
getElementsByTagName,
toBpmnObject,
getElementDI,
getDefinitionsVersionInformation,
} from '@proceed/bpmn-helper';

/**
Expand All @@ -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)
};

/**
Expand All @@ -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: {
Expand Down Expand Up @@ -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
*/
Expand All @@ -105,10 +125,9 @@ type ExportMap = {
definitionName: string;
versions: {
[version: string]: {
name?: string;
bpmn: string;
artefactsToExport: {
userTaskFiles: string[];
};
subprocesses: { id: string; name: string }[];
};
};
userTasks: {
Expand Down Expand Up @@ -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<string> = new Set();
Expand All @@ -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);
}
Expand Down
124 changes: 79 additions & 45 deletions src/management-system-v2/lib/process-export/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand All @@ -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) {
Expand All @@ -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);
}
}
}
}
Expand Down Expand Up @@ -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;
Expand All @@ -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);
}
Expand Down

0 comments on commit d5da3c3

Please sign in to comment.