mirror of
https://github.com/hcengineering/platform.git
synced 2026-08-18 02:15:44 +02:00
931 lines
29 KiB
TypeScript
931 lines
29 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, type MasterTag } from '@hcengineering/card'
|
|
import core, {
|
|
type AnyAttribute,
|
|
type ArrOf,
|
|
type Association,
|
|
type Class,
|
|
type Client,
|
|
type Doc,
|
|
type DocumentQuery,
|
|
type DocumentUpdate,
|
|
findProperty,
|
|
generateId,
|
|
matchQuery,
|
|
type Ref,
|
|
type RefTo,
|
|
type Space,
|
|
type TxCUD,
|
|
type TxFactory,
|
|
TxProcessor,
|
|
type Type
|
|
} from '@hcengineering/core'
|
|
import { getResource, type IntlString, PlatformError, Severity, Status } from '@hcengineering/platform'
|
|
import { getClient } from '@hcengineering/presentation'
|
|
import {
|
|
type Context,
|
|
type ContextId,
|
|
createContext,
|
|
type Execution,
|
|
type ExecutionContext,
|
|
ExecutionStatus,
|
|
type Func,
|
|
type Method,
|
|
type NestedContext,
|
|
parseContext,
|
|
type Process,
|
|
type ProcessExecutionContext,
|
|
type ProcessFunction,
|
|
type RelatedContext,
|
|
type SelectedContext,
|
|
type SelectedUserRequest,
|
|
type State,
|
|
type Step,
|
|
type StepId,
|
|
type Transition,
|
|
type UpdateCriteriaComponent,
|
|
type UserResult
|
|
} from '@hcengineering/process'
|
|
import { showPopup } from '@hcengineering/ui'
|
|
import { type AttributeCategory } from '@hcengineering/view'
|
|
import process from './plugin'
|
|
|
|
export function isTypeEqual (toCheck: Type<any> | undefined, attr: Type<any>): 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<any>] !== attr[key as keyof Type<any>]) return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
export function generateContextId (): ContextId {
|
|
return generateId() as string as ContextId
|
|
}
|
|
|
|
export function getContextMasterTag (
|
|
client: Client,
|
|
context: SelectedContext | undefined,
|
|
process: Process
|
|
): Ref<MasterTag> | undefined {
|
|
if (context === undefined) return
|
|
const h = client.getHierarchy()
|
|
const model = client.getModel()
|
|
if (context.type === 'attribute') {
|
|
const attr = h.findAttribute(process.masterTag, context.key)
|
|
if (attr === undefined) return
|
|
const parentType = attr.type._class === core.class.ArrOf ? (attr.type as ArrOf<Doc>).of : attr.type
|
|
if (parentType._class === core.class.RefTo) return (parentType as RefTo<Doc>).to
|
|
}
|
|
if (context.type === 'nested') {
|
|
const attr = h.findAttribute(process.masterTag, context.path)
|
|
if (attr === undefined) return
|
|
const parentType = attr.type._class === core.class.ArrOf ? (attr.type as ArrOf<Doc>).of : attr.type
|
|
const targetClass = parentType._class === core.class.RefTo ? (parentType as RefTo<Doc>).to : parentType._class
|
|
const nested = h.findAttribute(targetClass, context.key)
|
|
return (nested?.type as RefTo<Doc>)?.to
|
|
}
|
|
if (context.type === 'relation') {
|
|
const assoc = model.findObject(context.association)
|
|
if (assoc === undefined) return
|
|
const targetClass = context.direction === 'A' ? assoc.classA : assoc.classB
|
|
if (context.key === '_id') return targetClass as Ref<MasterTag>
|
|
const nested = h.findAttribute(targetClass, context.key)
|
|
return (nested?.type as RefTo<Doc>)?.to
|
|
}
|
|
if (context.type === 'context') {
|
|
const execContext = process.context[context.id]
|
|
if (execContext === undefined) return
|
|
return execContext._class
|
|
}
|
|
}
|
|
|
|
export async function pickTransition (
|
|
client: Client,
|
|
execution: Execution,
|
|
transitions: Transition[],
|
|
context: Record<string, any>
|
|
): Promise<Transition | undefined> {
|
|
for (const tr of transitions) {
|
|
const trigger = client.getModel().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(client, execution, filled, context)
|
|
if (res) return tr
|
|
}
|
|
}
|
|
|
|
function fillParams (params: Record<string, any>, execution: Execution): Record<string, any> {
|
|
const res: Record<string, any> = {}
|
|
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<T extends Doc> (methodId: Ref<Method<T>>): Promise<Step<T>> {
|
|
const client = getClient()
|
|
const method = client.getModel().findObject(methodId)
|
|
if (method === undefined) {
|
|
throw new Error('Process not found')
|
|
}
|
|
const step: Step<T> = {
|
|
_id: generateId() as string as StepId,
|
|
context:
|
|
method.createdContext !== null
|
|
? {
|
|
_id: generateContextId(),
|
|
_class: method.createdContext._class
|
|
}
|
|
: null,
|
|
methodId,
|
|
params: method.defaultParams ?? {}
|
|
}
|
|
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<Class<Type<any>>>,
|
|
category: AttributeCategory,
|
|
attr?: Ref<AnyAttribute>,
|
|
includeConvertible: boolean = false
|
|
): 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 executionContext: Record<string, ProcessExecutionContext> = {}
|
|
|
|
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 ancestors = new Set(client.getHierarchy().getAncestors(_process.masterTag))
|
|
|
|
const relationsA = allRelations.filter((it) => ancestors.has(it.classA))
|
|
for (const rel of relationsA) {
|
|
if (['object', 'array'].includes(category) && client.getHierarchy().isDerived(rel.classB, target)) {
|
|
relations[rel.nameB] = {
|
|
name: rel.nameB,
|
|
association: rel._id,
|
|
direction: 'B',
|
|
attributes: []
|
|
}
|
|
} else {
|
|
const refAttributes = getClassAttributes(client, rel.classB, target, 'attribute')
|
|
if (refAttributes.length > 0) {
|
|
relations[rel.nameB] = {
|
|
name: rel.nameB,
|
|
association: rel._id,
|
|
direction: 'B',
|
|
attributes: refAttributes
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const relationsB = allRelations.filter((it) => ancestors.has(it.classB))
|
|
for (const rel of relationsB) {
|
|
if (['object', 'array'].includes(category) && client.getHierarchy().isDerived(rel.classA, target)) {
|
|
relations[rel.nameA] = {
|
|
name: rel.nameA,
|
|
association: rel._id,
|
|
direction: 'A',
|
|
attributes: []
|
|
}
|
|
} else {
|
|
const refAttributes = getClassAttributes(client, rel.classA, target, 'attribute')
|
|
if (refAttributes.length > 0) {
|
|
relations[rel.nameA] = {
|
|
name: rel.nameA,
|
|
association: rel._id,
|
|
direction: 'A',
|
|
attributes: refAttributes
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const key in _process.context) {
|
|
const contextId = key as ContextId
|
|
const value = _process.context[contextId]
|
|
if (client.getHierarchy().isDerived(value._class, target)) {
|
|
executionContext[key] = {
|
|
attributes: [],
|
|
name: value.name,
|
|
context: contextId,
|
|
value: value.value
|
|
}
|
|
} else {
|
|
const contextAttributes = getClassAttributes(client, value._class, target, category)
|
|
if (contextAttributes.length > 0) {
|
|
executionContext[key] = {
|
|
name: value.name,
|
|
context: contextId,
|
|
value: value.value,
|
|
attributes: contextAttributes
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const convertible: Array<{ func: Ref<ProcessFunction>, context: Context }> = []
|
|
if (includeConvertible) {
|
|
const funcs = client.getModel().findAllSync(process.class.ProcessFunction, { type: 'convert', to: target })
|
|
for (const func of funcs) {
|
|
const convContext = getContext(client, _process, func.of as Ref<Class<Type<any>>>, category, attr, false)
|
|
if (
|
|
convContext.attributes.length > 0 ||
|
|
Object.keys(convContext.executionContext).length > 0 ||
|
|
Object.keys(convContext.nested).length > 0 ||
|
|
Object.keys(convContext.relations).length > 0 ||
|
|
convContext.functions.length > 0
|
|
) {
|
|
convertible.push({
|
|
func: func._id,
|
|
context: convContext
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
functions,
|
|
attributes,
|
|
nested,
|
|
relations,
|
|
executionContext,
|
|
convertible: convertible.length > 0 ? convertible : undefined
|
|
}
|
|
}
|
|
|
|
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 as unknown as ProcessFunction[]) {
|
|
switch (category) {
|
|
case 'array': {
|
|
if (func.category === 'array') {
|
|
if (hierarchy.isDerived(func.of, target) || func.of === core.class.ArrOf) {
|
|
matched.push(func._id)
|
|
}
|
|
}
|
|
break
|
|
}
|
|
default: {
|
|
if (hierarchy.isDerived(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])
|
|
}
|
|
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])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return matchedAttributes
|
|
}
|
|
|
|
export function getRelationObjectReduceFunc (
|
|
client: Client,
|
|
association: Ref<Association>,
|
|
direction: 'A' | 'B',
|
|
target: AnyAttribute
|
|
): Func | 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 === 'A') return undefined
|
|
if (target.type._class === core.class.ArrOf) return undefined
|
|
return { func: process.function.FirstValue, props: {} }
|
|
}
|
|
|
|
export function getRelationReduceFunc (
|
|
client: Client,
|
|
association: Ref<Association>,
|
|
direction: 'A' | 'B'
|
|
): Func | 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 === 'A') return undefined
|
|
return { func: process.function.FirstValue, props: {} }
|
|
}
|
|
|
|
export function getValueReduceFunc (source: AnyAttribute, target: AnyAttribute): Func | undefined {
|
|
if (source.type._class !== core.class.ArrOf) return undefined
|
|
if (target.type._class === core.class.ArrOf) return undefined
|
|
return { func: process.function.FirstValue, props: {} }
|
|
}
|
|
|
|
export function getContextFunctionReduce (func: ProcessFunction, target: AnyAttribute): Func | undefined {
|
|
if (func.category !== 'array') return undefined
|
|
if (target.type._class === core.class.ArrOf) return undefined
|
|
return { func: process.function.FirstValue, props: {} }
|
|
}
|
|
|
|
export function showDoneQuery (value: any, query: DocumentQuery<Execution>): DocumentQuery<Execution> {
|
|
if (value === false) {
|
|
return { ...query, status: ExecutionStatus.Active }
|
|
}
|
|
return query
|
|
}
|
|
|
|
export async function continueExecution (value: Execution): Promise<void> {
|
|
if (value.error == null) return
|
|
const client = getClient()
|
|
let context = value.context
|
|
const transition = value.error[0].transition
|
|
if (transition == null) {
|
|
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
|
|
}
|
|
await client.update(value, { status: ExecutionStatus.Active, context })
|
|
}
|
|
|
|
export async function requestUserInput (
|
|
processId: Ref<Process>,
|
|
space: Ref<Space>,
|
|
target: Transition,
|
|
execution: Execution,
|
|
userContext: ExecutionContext,
|
|
inputContext: Record<string, any> = {},
|
|
skipExisting: boolean = false
|
|
): Promise<{ context: ExecutionContext, state: Ref<State>, changed: boolean }> {
|
|
const client = getClient()
|
|
let changed = false
|
|
const tr = await getTransitionUserInput(processId, space, target, userContext, skipExisting)
|
|
if (tr !== undefined) {
|
|
userContext = { ...userContext, ...tr }
|
|
changed = true
|
|
}
|
|
const sub = await getSubProcessesUserInput(execution, 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 && t.trigger !== process.trigger.OnSubProcessesDone
|
|
)
|
|
|
|
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 { context: userContext, state: target.to, changed }
|
|
}
|
|
|
|
export async function getTransitionUserInput (
|
|
processId: Ref<Process>,
|
|
space: Ref<Space>,
|
|
transition: Transition,
|
|
userContext: ExecutionContext,
|
|
skipExisting: boolean = false
|
|
): Promise<ExecutionContext | undefined> {
|
|
let changed = false
|
|
for (const action of transition.actions) {
|
|
if (action == null) continue
|
|
const inputs: SelectedUserRequest[] = []
|
|
for (const key in action.params) {
|
|
const value = (action.params as any)[key]
|
|
if (typeof value === 'string') {
|
|
const context = parseContext(value)
|
|
if (context !== undefined && context.type === 'userRequest') {
|
|
if (skipExisting && userContext[context.id] !== undefined) continue
|
|
inputs.push(context)
|
|
}
|
|
} else if (typeof value === 'object' && !Array.isArray(value)) {
|
|
for (const element of Object.values(value)) {
|
|
const context = parseContext(element)
|
|
if (context !== undefined && context.type === 'userRequest') {
|
|
if (skipExisting && userContext[context.id] !== undefined) continue
|
|
inputs.push(context)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (inputs.length > 0) {
|
|
const promise = new Promise<void>((resolve) => {
|
|
showPopup(
|
|
process.component.RequestUserInput,
|
|
{
|
|
processId,
|
|
transition: transition._id,
|
|
space,
|
|
inputs,
|
|
values: {}
|
|
},
|
|
undefined,
|
|
(res) => {
|
|
if (res?.value !== undefined) {
|
|
changed = true
|
|
for (const [key, value] of Object.entries(res.value)) {
|
|
userContext[key as ContextId] = value
|
|
}
|
|
}
|
|
resolve()
|
|
}
|
|
)
|
|
})
|
|
await promise
|
|
}
|
|
}
|
|
return changed ? userContext : undefined
|
|
}
|
|
|
|
function getEmptyContext (): ExecutionContext {
|
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
return {} as ExecutionContext
|
|
}
|
|
|
|
export async function getSubProcessesUserInput (
|
|
execution: Execution,
|
|
space: Ref<Space>,
|
|
transition: Transition,
|
|
userContext: ExecutionContext
|
|
): Promise<ExecutionContext | undefined> {
|
|
let changed = false
|
|
for (const action of transition.actions) {
|
|
if (action.methodId !== process.method.RunSubProcess) continue
|
|
const processId = action.params._id as Ref<Process>
|
|
if (processId === undefined) continue
|
|
const context: ExecutionContext = JSON.parse(JSON.stringify(action.params.context ?? getEmptyContext()))
|
|
for (const [k, v] of Object.entries(context)) {
|
|
const c = parseContext(v)
|
|
if (c !== undefined && c.type === 'userRequest') {
|
|
if (userContext[c.id] !== undefined) continue
|
|
;(context as any)[k] = userContext[c.id]
|
|
}
|
|
}
|
|
const initTransition = getClient().getModel().findAllSync(process.class.Transition, {
|
|
process: processId,
|
|
from: null
|
|
})[0]
|
|
if (initTransition === undefined) continue
|
|
const card = action.params.card ?? execution.card
|
|
const client = getClient()
|
|
const mockExecution: Execution = {
|
|
_id: generateId(),
|
|
process: processId,
|
|
currentState: null as any,
|
|
card,
|
|
rollback: [],
|
|
context,
|
|
status: ExecutionStatus.Active,
|
|
space,
|
|
_class: process.class.Execution,
|
|
modifiedOn: 0,
|
|
modifiedBy: client.user
|
|
}
|
|
const res = await newExecutionUserInput(processId, space, mockExecution, true)
|
|
if (action.context == null || res === undefined) continue
|
|
userContext[action.context._id] = res
|
|
userContext = { ...userContext, ...res.context }
|
|
changed = true
|
|
}
|
|
return changed ? userContext : undefined
|
|
}
|
|
|
|
export async function newExecutionUserInput (
|
|
_id: Ref<Process>,
|
|
space: Ref<Space>,
|
|
execution: Execution,
|
|
skipExisting: boolean = false
|
|
): Promise<{ context: ExecutionContext, state: Ref<State>, changed: boolean } | undefined> {
|
|
const client = getClient()
|
|
const initTransition = client.getModel().findAllSync(process.class.Transition, {
|
|
process: _id,
|
|
from: null
|
|
})[0]
|
|
if (initTransition === undefined) return undefined
|
|
return await requestUserInput(_id, space, initTransition, execution, execution.context, {}, skipExisting)
|
|
}
|
|
|
|
export async function getNextStateUserInput (
|
|
execution: Execution,
|
|
transition: Transition,
|
|
userContext: ExecutionContext,
|
|
inputContext: Record<string, any> = {}
|
|
): Promise<{ context: ExecutionContext, state: Ref<State>, changed: boolean } | undefined> {
|
|
const client = getClient()
|
|
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<Card>,
|
|
_id: Ref<Process>,
|
|
space: Ref<Space>,
|
|
txFactory: TxFactory
|
|
): Promise<TxCUD<Doc> | undefined> {
|
|
const client = getClient()
|
|
|
|
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
|
|
const executionId = generateId<Execution>()
|
|
|
|
const mockExecution: Execution = {
|
|
_id: executionId,
|
|
process: _id,
|
|
currentState: null as any,
|
|
card,
|
|
rollback: [],
|
|
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: null as any,
|
|
card,
|
|
rollback: [],
|
|
context: result?.context ?? getEmptyContext(),
|
|
status: ExecutionStatus.Active
|
|
},
|
|
executionId
|
|
)
|
|
}
|
|
|
|
export function getToDoEndAction (prevState: State): Step<Doc> {
|
|
const context: SelectedUserRequest = {
|
|
id: generateContextId(),
|
|
type: 'userRequest',
|
|
key: 'user',
|
|
_class: process.class.ProcessToDo
|
|
}
|
|
const endAction: Step<Doc> = {
|
|
_id: generateId() as string as StepId,
|
|
context: {
|
|
_id: context.id,
|
|
_class: process.class.ProcessToDo
|
|
},
|
|
methodId: process.method.CreateToDo,
|
|
params: {
|
|
state: prevState._id,
|
|
title: prevState.title,
|
|
user: createContext(context)
|
|
}
|
|
}
|
|
return endAction
|
|
}
|
|
|
|
export async function requestResult (
|
|
execution: Execution,
|
|
results: UserResult[] | undefined,
|
|
context: ExecutionContext
|
|
): Promise<ExecutionContext | undefined> {
|
|
if (results == null || results.length === 0) return
|
|
const promise = new Promise<void>((resolve, reject) => {
|
|
showPopup(process.component.ResultInput, { results, context }, undefined, (res) => {
|
|
if (res !== undefined) {
|
|
for (const contextId in res) {
|
|
const val = res[contextId]
|
|
context[contextId as ContextId] = val
|
|
}
|
|
resolve()
|
|
} else {
|
|
reject(new PlatformError(new Status(Severity.ERROR, process.error.ResultNotProvided, {})))
|
|
}
|
|
})
|
|
})
|
|
await promise
|
|
return context
|
|
}
|
|
|
|
export function todoTranstionCheck (
|
|
client: Client,
|
|
execution: Execution,
|
|
params: Record<string, any>,
|
|
context: Record<string, any>
|
|
): boolean {
|
|
if (params._id === undefined) return false
|
|
return context.todo?._id === params._id && checkResult(context, params.result)
|
|
}
|
|
|
|
function checkResult (context: Record<string, any>, results: Record<string, any> | undefined): boolean {
|
|
if (results === undefined) return true
|
|
for (const [key, value] of Object.entries(results)) {
|
|
const res = findProperty([context as any], key, value)
|
|
if (res.length === 0) return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
export function timeTransitionCheck (
|
|
client: Client,
|
|
execution: Execution,
|
|
params: Record<string, any>,
|
|
context: Record<string, any>
|
|
): boolean {
|
|
if (params.value === undefined) return false
|
|
return params.value <= Date.now()
|
|
}
|
|
|
|
export function eventCheck (
|
|
client: Client,
|
|
execution: Execution,
|
|
params: Record<string, any>,
|
|
context: Record<string, any>
|
|
): boolean {
|
|
if (params.eventType === undefined) return false
|
|
return context.eventType === params.eventType
|
|
}
|
|
|
|
export async function approveRequestApproved (
|
|
client: Client,
|
|
execution: Execution,
|
|
params: Record<string, any>,
|
|
context: Record<string, any>
|
|
): Promise<boolean> {
|
|
if (params._id === undefined) return false
|
|
return context.todo?.group === params._id && context.todo?.approved === true
|
|
}
|
|
|
|
export async function approveRequestRejected (
|
|
client: Client,
|
|
execution: Execution,
|
|
params: Record<string, any>,
|
|
context: Record<string, any>
|
|
): Promise<boolean> {
|
|
if (params._id === undefined) return false
|
|
return context.todo?.group === params._id && context.todo?.approved === false
|
|
}
|
|
|
|
export function matchCardCheck (
|
|
client: Client,
|
|
execution: Execution,
|
|
params: Record<string, any>,
|
|
context: Record<string, any>
|
|
): boolean {
|
|
let doc = context.card
|
|
if (doc === undefined) return false
|
|
const process = client.getModel().findObject(execution.process)
|
|
if (process === undefined) return false
|
|
if (client.getHierarchy().isMixin(process.masterTag)) {
|
|
doc = client.getHierarchy().as(doc, process.masterTag)
|
|
}
|
|
const res = matchQuery([doc], params, doc._class, client.getHierarchy(), true)
|
|
return res.length > 0
|
|
}
|
|
|
|
export function fieldChangesCheck (
|
|
client: Client,
|
|
execution: Execution,
|
|
params: Record<string, any>,
|
|
context: Record<string, any>
|
|
): boolean {
|
|
const doc = context.card
|
|
if (doc === undefined) return false
|
|
const operations = (context.operations ?? {}) as DocumentUpdate<Doc>
|
|
const target = Object.keys(params)[0]
|
|
if (!TxProcessor.hasUpdate(operations, target)) return false
|
|
const res = matchQuery([doc], params, doc._class, client.getHierarchy(), true)
|
|
return res.length > 0
|
|
}
|
|
|
|
export async function subProcessesDoneCheck (
|
|
client: Client,
|
|
execution: Execution,
|
|
params: Record<string, any>,
|
|
context: Record<string, any>
|
|
): Promise<boolean> {
|
|
const res = await client.findOne(process.class.Execution, {
|
|
parentId: execution._id,
|
|
status: ExecutionStatus.Active
|
|
})
|
|
return res === undefined
|
|
}
|
|
|
|
export async function subProcessMatchCheck (
|
|
client: Client,
|
|
execution: Execution,
|
|
params: Record<string, any>,
|
|
context: Record<string, any>
|
|
): Promise<boolean> {
|
|
const { process: _process, ...otherCritera } = params
|
|
if (_process === undefined) return false
|
|
if (Object.keys(otherCritera).length === 0) return true
|
|
|
|
const subProcesses = await client.findAll(process.class.Execution, {
|
|
parentId: execution._id,
|
|
process: params.process
|
|
})
|
|
|
|
if (subProcesses.length === 0) return false
|
|
|
|
const [predicate, value] = Object.entries(otherCritera)[0]
|
|
|
|
const res = matchQuery(
|
|
subProcesses,
|
|
{ currentState: { $in: value } },
|
|
process.class.Execution,
|
|
client.getHierarchy(),
|
|
true
|
|
)
|
|
if (predicate === '$all') {
|
|
return res.length === subProcesses.length
|
|
} else if (predicate === '$any') {
|
|
return res.length > 0
|
|
} else if (predicate === '$nin') {
|
|
return res.length === 0
|
|
}
|
|
return false
|
|
}
|
|
|
|
export function getCriteriaEditor (
|
|
of: Ref<Class<Doc>>,
|
|
category: AttributeCategory
|
|
): UpdateCriteriaComponent | undefined {
|
|
const client = getClient()
|
|
if (category !== 'attribute') {
|
|
const res = client.getModel().findAllSync(process.class.UpdateCriteriaComponent, {
|
|
category
|
|
})[0]
|
|
return res
|
|
}
|
|
const res = client.getModel().findAllSync(process.class.UpdateCriteriaComponent, {
|
|
category,
|
|
of
|
|
})[0]
|
|
return res
|
|
}
|
|
|
|
export function getMockAttribute (_class: Ref<Class<Doc>>, label: IntlString, type: Type<any>): AnyAttribute {
|
|
return {
|
|
attributeOf: _class,
|
|
name: '',
|
|
_id: generateId(),
|
|
space: core.space.Model,
|
|
modifiedOn: 0,
|
|
modifiedBy: core.account.System,
|
|
_class: core.class.Attribute,
|
|
type,
|
|
label
|
|
}
|
|
}
|
|
|
|
export async function checkProcessSectionVisibility (doc: Card): Promise<boolean> {
|
|
const client = getClient()
|
|
const anc = client.getHierarchy().getAncestors(doc._class)
|
|
const processes = client.getModel().findAllSync(process.class.Process, { masterTag: { $in: anc } })
|
|
return processes.length > 0
|
|
}
|