mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-07-01 18:52:20 +08:00
939 lines
31 KiB
TypeScript
939 lines
31 KiB
TypeScript
/**
|
||
* 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 { action, autorun, define, observable, observe } from '@formily/reactive';
|
||
import _ from 'lodash';
|
||
import React from 'react';
|
||
import { uid } from 'uid/secure';
|
||
import { openRequiredParamsStepFormDialog as openRequiredParamsStepFormDialogFn } from '../components/settings/wrappers/contextual/StepRequiredSettingsDialog';
|
||
import { openStepSettingsDialog as openStepSettingsDialogFn } from '../components/settings/wrappers/contextual/StepSettingsDialog';
|
||
import { Emitter } from '../emitter';
|
||
import { FlowEngine } from '../flowEngine';
|
||
import type {
|
||
ArrayElementType,
|
||
CreateModelOptions,
|
||
CreateSubModelOptions,
|
||
DefaultStructure,
|
||
FlowContext,
|
||
FlowDefinition,
|
||
FlowModelMeta,
|
||
FlowModelOptions,
|
||
ParentFlowModel,
|
||
StepDefinition,
|
||
StepParams,
|
||
} from '../types';
|
||
import { ExtendedFlowDefinition, FlowExtraContext, IModelComponentProps, ReadonlyModelProps } from '../types';
|
||
import { FlowExitException, generateUid, mergeFlowDefinitions, resolveDefaultParams } from '../utils';
|
||
import { ForkFlowModel } from './forkFlowModel';
|
||
|
||
// 使用WeakMap存储每个类的meta
|
||
const modelMetas = new WeakMap<typeof FlowModel, FlowModelMeta>();
|
||
|
||
// 使用WeakMap存储每个类的flows
|
||
const modelFlows = new WeakMap<typeof FlowModel, Map<string, FlowDefinition>>();
|
||
|
||
export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
|
||
public readonly uid: string;
|
||
public sortIndex: number;
|
||
public props: IModelComponentProps = {};
|
||
public stepParams: StepParams = {};
|
||
public flowEngine: FlowEngine;
|
||
public parent: ParentFlowModel<Structure>;
|
||
public subModels: Structure['subModels'];
|
||
private _options: FlowModelOptions<Structure>;
|
||
|
||
/**
|
||
* 所有 fork 实例的引用集合。
|
||
* 使用 Set 便于在销毁时主动遍历并调用 dispose,避免悬挂引用。
|
||
*/
|
||
public forks: Set<ForkFlowModel<any>> = new Set();
|
||
public emitter: Emitter = new Emitter();
|
||
|
||
/**
|
||
* 基于 key 的 fork 实例缓存,用于复用 fork 实例
|
||
*/
|
||
private forkCache: Map<string, ForkFlowModel<any>> = new Map();
|
||
|
||
/**
|
||
* model 树的共享运行上下文
|
||
*/
|
||
protected _sharedContext: Record<string, any> = {};
|
||
|
||
/**
|
||
* 上一次 applyAutoFlows 的执行参数
|
||
*/
|
||
private _lastAutoRunParams: any[] | null = null;
|
||
private observerDispose: () => void;
|
||
|
||
constructor(options: FlowModelOptions<Structure>) {
|
||
if (!options.flowEngine) {
|
||
throw new Error('FlowModel must be initialized with a FlowEngine instance.');
|
||
}
|
||
this.flowEngine = options.flowEngine;
|
||
if (this.flowEngine.getModel(options.uid)) {
|
||
// 此时 new FlowModel 并不创建新实例,而是返回已存在的实例,避免重复创建同一个model实例
|
||
return this.flowEngine.getModel(options.uid);
|
||
}
|
||
|
||
if (!options.uid) {
|
||
options.uid = uid();
|
||
}
|
||
|
||
this.uid = options.uid;
|
||
this.props = options.props || {};
|
||
this.stepParams = options.stepParams || {};
|
||
this.subModels = {};
|
||
this.sortIndex = options.sortIndex || 0;
|
||
this._options = options;
|
||
|
||
define(this, {
|
||
props: observable,
|
||
subModels: observable.shallow,
|
||
stepParams: observable,
|
||
setProps: action,
|
||
setStepParams: action,
|
||
});
|
||
// 保证onInit在所有属性都定义完成后调用
|
||
// queueMicrotask(() => {
|
||
// this.onInit(options);
|
||
// });
|
||
this.createSubModels(options.subModels);
|
||
|
||
this.observerDispose = observe(this.stepParams, () => {
|
||
if (this.flowEngine) {
|
||
this.clearAutoFlowCache();
|
||
}
|
||
this._rerunLastAutoRun();
|
||
});
|
||
}
|
||
|
||
on(eventName: string, listener: (...args: any[]) => void) {
|
||
this.emitter.on(eventName, listener);
|
||
}
|
||
|
||
onInit(options) {}
|
||
|
||
get async() {
|
||
return this._options.async || false;
|
||
}
|
||
|
||
get subKey() {
|
||
return this._options.subKey;
|
||
}
|
||
|
||
get reactView() {
|
||
return this.flowEngine.reactView;
|
||
}
|
||
|
||
static get meta() {
|
||
return modelMetas.get(this);
|
||
}
|
||
|
||
private createSubModels(subModels: Record<string, CreateSubModelOptions | CreateSubModelOptions[]>) {
|
||
Object.entries(subModels || {}).forEach(([key, value]) => {
|
||
if (Array.isArray(value)) {
|
||
value
|
||
.sort((a, b) => (a.sortIndex || 0) - (b.sortIndex || 0))
|
||
.forEach((item) => {
|
||
this.addSubModel(key, item);
|
||
});
|
||
} else {
|
||
this.setSubModel(key, value);
|
||
}
|
||
});
|
||
}
|
||
|
||
private clearAutoFlowCache() {
|
||
if (this.flowEngine) {
|
||
const cacheKey = FlowEngine.generateApplyFlowCacheKey('autoFlow', 'all', this.uid);
|
||
this.flowEngine.applyFlowCache.delete(cacheKey);
|
||
this.forks.forEach((fork) => {
|
||
const forkCacheKey = FlowEngine.generateApplyFlowCacheKey(`${fork['forkId']}`, 'all', this.uid);
|
||
this.flowEngine.applyFlowCache.delete(forkCacheKey);
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 设置FlowEngine实例
|
||
* @param {FlowEngine} flowEngine FlowEngine实例
|
||
*/
|
||
setFlowEngine(flowEngine: FlowEngine): void {
|
||
// this.flowEngine = flowEngine;
|
||
}
|
||
|
||
static define(meta: FlowModelMeta) {
|
||
modelMetas.set(this, meta);
|
||
}
|
||
|
||
/**
|
||
* 注册一个流程 (Flow)。支持泛型,能够正确推导出模型类型。
|
||
* @template TModel 具体的FlowModel子类类型
|
||
* @param {string | FlowDefinition<TModel>} keyOrDefinition 流程的 Key 或 FlowDefinition 对象。
|
||
* 如果为字符串,则为流程 Key,需要配合 flowDefinition 参数。
|
||
* 如果为对象,则为包含 key 属性的完整 FlowDefinition。
|
||
* @param {FlowDefinition<TModel>} [flowDefinition] 当第一个参数为流程 Key 时,此参数为流程的定义。
|
||
* @returns {void}
|
||
*/
|
||
public static registerFlow<TModel extends new (...args: any[]) => FlowModel<any>>(
|
||
this: TModel,
|
||
keyOrDefinition: string | FlowDefinition<InstanceType<TModel>>,
|
||
flowDefinition?: Omit<FlowDefinition<InstanceType<TModel>>, 'key'> & { key?: string },
|
||
): void {
|
||
let definition: FlowDefinition<InstanceType<TModel>>;
|
||
let key: string;
|
||
|
||
if (typeof keyOrDefinition === 'string' && flowDefinition) {
|
||
key = keyOrDefinition;
|
||
definition = {
|
||
...flowDefinition,
|
||
key,
|
||
};
|
||
} else if (typeof keyOrDefinition === 'object' && 'key' in keyOrDefinition) {
|
||
key = keyOrDefinition.key;
|
||
definition = keyOrDefinition;
|
||
} else {
|
||
throw new Error('Invalid arguments for registerFlow');
|
||
}
|
||
|
||
// 确保当前类有自己的flows Map
|
||
const ModelClass = this;
|
||
if (!modelFlows.has(ModelClass as any)) {
|
||
modelFlows.set(ModelClass as any, new Map<string, FlowDefinition>());
|
||
}
|
||
const flows = modelFlows.get(ModelClass as any)!;
|
||
|
||
if (flows.has(key)) {
|
||
console.warn(`FlowModel: Flow with key '${key}' is already registered and will be overwritten.`);
|
||
}
|
||
flows.set(key, definition as FlowDefinition);
|
||
}
|
||
|
||
/**
|
||
* 扩展已存在的流程定义。通过合并现有流程和扩展定义来创建新的流程。
|
||
* @template TModel 具体的FlowModel子类类型
|
||
* @param {string | ExtendedFlowDefinition} keyOrDefinition 流程的 Key 或 ExtendedFlowDefinition 对象。
|
||
* 如果为字符串,则为流程 Key,需要配合 extendDefinition 参数。
|
||
* 如果为对象,则为包含 key 属性的完整 ExtendedFlowDefinition。
|
||
* @param {Omit<ExtendedFlowDefinition, 'key'>} [extendDefinition] 当第一个参数为流程 Key 时,此参数为流程的扩展定义。
|
||
* @returns {void}
|
||
*/
|
||
public static extendFlow<TModel extends FlowModel = FlowModel>(
|
||
keyOrDefinition: string | ExtendedFlowDefinition,
|
||
extendDefinition?: Omit<ExtendedFlowDefinition, 'key'>,
|
||
): void {
|
||
let definition: ExtendedFlowDefinition;
|
||
let key: string;
|
||
|
||
if (typeof keyOrDefinition === 'string' && extendDefinition) {
|
||
key = keyOrDefinition;
|
||
definition = {
|
||
...extendDefinition,
|
||
key,
|
||
};
|
||
} else if (typeof keyOrDefinition === 'object' && 'key' in keyOrDefinition) {
|
||
key = keyOrDefinition.key;
|
||
definition = keyOrDefinition;
|
||
} else {
|
||
throw new Error('Invalid arguments for extendFlow');
|
||
}
|
||
|
||
// 获取所有流程(包括从父类继承的)
|
||
const allFlows = this.getFlows();
|
||
const originalFlow = allFlows.get(key);
|
||
|
||
if (!originalFlow) {
|
||
console.warn(
|
||
`FlowModel.extendFlow: Cannot extend flow '${key}' as it does not exist in parent class. Registering as new flow.`,
|
||
);
|
||
// 移除patch标记,作为新流程注册
|
||
const { patch, ...newFlowDef } = definition;
|
||
this.registerFlow(newFlowDef as FlowDefinition<TModel>);
|
||
return;
|
||
}
|
||
|
||
// 合并流程定义
|
||
const mergedFlow = mergeFlowDefinitions(originalFlow, definition);
|
||
|
||
// 注册合并后的流程
|
||
this.registerFlow(mergedFlow as FlowDefinition<TModel>);
|
||
}
|
||
|
||
/**
|
||
* 获取已注册的流程定义。
|
||
* 如果当前类不存在对应的flow,会继续往父类查找。
|
||
* @param {string} key 流程 Key。
|
||
* @returns {FlowDefinition | undefined} 流程定义,如果未找到则返回 undefined。
|
||
*/
|
||
public getFlow(key: string): FlowDefinition | undefined {
|
||
// 获取当前类的构造函数
|
||
const currentClass = this.constructor as typeof FlowModel;
|
||
|
||
// 遍历类继承链,查找流程
|
||
let cls: typeof FlowModel | null = currentClass;
|
||
while (cls) {
|
||
const flows = modelFlows.get(cls);
|
||
if (flows && flows.has(key)) {
|
||
return flows.get(key);
|
||
}
|
||
|
||
// 获取父类
|
||
const proto = Object.getPrototypeOf(cls);
|
||
if (proto === Function.prototype || proto === Object.prototype) {
|
||
break;
|
||
}
|
||
cls = proto as typeof FlowModel;
|
||
}
|
||
|
||
return undefined;
|
||
}
|
||
|
||
/**
|
||
* 获取所有已注册的流程定义,包括从父类继承的流程。
|
||
* @returns {Map<string, FlowDefinition>} 一个包含所有流程定义的 Map 对象,Key 为流程 Key,Value 为流程定义。
|
||
*/
|
||
public static getFlows(): Map<string, FlowDefinition> {
|
||
// 创建一个新的Map来存储所有流程
|
||
const allFlows = new Map<string, FlowDefinition>();
|
||
|
||
// 遍历类继承链,收集所有流程
|
||
let cls: typeof FlowModel | null = this;
|
||
while (cls) {
|
||
const flows = modelFlows.get(cls);
|
||
if (flows) {
|
||
// 合并流程,但如果已有同名流程则不覆盖
|
||
for (const [key, flow] of flows.entries()) {
|
||
if (!allFlows.has(key)) {
|
||
allFlows.set(key, flow);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 获取父类
|
||
const proto = Object.getPrototypeOf(cls);
|
||
if (proto === Function.prototype || proto === Object.prototype) {
|
||
break;
|
||
}
|
||
cls = proto as typeof FlowModel;
|
||
}
|
||
|
||
return allFlows;
|
||
}
|
||
|
||
setProps(props: IModelComponentProps): void;
|
||
setProps(key: string, value: any): void;
|
||
setProps(props: IModelComponentProps | string, value?: any): void {
|
||
if (typeof props === 'string') {
|
||
this.props[props] = value;
|
||
} else {
|
||
this.props = { ...this.props, ...props };
|
||
}
|
||
}
|
||
|
||
getProps(): ReadonlyModelProps {
|
||
return this.props as ReadonlyModelProps;
|
||
}
|
||
|
||
setStepParams(flowKey: string, stepKey: string, params: any): void;
|
||
setStepParams(flowKey: string, stepParams: Record<string, any>): void;
|
||
setStepParams(allParams: StepParams): void;
|
||
setStepParams(
|
||
flowKeyOrAllParams: string | StepParams,
|
||
stepKeyOrStepsParams?: string | Record<string, any>,
|
||
params?: any,
|
||
): void {
|
||
if (typeof flowKeyOrAllParams === 'string') {
|
||
const flowKey = flowKeyOrAllParams;
|
||
if (typeof stepKeyOrStepsParams === 'string' && params !== undefined) {
|
||
if (!this.stepParams[flowKey]) {
|
||
this.stepParams[flowKey] = {};
|
||
}
|
||
this.stepParams[flowKey][stepKeyOrStepsParams] = {
|
||
...this.stepParams[flowKey][stepKeyOrStepsParams],
|
||
...params,
|
||
};
|
||
} else if (typeof stepKeyOrStepsParams === 'object' && stepKeyOrStepsParams !== null) {
|
||
this.stepParams[flowKey] = { ...(this.stepParams[flowKey] || {}), ...stepKeyOrStepsParams };
|
||
}
|
||
} else if (typeof flowKeyOrAllParams === 'object' && flowKeyOrAllParams !== null) {
|
||
for (const fk in flowKeyOrAllParams) {
|
||
if (Object.prototype.hasOwnProperty.call(flowKeyOrAllParams, fk)) {
|
||
this.stepParams[fk] = { ...(this.stepParams[fk] || {}), ...flowKeyOrAllParams[fk] };
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
getStepParams(flowKey: string, stepKey: string): any | undefined;
|
||
getStepParams(flowKey: string): Record<string, any> | undefined;
|
||
getStepParams(): StepParams;
|
||
getStepParams(flowKey?: string, stepKey?: string): any {
|
||
if (flowKey && stepKey) {
|
||
return this.stepParams[flowKey]?.[stepKey];
|
||
}
|
||
if (flowKey) {
|
||
return this.stepParams[flowKey];
|
||
}
|
||
return this.stepParams;
|
||
}
|
||
|
||
async applyFlow(flowKey: string, extra?: FlowExtraContext): Promise<any> {
|
||
const currentFlowEngine = this.flowEngine;
|
||
if (!currentFlowEngine) {
|
||
console.warn('FlowEngine not available on this model for applyFlow. Check and model.flowEngine setup.');
|
||
return Promise.reject(new Error('FlowEngine not available for applyFlow. Please set flowEngine on the model.'));
|
||
}
|
||
|
||
const flow = this.getFlow(flowKey);
|
||
|
||
if (!flow) {
|
||
console.error(`BaseModel.applyFlow: Flow with key '${flowKey}' not found.`);
|
||
return Promise.reject(new Error(`Flow '${flowKey}' not found.`));
|
||
}
|
||
|
||
let lastResult: any;
|
||
const stepResults: Record<string, any> = {};
|
||
|
||
// Create a new FlowContext instance for this flow execution
|
||
const createLogger = (level: string) => (message: string, meta?: any) => {
|
||
const logMessage = `[${level.toUpperCase()}] [Flow: ${flowKey}] [Model: ${this.uid}] ${message}`;
|
||
const logMeta = { flowKey, modelUid: this.uid, ...meta };
|
||
console[level.toLowerCase()](logMessage, logMeta);
|
||
};
|
||
|
||
const globalContexts = currentFlowEngine.getContext();
|
||
const flowContext: FlowContext<this> = {
|
||
exit: () => {
|
||
throw new FlowExitException(flowKey, this.uid);
|
||
},
|
||
logger: {
|
||
info: createLogger('INFO'),
|
||
warn: createLogger('WARN'),
|
||
error: createLogger('ERROR'),
|
||
debug: createLogger('DEBUG'),
|
||
},
|
||
reactView: this.reactView,
|
||
stepResults,
|
||
shared: this.getSharedContext(),
|
||
globals: globalContexts,
|
||
extra: extra || {},
|
||
model: this,
|
||
app: globalContexts.app || {},
|
||
};
|
||
|
||
for (const stepKey in flow.steps) {
|
||
if (Object.prototype.hasOwnProperty.call(flow.steps, stepKey)) {
|
||
const step: StepDefinition = flow.steps[stepKey];
|
||
let handler: ((ctx: FlowContext<this>, params: any) => Promise<any> | any) | undefined;
|
||
let combinedParams: Record<string, any> = {};
|
||
let actionDefinition;
|
||
|
||
if (step.use) {
|
||
// Step references a registered action
|
||
actionDefinition = currentFlowEngine.getAction(step.use);
|
||
if (!actionDefinition) {
|
||
console.error(
|
||
`BaseModel.applyFlow: Action '${step.use}' not found for step '${stepKey}' in flow '${flowKey}'. Skipping.`,
|
||
);
|
||
continue;
|
||
}
|
||
// Use step's handler if provided, otherwise use action's handler
|
||
handler = step.handler || actionDefinition.handler;
|
||
// Merge default params: action defaults first, then step defaults
|
||
const actionDefaultParams = await resolveDefaultParams(actionDefinition.defaultParams, flowContext);
|
||
const stepDefaultParams = await resolveDefaultParams(step.defaultParams, flowContext);
|
||
combinedParams = { ...actionDefaultParams, ...stepDefaultParams };
|
||
} else if (step.handler) {
|
||
// Step defines its own inline handler
|
||
handler = step.handler;
|
||
const stepDefaultParams = await resolveDefaultParams(step.defaultParams, flowContext);
|
||
combinedParams = { ...stepDefaultParams };
|
||
} else {
|
||
console.error(
|
||
`BaseModel.applyFlow: Step '${stepKey}' in flow '${flowKey}' has neither 'use' nor 'handler'. Skipping.`,
|
||
);
|
||
continue;
|
||
}
|
||
|
||
const modelStepParams = this.getStepParams(flowKey, stepKey);
|
||
if (modelStepParams !== undefined) {
|
||
combinedParams = { ...combinedParams, ...modelStepParams };
|
||
}
|
||
|
||
try {
|
||
const currentStepResult = handler!(flowContext, combinedParams);
|
||
if (step.isAwait !== false) {
|
||
lastResult = await currentStepResult;
|
||
} else {
|
||
lastResult = currentStepResult;
|
||
}
|
||
|
||
// Store step result
|
||
stepResults[stepKey] = lastResult;
|
||
} catch (error) {
|
||
// 检查是否是通过 ctx.exit() 正常退出
|
||
if (error instanceof FlowExitException) {
|
||
console.log(`[FlowEngine] ${error.message}`);
|
||
return Promise.resolve(stepResults);
|
||
}
|
||
|
||
console.error(`BaseModel.applyFlow: Error executing step '${stepKey}' in flow '${flowKey}':`, error);
|
||
return Promise.reject(error);
|
||
}
|
||
}
|
||
}
|
||
return Promise.resolve(stepResults);
|
||
}
|
||
|
||
dispatchEvent(eventName: string, extra?: FlowExtraContext): void {
|
||
const currentFlowEngine = this.flowEngine;
|
||
if (!currentFlowEngine) {
|
||
console.warn('FlowEngine not available on this model for dispatchEvent. Please set flowEngine on the model.');
|
||
return;
|
||
}
|
||
|
||
// 获取所有流程
|
||
const constructor = this.constructor as typeof FlowModel;
|
||
const allFlows = constructor.getFlows();
|
||
|
||
allFlows.forEach((flow) => {
|
||
if (flow.on && flow.on.eventName === eventName) {
|
||
console.log(`BaseModel '${this.uid}' dispatching event '${eventName}' to flow '${flow.key}'.`);
|
||
this.applyFlow(flow.key, extra).catch((error) => {
|
||
console.error(
|
||
`BaseModel.dispatchEvent: Error executing event-triggered flow '${flow.key}' for event '${eventName}':`,
|
||
error,
|
||
);
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 创建一个新的 FlowModel 子类,并预注册指定的流程。
|
||
* @param {ExtendedFlowDefinition[]} flows 要预注册的流程定义数组
|
||
* 如果flow.patch为true,则表示这是对父类同名流程的部分覆盖
|
||
* @returns 新创建的 FlowModel 子类
|
||
*/
|
||
public static extends<T extends typeof FlowModel>(this: T, flows: ExtendedFlowDefinition[] = []): T {
|
||
class CustomFlowModel extends (this as unknown as typeof FlowModel) {
|
||
// @ts-ignore
|
||
static name = `CustomFlowModel_${generateUid()}`;
|
||
}
|
||
|
||
// 处理流程注册和覆盖
|
||
if (flows.length > 0) {
|
||
flows.forEach((flowDefinition) => {
|
||
// 如果标记为部分覆盖,则调用extendFlow方法
|
||
if (flowDefinition.patch === true) {
|
||
CustomFlowModel.extendFlow(flowDefinition);
|
||
} else {
|
||
// 完全覆盖或新增流程
|
||
CustomFlowModel.registerFlow(flowDefinition as FlowDefinition);
|
||
}
|
||
});
|
||
}
|
||
|
||
return CustomFlowModel as unknown as T;
|
||
}
|
||
|
||
/**
|
||
* 获取所有自动应用流程定义并按 sort 排序
|
||
* @returns {FlowDefinition[]} 按 sort 排序的自动应用流程定义数组
|
||
*/
|
||
public getAutoFlows(): FlowDefinition[] {
|
||
const constructor = this.constructor as typeof FlowModel;
|
||
const allFlows = constructor.getFlows();
|
||
|
||
// 过滤出自动流程并按 sort 排序
|
||
const autoFlows = Array.from(allFlows.values())
|
||
.filter((flow) => flow.auto === true)
|
||
.sort((a, b) => (a.sort || 0) - (b.sort || 0));
|
||
|
||
return autoFlows;
|
||
}
|
||
|
||
/**
|
||
* 重新执行上一次的 applyAutoFlows,保持参数一致
|
||
* 如果之前没有执行过,则直接跳过
|
||
* 使用 lodash debounce 避免频繁调用
|
||
*/
|
||
private _rerunLastAutoRun = _.debounce(async () => {
|
||
if (this._lastAutoRunParams) {
|
||
try {
|
||
await this.applyAutoFlows(...this._lastAutoRunParams);
|
||
} catch (error) {
|
||
console.error('FlowModel._rerunLastAutoRun: Error during rerun:', error);
|
||
}
|
||
}
|
||
}, 100);
|
||
|
||
/**
|
||
* 执行所有自动应用流程
|
||
* @param {FlowExtraContext} [extra] 可选的额外上下文
|
||
* @param {boolean} [useCache=true] 是否使用缓存机制,默认为 true
|
||
* @returns {Promise<any[]>} 所有自动应用流程的执行结果数组
|
||
*/
|
||
async applyAutoFlows(extra?: FlowExtraContext, useCache?: boolean): Promise<any[]>;
|
||
async applyAutoFlows(...args: any[]): Promise<any[]> {
|
||
const [extra, useCache = true] = args;
|
||
// 生成缓存键,包含 stepParams 的序列化版本以确保参数变化时重新执行
|
||
const cacheKey = useCache
|
||
? FlowEngine.generateApplyFlowCacheKey(this['forkId'] ?? 'autoFlow', 'all', this.uid)
|
||
: null;
|
||
|
||
if (!_.isEqual(extra, this._lastAutoRunParams?.[0]) && cacheKey) {
|
||
this.flowEngine.applyFlowCache.delete(cacheKey);
|
||
}
|
||
|
||
// 存储本次执行的参数,用于后续重新执行
|
||
this._lastAutoRunParams = args;
|
||
|
||
const autoApplyFlows = this.getAutoFlows();
|
||
|
||
if (autoApplyFlows.length === 0) {
|
||
console.warn(`FlowModel: No auto-apply flows found for model '${this.uid}'`);
|
||
return [];
|
||
}
|
||
|
||
// 检查缓存
|
||
if (cacheKey && this.flowEngine) {
|
||
const cachedEntry = this.flowEngine.applyFlowCache.get(cacheKey);
|
||
if (cachedEntry) {
|
||
if (cachedEntry.status === 'resolved') {
|
||
console.log(`[FlowEngine.applyAutoFlows] Using cached result for model: ${this.uid}`);
|
||
return cachedEntry.data;
|
||
}
|
||
if (cachedEntry.status === 'rejected') throw cachedEntry.error;
|
||
if (cachedEntry.status === 'pending') return await cachedEntry.promise;
|
||
}
|
||
}
|
||
|
||
// 执行 autoFlows
|
||
const executeAutoFlows = async (): Promise<any[]> => {
|
||
const results: any[] = [];
|
||
for (const flow of autoApplyFlows) {
|
||
try {
|
||
const result = await this.applyFlow(flow.key, extra);
|
||
results.push(result);
|
||
} catch (error) {
|
||
console.error(`FlowModel.applyAutoFlows: Error executing auto-apply flow '${flow.key}':`, error);
|
||
throw error;
|
||
}
|
||
}
|
||
return results;
|
||
};
|
||
|
||
// 如果不使用缓存,直接执行
|
||
if (!cacheKey || !this.flowEngine) {
|
||
return await executeAutoFlows();
|
||
}
|
||
|
||
// 使用缓存机制
|
||
const promise = executeAutoFlows()
|
||
.then((result) => {
|
||
this.flowEngine.applyFlowCache.set(cacheKey, {
|
||
status: 'resolved',
|
||
data: result,
|
||
promise: Promise.resolve(result),
|
||
});
|
||
return result;
|
||
})
|
||
.catch((err) => {
|
||
this.flowEngine.applyFlowCache.set(cacheKey, {
|
||
status: 'rejected',
|
||
error: err,
|
||
promise: Promise.reject(err),
|
||
});
|
||
throw err;
|
||
});
|
||
|
||
this.flowEngine.applyFlowCache.set(cacheKey, { status: 'pending', promise });
|
||
return await promise;
|
||
}
|
||
|
||
/**
|
||
* Renders the React representation of this flow model.
|
||
* @returns {React.ReactNode} The React node to render.
|
||
*/
|
||
public render(): any {
|
||
// console.warn('FlowModel.render() not implemented. Override in subclass for FlowModelRenderer.');
|
||
// 默认返回一个空的div,子类可以覆盖这个方法来实现具体的渲染逻辑
|
||
return <div {...this.props}></div>;
|
||
}
|
||
|
||
async rerender() {
|
||
await this.applyAutoFlows(this._lastAutoRunParams?.[0], false);
|
||
}
|
||
|
||
setParent(parent: FlowModel): void {
|
||
if (!parent || !(parent instanceof FlowModel)) {
|
||
throw new Error('Parent must be an instance of FlowModel.');
|
||
}
|
||
this.parent = parent as ParentFlowModel<Structure>;
|
||
this._options.parentId = parent.uid;
|
||
}
|
||
|
||
addSubModel(subKey: string, options: CreateModelOptions | FlowModel) {
|
||
let model: FlowModel;
|
||
if (options instanceof FlowModel) {
|
||
if (options.parent && options.parent !== this) {
|
||
throw new Error('Sub model already has a parent.');
|
||
}
|
||
model = options;
|
||
} else {
|
||
model = this.flowEngine.createModel({ ...options, subKey, subType: 'array' });
|
||
}
|
||
model.setParent(this);
|
||
const subModels = this.subModels as {
|
||
[subKey: string]: FlowModel[];
|
||
};
|
||
if (!Array.isArray(subModels[subKey])) {
|
||
subModels[subKey] = observable.shallow([]);
|
||
}
|
||
const maxSortIndex = Math.max(...(subModels[subKey] as FlowModel[]).map((item) => item.sortIndex || 0), 0);
|
||
model.sortIndex = maxSortIndex + 1;
|
||
subModels[subKey].push(model);
|
||
this.emitter.emit('onSubModelAdded', model);
|
||
return model;
|
||
}
|
||
|
||
setSubModel(subKey: string, options: CreateModelOptions | FlowModel) {
|
||
let model: FlowModel;
|
||
if (options instanceof FlowModel) {
|
||
if (options.parent && options.parent !== this) {
|
||
throw new Error('Sub model already has a parent.');
|
||
}
|
||
model = options;
|
||
} else {
|
||
model = this.flowEngine.createModel({ ...options, parentId: this.uid, subKey, subType: 'object' });
|
||
}
|
||
model.setParent(this);
|
||
(this.subModels as any)[subKey] = model;
|
||
this.emitter.emit('onSubModelAdded', model);
|
||
return model;
|
||
}
|
||
|
||
mapSubModels<K extends keyof Structure['subModels'], R>(
|
||
subKey: K,
|
||
callback: (model: ArrayElementType<Structure['subModels'][K]>, index: number) => R,
|
||
): R[] {
|
||
const model = (this.subModels as any)[subKey as string];
|
||
|
||
if (!model) {
|
||
return [];
|
||
}
|
||
|
||
const results: R[] = [];
|
||
|
||
_.castArray(model).forEach((item, index) => {
|
||
const result = (callback as (model: any, index: number) => R)(item, index);
|
||
results.push(result);
|
||
});
|
||
|
||
return results;
|
||
}
|
||
|
||
findSubModel<K extends keyof Structure['subModels'], R>(
|
||
subKey: K,
|
||
callback: (model: ArrayElementType<Structure['subModels'][K]>) => R,
|
||
): ArrayElementType<Structure['subModels'][K]> | null {
|
||
const model = (this.subModels as any)[subKey as string];
|
||
|
||
if (!model) {
|
||
return null;
|
||
}
|
||
|
||
return (
|
||
(_.castArray(model).find((item) => {
|
||
return (callback as (model: any) => R)(item);
|
||
}) as ArrayElementType<Structure['subModels'][K]> | undefined) || null
|
||
);
|
||
}
|
||
|
||
createRootModel(options) {
|
||
return this.flowEngine.createModel(options);
|
||
}
|
||
|
||
async applySubModelsAutoFlows<K extends keyof Structure['subModels']>(
|
||
subKey: K,
|
||
extra?: Record<string, any>,
|
||
shared?: Record<string, any>,
|
||
) {
|
||
await Promise.all(
|
||
this.mapSubModels(subKey, async (sub) => {
|
||
sub.setSharedContext(shared);
|
||
await sub.applyAutoFlows(extra);
|
||
}),
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 创建一个 fork 实例,实现"一份数据(master)多视图(fork)"的能力。
|
||
* @param {IModelComponentProps} [localProps={}] fork 专属的局部 props,优先级高于 master.props
|
||
* @param {string} [key] 可选的 key,用于复用 fork 实例。如果提供了 key,会尝试复用已存在的 fork
|
||
* @returns {ForkFlowModel<this>} 创建的 fork 实例
|
||
*/
|
||
createFork(localProps?: IModelComponentProps, key?: string): ForkFlowModel<this> {
|
||
// 如果提供了 key,尝试从缓存中获取
|
||
if (key) {
|
||
const cachedFork = this.forkCache.get(key);
|
||
if (cachedFork && !(cachedFork as any).disposed) {
|
||
// 更新 localProps
|
||
cachedFork.setProps(localProps || {});
|
||
return cachedFork as ForkFlowModel<this>;
|
||
}
|
||
}
|
||
|
||
// 创建新的 fork 实例
|
||
const forkId = this.forks.size; // 当前集合大小作为索引
|
||
const fork = new ForkFlowModel<this>(this as any, localProps, forkId);
|
||
this.forks.add(fork as any);
|
||
|
||
// 如果提供了 key,将 fork 缓存起来
|
||
if (key) {
|
||
this.forkCache.set(key, fork as any);
|
||
}
|
||
|
||
return fork as ForkFlowModel<this>;
|
||
}
|
||
|
||
clearForks() {
|
||
console.log(`FlowModel ${this.uid} clearing all forks.`);
|
||
// 主动使所有 fork 失效
|
||
if (this.forks?.size) {
|
||
this.forks.forEach((fork) => fork.dispose());
|
||
this.forks.clear();
|
||
}
|
||
// 清理 fork 缓存
|
||
this.forkCache.clear();
|
||
}
|
||
|
||
/**
|
||
* 移动当前模型到目标模型的位置
|
||
* @param {FlowModel} targetModel 目标模型
|
||
* @returns {boolean} 是否成功移动
|
||
*/
|
||
moveTo(targetModel: FlowModel) {
|
||
if (!this.flowEngine) {
|
||
throw new Error('FlowEngine is not set on this model. Please set flowEngine before saving.');
|
||
}
|
||
return this.flowEngine.moveModel(this.uid, targetModel.uid);
|
||
}
|
||
|
||
remove() {
|
||
if (!this.flowEngine) {
|
||
throw new Error('FlowEngine is not set on this model. Please set flowEngine before saving.');
|
||
}
|
||
this.observerDispose();
|
||
return this.flowEngine.removeModel(this.uid);
|
||
}
|
||
|
||
async save() {
|
||
if (!this.flowEngine) {
|
||
throw new Error('FlowEngine is not set on this model. Please set flowEngine before saving.');
|
||
}
|
||
return this.flowEngine.saveModel(this);
|
||
}
|
||
|
||
async destroy() {
|
||
if (!this.flowEngine) {
|
||
throw new Error('FlowEngine is not set on this model. Please set flowEngine before deleting.');
|
||
}
|
||
this.observerDispose();
|
||
// 从 FlowEngine 中销毁模型
|
||
return this.flowEngine.destroyModel(this.uid);
|
||
}
|
||
|
||
/**
|
||
* 打开步骤设置对话框
|
||
* 用于配置流程中特定步骤的参数和设置
|
||
* @param {string} flowKey 流程的唯一标识符
|
||
* @param {string} stepKey 步骤的唯一标识符
|
||
* @returns {void}
|
||
*/
|
||
openStepSettingsDialog(flowKey: string, stepKey: string) {
|
||
return openStepSettingsDialogFn({
|
||
model: this,
|
||
flowKey,
|
||
stepKey,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 配置必填步骤参数
|
||
* 用于在一个分步表单中配置所有需要参数的步骤
|
||
* @param {number | string} [dialogWidth=800] 对话框宽度,默认为800
|
||
* @param {string} [dialogTitle='步骤参数配置'] 对话框标题,默认为'步骤参数配置'
|
||
* @returns {Promise<any>} 返回表单提交的值
|
||
*/
|
||
async configureRequiredSteps(dialogWidth?: number | string, dialogTitle?: string) {
|
||
return openRequiredParamsStepFormDialogFn({
|
||
model: this,
|
||
dialogWidth,
|
||
dialogTitle,
|
||
});
|
||
}
|
||
|
||
get ctx() {
|
||
return {
|
||
globals: this.flowEngine.getContext(),
|
||
shared: this.getSharedContext(),
|
||
};
|
||
}
|
||
|
||
get translate() {
|
||
return this.flowEngine.translate.bind(this.flowEngine);
|
||
}
|
||
|
||
public setSharedContext(ctx: Record<string, any>) {
|
||
this._sharedContext = { ...this._sharedContext, ...ctx };
|
||
}
|
||
|
||
public getSharedContext() {
|
||
if (this.async || !this.parent) {
|
||
return this._sharedContext;
|
||
}
|
||
return {
|
||
...this.parent?.getSharedContext(),
|
||
...this._sharedContext, // 当前实例的 context 优先级最高
|
||
};
|
||
}
|
||
|
||
// TODO: 不完整,需要考虑 sub-model 的情况
|
||
serialize(): Record<string, any> {
|
||
const data = {
|
||
uid: this.uid,
|
||
..._.omit(this._options, ['flowEngine']),
|
||
// props: this.props,
|
||
stepParams: this.stepParams,
|
||
sortIndex: this.sortIndex,
|
||
};
|
||
const subModels = this.subModels as {
|
||
[key: string]: FlowModel | FlowModel[];
|
||
};
|
||
for (const subModelKey in subModels) {
|
||
data.subModels = data.subModels || {};
|
||
if (Array.isArray(subModels[subModelKey])) {
|
||
(data.subModels as any)[subModelKey] = (subModels[subModelKey] as FlowModel[]).map((model, index) => ({
|
||
...model.serialize(),
|
||
sortIndex: index,
|
||
}));
|
||
} else if (subModels[subModelKey] instanceof FlowModel) {
|
||
(data.subModels as any)[subModelKey] = (subModels[subModelKey] as FlowModel).serialize();
|
||
}
|
||
}
|
||
return data;
|
||
}
|
||
}
|
||
|
||
export function defineFlow<TModel extends FlowModel = FlowModel>(definition: FlowDefinition): FlowDefinition<TModel> {
|
||
return definition as FlowDefinition<TModel>;
|
||
}
|