mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-13 21:27:46 +02:00
feat: Implement CancelSubProcess action, fix process execution flow. (#10530)
Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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<Mixin<Doc>> = []
|
||||
export let ignoreKeys: string[]
|
||||
|
||||
const client = getClient()
|
||||
const h = client.getHierarchy()
|
||||
|
||||
let width: number = 0
|
||||
|
||||
let columns = 1
|
||||
@@ -57,7 +61,7 @@
|
||||
<div class="masterTag">
|
||||
<MasterTagAttributes
|
||||
bind:this={masterTagAttributes}
|
||||
readonly={readonly || value.readonlySections?.includes(value._class)}
|
||||
readonly={readonly || h.getAncestors(value._class).some((p) => value.readonlySections?.includes(p))}
|
||||
{value}
|
||||
{ignoreKeys}
|
||||
fourRows={columns === 2}
|
||||
|
||||
@@ -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)
|
||||
</script>
|
||||
|
||||
@@ -52,7 +56,9 @@
|
||||
<div class="content" class:hidden>
|
||||
<Content
|
||||
{doc}
|
||||
readonly={readonly || updatePermissionForbidden || doc.readonlySections?.includes(doc._class)}
|
||||
readonly={readonly ||
|
||||
updatePermissionForbidden ||
|
||||
h.getAncestors(doc._class).some((p) => doc.readonlySections?.includes(p))}
|
||||
content={contentDiv}
|
||||
showToc={false}
|
||||
on:loaded
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -147,7 +147,8 @@
|
||||
"UnlockSection": "セクションのロックを解除",
|
||||
"CancelToDo": "アクションをキャンセル",
|
||||
"Export": "エクスポート",
|
||||
"Import": "インポート"
|
||||
"Import": "インポート",
|
||||
"CancelProcess": "プロセスをキャンセル"
|
||||
},
|
||||
"error": {
|
||||
"MethodNotFound": "メソッドが見つかりません: {methodId}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -148,7 +148,8 @@
|
||||
"UnlockSection": "Разблокировать секцию",
|
||||
"CancelToDo": "Отменить задачу",
|
||||
"Export": "Экспорт",
|
||||
"Import": "Импорт"
|
||||
"Import": "Импорт",
|
||||
"CancelProcess": "Отменить процесс"
|
||||
},
|
||||
"error": {
|
||||
"MethodNotFound": "Метод не найден: {methodId}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -148,7 +148,8 @@
|
||||
"UnlockSection": "解锁部分",
|
||||
"CancelToDo": "取消任务",
|
||||
"Export": "导出",
|
||||
"Import": "导入"
|
||||
"Import": "导入",
|
||||
"CancelProcess": "取消过程"
|
||||
},
|
||||
"error": {
|
||||
"MethodNotFound": "找不到方法:{methodId}",
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
process.class.Execution,
|
||||
{
|
||||
card: card._id,
|
||||
status: { $ne: ExecutionStatus.Cancelled }
|
||||
status: ExecutionStatus.Active
|
||||
},
|
||||
(res) => {
|
||||
docs = res
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<!--
|
||||
// Copyright © 2026 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import card from '@hcengineering/card'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Process, Step } from '@hcengineering/process'
|
||||
import { DropdownLabels, DropdownTextItem, Label } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import plugin from '../../plugin'
|
||||
|
||||
export let process: Process
|
||||
export let step: Step<Process>
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
const params = step.params
|
||||
let _id = params._id as Ref<Process>
|
||||
|
||||
function change (e: CustomEvent): void {
|
||||
if (e.detail !== undefined) {
|
||||
_id = e.detail
|
||||
params._id = _id
|
||||
step.params = params
|
||||
dispatch('change', step)
|
||||
}
|
||||
}
|
||||
|
||||
let processes: Process[] = []
|
||||
let items: DropdownTextItem[] = []
|
||||
|
||||
$: items = processes.map((it) => ({
|
||||
id: it._id,
|
||||
label: it.name
|
||||
}))
|
||||
|
||||
$: ancestors = hierarchy.getAncestors(process.masterTag).filter((it) => hierarchy.isDerived(it, card.class.Card))
|
||||
|
||||
$: selected = _id !== undefined ? items.find((it) => it.id === _id)?.id : undefined
|
||||
|
||||
$: processes = client
|
||||
.getModel()
|
||||
.findAllSync(plugin.class.Process, { masterTag: { $in: ancestors }, _id: { $ne: process._id } })
|
||||
</script>
|
||||
|
||||
<div class="editor-grid">
|
||||
<Label label={plugin.string.Process} />
|
||||
<DropdownLabels
|
||||
autoSelect={false}
|
||||
enableSearch={false}
|
||||
width={'100%'}
|
||||
{items}
|
||||
{selected}
|
||||
placeholder={plugin.string.Process}
|
||||
on:selected={change}
|
||||
/>
|
||||
</div>
|
||||
@@ -41,5 +41,5 @@
|
||||
|
||||
<div class="editor-grid">
|
||||
<Label label={plugin.string.ToDo} />
|
||||
<ToDoContextSelector readonly={false} {process} value={_id} on:change={change} />
|
||||
<ToDoContextSelector readonly={false} skipRollback {process} value={_id} on:change={change} />
|
||||
</div>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
function change (e: CustomEvent<string>): void {
|
||||
if (readonly || e.detail == null) return
|
||||
params._id = e.detail
|
||||
params.result = undefined
|
||||
params.result = {}
|
||||
dispatch('change', { params })
|
||||
}
|
||||
|
||||
@@ -46,6 +46,12 @@
|
||||
<Label label={plugin.string.ToDo} />
|
||||
<ToDoContextSelector {readonly} {skipRollback} {process} value={params._id} on:change={change} />
|
||||
{#if !skipRollback}
|
||||
<ResultCriteriaEditor {readonly} {process} result={params.result} value={params._id} on:change={changeResult} />
|
||||
<ResultCriteriaEditor
|
||||
{readonly}
|
||||
{process}
|
||||
result={params.result ?? {}}
|
||||
value={params._id}
|
||||
on:change={changeResult}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -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<Resources> => ({
|
||||
LockSectionEditor,
|
||||
UnLockSectionPresenter,
|
||||
CancelToDoEditor,
|
||||
CancelSubProcessEditor,
|
||||
ToDoValuePresenter
|
||||
},
|
||||
criteriaEditor: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -237,6 +237,7 @@ export default plugin(processId, {
|
||||
},
|
||||
method: {
|
||||
RunSubProcess: '' as Ref<Method<Process>>,
|
||||
CancelSubProcess: '' as Ref<Method<Process>>,
|
||||
CreateAction: '' as Ref<Method<EventButton>>,
|
||||
CancellAction: '' as Ref<Method<EventButton>>,
|
||||
CreateToDo: '' as Ref<Method<ProcessToDo>>,
|
||||
|
||||
@@ -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: Execution,
|
||||
control: ProcessControl
|
||||
): Promise<ExecuteResult> {
|
||||
const processId = params._id as Ref<Process>
|
||||
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: Execution,
|
||||
@@ -611,7 +643,7 @@ export async function CancelToDo (
|
||||
): Promise<ExecuteResult> {
|
||||
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[] = [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -48,6 +48,7 @@ export default plugin(serverProcessId, {
|
||||
},
|
||||
func: {
|
||||
RunSubProcess: '' as Resource<ExecuteFunc>,
|
||||
CancelSubProcess: '' as Resource<ExecuteFunc>,
|
||||
CreateToDo: '' as Resource<ExecuteFunc>,
|
||||
UpdateCard: '' as Resource<ExecuteFunc>,
|
||||
CreateCard: '' as Resource<ExecuteFunc>,
|
||||
|
||||
@@ -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<T extends Doc> (
|
||||
return res
|
||||
}
|
||||
|
||||
async function checkParent (execution: Execution, control: ProcessControl): Promise<void> {
|
||||
async function checkParent (execution: Execution, control: ProcessControl, isDone: boolean): Promise<void> {
|
||||
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<Trigger>[] = [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 }
|
||||
|
||||
Reference in New Issue
Block a user