diff --git a/plugins/process-resources/src/components/RunProcessCardPopup.svelte b/plugins/process-resources/src/components/RunProcessCardPopup.svelte index 4e49c4e3aae..e8788b9aa5c 100644 --- a/plugins/process-resources/src/components/RunProcessCardPopup.svelte +++ b/plugins/process-resources/src/components/RunProcessCardPopup.svelte @@ -41,7 +41,8 @@ async function runProcess (): Promise { if (process === undefined || card === undefined) return - await createExecution(card._id, process, card.space) + const tx = await createExecution(card._id, process, card.space, client.txFactory) + if (tx) await client.tx(tx) dispatch('close') } diff --git a/plugins/process-resources/src/components/RunProcessPopup.svelte b/plugins/process-resources/src/components/RunProcessPopup.svelte index b526f7cc8cd..6c11f37da7e 100644 --- a/plugins/process-resources/src/components/RunProcessPopup.svelte +++ b/plugins/process-resources/src/components/RunProcessPopup.svelte @@ -94,7 +94,8 @@ async function runProcess (_id: Ref): Promise { if (!value) return for (const element of values) { - await createExecution(element._id, _id, element.space) + const tx = await createExecution(element._id, _id, element.space, client.txFactory) + if (tx) await client.tx(tx) } dispatch('close') } diff --git a/plugins/process-resources/src/middleware.ts b/plugins/process-resources/src/middleware.ts index dc7d8a0c279..f41e793c6b8 100644 --- a/plugins/process-resources/src/middleware.ts +++ b/plugins/process-resources/src/middleware.ts @@ -13,14 +13,17 @@ import cardPlugin, { type Card } from '@hcengineering/card' import core, { + generateId, getCurrentAccount, SortingOrder, TxOperations, TxProcessor, type Client, + type Doc, type Tx, type TxApplyIf, type TxCreateDoc, + type TxCUD, type TxMixin, type TxResult, type TxUpdateDoc @@ -50,27 +53,47 @@ export class ProcessMiddleware extends BasePresentationMiddleware implements Pre return new ProcessMiddleware(client, next) } + private readonly txFactory = new TxOperations(this.client, getCurrentAccount().primarySocialId).txFactory + async tx (tx: Tx): Promise { - await this.handleTx(tx) + const preTx: Array> = [] + const postTx: Array> = [] + await this.handleTx(preTx, postTx, tx) + + if (preTx.length > 0 || postTx.length > 0) { + if (TxProcessor.isExtendsCUD(tx._class)) { + const applyIf = this.txFactory.createTxApplyIf( + core.space.Tx, + generateId(), + [], + [], + [...preTx, tx as TxCUD, ...postTx], + 'process', + true + ) + return await this.provideTx(applyIf) + } + } + return await this.provideTx(tx) } - private async handleTx (...txes: Tx[]): Promise { + private async handleTx (preTx: Array>, postTx: Array>, ...txes: Tx[]): Promise { for (const etx of txes) { if (etx._class === core.class.TxApplyIf) { const applyIf = etx as TxApplyIf - await this.handleTx(...applyIf.txes) + await this.handleTx(preTx, postTx, ...applyIf.txes) } - await this.handleCardCreate(etx) - await this.handleCardUpdate(etx) - await this.handleTagAdd(etx) - await this.handleToDoDone(etx) - await this.handleApproveRequest(etx) + await this.handleCardCreate(postTx, etx) + await this.handleCardUpdate(preTx, etx) + await this.handleTagAdd(postTx, etx) + await this.handleToDoDone(preTx, etx) + await this.handleApproveRequest(preTx, etx) } } - private async handleCardUpdate (etx: Tx): Promise { + private async handleCardUpdate (preTx: Array>, etx: Tx): Promise { if (etx._class === core.class.TxUpdateDoc || etx._class === core.class.TxMixin) { const updateTx = etx as TxUpdateDoc | TxMixin const hierarchy = this.client.getHierarchy() @@ -97,21 +120,26 @@ export class ProcessMiddleware extends BasePresentationMiddleware implements Pre }, { sort: { rank: SortingOrder.Ascending } } ) - const transition = await pickTransition(this.client, execution, transitions, { + const inputContext = { + ...execution.context, card: updated, operations: isUpdateTx(updateTx) ? updateTx.operations : updateTx.attributes - }) + } + const transition = await pickTransition(this.client, execution, transitions, inputContext) if (transition === undefined) return - const context = await getNextStateUserInput(execution, transition, execution.context) - const txop = new TxOperations(this.client, getCurrentAccount().primarySocialId) - await txop.update(execution, { - context - }) + const result = await getNextStateUserInput(execution, transition, execution.context, inputContext) + if (result?.changed === true) { + preTx.push( + this.txFactory.createTxUpdateDoc(execution._class, execution.space, execution._id, { + context: result.context + }) + ) + } } } } - private async handleCardCreate (etx: Tx): Promise { + private async handleCardCreate (postTx: Array>, etx: Tx): Promise { if (etx._class === core.class.TxCreateDoc) { const createTx = etx as TxCreateDoc const doc = TxProcessor.createDoc2Doc(createTx) @@ -130,12 +158,13 @@ export class ProcessMiddleware extends BasePresentationMiddleware implements Pre autoStart: true }) for (const proc of processes) { - await createExecution(createTx.objectId, proc._id, createTx.objectSpace) + const res = await createExecution(createTx.objectId, proc._id, createTx.objectSpace, this.txFactory) + if (res !== undefined) postTx.push(res) } } } - private async handleTagAdd (tx: Tx): Promise { + private async handleTagAdd (postTx: Array>, tx: Tx): Promise { if (tx._class !== core.class.TxMixin) return const mixinTx = tx as TxMixin const hierarchy = this.client.getHierarchy() @@ -146,11 +175,12 @@ export class ProcessMiddleware extends BasePresentationMiddleware implements Pre .getModel() .findAllSync(process.class.Process, { masterTag: mixinTx.mixin, autoStart: true }) for (const proc of processes) { - await createExecution(mixinTx.objectId, proc._id, mixinTx.objectSpace) + const res = await createExecution(mixinTx.objectId, proc._id, mixinTx.objectSpace, this.txFactory) + if (res !== undefined) postTx.push(res) } } - private async handleApproveRequest (etx: Tx): Promise { + private async handleApproveRequest (preTx: Array>, etx: Tx): Promise { if (etx._class === core.class.TxUpdateDoc) { const cud = etx as TxUpdateDoc if (cud.objectClass !== process.class.ApproveRequest) return @@ -163,7 +193,6 @@ export class ProcessMiddleware extends BasePresentationMiddleware implements Pre _id: approveRequest.execution }) if (execution === undefined) return - const txop = new TxOperations(this.client, getCurrentAccount().primarySocialId) const transitions = this.client.getModel().findAllSync( process.class.Transition, { @@ -176,18 +205,24 @@ export class ProcessMiddleware extends BasePresentationMiddleware implements Pre { sort: { rank: SortingOrder.Ascending } } ) const updatedApproveRequest = TxProcessor.updateDoc2Doc(approveRequest, cud) - const transition = await pickTransition(this.client, execution, transitions, { + const inputContext = { + ...execution.context, todo: updatedApproveRequest - }) + } + const transition = await pickTransition(this.client, execution, transitions, inputContext) if (transition === undefined) return - const context = await getNextStateUserInput(execution, transition, execution.context) - await txop.update(execution, { - context - }) + const result = await getNextStateUserInput(execution, transition, execution.context, inputContext) + if (result?.changed === true) { + preTx.push( + this.txFactory.createTxUpdateDoc(execution._class, execution.space, execution._id, { + context: result.context + }) + ) + } } } - private async handleToDoDone (etx: Tx): Promise { + private async handleToDoDone (preTx: Array>, etx: Tx): Promise { if (etx._class === core.class.TxUpdateDoc) { const cud = etx as TxUpdateDoc if (cud.objectClass !== process.class.ProcessToDo) return @@ -200,22 +235,42 @@ export class ProcessMiddleware extends BasePresentationMiddleware implements Pre _id: todo.execution }) if (execution === undefined) return - const txop = new TxOperations(this.client, getCurrentAccount().primarySocialId) - await requestResult(txop, execution, todo.results, execution.context) + + const context = await requestResult(execution, todo.results, execution.context) + const transitions = this.client.getModel().findAllSync(process.class.Transition, { process: execution.process, from: execution.currentState, trigger: process.trigger.OnToDoClose }) - const transition = await pickTransition(this.client, execution, transitions, { + const inputContext = { + ...(context ?? execution.context), todo - }) - if (transition === undefined) return - const context = await getNextStateUserInput(execution, transition, execution.context) - if (context !== undefined) { - await txop.update(execution, { - context - }) + } + const transition = await pickTransition(this.client, execution, transitions, inputContext) + if (transition === undefined) { + if (context !== undefined) { + preTx.push( + this.txFactory.createTxUpdateDoc(execution._class, execution.space, execution._id, { + context + }) + ) + } + return + } + const finalResult = await getNextStateUserInput(execution, transition, context ?? execution.context, inputContext) + if (finalResult?.changed === true) { + preTx.push( + this.txFactory.createTxUpdateDoc(execution._class, execution.space, execution._id, { + context: finalResult.context + }) + ) + } else if (context !== undefined) { + preTx.push( + this.txFactory.createTxUpdateDoc(execution._class, execution.space, execution._id, { + context + }) + ) } } } diff --git a/plugins/process-resources/src/utils.ts b/plugins/process-resources/src/utils.ts index 6437ebce6f7..9fcf57024c7 100644 --- a/plugins/process-resources/src/utils.ts +++ b/plugins/process-resources/src/utils.ts @@ -26,7 +26,8 @@ import core, { type Ref, type RefTo, type Space, - type TxOperations, + type TxCUD, + type TxFactory, TxProcessor, type Type } from '@hcengineering/core' @@ -422,13 +423,13 @@ export async function continueExecution (value: Execution): Promise { let context = value.context const transition = value.error[0].transition if (transition == null) { - const res = await newExecutionUserInput(value.process, value.space, context) - context = res ?? context + const res = await newExecutionUserInput(value.process, value.space, value) + context = res?.context ?? context } else { const _transition = client.getModel().findObject(transition) if (_transition === undefined) return const res = await getNextStateUserInput(value, _transition, context) - context = res ?? context + context = res?.context ?? context } await client.update(value, { status: ExecutionStatus.Active, context }) } @@ -437,17 +438,57 @@ export async function requestUserInput ( processId: Ref, space: Ref, target: Transition, - userContext: ExecutionContext -): Promise { + execution: Execution, + userContext: ExecutionContext, + inputContext: Record = {} +): Promise<{ context: ExecutionContext, state: Ref, changed: boolean }> { + const client = getClient() + let changed = false const tr = await getTransitionUserInput(processId, space, target, userContext) if (tr !== undefined) { userContext = { ...userContext, ...tr } + changed = true } const sub = await getSubProcessesUserInput(space, target, userContext) if (sub !== undefined) { userContext = { ...userContext, ...sub } + changed = true + } + + // Follow auto transitions + const nextAutoTransitions = client + .getModel() + .findAllSync(process.class.Transition, { + process: processId, + from: target.to + }) + .filter((t) => client.getModel().findObject(t.trigger)?.auto === true) + + if (nextAutoTransitions.length > 0) { + const newExecution = { + ...execution, + context: userContext, + currentState: target.to + } + const nextTransition = await pickTransition(client, newExecution, nextAutoTransitions, inputContext) + if (nextTransition !== undefined) { + const recursive = await requestUserInput( + processId, + space, + nextTransition, + newExecution, + userContext, + inputContext + ) + return { + context: recursive.context, + state: recursive.state, + changed: changed || recursive.changed + } + } } - return sub !== undefined || tr !== undefined ? userContext : undefined + + return { context: userContext, state: target.to, changed } } export async function getTransitionUserInput ( @@ -523,47 +564,76 @@ export async function getSubProcessesUserInput ( export async function newExecutionUserInput ( _id: Ref, space: Ref, - userContext?: ExecutionContext -): Promise { + execution: Execution +): Promise<{ context: ExecutionContext, state: Ref, changed: boolean } | undefined> { const client = getClient() const initTransition = client.getModel().findAllSync(process.class.Transition, { process: _id, from: null })[0] - if (initTransition === undefined) return userContext - return await requestUserInput(_id, space, initTransition, userContext ?? getEmptyContext()) + if (initTransition === undefined) return undefined + return await requestUserInput(_id, space, initTransition, execution, execution.context) } export async function getNextStateUserInput ( execution: Execution, transition: Transition, - userContext: ExecutionContext -): Promise { + userContext: ExecutionContext, + inputContext: Record = {} +): Promise<{ context: ExecutionContext, state: Ref, changed: boolean } | undefined> { const client = getClient() - const process = client.getModel().findObject(execution.process) - if (process === undefined) return userContext - return await requestUserInput(execution.process, execution.space, transition, userContext) + const _process = client.getModel().findObject(execution.process) + if (_process === undefined) return undefined + return await requestUserInput(execution.process, execution.space, transition, execution, userContext, inputContext) } -export async function createExecution (card: Ref, _id: Ref, space: Ref): Promise { +export async function createExecution ( + card: Ref, + _id: Ref, + space: Ref, + txFactory: TxFactory +): Promise | undefined> { const client = getClient() - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - const context = await newExecutionUserInput(_id, space) + const _process = client.getModel().findObject(_id) if (_process === undefined) return + const initTransition = client.getModel().findAllSync(process.class.Transition, { process: _id, from: null })[0] if (initTransition === undefined) return - await client.createDoc(process.class.Execution, space, { + const executionId = generateId() + + const mockExecution: Execution = { + _id: executionId, process: _id, currentState: initTransition.to, card, rollback: [], - context: context ?? getEmptyContext(), - status: ExecutionStatus.Active - }) + context: getEmptyContext(), + status: ExecutionStatus.Active, + space, + _class: process.class.Execution, + modifiedOn: 0, + modifiedBy: client.user + } + + const result = await newExecutionUserInput(_id, space, mockExecution) + + return txFactory.createTxCreateDoc( + process.class.Execution, + space, + { + process: _id, + currentState: initTransition.to, + card, + rollback: [], + context: result?.context ?? getEmptyContext(), + status: ExecutionStatus.Active + }, + executionId + ) } export function getToDoEndAction (prevState: State): Step { @@ -590,11 +660,10 @@ export function getToDoEndAction (prevState: State): Step { } export async function requestResult ( - txop: TxOperations, execution: Execution, results: UserResult[] | undefined, context: ExecutionContext -): Promise { +): Promise { if (results == null || results.length === 0) return const promise = new Promise((resolve, reject) => { showPopup(process.component.ResultInput, { results, context }, undefined, (res) => { @@ -610,9 +679,7 @@ export async function requestResult ( }) }) await promise - await txop.update(execution, { - context - }) + return context } export function todoTranstionCheck (