UNPKG

@nocobase/flow-engine

Version:

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

158 lines (146 loc) 5.74 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 type { FlowContext, PropertyMeta, PropertyMetaFactory } from '../flowContext'; import { inferRecordRef, buildRecordMeta } from '../utils/variablesParams'; // 判断是否为普通对象(Plain Object),避免对类实例/代理等进行深度遍历 function isPlainObject(val: any) { if (val === null || typeof val !== 'object') return false; const proto = Object.getPrototypeOf(val); return proto === Object.prototype || proto === null; } // 懒加载元信息:仅在展开节点时计算子属性;并加入循环引用防护 function makeMetaFromValue(value: any, title?: string, seen?: WeakSet<any>): any { const t = typeof value; if (value === null || value === undefined) return { type: 'any', title }; if (Array.isArray(value)) { return { type: 'array', title, properties: async () => { const max = Math.min(value.length || 0, 10); const children: Record<string, any> = {}; const nextSeen = seen ?? new WeakSet<any>(); for (let i = 0; i < max; i++) { children[String(i)] = makeMetaFromValue(value[i], `#${i}`, nextSeen); } return children; }, }; } if (t === 'string') return { type: 'string', title }; if (t === 'number') return { type: 'number', title }; if (t === 'boolean') return { type: 'boolean', title }; // 仅对普通对象做懒递归;其它对象(如运行时实例)视为 any,防止进入复杂的循环引用 if (t === 'object' && isPlainObject(value)) { return { type: 'object', title, properties: async () => { const nextSeen = seen ?? new WeakSet<any>(); if (nextSeen.has(value)) { // 循环引用:不再展开子节点 return {}; } nextSeen.add(value); const props: Record<string, any> = {}; Object.keys(value || {}).forEach((k) => { props[k] = makeMetaFromValue(value[k], k, nextSeen); }); return props; }, }; } return { type: 'any', title }; } function buildNavigationMeta(ctx: FlowContext, getView: () => any): any { // 与运行时对象保持一致:无 current,仅暴露 viewStack;并动态展开 viewStack 项 const t = (key: string) => (ctx as any)?.t?.(key) || key; return { type: 'object', title: t('导航'), properties: async () => { const view = getView(); const nav = view?.navigation; const props: Record<string, any> = {}; // 动态 viewStack:按索引展开,并为每个项提供字段 const stack = Array.isArray(nav?.viewStack) ? nav.viewStack : []; const stackMeta: any = { type: 'array', title: t('视图堆栈'), properties: undefined as Record<string, any> | undefined, }; if (stack.length) { const children: Record<string, any> = {}; const max = Math.min(stack.length, 20); for (let i = 0; i < max; i++) { const item = stack[i] || {}; children[String(i)] = { type: 'object', title: `#${i}`, properties: { viewUid: { type: 'string', title: 'viewUid' }, tabUid: { type: 'string', title: 'tabUid' }, filterByTk: { type: 'string', title: 'filterByTk' }, sourceId: { type: 'string', title: 'sourceId' }, }, }; } stackMeta.properties = children; } props.viewStack = stackMeta; return props; }, }; } /** * Create a meta factory for ctx.view that includes: * - buildVariablesParams: { record } via inferRecordRef * - properties.record: full collection meta via buildRecordMeta * - type/preventClose/inputArgs/navigation fields for better variable selection UX */ export function createViewMeta(ctx: FlowContext, getView: () => any): PropertyMetaFactory { const viewTitle = (ctx as any)?.t?.('当前视图') || '当前视图'; const factory: PropertyMetaFactory = async () => { const recordMeta = await buildRecordMeta( () => { try { const ref = inferRecordRef(ctx); if (!ref?.collection) return null; const ds = ctx.dataSourceManager?.getDataSource?.(ref.dataSourceKey || 'main'); return ds?.collectionManager?.getCollection?.(ref.collection) || null; } catch (e) { return null; } }, (ctx as any)?.t?.('当前视图记录') || '当前视图记录', (c) => inferRecordRef(c), ); const view = getView(); return { type: 'object', title: viewTitle, buildVariablesParams: (c) => { const ref = inferRecordRef(c); return ref ? { record: ref } : undefined; }, properties: async () => { const props: Record<string, any> = {}; if (recordMeta) props.record = recordMeta; props.type = { type: 'string', title: (ctx as any)?.t?.('类型') || '类型' }; props.preventClose = { type: 'boolean', title: (ctx as any)?.t?.('是否允许关闭') || '是否允许关闭' }; props.inputArgs = makeMetaFromValue(view?.inputArgs, (ctx as any)?.t?.('输入参数') || '输入参数'); props.navigation = buildNavigationMeta(ctx, getView); return props; }, } as PropertyMeta; }; // 设置工厂函数的 title,让未加载前的占位标题就是“当前视图” factory.title = viewTitle; return factory; }