From d511098d24f3a41fe64c93de4c4b2a45be0f3e9a Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Tue, 10 Feb 2026 09:19:00 +0100 Subject: [PATCH 01/17] Added two ways to deploy processes from the modeler 1. The version creation modal has a new button that combines version creation and deployment 2. There is a new button in the toolbar to deploy the latest version and automatically start an instance of it --- .../[processId]/process-deployment-view.tsx | 18 +- .../[processId]/start-form-modal.tsx | 58 ++++- .../[mode]/[processId]/modeler-toolbar.tsx | 94 +------ .../script-task-editor/script-task-editor.tsx | 17 +- .../version-and-deploy-section.tsx | 244 ++++++++++++++++++ .../components/engine-selection.tsx | 94 +++++++ .../components/html-form-editor/utils.tsx | 1 - .../components/processes/index.tsx | 11 +- .../components/version-creation-button.tsx | 185 +++++++++++-- .../lib/engines/deployment.ts | 41 ++- .../lib/engines/server-actions.ts | 24 +- .../lib/helpers/processVersioning.ts | 76 +++--- 12 files changed, 679 insertions(+), 184 deletions(-) create mode 100644 src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/version-and-deploy-section.tsx create mode 100644 src/management-system-v2/components/engine-selection.tsx diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/process-deployment-view.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/process-deployment-view.tsx index a29c81eb6..e89af204b 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/process-deployment-view.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/process-deployment-view.tsx @@ -193,14 +193,6 @@ export default function ProcessDeploymentView({ if (typeof startForm !== 'string') return startForm; if (startForm) { - const mappedVariables = Object.fromEntries( - variables - .filter((variable) => variable.value !== undefined) - .map((variable) => [variable.name, variable.value]), - ); - startForm = inlineScript(startForm, '', '', variableDefinitions); - startForm = inlineUserTaskData(startForm, mappedVariables, []); - setStartForm(startForm); } else { return startInstance(versionId); @@ -363,19 +355,13 @@ export default function ProcessDeploymentView({ { const versionId = getLatestDeployment(deploymentInfo).versionId; - const mappedVariables: Record = {}; - - // set the values of variables to the ones coming from the start form - Object.entries(submitVariables).forEach( - ([key, value]) => (mappedVariables[key] = { value }), - ); - // start the instance with the initial variable values from the start form await wrapServerCall({ - fn: () => startInstance(versionId, mappedVariables), + fn: () => startInstance(versionId, submitVariables), onSuccess: async (instanceId) => { await refetch(); diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/start-form-modal.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/start-form-modal.tsx index b0d4d7c94..fc1622a93 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/start-form-modal.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/start-form-modal.tsx @@ -1,16 +1,66 @@ +import { useMemo } from 'react'; + import { Modal } from 'antd'; import { UserTaskForm } from '../../../tasklist/user-task-view'; +import { ProcessVariable } from '@/lib/process-variable-schema'; +import { inlineScript, inlineUserTaskData } from '@proceed/user-task-helper'; type StartFormModalProps = { html?: string; - onSubmit: (variables: { [key: string]: any }) => Promise; + variableDefinitions?: ProcessVariable[]; + onSubmit: (variables: { [key: string]: { value: any } }) => void; onCancel: () => void; }; -const StartFormModal: React.FC = ({ html, onSubmit, onCancel }) => { +const StartFormModal: React.FC = ({ + html, + variableDefinitions, + onSubmit, + onCancel, +}) => { + const finalHtml = useMemo(() => { + if (!html) return; + + // populate the placeholders with default values from the variable definitions or with empty + // strings + const mappedVariables = Object.fromEntries( + (variableDefinitions || []) + .filter((variable) => variable.defaultValue !== undefined) + .map((variable) => { + let value: string | number | boolean | undefined = variable.defaultValue; + + if (value) { + // transform from the string representation of the default value to the type defined for + // the respective variable + switch (variable.dataType) { + case 'number': + value = parseFloat(value); + break; + case 'boolean': + value = value === 'true' ? true : false; + break; + } + } + + return [variable.name, value]; + }), + ); + const finalHtml = inlineScript(html, '', '', variableDefinitions); + return inlineUserTaskData(finalHtml, mappedVariables, []); + }, [html, variableDefinitions]); + + const handleSubmit = async (variables: Record) => { + // map the variable info to the format expected by the engine + const mappedVariables = Object.fromEntries( + Object.entries(variables).map(([key, value]) => [key, { value }]), + ); + + onSubmit(mappedVariables); + }; + return ( = ({ html, onSubmit, onCance }, }} > - + ); }; diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/modeler-toolbar.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/modeler-toolbar.tsx index 34039ce3b..d72b199ef 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/modeler-toolbar.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/modeler-toolbar.tsx @@ -1,11 +1,10 @@ import { ComponentProps, use, useEffect, useState } from 'react'; import { is as bpmnIs } from 'bpmn-js/lib/util/ModelUtil'; -import { App, Tooltip, Button, Space, Select, SelectProps, Divider } from 'antd'; +import { Tooltip, Button, Space, Divider } from 'antd'; import { Toolbar, ToolbarGroup } from '@/components/toolbar'; import styles from './modeler-toolbar.module.scss'; import Icon, { InfoCircleOutlined, - PlusOutlined, UndoOutlined, RedoOutlined, ArrowUpOutlined, @@ -17,15 +16,13 @@ import { PiDownloadSimple } from 'react-icons/pi'; import { SvgGantt, SvgXML } from '@/components/svg'; import PropertiesPanel from './properties-panel'; import useModelerStateStore from './use-modeler-state-store'; -import { useRouter, useSearchParams, usePathname } from 'next/navigation'; -import VersionCreationButton from '@/components/version-creation-button'; +import { useSearchParams } from 'next/navigation'; import useMobileModeler from '@/lib/useMobileModeler'; -import { createVersion, updateProcess, getProcessBPMN } from '@/lib/data/processes'; +import { updateProcess } from '@/lib/data/processes'; import { Root } from 'bpmn-js/lib/model/Types'; import { useEnvironment } from '@/components/auth-can'; import { ShareModal } from '@/components/share-modal/share-modal'; import { useAddControlCallback } from '@/lib/controls-store'; -import { spaceURL } from '@/lib/utils'; import { isUserErrorResponse } from '@/lib/user-error'; import useTimelineViewStore from '@/lib/use-timeline-view-store'; import { handleOpenDocumentation } from '../../processes-helper'; @@ -42,7 +39,7 @@ import { Element } from 'bpmn-js/lib/model/Types'; import { ScriptTaskEditorEnvironment } from './script-task-editor/script-task-editor-environment'; import { Folder } from '@/lib/data/folder-schema'; -const LATEST_VERSION = { id: '-1', name: 'Latest Version', description: '' }; +import VersionAndDeploy, { LATEST_VERSION } from './version-and-deploy-section'; type ModelerToolbarProps = { process: Process; @@ -60,11 +57,7 @@ const ModelerToolbar = ({ }: ModelerToolbarProps) => { const processId = process.id; - const router = useRouter(); - const pathname = usePathname(); const environment = useEnvironment(); - const app = App.useApp(); - const message = app.message; const env = use(EnvVarsContext); const [showUserTaskEditor, setShowUserTaskEditor] = useState(false); @@ -113,7 +106,7 @@ const ModelerToolbar = ({ const query = useSearchParams(); const subprocessId = query.get('subprocess'); - const { isListView, processContextPath } = useProcessView(); + const { isListView } = useProcessView(); const modeler = useModelerStateStore((state) => state.modeler); const isExecutable = useModelerStateStore((state) => state.isExecutable); @@ -170,41 +163,6 @@ const ModelerToolbar = ({ const selectedVersionId = query.get('version'); - const createProcessVersion = async (values: { - versionName: string; - versionDescription: string; - }) => { - try { - // Ensure latest BPMN on server. - const xml = (await modeler?.getXML()) as string; - if (isUserErrorResponse(await updateProcess(processId, environment.spaceId, xml))) - throw new Error(); - - if ( - isUserErrorResponse( - await createVersion( - values.versionName, - values.versionDescription, - processId, - environment.spaceId, - ), - ) - ) - throw new Error(); - - // reimport the new version since the backend has added versionBasedOn information that would - // be overwritten by following changes - const newBpmn = await getProcessBPMN(processId, environment.spaceId); - if (newBpmn && typeof newBpmn === 'string') { - await modeler?.loadBPMN(newBpmn); - } - - router.refresh(); - message.success('Version Created'); - } catch (_) { - message.error('Something went wrong'); - } - }; const handlePropertiesPanelToggle = () => { setShowPropertiesPanel(!showPropertiesPanel); }; @@ -253,9 +211,6 @@ const ModelerToolbar = ({ } }; - const filterOption: SelectProps['filterOption'] = (input, option) => - ((option?.label as string) ?? '').toLowerCase().includes(input.toLowerCase()); - const selectedVersion = process.versions.find((version) => version.id === (selectedVersionId ?? '-1')) ?? LATEST_VERSION; @@ -285,46 +240,9 @@ const ModelerToolbar = ({ }} > - { + // change the version info in the query but keep other info (e.g. the currently open subprocess) + const searchParams = new URLSearchParams(query); + if (!value || value === '-1') searchParams.delete('version'); + else searchParams.set(`version`, `${value}`); + router.push( + spaceURL( + environment, + `/processes${processContextPath}/${processId as string}${ + searchParams.size ? '?' + searchParams.toString() : '' + }`, + ), + ); + }} + options={(isListView ? [] : [LATEST_VERSION]) + .concat(process.versions ?? []) + .map(({ id, name }) => ({ + value: id, + label: name, + }))} + /> + {!showMobileView && LATEST_VERSION.id === selectedVersion.id && ( + <> + + } + createVersion={createProcessVersion} + disabled={isListView} + isExecutable={isExecutable} + /> + + {env.PROCEED_PUBLIC_PROCESS_AUTOMATION_ACTIVE && isExecutable && ( + <> + + { - if (values) { - const createResult = createVersion(values); + processId={processId} + close={async (values, deploy) => { + if (values || deploy) { + const createResult = createVersion(values, deploy); if (createResult instanceof Promise) { setLoading(true); await createResult; @@ -98,6 +226,7 @@ const VersionCreationButton = forwardRef ); diff --git a/src/management-system-v2/lib/engines/deployment.ts b/src/management-system-v2/lib/engines/deployment.ts index 963fd11a5..db744c739 100644 --- a/src/management-system-v2/lib/engines/deployment.ts +++ b/src/management-system-v2/lib/engines/deployment.ts @@ -5,7 +5,9 @@ import { getProcessConstraints, getStartEvents, getTaskConstraintMapping, + setDefinitionsVersionInformation, toBpmnObject, + toBpmnXml, } from '@proceed/bpmn-helper'; // TODO: remove this ignore once the decider is typed // @ts-ignore @@ -14,8 +16,9 @@ import { Engine } from './machines'; import { prepareExport } from '../process-export/export-preparation'; import { Prettify } from '../typescript-utils'; import { engineRequest } from './endpoints/index'; -import { asyncForEach } from '../helpers/javascriptHelpers'; +import { asyncForEach, asyncMap } from '../helpers/javascriptHelpers'; import { UserFacingError } from '../user-error'; +import { toCustomUTCString } from '../helpers/timeHelper'; type ProcessesExportData = Prettify>>; @@ -40,6 +43,7 @@ async function deployProcessToMachines( return Promise.all( processesExportData!.map(async (exportData) => { const version = Object.values(exportData.versions)[0]; + await engineRequest({ method: 'post', endpoint: '/process/', @@ -47,8 +51,9 @@ async function deployProcessToMachines( engine, }); + let startForm = Promise.resolve(); if (version.startForm) { - engineRequest({ + startForm = engineRequest({ method: 'put', endpoint: '/process/:definitionId/versions/:version/start-form', pathParams: { @@ -99,7 +104,7 @@ async function deployProcessToMachines( }), ); - await Promise.all([...scripts, ...userTasks, ...images]); + await Promise.all([...scripts, ...userTasks, ...images, startForm]); }), ); }); @@ -208,7 +213,7 @@ export async function deployProcess( ) { if (machines.length === 0) throw new UserFacingError('No machines available for deployment'); - const processesExportData = await prepareExport( + let processesExportData = await prepareExport( { type: 'bpmn', subprocesses: true, @@ -226,10 +231,34 @@ export async function deployProcess( spaceId, ); + // when deploying the latest version as a test deployment make sure it contains version + // information + processesExportData = await asyncMap(processesExportData, async (process) => { + let versions = process.versions; + + if ('latest' in process.versions) { + const versionCreatedOn = toCustomUTCString(new Date()); + const { latest } = versions; + latest.name = 'Latest'; + const bpmnObj = await toBpmnObject(latest.bpmn); + await setDefinitionsVersionInformation(bpmnObj, { + versionId: '_latest', + versionName: 'Latest', + versionDescription: 'Test version for the current state of the bpmn in the modeler', + versionCreatedOn, + }); + latest.bpmn = await toBpmnXml(bpmnObj); + process.versions._latest = latest; + delete process.versions.latest; + } + + return { ...process, versions }; + }); + if (method === 'static') { - await staticDeployment(definitionId, version, processesExportData, machines); + await staticDeployment(definitionId, version || '_latest', processesExportData, machines); } else { - await dynamicDeployment(definitionId, version, processesExportData, machines); + await dynamicDeployment(definitionId, version || '_latest', processesExportData, machines); } } export type ImportInformation = { definitionId: string; processId: string; version: number }; diff --git a/src/management-system-v2/lib/engines/server-actions.ts b/src/management-system-v2/lib/engines/server-actions.ts index 858a997c4..0f832fca2 100644 --- a/src/management-system-v2/lib/engines/server-actions.ts +++ b/src/management-system-v2/lib/engines/server-actions.ts @@ -49,6 +49,7 @@ import { Variable } from '@proceed/bpmn-helper/src/getters'; import { getUsersInSpace } from '../data/db/iam/memberships'; import Ability from '../ability/abilityHelper'; import { getUserById } from '../data/db/iam/users'; +import { engineRequest } from './endpoints'; export async function getCorrectTargetEngines( spaceId: string, @@ -78,12 +79,33 @@ export async function getCorrectTargetEngines( return engines; } +export async function getExtendedEngines(spaceId: string) { + const engines = await getCorrectTargetEngines(spaceId); + + let extendedEngines = await asyncMap(engines, async (engine) => { + const { name } = (await engineRequest({ + engine, + method: 'get', + endpoint: '/machine/:properties', + pathParams: { properties: 'name' }, + })) as { name: string }; + + return { ...engine, name }; + }); + + const uniqueEngines: Record = {}; + + extendedEngines.forEach((engine) => (uniqueEngines[engine.id] = engine)); + + return Object.values(uniqueEngines); +} + export async function deployProcess( definitionId: string, versionId: string, spaceId: string, method: 'static' | 'dynamic' = 'dynamic', - _forceEngine?: SpaceEngine | 'PROCEED', + _forceEngine?: Engine | 'PROCEED', ) { try { // TODO: manage permissions for deploying a process diff --git a/src/management-system-v2/lib/helpers/processVersioning.ts b/src/management-system-v2/lib/helpers/processVersioning.ts index c70a60aa4..9c468893d 100644 --- a/src/management-system-v2/lib/helpers/processVersioning.ts +++ b/src/management-system-v2/lib/helpers/processVersioning.ts @@ -11,6 +11,7 @@ import { setScriptTaskData, getStartFormFileNameMapping, setStartFormFileName, + getDefinitionsId, } from '@proceed/bpmn-helper'; import { asyncForEach } from './javascriptHelpers'; @@ -27,7 +28,6 @@ import { getProcessHtmlFormJSON, getHtmlForm, } from '@/lib/data/db/process'; -import { getProcessHtmlFormHTML } from '../data/processes'; const { diff } = require('bpmn-js-differ'); // TODO: This used to be a helper file in the old management system. It used @@ -36,39 +36,57 @@ const { diff } = require('bpmn-js-differ'); // should be refactored to reflect the fact this runs on the server now. export async function areVersionsEqual(bpmn: string, otherBpmn: string) { - const bpmnObj = await toBpmnObject(bpmn); - const otherBpmnObj = await toBpmnObject(otherBpmn); + // compare the bpmn of both versions excluding the differences that would come from one being + // versioned and the other not being versioned + const unversionedOther = await convertToEditableBpmn(otherBpmn); - const { - versionId, - name: versionName, - description: versionDescription, - versionBasedOn, - versionCreatedOn, - } = await getDefinitionsVersionInformation(otherBpmnObj); - - if (versionId) { - // check if the two bpmns were the same if they had the same version information - await setDefinitionsVersionInformation(bpmnObj, { - versionId, - versionName, - versionDescription, - versionBasedOn, - versionCreatedOn, - }); + const bpmnObj = await toBpmnObject(bpmn); + const unversionedOtherObj = await toBpmnObject(unversionedOther.bpmn); + + // compare the two bpmns in a way that is not affected by the ordering of elements in the text + // file + const changes = diff(unversionedOtherObj, bpmnObj); + const hasChanges = + Object.keys(changes._changed).length || + Object.keys(changes._removed).length || + Object.keys(changes._added).length || + Object.keys(changes._layoutChanged).length; + + if (hasChanges) return false; + + const definitionsId = await getDefinitionsId(bpmnObj); + + // compare the assets used in the bpmns + // (we expect that the "same" asset names are referenced since the comparison above would find differences otherwise) + + // compare start forms + for (const [versioned, unversioned] of Object.entries( + unversionedOther.changedStartFormTaskFileNames, + )) { + const versionedHtmlForm = await getHtmlForm(definitionsId, versioned); + const unversionedHtmlForm = await getHtmlForm(definitionsId, unversioned); + if (versionedHtmlForm !== unversionedHtmlForm) return false; + } - // compare the two bpmns - const changes = diff(otherBpmnObj, bpmnObj); - const hasChanges = - Object.keys(changes._changed).length || - Object.keys(changes._removed).length || - Object.keys(changes._added).length || - Object.keys(changes._layoutChanged).length; + // compare user tasks + for (const [versioned, unversioned] of Object.entries( + unversionedOther.changedUserTaskFileNames, + )) { + const versionedHtmlForm = await getHtmlForm(definitionsId, versioned); + const unversionedHtmlForm = await getHtmlForm(definitionsId, unversioned); + if (versionedHtmlForm !== unversionedHtmlForm) return false; + } - return !hasChanges; + // compare script tasks + for (const [versioned, unversioned] of Object.entries( + unversionedOther.changedScriptTaskFileNames, + )) { + const versionedScript = await getProcessScriptTaskScript(definitionsId, versioned + '.js'); + const unversionedScript = await getProcessScriptTaskScript(definitionsId, unversioned + '.js'); + if (versionedScript !== unversionedScript) return false; } - return false; + return true; } export async function convertToEditableBpmn(bpmn: string) { From 54c8336d25f77445bf0867d6e879e87765fa5982 Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Tue, 10 Feb 2026 13:11:03 +0100 Subject: [PATCH 02/17] Fixed: the executable checkbox in the properties panel always shows the value for the latest version and not for the currently selected version --- .../processes/[mode]/[processId]/modeler.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/modeler.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/modeler.tsx index 96d02f3e6..4d7267e6c 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/modeler.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/modeler.tsx @@ -55,10 +55,6 @@ const Modeler = ({ versionName, process, folder, inEditing, ...divProps }: Model const setFullScreen = useModelerStateStore((state) => state.setFullScreen); const setIsExecutable = useModelerStateStore((state) => state.setIsExecutable); - useEffect(() => { - setIsExecutable(process.executable || false); - }, [process]); - const { isListView, processContextPath } = useProcessView(); /// Derived State @@ -286,6 +282,14 @@ const Modeler = ({ versionName, process, folder, inEditing, ...divProps }: Model ); } } + + // set the executable value for the currently open version + const root = modeler.current?.getCurrentRoot(); + if (root && bpmnIs(root, 'bpmn:Process')) { + const executable = root.businessObject.isExecutable; + console.log(executable); + setIsExecutable(executable || false); + } }, [messageApi, subprocessId]); const onShapeRemove = useCallback['onShapeRemove']>((element) => { From 5b52a3637c2b2b904ed99531e7dd080588ee35d4 Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Tue, 10 Feb 2026 14:36:36 +0100 Subject: [PATCH 03/17] Removed unused imports --- src/management-system-v2/lib/engines/server-actions.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/management-system-v2/lib/engines/server-actions.ts b/src/management-system-v2/lib/engines/server-actions.ts index 0f832fca2..d81246d8e 100644 --- a/src/management-system-v2/lib/engines/server-actions.ts +++ b/src/management-system-v2/lib/engines/server-actions.ts @@ -2,7 +2,6 @@ import { UserFacingError, getErrorMessage, userError } from '../user-error'; import { - DeployedProcessInfo, deployProcess as _deployProcess, getDeployments as fetchDeployments, getDeployment as fetchDeployment, @@ -11,7 +10,7 @@ import { } from './deployment'; import { Engine, SpaceEngine } from './machines'; import { savedEnginesToEngines } from './saved-engines-helpers'; -import { getCurrentEnvironment, getCurrentUser } from '@/components/auth'; +import { getCurrentEnvironment } from '@/components/auth'; import { enableUseDB } from 'FeatureFlags'; import { getDbEngines, getDbEngineByAddress } from '@/lib/data/db/engines'; import { asyncFilter, asyncMap, asyncForEach } from '../helpers/javascriptHelpers'; @@ -46,8 +45,6 @@ import { import { getFileFromMachine, submitFileToMachine, updateVariablesOnMachine } from './instances'; import { getProcessIds, getVariablesFromElementById } from '@proceed/bpmn-helper'; import { Variable } from '@proceed/bpmn-helper/src/getters'; -import { getUsersInSpace } from '../data/db/iam/memberships'; -import Ability from '../ability/abilityHelper'; import { getUserById } from '../data/db/iam/users'; import { engineRequest } from './endpoints'; From 7242465437a4ded35ddacb36a386e5b1875b646e Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Tue, 10 Feb 2026 14:44:07 +0100 Subject: [PATCH 04/17] Added version and deploy functionality to the process list and added button to deploy and start a process from the process editoor --- .../version-and-deploy-section.tsx | 259 +++++++++++++----- .../components/processes/index.tsx | 86 ++++-- 2 files changed, 241 insertions(+), 104 deletions(-) diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/version-and-deploy-section.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/version-and-deploy-section.tsx index fb894a108..65582033b 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/version-and-deploy-section.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/version-and-deploy-section.tsx @@ -1,5 +1,5 @@ import { App, Button, Select, SelectProps, Tooltip } from 'antd'; -import { PlusOutlined, ExperimentOutlined } from '@ant-design/icons'; +import { PlusOutlined, ExperimentOutlined, CaretRightOutlined } from '@ant-design/icons'; import { useEnvironment } from '@/components/auth-can'; import { Process } from '@/lib/data/process-schema'; @@ -34,36 +34,50 @@ type VersionAndDeployProps = { process: Process; }; -const VersionAndDeploy: React.FC = ({ process }) => { - const processId = process.id; +export function useVersionAndDeploy( + processId: string | undefined, + isExecutable: boolean, + getBpmn: (versionId?: string) => Promise, + beforeCreateVersion?: () => Promise, + afterCreateVersion?: () => Promise, +) { const router = useRouter(); - const query = useSearchParams(); const environment = useEnvironment(); - const app = App.useApp(); - const message = app.message; + const env = use(EnvVarsContext); - const { isListView, processContextPath } = useProcessView(); - const showMobileView = useMobileModeler(); + const { message } = App.useApp(); const [versionToDeploy, setVersionToDeploy] = useState(''); - const [startForm, setStartForm] = useState(''); + const [autoStartInstance, setAutoStartInstance] = useState(false); - const env = use(EnvVarsContext); - - const modeler = useModelerStateStore((state) => state.modeler); - const isExecutable = useModelerStateStore((state) => state.isExecutable); + const [startForm, setStartForm] = useState(''); - const { variables } = useProcessVariables(); + const [deployTo, setDeployTo] = useState(); - const selectedVersionId = query.get('version'); + const cancelStartForm = () => { + setStartForm(''); + setDeployTo(undefined); + }; - const selectedVersion = - process.versions.find((version) => version.id === (selectedVersionId ?? '-1')) ?? - LATEST_VERSION; + const cancelDeploy = () => { + setAutoStartInstance(false); + setVersionToDeploy(''); + }; - const filterOption: SelectProps['filterOption'] = (input, option) => - ((option?.label as string) ?? '').toLowerCase().includes(input.toLowerCase()); + if (!processId) { + return { + createProcessVersion: async () => {}, + deployVersion: async () => {}, + startInstance: async () => {}, + startForm, + cancelStartForm, + versionToDeploy, + setVersionToDeploy, + cancelDeploy, + autoStartInstance: () => setAutoStartInstance(true), + }; + } const createProcessVersion = async ( values?: { @@ -76,10 +90,7 @@ const VersionAndDeploy: React.FC = ({ process }) => { let toDeploy = deploy; if (values) { - // Ensure latest BPMN on server. - const xml = (await modeler?.getXML()) as string; - if (isUserErrorResponse(await updateProcess(processId, environment.spaceId, xml))) - throw new Error(); + if (beforeCreateVersion) await beforeCreateVersion(); const newVersion = await createVersion( values.versionName, @@ -92,12 +103,7 @@ const VersionAndDeploy: React.FC = ({ process }) => { toDeploy = newVersion || false; - // reimport the new version since the backend has added versionBasedOn information that would - // be overwritten by following changes - const newBpmn = await getProcessBPMN(processId, environment.spaceId); - if (newBpmn && typeof newBpmn === 'string') { - await modeler?.loadBPMN(newBpmn); - } + if (afterCreateVersion) await afterCreateVersion(); router.refresh(); message.success('Version Created'); @@ -111,27 +117,43 @@ const VersionAndDeploy: React.FC = ({ process }) => { } }; - const [deployTo, setDeployTo] = useState(); - const startInstance = async (variables?: Record) => { - if (deployTo) { - const instanceId = await startInstanceOnMachine(process.id, '_latest', deployTo, variables); - router.push(spaceURL(environment, `/executions/${process.id}?instance=${instanceId}`)); + if (!env.PROCEED_PUBLIC_PROCESS_AUTOMATION_ACTIVE) return; + + if (!isExecutable) { + message.error( + 'Starting an instance is not possible while the process is not set to executable.', + ); + return; } - setStartForm(''); - setDeployTo(undefined); - }; - const cancelStartForm = async () => { + if (deployTo) { + const instanceId = await startInstanceOnMachine( + processId, + versionToDeploy === 'latest' ? '_latest' : versionToDeploy, + deployTo, + variables, + ); + router.push(spaceURL(environment, `/executions/${processId}?instance=${instanceId}`)); + } setStartForm(''); + setVersionToDeploy(''); + setAutoStartInstance(false); setDeployTo(undefined); }; const deployVersion = async (engine: Engine) => { + if (!env.PROCEED_PUBLIC_PROCESS_AUTOMATION_ACTIVE) return; + + if (!isExecutable) { + message.error('Deploying the version is not possible since it is not set to executable.'); + return; + } + await wrapServerCall({ fn: async () => await deployProcess( - process.id, + processId, versionToDeploy === 'latest' ? '' : versionToDeploy, environment.spaceId, 'dynamic', @@ -139,9 +161,10 @@ const VersionAndDeploy: React.FC = ({ process }) => { ), onSuccess: async () => { message.success('Process Deployed'); - let path = `/executions/${process.id}`; - if (versionToDeploy === 'latest') { - const bpmn = await modeler?.getXML(); + let path = `/executions/${processId}`; + if (autoStartInstance) { + setAutoStartInstance(false); + const bpmn = await getBpmn(versionToDeploy === 'latest' ? undefined : versionToDeploy); if (bpmn) { const [startFormId] = Object.values(await getStartFormFileNameMapping(bpmn)); @@ -163,17 +186,90 @@ const VersionAndDeploy: React.FC = ({ process }) => { return; } } - - const instanceId = await startInstanceOnMachine(process.id, '_latest', engine); + const instanceId = await startInstanceOnMachine( + processId, + versionToDeploy === 'latest' ? '_latest' : versionToDeploy, + engine, + ); path += `?instance=${instanceId}`; } router.push(spaceURL(environment, path)); }, - onError: 'Failed to deploy the process', + onError: () => { + message.error('Failed to deploy the process'); + setVersionToDeploy(''); + }, }); - setVersionToDeploy(''); }; + return { + createProcessVersion, + deployVersion, + startInstance, + startForm, + cancelStartForm, + versionToDeploy, + setVersionToDeploy, + cancelDeploy, + autoStartInstance: () => setAutoStartInstance(true), + }; +} + +const VersionAndDeploy: React.FC = ({ process }) => { + const processId = process.id; + const router = useRouter(); + const query = useSearchParams(); + const environment = useEnvironment(); + + const { isListView, processContextPath } = useProcessView(); + const showMobileView = useMobileModeler(); + + const env = use(EnvVarsContext); + + const modeler = useModelerStateStore((state) => state.modeler); + const isExecutable = useModelerStateStore((state) => state.isExecutable); + + const { variables } = useProcessVariables(); + + const selectedVersionId = query.get('version'); + + const selectedVersion = + process.versions.find((version) => version.id === (selectedVersionId ?? '-1')) ?? + LATEST_VERSION; + + const filterOption: SelectProps['filterOption'] = (input, option) => + ((option?.label as string) ?? '').toLowerCase().includes(input.toLowerCase()); + + const { + createProcessVersion, + deployVersion, + startInstance, + startForm, + cancelStartForm, + versionToDeploy, + setVersionToDeploy, + cancelDeploy, + autoStartInstance, + } = useVersionAndDeploy( + process.id, + isExecutable, + async () => await modeler?.getXML(), + async () => { + // Ensure latest BPMN on server. + const xml = (await modeler?.getXML()) as string; + if (isUserErrorResponse(await updateProcess(processId, environment.spaceId, xml))) + throw new Error(); + }, + async () => { + // reimport the new version since the backend has added versionBasedOn information that would + // be overwritten by following changes + const newBpmn = await getProcessBPMN(processId, environment.spaceId); + if (newBpmn && typeof newBpmn === 'string') { + await modeler?.loadBPMN(newBpmn); + } + }, + ); + return ( <> setSelectedVersionId(id)} + optionRender={({ data: { versionName, versionDescription } }) => ( + + {versionName} + + )} + /> + {selectedVersion && ( + + + {!selectedVersion.executable && ( + + )} + + + )} + + )} + + ); +}; + export default VersionAndDeploy; diff --git a/src/management-system-v2/components/processes/index.tsx b/src/management-system-v2/components/processes/index.tsx index c43c7ad65..f45f97bfa 100644 --- a/src/management-system-v2/components/processes/index.tsx +++ b/src/management-system-v2/components/processes/index.tsx @@ -25,7 +25,7 @@ import { MenuProps, Typography, } from 'antd'; -import { InfoCircleOutlined } from '@ant-design/icons'; +import { InfoCircleOutlined, PlayCircleOutlined } from '@ant-design/icons'; import { ComponentProps, use, useMemo, useRef, useState, useTransition } from 'react'; import { @@ -57,7 +57,7 @@ import { import ProcessModal from '@/components/process-modal'; import ConfirmationButton from '@/components/confirmation-button'; import ProcessImportButton from '@/components/process-import'; -import { Process, ProcessMetadata } from '@/lib/data/process-schema'; +import { ProcessMetadata } from '@/lib/data/process-schema'; import MetaDataContent from '@/components/process-info-card-content'; import { useEnvironment } from '@/components/auth-can'; import { Folder } from '@/lib/data/folder-schema'; @@ -89,11 +89,13 @@ import { ContextActions, RowActions } from './types'; import { canDoActionOnResource } from './helpers'; import { useInitialisePotentialOwnerStore } from '@/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/use-potentialOwner-store'; import { useSession } from 'next-auth/react'; -import { useVersionAndDeploy } from '@/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/version-and-deploy-section'; +import { + VersionSelectionModal, + useVersionAndDeploy, +} from '@/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/version-and-deploy-section'; import { EnvVarsContext } from '../env-vars-context'; import EngineSelection from '../engine-selection'; import StartFormModal from '@/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/start-form-modal'; -import { is } from 'bpmn-js/lib/util/ModelUtil'; // TODO: improve ordering export type ProcessActions = { @@ -174,6 +176,7 @@ const Processes = ({ const [exportModalTab, setExportModalTab] = useState<'bpmn' | 'share-public-link' | undefined>( undefined, ); + const [showVersionSelectionModal, setShowVersionSelectionModal] = useState(false); const [openCopyModal, setOpenCopyModal] = useState(false); const [openEditModal, setOpenEditModal] = useState(false); const [openDeleteModal, setOpenDeleteModal] = useState(false); @@ -497,8 +500,10 @@ const Processes = ({ startForm, cancelStartForm, versionToDeploy, + setVersionToDeploy, cancelDeploy, startInstance, + autoStartInstance, } = useVersionAndDeploy(selectedProcessId, isExecutable, async (versionId) => { return await wrapServerCall({ fn: () => getProcessBPMN(selectedProcessId!, space.spaceId, versionId), @@ -638,6 +643,17 @@ const Processes = ({ > )} + { + + , + // we can deploy when the only thing preventing from submitting is that the version information has not changed and when deploying is enabled + const deployable = (versionable || (completelyUnchanged && errors.length === 1)) && isDeployable; + + const footerButtons = [ + , + + + , + ]; + + if (isDeployable) { + footerButtons.push( + , - env.PROCEED_PUBLIC_PROCESS_AUTOMATION_ACTIVE && isExecutable && ( - - ), - ]} + {completelyUnchanged ? 'Deploy' : 'Release and Deploy'} + + , + ); + } + + return ( + {!!completelyUnchanged && ( )} -
- - setName(e.target.value)} - /> - - - setDescription(e.target.value)} - /> - + +
); @@ -193,13 +190,23 @@ type VersionCreationButtonProps = ButtonProps & { values?: { versionName: string; versionDescription: string }, deploy?: boolean | string, ) => any; - isExecutable?: boolean; + isDeployable?: boolean; }; -const VersionCreationButton = forwardRef( - ({ processId, createVersion, isExecutable, ...props }, ref) => { +const VersionAndDeployButton = forwardRef( + ({ processId, createVersion, isDeployable, ...props }, ref) => { const [isVersionModalOpen, setIsVersionModalOpen] = useState(false); const [loading, setLoading] = useState(false); + const handleSubmit: VersionAndDeployModalProps['close'] = async (values, deploy) => { + if (values || deploy) { + setLoading(true); + await createVersion(values, deploy); + setLoading(false); + } + + setIsVersionModalOpen(false); + }; + return ( <> - { - if (values || deploy) { - const createResult = createVersion(values, deploy); - if (createResult instanceof Promise) { - setLoading(true); - await createResult; - setLoading(false); - } - } - - setIsVersionModalOpen(false); - }} show={isVersionModalOpen} loading={loading} - isExecutable={isExecutable} - > + close={handleSubmit} + isDeployable={isDeployable} + /> ); }, ); -VersionCreationButton.displayName = 'VersionCreationButton'; +VersionAndDeployButton.displayName = 'VersionCreationButton'; -export default VersionCreationButton; +export default VersionAndDeployButton; diff --git a/src/management-system-v2/lib/data/processes.tsx b/src/management-system-v2/lib/data/processes.tsx index df3894cb2..eccf6a708 100644 --- a/src/management-system-v2/lib/data/processes.tsx +++ b/src/management-system-v2/lib/data/processes.tsx @@ -558,16 +558,23 @@ export const copyProcesses = async ( return copiedProcesses; }; -// TODO: fix: this function doesn't work yet -export const processHasChangesSinceLastVersion = async (processId: string, spaceId: string) => { +/** + * Function that checks if a process' latest version is unchanged from the version it is based on + * + * @returns if unchanged the version id of the based on version is returned otherwise undefined is + * returned + **/ +export const processUnchangedFromBasedOnVersion = async (processId: string, spaceId: string) => { const error = await checkValidity(processId, 'view', spaceId); if (error) return error; const process = await _getProcess(processId, true); if (!process) return userError('Process not found', UserErrorType.NotFoundError); + if (!process.versions.length) return; + const bpmnObj = await toBpmnObject(process.bpmn!); - const { versionBasedOn, versionCreatedOn } = await getDefinitionsVersionInformation(bpmnObj); + const { versionBasedOn } = await getDefinitionsVersionInformation(bpmnObj); const versionedBpmn = await toBpmnXml(bpmnObj); @@ -578,7 +585,27 @@ export const processHasChangesSinceLastVersion = async (processId: string, space : undefined; const versionsAreEqual = basedOnBPMN && (await areVersionsEqual(versionedBpmn, basedOnBPMN)); - return !versionsAreEqual; + + if (versionsAreEqual) return versionBasedOn; +}; + +export const getUnchangedVersionInfo = async (processId: string, spaceId: string) => { + // this also checks if the user is allowed to use this function + const unchangedVersion = await processUnchangedFromBasedOnVersion(processId, spaceId); + + if (isUserErrorResponse(unchangedVersion)) return unchangedVersion; + + if (!unchangedVersion) return; + + const process = await _getProcess(processId, false); + if (!process?.versions.length) return; + + const version = process.versions.find((v) => v.id === unchangedVersion); + + // this should not happen when the data in the MS is correct + if (!version) return; + + return version; }; export const createVersion = async ( diff --git a/src/management-system-v2/lib/use-can-submit-form.tsx b/src/management-system-v2/lib/use-can-submit-form.tsx new file mode 100644 index 000000000..405874c5a --- /dev/null +++ b/src/management-system-v2/lib/use-can-submit-form.tsx @@ -0,0 +1,39 @@ +import { Form, FormInstance } from 'antd'; +import { useCallback, useEffect, useState } from 'react'; + +export class ValidationError extends Error { } + +function useCanSubmit( + form: FormInstance, + externalValidation?: (values: any) => Promise | void, +) { + const [submittable, setSubmittable] = useState(false); + const [errors, setErrors] = useState>([]); + + const values = Form.useWatch([], form); + + const validate = useCallback(async () => { + try { + const values = await form.validateFields({ validateOnly: true }); + + await externalValidation?.(values); + + setSubmittable(true); + setErrors([]); + } catch (err) { + setSubmittable(false); + + if (err instanceof ValidationError) + setErrors([{ name: [], errors: [err.message], warnings: [] }]); + else setErrors(form.getFieldsError().filter((entry) => entry.errors.length)); + } + }, [form, values, externalValidation]); + + useEffect(() => { + validate(); + }, [validate]); + + return { submittable, values, errors }; +} + +export default useCanSubmit; From c384e8227d5717bc955e38a3e5ac4457ee4703fd Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Tue, 17 Mar 2026 16:30:24 +0100 Subject: [PATCH 12/17] Ran prettier --- .../[mode]/[processId]/version-and-deploy-section.tsx | 7 ++++--- src/management-system-v2/components/processes/index.tsx | 8 ++++---- src/management-system-v2/lib/use-can-submit-form.tsx | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/version-and-deploy-section.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/version-and-deploy-section.tsx index 296c2a934..0d69e7cdd 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/version-and-deploy-section.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/version-and-deploy-section.tsx @@ -103,8 +103,8 @@ export function useVersionAndDeploy( return { canDeploy, handleVersionCreation, - handleDeploy: async () => { }, - handleStartInstance: async () => { }, + handleDeploy: async () => {}, + handleStartInstance: async () => {}, }; } @@ -235,7 +235,8 @@ const VersionAndDeploy: React.FC = ({ process }) => { router.push( spaceURL( environment, - `/processes${processContextPath}/${processId as string}${searchParams.size ? '?' + searchParams.toString() : '' + `/processes${processContextPath}/${processId as string}${ + searchParams.size ? '?' + searchParams.toString() : '' }`, ), ); diff --git a/src/management-system-v2/components/processes/index.tsx b/src/management-system-v2/components/processes/index.tsx index e3600b281..0424a0398 100644 --- a/src/management-system-v2/components/processes/index.tsx +++ b/src/management-system-v2/components/processes/index.tsx @@ -187,7 +187,7 @@ const Processes = ({ const [movingItem, startMovingItemTransition] = useTransition(); const [openCreateProcessModal, setOpenCreateProcessModal] = useState( typeof window !== 'undefined' && - new URLSearchParams(document.location.search).has('createprocess'), + new URLSearchParams(document.location.search).has('createprocess'), ); const [openCreateFolderModal, setOpenCreateFolderModal] = useState(false); const [openVersionModal, setOpenVersionModal] = useState(false); @@ -818,9 +818,9 @@ const Processes = ({ const items = selectedRowKeys.length > 0 ? selectedRowElements.map((element) => ({ - type: element.type, - id: element.id, - })) + type: element.type, + id: element.id, + })) : [{ type: active.type, id: active.id }]; moveItems(items, over.id); diff --git a/src/management-system-v2/lib/use-can-submit-form.tsx b/src/management-system-v2/lib/use-can-submit-form.tsx index 405874c5a..196ef23c7 100644 --- a/src/management-system-v2/lib/use-can-submit-form.tsx +++ b/src/management-system-v2/lib/use-can-submit-form.tsx @@ -1,7 +1,7 @@ import { Form, FormInstance } from 'antd'; import { useCallback, useEffect, useState } from 'react'; -export class ValidationError extends Error { } +export class ValidationError extends Error {} function useCanSubmit( form: FormInstance, From 7507f7abe03b37764d0fc2d45685e0fbf3f71d64 Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Tue, 17 Mar 2026 16:44:53 +0100 Subject: [PATCH 13/17] Removed redundant code --- .../version-and-deploy-section.tsx | 14 +++--- .../components/engine-selection.tsx | 49 ------------------- .../lib/engines/server-actions.ts | 23 +-------- 3 files changed, 7 insertions(+), 79 deletions(-) delete mode 100644 src/management-system-v2/components/engine-selection.tsx diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/version-and-deploy-section.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/version-and-deploy-section.tsx index 0d69e7cdd..f4c485ba2 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/version-and-deploy-section.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/version-and-deploy-section.tsx @@ -17,13 +17,11 @@ import { IoPlayOutline } from 'react-icons/io5'; import { useEnvironment } from '@/components/auth-can'; import { Process } from '@/lib/data/process-schema'; -import { Engine } from '@/lib/engines/machines'; import { spaceURL } from '@/lib/utils'; import { useRouter, useSearchParams } from 'next/navigation'; import { useProcessView } from './process-view-context'; import useMobileModeler from '@/lib/useMobileModeler'; import VersionCreationButton from '@/components/version-creation-button'; -import { automaticDeploymentId, useUniqueEngines } from '@/components/engine-selection'; import { use, useMemo, useState } from 'react'; import { isUserErrorResponse } from '@/lib/user-error'; @@ -49,6 +47,7 @@ import { wrapServerCall } from '@/lib/wrap-server-call'; import { useQuery } from '@tanstack/react-query'; import { asyncMap } from '@/lib/helpers/javascriptHelpers'; import BPMNCanvas from '@/components/bpmn-canvas'; +import useEngines from '@/lib/engines/use-engines'; export const LATEST_VERSION = { id: '-1', name: 'Latest Version', description: '' }; @@ -67,8 +66,8 @@ export function useVersionAndDeploy( const { message } = App.useApp(); const env = use(EnvVarsContext); - const engines = useUniqueEngines(!isExecutable); - const engine = engines?.find((e) => e.id !== automaticDeploymentId) as Engine | undefined; + const { data: engines } = useEngines(environment); + const engine = engines?.[0]; const canDeploy = !!processId && !!env.PROCEED_PUBLIC_PROCESS_AUTOMATION_ACTIVE && !!isExecutable && !!engine; @@ -103,8 +102,8 @@ export function useVersionAndDeploy( return { canDeploy, handleVersionCreation, - handleDeploy: async () => {}, - handleStartInstance: async () => {}, + handleDeploy: async () => { }, + handleStartInstance: async () => { }, }; } @@ -235,8 +234,7 @@ const VersionAndDeploy: React.FC = ({ process }) => { router.push( spaceURL( environment, - `/processes${processContextPath}/${processId as string}${ - searchParams.size ? '?' + searchParams.toString() : '' + `/processes${processContextPath}/${processId as string}${searchParams.size ? '?' + searchParams.toString() : '' }`, ), ); diff --git a/src/management-system-v2/components/engine-selection.tsx b/src/management-system-v2/components/engine-selection.tsx deleted file mode 100644 index ef13430d5..000000000 --- a/src/management-system-v2/components/engine-selection.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { getUniqueEngines } from '@/lib/engines/server-actions'; -import { useQuery } from '@tanstack/react-query'; -import { Alert, Select } from 'antd'; -import { useEnvironment } from './auth-can'; - -export const automaticDeploymentId = '__automatic_engine_selection__'; - -const automaticDeployment = { - name: 'Automatic', - id: automaticDeploymentId, - isAutomaticDeployment: true, -} as const; - -export const useUniqueEngines = (disabled = false) => { - const environment = useEnvironment(); - - const { data } = useQuery({ - queryFn: async () => await getUniqueEngines(environment.spaceId), - queryKey: ['unique-engines', environment.spaceId], - enabled: !disabled, - }); - - if (!data) return; - if (!data.length) return data; - - return [automaticDeployment, ...data]; -}; - -type SelectableEngines = ReturnType; - -export const EngineSelection: React.FC<{ - selectedEngineId?: string; - engines: NonNullable; - onChange: (selectedId: string) => void; -}> = ({ selectedEngineId = automaticDeploymentId, engines, onChange }) => { - if (!engines.length) { - return ; - } - - return ( - - - - - - - ); -}; - const unchangedError = 'No changes from previous version'; type VersionAndDeployModalProps = { @@ -118,6 +86,11 @@ export const VersionAndDeployModal: React.FC = ({ form.resetFields(); }; + useEffect(() => { + form.setFieldValue('versionName', unchangedVersion?.name || ''); + form.setFieldValue('versionDescription', unchangedVersion?.description || ''); + }, [unchangedVersion, form]); + // we can deploy when the only thing preventing from submitting is that the version information has not changed and when deploying is enabled const deployable = (versionable || (completelyUnchanged && errors.length === 1)) && isDeployable; @@ -178,7 +151,23 @@ export const VersionAndDeployModal: React.FC = ({ autoComplete="off" layout="vertical" > - + + + + + + ); diff --git a/src/management-system-v2/lib/data/processes.tsx b/src/management-system-v2/lib/data/processes.tsx index eccf6a708..2def86453 100644 --- a/src/management-system-v2/lib/data/processes.tsx +++ b/src/management-system-v2/lib/data/processes.tsx @@ -561,7 +561,7 @@ export const copyProcesses = async ( /** * Function that checks if a process' latest version is unchanged from the version it is based on * - * @returns if unchanged the version id of the based on version is returned otherwise undefined is + * @returns if unchanged, the version id of the based on version is returned, otherwise undefined is * returned **/ export const processUnchangedFromBasedOnVersion = async (processId: string, spaceId: string) => { From 62c677b2720c9a66d14abd358bb801e18cb57fdb Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Tue, 17 Mar 2026 18:09:26 +0100 Subject: [PATCH 15/17] Removed another change --- .../components/version-creation-button.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/management-system-v2/components/version-creation-button.tsx b/src/management-system-v2/components/version-creation-button.tsx index 73812176e..c73f9695c 100644 --- a/src/management-system-v2/components/version-creation-button.tsx +++ b/src/management-system-v2/components/version-creation-button.tsx @@ -144,13 +144,7 @@ export const VersionAndDeployModal: React.FC = ({ title="This process has not been changed since the last version." /> )} -
+ Date: Tue, 17 Mar 2026 18:23:28 +0100 Subject: [PATCH 16/17] Small improvements --- .../version-and-deploy-section.tsx | 9 +- .../components/processes/index.tsx | 2 +- .../components/version-creation-button.tsx | 108 +++++++++--------- 3 files changed, 58 insertions(+), 61 deletions(-) diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/version-and-deploy-section.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/version-and-deploy-section.tsx index f4c485ba2..d36eb90f4 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/version-and-deploy-section.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/version-and-deploy-section.tsx @@ -102,8 +102,8 @@ export function useVersionAndDeploy( return { canDeploy, handleVersionCreation, - handleDeploy: async () => { }, - handleStartInstance: async () => { }, + handleDeploy: async () => {}, + handleStartInstance: async () => {}, }; } @@ -234,7 +234,8 @@ const VersionAndDeploy: React.FC = ({ process }) => { router.push( spaceURL( environment, - `/processes${processContextPath}/${processId as string}${searchParams.size ? '?' + searchParams.toString() : '' + `/processes${processContextPath}/${processId as string}${ + searchParams.size ? '?' + searchParams.toString() : '' }`, ), ); @@ -251,7 +252,7 @@ const VersionAndDeploy: React.FC = ({ process }) => { } - createVersion={async (values, deploy) => { + close={async (values, deploy) => { await beforeVersioning(); await handleVersionCreation(processId, values, deploy); }} diff --git a/src/management-system-v2/components/processes/index.tsx b/src/management-system-v2/components/processes/index.tsx index 0424a0398..2c7a1cd08 100644 --- a/src/management-system-v2/components/processes/index.tsx +++ b/src/management-system-v2/components/processes/index.tsx @@ -621,7 +621,7 @@ const Processes = ({ processId={selectedRowElements[0].id} type="text" icon={} - createVersion={async (values, deploy) => { + close={async (values, deploy) => { await handleVersionCreation( selectedRowElements[0].id, values, diff --git a/src/management-system-v2/components/version-creation-button.tsx b/src/management-system-v2/components/version-creation-button.tsx index c73f9695c..f806bd2d2 100644 --- a/src/management-system-v2/components/version-creation-button.tsx +++ b/src/management-system-v2/components/version-creation-button.tsx @@ -9,12 +9,14 @@ import { getUnchangedVersionInfo } from '@/lib/data/processes'; import { isUserErrorResponse } from '@/lib/user-error'; import useCanSubmit, { ValidationError } from '@/lib/use-can-submit-form'; -type UnchangedVersion = { id: string; name: string; description: string }; - function useVersioningModal(processId: string, show: boolean, form: FormInstance) { const { spaceId } = useEnvironment(); - const [unchangedVersion, setUnchangedVersion] = useState(); + const [unchangedVersion, setUnchangedVersion] = useState<{ + id: string; + name: string; + description: string; + }>(); const validator = useCallback(() => { const name = form.getFieldValue('versionName'); @@ -51,16 +53,20 @@ function useVersioningModal(processId: string, show: boolean, form: FormInstance const unchangedError = 'No changes from previous version'; -type VersionAndDeployModalProps = { +type VersionCreationButtonProps = ButtonProps & { processId: string; - show: boolean; close: ( values?: { versionName: string; versionDescription: string }, deploy?: boolean | string, - ) => void; - loading?: boolean; + ) => Promise | void; isDeployable?: boolean; }; + +type VersionAndDeployModalProps = VersionCreationButtonProps & { + show: boolean; + loading?: boolean; +}; + export const VersionAndDeployModal: React.FC = ({ processId, show, @@ -82,10 +88,13 @@ export const VersionAndDeployModal: React.FC = ({ ); const handleClose = async () => { - if (!loading) close(); - form.resetFields(); + if (!loading) { + close(); + form.resetFields(); + } }; + // reset fields when the initial info changes useEffect(() => { form.setFieldValue('versionName', unchangedVersion?.name || ''); form.setFieldValue('versionDescription', unchangedVersion?.description || ''); @@ -94,48 +103,43 @@ export const VersionAndDeployModal: React.FC = ({ // we can deploy when the only thing preventing from submitting is that the version information has not changed and when deploying is enabled const deployable = (versionable || (completelyUnchanged && errors.length === 1)) && isDeployable; - const footerButtons = [ - , - - - , - ]; - - if (isDeployable) { - footerButtons.push( - - - , - ); - } - return ( + Cancel + , + + + , + !!isDeployable && ( + + + + ), + ]} > {!!completelyUnchanged && ( = ({ ); }; -type VersionCreationButtonProps = ButtonProps & { - processId: string; - createVersion: ( - values?: { versionName: string; versionDescription: string }, - deploy?: boolean | string, - ) => any; - isDeployable?: boolean; -}; const VersionAndDeployButton = forwardRef( - ({ processId, createVersion, isDeployable, ...props }, ref) => { + ({ processId, close, isDeployable, ...props }, ref) => { const [isVersionModalOpen, setIsVersionModalOpen] = useState(false); const [loading, setLoading] = useState(false); const handleSubmit: VersionAndDeployModalProps['close'] = async (values, deploy) => { if (values || deploy) { setLoading(true); - await createVersion(values, deploy); + await close(values, deploy); setLoading(false); } From 7e813d190cf410867ba5cdd62544792e644260b5 Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Tue, 17 Mar 2026 18:43:54 +0100 Subject: [PATCH 17/17] Undid some more unnecessary changes --- .../[processId]/process-deployment-view.tsx | 2 +- .../[mode]/[processId]/modeler-toolbar.tsx | 43 ++++++++++- .../version-and-deploy-section.tsx | 77 +++++-------------- 3 files changed, 61 insertions(+), 61 deletions(-) diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/process-deployment-view.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/process-deployment-view.tsx index 985de1b82..714f9dfa0 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/process-deployment-view.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/process-deployment-view.tsx @@ -116,7 +116,7 @@ export default function ProcessDeploymentView({ }; }, [deploymentInfo, selectedVersionId, selectedInstanceId]); - const { variableDefinitions, variables } = useInstanceVariables({ + const { variableDefinitions } = useInstanceVariables({ process: deploymentInfo, version: currentVersion, }); diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/modeler-toolbar.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/modeler-toolbar.tsx index 1ff6a87cb..2fe943230 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/modeler-toolbar.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/modeler-toolbar.tsx @@ -1,6 +1,6 @@ import { ComponentProps, use, useEffect, useState } from 'react'; import { is as bpmnIs } from 'bpmn-js/lib/util/ModelUtil'; -import { Tooltip, Button, Space, Divider, Select } from 'antd'; +import { Tooltip, Button, Space, Divider, Select, SelectProps } from 'antd'; import { Toolbar, ToolbarGroup } from '@/components/toolbar'; import styles from './modeler-toolbar.module.scss'; import Icon, { @@ -16,7 +16,7 @@ import { PiDownloadSimple } from 'react-icons/pi'; import { SvgGantt, SvgXML } from '@/components/svg'; import PropertiesPanel from './properties-panel'; import useModelerStateStore from './use-modeler-state-store'; -import { useSearchParams } from 'next/navigation'; +import { useRouter, useSearchParams } from 'next/navigation'; import useMobileModeler from '@/lib/useMobileModeler'; import { updateProcess } from '@/lib/data/processes'; import { Root } from 'bpmn-js/lib/model/Types'; @@ -40,6 +40,7 @@ import { ScriptTaskEditorEnvironment } from './script-task-editor/script-task-ed import { Folder } from '@/lib/data/folder-schema'; import VersionAndDeploy, { LATEST_VERSION } from './version-and-deploy-section'; +import { spaceURL } from '@/lib/utils'; type ModelerToolbarProps = { process: Process; @@ -60,6 +61,8 @@ const ModelerToolbar = ({ const environment = useEnvironment(); const env = use(EnvVarsContext); + const router = useRouter(); + const [showUserTaskEditor, setShowUserTaskEditor] = useState(false); const [showPropertiesPanel, setShowPropertiesPanel] = useState(false); @@ -106,7 +109,7 @@ const ModelerToolbar = ({ const query = useSearchParams(); const subprocessId = query.get('subprocess'); - const { isListView } = useProcessView(); + const { isListView, processContextPath } = useProcessView(); const modeler = useModelerStateStore((state) => state.modeler); const isExecutable = useModelerStateStore((state) => state.isExecutable); @@ -211,6 +214,9 @@ const ModelerToolbar = ({ } }; + const filterOption: SelectProps['filterOption'] = (input, option) => + ((option?.label as string) ?? '').toLowerCase().includes(input.toLowerCase()); + const selectedVersion = process.versions.find((version) => version.id === (selectedVersionId ?? '-1')) ?? LATEST_VERSION; @@ -240,6 +246,37 @@ const ModelerToolbar = ({ }} > + { - // change the version info in the query but keep other info (e.g. the currently open subprocess) - const searchParams = new URLSearchParams(query); - if (!value || value === '-1') searchParams.delete('version'); - else searchParams.set(`version`, `${value}`); - router.push( - spaceURL( - environment, - `/processes${processContextPath}/${processId as string}${ - searchParams.size ? '?' + searchParams.toString() : '' - }`, - ), - ); - }} - options={(isListView ? [] : [LATEST_VERSION]) - .concat(process.versions ?? []) - .map(({ id, name }) => ({ - value: id, - label: name, - }))} - /> {!showMobileView && LATEST_VERSION.id === selectedVersion.id && ( } close={async (values, deploy) => { - await beforeVersioning(); - await handleVersionCreation(processId, values, deploy); + // Ensure latest BPMN on server. + const xml = (await modeler?.getXML()) as string; + if (isUserErrorResponse(await updateProcess(processId, environment.spaceId, xml))) + throw new Error(); + + try { + await handleVersionCreation(processId, values, deploy); + } finally { + // reimport the new version since the backend has added versionBasedOn information that would + // be overwritten by following changes + const newBpmn = await getProcessBPMN(processId, environment.spaceId); + if (newBpmn && typeof newBpmn === 'string') { + await modeler?.loadBPMN(newBpmn); + } + router.refresh(); + } }} disabled={isListView} isDeployable={canDeploy}