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 86fea36dd..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 @@ -30,9 +30,8 @@ import { getLatestDeployment, getVersionInstances, getYoungestInstance } from '. import useColors from './use-colors'; import useTokens from './use-tokens'; import { DeployedProcessInfo } from '@/lib/engines/deployment'; -import StartFormModal from './start-form-modal'; +import StartFormModal from '@/components/start-form-modal'; import useInstanceVariables from './use-instance-variables'; -import { inlineScript, inlineUserTaskData } from '@proceed/user-task-helper'; export default function ProcessDeploymentView({ processId, @@ -117,7 +116,7 @@ export default function ProcessDeploymentView({ }; }, [deploymentInfo, selectedVersionId, selectedInstanceId]); - const { variableDefinitions, variables } = useInstanceVariables({ + const { variableDefinitions } = useInstanceVariables({ process: deploymentInfo, version: currentVersion, }); @@ -194,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); @@ -364,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 deleted file mode 100644 index 00b0418dd..000000000 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/start-form-modal.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Modal } from 'antd'; -import { UserTaskForm } from '../../../tasklist/user-task-view'; - -type StartFormModalProps = { - html?: string; - onSubmit: (variables: { [key: string]: any }) => Promise; - onCancel: () => void; -}; - -const StartFormModal: React.FC = ({ html, onSubmit, onCancel }) => { - return ( - - - - ); -}; - -export default StartFormModal; diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/deployments-view.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/deployments-view.tsx index e76e55c05..866d2d037 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/deployments-view.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/deployments-view.tsx @@ -9,13 +9,13 @@ import DeploymentsList from './deployments-list'; import { Folder } from '@/lib/data/folder-schema'; import { Process, ProcessMetadata } from '@/lib/data/process-schema'; import { useEnvironment } from '@/components/auth-can'; -import { processHasChangesSinceLastVersion } from '@/lib/data/processes'; +import { processUnchangedFromBasedOnVersion } from '@/lib/data/processes'; import type { DeployedProcessInfo } from '@/lib/engines/deployment'; import { useRouter } from 'next/navigation'; import { deployProcess as serverDeployProcess } from '@/lib/engines/server-actions'; import { wrapServerCall } from '@/lib/wrap-server-call'; import { SpaceEngine } from '@/lib/engines/machines'; -import { userError } from '@/lib/user-error'; +import { isUserErrorResponse, userError } from '@/lib/user-error'; import { removeDeployment as serverRemoveDeployment } from '@/lib/engines/server-actions'; import { useQueryClient } from '@tanstack/react-query'; @@ -58,22 +58,29 @@ const DeploymentsView = ({ startCheckingProcessVersion(async () => { wrapServerCall({ fn: async () => { - const processChangedSinceLastVersion = await processHasChangesSinceLastVersion( + const unchangedVersion = await processUnchangedFromBasedOnVersion( process.id, space.spaceId, ); - if (typeof processChangedSinceLastVersion === 'object') - return processChangedSinceLastVersion; + if (isUserErrorResponse(unchangedVersion)) { + return unchangedVersion; + } - let latestVersion = process.versions[0]; - for (const version of process.versions) - if (+version.createdOn > +latestVersion.createdOn) latestVersion = version; + let versionToUse = unchangedVersion; - if (!latestVersion) throw userError('Process has no versions').error; + if (!versionToUse) { + let latestVersion = process.versions[0]; + for (const version of process.versions) + if (+version.createdOn > +latestVersion.createdOn) latestVersion = version; + + versionToUse = latestVersion.id; + } + + if (!versionToUse) throw userError('Process has no versions').error; const res = await serverDeployProcess( process.id, - latestVersion.id, + versionToUse, space.spaceId, 'dynamic', forceEngine, 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 9a9af359d..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,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, Select, SelectProps } 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 { useRouter, 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,8 @@ 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'; +import { spaceURL } from '@/lib/utils'; type ModelerToolbarProps = { process: Process; @@ -60,13 +58,11 @@ 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 router = useRouter(); + const [showUserTaskEditor, setShowUserTaskEditor] = useState(false); const [showPropertiesPanel, setShowPropertiesPanel] = useState(false); @@ -170,41 +166,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); }; @@ -316,15 +277,9 @@ const ModelerToolbar = ({ label: name, }))} /> + {!showMobileView && LATEST_VERSION.id === selectedVersion.id && ( <> - - } - createVersion={createProcessVersion} - disabled={isListView} - > - 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 d423379d9..434f03ef0 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 @@ -58,10 +58,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 @@ -167,7 +163,6 @@ const Modeler = ({ versionName, process, folder, inEditing, ...divProps }: Model }, [process.id]); useEffect(() => { - console.log('modeler changed'); setModeler(modeler.current); setCanUndo(false); @@ -210,7 +205,6 @@ const Modeler = ({ versionName, process, folder, inEditing, ...divProps }: Model const onRootChange = useCallback['onRootChange']>( async (root) => { - console.log('root changed'); setRootElement(root); // When the current root (the visible layer [the main // process/collaboration or some collapsed subprocess]) is changed to a @@ -275,7 +269,6 @@ const Modeler = ({ versionName, process, folder, inEditing, ...divProps }: Model // (unless the subprocess does not exist anymore because the process // changed) setLoaded(true); - console.log('onLoaded'); if (subprocessId && modeler.current) { const canvas = modeler.current.getCanvas(); const subprocessPlane = canvas @@ -289,6 +282,13 @@ 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; + setIsExecutable(executable || false); + } }, [messageApi, subprocessId]); const onShapeRemove = useCallback['onShapeRemove']>((element) => { diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/script-task-editor/script-task-editor.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/script-task-editor/script-task-editor.tsx index 01ac9ee4c..0a5429538 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/script-task-editor/script-task-editor.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/script-task-editor/script-task-editor.tsx @@ -1,14 +1,6 @@ 'use client'; -import { - useCallback, - useEffect, - useImperativeHandle, - useMemo, - useRef, - useState, - forwardRef, -} from 'react'; +import { useEffect, useImperativeHandle, useMemo, useRef, useState, forwardRef } from 'react'; import dynamic from 'next/dynamic'; import { Button, @@ -275,10 +267,15 @@ const ScriptEditor = forwardRef( } onClick={() => setSelectedEditor('blockly')} + disabled={!canEdit} > No-Code Block Editor - 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 new file mode 100644 index 000000000..eafc8ff17 --- /dev/null +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/version-and-deploy-section.tsx @@ -0,0 +1,421 @@ +import { + Alert, + App, + Button, + Card, + Divider, + Modal, + Select, + Skeleton, + Space, + Tooltip, + Typography, +} from 'antd'; +import { PlusOutlined, ExperimentOutlined } from '@ant-design/icons'; +import { IoPlayOutline } from 'react-icons/io5'; + +import { useEnvironment } from '@/components/auth-can'; +import { Process } from '@/lib/data/process-schema'; +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 { use, useMemo, useState } from 'react'; +import { isUserErrorResponse } from '@/lib/user-error'; + +import { + createVersion, + updateProcess, + getProcessBPMN, + getProcessHtmlFormHTML, + getProcess, +} from '@/lib/data/processes'; +import useModelerStateStore from './use-modeler-state-store'; +import { startInstanceOnMachine } from '@/lib/engines/instances'; +import { deployProcess } from '@/lib/engines/server-actions'; +import { EnvVarsContext } from '@/components/env-vars-context'; +import StartFormModal, { StartForm } from '@/components/start-form-modal'; +import { + getElementsByTagName, + getStartFormFileNameMapping, + toBpmnObject, +} from '@proceed/bpmn-helper'; +import useProcessVariables from './use-process-variables'; +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: '' }; + +type VersionAndDeployProps = { + process: Process; +}; + +export function useVersionAndDeploy(processId: string | undefined, isExecutable: boolean) { + const router = useRouter(); + const environment = useEnvironment(); + + const { message } = App.useApp(); + + const env = use(EnvVarsContext); + const { data: engines } = useEngines(environment); + const engine = engines?.[0]; + + const canDeploy = + !!processId && !!env.PROCEED_PUBLIC_PROCESS_AUTOMATION_ACTIVE && !!isExecutable && !!engine; + + const handleVersionCreation = async ( + processId: string, + values?: { + versionName: string; + versionDescription: string; + }, + deploy?: boolean | string, + ) => { + if (values) { + const newVersion = await createVersion( + values.versionName, + values.versionDescription, + processId, + environment.spaceId, + ); + + if (isUserErrorResponse(newVersion)) throw new Error(); + + if (deploy && newVersion) deploy = newVersion; + } + + if (typeof deploy === 'string' && canDeploy) handleDeploy(processId, deploy); + }; + + if (!canDeploy) { + return { + canDeploy, + handleVersionCreation, + handleDeploy: async () => {}, + handleStartInstance: async () => {}, + }; + } + + const handleDeploy = async (processId: string, deploy: string, noReroute = false) => { + await wrapServerCall({ + fn: async () => + await deployProcess(processId, deploy, environment.spaceId, 'dynamic', engine), + onSuccess: async () => { + message.success('Process Deployed'); + if (!noReroute) { + let path = `/executions/${processId}`; + router.push(spaceURL(environment, path)); + } + }, + onError: 'Failed to deploy the process', + }); + }; + + const handleStartInstance = async ( + version: string, + variables?: Record, + ) => { + await handleDeploy(processId, version === 'latest' ? '' : version, true); + + const instanceId = await startInstanceOnMachine( + processId, + version === 'latest' ? '_latest' : version, + engine, + variables, + ); + + router.push(spaceURL(environment, `/executions/${processId}?instance=${instanceId}`)); + }; + + return { + handleVersionCreation, + handleDeploy, + handleStartInstance, + canDeploy: !!engine, + }; +} + +const VersionAndDeploy: React.FC = ({ process }) => { + const processId = process.id; + const query = useSearchParams(); + const environment = useEnvironment(); + + const router = useRouter(); + + const { isListView } = useProcessView(); + const showMobileView = useMobileModeler(); + + 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 { handleVersionCreation, handleStartInstance, canDeploy } = useVersionAndDeploy( + process.id, + isExecutable, + ); + + const { message } = App.useApp(); + const [startForm, setStartForm] = useState(''); + + const tryStartInstance = async (version: string) => { + if (!canDeploy) return; + + const rootElement = modeler?.getCurrentRoot(); + + if (!rootElement) return; + + const [startFormId] = Object.values( + await getStartFormFileNameMapping(rootElement?.businessObject), + ); + + if (startFormId) { + let startForm = await getProcessHtmlFormHTML(processId, startFormId, environment.spaceId); + + if (typeof startForm !== 'string') { + message.error('Failed to fetch the start form of the process'); + return; + } + + setStartForm(startForm); + return; + } + + await handleStartInstance(version); + }; + + return ( + <> + {!showMobileView && LATEST_VERSION.id === selectedVersion.id && ( + + } + close={async (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} + /> + + )} + {!showMobileView && canDeploy && isExecutable && ( + <> + {LATEST_VERSION.id === selectedVersion.id ? ( + +