From 738a38ec9ae946b96fa35bb8b6c1e2717bf1841a Mon Sep 17 00:00:00 2001 From: Denis Bykhov Date: Fri, 20 Feb 2026 04:16:18 +0500 Subject: [PATCH] feat: Implement CancelSubProcess action, fix process execution flow. Signed-off-by: Denis Bykhov --- models/process/src/actions.ts | 14 ++++ models/server-process/src/index.ts | 4 ++ .../src/components/CardAttributeEditor.svelte | 6 +- .../components/sections/ContentSection.svelte | 8 ++- plugins/process-assets/lang/cs.json | 3 +- plugins/process-assets/lang/de.json | 3 +- plugins/process-assets/lang/en.json | 3 +- plugins/process-assets/lang/es.json | 3 +- plugins/process-assets/lang/fr.json | 3 +- plugins/process-assets/lang/it.json | 3 +- plugins/process-assets/lang/ja.json | 3 +- plugins/process-assets/lang/pt-br.json | 3 +- plugins/process-assets/lang/pt.json | 3 +- plugins/process-assets/lang/ru.json | 3 +- plugins/process-assets/lang/tr.json | 3 +- plugins/process-assets/lang/zh.json | 3 +- .../src/components/ExecutionMyToDos.svelte | 29 ++++---- .../ProcessesHeaderExtension.svelte | 2 +- .../settings/CancelSubProcessEditor.svelte | 72 +++++++++++++++++++ .../settings/CancelToDoEditor.svelte | 2 +- .../settings/ToDoParamsEditor.svelte | 10 ++- plugins/process-resources/src/index.ts | 2 + plugins/process-resources/src/plugin.ts | 2 + plugins/process/src/index.ts | 1 + .../process-resources/src/functions.ts | 36 +++++++++- server-plugins/process-resources/src/index.ts | 2 + server-plugins/process/src/index.ts | 1 + services/process/src/main.ts | 16 ++--- 28 files changed, 202 insertions(+), 41 deletions(-) create mode 100644 plugins/process-resources/src/components/settings/CancelSubProcessEditor.svelte diff --git a/models/process/src/actions.ts b/models/process/src/actions.ts index e3f8d96da3a..16dbbe6d1ab 100644 --- a/models/process/src/actions.ts +++ b/models/process/src/actions.ts @@ -134,6 +134,20 @@ export function defineMethods (builder: Builder): void { process.method.CancelToDo ) + builder.createDoc( + process.class.Method, + core.space.Model, + { + label: process.string.CancelProcess, + editor: process.component.CancelSubProcessEditor, + presenter: process.component.SubProcessPresenter, + objectClass: process.class.Process, + requiredParams: ['_id'], + createdContext: null + }, + process.method.CancelSubProcess + ) + builder.createDoc( process.class.Method, core.space.Model, diff --git a/models/server-process/src/index.ts b/models/server-process/src/index.ts index 5338f95f62e..5311e13197b 100644 --- a/models/server-process/src/index.ts +++ b/models/server-process/src/index.ts @@ -107,6 +107,10 @@ export function createModel (builder: Builder): void { func: serverProcess.func.RunSubProcess }) + builder.mixin(process.method.CancelSubProcess, process.class.Method, serverProcess.mixin.MethodImpl, { + func: serverProcess.func.CancelSubProcess + }) + builder.mixin(process.method.CreateToDo, process.class.Method, serverProcess.mixin.MethodImpl, { func: serverProcess.func.CreateToDo }) diff --git a/plugins/card-resources/src/components/CardAttributeEditor.svelte b/plugins/card-resources/src/components/CardAttributeEditor.svelte index 7037c17b877..75e87a9eff2 100644 --- a/plugins/card-resources/src/components/CardAttributeEditor.svelte +++ b/plugins/card-resources/src/components/CardAttributeEditor.svelte @@ -19,12 +19,16 @@ import card from '../plugin' import MasterTagAttributes from './MasterTagAttributes.svelte' import TagAttributes from './TagAttributes.svelte' + import { getClient } from '@hcengineering/presentation' export let value: Card export let readonly: boolean = false export let mixins: Array> = [] export let ignoreKeys: string[] + const client = getClient() + const h = client.getHierarchy() + let width: number = 0 let columns = 1 @@ -57,7 +61,7 @@
value.readonlySections?.includes(p))} {value} {ignoreKeys} fourRows={columns === 2} diff --git a/plugins/card-resources/src/components/sections/ContentSection.svelte b/plugins/card-resources/src/components/sections/ContentSection.svelte index 5db069be7e8..8549eda7633 100644 --- a/plugins/card-resources/src/components/sections/ContentSection.svelte +++ b/plugins/card-resources/src/components/sections/ContentSection.svelte @@ -22,6 +22,7 @@ import { CardSectionAction } from '../../types' import { permissionsStore } from '@hcengineering/contact-resources' import { canChangeDoc } from '@hcengineering/view-resources' + import { getClient } from '@hcengineering/presentation' export let readonly: boolean = false export let doc: Card @@ -45,6 +46,9 @@ dispatch('action', { id: 'toc', toc: [] }) }) + const client = getClient() + const h = client.getHierarchy() + $: updatePermissionForbidden = doc && !canChangeDoc(doc?._class, doc?.space, $permissionsStore) @@ -52,7 +56,9 @@
doc.readonlySections?.includes(p))} content={contentDiv} showToc={false} on:loaded diff --git a/plugins/process-assets/lang/cs.json b/plugins/process-assets/lang/cs.json index 15eec12828e..79b38bb09d5 100644 --- a/plugins/process-assets/lang/cs.json +++ b/plugins/process-assets/lang/cs.json @@ -143,7 +143,8 @@ "UnlockSection": "Odemknout sekci", "CancelToDo": "Zrušit akční položku", "Export": "Exportovat", - "Import": "Importovat" + "Import": "Importovat", + "CancelProcess": "Zrušit proces" }, "error": { "MethodNotFound": "Metoda nenalezena: {methodId}", diff --git a/plugins/process-assets/lang/de.json b/plugins/process-assets/lang/de.json index 6812871aa46..010a0f97fd5 100644 --- a/plugins/process-assets/lang/de.json +++ b/plugins/process-assets/lang/de.json @@ -143,7 +143,8 @@ "UnlockSection": "Abschnitt entsperren", "CancelToDo": "Action Item abbrechen", "Export": "Exportieren", - "Import": "Importieren" + "Import": "Importieren", + "CancelProcess": "Prozess abbrechen" }, "error": { "MethodNotFound": "Methode nicht gefunden: {methodId}", diff --git a/plugins/process-assets/lang/en.json b/plugins/process-assets/lang/en.json index 9327ab32ea8..4df0e0f90dd 100644 --- a/plugins/process-assets/lang/en.json +++ b/plugins/process-assets/lang/en.json @@ -148,7 +148,8 @@ "UnlockSection": "Unlock section", "CancelToDo": "Cancel Action item", "Export": "Export", - "Import": "Import" + "Import": "Import", + "CancelProcess": "Cancel process" }, "error": { "MethodNotFound": "Method not found: {methodId}", diff --git a/plugins/process-assets/lang/es.json b/plugins/process-assets/lang/es.json index 8d7e7846225..c28d0cf55e7 100644 --- a/plugins/process-assets/lang/es.json +++ b/plugins/process-assets/lang/es.json @@ -148,7 +148,8 @@ "UnlockSection": "Desbloquear sección", "CancelToDo": "Cancelar acción", "Export": "Exportar", - "Import": "Importar" + "Import": "Importar", + "CancelProcess": "Cancelar proceso" }, "error": { "MethodNotFound": "Método no encontrado: {methodId}", diff --git a/plugins/process-assets/lang/fr.json b/plugins/process-assets/lang/fr.json index 71abb5ff372..51103def4e7 100644 --- a/plugins/process-assets/lang/fr.json +++ b/plugins/process-assets/lang/fr.json @@ -148,7 +148,8 @@ "UnlockSection": "Déverrouiller la section", "CancelToDo": "Annuler l'action", "Export": "Exporter", - "Import": "Importer" + "Import": "Importer", + "CancelProcess": "Annuler le processus" }, "error": { "MethodNotFound": "Méthode introuvable : {methodId}", diff --git a/plugins/process-assets/lang/it.json b/plugins/process-assets/lang/it.json index f8ab40ce429..627a7d83705 100644 --- a/plugins/process-assets/lang/it.json +++ b/plugins/process-assets/lang/it.json @@ -148,7 +148,8 @@ "UnlockSection": "Sblocca sezione", "CancelToDo": "Annulla azione", "Export": "Esporta", - "Import": "Importa" + "Import": "Importa", + "CancelProcess": "Annulla processo" }, "error": { "MethodNotFound": "Metodo non trovato: {methodId}", diff --git a/plugins/process-assets/lang/ja.json b/plugins/process-assets/lang/ja.json index 258fdb2fb70..5656ab25923 100644 --- a/plugins/process-assets/lang/ja.json +++ b/plugins/process-assets/lang/ja.json @@ -147,7 +147,8 @@ "UnlockSection": "セクションのロックを解除", "CancelToDo": "アクションをキャンセル", "Export": "エクスポート", - "Import": "インポート" + "Import": "インポート", + "CancelProcess": "プロセスをキャンセル" }, "error": { "MethodNotFound": "メソッドが見つかりません: {methodId}", diff --git a/plugins/process-assets/lang/pt-br.json b/plugins/process-assets/lang/pt-br.json index 2cb6b65eb85..b98f0f08742 100644 --- a/plugins/process-assets/lang/pt-br.json +++ b/plugins/process-assets/lang/pt-br.json @@ -136,7 +136,8 @@ "UnlockSection": "Desbloquear seção", "CancelToDo": "Cancelar ação", "Export": "Exportar", - "Import": "Importar" + "Import": "Importar", + "CancelProcess": "Cancelar processo" }, "error": { "MethodNotFound": "Método não encontrado: {methodId}", diff --git a/plugins/process-assets/lang/pt.json b/plugins/process-assets/lang/pt.json index 9ea9da61fcf..780cbdc467e 100644 --- a/plugins/process-assets/lang/pt.json +++ b/plugins/process-assets/lang/pt.json @@ -148,7 +148,8 @@ "UnlockSection": "Desbloquear seção", "CancelToDo": "Cancelar ação", "Export": "Exportar", - "Import": "Importar" + "Import": "Importar", + "CancelProcess": "Cancelar processo" }, "error": { "MethodNotFound": "Método não encontrado: {methodId}", diff --git a/plugins/process-assets/lang/ru.json b/plugins/process-assets/lang/ru.json index ddf0c2892e8..a53bfe22993 100644 --- a/plugins/process-assets/lang/ru.json +++ b/plugins/process-assets/lang/ru.json @@ -148,7 +148,8 @@ "UnlockSection": "Разблокировать секцию", "CancelToDo": "Отменить задачу", "Export": "Экспорт", - "Import": "Импорт" + "Import": "Импорт", + "CancelProcess": "Отменить процесс" }, "error": { "MethodNotFound": "Метод не найден: {methodId}", diff --git a/plugins/process-assets/lang/tr.json b/plugins/process-assets/lang/tr.json index 04efd6cd656..7083ebddfca 100644 --- a/plugins/process-assets/lang/tr.json +++ b/plugins/process-assets/lang/tr.json @@ -145,7 +145,8 @@ "UnlockSection": "Bölümün kilidini aç", "CancelToDo": "Eylem öğesini iptal et", "Export": "Dışa aktar", - "Import": "İçe aktar" + "Import": "İçe aktar", + "CancelProcess": "Süreci iptal et" }, "error": { "MethodNotFound": "Metod bulunamadı: {methodId}", diff --git a/plugins/process-assets/lang/zh.json b/plugins/process-assets/lang/zh.json index b909465ca99..4d395998fb9 100644 --- a/plugins/process-assets/lang/zh.json +++ b/plugins/process-assets/lang/zh.json @@ -148,7 +148,8 @@ "UnlockSection": "解锁部分", "CancelToDo": "取消任务", "Export": "导出", - "Import": "导入" + "Import": "导入", + "CancelProcess": "取消过程" }, "error": { "MethodNotFound": "找不到方法:{methodId}", diff --git a/plugins/process-resources/src/components/ExecutionMyToDos.svelte b/plugins/process-resources/src/components/ExecutionMyToDos.svelte index fd13d32f198..67b864f6b95 100644 --- a/plugins/process-resources/src/components/ExecutionMyToDos.svelte +++ b/plugins/process-resources/src/components/ExecutionMyToDos.svelte @@ -16,7 +16,7 @@ import { getCurrentEmployee } from '@hcengineering/contact' import { getEmbeddedLabel } from '@hcengineering/platform' import { createQuery, getClient } from '@hcengineering/presentation' - import { ApproveRequest, Execution, ProcessToDo } from '@hcengineering/process' + import { ApproveRequest, Execution, ExecutionStatus, ProcessToDo } from '@hcengineering/process' import { Button } from '@hcengineering/ui' import plugin from '../plugin' import ApproveRequestButtons from './ApproveRequestButtons.svelte' @@ -30,17 +30,22 @@ const emp = getCurrentEmployee() const query = createQuery() - query.query( - plugin.class.ProcessToDo, - { - execution: value._id, - user: emp, - doneOn: null - }, - (res) => { - todos = res - } - ) + $: if (value.status === ExecutionStatus.Active) { + query.query( + plugin.class.ProcessToDo, + { + execution: value._id, + user: emp, + doneOn: null + }, + (res) => { + todos = res + } + ) + } else { + query.unsubscribe() + todos = [] + } async function checkTodo (todo: ProcessToDo) { await client.update(todo, { diff --git a/plugins/process-resources/src/components/ProcessesHeaderExtension.svelte b/plugins/process-resources/src/components/ProcessesHeaderExtension.svelte index fe612ee4764..e43273da296 100644 --- a/plugins/process-resources/src/components/ProcessesHeaderExtension.svelte +++ b/plugins/process-resources/src/components/ProcessesHeaderExtension.svelte @@ -44,7 +44,7 @@ process.class.Execution, { card: card._id, - status: { $ne: ExecutionStatus.Cancelled } + status: ExecutionStatus.Active }, (res) => { docs = res diff --git a/plugins/process-resources/src/components/settings/CancelSubProcessEditor.svelte b/plugins/process-resources/src/components/settings/CancelSubProcessEditor.svelte new file mode 100644 index 00000000000..efcfab8641d --- /dev/null +++ b/plugins/process-resources/src/components/settings/CancelSubProcessEditor.svelte @@ -0,0 +1,72 @@ + + + + +
+
diff --git a/plugins/process-resources/src/components/settings/CancelToDoEditor.svelte b/plugins/process-resources/src/components/settings/CancelToDoEditor.svelte index ae2a0412d8d..afb5ff73bc1 100644 --- a/plugins/process-resources/src/components/settings/CancelToDoEditor.svelte +++ b/plugins/process-resources/src/components/settings/CancelToDoEditor.svelte @@ -41,5 +41,5 @@
diff --git a/plugins/process-resources/src/components/settings/ToDoParamsEditor.svelte b/plugins/process-resources/src/components/settings/ToDoParamsEditor.svelte index b6d6b27df14..b865e047ee0 100644 --- a/plugins/process-resources/src/components/settings/ToDoParamsEditor.svelte +++ b/plugins/process-resources/src/components/settings/ToDoParamsEditor.svelte @@ -31,7 +31,7 @@ function change (e: CustomEvent): void { if (readonly || e.detail == null) return params._id = e.detail - params.result = undefined + params.result = {} dispatch('change', { params }) } @@ -46,6 +46,12 @@
diff --git a/plugins/process-resources/src/index.ts b/plugins/process-resources/src/index.ts index 3c1383f4d27..244fc9937ba 100644 --- a/plugins/process-resources/src/index.ts +++ b/plugins/process-resources/src/index.ts @@ -37,6 +37,7 @@ import RequestsExtension from './components/RequestsExtension.svelte' import RunProcessCardPopup from './components/RunProcessCardPopup.svelte' import RunProcessPopup from './components/RunProcessPopup.svelte' import ActionsPresenter from './components/settings/ActionsPresenter.svelte' +import CancelSubProcessEditor from './components/settings/CancelSubProcessEditor.svelte' import CancelToDoEditor from './components/settings/CancelToDoEditor.svelte' import FunctionSubmenu from './components/settings/FunctionSubmenu.svelte' import ProcessEditor from './components/settings/ProcessEditor.svelte' @@ -173,6 +174,7 @@ export default async (): Promise => ({ LockSectionEditor, UnLockSectionPresenter, CancelToDoEditor, + CancelSubProcessEditor, ToDoValuePresenter }, criteriaEditor: { diff --git a/plugins/process-resources/src/plugin.ts b/plugins/process-resources/src/plugin.ts index 7a34bfed552..388154d3143 100644 --- a/plugins/process-resources/src/plugin.ts +++ b/plugins/process-resources/src/plugin.ts @@ -30,6 +30,7 @@ export default mergeIds(processId, process, { ProcessEditor: '' as AnyComponent, ProcessesSettingSection: '' as AnyComponent, SubProcessEditor: '' as AnyComponent, + CancelSubProcessEditor: '' as AnyComponent, ApproveRequestEditor: '' as AnyComponent, ApproveRequestPresenter: '' as AnyComponent, ApproveRequestTriggerEditor: '' as AnyComponent, @@ -121,6 +122,7 @@ export default mergeIds(processId, process, { DeleteState: '' as IntlString, DeleteStateConfirm: '' as IntlString, RunProcess: '' as IntlString, + CancelProcess: '' as IntlString, Processes: '' as IntlString, Untitled: '' as IntlString, States: '' as IntlString, diff --git a/plugins/process/src/index.ts b/plugins/process/src/index.ts index 91fbdf99e7e..d43c1a474ab 100644 --- a/plugins/process/src/index.ts +++ b/plugins/process/src/index.ts @@ -237,6 +237,7 @@ export default plugin(processId, { }, method: { RunSubProcess: '' as Ref>, + CancelSubProcess: '' as Ref>, CreateAction: '' as Ref>, CancellAction: '' as Ref>, CreateToDo: '' as Ref>, diff --git a/server-plugins/process-resources/src/functions.ts b/server-plugins/process-resources/src/functions.ts index 2bbe6cb70d6..a9bd804fae4 100644 --- a/server-plugins/process-resources/src/functions.ts +++ b/server-plugins/process-resources/src/functions.ts @@ -126,7 +126,7 @@ export async function CheckSubProcessMatch ( ) if (predicate === '$all') { return res.length === subExecutions.length - } else if (predicate === '$any') { + } else if (predicate === '$in') { return res.length > 0 } else if (predicate === '$nin') { return res.length === 0 @@ -337,6 +337,38 @@ export async function AddTag ( } } +export async function CancelSubProcess ( + params: MethodParams, + execution: Execution, + control: ProcessControl +): Promise { + const processId = params._id as Ref + if (processId === undefined) throw processError(process.error.RequiredParamsNotProvided, { params: '_id' }) + const target = control.client.getModel().findObject(processId) + if (target === undefined) throw processError(process.error.ObjectNotFound, { _id: processId }) + const res: Tx[] = [] + const rollback: Tx[] = [] + const executions = await control.client.findAll(process.class.Execution, { + card: execution.card, + process: processId, + status: ExecutionStatus.Active + }) + for (const exec of executions) { + res.push( + control.client.txFactory.createTxUpdateDoc(process.class.Execution, execution.space, exec._id, { + status: ExecutionStatus.Cancelled + }) + ) + rollback.push( + control.client.txFactory.createTxUpdateDoc(process.class.Execution, execution.space, exec._id, { + status: ExecutionStatus.Active + }) + ) + } + + return { txes: res, rollback, context: null } +} + export async function RunSubProcess ( params: MethodParams, execution: Execution, @@ -611,7 +643,7 @@ export async function CancelToDo ( ): Promise { if (params._id === undefined) throw processError(process.error.RequiredParamsNotProvided, { params: '_id' }) const todo = await control.client.findOne(process.class.ProcessToDo, { _id: params._id as any }) - if (todo === undefined) throw processError(process.error.ObjectNotFound, { _id: params._id }) + if (todo === undefined) return { txes: [], rollback: [], context: null } if (todo.doneOn !== null) throw processError(process.error.ToDoAlreadyCompleted, { _id: params._id }) const res: Tx[] = [control.client.txFactory.createTxRemoveDoc(todo._class, todo.space, todo._id)] const rollback: Tx[] = [ diff --git a/server-plugins/process-resources/src/index.ts b/server-plugins/process-resources/src/index.ts index d18b8498da3..ee45570ffd4 100644 --- a/server-plugins/process-resources/src/index.ts +++ b/server-plugins/process-resources/src/index.ts @@ -89,6 +89,7 @@ import { } from './transform' import { RunSubProcess, + CancelSubProcess, CreateToDo, UpdateCard, CreateCard, @@ -489,6 +490,7 @@ export * from './utils' export default async () => ({ func: { RunSubProcess, + CancelSubProcess, CreateToDo, UpdateCard, CreateCard, diff --git a/server-plugins/process/src/index.ts b/server-plugins/process/src/index.ts index cd10f3b7d5c..48ea82bb996 100644 --- a/server-plugins/process/src/index.ts +++ b/server-plugins/process/src/index.ts @@ -48,6 +48,7 @@ export default plugin(serverProcessId, { }, func: { RunSubProcess: '' as Resource, + CancelSubProcess: '' as Resource, CreateToDo: '' as Resource, UpdateCard: '' as Resource, CreateCard: '' as Resource, diff --git a/services/process/src/main.ts b/services/process/src/main.ts index 83f05eee114..a2fa8edec94 100644 --- a/services/process/src/main.ts +++ b/services/process/src/main.ts @@ -488,10 +488,10 @@ async function executeTransition ( ) await client.tx(apply) await sendEvent(control, execution, transition, card, isDone) - if (isDone && execution.parentId !== undefined) { - await checkParent(execution, control) - } TxProcessor.applyUpdate(execution, executionUpdate) + if (execution.parentId !== undefined) { + await checkParent(execution, control, isDone) + } currTransition = transition transition = await checkNext(control, execution, context) nested = true @@ -734,28 +734,26 @@ async function fillParams ( return res } -async function checkParent (execution: Execution, control: ProcessControl): Promise { +async function checkParent (execution: Execution, control: ProcessControl, isDone: boolean): Promise { try { const subProcesses = await control.client.findAll(process.class.Execution, { parentId: execution.parentId, status: ExecutionStatus.Active }) const filtered = subProcesses.filter((it) => it._id !== execution._id) - if (filtered.length !== 0) return const parent = (await control.client.findAll(process.class.Execution, { _id: execution.parentId }))[0] if (parent === undefined) return const _process = control.client.getModel().findObject(parent.process) if (_process === undefined) return if (parent.status !== ExecutionStatus.Active) return + const triggers: Ref[] = [process.trigger.OnSubProcessMatch] + if (filtered.length === 0 && isDone) triggers.push(process.trigger.OnSubProcessesDone) const transitions = control.client.getModel().findAllSync( process.class.Transition, { from: parent.currentState, process: parent.process, - trigger: - filtered.length === 0 - ? { $in: [process.trigger.OnSubProcessesDone, process.trigger.OnSubProcessMatch] } - : process.trigger.OnSubProcessMatch + trigger: { $in: triggers } }, { sort: { rank: SortingOrder.Ascending }