Skip to content

Commit

Permalink
Ms2 Process Export (#85)
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

---------

Co-authored-by: winnie <[email protected]>
Co-authored-by: LaMaLein <[email protected]>
Co-authored-by: LaMaLein <[email protected]>
Co-authored-by: Kai Rohwer <[email protected]>
  • Loading branch information
5 people authored Oct 9, 2023
1 parent 926ce36 commit fc491d4
Show file tree
Hide file tree
Showing 8 changed files with 328 additions and 27 deletions.
17 changes: 16 additions & 1 deletion src/management-system-v2/components/modeler-toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import PropertiesPanel from './properties-panel';

import useModelerStateStore from '@/lib/use-modeler-state-store';
import { useParams } 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';
Expand All @@ -34,6 +37,7 @@ const ModelerToolbar: React.FC<ModelerToolbarProps> = ({ onOpenXmlEditor }) => {
const svgShare = <Icon component={SvgShare} />;

const [showPropertiesPanel, setShowPropertiesPanel] = useState(false);
const [showProcessExportModal, setShowProcessExportModal] = useState(false);

const modeler = useModelerStateStore((state) => state.modeler);
const selectedElementId = useModelerStateStore((state) => state.selectedElementId);
Expand Down Expand Up @@ -78,6 +82,12 @@ const ModelerToolbar: React.FC<ModelerToolbarProps> = ({ onOpenXmlEditor }) => {
setShowPropertiesPanel(!showPropertiesPanel);
};

const handleProcessExportModalToggle = async () => {
setShowProcessExportModal(!showProcessExportModal);
};

const selectedVersion = useModelerStateStore((state) => state.selectedVersion);

return (
<>
<Toolbar>
Expand All @@ -96,7 +106,7 @@ const ModelerToolbar: React.FC<ModelerToolbarProps> = ({ onOpenXmlEditor }) => {
<Button icon={svgXML} onClick={onOpenXmlEditor}></Button>
</Tooltip>
<Tooltip title="Export">
<Button icon={<ExportOutlined />}></Button>
<Button icon={<ExportOutlined />} onClick={handleProcessExportModalToggle}></Button>
</Tooltip>
<Tooltip title="Hide Non-Executeable Elements">
<Button icon={<EyeOutlined />}></Button>
Expand Down Expand Up @@ -129,6 +139,11 @@ const ModelerToolbar: React.FC<ModelerToolbarProps> = ({ onOpenXmlEditor }) => {
{/* {showPropertiesPanel && selectedElement && (
<PropertiesPanel selectedElement={selectedElement} setOpen={setShowPropertiesPanel} />
)} */}
<ProcessExportModal
processId={showProcessExportModal ? (processId as string) : undefined}
onClose={() => setShowProcessExportModal(false)}
processVersion={selectedVersion || undefined}
/>
</>
);
};
Expand Down
72 changes: 72 additions & 0 deletions src/management-system-v2/components/process-export.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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';

const exportTypeOptions = [
{ label: 'BPMN', value: 'bpmn' },
{ label: 'PDF', value: 'pdf' },
{ label: 'SVG', value: 'svg' },
];

type ProcessExportModalProps = {
processId?: string; // the id of the process to export; also used to decide if the modal should be opened
onClose: () => void;
processVersion?: number | string;
};

const ProcessExportModal: React.FC<ProcessExportModalProps> = ({
processId,
onClose,
processVersion,
}) => {
const [selectedTypes, setSelectedTypes] = useState<CheckboxValueType[]>([]);

const handleTypeSelectionChange = (checkedValues: CheckboxValueType[]) => {
// allow selection of exactly one element
if (!checkedValues.length) return;
setSelectedTypes(checkedValues.filter((el) => !selectedTypes.includes(el)));
};

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!';
}

onClose();
};

return (
<>
<Modal
title="Export selected process"
open={!!processId}
onOk={handleProcessExport}
onCancel={onClose}
centered
okButtonProps={{ disabled: !selectedTypes.length }}
>
<Checkbox.Group
options={exportTypeOptions}
onChange={handleTypeSelectionChange}
value={selectedTypes}
style={{ flexDirection: 'column' }}
/>
</Modal>
</>
);
};

export default ProcessExportModal;
15 changes: 13 additions & 2 deletions src/management-system-v2/components/process-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type ProcessListProps = PropsWithChildren<{
selection: Key[];
setSelection: Dispatch<SetStateAction<Key[]>>;
isLoading?: boolean;
onExportProcess: Dispatch<SetStateAction<string | undefined>>;
}>;

const ColumnHeader = [
Expand Down Expand Up @@ -64,7 +65,13 @@ const numberOfRows =
typeof window !== 'undefined' ? Math.floor((window?.innerHeight - 340) / 47) : 10;
console.log(numberOfRows);

const ProcessList: FC<ProcessListProps> = ({ data, selection, setSelection, isLoading }) => {
const ProcessList: FC<ProcessListProps> = ({
data,
selection,
setSelection,
isLoading,
onExportProcess,
}) => {
const router = useRouter();

const [previewerOpen, setPreviewerOpen] = useState(false);
Expand Down Expand Up @@ -104,7 +111,11 @@ const ProcessList: FC<ProcessListProps> = ({ data, selection, setSelection, isLo
<CopyOutlined />
</Tooltip>
<Tooltip placement="top" title={'Export'}>
<ExportOutlined />
<ExportOutlined
onClick={() => {
onExportProcess(record.definitionId);
}}
/>
</Tooltip>
<Tooltip placement="top" title={'Delete'}>
<DeleteOutlined />
Expand Down
14 changes: 13 additions & 1 deletion src/management-system-v2/components/processes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import IconView from './process-icon-list';
import ProcessList from './process-list';
import { Preferences, getPreferences, addUserPreference } from '@/lib/utils';
import MetaData from './process-info-card';
import ProcessExportModal from './process-export';
import Bar from './bar';

type Processes = ApiData<'/process', 'get'>;
Expand Down Expand Up @@ -58,6 +59,8 @@ const Processes: FC = () => {

const [iconView, setIconView] = useState(prefs['icon-view-in-process-list']);

const [exportProcessId, setExportProcessId] = useState<string | undefined>();

const actionBar = (
<>
{/* <Tooltip placement="top" title={'Preview'}>
Expand All @@ -67,7 +70,11 @@ const Processes: FC = () => {
<CopyOutlined />
</Tooltip>
<Tooltip placement="top" title={'Export'}>
<ExportOutlined />
<ExportOutlined
onClick={() => {
setExportProcessId(selectedRowKeys[0].toString());
}}
/>
</Tooltip>
<Tooltip placement="top" title={'Delete'}>
<DeleteOutlined />
Expand Down Expand Up @@ -173,12 +180,17 @@ const Processes: FC = () => {
selection={selectedRowKeys}
setSelection={setSelectedRowKeys}
isLoading={isLoading}
onExportProcess={setExportProcessId}
/>
)}
</div>
{/* Meta Data Panel */}
<MetaData data={filteredData} selection={selectedRowKeys} triggerRerender={rerenderLists} />
</div>
<ProcessExportModal
processId={exportProcessId}
onClose={() => setExportProcessId(undefined)}
/>
</>
);
};
Expand Down
131 changes: 131 additions & 0 deletions src/management-system-v2/lib/process-export.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { v4 } from 'uuid';
import { jsPDF } from 'jspdf';
import 'svg2pdf.js';

import { fetchProcessVersionBpmn, fetchProcess } from './process-queries';

async function getProcessData(processId: string, processVersion?: string | number) {
// TODO: we use the data for the name but it is maybe better to get the name from the bpmn since it might be different in versioned bpmn
const data = await fetchProcess(processId);

if (!data) {
throw new Error(
`Failed to get process info (definitionId: ${processId}) during process export`,
);
}

if (processVersion) {
const versionBpmn = await fetchProcessVersionBpmn(processId, processVersion);
if (!versionBpmn) {
throw new Error(
`Failed to get the bpmn for a version (${processVersion}) of a process (definitionId: ${processId})`,
);
}

data.bpmn = versionBpmn;
}

return data;
}

export async function exportBpmn(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);
}

async function getSVGFromBPMN(bpmn: string) {
const Viewer = (await import('bpmn-js/lib/Viewer')).default;

//Creating temporary element for BPMN Viewer
const viewerElement = document.createElement('div');

//Assiging process id to temp element and append to DOM
viewerElement.id = 'canvas_' + v4();
document.body.appendChild(viewerElement);

//Create a viewer to transform the bpmn into an svg
const viewer = new Viewer({ container: '#' + viewerElement.id });
await viewer.importXML(bpmn);
const { svg } = await viewer.saveSVG();

return svg;
}

export async function exportSVG(processId: string, processVersion?: string | number) {
const process = await getProcessData(processId, processVersion);

const svg = await getSVGFromBPMN(process.bpmn!);

const svgBlob = new Blob([svg], {
type: 'image/svg+xml',
});

exportFile(`${process.definitionName}.svg`, svgBlob);
}

export async function exportPDF(processId: string, processVersion?: string | number) {
const process = await getProcessData(processId, processVersion);

const svg = await getSVGFromBPMN(process.bpmn!);

const parser = new DOMParser();
const svgDOM = parser.parseFromString(svg, 'image/svg+xml');

const doc = new jsPDF({
unit: 'pt', // needed due to a bug in jsPDF: https://github.com/yWorks/svg2pdf.js/issues/245#issuecomment-1671624250
format: 'a4',
orientation: 'landscape',
});

doc.deletePage(1);

// 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: ${process.definitionName} \n`, 10, 15);

await doc.svg(svgDOM.children[0], {
x: 0,
y: 10,
width: pageWidth,
height: pageHeight,
});

await doc.save(`${process.definitionName}.pdf`);
}

function exportFile(processName: string, data: Blob) {
const objectURL = URL.createObjectURL(data);

// Creating Anchor Element to trigger download feature
const aLink = document.createElement('a');

// Setting anchor tag properties
aLink.style.display = 'none';
aLink.download = processName;
aLink.href = objectURL;

// Setting anchor tag to DOM
document.body.appendChild(aLink);
aLink.click();
document.body.removeChild(aLink);

// Release Object URL, so browser dont keep reference
URL.revokeObjectURL(objectURL);
}
Loading

0 comments on commit fc491d4

Please sign in to comment.