mirror of
https://github.com/nocobase/nocobase.git
synced 2026-09-24 16:02:20 +08:00
refactor: auto flows (#7577)
* feat: add beforeRender event * feat: refactor auto flows * feat: add disptach event hooks * feat: update cache for flow execution * refactor: auto flows hook * chore: remove applyAutoFlows * chore: update doc * chore: update docs for auto flow * chore: update docs and tests * fix: not rerender after updating step params * chore: update docs * fix: error fallback bug
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
interface FlowDefinition<TModel extends FlowModel = FlowModel> {
|
||||
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 默认顺序执行并使用缓存(可覆盖)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -18,10 +18,7 @@ interface FlowModelRendererProps {
|
||||
/** 是否在设置中隐藏移除按钮 */
|
||||
hideRemoveInSettings?: boolean; // 默认 false
|
||||
|
||||
/** 是否跳过自动应用流,默认 false */
|
||||
skipApplyAutoFlows?: boolean; // 默认 false
|
||||
|
||||
/** 当 skipApplyAutoFlows !== false 时,传递给 useApplyAutoFlows 的运行时参数 */
|
||||
/** 传递给 beforeRender 事件的运行时参数 */
|
||||
inputArgs?: Record<string, any>
|
||||
|
||||
/** 设置菜单层级: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 事件上下文
|
||||
<FlowModelRenderer
|
||||
model={model}
|
||||
skipApplyAutoFlows={true}
|
||||
inputArgs={{ customData: 'value' }}
|
||||
/>
|
||||
|
||||
// 传递额外上下文
|
||||
@@ -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
|
||||
<FlowModelRenderer
|
||||
model={flowModel}
|
||||
skipApplyAutoFlows={true}
|
||||
/>
|
||||
```
|
||||
|
||||
### 5. 传递自定义上下文
|
||||
### 4. 传递自定义上下文
|
||||
当需要向流传递特定上下文数据时:
|
||||
|
||||
```tsx | pure
|
||||
@@ -160,7 +145,7 @@ interface FlowModelRendererProps {
|
||||
/>
|
||||
```
|
||||
|
||||
### 6. 控制设置菜单层级
|
||||
### 5. 控制设置菜单层级
|
||||
当需要控制设置菜单显示范围时,可以通过 `settingsMenuLevel` 参数配置:
|
||||
|
||||
```tsx | pure
|
||||
|
||||
@@ -75,14 +75,14 @@
|
||||
- **applyFlow(flowKey: string, inputArgs?: Record<string, any>): Promise\<any\>**
|
||||
执行指定流。
|
||||
|
||||
- **dispatchEvent(eventName: string, inputArgs?: Record<string, any>): void**
|
||||
触发事件,自动匹配并执行相关流。
|
||||
- **dispatchEvent(eventName: string, inputArgs?: Record<string, any>, options?: { debounce?: boolean; sequential?: boolean; useCache?: boolean }): Promise<any[]>**
|
||||
触发事件并执行匹配的流;支持顺序或并行执行。
|
||||
- `sequential`: 是否顺序执行(默认并行);beforeRender 的默认值为顺序执行(可覆盖)。
|
||||
- `useCache`: 是否启用事件层面的缓存(默认 `false`);beforeRender 的默认值为启用缓存(可覆盖)。
|
||||
- `debounce`: 是否启用防抖(默认 `false`)。
|
||||
|
||||
- **applyAutoFlows(inputArgs?: Record<string, any>): Promise\<any[]\>**
|
||||
执行所有自动流。
|
||||
|
||||
- **applySubModelsAutoFlows(subKey: string, extra?: Record\<string, any\>): Promise\<void\>**
|
||||
执行指定子模型的自动流。
|
||||
- **applySubModelsBeforeRenderFlows(subKey: string, inputArgs?: Record\<string, any\>): Promise\<void\>**
|
||||
对指定子模型派发 `beforeRender` 事件(内部顺序执行并使用缓存)。
|
||||
|
||||
- **getFlow(key: string): FlowDefinition \| undefined**
|
||||
获取指定 key 的流配置。
|
||||
@@ -90,23 +90,22 @@
|
||||
- **getFlows(): Map\<string, FlowDefinition\>**
|
||||
获取所有已配置流(含继承)。
|
||||
|
||||
- **getAutoFlows(): FlowDefinition[]**
|
||||
获取所有自动应用流程定义并按 sort 排序。
|
||||
- **getEventFlows(eventName: string): FlowDefinition[]**
|
||||
按事件名获取流程集合并按 sort 排序。
|
||||
- 特殊事件 `beforeRender`: 兼容包含“未声明 on 且 manual !== true”的流程。
|
||||
|
||||
---
|
||||
|
||||
### 自动流执行生命周期钩子
|
||||
### 事件分发钩子
|
||||
|
||||
FlowModel 提供了三个全局自动流执行生命周期钩子,子类可以重写这些方法来实现自定义逻辑:
|
||||
- **async onDispatchEventStart(eventName: string, options?: DispatchEventOptions, inputArgs?: Record<string, any>)**
|
||||
事件执行前调用;beforeRender 可通过抛出 `FlowExitException` 终止执行。
|
||||
|
||||
- **async onBeforeAutoFlows(inputArgs?: Record<string, any>): Promise\<void\>**
|
||||
在所有自动流执行前调用。可以通过抛出 `FlowExitException` 来终止后续流程执行。
|
||||
- **async onDispatchEventEnd(eventName: string, options?: DispatchEventOptions, inputArgs?: Record<string, any>, results?: any[])**
|
||||
事件执行后调用;`results` 为按 sort 顺序收集到的步骤结果数组。
|
||||
|
||||
- **async onAfterAutoFlows(results: any[], inputArgs?: Record<string, any>): Promise\<void\>**
|
||||
在所有自动流执行后调用。可以访问所有自动流的执行结果。
|
||||
|
||||
- **async onAutoFlowsError(error: Error, inputArgs?: Record<string, any>): Promise\<void\>**
|
||||
在自动流执行或生命周期钩子出错时调用。可进行自定义错误处理。
|
||||
- **async onDispatchEventError(eventName: string, options?: DispatchEventOptions, inputArgs?: Record<string, any>, 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<string, FlowModel | FlowModel[]>;
|
||||
}
|
||||
```
|
||||
- **invalidateFlowCache(eventName?: string, deep?: boolean): void**
|
||||
失效当前模型指定事件的流程缓存;未指定 `eventName` 时失效当前模型的全部事件缓存。常用:`invalidateFlowCache('beforeRender')`。
|
||||
|
||||
@@ -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());
|
||||
<Table columns={columns} />
|
||||
```
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
interface FlowDefinition<TModel extends FlowModel = FlowModel> {
|
||||
key: string; // 流唯一标识
|
||||
title?: string; // 可选:流显示名称
|
||||
auto?: boolean; // 可选:是否自动运行
|
||||
sort?: number; // 可选:流执行排序,数字越小越先执行,默认为 0,可为负数
|
||||
on?: { eventName: string }; // 可选:事件触发配置
|
||||
steps: Record<string, StepDefinition<TModel>>; // 流步骤定义
|
||||
@@ -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<string, StepDefinition>` | 步骤集合,键为步骤名,值为步骤定义 |
|
||||
|
||||
|
||||
@@ -18,10 +18,7 @@ interface FlowModelRendererProps {
|
||||
/** 是否在设置中隐藏移除按钮 */
|
||||
hideRemoveInSettings?: boolean; // 默认 false
|
||||
|
||||
/** 是否跳过自动应用流,默认 false */
|
||||
skipApplyAutoFlows?: boolean; // 默认 false
|
||||
|
||||
/** 当 skipApplyAutoFlows !== false 时,传递给 useApplyAutoFlows 的运行时参数 */
|
||||
/** 传递给 beforeRender 事件的运行时参数 */
|
||||
inputArgs?: Record<string, any>
|
||||
|
||||
/** 设置菜单层级: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 事件上下文
|
||||
<FlowModelRenderer
|
||||
model={model}
|
||||
skipApplyAutoFlows={true}
|
||||
inputArgs={{ customData: 'value' }}
|
||||
/>
|
||||
|
||||
// 传递额外上下文
|
||||
@@ -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
|
||||
<FlowModelRenderer
|
||||
model={flowModel}
|
||||
skipApplyAutoFlows={true}
|
||||
/>
|
||||
```
|
||||
|
||||
### 5. 传递自定义上下文
|
||||
### 4. 传递自定义上下文
|
||||
当需要向流传递特定上下文数据时:
|
||||
|
||||
```tsx | pure
|
||||
@@ -160,7 +145,7 @@ interface FlowModelRendererProps {
|
||||
/>
|
||||
```
|
||||
|
||||
### 6. 控制设置菜单层级
|
||||
### 5. 控制设置菜单层级
|
||||
当需要控制设置菜单显示范围时,可以通过 `settingsMenuLevel` 参数配置:
|
||||
|
||||
```tsx | pure
|
||||
|
||||
@@ -75,17 +75,14 @@
|
||||
- **applyFlow(flowKey: string, inputArgs?: Record<string, any>): Promise\<any\>**
|
||||
执行指定流。
|
||||
|
||||
- **dispatchEvent(eventName: string, inputArgs?: Record<string, any>, options?: { debounce: boolean }): Promise\<void\>**
|
||||
触发事件,自动匹配并执行相关流。支持防抖功能,防止快速连续触发。
|
||||
- `eventName`: 要触发的事件名称
|
||||
- `inputArgs`: 可选的事件参数
|
||||
- `options.debounce`: 是否启用防抖功能,默认为 `false`。当设为 `true` 时,100ms 内的重复调用会被防抖处理
|
||||
- **dispatchEvent(eventName: string, inputArgs?: Record<string, any>, options?: { debounce?: boolean; sequential?: boolean; useCache?: boolean }): Promise\<any[]\>**
|
||||
触发事件并执行对应的流程集合。
|
||||
- `sequential`: 是否顺序执行(默认并行);beforeRender 的默认值为顺序执行(可覆盖)
|
||||
- `useCache`: 是否启用事件级缓存(默认 false);beforeRender 的默认值为启用缓存(可覆盖)
|
||||
- `debounce`: 是否启用防抖(默认 false)
|
||||
|
||||
- **applyAutoFlows(inputArgs?: Record<string, any>): Promise\<any[]\>**
|
||||
执行所有自动流。
|
||||
|
||||
- **applySubModelsAutoFlows(subKey: string, extra?: Record\<string, any\>): Promise\<void\>**
|
||||
执行指定子模型的自动流。
|
||||
- **applySubModelsBeforeRenderFlows(subKey: string, inputArgs?: Record\<string, any\>): Promise\<void\>**
|
||||
对指定子模型派发 `beforeRender` 事件(内部顺序执行并使用缓存)。
|
||||
|
||||
- **getFlow(key: string): FlowDefinition \| undefined**
|
||||
获取指定 key 的流配置。
|
||||
@@ -93,23 +90,17 @@
|
||||
- **getFlows(): Map\<string, FlowDefinition\>**
|
||||
获取所有已配置流(含继承)。
|
||||
|
||||
- **getAutoFlows(): FlowDefinition[]**
|
||||
获取所有自动应用流程定义并按 sort 排序。
|
||||
- **getEventFlows(eventName: string): FlowDefinition[]**
|
||||
按事件名获取流程集合并按 sort 排序。
|
||||
- 特殊事件 `beforeRender`: 兼容包含“未声明 on 且 manual !== true”的流程。
|
||||
|
||||
---
|
||||
|
||||
### 自动流执行生命周期钩子
|
||||
### 事件分发钩子
|
||||
|
||||
FlowModel 提供了三个全局自动流执行生命周期钩子,子类可以重写这些方法来实现自定义逻辑:
|
||||
|
||||
- **async onBeforeAutoFlows(inputArgs?: Record<string, any>): Promise\<void\>**
|
||||
在所有自动流执行前调用。可以通过抛出 `FlowExitException` 来终止后续流程执行。
|
||||
|
||||
- **async onAfterAutoFlows(results: any[], inputArgs?: Record<string, any>): Promise\<void\>**
|
||||
在所有自动流执行后调用。可以访问所有自动流的执行结果。
|
||||
|
||||
- **async onAutoFlowsError(error: Error, inputArgs?: Record<string, any>): Promise\<void\>**
|
||||
在自动流执行或生命周期钩子出错时调用。可进行自定义错误处理。
|
||||
- **async onDispatchEventStart(eventName: string, options?: DispatchEventOptions, inputArgs?: Record<string, any>)**
|
||||
- **async onDispatchEventEnd(eventName: string, options?: DispatchEventOptions, inputArgs?: Record<string, any>, results?: any[])**
|
||||
- **async onDispatchEventError(eventName: string, options?: DispatchEventOptions, inputArgs?: Record<string, any>, error?: Error)**
|
||||
|
||||
---
|
||||
|
||||
@@ -205,7 +196,7 @@ await model.save();
|
||||
|
||||
// 执行流
|
||||
await model.applyFlow('default');
|
||||
await model.applyAutoFlows();
|
||||
await model.dispatchEvent('beforeRender');
|
||||
|
||||
// 触发事件(立即执行)
|
||||
await model.dispatchEvent('click');
|
||||
|
||||
@@ -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());
|
||||
<Table columns={columns} />
|
||||
```
|
||||
|
||||
@@ -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: '/',
|
||||
|
||||
@@ -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: '/',
|
||||
|
||||
@@ -41,7 +41,7 @@ export class TableModel extends FlowModel<S> {
|
||||
},
|
||||
},
|
||||
});
|
||||
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');
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
+5
-3
@@ -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 (
|
||||
|
||||
+5
-3
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
## 以下流执行时会创建 FlowRuntimeContext
|
||||
|
||||
- `model.applyAutoFlows(inputArgs)`
|
||||
- `model.dispatchEvent('beforeRender', inputArgs)`
|
||||
自动执行所有属性流,`inputArgs` 为流输入参数。
|
||||
|
||||
- `model.dispatchEvent(eventName, inputArgs)`
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
# 错误回退:CreateForm 自动流异常
|
||||
# 错误回退:CreateForm beforeRender事件流异常
|
||||
|
||||
当自动流(useApplyAutoFlows)执行过程中发生异常时,错误将被提升为渲染期错误并由外层错误边界捕获。
|
||||
当beforeRender事件流执行过程中发生异常时,错误将被提升为渲染期错误并由外层错误边界捕获。
|
||||
|
||||
<code src="./create-form-auto-flows-error.tsx"></code>
|
||||
|
||||
|
||||
+2
-11
@@ -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: (
|
||||
<FlowEngineProvider engine={this.flowEngine}>
|
||||
<div style={{ padding: 16 }}>
|
||||
<Card title="CreateForm 自动流异常演示" style={{ marginTop: 12 }}>
|
||||
<Card title="CreateForm beforeRender事件流异常演示" style={{ marginTop: 12 }}>
|
||||
<FlowModelRenderer model={this.form} showFlowSettings showErrorFallback />
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
本页汇总并演示三类常见的错误回退场景,便于一次性对比查看:
|
||||
|
||||
- CreateForm 字段渲染异常
|
||||
- CreateForm 自动流(useApplyAutoFlows)异常
|
||||
- CreateForm beforeRender事件流
|
||||
- 表格列(RenderFunction)单元格渲染异常
|
||||
|
||||
## CreateForm 字段异常
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
<code src="./create-form-field-error.tsx"></code>
|
||||
|
||||
## CreateForm 自动流异常
|
||||
## CreateForm beforeRender事件流异常
|
||||
|
||||
当自动流(useApplyAutoFlows)执行过程中发生异常时,错误将被提升为渲染期错误并由外层错误边界捕获。
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
title: 模型 cleanRun:每次自动流干净运行
|
||||
order: 99
|
||||
---
|
||||
|
||||
本示例演示如何通过模型选项 `cleanRun` 实现“每次自动流都在干净基线”运行,避免状态累积污染原始模型。
|
||||
|
||||
<code src="./index.tsx"></code>
|
||||
@@ -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 (
|
||||
<Card size="small" style={{ width: 360 }}>
|
||||
<div>
|
||||
title: <b>{title || 'Demo'}</b>
|
||||
</div>
|
||||
<div>
|
||||
count: <b>{count}</b>
|
||||
</div>
|
||||
<div style={{ wordBreak: 'break-all' }}>
|
||||
lastRunId: <code>{String(lastRunId || '')}</code>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
<Space direction="vertical" size={16}>
|
||||
<Space align="start">
|
||||
<Space direction="vertical">
|
||||
<div>cleanRun: false(状态累加)</div>
|
||||
<FlowModelRenderer model={model1} showFlowSettings />
|
||||
</Space>
|
||||
<Space direction="vertical">
|
||||
<div>cleanRun: true(每次干净基线)</div>
|
||||
<FlowModelRenderer model={model2} showFlowSettings />
|
||||
</Space>
|
||||
</Space>
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
this.router.add('root', {
|
||||
path: '/',
|
||||
element: <View />,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const app = new Application({
|
||||
router: { type: 'memory', initialEntries: ['/'] },
|
||||
plugins: [PluginDemo],
|
||||
});
|
||||
|
||||
export default app.getRootComponent();
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -101,6 +101,6 @@ async function rebuildFieldSubModel(ctx, model, binding: BindingOptions) {
|
||||
},
|
||||
});
|
||||
|
||||
await subModel.applyAutoFlows();
|
||||
await subModel.dispatchEvent('beforeRender');
|
||||
await model.save(); // 持久化
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -116,7 +116,7 @@ export const FieldAssignValueInput: React.FC<Props> = ({ 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) {
|
||||
|
||||
@@ -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',
|
||||
|
||||
+3
-3
@@ -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 <span data-testid={`dummy-${this.uid}`} ref={this.context.ref as any} />;
|
||||
}
|
||||
}
|
||||
|
||||
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<JSEditableFieldModel>({ use: 'JSEditableFieldModel', uid: 'f1' });
|
||||
|
||||
@@ -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 },
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -297,7 +297,7 @@ export class CollectionBlockModel<T = DefaultStructure> extends DataBlockModel<T
|
||||
resource.setDataSourceKey(params.dataSourceKey);
|
||||
resource.setResourceName(params.associationName || params.collectionName);
|
||||
resource.on('refresh', () => {
|
||||
this.invalidateAutoFlowCache();
|
||||
this.invalidateFlowCache('beforeRender');
|
||||
});
|
||||
return resource;
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -88,7 +88,7 @@ const AddActionToolbarComponent = ({ model }) => {
|
||||
|
||||
export class TableActionsColumnModel extends TableCustomColumnModel {
|
||||
async afterAddAsSubModel() {
|
||||
await this.applyAutoFlows();
|
||||
await this.dispatchEvent('beforeRender');
|
||||
}
|
||||
|
||||
getColumnProps() {
|
||||
|
||||
@@ -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 展示
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
+2
-2
@@ -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: {
|
||||
|
||||
+1
-1
@@ -139,7 +139,7 @@ SubTableFieldModel.registerFlow({
|
||||
},
|
||||
init: {
|
||||
async handler(ctx, params) {
|
||||
await ctx.model.applySubModelsAutoFlows('columns');
|
||||
await ctx.model.applySubModelsBeforeRenderFlows('columns');
|
||||
},
|
||||
},
|
||||
allowAddNew: {
|
||||
|
||||
+2
-2
@@ -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');
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -79,6 +79,7 @@ const JSFormRuntime: React.FC<{
|
||||
* - 子节点由脚本渲染(DOM 操作),用于完全自定义输入体验。
|
||||
*/
|
||||
export class JSEditableFieldModel extends FieldModel {
|
||||
private _mountedOnce = false;
|
||||
render() {
|
||||
return (
|
||||
<JSFormRuntime
|
||||
@@ -90,6 +91,21 @@ export class JSEditableFieldModel extends FieldModel {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 CreateForm / 提交后重置等场景下,字段可能被卸载并重新挂载。
|
||||
* 由于 beforeRender 命中缓存,新挂载的容器不会自动写入 JS 渲染内容。
|
||||
* 这里在二次挂载时检测容器已就绪,主动触发 rerender 来刷新 beforeRender(禁用缓存),
|
||||
* 保证输入框不会因为容器变更而空白。
|
||||
*/
|
||||
protected onMount() {
|
||||
if (this._mountedOnce) {
|
||||
if (this.context.ref?.current) {
|
||||
this.rerender();
|
||||
}
|
||||
}
|
||||
this._mountedOnce = true;
|
||||
}
|
||||
}
|
||||
|
||||
JSEditableFieldModel.define({
|
||||
|
||||
@@ -83,7 +83,7 @@ export class JSFieldModel extends FieldModel {
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件挂载后,如果容器引用已就绪,强制重跑一次自动流程以把 HTML 写入当前 DOM。
|
||||
* 组件挂载后,如果容器引用已就绪,强制重跑一次 beforeRender flows 以把 HTML 写入当前 DOM。
|
||||
* 解决新增后因命中缓存或执行时机导致内容未写入的问题(与 JSBlock/JSItem 一致处理)。
|
||||
*/
|
||||
protected onMount() {
|
||||
|
||||
@@ -49,7 +49,7 @@ export class JSItemModel extends CommonItemModel {
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件挂载后,如果容器引用已就绪,强制重跑一次自动流程以把 HTML 写入当前 DOM。
|
||||
* 组件挂载后,如果容器引用已就绪,强制重跑一次 beforeRender 以把 HTML 写入当前 DOM。
|
||||
* 解决某些情况下命中旧缓存导致新节点未写入而显示空白的问题。
|
||||
*/
|
||||
protected onMount() {
|
||||
|
||||
@@ -13,7 +13,7 @@ import { FlowEngine } from './flowEngine';
|
||||
* ViewScopedFlowEngine(视图作用域引擎)
|
||||
*
|
||||
* 设计目标:
|
||||
* - 仅在视图(dialog/drawer/embed)作用域内隔离“模型实例表”和“自动流缓存”;
|
||||
* - 仅在视图(dialog/drawer/embed)作用域内隔离“模型实例表”和“事件缓存”;
|
||||
* - 其余能力(动作/事件/模型类/资源/仓库/设置/日志/翻译等)全部直接代理到父引擎;
|
||||
* - 使用 Proxy 实现“最小代理”,保持与父引擎的全局一致性。
|
||||
*
|
||||
|
||||
@@ -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<CounterModel>({ use: CounterModel, uid });
|
||||
const cm = child.createModel<CounterModel>({ 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);
|
||||
});
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ const flowModelRendererPropKeys: (keyof FlowModelRendererProps)[] = [
|
||||
'flowSettingsVariant',
|
||||
'hideRemoveInSettings',
|
||||
'showTitle',
|
||||
'skipApplyAutoFlows',
|
||||
'inputArgs',
|
||||
'showErrorFallback',
|
||||
'settingsMenuLevel',
|
||||
|
||||
@@ -87,10 +87,7 @@ export interface FlowModelRendererProps {
|
||||
/** 是否在边框左上角显示模型title,默认 false */
|
||||
showTitle?: boolean; // 默认 false
|
||||
|
||||
/** 是否跳过自动应用流程,默认 false */
|
||||
skipApplyAutoFlows?: boolean; // 默认 false
|
||||
|
||||
/** 当 skipApplyAutoFlows !== false 时,传递给 useApplyAutoFlows 的额外上下文 */
|
||||
/** 传递给 beforeRender 事件的运行时参数 */
|
||||
inputArgs?: Record<string, any>;
|
||||
|
||||
/** 是否在最外层包装 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 (
|
||||
<FlowModelProvider model={model}>
|
||||
<FlowModelRendererCore
|
||||
model={model}
|
||||
showFlowSettings={showFlowSettings}
|
||||
flowSettingsVariant={flowSettingsVariant}
|
||||
hideRemoveInSettings={hideRemoveInSettings}
|
||||
showTitle={showTitle}
|
||||
showErrorFallback={showErrorFallback}
|
||||
settingsMenuLevel={settingsMenuLevel}
|
||||
extraToolbarItems={extraToolbarItems}
|
||||
/>
|
||||
</FlowModelProvider>
|
||||
);
|
||||
},
|
||||
);
|
||||
// 移除不带 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<FlowModelRendererProps> = observer(
|
||||
flowSettingsVariant = 'dropdown',
|
||||
hideRemoveInSettings = false,
|
||||
showTitle = false,
|
||||
skipApplyAutoFlows = false,
|
||||
inputArgs,
|
||||
showErrorFallback = true,
|
||||
settingsMenuLevel,
|
||||
@@ -381,19 +326,8 @@ export const FlowModelRenderer: React.FC<FlowModelRendererProps> = observer(
|
||||
return null;
|
||||
}
|
||||
|
||||
// 构建渲染内容
|
||||
const content = skipApplyAutoFlows ? (
|
||||
<FlowModelRendererWithoutAutoFlows
|
||||
model={model}
|
||||
showFlowSettings={showFlowSettings}
|
||||
flowSettingsVariant={flowSettingsVariant}
|
||||
hideRemoveInSettings={hideRemoveInSettings}
|
||||
showTitle={showTitle}
|
||||
showErrorFallback={showErrorFallback}
|
||||
settingsMenuLevel={settingsMenuLevel}
|
||||
extraToolbarItems={extraToolbarItems}
|
||||
/>
|
||||
) : (
|
||||
// 构建渲染内容:统一在渲染前触发 beforeRender 事件(带缓存)
|
||||
const content = (
|
||||
<FlowModelRendererWithAutoFlows
|
||||
model={model}
|
||||
showFlowSettings={showFlowSettings}
|
||||
|
||||
+5
-5
@@ -124,7 +124,7 @@ describe('Delete problematic model via FlowSettings menu', () => {
|
||||
<ConfigProvider>
|
||||
<App>
|
||||
<FlowEngineProvider engine={engine}>
|
||||
<FlowModelRenderer model={model} showFlowSettings showErrorFallback skipApplyAutoFlows />
|
||||
<FlowModelRenderer model={model} showFlowSettings showErrorFallback />
|
||||
</FlowEngineProvider>
|
||||
</App>
|
||||
</ConfigProvider>,
|
||||
@@ -141,7 +141,7 @@ describe('Delete problematic model via FlowSettings menu', () => {
|
||||
return (
|
||||
<div>
|
||||
{items.map((m: FlowModel) => (
|
||||
<FlowModelRenderer key={m.uid} model={m} showFlowSettings showErrorFallback skipApplyAutoFlows />
|
||||
<FlowModelRenderer key={m.uid} model={m} showFlowSettings showErrorFallback />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
@@ -167,7 +167,7 @@ describe('Delete problematic model via FlowSettings menu', () => {
|
||||
<ConfigProvider>
|
||||
<App>
|
||||
<FlowEngineProvider engine={engine}>
|
||||
<FlowModelRenderer model={parent} skipApplyAutoFlows />
|
||||
<FlowModelRenderer model={parent} />
|
||||
</FlowEngineProvider>
|
||||
</App>
|
||||
</ConfigProvider>,
|
||||
@@ -186,7 +186,7 @@ describe('Delete problematic model via FlowSettings menu', () => {
|
||||
return (
|
||||
<div>
|
||||
{cells.map((m: FlowModel) => (
|
||||
<FlowModelRenderer key={m.uid} model={m} showFlowSettings showErrorFallback skipApplyAutoFlows />
|
||||
<FlowModelRenderer key={m.uid} model={m} showFlowSettings showErrorFallback />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
@@ -212,7 +212,7 @@ describe('Delete problematic model via FlowSettings menu', () => {
|
||||
<ConfigProvider>
|
||||
<App>
|
||||
<FlowEngineProvider engine={engine}>
|
||||
<FlowModelRenderer model={parent} skipApplyAutoFlows />
|
||||
<FlowModelRenderer model={parent} />
|
||||
</FlowEngineProvider>
|
||||
</App>
|
||||
</ConfigProvider>,
|
||||
|
||||
@@ -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<T>(cacheKey: string | null, executor: () => Promise<T>): Promise<T> {
|
||||
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<string, any> = 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<string, StepDefinition>
|
||||
const stepDefs: Record<string, StepDefinition> = 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<string, { params: any; uiSchema?: any; result?: any }>;
|
||||
|
||||
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<string, any> = {};
|
||||
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<string, any>, useCache = true): Promise<any[]> {
|
||||
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<any[]> => {
|
||||
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<string, any>): Promise<void> {
|
||||
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<string, any>,
|
||||
options?: DispatchEventOptions,
|
||||
): Promise<any> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string, Omit<FlowDefinitionOptions, 'key'>>;
|
||||
|
||||
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<string, Omit<FlowDefinitionOptions, 'key'>>;
|
||||
|
||||
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<string, Omit<FlowDefinitionOptions, 'key'>>;
|
||||
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<string, Omit<FlowDefinitionOptions, 'key'>>;
|
||||
|
||||
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<string, Omit<FlowDefinitionOptions, 'key'>>;
|
||||
|
||||
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<string, Omit<FlowDefinitionOptions, 'key'>>;
|
||||
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<string, Omit<FlowDefinitionOptions, 'key'>>;
|
||||
|
||||
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 {}
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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<any> {};
|
||||
// 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']);
|
||||
|
||||
@@ -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<string, any>) {
|
||||
beforeHookSpy(inputArgs);
|
||||
async onDispatchEventStart(eventName: string, _opts?: any, inputArgs?: Record<string, any>) {
|
||||
if (eventName === 'beforeRender') beforeHookSpy(inputArgs);
|
||||
}
|
||||
|
||||
async onAfterAutoFlows(results: any[], inputArgs?: Record<string, any>) {
|
||||
afterHookSpy(results, inputArgs);
|
||||
async onDispatchEventEnd(eventName: string, _opts?: any, inputArgs?: Record<string, any>, results?: any[]) {
|
||||
if (eventName === 'beforeRender') afterHookSpy(results, inputArgs);
|
||||
}
|
||||
|
||||
async onAutoFlowsError(error: Error, inputArgs?: Record<string, any>) {
|
||||
errorHookSpy(error, inputArgs);
|
||||
async onDispatchEventError(eventName: string, _opts?: any, inputArgs?: Record<string, any>, 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<string, any>) {
|
||||
beforeHookSpy(inputArgs);
|
||||
throw new FlowExitException('autoFlows', this.uid);
|
||||
async onDispatchEventStart(eventName: string, _opts?: any, inputArgs?: Record<string, any>) {
|
||||
if (eventName === 'beforeRender') {
|
||||
beforeHookSpy(inputArgs);
|
||||
throw new FlowExitException('beforeRender', this.uid);
|
||||
}
|
||||
}
|
||||
|
||||
async onAfterAutoFlows(results: any[], inputArgs?: Record<string, any>) {
|
||||
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<string, any>) {
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<Structure extends DefaultStructure = DefaultStructure> {
|
||||
private forkCache: Map<string, ForkFlowModel<any>> = new Map();
|
||||
|
||||
/**
|
||||
* 上一次 applyAutoFlows 的执行参数
|
||||
* 上一次 beforeRender 的执行参数
|
||||
*/
|
||||
private _lastAutoRunParams: any[] | null = null;
|
||||
private _lastAutoRunParams: [Record<string, any> | undefined, boolean?] | null = null;
|
||||
protected observerDispose: () => void;
|
||||
#flowContext: FlowModelContext;
|
||||
|
||||
@@ -176,7 +176,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
|
||||
}
|
||||
|
||||
if (this.flowEngine) {
|
||||
this.invalidateAutoFlowCache();
|
||||
this.invalidateFlowCache('beforeRender');
|
||||
}
|
||||
this._rerunLastAutoRun();
|
||||
this.forks.forEach((fork) => {
|
||||
@@ -387,13 +387,63 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
|
||||
});
|
||||
}
|
||||
|
||||
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<Structure extends DefaultStructure = DefaultStructure> {
|
||||
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<Structure extends DefaultStructure = DefaultStructure> {
|
||||
return currentFlowEngine.executor.runFlow(target, flowKey, inputArgs, runId);
|
||||
}
|
||||
|
||||
private async _dispatchEvent(eventName: string, inputArgs?: Record<string, any>): Promise<void> {
|
||||
private async _dispatchEvent(
|
||||
eventName: string,
|
||||
inputArgs?: Record<string, any>,
|
||||
options?: DispatchEventOptions,
|
||||
): Promise<any[]> {
|
||||
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<Structure extends DefaultStructure = DefaultStructure> {
|
||||
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<string, any>) => {
|
||||
return this._dispatchEvent(eventName, inputArgs);
|
||||
}, 100);
|
||||
private _dispatchEventWithDebounce = _.debounce(
|
||||
async (eventName: string, inputArgs?: Record<string, any>, options?: DispatchEventOptions) => {
|
||||
return this._dispatchEvent(eventName, inputArgs, options);
|
||||
},
|
||||
100,
|
||||
);
|
||||
|
||||
async dispatchEvent(
|
||||
eventName: string,
|
||||
inputArgs?: Record<string, any>,
|
||||
options?: {
|
||||
/**
|
||||
* 是否要开启防抖功能
|
||||
*/
|
||||
debounce: boolean;
|
||||
},
|
||||
): Promise<void> {
|
||||
if (options?.debounce) {
|
||||
return this._dispatchEventWithDebounce(eventName, inputArgs);
|
||||
/** 是否要开启防抖功能 */
|
||||
debounce?: boolean;
|
||||
} & DispatchEventOptions,
|
||||
): Promise<any[]> {
|
||||
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<Structure extends DefaultStructure = DefaultStructure> {
|
||||
}, 100);
|
||||
|
||||
/**
|
||||
* 自动流程执行前钩子。
|
||||
* 子类可覆盖;可抛出 FlowExitException 提前终止。
|
||||
* 通用事件分发钩子:开始
|
||||
* 子类可覆盖;beforeRender 事件可通过抛出 FlowExitException 提前终止。
|
||||
*/
|
||||
public async onBeforeAutoFlows(inputArgs?: Record<string, any>): Promise<void> {}
|
||||
public async onDispatchEventStart(
|
||||
eventName: string,
|
||||
options?: DispatchEventOptions,
|
||||
inputArgs?: Record<string, any>,
|
||||
): Promise<void> {}
|
||||
|
||||
/**
|
||||
* 自动流程执行后钩子。
|
||||
* 通用事件分发钩子:结束
|
||||
* 子类可覆盖。
|
||||
*/
|
||||
public async onAfterAutoFlows(results: any[], inputArgs?: Record<string, any>): Promise<void> {}
|
||||
public async onDispatchEventEnd(
|
||||
eventName: string,
|
||||
options?: DispatchEventOptions,
|
||||
inputArgs?: Record<string, any>,
|
||||
results?: any[],
|
||||
): Promise<void> {}
|
||||
|
||||
/**
|
||||
* 自动流程错误钩子。
|
||||
* 通用事件分发钩子:错误
|
||||
* 子类可覆盖。
|
||||
*/
|
||||
public async onAutoFlowsError(error: Error, inputArgs?: Record<string, any>): Promise<void> {}
|
||||
public async onDispatchEventError(
|
||||
eventName: string,
|
||||
options?: DispatchEventOptions,
|
||||
inputArgs?: Record<string, any>,
|
||||
error?: Error,
|
||||
): Promise<void> {}
|
||||
|
||||
useHooksBeforeRender() {}
|
||||
|
||||
/**
|
||||
* 执行所有自动应用流程
|
||||
* @param {Record<string, any>} [inputArgs] 可选的运行时参数
|
||||
* @param {boolean} [useCache=true] 是否使用缓存机制,默认为 true
|
||||
* @returns {Promise<any[]>} 所有自动应用流程的执行结果数组
|
||||
*/
|
||||
async applyAutoFlows(inputArgs?: Record<string, any>, useCache?: boolean): Promise<any[]>;
|
||||
async applyAutoFlows(...args: any[]): Promise<any[]> {
|
||||
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<Structure extends DefaultStructure = DefaultStructure> {
|
||||
return modelInstance.renderHiddenInConfig?.();
|
||||
}
|
||||
|
||||
// 订阅 stepParams 变化,以便当步骤参数(如 JS 代码等)更新时触发一次 React 渲染,
|
||||
// 从而让 useHooksBeforeRender 中的副作用得以感知并执行(如 applyFlow('jsSettings'))。
|
||||
modelInstance.stepParams;
|
||||
|
||||
// 调用原始渲染方法
|
||||
return originalRender.call(renderTarget);
|
||||
});
|
||||
@@ -962,14 +982,14 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
|
||||
}
|
||||
|
||||
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<Structure extends DefaultStructure = DefaultStructure> {
|
||||
return this.flowEngine.createModel(options);
|
||||
}
|
||||
|
||||
async applySubModelsAutoFlows<K extends keyof Structure['subModels']>(
|
||||
/**
|
||||
* 对指定子模型派发 beforeRender 事件(顺序执行并使用缓存)。
|
||||
*/
|
||||
async applySubModelsBeforeRenderFlows<K extends keyof Structure['subModels']>(
|
||||
subKey: K,
|
||||
inputArgs?: Record<string, any>,
|
||||
shared?: Record<string, any>,
|
||||
) {
|
||||
await Promise.all(
|
||||
this.mapSubModels(subKey, async (sub) => {
|
||||
await sub.applyAutoFlows(inputArgs);
|
||||
await sub.dispatchEvent('beforeRender', inputArgs);
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -1209,7 +1232,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
|
||||
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<Structure extends DefaultStructure = DefaultStructure> {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -224,10 +224,10 @@ export class ForkFlowModel<TMaster extends FlowModel = FlowModel> {
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动流程缓存的作用域标识(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<TMaster extends FlowModel = FlowModel> {
|
||||
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 中移除自己
|
||||
|
||||
@@ -185,6 +185,16 @@ export type FlowEvent<TModel extends FlowModel = FlowModel> =
|
||||
| FlowEventName
|
||||
| { eventName: FlowEventName; defaultParams?: Record<string, any> };
|
||||
|
||||
/**
|
||||
* 事件分发选项。
|
||||
*/
|
||||
export interface DispatchEventOptions {
|
||||
/** 是否顺序执行(默认并行) */
|
||||
sequential?: boolean;
|
||||
/** 是否使用缓存(默认 false;beforeRender 的默认值为 true,可覆盖) */
|
||||
useCache?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 事件定义:用于事件注册表(全局/模型类级)。
|
||||
*/
|
||||
|
||||
@@ -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();
|
||||
|
||||
+1
-1
@@ -228,7 +228,7 @@ export class ChartBlockModel extends DataBlockModel<ChartBlockModelStructure> {
|
||||
}
|
||||
this.setStepParams('chartSettings', 'configure', params);
|
||||
this.rerender();
|
||||
// 调用链:rerender -> applyAutoFlows -> handler(处理数据、属性、事件) -> runQueryAndUpdateResult(查数据)
|
||||
// 调用链:rerender -> dispatchEvent('beforeRender') -> handler(处理数据、属性、事件) -> runQueryAndUpdateResult(查数据)
|
||||
}
|
||||
|
||||
// 取消预览,回滚stepParams,并刷新图表
|
||||
|
||||
Reference in New Issue
Block a user