From 1a17c07344ec07a4be67331d5bfd645b27507ad3 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 19 Oct 2023 18:35:14 +0200 Subject: [PATCH] added components for process creation; use them on multiple occasions --- .../processes/[processId]/page.tsx | 9 +- .../components/ProcessSider.tsx | 74 +++++++ .../components/layout.tsx | 102 +--------- .../components/process-creation-button.tsx | 72 +++++++ .../components/process-modal.tsx | 90 +++++++++ .../components/version-toolbar.tsx | 157 ++++----------- .../lib/helpers/processHelpers.ts | 181 ++++++++++++++++++ 7 files changed, 459 insertions(+), 226 deletions(-) create mode 100644 src/management-system-v2/components/ProcessSider.tsx create mode 100644 src/management-system-v2/components/process-creation-button.tsx create mode 100644 src/management-system-v2/components/process-modal.tsx create mode 100644 src/management-system-v2/lib/helpers/processHelpers.ts diff --git a/src/management-system-v2/app/(dashboard)/processes/[processId]/page.tsx b/src/management-system-v2/app/(dashboard)/processes/[processId]/page.tsx index 2a5418d2b..de3d99898 100644 --- a/src/management-system-v2/app/(dashboard)/processes/[processId]/page.tsx +++ b/src/management-system-v2/app/(dashboard)/processes/[processId]/page.tsx @@ -23,6 +23,7 @@ import useModelerStateStore from '@/lib/use-modeler-state-store'; import { createNewProcessVersion } from '@/lib/helpers/processVersioning'; import VersionCreationButton from '@/components/version-creation-button'; import Auth from '@/lib/AuthCanWrapper'; +import ProcessCreationButton from '@/components/process-creation-button'; type ProcessProps = { params: { processId: string }; @@ -75,10 +76,6 @@ const Processes: FC = () => { } }, [minimized]); - const createProcess = () => { - console.log('create process'); - }; - const createProcessVersion = async (values: { versionName: string; versionDescription: string; @@ -119,9 +116,9 @@ const Processes: FC = () => { {menu} - + )} diff --git a/src/management-system-v2/components/ProcessSider.tsx b/src/management-system-v2/components/ProcessSider.tsx new file mode 100644 index 000000000..4d8630793 --- /dev/null +++ b/src/management-system-v2/components/ProcessSider.tsx @@ -0,0 +1,74 @@ +'use client'; + +import { FC, PropsWithChildren } from 'react'; +import { Menu } from 'antd'; +const { SubMenu, Item, ItemGroup } = Menu; +import { EditOutlined, ProfileOutlined, FileAddOutlined, StarOutlined } from '@ant-design/icons'; +import { usePathname, useRouter } from 'next/navigation'; +import { useAuthStore } from '@/lib/iam'; +import ProcessCreationButton from './process-creation-button'; + +const ProcessSider: FC = () => { + const router = useRouter(); + const activeSegment = usePathname().slice(1) || 'processes'; + const ability = useAuthStore((state) => state.ability); + + return ( + <> + + {ability.can('view', 'Process') ? ( + { + router.push(`/processes`); + }} + > + Process List + + } + className={activeSegment === 'processes' ? 'SelectedSegment' : ''} + icon={ + { + router.push(`/processes`); + }} + /> + } + > + } + hidden={!ability.can('create', 'Process')} + > + New Process} + > + + }> + Favorites + + + ) : null} + + {ability.can('view', 'Template') ? ( + }> + } + hidden={!ability.can('create', 'Template')} + > + New Template + + }> + Favorites + + + ) : null} + + + ); +}; + +export default ProcessSider; diff --git a/src/management-system-v2/components/layout.tsx b/src/management-system-v2/components/layout.tsx index 422cac39e..f7f3f7423 100644 --- a/src/management-system-v2/components/layout.tsx +++ b/src/management-system-v2/components/layout.tsx @@ -2,44 +2,15 @@ import styles from './layout.module.scss'; import { FC, PropsWithChildren, useState } from 'react'; -import { Layout as AntLayout, Grid, Menu, MenuProps } from 'antd'; -const { SubMenu, Item, Divider, ItemGroup } = Menu; -import Logo from '@/public/proceed.svg'; -import { - EditOutlined, - UnorderedListOutlined, - ProfileOutlined, - FileAddOutlined, - SettingOutlined, - ApiOutlined, - UserOutlined, - StarOutlined, - UnlockOutlined, -} from '@ant-design/icons'; +import { Layout as AntLayout, Grid, Menu } from 'antd'; +const { Item, Divider, ItemGroup } = Menu; +import { SettingOutlined, ApiOutlined, UserOutlined, UnlockOutlined } from '@ant-design/icons'; import Image from 'next/image'; -import { usePathname, useRouter } from 'next/navigation'; +import { usePathname } from 'next/navigation'; import cn from 'classnames'; import { useAuthStore } from '@/lib/iam'; import Link from 'next/link'; - -const items: MenuProps['items'] = [ - { - key: 'processes', - icon: , - label: 'Processes', - }, - { - key: 'projects', - icon: , - label: 'Projects', - }, - { - key: 'templates', - icon: , - label: 'Templates', - disabled: true, - }, -]; +import ProcessSider from './ProcessSider'; /** * The main layout of the application. It defines the sidebar and footer. Note @@ -51,7 +22,6 @@ const items: MenuProps['items'] = [ * page content in parallel routes. */ const Layout: FC = ({ children }) => { - const router = useRouter(); const activeSegment = usePathname().slice(1) || 'processes'; const [collapsed, setCollapsed] = useState(false); const ability = useAuthStore((state) => state.ability); @@ -88,68 +58,10 @@ const Layout: FC = ({ children }) => { {loggedIn ? ( - { - const path = key.split(':').at(-1); - router.push(`/${path}`); - }} - > + {ability.can('view', 'Process') || ability.can('view', 'Template') ? ( <> - - {ability.can('view', 'Process') ? ( - { - router.push(`/processes`); - }} - > - Process List - - } - className={activeSegment === 'processes' ? 'SelectedSegment' : ''} - icon={ - { - router.push(`/processes`); - }} - /> - } - > - } - hidden={!ability.can('create', 'Process')} - > - New Process - - }> - Favorites - - - ) : null} - - {ability.can('view', 'Template') ? ( - }> - } - hidden={!ability.can('create', 'Template')} - > - New Template - - }> - Favorites - - - ) : null} - - + ) : null} diff --git a/src/management-system-v2/components/process-creation-button.tsx b/src/management-system-v2/components/process-creation-button.tsx new file mode 100644 index 000000000..4ccae2c83 --- /dev/null +++ b/src/management-system-v2/components/process-creation-button.tsx @@ -0,0 +1,72 @@ +'use client'; + +import React, { ReactNode, useState } from 'react'; + +import { Button } from 'antd'; +import type { ButtonProps } from 'antd'; +import ProcessModal from './process-modal'; +import { usePostAsset } from '@/lib/fetch-data'; +import { createProcess } from '@/lib/helpers/processHelpers'; + +type ProcessCreationButtonProps = ButtonProps & { + createProcess?: (values: { name: string; description: string }) => any; + wrapperElement?: ReactNode; +}; + +/** + * + * Button to create Processes including a Modal for inserting needed values. Alternatively, a custom wrapper element can be used instead of a button. + * Custom function for creation of process using inserted values can be applied + */ +const ProcessCreationButton: React.FC = ({ + createProcess: customCreateProcess, + wrapperElement, + ...props +}) => { + const [isProcessModalOpen, setIsProcessModalOpen] = useState(false); + const { mutateAsync: postProcess } = usePostAsset('/process'); + + const createNewProcess = async (values: { name: string; description: string }) => { + const { metaInfo, bpmn } = await createProcess(values); + try { + await postProcess({ + body: { bpmn: bpmn, departments: [] }, + }); + } catch (err) { + console.log(err); + } + }; + + return ( + <> + {wrapperElement ? ( +
{ + setIsProcessModalOpen(true); + }} + > + {wrapperElement} +
+ ) : ( + + )} + { + setIsProcessModalOpen(false); + + if (values) { + customCreateProcess ? customCreateProcess(values) : createNewProcess(values); + } + }} + show={isProcessModalOpen} + > + + ); +}; + +export default ProcessCreationButton; diff --git a/src/management-system-v2/components/process-modal.tsx b/src/management-system-v2/components/process-modal.tsx new file mode 100644 index 000000000..2e918ca22 --- /dev/null +++ b/src/management-system-v2/components/process-modal.tsx @@ -0,0 +1,90 @@ +'use client'; + +import React, { useState } from 'react'; + +import { Button, Modal, Form, Input } from 'antd'; + +import { FormInstance } from 'antd/es/form'; + +const ModalSubmitButton = ({ form, onSubmit }: { form: FormInstance; onSubmit: Function }) => { + const [submittable, setSubmittable] = useState(false); + + // Watch all values + const values = Form.useWatch([], form); + + React.useEffect(() => { + form.validateFields({ validateOnly: true }).then( + () => { + setSubmittable(true); + }, + () => { + setSubmittable(false); + }, + ); + }, [form, values]); + + return ( + + ); +}; + +type ProcessModalProps = { + show: boolean; + close: (values?: { name: string; description: string }) => void; +}; +const ProcessModal: React.FC = ({ show, close }) => { + const [form] = Form.useForm(); + + return ( + { + close(); + }} + footer={[ + , + , + ]} + > +
+ + + + + + +
+
+ ); +}; + +export default ProcessModal; diff --git a/src/management-system-v2/components/version-toolbar.tsx b/src/management-system-v2/components/version-toolbar.tsx index 331f3a538..fdb01d4ea 100644 --- a/src/management-system-v2/components/version-toolbar.tsx +++ b/src/management-system-v2/components/version-toolbar.tsx @@ -6,108 +6,22 @@ import { manipulateElementsByTagName, generateDefinitionsId, getUserTaskFileNameMapping, - getDefinitionsVersionInformation, - setDefinitionsVersionInformation, - setUserTaskData, - toBpmnObject, - toBpmnXml, } from '@proceed/bpmn-helper'; -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import type ElementRegistry from 'diagram-js/lib/core/ElementRegistry'; -import { Tooltip, Button, Space, Modal, Form, Input } from 'antd'; +import { Tooltip, Button, Space, Modal } from 'antd'; import { FormOutlined, PlusOutlined } from '@ant-design/icons'; import useModelerStateStore from '@/lib/use-modeler-state-store'; import { useParams, useRouter } from 'next/navigation'; -import { FormInstance } from 'antd/es/form'; -import { useGetAsset, post, get, del, put } from '@/lib/fetch-data'; -import { areVersionsEqual, convertToEditableBpmn } from '@/lib/helpers/processVersioning'; +import { get, del, put, usePostAsset } from '@/lib/fetch-data'; +import { convertToEditableBpmn } from '@/lib/helpers/processVersioning'; import { asyncForEach, asyncMap } from '@/lib/helpers/javascriptHelpers'; - -const ModalSubmitButton = ({ form, onSubmit }: { form: FormInstance; onSubmit: Function }) => { - const [submittable, setSubmittable] = useState(false); - - // Watch all values - const values = Form.useWatch([], form); - - React.useEffect(() => { - form.validateFields({ validateOnly: true }).then( - () => { - setSubmittable(true); - }, - () => { - setSubmittable(false); - }, - ); - }, [form, values]); - - return ( - - ); -}; - -type ProcessModalProps = { - show: boolean; - close: (values?: { name: string; description: string }) => void; -}; -const ProcessModal: React.FC = ({ show, close }) => { - const [form] = Form.useForm(); - - return ( - { - close(); - }} - footer={[ - , - , - ]} - > -
- - - - - - -
-
- ); -}; +import ProcessCreationButton from './process-creation-button'; type ConfirmationModalProps = { show: boolean; @@ -133,11 +47,11 @@ const ConfirmationModal: React.FC = ({ show, close, conf type VersionToolbarProps = {}; const VersionToolbar: React.FC = () => { - const [isProcessModalOpen, setIsProcessModalOpen] = useState(false); const [isConfirmationModalOpen, setIsConfirmationModalOpen] = useState(false); const router = useRouter(); const modeler = useModelerStateStore((state) => state.modeler); const selectedElementId = useModelerStateStore((state) => state.selectedElementId); + const { mutateAsync: postProcess } = usePostAsset('/process'); // const [index, setIndex] = useState(0); const { processId } = useParams(); @@ -152,10 +66,6 @@ const VersionToolbar: React.FC = () => { : elementRegistry.getAll().filter((el) => el.businessObject.$type === 'bpmn:Process')[0]; } - const openNewProcessModal = () => { - setIsProcessModalOpen(true); - }; - const openConfirmationModal = () => { setIsConfirmationModalOpen(true); }; @@ -163,24 +73,28 @@ const VersionToolbar: React.FC = () => { const createNewProcess = async (values: { name: string; description: string }) => { const saveXMLResult = await modeler?.saveXML({ format: true }); if (saveXMLResult?.xml) { - const bpmn = saveXMLResult.xml; - const defId = generateDefinitionsId(); - let newBpmn = await setDefinitionsId(bpmn, defId); - newBpmn = await setDefinitionsName(newBpmn, values.name); - newBpmn = (await manipulateElementsByTagName( - newBpmn, - 'bpmn:Definitions', - (definitions: any) => { - delete definitions.version; - delete definitions.versionName; - delete definitions.versionDescription; - delete definitions.versionBasedOn; - }, - )) as string; - const response = await post('/process', { - body: { bpmn: newBpmn, /*description: values.description,*/ departments: [] }, - parseAs: 'text', - }); + try { + const bpmn = saveXMLResult.xml; + const defId = generateDefinitionsId(); + let newBpmn = await setDefinitionsId(bpmn, defId); + newBpmn = await setDefinitionsName(newBpmn, values.name); + newBpmn = (await manipulateElementsByTagName( + newBpmn, + 'bpmn:Definitions', + (definitions: any) => { + delete definitions.version; + delete definitions.versionName; + delete definitions.versionDescription; + delete definitions.versionBasedOn; + }, + )) as string; + + await postProcess({ + body: { bpmn: newBpmn, departments: [] }, + }); + } catch (err) { + console.log(err); + } } }; @@ -282,23 +196,16 @@ const VersionToolbar: React.FC = () => {
- + } + createProcess={createNewProcess} + >
- { - setIsProcessModalOpen(false); - - if (values) { - createNewProcess(values); - } - }} - show={isProcessModalOpen} - > { setIsConfirmationModalOpen(false); diff --git a/src/management-system-v2/lib/helpers/processHelpers.ts b/src/management-system-v2/lib/helpers/processHelpers.ts new file mode 100644 index 000000000..f67a8cce2 --- /dev/null +++ b/src/management-system-v2/lib/helpers/processHelpers.ts @@ -0,0 +1,181 @@ +import { + toBpmnObject, + toBpmnXml, + getElementsByTagName, + generateDefinitionsId, + setDefinitionsId, + setStandardDefinitions, + setTargetNamespace, + addDocumentationToProcessObject, + getProcessDocumentationByObject, + getExporterName, + getExporterVersion, + getDefinitionsInfos, + initXml, + getDefinitionsName, + setDefinitionsName, + getOriginalDefinitionsId, + generateProcessId, + getIdentifyingInfos, +} from '@proceed/bpmn-helper'; + +interface ProceedProcess { + id?: string; + type?: string; + originalId?: string; + name?: string; + description?: string; + processIds?: string[]; + variables?: { name: string; type: string }[]; + departments?: string[]; + inEditingBy?: { id: string; task: string | null }[]; + createdOn?: string; + lastEdited?: string; + shared?: boolean; + versions?: Array; +} + +interface ProcessInfo { + bpmn: string; + metaInfo: ProceedProcess; +} + +/** + * Creates a default process meta object containing all fields we expect a process meta object to have + * + */ +export function getDefaultProcessMetaInfo() { + const date = new Date().toUTCString(); + return { + id: '', + type: 'process', + originalId: '', + name: 'Default Process', + description: '', + owner: null, + processIds: [], + variables: [], + departments: [], + inEditingBy: [], + createdOn: date, + lastEdited: date, + shared: false, + versions: [], + } as ProceedProcess; +} + +/** + * Creates a new proceed process either from a given bpmn or from a default bpmn template + * creates a bpmn and a meta info object + */ +export async function createProcess( + processInfo: ProceedProcess & { bpmn?: string }, + noDefaults: boolean = false, +) { + let metaInfo = { ...processInfo }; + delete metaInfo.bpmn; + + // create default bpmn if user didn't provide any + let bpmn = processInfo.bpmn || initXml(); + + let definitions; + + try { + const xmlObj = await toBpmnObject(bpmn); + [definitions] = getElementsByTagName(xmlObj, 'bpmn:Definitions'); + } catch (err) { + throw new Error(`Invalid bpmn: ${err}`); + } + + // if we import a process not created in proceed we set the id to a proceed conform id + const { exporter, id: importDefinitionsId } = await getDefinitionsInfos(definitions); + if ( + exporter !== getExporterName() && + (!processInfo.id || processInfo.id === importDefinitionsId) + ) { + processInfo.id = generateDefinitionsId(); + } + + if (!processInfo.name) { + // try to get name from bpmn object + metaInfo.name = (await getDefinitionsName(definitions))!; + } + + setStandardDefinitions(definitions, getExporterName(), getExporterVersion()); + + if (!metaInfo.name) { + throw new Error( + 'No name provided (name can be provided in the general information or in the definitions of the given bpmn)', + ); + } + + // specifically provided id takes precedence over existing id and if there is none a new one is created + metaInfo.id = processInfo.id || importDefinitionsId || generateDefinitionsId(); + + await setDefinitionsId(definitions, metaInfo.id); + await setDefinitionsName(definitions, metaInfo.name); + + if (!processInfo.originalId) { + metaInfo.originalId = (await getOriginalDefinitionsId(definitions))!; + } + + await setTargetNamespace(definitions, metaInfo.id); + + const processes = getElementsByTagName(definitions, 'bpmn:Process'); + + // make sure every process has an id + processes.forEach((p) => { + if (!p.id) { + p.id = generateProcessId(); + } + }); + + metaInfo.processIds = processes.map((p) => p.id); + + const [process] = processes; + + // if the user gave a process description make sure to write it into bpmn + if (process && processInfo.hasOwnProperty('description')) { + addDocumentationToProcessObject(process, processInfo.description); + } + + metaInfo.description = getProcessDocumentationByObject(process); + + bpmn = await toBpmnXml(definitions); + + if (!noDefaults) { + // make sure metaInfo has all necessary entries for a process meta object + metaInfo = { ...getDefaultProcessMetaInfo(), ...metaInfo }; + } + + return { metaInfo, bpmn } as ProcessInfo; +} + +/** + * Parses all necessary information from a process description + * + */ +export async function getProcessInfo(bpmn: string) { + if (!bpmn || typeof bpmn !== 'string') { + throw new Error(`Expected given bpmn to be of type string but got ${typeof bpmn} instead!`); + } + + let definitions; + try { + definitions = await toBpmnObject(bpmn); + } catch (err) { + throw new Error(`Given process description is invalid. Reason:\n${err}`); + } + + const metadata = await getIdentifyingInfos(definitions); + + if (!metadata.id) { + throw new Error('Process definitions do not contain an id.'); + } + + if (!metadata.name) { + throw new Error('Process definitions do not contain a name.'); + } + + return metadata; +}