@nocobase/flow-engine 2.1.0-alpha.3 → 2.1.0-alpha.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/LICENSE +201 -661
  2. package/README.md +79 -10
  3. package/lib/JSRunner.d.ts +10 -1
  4. package/lib/JSRunner.js +50 -5
  5. package/lib/ViewScopedFlowEngine.js +5 -1
  6. package/lib/components/FieldModelRenderer.js +2 -2
  7. package/lib/components/FlowModelRenderer.d.ts +3 -1
  8. package/lib/components/FlowModelRenderer.js +12 -6
  9. package/lib/components/FormItem.d.ts +6 -0
  10. package/lib/components/FormItem.js +11 -3
  11. package/lib/components/MobilePopup.js +6 -5
  12. package/lib/components/dnd/gridDragPlanner.d.ts +59 -2
  13. package/lib/components/dnd/gridDragPlanner.js +613 -21
  14. package/lib/components/dnd/index.d.ts +19 -1
  15. package/lib/components/dnd/index.js +243 -23
  16. package/lib/components/settings/wrappers/component/SelectWithTitle.d.ts +2 -1
  17. package/lib/components/settings/wrappers/component/SelectWithTitle.js +14 -12
  18. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.d.ts +3 -0
  19. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +68 -10
  20. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +23 -43
  21. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +352 -295
  22. package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +16 -2
  23. package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.d.ts +36 -0
  24. package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.js +274 -0
  25. package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.d.ts +30 -0
  26. package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +315 -0
  27. package/lib/components/subModel/AddSubModelButton.js +27 -1
  28. package/lib/components/subModel/index.d.ts +1 -0
  29. package/lib/components/subModel/index.js +19 -0
  30. package/lib/components/subModel/utils.d.ts +1 -1
  31. package/lib/components/subModel/utils.js +2 -2
  32. package/lib/data-source/index.d.ts +75 -0
  33. package/lib/data-source/index.js +246 -4
  34. package/lib/executor/FlowExecutor.js +31 -8
  35. package/lib/flowContext.d.ts +2 -0
  36. package/lib/flowContext.js +31 -1
  37. package/lib/flowEngine.d.ts +151 -1
  38. package/lib/flowEngine.js +389 -15
  39. package/lib/flowI18n.js +2 -1
  40. package/lib/flowSettings.d.ts +14 -6
  41. package/lib/flowSettings.js +34 -6
  42. package/lib/lazy-helper.d.ts +14 -0
  43. package/lib/lazy-helper.js +71 -0
  44. package/lib/locale/en-US.json +1 -0
  45. package/lib/locale/index.d.ts +2 -0
  46. package/lib/locale/zh-CN.json +1 -0
  47. package/lib/models/DisplayItemModel.d.ts +1 -1
  48. package/lib/models/EditableItemModel.d.ts +1 -1
  49. package/lib/models/FilterableItemModel.d.ts +1 -1
  50. package/lib/models/flowModel.d.ts +13 -10
  51. package/lib/models/flowModel.js +78 -18
  52. package/lib/provider.js +38 -23
  53. package/lib/reactive/observer.js +46 -16
  54. package/lib/runjs-context/registry.d.ts +1 -1
  55. package/lib/runjs-context/setup.js +20 -12
  56. package/lib/runjs-context/snippets/index.js +13 -2
  57. package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.d.ts +11 -0
  58. package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.js +50 -0
  59. package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.d.ts +11 -0
  60. package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.js +54 -0
  61. package/lib/scheduler/ModelOperationScheduler.d.ts +5 -1
  62. package/lib/scheduler/ModelOperationScheduler.js +3 -2
  63. package/lib/types.d.ts +47 -1
  64. package/lib/utils/createCollectionContextMeta.js +6 -2
  65. package/lib/utils/index.d.ts +2 -2
  66. package/lib/utils/index.js +4 -0
  67. package/lib/utils/parsePathnameToViewParams.js +1 -1
  68. package/lib/utils/runjsTemplateCompat.js +1 -1
  69. package/lib/utils/runjsValue.js +41 -11
  70. package/lib/utils/schema-utils.d.ts +7 -1
  71. package/lib/utils/schema-utils.js +19 -0
  72. package/lib/views/FlowView.d.ts +7 -1
  73. package/lib/views/runViewBeforeClose.d.ts +10 -0
  74. package/lib/views/runViewBeforeClose.js +45 -0
  75. package/lib/views/useDialog.d.ts +2 -1
  76. package/lib/views/useDialog.js +20 -3
  77. package/lib/views/useDrawer.d.ts +2 -1
  78. package/lib/views/useDrawer.js +20 -3
  79. package/lib/views/usePage.d.ts +2 -1
  80. package/lib/views/usePage.js +10 -3
  81. package/package.json +6 -5
  82. package/src/JSRunner.ts +68 -4
  83. package/src/ViewScopedFlowEngine.ts +4 -0
  84. package/src/__tests__/JSRunner.test.ts +27 -1
  85. package/src/__tests__/flow-engine.test.ts +166 -0
  86. package/src/__tests__/flowContext.test.ts +65 -1
  87. package/src/__tests__/flowEngine.modelLoaders.test.ts +245 -0
  88. package/src/__tests__/flowSettings.test.ts +94 -15
  89. package/src/__tests__/objectVariable.test.ts +24 -0
  90. package/src/__tests__/provider.test.tsx +24 -2
  91. package/src/__tests__/renderHiddenInConfig.test.tsx +6 -6
  92. package/src/__tests__/runjsContext.test.ts +16 -0
  93. package/src/__tests__/runjsContextRuntime.test.ts +2 -0
  94. package/src/__tests__/runjsPreprocessDefault.test.ts +23 -0
  95. package/src/__tests__/runjsSnippets.test.ts +21 -0
  96. package/src/__tests__/viewScopedFlowEngine.test.ts +3 -3
  97. package/src/components/FieldModelRenderer.tsx +2 -1
  98. package/src/components/FlowModelRenderer.tsx +18 -6
  99. package/src/components/FormItem.tsx +7 -1
  100. package/src/components/MobilePopup.tsx +4 -2
  101. package/src/components/__tests__/FlowModelRenderer.test.tsx +65 -2
  102. package/src/components/__tests__/FormItem.test.tsx +25 -0
  103. package/src/components/__tests__/dnd.test.ts +44 -0
  104. package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +20 -10
  105. package/src/components/__tests__/gridDragPlanner.test.ts +558 -3
  106. package/src/components/dnd/__tests__/DndProvider.test.tsx +98 -0
  107. package/src/components/dnd/gridDragPlanner.ts +758 -19
  108. package/src/components/dnd/index.tsx +291 -27
  109. package/src/components/settings/wrappers/component/SelectWithTitle.tsx +21 -9
  110. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +88 -10
  111. package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +487 -440
  112. package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +18 -2
  113. package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +189 -3
  114. package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +778 -0
  115. package/src/components/settings/wrappers/contextual/useFloatToolbarPortal.ts +360 -0
  116. package/src/components/settings/wrappers/contextual/useFloatToolbarVisibility.ts +361 -0
  117. package/src/components/subModel/AddSubModelButton.tsx +32 -2
  118. package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +142 -32
  119. package/src/components/subModel/index.ts +1 -0
  120. package/src/components/subModel/utils.ts +1 -1
  121. package/src/data-source/__tests__/collection.test.ts +41 -2
  122. package/src/data-source/__tests__/index.test.ts +68 -1
  123. package/src/data-source/index.ts +303 -5
  124. package/src/executor/FlowExecutor.ts +34 -9
  125. package/src/executor/__tests__/flowExecutor.test.ts +57 -0
  126. package/src/flowContext.ts +37 -3
  127. package/src/flowEngine.ts +445 -11
  128. package/src/flowI18n.ts +2 -1
  129. package/src/flowSettings.ts +40 -6
  130. package/src/lazy-helper.tsx +57 -0
  131. package/src/locale/en-US.json +1 -0
  132. package/src/locale/zh-CN.json +1 -0
  133. package/src/models/DisplayItemModel.tsx +1 -1
  134. package/src/models/EditableItemModel.tsx +1 -1
  135. package/src/models/FilterableItemModel.tsx +1 -1
  136. package/src/models/__tests__/dispatchEvent.when.test.ts +214 -0
  137. package/src/models/__tests__/flowModel.test.ts +19 -3
  138. package/src/models/flowModel.tsx +119 -33
  139. package/src/provider.tsx +41 -25
  140. package/src/reactive/__tests__/observer.test.tsx +82 -0
  141. package/src/reactive/observer.tsx +87 -25
  142. package/src/runjs-context/registry.ts +1 -1
  143. package/src/runjs-context/setup.ts +22 -12
  144. package/src/runjs-context/snippets/index.ts +12 -1
  145. package/src/runjs-context/snippets/scene/detail/set-field-style.snippet.ts +30 -0
  146. package/src/runjs-context/snippets/scene/table/set-cell-style.snippet.ts +34 -0
  147. package/src/scheduler/ModelOperationScheduler.ts +14 -3
  148. package/src/types.ts +60 -0
  149. package/src/utils/__tests__/createCollectionContextMeta.test.ts +48 -0
  150. package/src/utils/__tests__/parsePathnameToViewParams.test.ts +7 -0
  151. package/src/utils/__tests__/runjsValue.test.ts +11 -0
  152. package/src/utils/__tests__/utils.test.ts +62 -0
  153. package/src/utils/createCollectionContextMeta.ts +6 -2
  154. package/src/utils/index.ts +2 -1
  155. package/src/utils/parsePathnameToViewParams.ts +2 -2
  156. package/src/utils/runjsTemplateCompat.ts +1 -1
  157. package/src/utils/runjsValue.ts +50 -11
  158. package/src/utils/schema-utils.ts +30 -1
  159. package/src/views/FlowView.tsx +11 -1
  160. package/src/views/__tests__/runViewBeforeClose.test.ts +30 -0
  161. package/src/views/__tests__/useDialog.closeDestroy.test.tsx +13 -12
  162. package/src/views/runViewBeforeClose.ts +19 -0
  163. package/src/views/useDialog.tsx +25 -3
  164. package/src/views/useDrawer.tsx +25 -3
  165. package/src/views/usePage.tsx +12 -3
@@ -35,6 +35,7 @@ import {
35
35
  import { FlowExitAllException } from './utils/exceptions';
36
36
  import { FlowStepContext } from './hooks/useFlowStep';
37
37
  import { GLOBAL_EMBED_CONTAINER_ID, EMBED_REPLACING_DATA_KEY } from './views';
38
+ import { lazy } from './lazy-helper';
38
39
 
39
40
  const Panel = Collapse.Panel;
40
41
 
@@ -114,16 +115,23 @@ export interface FlowSettingsOpenOptions {
114
115
  onSaved?: () => void | Promise<void>;
115
116
  }
116
117
 
118
+ export type FlowSettingsComponent = React.ComponentType<any>;
119
+ export type FlowSettingsComponentModule = { default?: FlowSettingsComponent } | Record<string, FlowSettingsComponent>;
120
+ export type FlowSettingsComponentLoader = () => Promise<FlowSettingsComponentModule | FlowSettingsComponent>;
121
+ export type FlowSettingsComponentLoaderMap = Record<string, FlowSettingsComponentLoader>;
122
+
117
123
  export class FlowSettings {
118
124
  public components: Record<string, any> = {};
119
125
  public scopes: Record<string, any> = {};
120
126
  private antdComponentsLoaded = false;
121
127
  public enabled: boolean;
128
+ private engine: FlowEngine;
122
129
  #forceEnabled = false; // 强制启用状态,主要用于设计模式下的强制启用
123
130
  public toolbarItems: ToolbarItemConfig[] = [];
124
131
  #emitter: Emitter = new Emitter();
125
132
 
126
133
  constructor(engine: FlowEngine) {
134
+ this.engine = engine;
127
135
  // 初始默认为 false,由 SchemaComponentProvider 根据实际设计模式状态同步设置
128
136
  this.enabled = false;
129
137
  engine.context.defineProperty('flowSettingsEnabled', {
@@ -291,6 +299,30 @@ export class FlowSettings {
291
299
  });
292
300
  }
293
301
 
302
+ public registerComponentLoaders(loaders: FlowSettingsComponentLoaderMap): void {
303
+ Object.entries(loaders).forEach(([name, loader]) => {
304
+ if (this.components[name]) {
305
+ console.warn(`FlowSettings: Component with name '${name}' is already registered and will be overwritten.`);
306
+ }
307
+ this.components[name] = lazy(async () => {
308
+ const loaded = await loader();
309
+ if (typeof loaded === 'function') {
310
+ return { default: loaded };
311
+ }
312
+ if (loaded?.default && typeof loaded.default === 'function') {
313
+ return { default: loaded.default };
314
+ }
315
+ const namedComponent = loaded?.[name];
316
+ if (typeof namedComponent === 'function') {
317
+ return { default: namedComponent };
318
+ }
319
+ throw new Error(
320
+ `FlowSettings: component loader for '${name}' must resolve to a React component or a module exporting it.`,
321
+ );
322
+ });
323
+ });
324
+ }
325
+
294
326
  /**
295
327
  * 添加作用域到 FlowSettings 的作用域注册表中。
296
328
  * 这些作用域可以在 flow step 的 uiSchema 中使用。
@@ -311,13 +343,15 @@ export class FlowSettings {
311
343
  /**
312
344
  * 启用流程设置组件的显示
313
345
  * @example
314
- * flowSettings.enable();
346
+ * await flowSettings.enable();
315
347
  */
316
- public enable(): void {
348
+ public async enable(): Promise<void> {
349
+ await this.engine.preloadModelLoaders();
317
350
  this.enabled = true;
318
351
  }
319
352
 
320
- public forceEnable() {
353
+ public async forceEnable(): Promise<void> {
354
+ await this.engine.preloadModelLoaders();
321
355
  this.#forceEnabled = true;
322
356
  this.enabled = true;
323
357
  }
@@ -325,16 +359,16 @@ export class FlowSettings {
325
359
  /**
326
360
  * 禁用流程设置组件的显示
327
361
  * @example
328
- * flowSettings.disable();
362
+ * await flowSettings.disable();
329
363
  */
330
- public disable(): void {
364
+ public async disable(): Promise<void> {
331
365
  if (this.#forceEnabled) {
332
366
  return;
333
367
  }
334
368
  this.enabled = false;
335
369
  }
336
370
 
337
- public forceDisable() {
371
+ public async forceDisable(): Promise<void> {
338
372
  this.#forceEnabled = false;
339
373
  this.enabled = false;
340
374
  }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import React, { lazy as reactLazy } from 'react';
11
+
12
+ type LazyComponentType<M extends Record<string, any>, K extends keyof M> = {
13
+ [P in K]: M[P];
14
+ };
15
+
16
+ export function lazy<M extends Record<'default', any>>(factory: () => Promise<M>): M['default'];
17
+
18
+ export function lazy<M extends Record<string, any>, K extends keyof M = keyof M>(
19
+ factory: () => Promise<M>,
20
+ ...componentNames: K[]
21
+ ): LazyComponentType<M, K>;
22
+
23
+ export function lazy<M extends Record<string, any>, K extends keyof M>(
24
+ factory: () => Promise<M>,
25
+ ...componentNames: K[]
26
+ ) {
27
+ if (componentNames.length === 0) {
28
+ const LazyComponent = reactLazy(() =>
29
+ factory().then((module) => ({
30
+ default: module.default,
31
+ })),
32
+ );
33
+ const Component = (props) => (
34
+ <React.Suspense fallback={null}>
35
+ <LazyComponent {...props} />
36
+ </React.Suspense>
37
+ );
38
+ return Component;
39
+ }
40
+
41
+ return componentNames.reduce(
42
+ (acc, name) => {
43
+ const LazyComponent = reactLazy(() =>
44
+ factory().then((module) => ({
45
+ default: module[name],
46
+ })),
47
+ );
48
+ acc[name] = ((props) => (
49
+ <React.Suspense fallback={null}>
50
+ <LazyComponent {...props} />
51
+ </React.Suspense>
52
+ )) as M[K];
53
+ return acc;
54
+ },
55
+ {} as LazyComponentType<M, K>,
56
+ );
57
+ }
@@ -34,6 +34,7 @@
34
34
  "Failed to destroy model after creation error": "Failed to destroy model after creation error",
35
35
  "Failed to get action {{action}}": "Failed to get action {{action}}",
36
36
  "Failed to get configurable flows for model {{model}}": "Failed to get configurable flows for model {{model}}",
37
+ "Attributes are unavailable before selecting a record": "Attributes are unavailable before selecting a record",
37
38
  "Failed to import FormDialog": "Failed to import FormDialog",
38
39
  "Failed to import FormDialog or FormStep": "Failed to import FormDialog or FormStep",
39
40
  "Failed to import Formily components": "Failed to import Formily components",
@@ -31,6 +31,7 @@
31
31
  "Failed to destroy model after creation error": "创建错误后销毁模型失败",
32
32
  "Failed to get action {{action}}": "获取 action '{{action}}' 失败",
33
33
  "Failed to get configurable flows for model {{model}}": "获取模型 '{{model}}' 的可配置 flows 失败",
34
+ "Attributes are unavailable before selecting a record": "选择记录之前,当前项属性不可用",
34
35
  "Failed to import FormDialog": "导入 FormDialog 失败",
35
36
  "Failed to import FormDialog or FormStep": "导入 FormDialog 或 FormStep 失败",
36
37
  "Failed to import Formily components": "导入 Formily 组件失败",
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { DefaultStructure } from '@nocobase/flow-engine';
10
+ import type { DefaultStructure } from '../types';
11
11
  import { CollectionFieldModel } from './CollectionFieldModel';
12
12
 
13
13
  export class DisplayItemModel<T extends DefaultStructure = DefaultStructure> extends CollectionFieldModel<T> {}
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { DefaultStructure } from '@nocobase/flow-engine';
10
+ import type { DefaultStructure } from '../types';
11
11
  import { CollectionFieldModel } from './CollectionFieldModel';
12
12
 
13
13
  export class EditableItemModel<T extends DefaultStructure = DefaultStructure> extends CollectionFieldModel<T> {}
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { DefaultStructure } from '@nocobase/flow-engine';
10
+ import type { DefaultStructure } from '../types';
11
11
  import { CollectionFieldModel } from './CollectionFieldModel';
12
12
 
13
13
  export class FilterableItemModel<T extends DefaultStructure = DefaultStructure> extends CollectionFieldModel<T> {}
@@ -105,6 +105,45 @@ describe('dispatchEvent dynamic event flow phase (scheduleModelOperation integra
105
105
  expect(calls).toEqual(['static-a', 'dynamic']);
106
106
  });
107
107
 
108
+ test("phase='afterAllFlows': skips when event aborted by ctx.exitAll()", async () => {
109
+ const engine = new FlowEngine();
110
+ class M extends FlowModel {}
111
+ engine.registerModels({ M });
112
+
113
+ const calls: string[] = [];
114
+
115
+ M.registerFlow({
116
+ key: 'S',
117
+ on: { eventName: 'go' },
118
+ steps: {
119
+ a: { handler: async () => void calls.push('static-a') } as any,
120
+ },
121
+ });
122
+
123
+ const model = engine.createModel({ use: 'M' });
124
+ model.registerFlow('Abort', {
125
+ on: { eventName: 'go' },
126
+ sort: -10,
127
+ steps: {
128
+ d: {
129
+ handler: async (ctx: any) => {
130
+ calls.push('abort');
131
+ ctx.exitAll();
132
+ },
133
+ } as any,
134
+ },
135
+ });
136
+ model.registerFlow('AfterAll', {
137
+ on: { eventName: 'go', phase: 'afterAllFlows' },
138
+ steps: {
139
+ d: { handler: async () => void calls.push('after-all') } as any,
140
+ },
141
+ });
142
+
143
+ await model.dispatchEvent('go');
144
+ expect(calls).toEqual(['abort']);
145
+ });
146
+
108
147
  test("phase='beforeFlow': instance flow runs before the target static flow", async () => {
109
148
  const engine = new FlowEngine();
110
149
  class M extends FlowModel {}
@@ -161,6 +200,39 @@ describe('dispatchEvent dynamic event flow phase (scheduleModelOperation integra
161
200
  expect(calls).toEqual(['static-a', 'static-b', 'dynamic']);
162
201
  });
163
202
 
203
+ test("phase='afterFlow': skips when anchor flow is aborted by ctx.exitAll()", async () => {
204
+ const engine = new FlowEngine();
205
+ class M extends FlowModel {}
206
+ engine.registerModels({ M });
207
+
208
+ const calls: string[] = [];
209
+
210
+ M.registerFlow({
211
+ key: 'S',
212
+ on: { eventName: 'go' },
213
+ steps: {
214
+ a: {
215
+ handler: async (ctx: any) => {
216
+ calls.push('static-a');
217
+ ctx.exitAll();
218
+ },
219
+ } as any,
220
+ b: { handler: async () => void calls.push('static-b') } as any,
221
+ },
222
+ });
223
+
224
+ const model = engine.createModel({ use: 'M' });
225
+ model.registerFlow('D', {
226
+ on: { eventName: 'go', phase: 'afterFlow', flowKey: 'S' },
227
+ steps: {
228
+ d: { handler: async () => void calls.push('dynamic') } as any,
229
+ },
230
+ });
231
+
232
+ await model.dispatchEvent('go');
233
+ expect(calls).toEqual(['static-a']);
234
+ });
235
+
164
236
  test("phase='beforeStep': instance flow runs before the target static step", async () => {
165
237
  const engine = new FlowEngine();
166
238
  class M extends FlowModel {}
@@ -217,6 +289,39 @@ describe('dispatchEvent dynamic event flow phase (scheduleModelOperation integra
217
289
  expect(calls).toEqual(['static-a', 'dynamic', 'static-b']);
218
290
  });
219
291
 
292
+ test("phase='afterStep': skips when anchor step is aborted by ctx.exitAll()", async () => {
293
+ const engine = new FlowEngine();
294
+ class M extends FlowModel {}
295
+ engine.registerModels({ M });
296
+
297
+ const calls: string[] = [];
298
+
299
+ M.registerFlow({
300
+ key: 'S',
301
+ on: { eventName: 'go' },
302
+ steps: {
303
+ a: {
304
+ handler: async (ctx: any) => {
305
+ calls.push('static-a');
306
+ ctx.exitAll();
307
+ },
308
+ } as any,
309
+ b: { handler: async () => void calls.push('static-b') } as any,
310
+ },
311
+ });
312
+
313
+ const model = engine.createModel({ use: 'M' });
314
+ model.registerFlow('D', {
315
+ on: { eventName: 'go', phase: 'afterStep', flowKey: 'S', stepKey: 'a' },
316
+ steps: {
317
+ d: { handler: async () => void calls.push('dynamic') } as any,
318
+ },
319
+ });
320
+
321
+ await model.dispatchEvent('go');
322
+ expect(calls).toEqual(['static-a']);
323
+ });
324
+
220
325
  test("phase='beforeFlow': ctx.exitAll() stops anchor flow and subsequent flows", async () => {
221
326
  const engine = new FlowEngine();
222
327
  class M extends FlowModel {}
@@ -430,6 +535,115 @@ describe('dispatchEvent dynamic event flow phase (scheduleModelOperation integra
430
535
  expect(calls).toEqual(['static-a', 'dynamic']);
431
536
  });
432
537
 
538
+ test('fallback to event:end (missing anchor) skips when event aborted by ctx.exitAll()', async () => {
539
+ const engine = new FlowEngine();
540
+ class M extends FlowModel {}
541
+ engine.registerModels({ M });
542
+
543
+ const calls: string[] = [];
544
+
545
+ M.registerFlow({
546
+ key: 'S',
547
+ on: { eventName: 'go' },
548
+ steps: {
549
+ a: { handler: async () => void calls.push('static-a') } as any,
550
+ },
551
+ });
552
+
553
+ const model = engine.createModel({ use: 'M' });
554
+ model.registerFlow('Abort', {
555
+ on: { eventName: 'go' },
556
+ sort: -10,
557
+ steps: {
558
+ d: {
559
+ handler: async (ctx: any) => {
560
+ calls.push('abort');
561
+ ctx.exitAll();
562
+ },
563
+ } as any,
564
+ },
565
+ });
566
+ model.registerFlow('FallbackToEnd', {
567
+ on: { eventName: 'go', phase: 'beforeFlow', flowKey: 'missing' },
568
+ steps: {
569
+ d: { handler: async () => void calls.push('fallback-end') } as any,
570
+ },
571
+ });
572
+
573
+ await model.dispatchEvent('go');
574
+ expect(calls).toEqual(['abort']);
575
+ });
576
+
577
+ test('event:end is still emitted with aborted=true when exitAll happens', async () => {
578
+ const engine = new FlowEngine();
579
+ class M extends FlowModel {}
580
+ engine.registerModels({ M });
581
+
582
+ const endEvents: any[] = [];
583
+ const onEnd = (payload: any) => {
584
+ endEvents.push(payload);
585
+ };
586
+ engine.emitter.on('model:event:go:end', onEnd);
587
+
588
+ const model = engine.createModel({ use: 'M' });
589
+ model.registerFlow('Abort', {
590
+ on: { eventName: 'go' },
591
+ steps: {
592
+ d: {
593
+ handler: async (ctx: any) => {
594
+ ctx.exitAll();
595
+ },
596
+ } as any,
597
+ },
598
+ });
599
+
600
+ await model.dispatchEvent('go');
601
+ engine.emitter.off('model:event:go:end', onEnd);
602
+
603
+ expect(endEvents).toHaveLength(1);
604
+ expect(endEvents[0]?.aborted).toBe(true);
605
+ });
606
+
607
+ test('flow:end/step:end are emitted with aborted=true when exitAll happens', async () => {
608
+ const engine = new FlowEngine();
609
+ class M extends FlowModel {}
610
+ engine.registerModels({ M });
611
+
612
+ const flowEndEvents: any[] = [];
613
+ const stepEndEvents: any[] = [];
614
+ const onFlowEnd = (payload: any) => {
615
+ flowEndEvents.push(payload);
616
+ };
617
+ const onStepEnd = (payload: any) => {
618
+ stepEndEvents.push(payload);
619
+ };
620
+ engine.emitter.on('model:event:go:flow:S:end', onFlowEnd);
621
+ engine.emitter.on('model:event:go:flow:S:step:a:end', onStepEnd);
622
+
623
+ M.registerFlow({
624
+ key: 'S',
625
+ on: { eventName: 'go' },
626
+ steps: {
627
+ a: {
628
+ handler: async (ctx: any) => {
629
+ ctx.exitAll();
630
+ },
631
+ } as any,
632
+ },
633
+ });
634
+
635
+ const model = engine.createModel({ use: 'M' });
636
+ await model.dispatchEvent('go');
637
+
638
+ engine.emitter.off('model:event:go:flow:S:end', onFlowEnd);
639
+ engine.emitter.off('model:event:go:flow:S:step:a:end', onStepEnd);
640
+
641
+ expect(flowEndEvents).toHaveLength(1);
642
+ expect(stepEndEvents).toHaveLength(1);
643
+ expect(flowEndEvents[0]?.aborted).toBe(true);
644
+ expect(stepEndEvents[0]?.aborted).toBe(true);
645
+ });
646
+
433
647
  test('multiple flows on same anchor: executes by flow.sort asc (stable)', async () => {
434
648
  const engine = new FlowEngine();
435
649
  class M extends FlowModel {}
@@ -1855,7 +1855,7 @@ describe('FlowModel', () => {
1855
1855
  });
1856
1856
 
1857
1857
  describe('serialization', () => {
1858
- test('should serialize basic model data, excluding props and flowEngine', () => {
1858
+ test('should serialize basic model data with the latest props, excluding flowEngine', () => {
1859
1859
  model.sortIndex = 5;
1860
1860
  model.setProps({ name: 'Test Model', value: 42 });
1861
1861
  model.setStepParams({
@@ -1867,13 +1867,12 @@ describe('FlowModel', () => {
1867
1867
  expect(serialized).toEqual(
1868
1868
  expect.objectContaining({
1869
1869
  uid: model.uid,
1870
+ props: expect.objectContaining({ name: 'Test Model', value: 42 }),
1870
1871
  stepParams: expect.objectContaining({ flow1: { step1: { param1: 'value1' } } }),
1871
1872
  sortIndex: 5,
1872
1873
  subModels: expect.any(Object),
1873
1874
  }),
1874
1875
  );
1875
- // props should be excluded from serialization
1876
- expect(serialized.props).toBeUndefined();
1877
1876
  expect(serialized.flowEngine).toBeUndefined();
1878
1877
  });
1879
1878
 
@@ -1892,6 +1891,7 @@ describe('FlowModel', () => {
1892
1891
  expect(serialized).toEqual(
1893
1892
  expect.objectContaining({
1894
1893
  uid: 'empty-model',
1894
+ props: expect.objectContaining({ foo: 'bar' }),
1895
1895
  stepParams: expect.any(Object),
1896
1896
  sortIndex: expect.any(Number),
1897
1897
  subModels: expect.any(Object),
@@ -1899,6 +1899,22 @@ describe('FlowModel', () => {
1899
1899
  );
1900
1900
  expect(serialized.flowEngine).toBeUndefined();
1901
1901
  });
1902
+
1903
+ test('should serialize the latest props after multiple updates', () => {
1904
+ model.setProps({ fieldNames: { title: 'name' }, searchable: true });
1905
+ model.setProps({ fieldNames: { title: 'age' } });
1906
+ model.setProps('defaultExpandAll', false);
1907
+
1908
+ const serialized = model.serialize();
1909
+
1910
+ expect(serialized.props).toEqual(
1911
+ expect.objectContaining({
1912
+ fieldNames: { title: 'age' },
1913
+ searchable: true,
1914
+ defaultExpandAll: false,
1915
+ }),
1916
+ );
1917
+ });
1902
1918
  });
1903
1919
  });
1904
1920