UNPKG

@nocobase/flow-engine

Version:

A standalone flow engine for NocoBase, managing workflows, models, and actions.

279 lines (260 loc) 10.5 kB
/** * This file is part of the NocoBase (R) project. * Copyright (c) 2020-2024 NocoBase Co., Ltd. * Authors: NocoBase Team. * * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. * For more information, please refer to: https://www.nocobase.com/agreement. */ import * as _ from 'lodash'; import type { Collection } from '../../data-source'; import { FlowModelContext } from '../../flowContext'; import { FlowModelMeta, ModelConstructor } from '../../types'; import { isInheritedFrom, resolveCreateModelOptions } from '../../utils'; import { SubModelItem } from './AddSubModelButton'; export function buildSubModelItem(M: ModelConstructor, ctx: FlowModelContext, skipHide = false): SubModelItem { const meta: FlowModelMeta = (M.meta ?? {}) as FlowModelMeta; if (meta.hide && !skipHide) { return; } // 判断是否为 CollectionBlockModel 的子类(用于集合选择层开启搜索) const item: SubModelItem = { key: M.name, label: meta.label || M.name, // 子菜单级搜索:仅尊重模型 meta 显式配置,避免在工具层做类型耦合判断 searchable: !!meta.searchable, searchPlaceholder: meta.searchPlaceholder, // Ensure toggleable models can be detected and toggled in menus // Meta.toggleable indicates the item should behave like a switch (unique per parent) // Add corresponding flags so AddSubModelButton can compute toggle state and removal toggleable: meta.toggleable, // Use the model class name for toggle detection (engine.getModelClass) // This is required by AddSubModelButton to locate existing instances useModel: M.name, sort: meta.sort || 1000, children: buildSubModelChildren(M, ctx), }; item['createModelOptions'] = meta.createModelOptions || { use: M.name, //TODO: this is wrong after code minized, we need to fix this }; return item; } function buildSubModelChildren(M: ModelConstructor, ctx: FlowModelContext) { const meta: FlowModelMeta = (M.meta ?? {}) as FlowModelMeta; let children: any; if (meta.children === false) { return false; } if (meta.children) { children = meta.children; } if (M['defineChildren']) { children = M['defineChildren'].bind(M); } if (typeof children === 'function') { const orininalChildren = children; children = async () => { const resolved = await orininalChildren(ctx); // deep clone and wrap createModelOptions const wrap = (nodes: any[]): any[] => nodes?.map((n) => { const node = { ...n }; if (node.children) { node.children = Array.isArray(node.children) ? wrap(node.children) : node.children; } else if (node.createModelOptions) { const src = node.createModelOptions; node.createModelOptions = async (...args: any[]) => { const extraArg = args && args.length > 0 ? args[args.length - 1] : undefined; // Resolve default options from FlowModel.define meta first, then child-specific options. const defaultOpts = await resolveCreateModelOptions(meta?.createModelOptions, ctx, extraArg); const childOpts = await resolveCreateModelOptions(src, ctx, extraArg); // Merge with child options taking precedence over defaults. return _.merge({}, _.cloneDeep(defaultOpts), childOpts); }; } return node; }); return wrap(resolved); }; } return children; } export function buildItems(subModelBaseClass: string | ModelConstructor) { return async (ctx: FlowModelContext) => { const items = await buildSubModelGroups([subModelBaseClass])(ctx); if (items.length === 0) { return []; } const children = items.shift().children; if (typeof children === 'function') { return children(ctx); } return children as any; }; } export function buildSubModelItems(subModelBaseClass: string | ModelConstructor, exclude = []) { return async (ctx: FlowModelContext) => { const SubModelClasses = ctx.engine.getSubclassesOf(subModelBaseClass); // Collect and sort subclasses by meta.sort (ascending), excluding hidden or inherited ones in `exclude` let candidates = Array.from(SubModelClasses.values()) .filter((C) => !C.meta?.hide) .filter((C) => { for (const P of exclude as (string | ModelConstructor)[]) { if (typeof P === 'string') { if (C.name === P) return false; } else if (C === P || isInheritedFrom(C, P)) { return false; } } return true; }) .sort((A, B) => (A.meta?.sort ?? 1000) - (B.meta?.sort ?? 1000)); // If no subclasses found, fall back to include the base class itself when visible. if (candidates.length === 0) { const BaseClass = typeof subModelBaseClass === 'string' ? ctx.engine.getModelClass(subModelBaseClass) : subModelBaseClass; if (BaseClass && !BaseClass.meta?.hide) { candidates = [BaseClass]; } } const items: SubModelItem[] = []; for (const M of candidates) { const item = buildSubModelItem(M, ctx); if (item) items.push(item); } return items; }; } export function buildSubModelGroups(subModelBaseClasses: (string | ModelConstructor)[] = []) { return async (ctx: FlowModelContext) => { const items: SubModelItem[] = []; const exclude: (string | ModelConstructor)[] = []; for (const subModelBaseClass of subModelBaseClasses) { const BaseClass = typeof subModelBaseClass === 'string' ? ctx.engine.getModelClass(subModelBaseClass) : subModelBaseClass; if (!BaseClass) { continue; } let children = buildSubModelChildren(BaseClass, ctx); if (!children) { children = await buildSubModelItems(subModelBaseClass, exclude)(ctx); } exclude.push(BaseClass); // 若 children 为函数,则预解析一层以判断是否有子项; // 这样当解析结果为空数组时,可自动跳过(隐藏)该分组。 let hasChildren = false; if (typeof children === 'function') { try { // 兼容签名:我们传入 ctx,但若函数不接收也不会出问题 const resolved = await children(ctx as FlowModelContext); hasChildren = Array.isArray(resolved) ? resolved.length > 0 : !!resolved; } catch (e) { // 若解析异常,视为无可用子项,跳过该分组,避免空分组 hasChildren = false; } } else if (Array.isArray(children)) { hasChildren = children.length > 0; } else { hasChildren = !!children; } if (!hasChildren) continue; // 优先使用父类的 meta.label;若无则回退到传入的基类字符串 const groupLabel = BaseClass?.meta?.label || (typeof subModelBaseClass === 'string' ? subModelBaseClass : BaseClass.name); const baseKey = typeof subModelBaseClass === 'string' ? subModelBaseClass : BaseClass.name; const menuType = BaseClass?.meta?.menuType || 'group'; const groupSort = BaseClass?.meta?.sort ?? 1000; if (menuType === 'submenu') { // 作为可点击的一级项,展开二级子菜单 items.push({ key: baseKey, label: groupLabel, sort: groupSort, children, }); } else { // 默认作为分组标题,子项平铺显示 items.push({ key: baseKey, type: 'group', label: groupLabel, sort: groupSort, children, }); } } // 基于 meta.sort 对分组进行稳定排序(升序);未指定时默认 1000 return items.sort((a: any, b: any) => (a?.sort ?? 1000) - (b?.sort ?? 1000)); }; } // ==================== Field-driven children builders ==================== export interface BuildFieldChildrenOptions { useModel: string; fieldUseModel?: string | ((field: any) => string); collection?: Collection; associationPathName?: string; /** * 点击这些子项后,除自身路径外,还需要联动刷新的其他菜单路径前缀 */ refreshTargets?: string[]; } export function buildWrapperFieldChildren(ctx: FlowModelContext, options: BuildFieldChildrenOptions) { const { useModel, fieldUseModel, associationPathName, refreshTargets } = options; const collection: Collection = options.collection || ctx.model['collection'] || ctx.collection; const fields = collection.getFields(); const defaultItemKeys = ['fieldSettings', 'init']; const children: SubModelItem[] = []; for (const f of fields) { if (!f?.options?.interface) continue; const fieldPath = associationPathName ? `${associationPathName}.${f.name}` : f.name; const childUse = typeof fieldUseModel === 'function' ? fieldUseModel(f) : fieldUseModel ?? 'FieldModel'; if (childUse) { const stepPayload = { dataSourceKey: collection.dataSourceKey, collectionName: collection.name, fieldPath: f.name, associationPathName, }; children.push({ key: fieldPath, label: f.title, toggleable: (subModel) => { const { associationPathName, fieldPath: fieldName } = subModel.getStepParams('fieldSettings', 'init') || {}; return (associationPathName ? `${associationPathName}.${fieldName}` : fieldName) === fieldPath; }, useModel: useModel, refreshTargets: refreshTargets, createModelOptions: () => ({ use: useModel, stepParams: { fieldSettings: { init: stepPayload, }, }, subModels: { field: { use: childUse, stepParams: { fieldSettings: { init: stepPayload, }, }, }, }, }), }); } } const groupKey = `addField_${collection.name}`; const finalSearchPlaceholder = ctx.t('Search fields'); return [ { key: groupKey, label: '', // 这个是为了搜索框的占位group, 如果写入内容,会导致出现两层group labels, 本问题的本质是 subModelBaseClass 构建的goup没地方指定是否允许搜索 type: 'group' as const, searchable: true, searchPlaceholder: finalSearchPlaceholder, children: children.filter(Boolean), }, ]; }