platform/plugins/process-resources/src/utils.ts
Denis Bykhov 470c629f9f
Process roles (#8654)
Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
2025-04-22 16:39:21 +07:00

405 lines
12 KiB
TypeScript

// 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<T extends Doc> (methodId: Ref<Method<T>>): Promise<Step<T>> {
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<Class<Type<any>>>,
category: AttributeCategory,
attr?: Ref<AnyAttribute>
): 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<string, NestedContext> = {}
const relations: Record<string, RelatedContext> = {}
const refs = getClassAttributes(client, process.masterTag, core.class.RefTo, 'attribute')
for (const ref of refs) {
const refAttributes = getClassAttributes(client, (ref.type as RefTo<Doc>).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<Doc>).of
if (arrOf._class !== core.class.RefTo) continue
const to = (arrOf as RefTo<Doc>).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<Class<Doc>>,
target: Ref<Class<Type<any>>>,
category: AttributeCategory
): Array<Ref<ProcessFunction>> {
const matched: Array<Ref<ProcessFunction>> = []
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<Class<Doc>>,
target: Ref<Class<Type<any>>>,
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<Doc>).of
const attrClass = arrOf._class === core.class.RefTo ? (arrOf as RefTo<Doc>).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<Doc>).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<Doc>).of
const attrClass = arrOf._class === core.class.RefTo ? (arrOf as RefTo<Doc>).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<Association>,
direction: 'A' | 'B'
): Ref<ProcessFunction> | 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<ProcessFunction> | 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<ProcessFunction> | 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<Doc>): DocumentQuery<Doc> {
if (value === false) {
return { ...query, done: false }
}
return query
}
export async function continueExecution (value: Execution): Promise<void> {
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<Process>,
target: Ref<State>,
userContext: Record<string, any>
): Promise<Record<string, any>> {
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<Process>,
state: State,
userContext: Record<string, any>
): Promise<Record<string, any>> {
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<void>((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<string, any>
): Promise<Record<string, any>> {
for (const action of state.actions) {
if (action.methodId !== process.method.RunSubProcess) continue
const processId = action.params._id as Ref<Process>
if (processId === undefined) continue
const res = await newExecutionUserInput(processId, {})
userContext[processId] = res
}
return userContext
}
export async function newExecutionUserInput (
_id: Ref<Process>,
userContext: Record<string, any>
): Promise<Record<string, any>> {
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<string, any>
): Promise<Record<string, any>> {
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<Card>, _id: Ref<Process>, space: Ref<Space>): Promise<void> {
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<Doc> {
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<Ref<State>, any>
): Promise<void> {
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<void>((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
})
}