// 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 } from '@hcengineering/card' import core, { generateId, type AnyAttribute, type ArrOf, type Association, type Class, type Client, type Doc, type DocumentQuery, type Ref, type RefTo, type Space, type TxOperations, type Type } from '@hcengineering/core' import { PlatformError, Severity, Status } from '@hcengineering/platform' import { getClient } from '@hcengineering/presentation' import { parseContext, type Context, type Execution, type Method, type NestedContext, type Process, type ProcessFunction, type RelatedContext, type SelectedUserRequest, type State, type Step } from '@hcengineering/process' import { showPopup } from '@hcengineering/ui' import { type AttributeCategory } from '@hcengineering/view' import process from './plugin' export async function initStep (methodId: Ref>): Promise> { return { methodId, params: {} } } // 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() const context = await getNextStateUserInput(value, value.context ?? {}) await client.update(value, { error: null, context }) } export async function requestUserInput ( processId: Ref, target: Ref, userContext: Record ): 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: Record ): Promise> { for (const action of [...state.actions, state.endAction]) { 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: Record ): 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 const res = await newExecutionUserInput(processId, {}) userContext[processId] = res } return userContext } export async function newExecutionUserInput ( _id: Ref, userContext: Record ): Promise> { const client = getClient() const process = client.getModel().findObject(_id) if (process === undefined) return userContext const stateId = process.states[0] if (stateId === undefined) return userContext return await requestUserInput(_id, stateId, userContext) } export async function getNextStateUserInput ( execution: Execution, userContext: Record ): Promise> { const client = getClient() const process = client.getModel().findObject(execution.process) if (process === undefined) return userContext const currentIndex = execution.currentState == null ? -1 : process.states.findIndex((p) => p === execution.currentState) const nextState = process.states[currentIndex + 1] return await requestUserInput(execution.process, nextState, userContext) } export async function createExecution (card: Ref, _id: Ref, space: Ref): Promise { const client = getClient() const context = await newExecutionUserInput(_id, {}) await client.createDoc(process.class.Execution, space, { process: _id, currentState: null, card, done: false, rollback: {}, currentToDo: null, assignee: null, context }) } export function getToDoEndAction (prevState: State): Step { const contex: SelectedUserRequest = { id: generateId(), type: 'userRequest', key: 'user', _class: process.class.ProcessToDo } const endAction = { methodId: process.method.CreateToDo, params: { state: prevState._id, title: prevState.title, user: '$' + JSON.stringify(contex) } } return endAction } export async function requestResult ( txop: TxOperations, execution: Execution, results: Record, any> ): Promise { if (execution.currentState === null) return const client = getClient() const state = client.getModel().findObject(execution.currentState) if (state === undefined) return if (state.resultType == null) return const promise = new Promise((resolve, reject) => { showPopup(process.component.ResultInput, { type: state.resultType }, undefined, (res) => { if (res?.value !== undefined) { results[state._id] = res.value resolve() } else { reject(new PlatformError(new Status(Severity.ERROR, process.error.ResultNotProvided, {}))) } }) }) await promise await txop.update(execution, { results }) }