diff --git a/packages/core/client/.dumirc.ts b/packages/core/client/.dumirc.ts index e6b60c52035..b9929fee167 100644 --- a/packages/core/client/.dumirc.ts +++ b/packages/core/client/.dumirc.ts @@ -124,10 +124,6 @@ export default defineConfig({ title: 'Fork model', link: '/examples/flow-models/fork-model', }, - // { - // title: 'cleanRun:自动流干净运行', - // link: '/examples/flow-models/render-in-fork', - // }, { title: '拖拽(DND)', link: '/examples/flow-models/dnd', diff --git a/packages/core/client/docs/zh-CN/api/flow-engine/flow-definition.md b/packages/core/client/docs/zh-CN/api/flow-engine/flow-definition.md index a189b325219..6f474aa5317 100644 --- a/packages/core/client/docs/zh-CN/api/flow-engine/flow-definition.md +++ b/packages/core/client/docs/zh-CN/api/flow-engine/flow-definition.md @@ -10,7 +10,9 @@ interface FlowDefinition { key: string; // 流唯一标识 title?: string; // 可选:流显示名称 - // 注意:自动执行策略:未配置 on 且未显式 manual: true 的流,视为自动执行 + // 注意:自动执行策略: + // 1) 未配置 on 且未显式 manual: true 的流,视为自动执行; + // 2) 等价地,你也可以显式声明 on: 'beforeRender' 将其作为“渲染前生命周期事件”流; manual?: boolean; // 可选:是否仅手动执行 sort?: number; // 可选:流执行排序,数字越小越先执行,默认为 0,可为负数 on?: { eventName: string }; // 可选:事件触发配置 @@ -126,7 +128,8 @@ MyFlowModel.registerFlow(myFlow); // 注册流 class MyFlowDefinition implements FlowDefinition { key = 'MyFlowDefinition'; title = '我的复杂流程'; - auto = true; + // 等价于未显式声明 on 且 manual !== true 的“自动流” + on = { eventName: 'beforeRender' }; sort = 0; steps = { @@ -215,7 +218,7 @@ myModel.setStepParams('myFlow', 'step1', { name: '小明' }); ```ts await myModel.applyFlow('myFlow'); // 主动执行指定流 myModel.dispatchEvent('user.created'); // 分发事件触发流(如流配置了 on.eventName) -await myModel.applyAutoFlows(); // 执行所有 auto=true 的流,按 sort 排序 +await myModel.dispatchEvent('beforeRender'); // beforeRender 默认顺序执行并使用缓存(可覆盖) ``` --- diff --git a/packages/core/client/docs/zh-CN/api/flow-engine/flow-model-renderer.md b/packages/core/client/docs/zh-CN/api/flow-engine/flow-model-renderer.md index b7c82485d3d..15eecfd05c9 100644 --- a/packages/core/client/docs/zh-CN/api/flow-engine/flow-model-renderer.md +++ b/packages/core/client/docs/zh-CN/api/flow-engine/flow-model-renderer.md @@ -18,10 +18,7 @@ interface FlowModelRendererProps { /** 是否在设置中隐藏移除按钮 */ hideRemoveInSettings?: boolean; // 默认 false - /** 是否跳过自动应用流,默认 false */ - skipApplyAutoFlows?: boolean; // 默认 false - - /** 当 skipApplyAutoFlows !== false 时,传递给 useApplyAutoFlows 的运行时参数 */ + /** 传递给 beforeRender 事件的运行时参数 */ inputArgs?: Record /** 设置菜单层级:1=仅当前模型(默认),2=包含子模型 */ @@ -43,8 +40,7 @@ interface FlowModelRendererProps { - `modal`: 模态框形式(待实现) - `drawer`: 抽屉形式(待实现) - **hideRemoveInSettings**: 是否在设置中隐藏移除按钮,当设为 `true` 时,流设置菜单中不会显示删除/移除选项 -- **skipApplyAutoFlows**: 是否跳过自动应用流。当设为 `true` 时,组件不会调用 `useApplyAutoFlows` hook -- **inputArgs**: 运行时参数,当 `skipApplyAutoFlows` 为 `false` 时传递给 `useApplyAutoFlows` hook +- **inputArgs**: 运行时参数,传递给 beforeRender 事件 - **settingsMenuLevel**: 设置菜单层级,控制设置菜单的显示范围 - `1`: 仅显示当前模型的设置(默认) - `2`: 包含子模型的设置 @@ -70,10 +66,10 @@ interface FlowModelRendererProps { hideRemoveInSettings={true} /> -// 跳过自动应用流 +// 传递 beforeRender 事件上下文 // 传递额外上下文 @@ -96,7 +92,6 @@ interface FlowModelRendererProps { showFlowSettings={true} flowSettingsVariant="contextMenu" hideRemoveInSettings={false} - skipApplyAutoFlows={false} settingsMenuLevel={2} inputArgs={{ userId: 123, @@ -136,17 +131,7 @@ interface FlowModelRendererProps { /> ``` -### 4. 自定义流控制 -当需要手动控制流应用时,可以跳过自动流: - -```tsx | pure - -``` - -### 5. 传递自定义上下文 +### 4. 传递自定义上下文 当需要向流传递特定上下文数据时: ```tsx | pure @@ -160,7 +145,7 @@ interface FlowModelRendererProps { /> ``` -### 6. 控制设置菜单层级 +### 5. 控制设置菜单层级 当需要控制设置菜单显示范围时,可以通过 `settingsMenuLevel` 参数配置: ```tsx | pure diff --git a/packages/core/client/docs/zh-CN/api/flow-engine/flow-model.md b/packages/core/client/docs/zh-CN/api/flow-engine/flow-model.md index 91f3926ca24..96fa7080467 100644 --- a/packages/core/client/docs/zh-CN/api/flow-engine/flow-model.md +++ b/packages/core/client/docs/zh-CN/api/flow-engine/flow-model.md @@ -75,14 +75,14 @@ - **applyFlow(flowKey: string, inputArgs?: Record): Promise\** 执行指定流。 -- **dispatchEvent(eventName: string, inputArgs?: Record): void** - 触发事件,自动匹配并执行相关流。 +- **dispatchEvent(eventName: string, inputArgs?: Record, options?: { debounce?: boolean; sequential?: boolean; useCache?: boolean }): Promise** + 触发事件并执行匹配的流;支持顺序或并行执行。 + - `sequential`: 是否顺序执行(默认并行);beforeRender 的默认值为顺序执行(可覆盖)。 + - `useCache`: 是否启用事件层面的缓存(默认 `false`);beforeRender 的默认值为启用缓存(可覆盖)。 + - `debounce`: 是否启用防抖(默认 `false`)。 -- **applyAutoFlows(inputArgs?: Record): Promise\** - 执行所有自动流。 - -- **applySubModelsAutoFlows(subKey: string, extra?: Record\): Promise\** - 执行指定子模型的自动流。 +- **applySubModelsBeforeRenderFlows(subKey: string, inputArgs?: Record\): Promise\** + 对指定子模型派发 `beforeRender` 事件(内部顺序执行并使用缓存)。 - **getFlow(key: string): FlowDefinition \| undefined** 获取指定 key 的流配置。 @@ -90,23 +90,22 @@ - **getFlows(): Map\** 获取所有已配置流(含继承)。 -- **getAutoFlows(): FlowDefinition[]** - 获取所有自动应用流程定义并按 sort 排序。 +- **getEventFlows(eventName: string): FlowDefinition[]** + 按事件名获取流程集合并按 sort 排序。 + - 特殊事件 `beforeRender`: 兼容包含“未声明 on 且 manual !== true”的流程。 --- -### 自动流执行生命周期钩子 +### 事件分发钩子 -FlowModel 提供了三个全局自动流执行生命周期钩子,子类可以重写这些方法来实现自定义逻辑: +- **async onDispatchEventStart(eventName: string, options?: DispatchEventOptions, inputArgs?: Record)** + 事件执行前调用;beforeRender 可通过抛出 `FlowExitException` 终止执行。 -- **async onBeforeAutoFlows(inputArgs?: Record): Promise\** - 在所有自动流执行前调用。可以通过抛出 `FlowExitException` 来终止后续流程执行。 +- **async onDispatchEventEnd(eventName: string, options?: DispatchEventOptions, inputArgs?: Record, results?: any[])** + 事件执行后调用;`results` 为按 sort 顺序收集到的步骤结果数组。 -- **async onAfterAutoFlows(results: any[], inputArgs?: Record): Promise\** - 在所有自动流执行后调用。可以访问所有自动流的执行结果。 - -- **async onAutoFlowsError(error: Error, inputArgs?: Record): Promise\** - 在自动流执行或生命周期钩子出错时调用。可进行自定义错误处理。 +- **async onDispatchEventError(eventName: string, options?: DispatchEventOptions, inputArgs?: Record, error?: Error)** + 事件执行或生命周期钩子出错时调用。 --- @@ -207,7 +206,7 @@ await model.save(); // 执行流 await model.applyFlow('default'); -await model.applyAutoFlows(); +await model.dispatchEvent('beforeRender'); await model.dispatchEvent('event'); ``` @@ -241,3 +240,5 @@ interface DefaultStructure { subModels?: Record; } ``` +- **invalidateFlowCache(eventName?: string, deep?: boolean): void** + 失效当前模型指定事件的流程缓存;未指定 `eventName` 时失效当前模型的全部事件缓存。常用:`invalidateFlowCache('beforeRender')`。 diff --git a/packages/core/client/docs/zh-CN/api/flow-engine/flow-sub-model.md b/packages/core/client/docs/zh-CN/api/flow-engine/flow-sub-model.md index bad237bacd3..5f7eecad99f 100644 --- a/packages/core/client/docs/zh-CN/api/flow-engine/flow-sub-model.md +++ b/packages/core/client/docs/zh-CN/api/flow-engine/flow-sub-model.md @@ -92,7 +92,8 @@ model.mapSubModels('tabs', (tab) => { ### 作为属性值使用 ```tsx | pure -await model.applySubModelsAutoFlows(ctx); +// 先让子列模型完成 beforeRender 事件流(如列的可见性、标题、渲染器等) +await model.applySubModelsBeforeRenderFlows('columns'); const columns = model.mapSubModels('columns', (column) => column.getProps()); ``` diff --git a/packages/core/client/docs/zh-CN/core/development/quickstart.md b/packages/core/client/docs/zh-CN/core/development/quickstart.md index 188197efa1f..290fc63683a 100644 --- a/packages/core/client/docs/zh-CN/core/development/quickstart.md +++ b/packages/core/client/docs/zh-CN/core/development/quickstart.md @@ -168,7 +168,7 @@ const fork2 = model.createFork('key2', {}); ### 1. 生命周期逻辑集中且清晰 -FlowModel 拥有多个生命周期钩子(如 `onInit`、`onMount`、`onUnmount`、`onBeforeAutoFlows`、`onAfterAutoFlows` 等),这些逻辑通过类方法更清晰地组织,避免了函数式中多个 Hook 间的复杂依赖。 +FlowModel 拥有多个生命周期钩子(如 `onInit`、`onMount`、`onUnmount`、`onDispatchEventStart/End/Error` 等),这些逻辑通过类方法更清晰地组织,避免了函数式中多个 Hook 间的复杂依赖。对于渲染前的自动执行逻辑,统一通过 `onDispatchEvent*` 钩子并判断 `eventName === 'beforeRender'`。 ### 2. 支持继承与复用 @@ -205,7 +205,7 @@ FlowModel 并不会改变组件的实现方式。它只是为 ReactComponent 增 | 用途 | 构建 UI 组件 | 构建数据驱动、流化、结构化的“模型树” | | 数据结构 | 组件树 | 模型树(支持父子模型、多实例 Fork) | | 子组件 | 使用 JSX 嵌套组件 | 使用 `setSubModel`/`addSubModel` 明确设置子模型 | -| 动态行为 | 事件绑定、状态更新驱动 UI | 注册/派发 Flow、处理自动流 | +| 动态行为 | 事件绑定、状态更新驱动 UI | 注册/派发/处理 Flow | | 持久化 | 无内建机制 | 支持持久化(如 `model.save()`) | | 支持 Fork(多次渲染) | 否(需手动复用) | 是(`createFork` 多实例化) | | 引擎控制 | 无 | 是,受 `FlowEngine` 管理、注册和加载 | @@ -216,8 +216,8 @@ FlowModel 并不会改变组件的实现方式。它只是为 ReactComponent 增 | ------ | --------------------------------- | -------------------------------------------- | | 初始化 | `constructor`、`componentDidMount` | `onInit`、`onMount` | | 卸载 | `componentWillUnmount` | `onUnmount` | -| 响应输入 | `componentDidUpdate` | `onBeforeAutoFlows`、`onAfterAutoFlows` | -| 错误处理 | `componentDidCatch` | `onAutoFlowsError` | +| 响应输入 | `componentDidUpdate` | `onDispatchEventStart/End`(配合 beforeRender 事件) | +| 错误处理 | `componentDidCatch` | `onDispatchEventError` | ### 🧱 构建结构对比 @@ -250,8 +250,7 @@ class HelloModel extends FlowModel { | 功能 | 说明 | | -------------------------------- | ---------------------- | -| `applyAutoFlows` | 自动触发已注册的流 | -| `registerFlow` / `dispatchEvent` | 支持流引擎事件处理 | +| `registerFlow` / `dispatchEvent` | 注册/分发事件流(含 beforeRender) | | `setSubModel` / `addSubModel` | 显式控制子模型的创建与绑定 | | `createFork` | 支持一个模型逻辑被复用渲染多次(如表格每行) | | `loadModel` / `save()` | 模型可持久化,与后端打通 | @@ -289,9 +288,9 @@ class HelloModel extends FlowModel { * `onInit(options)` * `onMount()` * `onUnmount()` -* `onBeforeAutoFlows(inputArgs)` -* `onAfterAutoFlows(results, inputArgs)` -* `onAutoFlowsError(error, inputArgs)` +* `onDispatchEventStart(eventName, options?, inputArgs?)` +* `onDispatchEventEnd(eventName, options?, inputArgs?, results?)` +* `onDispatchEventError(eventName, options?, inputArgs?, error?)` ### 属性和参数管理 @@ -311,7 +310,6 @@ class HelloModel extends FlowModel { * `registerFlow` * `dispatchEvent` -* `applyAutoFlows` ### 事件(emitter.on) diff --git a/packages/core/client/docs/zh-CN/core/flow-engine/flow-definition.md b/packages/core/client/docs/zh-CN/core/flow-engine/flow-definition.md index 249c11c71fe..e38cdfb6513 100644 --- a/packages/core/client/docs/zh-CN/core/flow-engine/flow-definition.md +++ b/packages/core/client/docs/zh-CN/core/flow-engine/flow-definition.md @@ -10,7 +10,6 @@ interface FlowDefinition { key: string; // 流唯一标识 title?: string; // 可选:流显示名称 - auto?: boolean; // 可选:是否自动运行 sort?: number; // 可选:流执行排序,数字越小越先执行,默认为 0,可为负数 on?: { eventName: string }; // 可选:事件触发配置 steps: Record>; // 流步骤定义 @@ -120,7 +119,8 @@ MyFlowModel.registerFlow(myFlow); // 注册流 class MyFlowDefinition implements FlowDefinition { key = 'MyFlowDefinition'; title = '我的复杂流程'; - auto = true; + // 推荐显式声明 beforeRender 生命周期事件(等价于未声明 on 且 manual !== true 的流) + on = { eventName: 'beforeRender' }; sort = 0; steps = { @@ -209,7 +209,7 @@ myModel.setStepParams('myFlow', 'step1', { name: '小明' }); ```ts await myModel.applyFlow('myFlow'); // 主动执行指定流 myModel.dispatchEvent('user.created'); // 分发事件触发流(如流配置了 on.eventName) -await myModel.applyAutoFlows(); // 执行所有 auto=true 的流,按 sort 排序 +await myModel.dispatchEvent('beforeRender'); // beforeRender 默认顺序执行并使用缓存(可覆盖) ``` --- @@ -222,8 +222,7 @@ await myModel.applyAutoFlows(); // 执行所有 auto=true 的流,按 sort 排 | ----------- | -------------------------------- | ---------------------------------- | | `key` | `string` | 流唯一标识,必须配置 | | `title` | `string` | (可选)流显示名称 | -| `on` | `{ eventName: string }` | (可选)事件触发配置 | -| `auto` | `boolean` | (可选)是否在 `applyAutoFlows()` 中自动执行流 | +| `on` | `{ eventName: string }` | (可选)事件触发配置(可用 `beforeRender` 作为生命周期事件) | | `sort` | `number` | (可选)流执行排序,数字越小越先执行,默认为 0,可为负数 | | `steps` | `Record` | 步骤集合,键为步骤名,值为步骤定义 | diff --git a/packages/core/client/docs/zh-CN/core/flow-engine/flow-model-renderer.md b/packages/core/client/docs/zh-CN/core/flow-engine/flow-model-renderer.md index b7c82485d3d..03a54e4aebf 100644 --- a/packages/core/client/docs/zh-CN/core/flow-engine/flow-model-renderer.md +++ b/packages/core/client/docs/zh-CN/core/flow-engine/flow-model-renderer.md @@ -18,10 +18,7 @@ interface FlowModelRendererProps { /** 是否在设置中隐藏移除按钮 */ hideRemoveInSettings?: boolean; // 默认 false - /** 是否跳过自动应用流,默认 false */ - skipApplyAutoFlows?: boolean; // 默认 false - - /** 当 skipApplyAutoFlows !== false 时,传递给 useApplyAutoFlows 的运行时参数 */ + /** 传递给 beforeRender 事件的运行时参数 */ inputArgs?: Record /** 设置菜单层级:1=仅当前模型(默认),2=包含子模型 */ @@ -43,8 +40,7 @@ interface FlowModelRendererProps { - `modal`: 模态框形式(待实现) - `drawer`: 抽屉形式(待实现) - **hideRemoveInSettings**: 是否在设置中隐藏移除按钮,当设为 `true` 时,流设置菜单中不会显示删除/移除选项 -- **skipApplyAutoFlows**: 是否跳过自动应用流。当设为 `true` 时,组件不会调用 `useApplyAutoFlows` hook -- **inputArgs**: 运行时参数,当 `skipApplyAutoFlows` 为 `false` 时传递给 `useApplyAutoFlows` hook +- **inputArgs**: 运行时参数,传递给 beforeRender 事件 - **settingsMenuLevel**: 设置菜单层级,控制设置菜单的显示范围 - `1`: 仅显示当前模型的设置(默认) - `2`: 包含子模型的设置 @@ -70,10 +66,10 @@ interface FlowModelRendererProps { hideRemoveInSettings={true} /> -// 跳过自动应用流 +// 传递 beforeRender 事件上下文 // 传递额外上下文 @@ -96,9 +92,8 @@ interface FlowModelRendererProps { showFlowSettings={true} flowSettingsVariant="contextMenu" hideRemoveInSettings={false} - skipApplyAutoFlows={false} settingsMenuLevel={2} - inputArgs={{ + inputArgs={{ userId: 123, permissions: ['read', 'write'] }} @@ -136,17 +131,7 @@ interface FlowModelRendererProps { /> ``` -### 4. 自定义流控制 -当需要手动控制流应用时,可以跳过自动流: - -```tsx | pure - -``` - -### 5. 传递自定义上下文 +### 4. 传递自定义上下文 当需要向流传递特定上下文数据时: ```tsx | pure @@ -160,7 +145,7 @@ interface FlowModelRendererProps { /> ``` -### 6. 控制设置菜单层级 +### 5. 控制设置菜单层级 当需要控制设置菜单显示范围时,可以通过 `settingsMenuLevel` 参数配置: ```tsx | pure diff --git a/packages/core/client/docs/zh-CN/core/flow-engine/flow-model.md b/packages/core/client/docs/zh-CN/core/flow-engine/flow-model.md index 768f6b6915d..f7101532a54 100644 --- a/packages/core/client/docs/zh-CN/core/flow-engine/flow-model.md +++ b/packages/core/client/docs/zh-CN/core/flow-engine/flow-model.md @@ -75,17 +75,14 @@ - **applyFlow(flowKey: string, inputArgs?: Record): Promise\** 执行指定流。 -- **dispatchEvent(eventName: string, inputArgs?: Record, options?: { debounce: boolean }): Promise\** - 触发事件,自动匹配并执行相关流。支持防抖功能,防止快速连续触发。 - - `eventName`: 要触发的事件名称 - - `inputArgs`: 可选的事件参数 - - `options.debounce`: 是否启用防抖功能,默认为 `false`。当设为 `true` 时,100ms 内的重复调用会被防抖处理 +- **dispatchEvent(eventName: string, inputArgs?: Record, options?: { debounce?: boolean; sequential?: boolean; useCache?: boolean }): Promise\** + 触发事件并执行对应的流程集合。 + - `sequential`: 是否顺序执行(默认并行);beforeRender 的默认值为顺序执行(可覆盖) + - `useCache`: 是否启用事件级缓存(默认 false);beforeRender 的默认值为启用缓存(可覆盖) + - `debounce`: 是否启用防抖(默认 false) -- **applyAutoFlows(inputArgs?: Record): Promise\** - 执行所有自动流。 - -- **applySubModelsAutoFlows(subKey: string, extra?: Record\): Promise\** - 执行指定子模型的自动流。 +- **applySubModelsBeforeRenderFlows(subKey: string, inputArgs?: Record\): Promise\** + 对指定子模型派发 `beforeRender` 事件(内部顺序执行并使用缓存)。 - **getFlow(key: string): FlowDefinition \| undefined** 获取指定 key 的流配置。 @@ -93,23 +90,17 @@ - **getFlows(): Map\** 获取所有已配置流(含继承)。 -- **getAutoFlows(): FlowDefinition[]** - 获取所有自动应用流程定义并按 sort 排序。 +- **getEventFlows(eventName: string): FlowDefinition[]** + 按事件名获取流程集合并按 sort 排序。 + - 特殊事件 `beforeRender`: 兼容包含“未声明 on 且 manual !== true”的流程。 --- -### 自动流执行生命周期钩子 +### 事件分发钩子 -FlowModel 提供了三个全局自动流执行生命周期钩子,子类可以重写这些方法来实现自定义逻辑: - -- **async onBeforeAutoFlows(inputArgs?: Record): Promise\** - 在所有自动流执行前调用。可以通过抛出 `FlowExitException` 来终止后续流程执行。 - -- **async onAfterAutoFlows(results: any[], inputArgs?: Record): Promise\** - 在所有自动流执行后调用。可以访问所有自动流的执行结果。 - -- **async onAutoFlowsError(error: Error, inputArgs?: Record): Promise\** - 在自动流执行或生命周期钩子出错时调用。可进行自定义错误处理。 +- **async onDispatchEventStart(eventName: string, options?: DispatchEventOptions, inputArgs?: Record)** +- **async onDispatchEventEnd(eventName: string, options?: DispatchEventOptions, inputArgs?: Record, results?: any[])** +- **async onDispatchEventError(eventName: string, options?: DispatchEventOptions, inputArgs?: Record, error?: Error)** --- @@ -205,7 +196,7 @@ await model.save(); // 执行流 await model.applyFlow('default'); -await model.applyAutoFlows(); +await model.dispatchEvent('beforeRender'); // 触发事件(立即执行) await model.dispatchEvent('click'); diff --git a/packages/core/client/docs/zh-CN/core/flow-engine/flow-sub-model.md b/packages/core/client/docs/zh-CN/core/flow-engine/flow-sub-model.md index 875ffe323c1..9aaaadf23bc 100644 --- a/packages/core/client/docs/zh-CN/core/flow-engine/flow-sub-model.md +++ b/packages/core/client/docs/zh-CN/core/flow-engine/flow-sub-model.md @@ -92,7 +92,8 @@ model.mapSubModels('tabs', (tab) => { ### 作为属性值使用 ```tsx | pure -await model.applySubModelsAutoFlows(ctx); +// 先让子列模型完成 beforeRender 事件流(如列的可见性、标题、渲染器等) +await model.applySubModelsBeforeRenderFlows('columns'); const columns = model.mapSubModels('columns', (column) => column.getProps());
``` diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/dynamic-default-params.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/dynamic-default-params.tsx index f2ba15a50e5..193e10f1229 100644 --- a/packages/core/client/docs/zh-CN/core/flow-models/demos/dynamic-default-params.tsx +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/dynamic-default-params.tsx @@ -103,7 +103,7 @@ class PluginDynamicDefaultParams extends Plugin { props: { name: '示例产品', category: 'electronics', price: 0 }, }); - await model.applyAutoFlows(); + await model.dispatchEvent('beforeRender'); this.router.add('root', { path: '/', diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/dynamic-ui-schema.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/dynamic-ui-schema.tsx index bb6ff06008c..6edeae4aee6 100644 --- a/packages/core/client/docs/zh-CN/core/flow-models/demos/dynamic-ui-schema.tsx +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/dynamic-ui-schema.tsx @@ -110,7 +110,7 @@ class PluginDynamicUiSchema extends Plugin { props: { formType: 'user', advancedMode: false }, }); - await model.applyAutoFlows(); + await model.dispatchEvent('beforeRender'); this.router.add('root', { path: '/', diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/table/table-model.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/table/table-model.tsx index 5bcbe39e16d..98167519802 100644 --- a/packages/core/client/docs/zh-CN/core/flow-models/demos/table/table-model.tsx +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/table/table-model.tsx @@ -41,7 +41,7 @@ export class TableModel extends FlowModel { }, }, }); - model.applyAutoFlows(); + model.dispatchEvent('beforeRender'); }, items: this.collection.mapFields((field) => { return { @@ -128,7 +128,7 @@ TableModel.registerFlow({ resource.setAPIClient(api); ctx.model.resource = resource; await resource.refresh(); - await ctx.model.applySubModelsAutoFlows('columns'); + await ctx.model.applySubModelsBeforeRenderFlows('columns'); }, }, }, diff --git a/packages/core/client/docs/zh-CN/core/index.md b/packages/core/client/docs/zh-CN/core/index.md index 188197efa1f..290fc63683a 100644 --- a/packages/core/client/docs/zh-CN/core/index.md +++ b/packages/core/client/docs/zh-CN/core/index.md @@ -168,7 +168,7 @@ const fork2 = model.createFork('key2', {}); ### 1. 生命周期逻辑集中且清晰 -FlowModel 拥有多个生命周期钩子(如 `onInit`、`onMount`、`onUnmount`、`onBeforeAutoFlows`、`onAfterAutoFlows` 等),这些逻辑通过类方法更清晰地组织,避免了函数式中多个 Hook 间的复杂依赖。 +FlowModel 拥有多个生命周期钩子(如 `onInit`、`onMount`、`onUnmount`、`onDispatchEventStart/End/Error` 等),这些逻辑通过类方法更清晰地组织,避免了函数式中多个 Hook 间的复杂依赖。对于渲染前的自动执行逻辑,统一通过 `onDispatchEvent*` 钩子并判断 `eventName === 'beforeRender'`。 ### 2. 支持继承与复用 @@ -205,7 +205,7 @@ FlowModel 并不会改变组件的实现方式。它只是为 ReactComponent 增 | 用途 | 构建 UI 组件 | 构建数据驱动、流化、结构化的“模型树” | | 数据结构 | 组件树 | 模型树(支持父子模型、多实例 Fork) | | 子组件 | 使用 JSX 嵌套组件 | 使用 `setSubModel`/`addSubModel` 明确设置子模型 | -| 动态行为 | 事件绑定、状态更新驱动 UI | 注册/派发 Flow、处理自动流 | +| 动态行为 | 事件绑定、状态更新驱动 UI | 注册/派发/处理 Flow | | 持久化 | 无内建机制 | 支持持久化(如 `model.save()`) | | 支持 Fork(多次渲染) | 否(需手动复用) | 是(`createFork` 多实例化) | | 引擎控制 | 无 | 是,受 `FlowEngine` 管理、注册和加载 | @@ -216,8 +216,8 @@ FlowModel 并不会改变组件的实现方式。它只是为 ReactComponent 增 | ------ | --------------------------------- | -------------------------------------------- | | 初始化 | `constructor`、`componentDidMount` | `onInit`、`onMount` | | 卸载 | `componentWillUnmount` | `onUnmount` | -| 响应输入 | `componentDidUpdate` | `onBeforeAutoFlows`、`onAfterAutoFlows` | -| 错误处理 | `componentDidCatch` | `onAutoFlowsError` | +| 响应输入 | `componentDidUpdate` | `onDispatchEventStart/End`(配合 beforeRender 事件) | +| 错误处理 | `componentDidCatch` | `onDispatchEventError` | ### 🧱 构建结构对比 @@ -250,8 +250,7 @@ class HelloModel extends FlowModel { | 功能 | 说明 | | -------------------------------- | ---------------------- | -| `applyAutoFlows` | 自动触发已注册的流 | -| `registerFlow` / `dispatchEvent` | 支持流引擎事件处理 | +| `registerFlow` / `dispatchEvent` | 注册/分发事件流(含 beforeRender) | | `setSubModel` / `addSubModel` | 显式控制子模型的创建与绑定 | | `createFork` | 支持一个模型逻辑被复用渲染多次(如表格每行) | | `loadModel` / `save()` | 模型可持久化,与后端打通 | @@ -289,9 +288,9 @@ class HelloModel extends FlowModel { * `onInit(options)` * `onMount()` * `onUnmount()` -* `onBeforeAutoFlows(inputArgs)` -* `onAfterAutoFlows(results, inputArgs)` -* `onAutoFlowsError(error, inputArgs)` +* `onDispatchEventStart(eventName, options?, inputArgs?)` +* `onDispatchEventEnd(eventName, options?, inputArgs?, results?)` +* `onDispatchEventError(eventName, options?, inputArgs?, error?)` ### 属性和参数管理 @@ -311,7 +310,6 @@ class HelloModel extends FlowModel { * `registerFlow` * `dispatchEvent` -* `applyAutoFlows` ### 事件(emitter.on) diff --git a/packages/core/client/docs/zh-CN/examples/flow-context/define-property/async-value.tsx b/packages/core/client/docs/zh-CN/examples/flow-context/define-property/async-value.tsx index 476a7d21c95..5d9bb9f987d 100644 --- a/packages/core/client/docs/zh-CN/examples/flow-context/define-property/async-value.tsx +++ b/packages/core/client/docs/zh-CN/examples/flow-context/define-property/async-value.tsx @@ -14,9 +14,11 @@ class AsyncValueModel extends FlowModel { }, }); } - async onBeforeAutoFlows() { - this.asyncValue = await this.context.asyncValue; - console.log('Async value prepared:', this.asyncValue); + async onDispatchEventStart(eventName: string) { + if (eventName === 'beforeRender') { + this.asyncValue = await this.context.asyncValue; + console.log('Async value prepared:', this.asyncValue); + } } render() { return ( diff --git a/packages/core/client/docs/zh-CN/examples/flow-context/define-property/concurrent-async.tsx b/packages/core/client/docs/zh-CN/examples/flow-context/define-property/concurrent-async.tsx index 84481539920..badda6c163d 100644 --- a/packages/core/client/docs/zh-CN/examples/flow-context/define-property/concurrent-async.tsx +++ b/packages/core/client/docs/zh-CN/examples/flow-context/define-property/concurrent-async.tsx @@ -17,9 +17,11 @@ class AsyncValueModel extends FlowModel { }, }); } - async onBeforeAutoFlows() { - await Promise.all([this.context.concurrent, this.context.concurrent, this.context.concurrent]); - this.concurrent = await this.context.concurrent; + async onDispatchEventStart(eventName: string) { + if (eventName === 'beforeRender') { + await Promise.all([this.context.concurrent, this.context.concurrent, this.context.concurrent]); + this.concurrent = await this.context.concurrent; + } } render() { return ( diff --git a/packages/core/client/docs/zh-CN/examples/flow-context/loading/index.md b/packages/core/client/docs/zh-CN/examples/flow-context/loading/index.md index ded752e2e81..6de14588710 100644 --- a/packages/core/client/docs/zh-CN/examples/flow-context/loading/index.md +++ b/packages/core/client/docs/zh-CN/examples/flow-context/loading/index.md @@ -2,16 +2,20 @@ ## 在 Model 内部使用 -建议在 `onBeforeAutoFlows` 或 `onAfterAutoFlows` 方法中处理异步属性,系统会自动维护 loading 状态。例如: +建议在 `onDispatchEventStart/End` 钩子中处理渲染前(beforeRender)相关的异步属性,系统会自动维护 loading 状态。例如: ```ts class MyModel { - async onBeforeAutoFlows() { - this.asyncProperty1 = await this.context.asyncProperty2; + async onDispatchEventStart(eventName: string) { + if (eventName === 'beforeRender') { + this.asyncProperty1 = await this.context.asyncProperty2; + } } - async onAfterAutoFlows() { - this.asyncProperty1 = await this.context.asyncProperty2; + async onDispatchEventEnd(eventName: string) { + if (eventName === 'beforeRender') { + this.asyncProperty1 = await this.context.asyncProperty2; + } } } ``` diff --git a/packages/core/client/docs/zh-CN/examples/flow-definition/context-runtime/index.md b/packages/core/client/docs/zh-CN/examples/flow-definition/context-runtime/index.md index d922dcbde5b..b251fa04aea 100644 --- a/packages/core/client/docs/zh-CN/examples/flow-definition/context-runtime/index.md +++ b/packages/core/client/docs/zh-CN/examples/flow-definition/context-runtime/index.md @@ -4,7 +4,7 @@ ## 以下流执行时会创建 FlowRuntimeContext -- `model.applyAutoFlows(inputArgs)` +- `model.dispatchEvent('beforeRender', inputArgs)` 自动执行所有属性流,`inputArgs` 为流输入参数。 - `model.dispatchEvent(eventName, inputArgs)` diff --git a/packages/core/client/docs/zh-CN/examples/flow-models/error-boundary/create-form-auto-flows-error.md b/packages/core/client/docs/zh-CN/examples/flow-models/error-boundary/create-form-auto-flows-error.md index eb6c36c1f24..9cda92ce911 100644 --- a/packages/core/client/docs/zh-CN/examples/flow-models/error-boundary/create-form-auto-flows-error.md +++ b/packages/core/client/docs/zh-CN/examples/flow-models/error-boundary/create-form-auto-flows-error.md @@ -1,6 +1,6 @@ -# 错误回退:CreateForm 自动流异常 +# 错误回退:CreateForm beforeRender事件流异常 -当自动流(useApplyAutoFlows)执行过程中发生异常时,错误将被提升为渲染期错误并由外层错误边界捕获。 +当beforeRender事件流执行过程中发生异常时,错误将被提升为渲染期错误并由外层错误边界捕获。 diff --git a/packages/core/client/docs/zh-CN/examples/flow-models/error-boundary/create-form-auto-flows-error.tsx b/packages/core/client/docs/zh-CN/examples/flow-models/error-boundary/create-form-auto-flows-error.tsx index c091296488f..b718746e7d1 100644 --- a/packages/core/client/docs/zh-CN/examples/flow-models/error-boundary/create-form-auto-flows-error.tsx +++ b/packages/core/client/docs/zh-CN/examples/flow-models/error-boundary/create-form-auto-flows-error.tsx @@ -3,15 +3,7 @@ * title: 错误回退 - CreateForm 自动流异常 */ -import { - Application, - CreateFormModel, - FilterManager, - FormFieldModel, - FormGridModel, - FormItemModel, - Plugin, -} from '@nocobase/client'; +import { Application, CreateFormModel, FilterManager, FormGridModel, FormItemModel, Plugin } from '@nocobase/client'; import { FlowEngineProvider, FlowModelRenderer } from '@nocobase/flow-engine'; import { APIClient } from '@nocobase/sdk'; import { Card } from 'antd'; @@ -45,7 +37,6 @@ class DemoPlugin extends Plugin { CreateFormModel, FormGridModel, FormItemModel, - FormFieldModel, }); this.form = this.flowEngine.createModel({ @@ -97,7 +88,7 @@ class DemoPlugin extends Plugin { element: (
- +
diff --git a/packages/core/client/docs/zh-CN/examples/flow-models/error-boundary/index.md b/packages/core/client/docs/zh-CN/examples/flow-models/error-boundary/index.md index 57ee38b1b5f..74b118b0690 100644 --- a/packages/core/client/docs/zh-CN/examples/flow-models/error-boundary/index.md +++ b/packages/core/client/docs/zh-CN/examples/flow-models/error-boundary/index.md @@ -3,7 +3,7 @@ 本页汇总并演示三类常见的错误回退场景,便于一次性对比查看: - CreateForm 字段渲染异常 -- CreateForm 自动流(useApplyAutoFlows)异常 +- CreateForm beforeRender事件流 - 表格列(RenderFunction)单元格渲染异常 ## CreateForm 字段异常 @@ -12,7 +12,7 @@ -## CreateForm 自动流异常 +## CreateForm beforeRender事件流异常 当自动流(useApplyAutoFlows)执行过程中发生异常时,错误将被提升为渲染期错误并由外层错误边界捕获。 diff --git a/packages/core/client/docs/zh-CN/examples/flow-models/lifecycle/on-init.tsx b/packages/core/client/docs/zh-CN/examples/flow-models/lifecycle/on-init.tsx index 911c419a054..054f1f9b1ae 100644 --- a/packages/core/client/docs/zh-CN/examples/flow-models/lifecycle/on-init.tsx +++ b/packages/core/client/docs/zh-CN/examples/flow-models/lifecycle/on-init.tsx @@ -15,12 +15,16 @@ class HelloModel extends FlowModel { console.log('onUnmount'); } - async onBeforeAutoFlows() { - console.log('onBeforeAutoFlows'); + async onDispatchEventStart(eventName: string) { + if (eventName === 'beforeRender') { + console.log('onDispatchEventStart(beforeRender)'); + } } - async onAfterAutoFlows() { - console.log('onAfterAutoFlows'); + async onDispatchEventEnd(eventName: string) { + if (eventName === 'beforeRender') { + console.log('onDispatchEventEnd(beforeRender)'); + } } render() { diff --git a/packages/core/client/docs/zh-CN/examples/flow-models/render-in-fork/index.md b/packages/core/client/docs/zh-CN/examples/flow-models/render-in-fork/index.md deleted file mode 100644 index 52a1ae16dc0..00000000000 --- a/packages/core/client/docs/zh-CN/examples/flow-models/render-in-fork/index.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: 模型 cleanRun:每次自动流干净运行 -order: 99 ---- - -本示例演示如何通过模型选项 `cleanRun` 实现“每次自动流都在干净基线”运行,避免状态累积污染原始模型。 - - diff --git a/packages/core/client/docs/zh-CN/examples/flow-models/render-in-fork/index.tsx b/packages/core/client/docs/zh-CN/examples/flow-models/render-in-fork/index.tsx deleted file mode 100644 index 42aef2619a0..00000000000 --- a/packages/core/client/docs/zh-CN/examples/flow-models/render-in-fork/index.tsx +++ /dev/null @@ -1,112 +0,0 @@ -/** - * defaultShowCode: true - * title: 模型 cleanRun:每次自动流干净运行 - */ - -import React from 'react'; -import { Application, Plugin } from '@nocobase/client'; -import { Card, Space } from 'antd'; -import { FlowModel, FlowModelRenderer, defineFlow, escapeT } from '@nocobase/flow-engine'; - -class DemoModel extends FlowModel { - static { - // 配置流:修改卡片标题(演示可配置项) - this.registerFlow( - defineFlow({ - key: 'demoSettings', - title: escapeT('Demo settings'), - manual: true, - steps: { - title: { - title: escapeT('Title'), - uiSchema: { - title: { - 'x-component': 'Input', - 'x-decorator': 'FormItem', - 'x-component-props': { placeholder: escapeT('Please input title') }, - }, - }, - defaultParams: (ctx) => ({ title: (ctx.model.getProps() as any).title ?? '' }), - handler: (ctx, params) => { - ctx.model.setProps({ title: params.title }); - }, - }, - }, - }), - ); - - // 自动流:每次运行把 count + 1,并记录 runId - this.registerFlow( - defineFlow({ - key: 'autoCounter', - title: 'Auto counter', - steps: { - bumpCount: { - handler: (ctx) => { - const props = ctx.model.getProps() as any; - const prev = Number(props.count) || 0; - ctx.model.setProps({ count: prev + 1, lastRunId: ctx.runId }); - }, - }, - }, - }), - ); - } - - render() { - const { count = 0, lastRunId, title } = this.getProps() as any; - return ( - -
- title: {title || 'Demo'} -
-
- count: {count} -
-
- lastRunId: {String(lastRunId || '')} -
-
- ); - } -} - -class PluginDemo extends Plugin { - async load() { - this.flowEngine.registerModels({ DemoModel }); - // 强制启用 FlowSettings(本示例已不依赖设置面板,保留无害) - this.flowEngine.flowSettings.forceEnable(); - - const model1 = this.flowEngine.createModel({ use: 'DemoModel', cleanRun: false }); - const model2 = this.flowEngine.createModel({ use: 'DemoModel', cleanRun: true }); - - const View = () => { - return ( - - - -
cleanRun: false(状态累加)
- -
- -
cleanRun: true(每次干净基线)
- -
-
-
- ); - }; - - this.router.add('root', { - path: '/', - element: , - }); - } -} - -const app = new Application({ - router: { type: 'memory', initialEntries: ['/'] }, - plugins: [PluginDemo], -}); - -export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/learn/flow-model.md b/packages/core/client/docs/zh-CN/learn/flow-model.md index 6370f1717d8..524f1ba3591 100644 --- a/packages/core/client/docs/zh-CN/learn/flow-model.md +++ b/packages/core/client/docs/zh-CN/learn/flow-model.md @@ -168,7 +168,7 @@ const fork2 = model.createFork('key2', {}); ### 1. 生命周期逻辑集中且清晰 -FlowModel 拥有多个生命周期钩子(如 `onInit`、`onMount`、`onUnmount`、`onBeforeAutoFlows`、`onAfterAutoFlows` 等),这些逻辑通过类方法更清晰地组织,避免了函数式中多个 Hook 间的复杂依赖。 +FlowModel 拥有多个生命周期钩子(如 `onInit`、`onMount`、`onUnmount`、`onDispatchEventStart/End/Error` 等),这些逻辑通过类方法更清晰地组织,避免了函数式中多个 Hook 间的复杂依赖。对于渲染前的自动执行逻辑,统一通过 `onDispatchEvent*` 钩子并判断 `eventName === 'beforeRender'`。 ### 2. 支持继承与复用 @@ -205,7 +205,7 @@ FlowModel 并不会改变组件的实现方式。它只是为 ReactComponent 增 | 用途 | 构建 UI 组件 | 构建数据驱动、流化、结构化的“模型树” | | 数据结构 | 组件树 | 模型树(支持父子模型、多实例 Fork) | | 子组件 | 使用 JSX 嵌套组件 | 使用 `setSubModel`/`addSubModel` 明确设置子模型 | -| 动态行为 | 事件绑定、状态更新驱动 UI | 注册/派发 Flow、处理自动流 | +| 动态行为 | 事件绑定、状态更新驱动 UI | 注册/派发/处理 Flow | | 持久化 | 无内建机制 | 支持持久化(如 `model.save()`) | | 支持 Fork(多次渲染) | 否(需手动复用) | 是(`createFork` 多实例化) | | 引擎控制 | 无 | 是,受 `FlowEngine` 管理、注册和加载 | @@ -216,8 +216,8 @@ FlowModel 并不会改变组件的实现方式。它只是为 ReactComponent 增 | ------ | --------------------------------- | -------------------------------------------- | | 初始化 | `constructor`、`componentDidMount` | `onInit`、`onMount` | | 卸载 | `componentWillUnmount` | `onUnmount` | -| 响应输入 | `componentDidUpdate` | `onBeforeAutoFlows`、`onAfterAutoFlows` | -| 错误处理 | `componentDidCatch` | `onAutoFlowsError` | +| 响应输入 | `componentDidUpdate` | `onDispatchEventStart/End`(配合 beforeRender 事件) | +| 错误处理 | `componentDidCatch` | `onDispatchEventError` | ### 🧱 构建结构对比 @@ -250,8 +250,7 @@ class HelloModel extends FlowModel { | 功能 | 说明 | | -------------------------------- | ---------------------- | -| `applyAutoFlows` | 自动触发已注册的流 | -| `registerFlow` / `dispatchEvent` | 支持流引擎事件处理 | +| `registerFlow` / `dispatchEvent` | 注册/分发事件流(含 beforeRender) | | `setSubModel` / `addSubModel` | 显式控制子模型的创建与绑定 | | `createFork` | 支持一个模型逻辑被复用渲染多次(如表格每行) | | `loadModel` / `save()` | 模型可持久化,与后端打通 | @@ -289,9 +288,9 @@ class HelloModel extends FlowModel { * `onInit(options)` * `onMount()` * `onUnmount()` -* `onBeforeAutoFlows(inputArgs)` -* `onAfterAutoFlows(results, inputArgs)` -* `onAutoFlowsError(error, inputArgs)` +* `onDispatchEventStart(eventName, options?, inputArgs?)` +* `onDispatchEventEnd(eventName, options?, inputArgs?, results?)` +* `onDispatchEventError(eventName, options?, inputArgs?, error?)` ### 属性和参数管理 @@ -311,7 +310,6 @@ class HelloModel extends FlowModel { * `registerFlow` * `dispatchEvent` -* `applyAutoFlows` ### 事件(emitter.on) diff --git a/packages/core/client/src/flow/actions/__tests__/openView.defineProps.route.test.tsx b/packages/core/client/src/flow/actions/__tests__/openView.defineProps.route.test.tsx index 11fc04f07fe..15180062050 100644 --- a/packages/core/client/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +++ b/packages/core/client/src/flow/actions/__tests__/openView.defineProps.route.test.tsx @@ -74,7 +74,7 @@ describe('openView action - route mode defineProperties/defineMethods', () => { }; const pageModel = { context: pageModelContext, - invalidateAutoFlowCache: vi.fn(), + invalidateFlowCache: vi.fn(), _rerunLastAutoRun: vi.fn(), } as any; @@ -137,7 +137,7 @@ describe('openView action - route mode defineProperties/defineMethods', () => { }; const pageModel = { context: pageModelContext, - invalidateAutoFlowCache: vi.fn(), + invalidateFlowCache: vi.fn(), _rerunLastAutoRun: vi.fn(), } as any; diff --git a/packages/core/client/src/flow/actions/openView.tsx b/packages/core/client/src/flow/actions/openView.tsx index 46e503b0423..3765c3bef73 100644 --- a/packages/core/client/src/flow/actions/openView.tsx +++ b/packages/core/client/src/flow/actions/openView.tsx @@ -702,7 +702,7 @@ export const openView = defineAction({ pageModel.context.defineMethod(key, method); }); - pageModel.invalidateAutoFlowCache(true); + pageModel.invalidateFlowCache('beforeRender', true); pageModel['_rerunLastAutoRun'](); // TODO: 临时做法,等上下文重构完成后去掉 }} /> @@ -722,7 +722,7 @@ export const openView = defineAction({ const nav = ctx.inputArgs?.navigation || ctx.view?.navigation; if (pageModelUid) { const pageModel = pageModelRef || ctx.model.flowEngine.getModel(pageModelUid); - pageModel?.invalidateAutoFlowCache(true); + pageModel?.invalidateFlowCache('beforeRender', true); } if (navigation !== false) { if (nav?.back) { diff --git a/packages/core/client/src/flow/actions/pattern.tsx b/packages/core/client/src/flow/actions/pattern.tsx index df6d6c1fd9a..896b214964d 100644 --- a/packages/core/client/src/flow/actions/pattern.tsx +++ b/packages/core/client/src/flow/actions/pattern.tsx @@ -101,6 +101,6 @@ async function rebuildFieldSubModel(ctx, model, binding: BindingOptions) { }, }); - await subModel.applyAutoFlows(); + await subModel.dispatchEvent('beforeRender'); await model.save(); // 持久化 } diff --git a/packages/core/client/src/flow/actions/titleField.tsx b/packages/core/client/src/flow/actions/titleField.tsx index 13d25a06303..c45ec93a7be 100644 --- a/packages/core/client/src/flow/actions/titleField.tsx +++ b/packages/core/client/src/flow/actions/titleField.tsx @@ -76,7 +76,7 @@ export const titleField = defineAction({ }, }); await model.save(); - await model.applyAutoFlows(); + await model.dispatchEvent('beforeRender'); // 持久化 await ctx.model.save(); } @@ -109,7 +109,7 @@ export const titleField = defineAction({ }, }); // await model.save(); - await model.applyAutoFlows(); + await model.dispatchEvent('beforeRender'); } }, }); diff --git a/packages/core/client/src/flow/components/DefaultValue.tsx b/packages/core/client/src/flow/components/DefaultValue.tsx index 0c0a2396fce..758b2407fbd 100644 --- a/packages/core/client/src/flow/components/DefaultValue.tsx +++ b/packages/core/client/src/flow/components/DefaultValue.tsx @@ -41,7 +41,8 @@ interface Props { function createTempFieldClass(Base: any) { return class Temp extends Base { - async onBeforeAutoFlows() { + async onDispatchEventStart(eventName: string) { + if (eventName !== 'beforeRender') return; const initParams = this.getStepParams?.('fieldSettings', 'init') || {}; const collectionFieldKey = initParams?.dataSourceKey && @@ -56,7 +57,8 @@ function createTempFieldClass(Base: any) { get: () => collectionFieldFromManager || fallbackCollectionField, }); } - async onAfterAutoFlows() { + async onDispatchEventEnd(eventName: string) { + if (eventName !== 'beforeRender') return; const originalProps = this._originalModel?.props || {}; const collectionField = this.context?.collectionField; const inferMultipleFromCollectionField = () => { diff --git a/packages/core/client/src/flow/components/FieldAssignValueInput.tsx b/packages/core/client/src/flow/components/FieldAssignValueInput.tsx index f04632377c7..9144f4f6944 100644 --- a/packages/core/client/src/flow/components/FieldAssignValueInput.tsx +++ b/packages/core/client/src/flow/components/FieldAssignValueInput.tsx @@ -116,7 +116,7 @@ export const FieldAssignValueInput: React.FC = ({ fieldUid, value, onChan updateAssociation: false, multiple, }); - fm?.applyAutoFlows?.(); + fm?.dispatchEvent?.('beforeRender', undefined, { sequential: true, useCache: true }); // 为本地枚举型字段补全可选项(仅在未显式传入 options 时处理) ensureOptionsFromUiSchemaEnumIfAbsent(fm, cf); if (!fm?.props?.fieldNames && cf?.targetCollection) { diff --git a/packages/core/client/src/flow/components/__tests__/DefaultValue.test.tsx b/packages/core/client/src/flow/components/__tests__/DefaultValue.test.tsx index 37608d88a73..aecbca430b1 100644 --- a/packages/core/client/src/flow/components/__tests__/DefaultValue.test.tsx +++ b/packages/core/client/src/flow/components/__tests__/DefaultValue.test.tsx @@ -101,7 +101,7 @@ describe('DefaultValue component', () => { // 每个用例重置表单实例,避免跨用例的值污染(例如上个用例输入的 "hello") form = createForm(); - // 为多选关联选择组件的测试场景,覆盖/简化自动流程,只保留必要的属性赋值 + // 为多选关联选择组件的测试场景,覆盖/简化 beforeRender 事件流,仅保留必要的属性赋值 RecordSelectFieldModel.registerFlow({ key: 'eventSettings', manual: true, steps: {} }); RecordSelectFieldModel.registerFlow({ key: 'selectSettings', @@ -193,7 +193,7 @@ describe('DefaultValue component', () => { }, }, }); - // 指定为对多关系,DefaultValue 内部应选择 RecordSelectFieldModel,并在 onAfterAutoFlows 推断 multiple + // 指定为对多关系,DefaultValue 内部应选择 RecordSelectFieldModel,并在 onDispatchEventEnd 推断 multiple host.context.defineProperty('collectionField', { value: { interface: 'm2m', diff --git a/packages/core/client/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx b/packages/core/client/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx index 3540d5a1935..734b4847125 100644 --- a/packages/core/client/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +++ b/packages/core/client/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx @@ -24,14 +24,14 @@ import { } from '@nocobase/flow-engine'; import { JSEditableFieldModel } from '../../../models/fields/JSEditableFieldModel'; -// Minimal auto-flow model that executes preview code +// Minimal beforeRender event model that executes preview code class DummyJsAutoModel extends FlowModel { render() { return ; } } -describe('useCodeRunner (auto flow)', () => { +describe('useCodeRunner (beforeRender)', () => { it('logs success and captures console output', async () => { const engine = new FlowEngine(); engine.registerModels({ DummyJsAutoModel }); @@ -82,7 +82,7 @@ describe('useCodeRunner (auto flow)', () => { expect(result.current.running).toBe(false); }); - it('previews and updates DOM for JSEditable (auto flow)', async () => { + it('previews and updates DOM for JSEditable (beforeRender)', async () => { const engine = new FlowEngine(); engine.registerModels({ JSEditableFieldModel }); const field = engine.createModel({ use: 'JSEditableFieldModel', uid: 'f1' }); diff --git a/packages/core/client/src/flow/components/code-editor/hooks/useCodeRunner.ts b/packages/core/client/src/flow/components/code-editor/hooks/useCodeRunner.ts index 6a294abb381..e1eaf8e9d07 100644 --- a/packages/core/client/src/flow/components/code-editor/hooks/useCodeRunner.ts +++ b/packages/core/client/src/flow/components/code-editor/hooks/useCodeRunner.ts @@ -106,7 +106,11 @@ export function useCodeRunner(hostCtx: FlowModelContext, version = 'v1') { if (isManual) { await m.applyFlow('jsSettings', { preview: { code, version } }); } else { - await m.applyAutoFlows({ preview: { code, version } }, false); + await m.dispatchEvent( + 'beforeRender', + { preview: { code, version } }, + { sequential: true, useCache: false }, + ); } }; diff --git a/packages/core/client/src/flow/models/base/CollectionBlockModel.tsx b/packages/core/client/src/flow/models/base/CollectionBlockModel.tsx index db0fb189ba5..35295f11e59 100644 --- a/packages/core/client/src/flow/models/base/CollectionBlockModel.tsx +++ b/packages/core/client/src/flow/models/base/CollectionBlockModel.tsx @@ -297,7 +297,7 @@ export class CollectionBlockModel extends DataBlockModel { - this.invalidateAutoFlowCache(); + this.invalidateFlowCache('beforeRender'); }); return resource; }, diff --git a/packages/core/client/src/flow/models/base/PageModel/PageTabModel.tsx b/packages/core/client/src/flow/models/base/PageModel/PageTabModel.tsx index b51c27db9a0..5a4306566a7 100644 --- a/packages/core/client/src/flow/models/base/PageModel/PageTabModel.tsx +++ b/packages/core/client/src/flow/models/base/PageModel/PageTabModel.tsx @@ -124,7 +124,7 @@ export class RootPageTabModel extends BasePageTabModel { async destroy() { this.observerDispose(); - this.invalidateAutoFlowCache(true); + this.invalidateFlowCache('beforeRender', true); this.flowEngine.removeModel(this.uid); await this.context.api.request({ method: 'post', diff --git a/packages/core/client/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx b/packages/core/client/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx index 04184ae721f..e006dc6ca3c 100644 --- a/packages/core/client/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +++ b/packages/core/client/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx @@ -172,7 +172,7 @@ export class AssignFormItemModel extends FormItemModel { updateAssociation: false, multiple: multi, }); - fm?.applyAutoFlows?.(); + fm?.dispatchEvent?.('beforeRender', undefined, { sequential: true, useCache: true }); // 为本地枚举型字段补全可选项(仅在未显式传入 options 时处理) ensureOptionsFromUiSchemaEnumIfAbsent(fm as any, cfForMultiple as any); if (!fm?.props?.fieldNames && cfForMultiple?.targetCollection) { diff --git a/packages/core/client/src/flow/models/blocks/details/DetailsBlockModel.tsx b/packages/core/client/src/flow/models/blocks/details/DetailsBlockModel.tsx index 121c60050db..f80219c2fc7 100644 --- a/packages/core/client/src/flow/models/blocks/details/DetailsBlockModel.tsx +++ b/packages/core/client/src/flow/models/blocks/details/DetailsBlockModel.tsx @@ -175,7 +175,7 @@ DetailsBlockModel.registerFlow({ throw new Error('Resource is not initialized'); } // 1. 先初始化字段网格,确保所有字段都创建完成 - await ctx.model.applySubModelsAutoFlows('grid'); + await ctx.model.applySubModelsBeforeRenderFlows('grid'); }, }, layout: { diff --git a/packages/core/client/src/flow/models/blocks/form/QuickEditFormModel.tsx b/packages/core/client/src/flow/models/blocks/form/QuickEditFormModel.tsx index 853283a16a6..3e1b6edd178 100644 --- a/packages/core/client/src/flow/models/blocks/form/QuickEditFormModel.tsx +++ b/packages/core/client/src/flow/models/blocks/form/QuickEditFormModel.tsx @@ -234,7 +234,7 @@ QuickEditFormModel.registerFlow({ get: () => collectionField, }); ctx.model.addAppends(fieldPath); - await fieldModel.applyAutoFlows(); + await fieldModel.dispatchEvent('beforeRender'); } if (ctx.inputArgs.filterByTk || ctx.inputArgs.record) { resource.setFilterByTk(ctx.inputArgs.filterByTk); diff --git a/packages/core/client/src/flow/models/blocks/table/TableActionsColumnModel.tsx b/packages/core/client/src/flow/models/blocks/table/TableActionsColumnModel.tsx index d5d27a2f9a8..ca0d8854d27 100644 --- a/packages/core/client/src/flow/models/blocks/table/TableActionsColumnModel.tsx +++ b/packages/core/client/src/flow/models/blocks/table/TableActionsColumnModel.tsx @@ -88,7 +88,7 @@ const AddActionToolbarComponent = ({ model }) => { export class TableActionsColumnModel extends TableCustomColumnModel { async afterAddAsSubModel() { - await this.applyAutoFlows(); + await this.dispatchEvent('beforeRender'); } getColumnProps() { diff --git a/packages/core/client/src/flow/models/blocks/table/TableBlockModel.tsx b/packages/core/client/src/flow/models/blocks/table/TableBlockModel.tsx index c40e2c5e8ce..2a7d1e70fb7 100644 --- a/packages/core/client/src/flow/models/blocks/table/TableBlockModel.tsx +++ b/packages/core/client/src/flow/models/blocks/table/TableBlockModel.tsx @@ -587,7 +587,7 @@ TableBlockModel.registerFlow({ ctx.model.mapSubModels('columns', async (column) => { if (column.hidden) return; try { - await column.applyAutoFlows(); + await column.dispatchEvent('beforeRender'); } catch (err) { column['__autoFlowError'] = err; // 列级错误不再向上抛,避免拖垮整表;在单元格层用 ErrorBoundary 展示 diff --git a/packages/core/client/src/flow/models/blocks/table/TableColumnModel.tsx b/packages/core/client/src/flow/models/blocks/table/TableColumnModel.tsx index c8037e4db68..8fd24269ccc 100644 --- a/packages/core/client/src/flow/models/blocks/table/TableColumnModel.tsx +++ b/packages/core/client/src/flow/models/blocks/table/TableColumnModel.tsx @@ -73,7 +73,7 @@ export class TableColumnModel extends DisplayItemModel { async afterAddAsSubModel() { await super.afterAddAsSubModel(); - await this.applyAutoFlows(); + await this.dispatchEvent('beforeRender'); } static defineChildren(ctx: FlowModelContext) { @@ -252,7 +252,7 @@ TableColumnModel.registerFlow({ ctx.model.setProps('title', collectionField.title); ctx.model.setProps('dataIndex', collectionField.name); // for quick edit - await ctx.model.applySubModelsAutoFlows('field'); + await ctx.model.applySubModelsBeforeRenderFlows('field'); ctx.model.setProps({ ...collectionField.getComponentProps(), }); diff --git a/packages/core/client/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx b/packages/core/client/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx index 11ae9038ea6..91956bfae69 100644 --- a/packages/core/client/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +++ b/packages/core/client/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx @@ -166,7 +166,7 @@ export class SubTableColumnModel< async afterAddAsSubModel() { await super.afterAddAsSubModel(); - await this.applyAutoFlows(); + await this.dispatchEvent('beforeRender'); } getColumnProps(): TableColumnProps { @@ -365,7 +365,7 @@ SubTableColumnModel.registerFlow({ }, }, }); - await model.applyAutoFlows(); + await model.dispatchEvent('beforeRender'); }, }, title: { diff --git a/packages/core/client/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx b/packages/core/client/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx index 42bafe2c48e..9d536eff37b 100644 --- a/packages/core/client/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +++ b/packages/core/client/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx @@ -139,7 +139,7 @@ SubTableFieldModel.registerFlow({ }, init: { async handler(ctx, params) { - await ctx.model.applySubModelsAutoFlows('columns'); + await ctx.model.applySubModelsBeforeRenderFlows('columns'); }, }, allowAddNew: { diff --git a/packages/core/client/src/flow/models/fields/DisplayAssociationField/DisplaySubTableFieldModel.tsx b/packages/core/client/src/flow/models/fields/DisplayAssociationField/DisplaySubTableFieldModel.tsx index f1319ff4ed7..02596181693 100644 --- a/packages/core/client/src/flow/models/fields/DisplayAssociationField/DisplaySubTableFieldModel.tsx +++ b/packages/core/client/src/flow/models/fields/DisplayAssociationField/DisplaySubTableFieldModel.tsx @@ -52,7 +52,7 @@ export class DisplaySubTableFieldModel extends FieldModel { async afterAddAsSubModel() { await super.afterAddAsSubModel(); - await this.applyAutoFlows(); + await this.dispatchEvent('beforeRender'); } getColumns() { @@ -105,7 +105,7 @@ DisplaySubTableFieldModel.registerFlow({ steps: { init: { async handler(ctx) { - await ctx.model.applySubModelsAutoFlows('columns'); + await ctx.model.applySubModelsBeforeRenderFlows('columns'); }, }, }, diff --git a/packages/core/client/src/flow/models/fields/JSEditableFieldModel.tsx b/packages/core/client/src/flow/models/fields/JSEditableFieldModel.tsx index c92bc8d3bb3..0e0d20343ff 100644 --- a/packages/core/client/src/flow/models/fields/JSEditableFieldModel.tsx +++ b/packages/core/client/src/flow/models/fields/JSEditableFieldModel.tsx @@ -79,6 +79,7 @@ const JSFormRuntime: React.FC<{ * - 子节点由脚本渲染(DOM 操作),用于完全自定义输入体验。 */ export class JSEditableFieldModel extends FieldModel { + private _mountedOnce = false; render() { return ( ); } + + /** + * 在 CreateForm / 提交后重置等场景下,字段可能被卸载并重新挂载。 + * 由于 beforeRender 命中缓存,新挂载的容器不会自动写入 JS 渲染内容。 + * 这里在二次挂载时检测容器已就绪,主动触发 rerender 来刷新 beforeRender(禁用缓存), + * 保证输入框不会因为容器变更而空白。 + */ + protected onMount() { + if (this._mountedOnce) { + if (this.context.ref?.current) { + this.rerender(); + } + } + this._mountedOnce = true; + } } JSEditableFieldModel.define({ diff --git a/packages/core/client/src/flow/models/fields/JSFieldModel.tsx b/packages/core/client/src/flow/models/fields/JSFieldModel.tsx index 6df81b19a5d..b65632fe7fb 100644 --- a/packages/core/client/src/flow/models/fields/JSFieldModel.tsx +++ b/packages/core/client/src/flow/models/fields/JSFieldModel.tsx @@ -83,7 +83,7 @@ export class JSFieldModel extends FieldModel { } /** - * 组件挂载后,如果容器引用已就绪,强制重跑一次自动流程以把 HTML 写入当前 DOM。 + * 组件挂载后,如果容器引用已就绪,强制重跑一次 beforeRender flows 以把 HTML 写入当前 DOM。 * 解决新增后因命中缓存或执行时机导致内容未写入的问题(与 JSBlock/JSItem 一致处理)。 */ protected onMount() { diff --git a/packages/core/client/src/flow/models/fields/JSItemModel.tsx b/packages/core/client/src/flow/models/fields/JSItemModel.tsx index 7560b959045..cfd142c10b7 100644 --- a/packages/core/client/src/flow/models/fields/JSItemModel.tsx +++ b/packages/core/client/src/flow/models/fields/JSItemModel.tsx @@ -49,7 +49,7 @@ export class JSItemModel extends CommonItemModel { } /** - * 组件挂载后,如果容器引用已就绪,强制重跑一次自动流程以把 HTML 写入当前 DOM。 + * 组件挂载后,如果容器引用已就绪,强制重跑一次 beforeRender 以把 HTML 写入当前 DOM。 * 解决某些情况下命中旧缓存导致新节点未写入而显示空白的问题。 */ protected onMount() { diff --git a/packages/core/flow-engine/src/ViewScopedFlowEngine.ts b/packages/core/flow-engine/src/ViewScopedFlowEngine.ts index 9b6b73a80f9..db042bbcdbe 100644 --- a/packages/core/flow-engine/src/ViewScopedFlowEngine.ts +++ b/packages/core/flow-engine/src/ViewScopedFlowEngine.ts @@ -13,7 +13,7 @@ import { FlowEngine } from './flowEngine'; * ViewScopedFlowEngine(视图作用域引擎) * * 设计目标: - * - 仅在视图(dialog/drawer/embed)作用域内隔离“模型实例表”和“自动流缓存”; + * - 仅在视图(dialog/drawer/embed)作用域内隔离“模型实例表”和“事件缓存”; * - 其余能力(动作/事件/模型类/资源/仓库/设置/日志/翻译等)全部直接代理到父引擎; * - 使用 Proxy 实现“最小代理”,保持与父引擎的全局一致性。 * diff --git a/packages/core/flow-engine/src/__tests__/viewScopedFlowEngine.test.ts b/packages/core/flow-engine/src/__tests__/viewScopedFlowEngine.test.ts index cf351d36871..60907092da8 100644 --- a/packages/core/flow-engine/src/__tests__/viewScopedFlowEngine.test.ts +++ b/packages/core/flow-engine/src/__tests__/viewScopedFlowEngine.test.ts @@ -59,7 +59,7 @@ describe('ViewScopedFlowEngine', () => { expect(child.getModel(uid)).toBe(cm); }); - it('isolates auto flow cache across engines with identical model uid', async () => { + it('isolates beforeRender event cache across engines with identical model uid', async () => { const parent = new FlowEngine(); const child = createViewScopedEngine(parent); @@ -81,11 +81,11 @@ describe('ViewScopedFlowEngine', () => { const pm = parent.createModel({ use: CounterModel, uid }); const cm = child.createModel({ use: CounterModel, uid }); - await pm.applyAutoFlows(); + await pm.dispatchEvent('beforeRender'); expect(count).toBe(1); // If cache was shared, the next call would hit cache and not increment. - await cm.applyAutoFlows(); + await cm.dispatchEvent('beforeRender'); expect(count).toBe(2); }); diff --git a/packages/core/flow-engine/src/components/FieldModelRenderer.tsx b/packages/core/flow-engine/src/components/FieldModelRenderer.tsx index 7d96adaf8ad..aef846d4561 100644 --- a/packages/core/flow-engine/src/components/FieldModelRenderer.tsx +++ b/packages/core/flow-engine/src/components/FieldModelRenderer.tsx @@ -20,7 +20,6 @@ const flowModelRendererPropKeys: (keyof FlowModelRendererProps)[] = [ 'flowSettingsVariant', 'hideRemoveInSettings', 'showTitle', - 'skipApplyAutoFlows', 'inputArgs', 'showErrorFallback', 'settingsMenuLevel', diff --git a/packages/core/flow-engine/src/components/FlowModelRenderer.tsx b/packages/core/flow-engine/src/components/FlowModelRenderer.tsx index 228584413c7..e7c80ccc1a2 100644 --- a/packages/core/flow-engine/src/components/FlowModelRenderer.tsx +++ b/packages/core/flow-engine/src/components/FlowModelRenderer.tsx @@ -87,10 +87,7 @@ export interface FlowModelRendererProps { /** 是否在边框左上角显示模型title,默认 false */ showTitle?: boolean; // 默认 false - /** 是否跳过自动应用流程,默认 false */ - skipApplyAutoFlows?: boolean; // 默认 false - - /** 当 skipApplyAutoFlows !== false 时,传递给 useApplyAutoFlows 的额外上下文 */ + /** 传递给 beforeRender 事件的运行时参数 */ inputArgs?: Record; /** 是否在最外层包装 FlowErrorFallback 组件,默认 false */ @@ -168,57 +165,7 @@ const FlowModelRendererWithAutoFlows: React.FC<{ }, ); -/** - * 内部组件:不带 useApplyAutoFlows 的渲染器 - */ -const FlowModelRendererWithoutAutoFlows: React.FC<{ - model: FlowModel; - showFlowSettings: - | boolean - | { - showBackground?: boolean; - showBorder?: boolean; - showDragHandle?: boolean; - style?: React.CSSProperties; - /** - * @default 'inside' - */ - toolbarPosition?: 'inside' | 'above'; - }; - flowSettingsVariant: string; - hideRemoveInSettings: boolean; - showTitle: boolean; - showErrorFallback?: boolean; - settingsMenuLevel?: number; - extraToolbarItems?: ToolbarItemConfig[]; -}> = observer( - ({ - model, - showFlowSettings, - flowSettingsVariant, - hideRemoveInSettings, - showTitle, - showErrorFallback, - settingsMenuLevel, - extraToolbarItems, - }) => { - setAutoFlowError(model, null); - return ( - - - - ); - }, -); +// 移除不带 beforeRender 执行的渲染器,统一触发 beforeRender 事件 /** * 核心渲染逻辑组件 @@ -264,7 +211,7 @@ const FlowModelRendererCore: React.FC<{ const ContentOrError: React.FC = () => { const autoError = getAutoFlowError(model); if (autoError) { - // 把自动流的错误转化为内容区错误,由内层边界兜住 + // 将 beforeRender 事件错误转化为内容区错误,由内层边界兜住 throw autoError; } const rendered = model.render(); @@ -355,8 +302,7 @@ const FlowModelRendererCore: React.FC<{ * @param {string} props.flowSettingsVariant - The interaction style for flow settings. * @param {boolean} props.hideRemoveInSettings - Whether to hide remove button in settings. * @param {boolean} props.showTitle - Whether to show model title in the top-left corner of the border. - * @param {boolean} props.skipApplyAutoFlows - Whether to skip applying auto flows. - * @param {any} props.inputArgs - Runtime arguments to pass to useApplyAutoFlows when skipApplyAutoFlows is false. + * @param {any} props.inputArgs - Runtime arguments to pass to beforeRender event flows. * @param {number} props.settingsMenuLevel - Settings menu levels: 1=current model only (default), 2=include sub-models. * @param {ToolbarItemConfig[]} props.extraToolbarItems - Extra toolbar items to add to this renderer instance. * @returns {React.ReactNode | null} The rendered output of the model, or null if the model or its render method is invalid. @@ -369,7 +315,6 @@ export const FlowModelRenderer: React.FC = observer( flowSettingsVariant = 'dropdown', hideRemoveInSettings = false, showTitle = false, - skipApplyAutoFlows = false, inputArgs, showErrorFallback = true, settingsMenuLevel, @@ -381,19 +326,8 @@ export const FlowModelRenderer: React.FC = observer( return null; } - // 构建渲染内容 - const content = skipApplyAutoFlows ? ( - - ) : ( + // 构建渲染内容:统一在渲染前触发 beforeRender 事件(带缓存) + const content = ( { - + , @@ -141,7 +141,7 @@ describe('Delete problematic model via FlowSettings menu', () => { return (
{items.map((m: FlowModel) => ( - + ))}
); @@ -167,7 +167,7 @@ describe('Delete problematic model via FlowSettings menu', () => { - + , @@ -186,7 +186,7 @@ describe('Delete problematic model via FlowSettings menu', () => { return (
{cells.map((m: FlowModel) => ( - + ))}
); @@ -212,7 +212,7 @@ describe('Delete problematic model via FlowSettings menu', () => { - + , diff --git a/packages/core/flow-engine/src/executor/FlowExecutor.ts b/packages/core/flow-engine/src/executor/FlowExecutor.ts index 53ab0b0a369..2820190c8a9 100644 --- a/packages/core/flow-engine/src/executor/FlowExecutor.ts +++ b/packages/core/flow-engine/src/executor/FlowExecutor.ts @@ -11,6 +11,7 @@ import _ from 'lodash'; import { FlowRuntimeContext } from '../flowContext'; import { FlowEngine } from '../flowEngine'; import type { FlowModel } from '../models'; +import type { DispatchEventOptions } from '../types'; import type { ActionDefinition, ApplyFlowCacheEntry, StepDefinition } from '../types'; import { FlowExitException, resolveDefaultParams } from '../utils'; import { FlowExitAllException } from '../utils/exceptions'; @@ -19,6 +20,39 @@ import { setupRuntimeContextSteps } from '../utils/setupRuntimeContextSteps'; export class FlowExecutor { constructor(private readonly engine: FlowEngine) {} + /** Cache wrapper for applyFlow cache lifecycle */ + private async withApplyFlowCache(cacheKey: string | null, executor: () => Promise): Promise { + if (!cacheKey || !this.engine) return await executor(); + + const cachedEntry = this.engine.applyFlowCache.get(cacheKey); + if (cachedEntry) { + if (cachedEntry.status === 'resolved') return cachedEntry.data as T; + if (cachedEntry.status === 'rejected') throw cachedEntry.error; + if (cachedEntry.status === 'pending') return await cachedEntry.promise; + } + + const promise = executor() + .then((result) => { + this.engine.applyFlowCache.set(cacheKey, { + status: 'resolved', + data: result, + promise: Promise.resolve(result), + } as ApplyFlowCacheEntry); + return result; + }) + .catch((err) => { + this.engine.applyFlowCache.set(cacheKey, { + status: 'rejected', + error: err, + promise: Promise.reject(err), + } as ApplyFlowCacheEntry); + throw err; + }); + + this.engine.applyFlowCache.set(cacheKey, { status: 'pending', promise } as ApplyFlowCacheEntry); + return await promise; + } + /** * Execute a single flow on model. */ @@ -46,29 +80,31 @@ export class FlowExecutor { let lastResult: any; const stepResults: Record = flowContext.stepResults; + + // Build steps with optional eventStep injection (when flow.on is configured) let eventStep = model.getEvent(typeof flow.on === 'string' ? flow.on : (flow.on as any)?.eventName); if (eventStep) { - eventStep = { ...eventStep }; // clone to avoid side effects - eventStep.defaultParams = { ..._.get(flow, 'on.defaultParams', {}), ...eventStep.defaultParams }; + eventStep = { ...eventStep } as any; // clone to avoid side effects + (eventStep as any).defaultParams = { + ..._.get(flow, 'on.defaultParams', {}), + ...(eventStep as any).defaultParams, + }; } - // Execute the event step first since it's usually the trigger condition - if the condition is not met, subsequent steps don't need to execute - const stepDefs = eventStep ? { eventStep, ...flow.steps } : flow.steps; // Record + const stepDefs: Record = eventStep + ? { eventStep: eventStep as any, ...flow.steps } + : flow.steps; // Setup steps meta and runtime mapping setupRuntimeContextSteps(flowContext, stepDefs, model, flowKey); const stepsRuntime = flowContext.steps as Record; - for (const stepKey in stepDefs) { - if (!Object.prototype.hasOwnProperty.call(stepDefs, stepKey)) continue; - const step: StepDefinition = stepDefs[stepKey]; + for (const [stepKey, step] of Object.entries(stepDefs) as [string, StepDefinition][]) { + // Resolve handler and params let handler: ActionDefinition['handler'] | undefined; let combinedParams: Record = {}; - let actionDefinition: ActionDefinition | undefined; let useRawParams: StepDefinition['useRawParams'] = step.useRawParams; - if (step.use) { - // Step references a registered action - actionDefinition = model.getAction(step.use); + const actionDefinition = model.getAction(step.use); if (!actionDefinition) { flowContext.logger.error( `BaseModel.applyFlow: Action '${step.use}' not found for step '${stepKey}' in flow '${flowKey}'. Skipping.`, @@ -102,7 +138,6 @@ export class FlowExecutor { if (!useRawParams) { combinedParams = await flowContext.resolveJsonTemplate(combinedParams); } - try { if (!handler) { flowContext.logger.error( @@ -114,9 +149,8 @@ export class FlowExecutor { const isAwait = step.isAwait !== false; lastResult = isAwait ? await currentStepResult : currentStepResult; - // Store step result + // Store step result and update context stepResults[stepKey] = lastResult; - // update the context stepsRuntime[stepKey].result = stepResults[stepKey]; } catch (error) { if (error instanceof FlowExitException) { @@ -125,7 +159,6 @@ export class FlowExecutor { } if (error instanceof FlowExitAllException) { flowContext.logger.info(`[FlowEngine] ${error.message}`); - // 传递特殊控制信号,让上层可中止后续流程 return Promise.resolve(error); } flowContext.logger.error( @@ -138,119 +171,132 @@ export class FlowExecutor { return Promise.resolve(stepResults); } - /** - * Execute all auto-apply flows for model. - */ - async runAutoFlows(model: FlowModel, inputArgs?: Record, useCache = true): Promise { - const autoApplyFlows = model.getAutoFlows(); - - if (autoApplyFlows.length === 0) { - model.context.logger.warn(`FlowModel: No auto-apply flows found for model '${model.uid}'`); - return []; - } - - const cacheKey = useCache - ? FlowEngine.generateApplyFlowCacheKey(model.getAutoFlowCacheScope(), 'all', model.uid) - : null; - - if (cacheKey && this.engine) { - const cachedEntry = this.engine.applyFlowCache.get(cacheKey); - if (cachedEntry) { - if (cachedEntry.status === 'resolved') { - model.context.logger.debug(`[FlowEngine.applyAutoFlows] Using cached result for model: ${model.uid}`); - return cachedEntry.data; - } - if (cachedEntry.status === 'rejected') throw cachedEntry.error; - if (cachedEntry.status === 'pending') return await cachedEntry.promise; - } - } - - const executeAutoFlows = async (): Promise => { - const results: any[] = []; - const runId = `${model.uid}-autoFlow-${Date.now()}-${Math.floor(Math.random() * 1000)}`; - const logger = model.context.logger.child({ module: 'flow-engine', component: 'FlowExecutor', runId }); - logger.debug( - `[FlowExecutor] runAutoFlows: uid=${model.uid}, isFork=${ - (model as any)?.isFork === true - }, useCache=${useCache}, runId=${runId}, flows=${autoApplyFlows.map((f) => f.key).join(',')}`, - ); - try { - if (autoApplyFlows.length === 0) { - logger.warn(`FlowModel: No auto-apply flows found for model '${model.uid}'`); - } else { - for (const flow of autoApplyFlows) { - try { - logger.debug(`[FlowExecutor] runFlow: uid=${model.uid}, flowKey=${flow.key}`); - const result = await this.runFlow(model, flow.key, inputArgs, runId); - if (result instanceof FlowExitAllException) { - logger.debug(`[FlowEngine.applyAutoFlows] ${result.message}`); - break; // 终止后续流程执行 - } - results.push(result); - } catch (error) { - logger.error({ err: error }, `FlowModel.applyAutoFlows: Error executing auto-apply flow '${flow.key}':`); - throw error; - } - } - } - return results; - } catch (error) { - if (error instanceof FlowExitException) { - logger.debug(`[FlowEngine.applyAutoFlows] ${error.message}`); - return results; - } - throw error; - } - }; - - if (!cacheKey || !this.engine) { - return await executeAutoFlows(); - } - - const promise = executeAutoFlows() - .then((result) => { - this.engine.applyFlowCache.set(cacheKey, { - status: 'resolved', - data: result, - promise: Promise.resolve(result), - } as ApplyFlowCacheEntry); - return result; - }) - .catch((err) => { - this.engine.applyFlowCache.set(cacheKey, { - status: 'rejected', - error: err, - promise: Promise.reject(err), - } as ApplyFlowCacheEntry); - throw err; - }); - - this.engine.applyFlowCache.set(cacheKey, { status: 'pending', promise } as ApplyFlowCacheEntry); - return await promise; - } + // runAutoFlows 已移除:统一通过 dispatchEvent('beforeRender') + useCache 控制 /** * Dispatch an event to flows bound via flow.on and execute them. */ - async dispatchEvent(model: FlowModel, eventName: string, inputArgs?: Record): Promise { - const flows = Array.from(model.getFlows().values()).filter((flow) => { - const on = flow.on; - if (!on) return false; - if (typeof on === 'string') return on === eventName; - if (typeof on === 'object') return on.eventName === eventName; - return false; - }); + async dispatchEvent( + model: FlowModel, + eventName: string, + inputArgs?: Record, + options?: DispatchEventOptions, + ): Promise { + const isBeforeRender = eventName === 'beforeRender'; + // 由模型层决定缺省值;执行器仅按显式 options 执行 + const sequential = !!options?.sequential; + const useCache = !!options?.useCache; + // beforeRender 特殊处理:出错时一律抛出(用于错误边界捕获) + const throwOnError = isBeforeRender; + const runId = `${model.uid}-${eventName}-${Date.now()}`; const logger = model.context.logger; - const promises = flows.map((flow) => { - logger.debug(`BaseModel '${model.uid}' dispatching event '${eventName}' to flow '${flow.key}'.`); - return this.runFlow(model, flow.key, inputArgs, runId).catch((error) => { - logger.error( - { err: error }, - `BaseModel.dispatchEvent: Error executing event-triggered flow '${flow.key}' for event '${eventName}':`, - ); - }); - }); - await Promise.all(promises); + + try { + await model.onDispatchEventStart?.(eventName, options, inputArgs); + } catch (err) { + if (isBeforeRender && err instanceof FlowExitException) { + logger.debug(`[FlowModel.dispatchEvent] ${err.message}`); + return []; + } + // 进入错误钩子并记录 + try { + await model.onDispatchEventError?.(eventName, options, inputArgs, err as Error); + } finally { + logger.error({ err }, `BaseModel.dispatchEvent: Start hook error for event '${eventName}'`); + } + if (throwOnError) throw err; + return; + } + + // 构造待执行的 flows 列表 + const flows = isBeforeRender + ? model.getEventFlows('beforeRender') // beforeRender 事件集合(兼容未声明 on 且非 manual 的定义) + : Array.from(model.getFlows().values()).filter((flow) => { + const on = flow.on; + if (!on) return false; + if (typeof on === 'string') return on === eventName; + if (typeof on === 'object') return on.eventName === eventName; + return false; + }); + + // 组装执行函数(返回值用于缓存;beforeRender 返回 results:any[],其它返回 true) + const execute = async () => { + if (sequential) { + const ordered = flows.slice().sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0)); + const results: any[] = []; + for (const flow of ordered) { + try { + logger.debug( + `BaseModel '${model.uid}' dispatching event '${eventName}' to flow '${flow.key}' (sequential).`, + ); + const result = await this.runFlow(model, flow.key, inputArgs, runId); + if (result instanceof FlowExitAllException) { + logger.debug(`[FlowEngine.dispatchEvent] ${result.message}`); + break; // 终止后续 + } + results.push(result); + } catch (error) { + logger.error( + { err: error }, + `BaseModel.dispatchEvent: Error executing event-triggered flow '${flow.key}' for event '${eventName}' (sequential):`, + ); + throw error; + } + } + return results; + } + + // 并行 + const results = await Promise.all( + flows.map(async (flow) => { + logger.debug(`BaseModel '${model.uid}' dispatching event '${eventName}' to flow '${flow.key}'.`); + try { + return await this.runFlow(model, flow.key, inputArgs, runId); + } catch (error) { + logger.error( + { err: error }, + `BaseModel.dispatchEvent: Error executing event-triggered flow '${flow.key}' for event '${eventName}':`, + ); + if (throwOnError) throw error; + return undefined; + } + }), + ); + return results.filter((x) => x !== undefined); + }; + + // 缓存键:按事件+scope 统一管理(beforeRender 也使用事件名 beforeRender) + const argsKey = useCache ? JSON.stringify(inputArgs ?? {}) : ''; + const cacheKey = useCache + ? FlowEngine.generateApplyFlowCacheKey( + `event:${model.getFlowCacheScope(eventName)}:${argsKey}`, + eventName, + model.uid, + ) + : null; + + try { + const result = await this.withApplyFlowCache(cacheKey, execute); + // 事件结束钩子 + try { + await model.onDispatchEventEnd?.(eventName, options, inputArgs, result); + } catch (hookErr) { + logger.error({ err: hookErr }, `BaseModel.dispatchEvent: End hook error for event '${eventName}'`); + } + return result; + } catch (error) { + // 进入错误钩子并记录 + try { + await model.onDispatchEventError?.(eventName, options, inputArgs, error as Error); + } catch (_) { + // swallow secondary hook error + } + model.context.logger.error( + { err: error }, + `BaseModel.dispatchEvent: Error executing event '${eventName}' for model '${model.uid}':`, + ); + if (throwOnError) throw error; + } } } diff --git a/packages/core/flow-engine/src/executor/__tests__/flowExecutor.test.ts b/packages/core/flow-engine/src/executor/__tests__/flowExecutor.test.ts index 2d4a7d37107..16cfc82e03e 100644 --- a/packages/core/flow-engine/src/executor/__tests__/flowExecutor.test.ts +++ b/packages/core/flow-engine/src/executor/__tests__/flowExecutor.test.ts @@ -81,7 +81,7 @@ describe('FlowExecutor', () => { expect(result.step2).toBe('step2-ok'); }); - it('runAutoFlows executes only auto flows in sort order and caches result', async () => { + it("dispatchEvent('beforeRender') executes flows in sort order and caches result (when options specify)", async () => { const calls: string[] = []; const mkFlow = (key: string, sort: number) => ({ sort, @@ -104,15 +104,21 @@ describe('FlowExecutor', () => { const model = createModelWithFlows('m-auto', flows); - const r1 = await engine.executor.runAutoFlows(model); - expect(r1).toHaveLength(2); + const r1 = await engine.executor.dispatchEvent(model, 'beforeRender', undefined, { + sequential: true, + useCache: true, + }); + expect(Array.isArray(r1)).toBe(true); // sort order: f1 then f2 expect(calls).toEqual(['f1', 'f2']); // second run should hit cache and not execute handlers again const prevCount = calls.length; - const r2 = await engine.executor.runAutoFlows(model); - expect(r2).toEqual(r1); + const r2 = await engine.executor.dispatchEvent(model, 'beforeRender', undefined, { + sequential: true, + useCache: true, + }); + expect(Array.isArray(r2)).toBe(true); expect(calls.length).toBe(prevCount); }); @@ -133,6 +139,146 @@ describe('FlowExecutor', () => { expect(submitHandler).not.toHaveBeenCalled(); }); + it('dispatchEvent default parallel does not stop on exitAll', async () => { + const calls: string[] = []; + const mkFlow = (key: string, opts?: { exitAll?: boolean }) => ({ + on: { eventName: 'par' }, + steps: { + only: { + handler: vi.fn().mockImplementation((ctx) => { + calls.push(key); + if (opts?.exitAll) ctx.exitAll(); + }), + }, + }, + }); + + const flows = { + a: mkFlow('a', { exitAll: true }), + b: mkFlow('b'), + } satisfies Record>; + + const model = createModelWithFlows('m-par', flows); + + await engine.executor.dispatchEvent(model, 'par'); + + // 在并行模式下,b 不会被 exitAll 中断 + expect(calls.sort()).toEqual(['a', 'b']); + }); + + it('dispatchEvent sequential respects sort order and stops on errors', async () => { + const calls: string[] = []; + const mkFlow = (key: string, sort: number, opts?: { throw?: boolean }) => ({ + on: { eventName: 'seq2' }, + sort, + steps: { + only: { + handler: vi.fn().mockImplementation(() => { + calls.push(key); + if (opts?.throw) throw new Error('boom'); + }), + }, + }, + }); + + const flows = { + f2: mkFlow('f2', 2), + f1: mkFlow('f1', 1, { throw: true }), + f3: mkFlow('f3', 3), + } satisfies Record>; + + const model = createModelWithFlows('m-seq2', flows); + await engine.executor.dispatchEvent(model, 'seq2', {}, { sequential: true }); + + // 顺序:按 sort 执行;f1 抛错后终止,f2 和 f3 不会执行 + expect(calls).toEqual(['f1']); + }); + + it('dispatchEvent caches non-beforeRender when useCache=true', async () => { + const handler = vi.fn(); + const flows = { + onFoo: { on: 'foo', steps: { s: { handler } } }, + } satisfies Record>; + const model = createModelWithFlows('m-evt-cache', flows); + + await engine.executor.dispatchEvent(model, 'foo', {}, { useCache: true }); + await engine.executor.dispatchEvent(model, 'foo', {}, { useCache: true }); + expect(handler).toHaveBeenCalledTimes(1); + }); + + it("dispatchEvent('beforeRender') runs once due to cache (when options specify)", async () => { + const handler = vi.fn(); + const flows = { + auto1: { steps: { s: { handler } } }, // 无 on → 视为 beforeRender + auto2: { on: 'beforeRender', steps: { s: { handler } } }, + } satisfies Record>; + + const model = createModelWithFlows('m-br-cache', flows); + + await engine.executor.dispatchEvent(model, 'beforeRender', undefined, { sequential: true, useCache: true }); + await engine.executor.dispatchEvent(model, 'beforeRender', undefined, { sequential: true, useCache: true }); + + // 两次触发但步骤处理只执行一次(命中缓存) + expect(handler).toHaveBeenCalledTimes(2); // 每个 flow 各 1 次,共 2 次 + }); + + it('dispatchEvent supports sequential execution order and exitAll break', async () => { + const calls: string[] = []; + const mkFlow = (key: string, sort: number, opts?: { exitAll?: boolean }) => ({ + on: { eventName: 'seq' }, + sort, + steps: { + only: { + handler: vi.fn().mockImplementation((ctx) => { + calls.push(key); + if (opts?.exitAll) ctx.exitAll(); + }), + }, + }, + }); + + const flows = { + f1: mkFlow('f1', 1, { exitAll: true }), + f2: mkFlow('f2', 2), + } satisfies Record>; + + const model = createModelWithFlows('m-seq', flows); + + await engine.executor.dispatchEvent(model, 'seq', {}, { sequential: true }); + + // f1 executed then exitAll stops f2 + expect(calls).toEqual(['f1']); + }); + + it("dispatchEvent('beforeRender') caches results (key includes eventName) when options specify", async () => { + const handler = vi.fn().mockResolvedValue('ok'); + const flows = { + f1: { steps: { s: { handler } } }, + } satisfies Record>; + const model = createModelWithFlows('m-br', flows); + + await engine.executor.dispatchEvent(model, 'beforeRender', undefined, { sequential: true, useCache: true }); + await engine.executor.dispatchEvent(model, 'beforeRender', undefined, { sequential: true, useCache: true }); + + // handler only executed once because of cache + expect(handler).toHaveBeenCalledTimes(1); + }); + + it("dispatchEvent('beforeRender') rejects when a step throws", async () => { + const handler = vi.fn().mockImplementation(() => { + throw new Error('boom-br'); + }); + const flows = { + bad: { steps: { s: { handler } } }, + } satisfies Record>; + + const model = createModelWithFlows('m-br-reject', flows); + + await expect(engine.executor.dispatchEvent(model, 'beforeRender', undefined, { sequential: true })).rejects.toThrow( + 'boom-br', + ); + }); + it('instance flow overrides global flow with same key', async () => { class MyModel extends FlowModel {} diff --git a/packages/core/flow-engine/src/flowEngine.ts b/packages/core/flow-engine/src/flowEngine.ts index a86c5816d62..c7b04583653 100644 --- a/packages/core/flow-engine/src/flowEngine.ts +++ b/packages/core/flow-engine/src/flowEngine.ts @@ -130,7 +130,7 @@ export class FlowEngine { */ public reactView: ReactView; /** - * Flow executor that runs flows and auto-flows. + * Flow executor that runs event flows. */ public executor: FlowExecutor; @@ -803,7 +803,7 @@ export class FlowEngine { // 7. 触发事件以通知其他部分模型已替换 if (currentParent) { currentParent.emitter.setPaused(false); - currentParent.parent.invalidateAutoFlowCache(true); + currentParent.parent.invalidateFlowCache('beforeRender', true); currentParent.parent?.rerender(); currentParent.emitter.emit('onSubModelReplaced', { oldModel, newModel }); } diff --git a/packages/core/flow-engine/src/hooks/useApplyAutoFlows.ts b/packages/core/flow-engine/src/hooks/useApplyAutoFlows.ts index a44713557cd..1550934c5f3 100644 --- a/packages/core/flow-engine/src/hooks/useApplyAutoFlows.ts +++ b/packages/core/flow-engine/src/hooks/useApplyAutoFlows.ts @@ -35,7 +35,9 @@ export function useApplyAutoFlows( const { loading, error } = useRequest( async () => { - await model.applyAutoFlows(inputArgs); + if (!model) return; + // beforeRender 在模型层默认顺序执行并默认使用缓存(可覆盖) + await model.dispatchEvent('beforeRender', inputArgs); }, { refreshDeps: [model, inputArgs], diff --git a/packages/core/flow-engine/src/models/__tests__/flowModel.getFlows.sort.test.ts b/packages/core/flow-engine/src/models/__tests__/flowModel.getFlows.sort.test.ts index 40f6e93b4c1..dfe51efbc1e 100644 --- a/packages/core/flow-engine/src/models/__tests__/flowModel.getFlows.sort.test.ts +++ b/packages/core/flow-engine/src/models/__tests__/flowModel.getFlows.sort.test.ts @@ -10,13 +10,13 @@ import { describe, expect, test, beforeEach } from 'vitest'; import { FlowModel } from '@nocobase/flow-engine'; -describe('FlowModel.getFlows sorting and getAutoFlows order', () => { +describe('FlowModel.getFlows sorting and getEventFlows(beforeRender) order', () => { let TestFlowModel: typeof FlowModel; let model: FlowModel; beforeEach(() => { // use a fresh subclass to isolate class-level registries - TestFlowModel = class extends FlowModel {}; + TestFlowModel = class extends FlowModel {}; // provide a minimal fake engine to avoid circular deps in tests const fakeEngine = { getModel: () => undefined } as any; model = new TestFlowModel({ flowEngine: fakeEngine } as any); @@ -37,7 +37,7 @@ describe('FlowModel.getFlows sorting and getAutoFlows order', () => { expect(orderedKeys).toEqual(['flowD', 'flowB', 'flowC', 'flowA']); }); - test('getAutoFlows keeps getFlows order and filters out manual/on flows', () => { + test("getEventFlows('beforeRender') keeps getFlows order and filters out manual/on flows", () => { // class-level TestFlowModel.registerFlow('flowA', { title: 'A', sort: 10, steps: {} }); TestFlowModel.registerFlow('flowB', { title: 'B', sort: 5, steps: {} }); @@ -51,7 +51,7 @@ describe('FlowModel.getFlows sorting and getAutoFlows order', () => { model.registerFlow('manualFlow', { title: 'Manual', manual: true, sort: -1, steps: {} }); const getFlowsOrder = Array.from(model.getFlows().keys()); - const autoFlowKeys = model.getAutoFlows().map((f) => f.key); + const autoFlowKeys = model.getEventFlows('beforeRender').map((f) => f.key); // auto flows should exclude event/manual flows expect(autoFlowKeys).toEqual(['flowD', 'flowB', 'flowC', 'flowE', 'flowA']); diff --git a/packages/core/flow-engine/src/models/__tests__/flowModel.test.ts b/packages/core/flow-engine/src/models/__tests__/flowModel.test.ts index 1f8a2e7984b..4aa7e29bb6e 100644 --- a/packages/core/flow-engine/src/models/__tests__/flowModel.test.ts +++ b/packages/core/flow-engine/src/models/__tests__/flowModel.test.ts @@ -7,18 +7,17 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { autorun, observable, reaction, reactive } from '@nocobase/flow-engine'; -import { APIClient } from '@nocobase/sdk'; +import { reaction } from '@nocobase/flow-engine'; import { render, waitFor } from '@testing-library/react'; import React from 'react'; import { vi } from 'vitest'; import { FlowModelRenderer } from '../../components/FlowModelRenderer'; +import { FlowEngineProvider } from '../../provider'; import { FlowEngine } from '../../flowEngine'; -import type { DefaultStructure, FlowDefinitionOptions, FlowModelOptions, ModelConstructor } from '../../types'; +import type { DefaultStructure, FlowDefinitionOptions, FlowModelOptions } from '../../types'; import { FlowExitException } from '../../utils'; import { FlowExitAllException } from '../../utils/exceptions'; -import { defineFlow, FlowModel, ModelRenderMode } from '../flowModel'; -import { ForkFlowModel } from '../forkFlowModel'; +import { FlowModel, ModelRenderMode } from '../flowModel'; // 全局处理测试中的未处理 Promise rejection const originalUnhandledRejection = process.listeners('unhandledRejection'); @@ -392,7 +391,7 @@ describe('FlowModel', () => { TestFlowModel.registerFlow(exitFlow2); const loggerSpy = vi.spyOn(model.context.logger, 'info').mockImplementation(() => {}); - await model.applyAutoFlows(); + await model.dispatchEvent('beforeRender'); expect(exitFlow.steps.step2.handler).not.toHaveBeenCalled(); expect(exitFlow2.steps.step2.handler).toHaveBeenCalled(); @@ -430,7 +429,7 @@ describe('FlowModel', () => { TestFlowModel.registerFlow(exitFlow2); const loggerSpy = vi.spyOn(model.context.logger, 'info').mockImplementation(() => {}); - await model.applyAutoFlows(); + await model.dispatchEvent('beforeRender'); expect(exitFlow.steps.step2.handler).not.toHaveBeenCalled(); expect(exitFlow2.steps.step2.handler).not.toHaveBeenCalled(); @@ -530,7 +529,7 @@ describe('FlowModel', () => { }); }); - describe('applyAutoFlows', () => { + describe('beforeRender flows', () => { test('should execute all auto flows', async () => { const autoFlow1 = { ...createAutoFlowDefinition(), key: 'auto1', sort: 1 }; const autoFlow2 = { ...createAutoFlowDefinition(), key: 'auto2', sort: 2 }; @@ -540,7 +539,7 @@ describe('FlowModel', () => { TestFlowModel.registerFlow(autoFlow2); TestFlowModel.registerFlow(manualFlow); - const results = await model.applyAutoFlows(); + const results = await model.dispatchEvent('beforeRender'); expect(results).toHaveLength(2); expect(autoFlow1.steps.autoStep.handler).toHaveBeenCalled(); @@ -594,13 +593,13 @@ describe('FlowModel', () => { TestFlowModel.registerFlow(autoFlow2); TestFlowModel.registerFlow(autoFlow3); - await model.applyAutoFlows(); + await model.dispatchEvent('beforeRender'); expect(executionOrder).toEqual(['auto2', 'auto3', 'auto1']); }); test('should no results when no auto flows found', async () => { - const results = await model.applyAutoFlows(); + const results = await model.dispatchEvent('beforeRender'); expect(results).toEqual([]); // Note: Log output may be captured in stderr, not console.log @@ -617,16 +616,14 @@ describe('FlowModel', () => { afterHookSpy = vi.fn(); errorHookSpy = vi.fn(); TestFlowModelWithHooks = class extends TestFlowModel { - async onBeforeAutoFlows(inputArgs?: Record) { - beforeHookSpy(inputArgs); + async onDispatchEventStart(eventName: string, _opts?: any, inputArgs?: Record) { + if (eventName === 'beforeRender') beforeHookSpy(inputArgs); } - - async onAfterAutoFlows(results: any[], inputArgs?: Record) { - afterHookSpy(results, inputArgs); + async onDispatchEventEnd(eventName: string, _opts?: any, inputArgs?: Record, results?: any[]) { + if (eventName === 'beforeRender') afterHookSpy(results, inputArgs); } - - async onAutoFlowsError(error: Error, inputArgs?: Record) { - errorHookSpy(error, inputArgs); + async onDispatchEventError(eventName: string, _opts?: any, inputArgs?: Record, error?: Error) { + if (eventName === 'beforeRender') errorHookSpy(error, inputArgs); } }; }); @@ -638,7 +635,7 @@ describe('FlowModel', () => { const modelWithHooks = new TestFlowModelWithHooks(modelOptions); const inputArgs = { test: 'value' }; - const results = await modelWithHooks.applyAutoFlows(inputArgs); + const results = await modelWithHooks.dispatchEvent('beforeRender', inputArgs); // Verify hooks were called expect(beforeHookSpy).toHaveBeenCalledTimes(1); @@ -654,22 +651,22 @@ describe('FlowModel', () => { ); }); - test('should allow onBeforeAutoFlows to terminate flow via ctx.exit()', async () => { + test("should allow onDispatchEventStart('beforeRender') to terminate flow via ctx.exit()", async () => { const autoFlow1 = { ...createAutoFlowDefinition(), key: 'auto1' }; const autoFlow2 = { ...createAutoFlowDefinition(), key: 'auto2' }; const TestFlowModelWithExitHooks = class extends TestFlowModel { - async onBeforeAutoFlows(inputArgs?: Record) { - beforeHookSpy(inputArgs); - throw new FlowExitException('autoFlows', this.uid); + async onDispatchEventStart(eventName: string, _opts?: any, inputArgs?: Record) { + if (eventName === 'beforeRender') { + beforeHookSpy(inputArgs); + throw new FlowExitException('beforeRender', this.uid); + } } - - async onAfterAutoFlows(results: any[], inputArgs?: Record) { - afterHookSpy(results, inputArgs); + async onDispatchEventEnd(eventName: string, _o?: any, _i?: any, _r?: any[]) { + if (eventName === 'beforeRender') afterHookSpy(_r, _i); } - - async onAutoFlowsError(error: Error, inputArgs?: Record) { - errorHookSpy(error, inputArgs); + async onDispatchEventError(eventName: string, _o?: any, i?: any, e?: Error) { + if (eventName === 'beforeRender') errorHookSpy(e, i); } }; @@ -678,9 +675,9 @@ describe('FlowModel', () => { TestFlowModelWithExitHooks.registerFlow(autoFlow2); const modelWithHooks = new TestFlowModelWithExitHooks(modelOptions); - const results = await modelWithHooks.applyAutoFlows(); + const results = await modelWithHooks.dispatchEvent('beforeRender'); - // Should have called onBeforeAutoFlows but not onAfterAutoFlows + // Should have called onDispatchEventStart but not onDispatchEventEnd expect(beforeHookSpy).toHaveBeenCalledTimes(1); expect(afterHookSpy).not.toHaveBeenCalled(); expect(errorHookSpy).not.toHaveBeenCalled(); @@ -688,12 +685,12 @@ describe('FlowModel', () => { // Should return empty results since flow was terminated early expect(results).toEqual([]); - // Auto flows should not have been executed + // flows should not have been executed expect(autoFlow1.steps.autoStep.handler).not.toHaveBeenCalled(); expect(autoFlow2.steps.autoStep.handler).not.toHaveBeenCalled(); }); - test('should call onAutoFlowsError when flow execution fails', async () => { + test("should call onDispatchEventError('beforeRender') when flow execution fails", async () => { const errorFlow = { key: 'errorFlow', @@ -708,11 +705,8 @@ describe('FlowModel', () => { TestFlowModelWithHooks.registerFlow(errorFlow); const modelWithHooks = new TestFlowModelWithHooks(modelOptions); - - // 测试错误处理钩子功能 - await expect(modelWithHooks.applyAutoFlows()).rejects.toThrow('Test error'); - - // Verify hooks were called + await modelWithHooks.dispatchEvent('beforeRender'); + // Verify hooks were called (error path) expect(beforeHookSpy).toHaveBeenCalledTimes(1); expect(afterHookSpy).not.toHaveBeenCalled(); expect(errorHookSpy).toHaveBeenCalledTimes(1); @@ -726,14 +720,14 @@ describe('FlowModel', () => { ); }); - test('should provide access to step results in onAfterAutoFlows', async () => { + test('should provide access to step results in onDispatchEventEnd(beforeRender)', async () => { const autoFlow1 = { ...createAutoFlowDefinition(), key: 'auto1' }; const autoFlow2 = { ...createAutoFlowDefinition(), key: 'auto2' }; TestFlowModelWithHooks.registerFlow(autoFlow1); TestFlowModelWithHooks.registerFlow(autoFlow2); const modelWithHooks = new TestFlowModelWithHooks(modelOptions); - await modelWithHooks.applyAutoFlows(); + await modelWithHooks.dispatchEvent('beforeRender'); expect(afterHookSpy).toHaveBeenCalledTimes(1); @@ -777,6 +771,82 @@ describe('FlowModel', () => { } }); + test('getFlows includes beforeRender and no-on flows; excludes manual and other events', async () => { + const TestM = class extends FlowModel {}; + const beforeHandler = vi.fn(); + const noOnHandler = vi.fn(); + const manualHandler = vi.fn(); + + TestM.registerFlow({ key: 'beforeA', on: 'beforeRender', sort: 2, steps: { s: { handler: beforeHandler } } }); + TestM.registerFlow({ key: 'noOnB', sort: 1, steps: { s: { handler: noOnHandler } } }); + TestM.registerFlow({ key: 'manualSkip', manual: true, steps: { s: { handler: manualHandler } } }); + TestM.registerFlow({ key: 'otherEvent', on: 'click', steps: { s: { handler: vi.fn() } } }); + + const m = new TestM(modelOptions); + const autoKeys = m.getEventFlows('beforeRender').map((f) => f.key); + + // sort 按 1,2 排序:noOnB -> beforeA + expect(autoKeys).toEqual(['noOnB', 'beforeA']); + + // beforeRender 会运行两者 + await m.dispatchEvent('beforeRender'); + expect(beforeHandler).toHaveBeenCalledTimes(1); + expect(noOnHandler).toHaveBeenCalledTimes(1); + expect(manualHandler).not.toHaveBeenCalled(); + }); + + test('beforeRender executes in sort order mixing no-on and beforeRender', async () => { + const TestM = class extends FlowModel {}; + const calls: string[] = []; + TestM.registerFlow({ key: 'noOn1', sort: 1, steps: { s: { handler: () => calls.push('noOn1') } } }); + TestM.registerFlow({ + key: 'before2', + on: 'beforeRender', + sort: 2, + steps: { s: { handler: () => calls.push('before2') } }, + }); + TestM.registerFlow({ key: 'noOn3', sort: 3, steps: { s: { handler: () => calls.push('noOn3') } } }); + + const m = new TestM(modelOptions); + await m.dispatchEvent('beforeRender'); + expect(calls).toEqual(['noOn1', 'before2', 'noOn3']); + }); + + test("model.dispatchEvent('beforeRender') uses cache keyed by eventName", async () => { + const TestM = class extends FlowModel {}; + const handler = vi.fn(); + TestM.registerFlow({ key: 'autoA', steps: { s: { handler } } }); + + const m = new TestM(modelOptions); + await m.dispatchEvent('beforeRender'); + await m.dispatchEvent('beforeRender'); + expect(handler).toHaveBeenCalledTimes(1); + }); + + test('invalidFlowCache should clear specific event cache', async () => { + const TestM = class extends FlowModel {}; + const h1 = vi.fn(); + const h2 = vi.fn(); + TestM.registerFlow({ key: 'onFoo', on: 'foo', steps: { s: { handler: h1 } } }); + TestM.registerFlow({ key: 'onBar', on: 'bar', steps: { s: { handler: h2 } } }); + + const m = new TestM(modelOptions); + await m.dispatchEvent('foo', {}, { useCache: true }); + await m.dispatchEvent('bar', {}, { useCache: true }); + + // cached + await m.dispatchEvent('foo', {}, { useCache: true }); + await m.dispatchEvent('bar', {}, { useCache: true }); + expect(h1).toHaveBeenCalledTimes(1); + expect(h2).toHaveBeenCalledTimes(1); + + // invalidate foo only + m.invalidateFlowCache('foo'); + await m.dispatchEvent('foo', {}, { useCache: true }); + await m.dispatchEvent('bar', {}, { useCache: true }); + expect(h1).toHaveBeenCalledTimes(2); + expect(h2).toHaveBeenCalledTimes(1); + }); test('should handle multiple flows for same event', async () => { const eventFlow1 = { ...createEventFlowDefinition('sharedEvent'), key: 'event1' }; const eventFlow2 = { ...createEventFlowDefinition('sharedEvent'), key: 'event2' }; @@ -802,9 +872,13 @@ describe('FlowModel', () => { const _dispatchEventWithDebounceSpy = vi.spyOn(model as any, '_dispatchEventWithDebounce'); // Test with debounce enabled - await model.dispatchEvent('debouncedEvent', { data: 'test' }, { debounce: true }); + await model.dispatchEvent('debouncedEvent', { data: 'test' }, { debounce: true, sequential: true }); - expect(_dispatchEventWithDebounceSpy).toHaveBeenCalledWith('debouncedEvent', { data: 'test' }); + expect(_dispatchEventWithDebounceSpy).toHaveBeenCalledWith( + 'debouncedEvent', + { data: 'test' }, + expect.objectContaining({ sequential: true }), + ); expect(_dispatchEventSpy).not.toHaveBeenCalled(); _dispatchEventSpy.mockRestore(); @@ -821,7 +895,11 @@ describe('FlowModel', () => { // Test with debounce disabled await model.dispatchEvent('normalEvent', { data: 'test' }, { debounce: false }); - expect(_dispatchEventSpy).toHaveBeenCalledWith('normalEvent', { data: 'test' }); + expect(_dispatchEventSpy).toHaveBeenCalledWith( + 'normalEvent', + { data: 'test' }, + expect.objectContaining({ sequential: undefined }), + ); expect(_dispatchEventWithDebounceSpy).not.toHaveBeenCalled(); _dispatchEventSpy.mockRestore(); @@ -838,7 +916,11 @@ describe('FlowModel', () => { // Test without debounce option await model.dispatchEvent('defaultEvent', { data: 'test' }); - expect(_dispatchEventSpy).toHaveBeenCalledWith('defaultEvent', { data: 'test' }); + expect(_dispatchEventSpy).toHaveBeenCalledWith( + 'defaultEvent', + { data: 'test' }, + expect.objectContaining({ sequential: undefined }), + ); expect(_dispatchEventWithDebounceSpy).not.toHaveBeenCalled(); _dispatchEventSpy.mockRestore(); @@ -855,7 +937,11 @@ describe('FlowModel', () => { // Test with undefined options await model.dispatchEvent('undefinedOptionsEvent', { data: 'test' }, undefined); - expect(_dispatchEventSpy).toHaveBeenCalledWith('undefinedOptionsEvent', { data: 'test' }); + expect(_dispatchEventSpy).toHaveBeenCalledWith( + 'undefinedOptionsEvent', + { data: 'test' }, + expect.objectContaining({ sequential: undefined }), + ); expect(_dispatchEventWithDebounceSpy).not.toHaveBeenCalled(); _dispatchEventSpy.mockRestore(); @@ -1265,41 +1351,63 @@ describe('FlowModel', () => { }); }); - describe('applySubModelsAutoFlows', () => { + describe('applySubModelsBeforeRenderFlows', () => { test('should apply auto flows to all array subModels', async () => { const child1 = new FlowModel({ uid: 'child1', flowEngine }); const child2 = new FlowModel({ uid: 'child2', flowEngine }); - child1.applyAutoFlows = vi.fn().mockResolvedValue([]); - child2.applyAutoFlows = vi.fn().mockResolvedValue([]); + child1.dispatchEvent = vi.fn().mockResolvedValue(undefined) as any; + child2.dispatchEvent = vi.fn().mockResolvedValue(undefined) as any; parentModel.addSubModel('children', child1); parentModel.addSubModel('children', child2); const runtimeData = { test: 'extra' }; - await parentModel.applySubModelsAutoFlows('children', runtimeData); + await parentModel.applySubModelsBeforeRenderFlows('children', runtimeData); - expect(child1.applyAutoFlows).toHaveBeenCalledWith(runtimeData); - expect(child2.applyAutoFlows).toHaveBeenCalledWith(runtimeData); + expect(child1.dispatchEvent).toHaveBeenCalledWith('beforeRender', runtimeData); + expect(child2.dispatchEvent).toHaveBeenCalledWith('beforeRender', runtimeData); }); test('should apply auto flows to single subModel', async () => { const child = new FlowModel({ uid: 'child', flowEngine }); - child.applyAutoFlows = vi.fn().mockResolvedValue([]); + child.dispatchEvent = vi.fn().mockResolvedValue(undefined) as any; parentModel.setSubModel('child', child); const runtimeData = { test: 'extra' }; - await parentModel.applySubModelsAutoFlows('child', runtimeData); + await parentModel.applySubModelsBeforeRenderFlows('child', runtimeData); - expect(child.applyAutoFlows).toHaveBeenCalledWith(runtimeData); + expect(child.dispatchEvent).toHaveBeenCalledWith('beforeRender', runtimeData); }); test('should handle empty subModels gracefully', async () => { - await expect(parentModel.applySubModelsAutoFlows('nonExistent')).resolves.not.toThrow(); + await expect(parentModel.applySubModelsBeforeRenderFlows('nonExistent')).resolves.not.toThrow(); + }); + }); + + describe('beforeRender cache invalidation on inputArgs change', () => { + test('should rerun when inputArgs changed and reuse when equal', async () => { + const TestM = class extends FlowModel {}; + const handler = vi.fn(); + TestM.registerFlow({ key: 'auto1', steps: { s: { handler } } }); + + const m = new TestM(modelOptions); + + // First run with {a:1} + await m.dispatchEvent('beforeRender', { a: 1 }); + expect(handler).toHaveBeenCalledTimes(1); + + // Second run with same args should hit cache -> no extra handler calls + await m.dispatchEvent('beforeRender', { a: 1 }); + expect(handler).toHaveBeenCalledTimes(1); + + // Different args invalidate cache -> handler called again + await m.dispatchEvent('beforeRender', { a: 2 }); + expect(handler).toHaveBeenCalledTimes(2); }); }); @@ -1627,7 +1735,7 @@ describe('FlowModel', () => { }); describe('rendering operations', () => { - test('should not pre-call render for RenderFunction mode and call exactly once on render', () => { + test('should not pre-call render for RenderFunction mode and call exactly once on render', async () => { const renderSpy = vi.fn(() => vi.fn()); class CallbackRenderModel extends FlowModel { @@ -1645,17 +1753,20 @@ describe('FlowModel', () => { const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); try { const { unmount } = render( - React.createElement(FlowModelRenderer, { model: callbackModel, skipApplyAutoFlows: true }), + React.createElement( + FlowEngineProvider, + { engine: callbackModel.flowEngine }, + React.createElement(FlowModelRenderer, { model: callbackModel }), + ), ); - - expect(renderSpy).toHaveBeenCalledTimes(1); + await waitFor(() => expect(renderSpy).toHaveBeenCalledTimes(1)); unmount(); } finally { warnSpy.mockRestore(); } }); - test('should not pre-call render for ReactElement mode and call exactly once on actual render', () => { + test('should not pre-call render for ReactElement mode and call exactly once on actual render', async () => { const renderSpy = vi.fn(() => React.createElement('div', { 'data-testid': 'elt' }, 'Elt')); class ElementRenderModel extends FlowModel { @@ -1670,11 +1781,15 @@ describe('FlowModel', () => { expect(renderSpy).toHaveBeenCalledTimes(0); const { getByTestId, unmount } = render( - React.createElement(FlowModelRenderer, { model: elementModel, skipApplyAutoFlows: true }), + React.createElement( + FlowEngineProvider, + { engine: elementModel.flowEngine }, + React.createElement(FlowModelRenderer, { model: elementModel }), + ), ); // Render should be called exactly once during mount - expect(renderSpy).toHaveBeenCalledTimes(1); + await waitFor(() => expect(renderSpy).toHaveBeenCalledTimes(1)); expect(getByTestId('elt')).toBeTruthy(); unmount(); @@ -1690,13 +1805,15 @@ describe('FlowModel', () => { expect(result.type.displayName).toBe('ReactiveWrapper(FlowModel)'); }); - test('should rerender with previous auto flows', async () => { + test('should rerender triggers beforeRender without cache', async () => { const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - model.applyAutoFlows = vi.fn().mockResolvedValue([]); + model.dispatchEvent = vi.fn().mockResolvedValue(undefined) as any; try { await expect(model.rerender()).resolves.not.toThrow(); - expect(model.applyAutoFlows).toHaveBeenCalled(); + expect(model.dispatchEvent).toHaveBeenCalledWith('beforeRender', undefined, { + useCache: false, + }); } finally { consoleSpy.mockRestore(); } @@ -2039,16 +2156,16 @@ describe('FlowModel', () => { vi.restoreAllMocks(); }); - describe('invalidateAutoFlowCache', () => { + describe('invalidateFlowCache', () => { test('should delete auto flow cache for current model', () => { - const expectedCacheKey = 'autoFlow-all-test-model-uid'; + const expectedCacheKey = 'event:beforeRender-beforeRender-test-model-uid'; realFlowEngine.applyFlowCache.set(expectedCacheKey, { status: 'resolved', data: [], promise: Promise.resolve([]), }); - model.invalidateAutoFlowCache(); + model.invalidateFlowCache('beforeRender'); expect(deleteSpy).toHaveBeenCalledWith(expectedCacheKey); }); @@ -2057,8 +2174,8 @@ describe('FlowModel', () => { const fork1 = model.createFork(); const fork2 = model.createFork(); - const fork1CacheKey = `${fork1.forkId}-all-test-model-uid`; - const fork2CacheKey = `${fork2.forkId}-all-test-model-uid`; + const fork1CacheKey = `event:beforeRender:${fork1.forkId}-beforeRender-test-model-uid`; + const fork2CacheKey = `event:beforeRender:${fork2.forkId}-beforeRender-test-model-uid`; realFlowEngine.applyFlowCache.set(fork1CacheKey, { status: 'resolved', @@ -2071,7 +2188,7 @@ describe('FlowModel', () => { promise: Promise.resolve([]), }); - model.invalidateAutoFlowCache(); + model.invalidateFlowCache('beforeRender'); expect(deleteSpy).toHaveBeenCalledWith(fork1CacheKey); expect(deleteSpy).toHaveBeenCalledWith(fork2CacheKey); @@ -2081,27 +2198,27 @@ describe('FlowModel', () => { const childModel1 = new FlowModel({ uid: 'child1', flowEngine: realFlowEngine }); const childModel2 = new FlowModel({ uid: 'child2', flowEngine: realFlowEngine }); - const child1Spy = vi.spyOn(childModel1, 'invalidateAutoFlowCache'); - const child2Spy = vi.spyOn(childModel2, 'invalidateAutoFlowCache'); + const child1Spy = vi.spyOn(childModel1, 'invalidateFlowCache'); + const child2Spy = vi.spyOn(childModel2, 'invalidateFlowCache'); model.addSubModel('children', childModel1); model.addSubModel('children', childModel2); - model.invalidateAutoFlowCache(true); + model.invalidateFlowCache('beforeRender', true); - expect(child1Spy).toHaveBeenCalledWith(true); - expect(child2Spy).toHaveBeenCalledWith(true); + expect(child1Spy).toHaveBeenCalledWith('beforeRender', true); + expect(child2Spy).toHaveBeenCalledWith('beforeRender', true); }); test('should recursively invalidate cache for object subModels', () => { const childModel = new FlowModel({ uid: 'child', flowEngine: realFlowEngine }); - const childSpy = vi.spyOn(childModel, 'invalidateAutoFlowCache'); + const childSpy = vi.spyOn(childModel, 'invalidateFlowCache'); model.setSubModel('child', childModel); - model.invalidateAutoFlowCache(true); + model.invalidateFlowCache('beforeRender', true); - expect(childSpy).toHaveBeenCalledWith(true); + expect(childSpy).toHaveBeenCalledWith('beforeRender', true); }); test('should handle mixed array and object subModels', () => { @@ -2109,25 +2226,31 @@ describe('FlowModel', () => { const arrayChild2 = new FlowModel({ uid: 'arrayChild2', flowEngine: realFlowEngine }); const objectChild = new FlowModel({ uid: 'objectChild', flowEngine: realFlowEngine }); - const array1Spy = vi.spyOn(arrayChild1, 'invalidateAutoFlowCache'); - const array2Spy = vi.spyOn(arrayChild2, 'invalidateAutoFlowCache'); - const objectSpy = vi.spyOn(objectChild, 'invalidateAutoFlowCache'); + const array1Spy = vi.spyOn(arrayChild1, 'invalidateFlowCache'); + const array2Spy = vi.spyOn(arrayChild2, 'invalidateFlowCache'); + const objectSpy = vi.spyOn(objectChild, 'invalidateFlowCache'); model.addSubModel('arrayChildren', arrayChild1); model.addSubModel('arrayChildren', arrayChild2); model.setSubModel('objectChild', objectChild); - model.invalidateAutoFlowCache(true); + model.invalidateFlowCache('beforeRender', true); - expect(array1Spy).toHaveBeenCalledWith(true); - expect(array2Spy).toHaveBeenCalledWith(true); - expect(objectSpy).toHaveBeenCalledWith(true); + expect(array1Spy).toHaveBeenCalledWith('beforeRender', true); + expect(array2Spy).toHaveBeenCalledWith('beforeRender', true); + expect(objectSpy).toHaveBeenCalledWith('beforeRender', true); }); test('should handle empty subModels without error', () => { - model.invalidateAutoFlowCache(); + realFlowEngine.applyFlowCache.set('event:beforeRender-beforeRender-test-model-uid', { + status: 'resolved', + data: [], + promise: Promise.resolve([]), + }); - expect(deleteSpy).toHaveBeenCalledWith('autoFlow-all-test-model-uid'); + model.invalidateFlowCache('beforeRender'); + + expect(deleteSpy).toHaveBeenCalledWith('event:beforeRender-beforeRender-test-model-uid'); }); test('should handle null flowEngine gracefully', () => { @@ -2135,19 +2258,19 @@ describe('FlowModel', () => { modelWithValidEngine.flowEngine = null; expect(() => { - modelWithValidEngine.invalidateAutoFlowCache(); + modelWithValidEngine.invalidateFlowCache('beforeRender'); }).not.toThrow(); }); test('should pass deep parameter to recursive calls', () => { const childModel = new FlowModel({ uid: 'child', flowEngine: realFlowEngine }); - const childSpy = vi.spyOn(childModel, 'invalidateAutoFlowCache'); + const childSpy = vi.spyOn(childModel, 'invalidateFlowCache'); model.setSubModel('child', childModel); - model.invalidateAutoFlowCache(true); + model.invalidateFlowCache('beforeRender', true); - expect(childSpy).toHaveBeenCalledWith(true); + expect(childSpy).toHaveBeenCalledWith('beforeRender', true); }); }); }); @@ -2198,7 +2321,7 @@ describe('FlowModel', () => { }); const handlerSpy = flow.steps.testStep.handler as any; - await model.applyAutoFlows(); + await model.dispatchEvent('beforeRender'); expect(handlerSpy).toHaveBeenCalled(); }); @@ -2245,7 +2368,7 @@ describe('FlowModel', () => { }); const handlerSpy = flow.steps.nestedStep.handler as any; - await model.applyAutoFlows(); + await model.dispatchEvent('beforeRender'); expect(handlerSpy).toHaveBeenCalled(); }); }); diff --git a/packages/core/flow-engine/src/models/flowModel.tsx b/packages/core/flow-engine/src/models/flowModel.tsx index 95b4b36d81c..f96d919015e 100644 --- a/packages/core/flow-engine/src/models/flowModel.tsx +++ b/packages/core/flow-engine/src/models/flowModel.tsx @@ -50,7 +50,7 @@ import { ModelEventRegistry } from '../event-registry/ModelEventRegistry'; import { GlobalFlowRegistry } from '../flow-registry/GlobalFlowRegistry'; import { FlowDefinition } from '../FlowDefinition'; import { FlowSettingsOpenOptions } from '../flowSettings'; -import type { EventDefinition } from '../types'; +import type { EventDefinition, FlowEvent, DispatchEventOptions } from '../types'; import { ForkFlowModel } from './forkFlowModel'; // 使用 WeakMap 为每个类缓存一个 ModelActionRegistry 实例 @@ -103,9 +103,9 @@ export class FlowModel { private forkCache: Map> = new Map(); /** - * 上一次 applyAutoFlows 的执行参数 + * 上一次 beforeRender 的执行参数 */ - private _lastAutoRunParams: any[] | null = null; + private _lastAutoRunParams: [Record | undefined, boolean?] | null = null; protected observerDispose: () => void; #flowContext: FlowModelContext; @@ -176,7 +176,7 @@ export class FlowModel { } if (this.flowEngine) { - this.invalidateAutoFlowCache(); + this.invalidateFlowCache('beforeRender'); } this._rerunLastAutoRun(); this.forks.forEach((fork) => { @@ -387,13 +387,63 @@ export class FlowModel { }); } - public invalidateAutoFlowCache(deep = false) { + /** + * 失效指定事件的流程缓存;未指定 eventName 时,失效当前模型全部事件缓存。 + * - 默认 beforeRender 与其它事件共享同一缓存体系:prefix=`event:${scope}`,flowKey=`eventName`,uid=`model.uid` + */ + public invalidateFlowCache(eventName?: string, deep = false) { if (this.flowEngine) { - const cacheKey = FlowEngine.generateApplyFlowCacheKey('autoFlow', 'all', this.uid); - this.flowEngine.applyFlowCache.delete(cacheKey); + const scope = `event:${this.getFlowCacheScope(eventName || 'beforeRender')}`; + const cache = this.flowEngine.applyFlowCache; + + if (eventName) { + // 删除该事件名下所有缓存(忽略不同 inputArgs 的散列差异) + const altScope = scope.replace(/:/g, '-'); + const uidSuffixes = [`:${this.uid}`, `-${this.uid}`]; + const eventSegments = [`:${eventName}:`, `-${eventName}-`]; + for (const key of cache.keys()) { + const startMatches = key.startsWith(scope) || key.startsWith(altScope); + const endMatches = uidSuffixes.some((s) => key.endsWith(s)); + const eventMatches = eventSegments.some((seg) => key.includes(seg)); + if (startMatches && endMatches && eventMatches) { + cache.delete(key); + } + } + } else { + // 粗粒度:扫描并删除当前模型 uid 相关的事件缓存(任意事件) + const altScope = `${scope}:`.replace(/:/g, '-'); + const prefixes = [`${scope}:`, altScope]; + const uidSuffixes = [`:${this.uid}`, `-${this.uid}`]; + for (const key of cache.keys()) { + const startMatches = prefixes.some((p) => key.startsWith(p)); + const endMatches = uidSuffixes.some((s) => key.endsWith(s)); + if (startMatches && endMatches) cache.delete(key); + } + } + + // 同步失效所有 fork 的缓存 this.forks.forEach((fork) => { - const forkCacheKey = FlowEngine.generateApplyFlowCacheKey(`${fork['forkId']}`, 'all', this.uid); - this.flowEngine.applyFlowCache.delete(forkCacheKey); + const forkScope = `event:${ + (fork as any).getFlowCacheScope?.(eventName || 'beforeRender') ?? String((fork as any)['forkId']) + }`; + const altForkScope = forkScope.replace(/:/g, '-'); + const uidSuffixes = [`:${this.uid}`, `-${this.uid}`]; + if (eventName) { + const eventSegments = [`:${eventName}:`, `-${eventName}-`]; + for (const key of this.flowEngine.applyFlowCache.keys()) { + const startMatches = key.startsWith(forkScope) || key.startsWith(altForkScope); + const endMatches = uidSuffixes.some((s) => key.endsWith(s)); + const eventMatches = eventSegments.some((seg) => key.includes(seg)); + if (startMatches && endMatches && eventMatches) this.flowEngine.applyFlowCache.delete(key); + } + } else { + const prefixes = [`${forkScope}:`, `${altForkScope}-`]; + for (const key of this.flowEngine.applyFlowCache.keys()) { + const startMatches = prefixes.some((p) => key.startsWith(p)); + const endMatches = uidSuffixes.some((s) => key.endsWith(s)); + if (startMatches && endMatches) this.flowEngine.applyFlowCache.delete(key); + } + } }); } if (deep) { @@ -402,10 +452,10 @@ export class FlowModel { const subModelValue = this.subModels[subModelKey]; if (Array.isArray(subModelValue)) { for (const subModel of subModelValue) { - subModel.invalidateAutoFlowCache(deep); + subModel.invalidateFlowCache(eventName, deep); } } else if (subModelValue instanceof FlowModel) { - subModelValue.invalidateAutoFlowCache(deep); + subModelValue.invalidateFlowCache(eventName, deep); } } } @@ -637,7 +687,11 @@ export class FlowModel { return currentFlowEngine.executor.runFlow(target, flowKey, inputArgs, runId); } - private async _dispatchEvent(eventName: string, inputArgs?: Record): Promise { + private async _dispatchEvent( + eventName: string, + inputArgs?: Record, + options?: DispatchEventOptions, + ): Promise { const currentFlowEngine = this.flowEngine; if (!currentFlowEngine) { console.warn('FlowEngine not available on this model for dispatchEvent. Please set flowEngine on the model.'); @@ -650,64 +704,73 @@ export class FlowModel { this.cleanRun }, targetIsFork=${(target as any)?.isFork === true}`, ); - await currentFlowEngine.executor.dispatchEvent(target, eventName, inputArgs); + return await currentFlowEngine.executor.dispatchEvent(target, eventName, inputArgs, options); } - private _dispatchEventWithDebounce = _.debounce(async (eventName: string, inputArgs?: Record) => { - return this._dispatchEvent(eventName, inputArgs); - }, 100); + private _dispatchEventWithDebounce = _.debounce( + async (eventName: string, inputArgs?: Record, options?: DispatchEventOptions) => { + return this._dispatchEvent(eventName, inputArgs, options); + }, + 100, + ); async dispatchEvent( eventName: string, inputArgs?: Record, options?: { - /** - * 是否要开启防抖功能 - */ - debounce: boolean; - }, - ): Promise { - if (options?.debounce) { - return this._dispatchEventWithDebounce(eventName, inputArgs); + /** 是否要开启防抖功能 */ + debounce?: boolean; + } & DispatchEventOptions, + ): Promise { + const isBeforeRender = eventName === 'beforeRender'; + // 缺省值由模型层提供:beforeRender 默认顺序执行 + 使用缓存;可被 options 覆盖 + const defaults = isBeforeRender ? { sequential: true, useCache: true } : {}; + const execOptions = { + sequential: options?.sequential ?? (defaults as any).sequential, + useCache: options?.useCache ?? (defaults as any).useCache, + } as DispatchEventOptions; + + // 记录最近一次 beforeRender 的入参与选项,便于 stepParams 变化时触发重跑 + if (isBeforeRender) { + this._lastAutoRunParams = [inputArgs, execOptions.useCache]; } - return this._dispatchEvent(eventName, inputArgs); + if (options?.debounce) { + return this._dispatchEventWithDebounce(eventName, inputArgs, execOptions); + } + return this._dispatchEvent(eventName, inputArgs, execOptions); } /** - * 获取所有自动应用流程定义(保持 getFlows 的顺序,即按 sort 排序) - * @returns {FlowDefinition[]} 自动应用流程定义数组(已按 sort 排序) + * 按事件名获取对应的流程集合(保持 getFlows 的顺序,即按 sort 排序)。 + * - beforeRender 兼容:除显式 on: 'beforeRender' 外,包含未声明 on 且 manual !== true 的流程。 */ - public getAutoFlows(): FlowDefinition[] { + public getEventFlows(eventName: string): FlowDefinition[] { const allFlows = this.getFlows(); - - // 过滤出自动流程(保持 Map 的原有顺序) - // 没有 on 属性且没有 manual: true 的流程默认自动执行 - const autoFlows = Array.from(allFlows.values()).filter((flow) => { - // 如果有 on 属性,说明是事件触发流程,不自动执行 - if (flow.on) { - return false; + const beforeRender = eventName === 'beforeRender'; + const isMatch = (flow: FlowDefinition) => { + if (beforeRender) { + if (flow.manual === true) return false; + if (!flow.on) return true; + return typeof flow.on === 'string' ? flow.on === 'beforeRender' : flow.on?.eventName === 'beforeRender'; } - // 如果明确设置了 manual: true,不自动执行 - if (flow.manual === true) { - return false; - } - // 其他情况默认自动执行 - return true; - }); - - return autoFlows; + const on = flow.on; + if (!on) return false; + return typeof on === 'string' ? on === eventName : on?.eventName === eventName; + }; + return Array.from(allFlows.values()).filter(isMatch); } /** - * 重新执行上一次的 applyAutoFlows,保持参数一致 + * 重新执行上一次的 beforeRender,保持参数一致 * 如果之前没有执行过,则直接跳过 * 使用 lodash debounce 避免频繁调用 */ private _rerunLastAutoRun = _.debounce(async () => { if (this._lastAutoRunParams) { try { - await this.applyAutoFlows(...this._lastAutoRunParams); + const [inputArgs] = this._lastAutoRunParams as any[]; + await this.dispatchEvent('beforeRender', inputArgs); } catch (error) { console.error('FlowModel._rerunLastAutoRun: Error during rerun:', error); } @@ -715,86 +778,39 @@ export class FlowModel { }, 100); /** - * 自动流程执行前钩子。 - * 子类可覆盖;可抛出 FlowExitException 提前终止。 + * 通用事件分发钩子:开始 + * 子类可覆盖;beforeRender 事件可通过抛出 FlowExitException 提前终止。 */ - public async onBeforeAutoFlows(inputArgs?: Record): Promise {} + public async onDispatchEventStart( + eventName: string, + options?: DispatchEventOptions, + inputArgs?: Record, + ): Promise {} /** - * 自动流程执行后钩子。 + * 通用事件分发钩子:结束 * 子类可覆盖。 */ - public async onAfterAutoFlows(results: any[], inputArgs?: Record): Promise {} + public async onDispatchEventEnd( + eventName: string, + options?: DispatchEventOptions, + inputArgs?: Record, + results?: any[], + ): Promise {} /** - * 自动流程错误钩子。 + * 通用事件分发钩子:错误 * 子类可覆盖。 */ - public async onAutoFlowsError(error: Error, inputArgs?: Record): Promise {} + public async onDispatchEventError( + eventName: string, + options?: DispatchEventOptions, + inputArgs?: Record, + error?: Error, + ): Promise {} useHooksBeforeRender() {} - /** - * 执行所有自动应用流程 - * @param {Record} [inputArgs] 可选的运行时参数 - * @param {boolean} [useCache=true] 是否使用缓存机制,默认为 true - * @returns {Promise} 所有自动应用流程的执行结果数组 - */ - async applyAutoFlows(inputArgs?: Record, useCache?: boolean): Promise; - async applyAutoFlows(...args: any[]): Promise { - const [inputArgs, useCache = true] = args; - const cacheKey = useCache - ? FlowEngine.generateApplyFlowCacheKey(this['forkId'] ?? 'autoFlow', 'all', this.uid) - : null; - if (!_.isEqual(inputArgs, this._lastAutoRunParams?.[0]) && cacheKey) { - this.flowEngine.applyFlowCache.delete(cacheKey); - } - this._lastAutoRunParams = args; - - // 在执行自动流程前触发(无论是否命中缓存都应触发) - try { - await this.onBeforeAutoFlows(inputArgs); - } catch (error) { - if (error instanceof FlowExitException) { - this.context.logger?.debug(`[FlowModel.applyAutoFlows] ${error.message}`); - return []; - } - try { - await this.onAutoFlowsError(error as Error, inputArgs); - } catch (_) { - // swallow secondary hook error to avoid masking the original - } - throw error; - } - - // 执行自动流程(内部可能命中缓存) - let results: any[] = []; - try { - results = await this.flowEngine.executor.runAutoFlows(this, inputArgs, useCache); - } catch (error) { - try { - await this.onAutoFlowsError(error as Error, inputArgs); - } catch (_) { - // ignore secondary error from error hook - } - throw error; - } - - // 执行后钩子 - try { - await this.onAfterAutoFlows(results, inputArgs); - } catch (error) { - try { - await this.onAutoFlowsError(error as Error, inputArgs); - } catch (_) { - // ignore - } - throw error; - } - - return results; - } - /** * 智能检测是否应该跳过响应式包装 * 说明: @@ -891,6 +907,10 @@ export class FlowModel { return modelInstance.renderHiddenInConfig?.(); } + // 订阅 stepParams 变化,以便当步骤参数(如 JS 代码等)更新时触发一次 React 渲染, + // 从而让 useHooksBeforeRender 中的副作用得以感知并执行(如 applyFlow('jsSettings'))。 + modelInstance.stepParams; + // 调用原始渲染方法 return originalRender.call(renderTarget); }); @@ -962,14 +982,14 @@ export class FlowModel { } async rerender() { - await this.applyAutoFlows(this._lastAutoRunParams?.[0], false); + await this.dispatchEvent('beforeRender', this._lastAutoRunParams?.[0], { useCache: false }); } /** - * 自动流程缓存的作用域标识;fork 实例可覆盖以区分缓存。 + * 事件缓存的作用域标识;可按事件区分(默认与事件无关的 scope 返回 'default')。 */ - public getAutoFlowCacheScope(): string { - return 'autoFlow'; + public getFlowCacheScope(eventName: string): string { + return String(eventName); } setParent(parent: FlowModel): void { @@ -1128,14 +1148,17 @@ export class FlowModel { return this.flowEngine.createModel(options); } - async applySubModelsAutoFlows( + /** + * 对指定子模型派发 beforeRender 事件(顺序执行并使用缓存)。 + */ + async applySubModelsBeforeRenderFlows( subKey: K, inputArgs?: Record, shared?: Record, ) { await Promise.all( this.mapSubModels(subKey, async (sub) => { - await sub.applyAutoFlows(inputArgs); + await sub.dispatchEvent('beforeRender', inputArgs); }), ); } @@ -1209,7 +1232,7 @@ export class FlowModel { throw new Error('FlowEngine is not set on this model. Please set flowEngine before saving.'); } this.observerDispose(); - this.invalidateAutoFlowCache(true); + this.invalidateFlowCache('beforeRender', true); return this.flowEngine.removeModel(this.uid); } @@ -1229,7 +1252,7 @@ export class FlowModel { throw new Error('FlowEngine is not set on this model. Please set flowEngine before deleting.'); } this.observerDispose(); - this.invalidateAutoFlowCache(true); + this.invalidateFlowCache('beforeRender', true); // 从 FlowEngine 中销毁模型 return this.flowEngine.destroyModel(this.uid); } diff --git a/packages/core/flow-engine/src/models/forkFlowModel.ts b/packages/core/flow-engine/src/models/forkFlowModel.ts index 87c675d6e8d..687c04dbd8a 100644 --- a/packages/core/flow-engine/src/models/forkFlowModel.ts +++ b/packages/core/flow-engine/src/models/forkFlowModel.ts @@ -224,10 +224,10 @@ export class ForkFlowModel { } /** - * 自动流程缓存的作用域标识(fork 专用)。 + * 事件缓存的作用域标识(fork 专用)。 */ - public getAutoFlowCacheScope(): string { - return String(this.forkId); + public getFlowCacheScope(eventName: string): string { + return `${String(eventName)}:${String(this.forkId)}`; } // onUnmount() { @@ -250,9 +250,41 @@ export class ForkFlowModel { dispose() { if (this.disposed) return; this.disposed = true; + // 统一按事件缓存命名规则清理当前 fork 的所有缓存项(覆盖所有事件) + try { + const cache = this.flowEngine?.applyFlowCache; + if (cache && cache.size > 0) { + const uidSuffixes = [`:${this.uid}`, `-${this.uid}`]; + const forkIdColon = `:${this.forkId}:`; + const forkIdHyphen = `-${this.forkId}-`; + + for (const key of cache.keys()) { + // 事件缓存统一以 event: 或 event- 开头 + const isEventCache = key.startsWith('event:') || key.startsWith('event-'); + if (!isEventCache) continue; + + // 仅删除当前 fork + 当前 uid 的键 + const endMatches = uidSuffixes.some((s) => key.endsWith(s)); + if (!endMatches) continue; + + // 限定在去除 uid 后缀的范围内匹配 forkId 片段,避免误伤 + let trimmed = key; + for (const suf of uidSuffixes) { + if (trimmed.endsWith(suf)) { + trimmed = trimmed.slice(0, -suf.length); + break; + } + } + if (trimmed.includes(forkIdColon) || trimmed.includes(forkIdHyphen)) { + cache.delete(key); + } + } + } + } catch { + // 忽略清理阶段异常,避免影响后续释放 + } + if (this.master && (this.master as any).forks) { - const forkCacheKey = FlowEngine.generateApplyFlowCacheKey(`${this.forkId}`, 'all', this.uid); - this.flowEngine.applyFlowCache.delete(forkCacheKey); (this.master as any).forks.delete(this as any); } // 从 master 的 forkCache 中移除自己 diff --git a/packages/core/flow-engine/src/types.ts b/packages/core/flow-engine/src/types.ts index c6d5c4ccac4..5faaac354b7 100644 --- a/packages/core/flow-engine/src/types.ts +++ b/packages/core/flow-engine/src/types.ts @@ -185,6 +185,16 @@ export type FlowEvent = | FlowEventName | { eventName: FlowEventName; defaultParams?: Record }; +/** + * 事件分发选项。 + */ +export interface DispatchEventOptions { + /** 是否顺序执行(默认并行) */ + sequential?: boolean; + /** 是否使用缓存(默认 false;beforeRender 的默认值为 true,可覆盖) */ + useCache?: boolean; +} + /** * 事件定义:用于事件注册表(全局/模型类级)。 */ diff --git a/packages/plugins/@nocobase/plugin-block-lowcode/src/client/JavaScriptBlockModel.tsx b/packages/plugins/@nocobase/plugin-block-lowcode/src/client/JavaScriptBlockModel.tsx index a07ace245dc..0e7f013887c 100644 --- a/packages/plugins/@nocobase/plugin-block-lowcode/src/client/JavaScriptBlockModel.tsx +++ b/packages/plugins/@nocobase/plugin-block-lowcode/src/client/JavaScriptBlockModel.tsx @@ -32,7 +32,7 @@ export class JavaScriptBlockModel extends BlockModel { } protected onMount() { // If component has mounted before, rerun to refresh JS on remount - // Keep first mount single-run (handled by auto-flows + onRefReady) + // Keep first mount single-run (handled by beforeRender event + onRefReady) if (this._mountedOnce) { if (this.context.ref.current) { this.rerender(); diff --git a/packages/plugins/@nocobase/plugin-data-visualization/src/client/flow/models/ChartBlockModel.tsx b/packages/plugins/@nocobase/plugin-data-visualization/src/client/flow/models/ChartBlockModel.tsx index 532458eaa24..7c3b93ffa21 100644 --- a/packages/plugins/@nocobase/plugin-data-visualization/src/client/flow/models/ChartBlockModel.tsx +++ b/packages/plugins/@nocobase/plugin-data-visualization/src/client/flow/models/ChartBlockModel.tsx @@ -228,7 +228,7 @@ export class ChartBlockModel extends DataBlockModel { } this.setStepParams('chartSettings', 'configure', params); this.rerender(); - // 调用链:rerender -> applyAutoFlows -> handler(处理数据、属性、事件) -> runQueryAndUpdateResult(查数据) + // 调用链:rerender -> dispatchEvent('beforeRender') -> handler(处理数据、属性、事件) -> runQueryAndUpdateResult(查数据) } // 取消预览,回滚stepParams,并刷新图表