Skip to content

Commit

Permalink
Merge branch 'main' into ms2/property-panel
Browse files Browse the repository at this point in the history
  • Loading branch information
LucasMGo committed Nov 22, 2023
2 parents f4267cb + 02c3515 commit c0a7e8f
Show file tree
Hide file tree
Showing 10 changed files with 475 additions and 164 deletions.
1 change: 1 addition & 0 deletions src/helper-modules/bpmn-helper/src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ function getChildren(travObj) {
'diagrams',
'imports',
'extensionElements',
'participants',
];

const allChildren = childNodeTypes
Expand Down
17 changes: 17 additions & 0 deletions src/management-system-v2/components/modeler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { usePutAsset } from '@/lib/fetch-data';
import { useProcessBpmn } from '@/lib/process-queries';
import VersionToolbar from './version-toolbar';

import { copyProcessImage } from '@/lib/process-export/copy-process-image';

// Conditionally load the BPMN modeler only on the client, because it uses
// "window" reference. It won't be included in the initial bundle, but will be
// immediately loaded when the initial script first executes (not after
Expand Down Expand Up @@ -94,6 +96,21 @@ const Modeler: FC<ModelerProps> = ({ minimized, ...props }) => {
});
}

// allow keyboard shortcuts like copy (strg+c) and paste (strg+v) etc.
(modeler.current.get('keyboard') as any).bind(document);

// create a custom copy behaviour where the whole process or selected parts can be copied to the clipboard as an image
(modeler.current.get('keyboard') as any).addListener(
async (_: any, events: { keyEvent: KeyboardEvent }) => {
const { keyEvent } = events;
// handle the copy shortcut
if (keyEvent.ctrlKey && keyEvent.key === 'c' && modeler.current) {
await copyProcessImage(modeler.current);
}
},
'keyboard.keyup',
);

setModeler(modeler.current);
setInitialized(true);
});
Expand Down
76 changes: 63 additions & 13 deletions src/management-system-v2/components/process-export.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,17 @@

import React, { useState } from 'react';

import { Modal, Checkbox, Radio, RadioChangeEvent, Space, Flex, Divider, Tooltip } from 'antd';
import {
Modal,
Checkbox,
Radio,
RadioChangeEvent,
Space,
Flex,
Divider,
Tooltip,
Slider,
} from 'antd';
import type { CheckboxValueType } from 'antd/es/checkbox/Group';

import { exportProcesses } from '@/lib/process-export';
Expand All @@ -12,6 +22,7 @@ const exportTypeOptions = [
{ label: 'BPMN', value: 'bpmn' },
{ label: 'PDF', value: 'pdf' },
{ label: 'SVG', value: 'svg' },
{ label: 'PNG', value: 'png' },
];

const exportSubOptions = {
Expand Down Expand Up @@ -62,6 +73,18 @@ const exportSubOptions = {
tooltip: 'Also export content of all collapsed subprocesses',
},
],
png: [
{
label: 'with referenced processes',
value: 'imports',
tooltip: 'Also export all referenced processes used in call-activities',
},
{
label: 'with collapsed subprocesses',
value: 'subprocesses',
tooltip: 'Also export content of all collapsed subprocesses',
},
],
};

type ProcessExportModalProps = {
Expand All @@ -73,6 +96,7 @@ const ProcessExportModal: React.FC<ProcessExportModalProps> = ({ processes = [],
const [selectedType, setSelectedType] = useState<ProcessExportOptions['type']>();
const [selectedOptions, setSelectedOptions] = useState<CheckboxValueType[]>(['metaData']);
const [isExporting, setIsExporting] = useState(false);
const [pngScalingFactor, setPngScalingFactor] = useState(1.5);

const handleTypeSelectionChange = ({ target: { value } }: RadioChangeEvent) => {
setSelectedType(value);
Expand All @@ -97,6 +121,7 @@ const ProcessExportModal: React.FC<ProcessExportModalProps> = ({ processes = [],
imports: selectedOptions.some((el) => el === 'imports'),
metaData: selectedOptions.some((el) => el === 'metaData'),
a4: selectedOptions.some((el) => el === 'a4'),
scaling: pngScalingFactor,
},
processes,
);
Expand All @@ -117,19 +142,43 @@ const ProcessExportModal: React.FC<ProcessExportModalProps> = ({ processes = [],
);

const optionSelection = (
<Checkbox.Group
onChange={handleOptionSelectionChange}
value={selectedOptions}
style={{ width: '50%' }}
>
<Space direction="vertical">
{(selectedType ? exportSubOptions[selectedType] : []).map(({ label, value, tooltip }) => (
<Tooltip placement="left" title={tooltip} key={label}>
<Checkbox value={value}>{label}</Checkbox>
<Space direction="vertical">
<Checkbox.Group
onChange={handleOptionSelectionChange}
value={selectedOptions}
style={{ width: '100%' }}
>
<Space direction="vertical">
{(selectedType ? exportSubOptions[selectedType] : []).map(({ label, value, tooltip }) => (
<Tooltip placement="left" title={tooltip} key={label}>
<Checkbox value={value}>{label}</Checkbox>
</Tooltip>
))}
</Space>
</Checkbox.Group>
{selectedType === 'png' && (
<div style={{ marginTop: '10px' }}>
<Tooltip placement="left" title="Export with different image resolutions">
<span>Quality:</span>
</Tooltip>
))}
</Space>
</Checkbox.Group>

<Radio.Group
onChange={(e) => setPngScalingFactor(e.target.value)}
value={pngScalingFactor}
>
<Tooltip placement="bottom" title="Smallest resolution and smallest file size">
<Radio value={1.5}>Normal</Radio>
</Tooltip>
<Tooltip placement="bottom" title="Medium resolution and medium file size">
<Radio value={2.5}>Good</Radio>
</Tooltip>
<Tooltip placement="bottom" title="Highest resolution and biggest file size">
<Radio value={4}>Excellent</Radio>
</Tooltip>
</Radio.Group>
</div>
)}
</Space>
);

return (
Expand All @@ -141,6 +190,7 @@ const ProcessExportModal: React.FC<ProcessExportModalProps> = ({ processes = [],
onCancel={handleClose}
centered
okButtonProps={{ disabled: !selectedType, loading: isExporting }}
width={540}
>
<Flex>
{typeSelection}
Expand Down
13 changes: 8 additions & 5 deletions src/management-system-v2/lib/helpers/javascriptHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
export async function asyncMap(array: Array<any>, cb: (entry: any, index: Number) => Promise<any>) {
export async function asyncMap<Type>(
array: Array<Type>,
cb: (entry: Type, index: Number) => Promise<any>,
) {
const mappingCallbacks = array.map(async (entry, index) => await cb(entry, index));

const mappedValues = await Promise.all(mappingCallbacks);

return mappedValues;
}

export async function asyncForEach(
array: Array<any>,
cb: (entry: any, index: Number) => Promise<any>,
export async function asyncForEach<Type>(
array: Array<Type>,
cb: (entry: Type, index: Number) => Promise<void>,
) {
await asyncMap(array, cb);
}

export async function asyncFilter(array: Array<any>, cb: (entry: any) => Promise<any>) {
export async function asyncFilter<Type>(array: Array<Type>, cb: (entry: Type) => Promise<boolean>) {
// map the elements to their value or undefined and then filter undefined entries
return (
await asyncMap(array, async (entry) => {
Expand Down
29 changes: 3 additions & 26 deletions src/management-system-v2/lib/process-export/PDFPageBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { jsPDF } from 'jspdf';

import { getImageDimensions } from './util';

type ContentPosition = 'left' | 'right' | 'center';

interface ContentInfo {
Expand Down Expand Up @@ -82,31 +84,6 @@ class PDFPageBuilder {
return { width: lineWidth, height: pdf.getLineHeight() };
}

/**
* Returns the dimensions of a vector-image
*
* @param svg the svg string to get the size from
* @returns the width and height of the image
*/
private getImageDimensions(svg: string) {
let width = 0;
let height = 0;

const viewBox = svg.split('<svg')[1].split('>')[0].split('viewBox="');

if (viewBox) {
[width, height] = viewBox[1].split('"')[0].split(' ').map(parseFloat).slice(2);
} else {
width = parseFloat(svg.split('width="')[1].split('"')[0]);
height = parseFloat(svg.split('height="')[1].split('"')[0]);
}

return {
width,
height,
};
}

/**
* Add a vector-image to the page
*
Expand Down Expand Up @@ -134,7 +111,7 @@ class PDFPageBuilder {
bottom: margins.bottom || 0,
};

const imageDimensions = this.getImageDimensions(svg);
const imageDimensions = getImageDimensions(svg);

// if the image is too small dont scale it up but place it in the middle of the space to occupy
if (size && imageDimensions.width < size.width && imageDimensions.height < size.height) {
Expand Down
34 changes: 34 additions & 0 deletions src/management-system-v2/lib/process-export/copy-process-image.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { toBpmnObject, toBpmnXml } from '@proceed/bpmn-helper';

import { getSVGFromBPMN } from './util';
import { getPNGFromSVG } from './image-export';

/**
* Adds an image of a process or selected parts of a process to the clipboard
*
* @param modeler the modeler to copy the process image from
*/
export async function copyProcessImage(modeler: any) {
let { xml } = await modeler.saveXML({ format: true });

// get the currently visible layer
const rootElement = (modeler.get('canvas') as any).getRootElement().businessObject;
const subprocessId =
rootElement.$type === 'bpmn:Process' || rootElement.$type === 'bpmn:Collaboration'
? undefined
: rootElement.id;

// get the selected elements
let selection: any[] = (modeler.get('selection') as any).get();
// get the png and copy it to the clipboard
const svg = await getSVGFromBPMN(
xml!,
subprocessId,
selection.map((el) => el.id),
);
const blob = await getPNGFromSVG(svg, 3);
const data = [new ClipboardItem({ 'image/png': blob })];
navigator.clipboard.write(data).then(() => {
console.log('Copied to clipboard');
});
}
30 changes: 28 additions & 2 deletions src/management-system-v2/lib/process-export/export-preparation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,20 @@ import {
getDefinitionsAndProcessIdForEveryCallActivity,
} from '@proceed/bpmn-helper';

import { asyncForEach, asyncMap } from '../helpers/javascriptHelpers';
import { getImageDimensions, getSVGFromBPMN } from './util';

/**
* The options that can be used to select what should be exported
*/
export type ProcessExportOptions = {
type: 'bpmn' | 'svg' | 'pdf';
type: 'bpmn' | 'svg' | 'pdf' | 'png';
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)
imports: boolean; // if processes referenced by this process should be exported as well
metaData: boolean; // (only pdf) if the process page should contain meta information about the process (name, version, [subprocess-id]) as text
a4: boolean; // if an a4 format should be used for the pdf pages (pdf)
scaling: number; // the scaling factor that should be used for png export
};

/**
Expand Down Expand Up @@ -155,6 +159,21 @@ function getVersionName(version?: string | number) {
return version ? `${version}` : 'latest';
}

async function getMaximumScalingFactor(exportData: ProcessesExportData) {
const allVersionBpmns = exportData.flatMap(({ versions }) =>
Object.values(versions).map(({ bpmn }) => bpmn),
);

const maximums = await asyncMap(allVersionBpmns, async (bpmn) => {
const svg = await getSVGFromBPMN(bpmn);
const diagramSize = getImageDimensions(svg);
// the canvas that is used to transform the svg to a png has a limited size (https://github.com/jhildenbiddle/canvas-size#test-results)
return Math.floor(Math.sqrt(268400000 / (diagramSize.width * diagramSize.height)));
});

return Math.min(...maximums);
}

/**
* Will fetch information for a process (version) from the backend if it is not present in the exportData yet
*
Expand Down Expand Up @@ -326,8 +345,15 @@ export async function prepareExport(
}
}

return Object.entries(exportData).map(([definitionId, data]) => ({
const finalExportData = Object.entries(exportData).map(([definitionId, data]) => ({
...data,
definitionId,
}));

if (options.type === 'png') {
// decrease the scaling factor if the image size would exceed export limits
options.scaling = Math.min(options.scaling, await getMaximumScalingFactor(finalExportData));
}

return finalExportData;
}
Loading

0 comments on commit c0a7e8f

Please sign in to comment.