// Copyright © 2025 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. import { type Card, type MasterTag } from '@hcengineering/card' import core, { generateId, type AnyAttribute, type ArrOf, type Association, type Class, type Client, type Doc, type DocumentQuery, type ModelDb, type Ref, type RefTo, type Space, type TxOperations, type Type } from '@hcengineering/core' import { getResource, PlatformError, Severity, Status } from '@hcengineering/platform' import { getClient } from '@hcengineering/presentation' import { ExecutionStatus, parseContext, type Context, type ContextId, type Execution, type ExecutionContext, type Method, type NestedContext, type Process, type ProcessFunction, type RelatedContext, type SelectedContext, type SelectedUserRequest, type State, type Step, type StepId, type Transition } from '@hcengineering/process' import { showPopup } from '@hcengineering/ui' import { type AttributeCategory } from '@hcengineering/view' import process from './plugin' export function isTypeEqual (toCheck: Type | undefined, attr: Type): boolean { const skip = ['label', 'icon', 'hidden', 'readonly'] if (toCheck === undefined) return true if (Object.keys(attr).length !== Object.keys(toCheck).length) return true for (const key of Object.keys(attr)) { if (skip.includes(key)) continue if (toCheck[key as keyof Type] !== attr[key as keyof Type]) return false } return true } export function generateContextId (): ContextId { return generateId() as string as ContextId } export function getContextAttribute ( client: Client, context: SelectedContext | undefined, masterTag: Ref ): AnyAttribute | undefined { if (context === undefined) return const h = client.getHierarchy() const model = client.getModel() if (context.type === 'attribute') { return h.findAttribute(masterTag, context.key) } if (context.type === 'nested') { const attr = h.findAttribute(masterTag, context.path) if (attr === undefined) return const parentType = attr.type._class === core.class.ArrOf ? (attr.type as ArrOf).of : attr.type const targetClass = parentType._class === core.class.RefTo ? (parentType as RefTo).to : parentType._class return h.findAttribute(targetClass, context.key) } if (context.type === 'relation') { const assoc = model.findObject(context.association) if (assoc === undefined) return const targetClass = context.direction === 'A' ? assoc.classA : assoc.classB return h.findAttribute(targetClass, context.key) } } export async function pickTransition ( model: ModelDb, execution: Execution, transitions: Transition[], doc: Doc ): Promise { for (const tr of transitions) { const trigger = model.findObject(tr.trigger) if (trigger === undefined) continue if (trigger.checkFunction === undefined) return tr const filled = fillParams(tr.triggerParams, execution) const checkFunc = await getResource(trigger.checkFunction) if (checkFunc === undefined) continue const res = await checkFunc(filled, doc) if (res) return tr } } function fillParams (params: Record, execution: Execution): Record { const res: Record = {} for (const key in params) { const value = params[key] const context = parseContext(value) if (context === undefined) { res[key] = value continue } if (context.type === 'context') { res[key] = execution.context[context.id] } } return res } export async function initState (methodId: Ref>): Promise> { const client = getClient() const method = client.getModel().findObject(methodId) if (method === undefined) { throw new Error('Process not found') } const step: Step = { _id: generateId() as string as StepId, contextId: method.contextClass !== null ? generateContextId() : null, methodId, params: {} } return step } // we should find all possible sources of data with selected type // I think one step depth should be enough for now export function getContext ( client: Client, process: Process, target: Ref>>, category: AttributeCategory, attr?: Ref ): Context { let attributes = getClassAttributes(client, process.masterTag, target, category) if (attr !== undefined && category === 'object') { attributes = attributes.filter((it) => it._id !== attr) } const functions = getContextFunctions(client, process.masterTag, target, category) const nested: Record = {} const relations: Record = {} const refs = getClassAttributes(client, process.masterTag, core.class.RefTo, 'attribute') for (const ref of refs) { const refAttributes = getClassAttributes(client, (ref.type as RefTo).to, target, 'attribute') if (refAttributes.length === 0) continue nested[ref.name] = { attribute: ref, attributes: refAttributes } } const arrs = getClassAttributes(client, process.masterTag, core.class.ArrOf, 'attribute') for (const arr of arrs) { const arrOf = (arr.type as ArrOf).of if (arrOf._class !== core.class.RefTo) continue const to = (arrOf as RefTo).to const arrAttributes = getClassAttributes(client, to, target, 'attribute') if (arrAttributes.length === 0) continue nested[arr.name] = { attribute: arr, attributes: arrAttributes } } const allRelations = client.getModel().findAllSync(core.class.Association, {}) const descendants = new Set(client.getHierarchy().getDescendants(process.masterTag)) const relationsA = allRelations.filter((it) => descendants.has(it.classA)) for (const rel of relationsA) { const refAttributes = getClassAttributes(client, rel.classB, target, 'attribute') if (refAttributes.length === 0) continue relations[rel.nameB] = { name: rel.nameB, association: rel._id, direction: 'B', attributes: refAttributes } } const relationsB = allRelations.filter((it) => descendants.has(it.classB)) for (const rel of relationsB) { const refAttributes = getClassAttributes(client, rel.classA, target, 'attribute') if (refAttributes.length === 0) continue relations[rel.nameA] = { name: rel.nameA, association: rel._id, direction: 'A', attributes: refAttributes } } return { functions, attributes, nested, relations } } function getContextFunctions ( client: Client, _class: Ref>, target: Ref>>, category: AttributeCategory ): Array> { const matched: Array> = [] const hierarchy = client.getHierarchy() const funcs = client.getModel().findAllSync(process.class.ProcessFunction, { type: 'context' }) for (const func of funcs) { switch (category) { case 'object': { if (func.category === 'array') { if (hierarchy.isDerived(func.of, target)) { matched.push(func._id) } } if (func.category === 'object') { if (hierarchy.isDerived(func.of, target)) { matched.push(func._id) } } break } case 'array': { if (func.category === 'array') { if (hierarchy.isDerived(func.of, target)) { matched.push(func._id) } } break } default: { if (func.of === target) { matched.push(func._id) } } } } return matched } function getClassAttributes ( client: Client, _class: Ref>, target: Ref>>, category: AttributeCategory ): AnyAttribute[] { const hierarchy = client.getHierarchy() const cardAttributes = hierarchy.getAllAttributes(_class) const matchedAttributes: AnyAttribute[] = [] for (const attr of cardAttributes) { if (attr[1].hidden === true) continue if (attr[1].label === undefined) continue switch (category) { case 'object': { if (attr[1].type._class === core.class.ArrOf) { const arrOf = (attr[1].type as ArrOf).of const attrClass = arrOf._class === core.class.RefTo ? (arrOf as RefTo).to : arrOf._class if (hierarchy.isDerived(attrClass, target)) { matchedAttributes.push(attr[1]) } } if (attr[1].type._class === core.class.RefTo) { const to = (attr[1].type as RefTo).to if (hierarchy.isDerived(to, target)) { matchedAttributes.push(attr[1]) } } break } case 'array': { if (attr[1].type._class === core.class.ArrOf) { const arrOf = (attr[1].type as ArrOf).of const attrClass = arrOf._class === core.class.RefTo ? (arrOf as RefTo).to : arrOf._class if (hierarchy.isDerived(attrClass, target)) { matchedAttributes.push(attr[1]) } } break } default: { if (attr[1].type._class === target) { matchedAttributes.push(attr[1]) } } } } return matchedAttributes } export function getRelationReduceFunc ( client: Client, association: Ref, direction: 'A' | 'B' ): Ref | undefined { const assoc = client.getModel().findObject(association) if (assoc === undefined) return undefined if (assoc.type === '1:1') return undefined if (assoc.type === '1:N' && direction === 'B') return undefined return process.function.FirstValue } export function getValueReduceFunc (source: AnyAttribute, target: AnyAttribute): Ref | undefined { if (source.type._class !== core.class.ArrOf) return undefined if (target.type._class === core.class.ArrOf) return undefined return process.function.FirstValue } export function getContextFunctionReduce ( func: ProcessFunction, target: AnyAttribute ): Ref | undefined { if (func.category !== 'array') return undefined if (target.type._class === core.class.ArrOf) return undefined return process.function.FirstValue } export function showDoneQuery (value: any, query: DocumentQuery): DocumentQuery { if (value === false) { return { ...query, done: false } } return query } export async function continueExecution (value: Execution): Promise { if (value.error == null) return const client = getClient() let context = value.context const transition = value.error[0].transition if (transition == null) { context = await newExecutionUserInput(value.process, context) } else { const _transition = client.getModel().findObject(transition) if (_transition === undefined) return const targetState = _transition.to context = targetState == null ? value.context : await getNextStateUserInput(value, targetState, value.context) } await client.update(value, { status: ExecutionStatus.Active, context }) } export async function requestUserInput ( processId: Ref, target: Ref, userContext: ExecutionContext ): Promise { const client = getClient() const state = client.getModel().findObject(target) if (state === undefined) return userContext userContext = await getStateUserInput(processId, state, userContext) userContext = await getSubProcessesUserInput(state, userContext) return userContext } export async function getStateUserInput ( processId: Ref, state: State, userContext: ExecutionContext ): Promise { for (const action of state.actions) { if (action == null) continue for (const key in action.params) { const value = (action.params as any)[key] const context = parseContext(value) if (context !== undefined && context.type === 'userRequest') { const promise = new Promise((resolve) => { showPopup( process.component.RequestUserInput, { processId, state: state._id, key: context.key, _class: context._class }, undefined, (res) => { if (res?.value !== undefined) { userContext[context.id] = res.value } resolve() } ) }) await promise } } } return userContext } export async function getSubProcessesUserInput (state: State, userContext: ExecutionContext): Promise { for (const action of state.actions) { if (action.methodId !== process.method.RunSubProcess) continue const processId = action.params._id as Ref if (processId === undefined) continue // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const res = await newExecutionUserInput(processId, {} as ExecutionContext) if (action.contextId == null) continue userContext[action.contextId] = res } return userContext } export async function newExecutionUserInput ( _id: Ref, userContext: ExecutionContext ): Promise { const client = getClient() const process = client.getModel().findObject(_id) if (process === undefined) return userContext return await requestUserInput(_id, process.initState, userContext) } export async function getNextStateUserInput ( execution: Execution, targetState: Ref, userContext: ExecutionContext ): Promise { const client = getClient() const process = client.getModel().findObject(execution.process) if (process === undefined) return userContext return await requestUserInput(execution.process, targetState, userContext) } export async function createExecution (card: Ref, _id: Ref, space: Ref): Promise { const client = getClient() // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const context = await newExecutionUserInput(_id, {} as ExecutionContext) const _process = client.getModel().findObject(_id) if (_process === undefined) return await client.createDoc(process.class.Execution, space, { process: _id, currentState: _process.initState, card, rollback: [], context, status: ExecutionStatus.Active }) } export function getToDoEndAction (prevState: State): Step { const context: SelectedUserRequest = { id: generateContextId(), type: 'userRequest', key: 'user', _class: process.class.ProcessToDo } const endAction = { _id: generateId() as string as StepId, contextId: generateContextId(), methodId: process.method.CreateToDo, system: true, params: { state: prevState._id, title: prevState.title, user: '$' + JSON.stringify(context) } } return endAction } export async function requestResult ( txop: TxOperations, execution: Execution, context: ExecutionContext ): Promise { if (execution.currentState === null) return const client = getClient() const state = client.getModel().findObject(execution.currentState) if (state === undefined) return for (const action of state.actions) { if (action.result == null) continue const promise = new Promise((resolve, reject) => { showPopup( process.component.ResultInput, { type: action.result?.type, name: action.result?.name }, undefined, (res) => { if (action.result?._id === undefined) return if (res?.value !== undefined) { context[action.result._id] = res.value resolve() } else { reject(new PlatformError(new Status(Severity.ERROR, process.error.ResultNotProvided, {}))) } } ) }) await promise await txop.update(execution, { context }) } } export function todoTranstionCheck (params: Record, doc: Doc): boolean { if (params._id === undefined) return false return doc._id === params._id }