@nocobase/flow-engine 2.1.0-beta.9 → 2.2.0-beta.1

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 (215) hide show
  1. package/lib/FlowContextProvider.d.ts +5 -1
  2. package/lib/FlowContextProvider.js +9 -2
  3. package/lib/components/FieldModelRenderer.js +2 -2
  4. package/lib/components/FlowModelRenderer.d.ts +3 -1
  5. package/lib/components/FlowModelRenderer.js +12 -6
  6. package/lib/components/FormItem.d.ts +6 -0
  7. package/lib/components/FormItem.js +11 -3
  8. package/lib/components/MobilePopup.js +6 -5
  9. package/lib/components/dnd/gridDragPlanner.d.ts +59 -2
  10. package/lib/components/dnd/gridDragPlanner.js +607 -19
  11. package/lib/components/dnd/index.d.ts +31 -2
  12. package/lib/components/dnd/index.js +244 -23
  13. package/lib/components/settings/wrappers/component/SelectWithTitle.d.ts +2 -1
  14. package/lib/components/settings/wrappers/component/SelectWithTitle.js +14 -12
  15. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.d.ts +3 -0
  16. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +152 -42
  17. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +23 -43
  18. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +352 -295
  19. package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.d.ts +36 -0
  20. package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.js +274 -0
  21. package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.d.ts +30 -0
  22. package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +315 -0
  23. package/lib/components/subModel/AddSubModelButton.js +12 -1
  24. package/lib/components/subModel/LazyDropdown.js +301 -52
  25. package/lib/components/subModel/index.d.ts +1 -0
  26. package/lib/components/subModel/index.js +19 -0
  27. package/lib/components/subModel/utils.d.ts +2 -1
  28. package/lib/components/subModel/utils.js +15 -5
  29. package/lib/components/variables/VariableHybridInput.d.ts +27 -0
  30. package/lib/components/variables/VariableHybridInput.js +499 -0
  31. package/lib/components/variables/index.d.ts +2 -0
  32. package/lib/components/variables/index.js +3 -0
  33. package/lib/data-source/index.d.ts +84 -0
  34. package/lib/data-source/index.js +269 -7
  35. package/lib/executor/FlowExecutor.js +6 -3
  36. package/lib/flow-registry/DetachedFlowRegistry.d.ts +21 -0
  37. package/lib/flow-registry/DetachedFlowRegistry.js +80 -0
  38. package/lib/flow-registry/index.d.ts +1 -0
  39. package/lib/flow-registry/index.js +3 -1
  40. package/lib/flowContext.d.ts +9 -1
  41. package/lib/flowContext.js +77 -6
  42. package/lib/flowEngine.d.ts +136 -4
  43. package/lib/flowEngine.js +429 -51
  44. package/lib/flowI18n.js +2 -1
  45. package/lib/flowSettings.d.ts +14 -6
  46. package/lib/flowSettings.js +34 -6
  47. package/lib/index.d.ts +2 -0
  48. package/lib/index.js +7 -0
  49. package/lib/lazy-helper.d.ts +14 -0
  50. package/lib/lazy-helper.js +71 -0
  51. package/lib/locale/en-US.json +1 -0
  52. package/lib/locale/index.d.ts +2 -0
  53. package/lib/locale/zh-CN.json +1 -0
  54. package/lib/models/DisplayItemModel.d.ts +1 -1
  55. package/lib/models/EditableItemModel.d.ts +1 -1
  56. package/lib/models/FilterableItemModel.d.ts +1 -1
  57. package/lib/models/flowModel.d.ts +13 -10
  58. package/lib/models/flowModel.js +126 -34
  59. package/lib/provider.js +38 -23
  60. package/lib/reactive/observer.js +46 -16
  61. package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +4 -3
  62. package/lib/runjs-context/contexts/JSBlockRunJSContext.js +4 -15
  63. package/lib/runjs-context/contexts/JSColumnRunJSContext.js +5 -2
  64. package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.js +5 -8
  65. package/lib/runjs-context/contexts/JSFieldRunJSContext.js +4 -3
  66. package/lib/runjs-context/contexts/JSItemRunJSContext.js +4 -3
  67. package/lib/runjs-context/contexts/base.js +464 -29
  68. package/lib/runjs-context/contexts/elementDoc.d.ts +11 -0
  69. package/lib/runjs-context/contexts/elementDoc.js +152 -0
  70. package/lib/runjs-context/setup.js +1 -0
  71. package/lib/runjs-context/snippets/index.js +13 -2
  72. package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.d.ts +11 -0
  73. package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.js +50 -0
  74. package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.d.ts +11 -0
  75. package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.js +54 -0
  76. package/lib/types.d.ts +50 -2
  77. package/lib/types.js +1 -0
  78. package/lib/utils/createCollectionContextMeta.js +6 -2
  79. package/lib/utils/index.d.ts +3 -2
  80. package/lib/utils/index.js +7 -0
  81. package/lib/utils/loadedPageCache.d.ts +24 -0
  82. package/lib/utils/loadedPageCache.js +139 -0
  83. package/lib/utils/parsePathnameToViewParams.d.ts +5 -1
  84. package/lib/utils/parsePathnameToViewParams.js +28 -4
  85. package/lib/utils/randomId.d.ts +39 -0
  86. package/lib/utils/randomId.js +45 -0
  87. package/lib/utils/runjsTemplateCompat.js +1 -1
  88. package/lib/utils/runjsValue.js +41 -11
  89. package/lib/utils/schema-utils.d.ts +7 -1
  90. package/lib/utils/schema-utils.js +19 -0
  91. package/lib/views/FlowView.d.ts +7 -1
  92. package/lib/views/FlowView.js +11 -1
  93. package/lib/views/PageComponent.js +8 -6
  94. package/lib/views/ViewNavigation.d.ts +12 -2
  95. package/lib/views/ViewNavigation.js +28 -9
  96. package/lib/views/createViewMeta.js +114 -50
  97. package/lib/views/inheritLayoutContext.d.ts +10 -0
  98. package/lib/views/inheritLayoutContext.js +50 -0
  99. package/lib/views/runViewBeforeClose.d.ts +10 -0
  100. package/lib/views/runViewBeforeClose.js +45 -0
  101. package/lib/views/useDialog.d.ts +2 -1
  102. package/lib/views/useDialog.js +12 -3
  103. package/lib/views/useDrawer.d.ts +2 -1
  104. package/lib/views/useDrawer.js +12 -3
  105. package/lib/views/usePage.d.ts +5 -11
  106. package/lib/views/usePage.js +304 -144
  107. package/package.json +5 -4
  108. package/src/FlowContextProvider.tsx +9 -1
  109. package/src/__tests__/createViewMeta.popup.test.ts +115 -1
  110. package/src/__tests__/flow-engine.test.ts +166 -0
  111. package/src/__tests__/flowContext.test.ts +105 -1
  112. package/src/__tests__/flowEngine.modelLoaders.test.ts +245 -0
  113. package/src/__tests__/flowEngine.moveModel.test.ts +81 -1
  114. package/src/__tests__/flowEngine.removeModel.test.ts +47 -3
  115. package/src/__tests__/flowSettings.test.ts +94 -15
  116. package/src/__tests__/objectVariable.test.ts +24 -0
  117. package/src/__tests__/provider.test.tsx +24 -2
  118. package/src/__tests__/renderHiddenInConfig.test.tsx +6 -6
  119. package/src/__tests__/runjsContext.test.ts +21 -0
  120. package/src/__tests__/runjsContextImplementations.test.ts +9 -2
  121. package/src/__tests__/runjsContextRuntime.test.ts +2 -0
  122. package/src/__tests__/runjsLocales.test.ts +6 -5
  123. package/src/__tests__/runjsSnippets.test.ts +21 -0
  124. package/src/__tests__/viewScopedFlowEngine.test.ts +136 -3
  125. package/src/components/FieldModelRenderer.tsx +2 -1
  126. package/src/components/FlowModelRenderer.tsx +18 -6
  127. package/src/components/FormItem.tsx +7 -1
  128. package/src/components/MobilePopup.tsx +4 -2
  129. package/src/components/__tests__/FlowModelRenderer.test.tsx +65 -2
  130. package/src/components/__tests__/FormItem.test.tsx +25 -0
  131. package/src/components/__tests__/dnd.test.ts +44 -0
  132. package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +20 -10
  133. package/src/components/__tests__/gridDragPlanner.test.ts +472 -5
  134. package/src/components/dnd/__tests__/DndProvider.test.tsx +98 -0
  135. package/src/components/dnd/gridDragPlanner.ts +750 -17
  136. package/src/components/dnd/index.tsx +305 -28
  137. package/src/components/settings/wrappers/component/SelectWithTitle.tsx +21 -9
  138. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +178 -48
  139. package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +487 -440
  140. package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +344 -8
  141. package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +778 -0
  142. package/src/components/settings/wrappers/contextual/useFloatToolbarPortal.ts +360 -0
  143. package/src/components/settings/wrappers/contextual/useFloatToolbarVisibility.ts +361 -0
  144. package/src/components/subModel/AddSubModelButton.tsx +16 -2
  145. package/src/components/subModel/LazyDropdown.tsx +341 -56
  146. package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +524 -38
  147. package/src/components/subModel/__tests__/utils.test.ts +24 -0
  148. package/src/components/subModel/index.ts +1 -0
  149. package/src/components/subModel/utils.ts +13 -2
  150. package/src/components/variables/VariableHybridInput.tsx +531 -0
  151. package/src/components/variables/index.ts +2 -0
  152. package/src/data-source/__tests__/collection.test.ts +41 -2
  153. package/src/data-source/__tests__/index.test.ts +69 -2
  154. package/src/data-source/index.ts +332 -8
  155. package/src/executor/FlowExecutor.ts +6 -3
  156. package/src/executor/__tests__/flowExecutor.test.ts +57 -0
  157. package/src/flow-registry/DetachedFlowRegistry.ts +46 -0
  158. package/src/flow-registry/__tests__/detachedFlowRegistry.test.ts +47 -0
  159. package/src/flow-registry/index.ts +1 -0
  160. package/src/flowContext.ts +85 -6
  161. package/src/flowEngine.ts +484 -45
  162. package/src/flowI18n.ts +2 -1
  163. package/src/flowSettings.ts +40 -6
  164. package/src/index.ts +2 -0
  165. package/src/lazy-helper.tsx +57 -0
  166. package/src/locale/en-US.json +1 -0
  167. package/src/locale/zh-CN.json +1 -0
  168. package/src/models/DisplayItemModel.tsx +1 -1
  169. package/src/models/EditableItemModel.tsx +1 -1
  170. package/src/models/FilterableItemModel.tsx +1 -1
  171. package/src/models/__tests__/flowEngine.resolveUse.test.ts +0 -15
  172. package/src/models/__tests__/flowModel.test.ts +65 -37
  173. package/src/models/flowModel.tsx +184 -65
  174. package/src/provider.tsx +41 -25
  175. package/src/reactive/__tests__/observer.test.tsx +82 -0
  176. package/src/reactive/observer.tsx +87 -25
  177. package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +4 -3
  178. package/src/runjs-context/contexts/JSBlockRunJSContext.ts +4 -15
  179. package/src/runjs-context/contexts/JSColumnRunJSContext.ts +4 -2
  180. package/src/runjs-context/contexts/JSEditableFieldRunJSContext.ts +5 -9
  181. package/src/runjs-context/contexts/JSFieldRunJSContext.ts +4 -3
  182. package/src/runjs-context/contexts/JSItemRunJSContext.ts +4 -3
  183. package/src/runjs-context/contexts/base.ts +467 -31
  184. package/src/runjs-context/contexts/elementDoc.ts +130 -0
  185. package/src/runjs-context/setup.ts +1 -0
  186. package/src/runjs-context/snippets/index.ts +12 -1
  187. package/src/runjs-context/snippets/scene/detail/set-field-style.snippet.ts +30 -0
  188. package/src/runjs-context/snippets/scene/table/set-cell-style.snippet.ts +34 -0
  189. package/src/types.ts +62 -0
  190. package/src/utils/__tests__/createCollectionContextMeta.test.ts +48 -0
  191. package/src/utils/__tests__/parsePathnameToViewParams.test.ts +21 -0
  192. package/src/utils/__tests__/runjsValue.test.ts +11 -0
  193. package/src/utils/__tests__/utils.test.ts +62 -0
  194. package/src/utils/createCollectionContextMeta.ts +6 -2
  195. package/src/utils/index.ts +5 -1
  196. package/src/utils/loadedPageCache.ts +147 -0
  197. package/src/utils/parsePathnameToViewParams.ts +45 -5
  198. package/src/utils/randomId.ts +48 -0
  199. package/src/utils/runjsTemplateCompat.ts +1 -1
  200. package/src/utils/runjsValue.ts +50 -11
  201. package/src/utils/schema-utils.ts +30 -1
  202. package/src/views/FlowView.tsx +22 -2
  203. package/src/views/PageComponent.tsx +7 -4
  204. package/src/views/ViewNavigation.ts +46 -9
  205. package/src/views/__tests__/FlowView.usePage.test.tsx +243 -3
  206. package/src/views/__tests__/ViewNavigation.test.ts +52 -0
  207. package/src/views/__tests__/inheritLayoutContext.test.ts +53 -0
  208. package/src/views/__tests__/runViewBeforeClose.test.ts +30 -0
  209. package/src/views/__tests__/useDialog.closeDestroy.test.tsx +12 -12
  210. package/src/views/createViewMeta.ts +106 -34
  211. package/src/views/inheritLayoutContext.ts +26 -0
  212. package/src/views/runViewBeforeClose.ts +19 -0
  213. package/src/views/useDialog.tsx +13 -3
  214. package/src/views/useDrawer.tsx +13 -3
  215. package/src/views/usePage.tsx +367 -180
package/src/flowI18n.ts CHANGED
@@ -52,7 +52,8 @@ export class FlowI18n {
52
52
  */
53
53
  private translateKey(key: string, options?: any): string {
54
54
  if (this.context?.i18n?.t) {
55
- return this.context.i18n.t(key, options);
55
+ const translated = this.context.i18n.t(key, options);
56
+ return translated == null || translated === '' ? key : translated;
56
57
  }
57
58
  // 如果没有翻译函数,返回原始键值
58
59
  return key;
@@ -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
  }
package/src/index.ts CHANGED
@@ -57,5 +57,7 @@ export {
57
57
  } from './views/viewEvents';
58
58
 
59
59
  export * from './FlowDefinition';
60
+ export { DetachedFlowRegistry, replaceFlowRegistry, serializeFlowRegistry } from './flow-registry';
61
+ export type { FlowRegistryData } from './flow-registry';
60
62
  export { createViewScopedEngine } from './ViewScopedFlowEngine';
61
63
  export { createBlockScopedEngine } from './BlockScopedFlowEngine';
@@ -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> {}
@@ -61,21 +61,6 @@ describe('FlowEngine.createModel resolveUse hook', () => {
61
61
  expect(warnSpy).not.toHaveBeenCalled();
62
62
  });
63
63
 
64
- test('should break resolveUse on circular reference and warn', () => {
65
- class LoopModel extends FlowModel {
66
- static resolveUse() {
67
- return 'LoopModel';
68
- }
69
- }
70
-
71
- engine.registerModels({ LoopModel });
72
-
73
- const model = engine.createModel({ use: 'LoopModel', uid: 'loop-model', flowEngine: engine });
74
-
75
- expect(model).toBeInstanceOf(LoopModel);
76
- expect(warnSpy).toHaveBeenCalled();
77
- });
78
-
79
64
  test('should fall back to ErrorFlowModel when resolveUse returns unregistered name', () => {
80
65
  class MissingTargetEntry extends FlowModel {
81
66
  static resolveUse() {
@@ -313,6 +313,19 @@ describe('FlowModel', () => {
313
313
 
314
314
  model.emitter.off('onStepParamsChanged', listener);
315
315
  });
316
+
317
+ test('should not emit onStepParamsChanged when params are unchanged', () => {
318
+ const listener = vi.fn();
319
+ model.emitter.on('onStepParamsChanged', listener);
320
+
321
+ model.setStepParams('testFlow', 'step1', { param1: 'value1' });
322
+ model.setStepParams('testFlow', { step1: { param1: 'value1' } });
323
+ model.setStepParams({ testFlow: { step1: { param1: 'value1' } } });
324
+
325
+ expect(listener).not.toHaveBeenCalled();
326
+
327
+ model.emitter.off('onStepParamsChanged', listener);
328
+ });
316
329
  });
317
330
  });
318
331
 
@@ -370,15 +383,17 @@ describe('FlowModel', () => {
370
383
  };
371
384
 
372
385
  TestFlowModel.registerFlow(exitFlow);
373
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
386
+ const loggerSpy = vi.spyOn(model.flowEngine.logger, 'debug').mockImplementation(() => {});
374
387
 
375
- const result = await model.applyFlow('exitFlow');
376
-
377
- expect(result).toBeInstanceOf(FlowExitAllException);
378
- expect(exitFlow.steps.step2.handler).not.toHaveBeenCalled();
379
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('[FlowModel]'));
388
+ try {
389
+ const result = await model.applyFlow('exitFlow');
380
390
 
381
- consoleSpy.mockRestore();
391
+ expect(result).toBeInstanceOf(FlowExitAllException);
392
+ expect(exitFlow.steps.step2.handler).not.toHaveBeenCalled();
393
+ expect(loggerSpy).toHaveBeenCalledWith(expect.stringContaining('[FlowModel]'));
394
+ } finally {
395
+ loggerSpy.mockRestore();
396
+ }
382
397
  });
383
398
 
384
399
  test('should handle ctx.exit() as FlowExitAllException in beforeRender dispatch', async () => {
@@ -474,15 +489,17 @@ describe('FlowModel', () => {
474
489
  };
475
490
 
476
491
  TestFlowModel.registerFlow(exitFlow);
477
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
478
-
479
- const result = await model.applyFlow('exitFlow');
492
+ const loggerSpy = vi.spyOn(model.flowEngine.logger, 'debug').mockImplementation(() => {});
480
493
 
481
- expect(result).toBeInstanceOf(FlowExitAllException);
482
- expect(exitFlow.steps.step2.handler).not.toHaveBeenCalled();
483
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('[FlowModel]'));
494
+ try {
495
+ const result = await model.applyFlow('exitFlow');
484
496
 
485
- consoleSpy.mockRestore();
497
+ expect(result).toBeInstanceOf(FlowExitAllException);
498
+ expect(exitFlow.steps.step2.handler).not.toHaveBeenCalled();
499
+ expect(loggerSpy).toHaveBeenCalledWith(expect.stringContaining('[FlowModel]'));
500
+ } finally {
501
+ loggerSpy.mockRestore();
502
+ }
486
503
  });
487
504
 
488
505
  test('should propagate step execution errors', async () => {
@@ -768,7 +785,7 @@ describe('FlowModel', () => {
768
785
  const eventFlow = createEventFlowDefinition('testEvent');
769
786
  TestFlowModel.registerFlow(eventFlow);
770
787
 
771
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
788
+ const loggerSpy = vi.spyOn(model.flowEngine.logger, 'debug').mockImplementation(() => {});
772
789
 
773
790
  try {
774
791
  model.dispatchEvent('testEvent', { data: 'payload' });
@@ -776,7 +793,7 @@ describe('FlowModel', () => {
776
793
  // Use a more reliable approach than arbitrary timeout
777
794
  await new Promise((resolve) => setTimeout(resolve, 0));
778
795
 
779
- expect(consoleSpy).toHaveBeenCalledWith(
796
+ expect(loggerSpy).toHaveBeenCalledWith(
780
797
  expect.stringContaining('[FlowModel] dispatchEvent: uid=test-model-uid, event=testEvent'),
781
798
  );
782
799
  expect(eventFlow.steps.eventStep.handler).toHaveBeenCalledWith(
@@ -786,7 +803,7 @@ describe('FlowModel', () => {
786
803
  expect.any(Object),
787
804
  );
788
805
  } finally {
789
- consoleSpy.mockRestore();
806
+ loggerSpy.mockRestore();
790
807
  }
791
808
  });
792
809
 
@@ -1597,7 +1614,7 @@ describe('FlowModel', () => {
1597
1614
  fork1.dispose = vi.fn();
1598
1615
  fork2.dispose = vi.fn();
1599
1616
 
1600
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1617
+ const loggerSpy = vi.spyOn(model.flowEngine.logger, 'debug').mockImplementation(() => {});
1601
1618
 
1602
1619
  try {
1603
1620
  model.clearForks();
@@ -1606,19 +1623,19 @@ describe('FlowModel', () => {
1606
1623
  expect(fork2.dispose).toHaveBeenCalled();
1607
1624
  expect(model.forks.size).toBe(0);
1608
1625
  } finally {
1609
- consoleSpy.mockRestore();
1626
+ loggerSpy.mockRestore();
1610
1627
  }
1611
1628
  });
1612
1629
 
1613
1630
  test('should handle empty forks collection when clearing', () => {
1614
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1631
+ const loggerSpy = vi.spyOn(model.flowEngine.logger, 'debug').mockImplementation(() => {});
1615
1632
 
1616
1633
  try {
1617
1634
  model.clearForks();
1618
1635
 
1619
1636
  expect(model.forks.size).toBe(0);
1620
1637
  } finally {
1621
- consoleSpy.mockRestore();
1638
+ loggerSpy.mockRestore();
1622
1639
  }
1623
1640
  });
1624
1641
  });
@@ -1746,7 +1763,7 @@ describe('FlowModel', () => {
1746
1763
  test('should clean up resources on remove', () => {
1747
1764
  model.createFork();
1748
1765
  model.createFork();
1749
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1766
+ const loggerSpy = vi.spyOn(model.flowEngine.logger, 'debug').mockImplementation(() => {});
1750
1767
 
1751
1768
  // Mock removeModel to simulate proper fork cleanup
1752
1769
  flowEngine.removeModel = vi.fn().mockImplementation(() => {
@@ -1763,7 +1780,7 @@ describe('FlowModel', () => {
1763
1780
  expect(model.forks.size).toBe(0);
1764
1781
  expect(flowEngine.removeModel).toHaveBeenCalledWith(model.uid);
1765
1782
  } finally {
1766
- consoleSpy.mockRestore();
1783
+ loggerSpy.mockRestore();
1767
1784
  }
1768
1785
  });
1769
1786
  });
@@ -1840,22 +1857,17 @@ describe('FlowModel', () => {
1840
1857
  });
1841
1858
 
1842
1859
  test('should rerender triggers beforeRender without cache', async () => {
1843
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1844
1860
  model.dispatchEvent = vi.fn().mockResolvedValue(undefined) as any;
1845
1861
 
1846
- try {
1847
- await expect(model.rerender()).resolves.not.toThrow();
1848
- expect(model.dispatchEvent).toHaveBeenCalledWith('beforeRender', undefined, {
1849
- useCache: false,
1850
- });
1851
- } finally {
1852
- consoleSpy.mockRestore();
1853
- }
1862
+ await expect(model.rerender()).resolves.not.toThrow();
1863
+ expect(model.dispatchEvent).toHaveBeenCalledWith('beforeRender', undefined, {
1864
+ useCache: false,
1865
+ });
1854
1866
  });
1855
1867
  });
1856
1868
 
1857
1869
  describe('serialization', () => {
1858
- test('should serialize basic model data, excluding props and flowEngine', () => {
1870
+ test('should serialize basic model data with the latest props, excluding flowEngine', () => {
1859
1871
  model.sortIndex = 5;
1860
1872
  model.setProps({ name: 'Test Model', value: 42 });
1861
1873
  model.setStepParams({
@@ -1867,13 +1879,12 @@ describe('FlowModel', () => {
1867
1879
  expect(serialized).toEqual(
1868
1880
  expect.objectContaining({
1869
1881
  uid: model.uid,
1882
+ props: expect.objectContaining({ name: 'Test Model', value: 42 }),
1870
1883
  stepParams: expect.objectContaining({ flow1: { step1: { param1: 'value1' } } }),
1871
1884
  sortIndex: 5,
1872
1885
  subModels: expect.any(Object),
1873
1886
  }),
1874
1887
  );
1875
- // props should be excluded from serialization
1876
- expect(serialized.props).toBeUndefined();
1877
1888
  expect(serialized.flowEngine).toBeUndefined();
1878
1889
  });
1879
1890
 
@@ -1892,6 +1903,7 @@ describe('FlowModel', () => {
1892
1903
  expect(serialized).toEqual(
1893
1904
  expect.objectContaining({
1894
1905
  uid: 'empty-model',
1906
+ props: expect.objectContaining({ foo: 'bar' }),
1895
1907
  stepParams: expect.any(Object),
1896
1908
  sortIndex: expect.any(Number),
1897
1909
  subModels: expect.any(Object),
@@ -1899,6 +1911,22 @@ describe('FlowModel', () => {
1899
1911
  );
1900
1912
  expect(serialized.flowEngine).toBeUndefined();
1901
1913
  });
1914
+
1915
+ test('should serialize the latest props after multiple updates', () => {
1916
+ model.setProps({ fieldNames: { title: 'name' }, searchable: true });
1917
+ model.setProps({ fieldNames: { title: 'age' } });
1918
+ model.setProps('defaultExpandAll', false);
1919
+
1920
+ const serialized = model.serialize();
1921
+
1922
+ expect(serialized.props).toEqual(
1923
+ expect.objectContaining({
1924
+ fieldNames: { title: 'age' },
1925
+ searchable: true,
1926
+ defaultExpandAll: false,
1927
+ }),
1928
+ );
1929
+ });
1902
1930
  });
1903
1931
  });
1904
1932
 
@@ -2874,7 +2902,7 @@ describe('FlowModel', () => {
2874
2902
  describe('Edge Cases & Error Handling', () => {
2875
2903
  test('should handle model destruction gracefully', () => {
2876
2904
  const model = new FlowModel(modelOptions);
2877
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
2905
+ const loggerSpy = vi.spyOn(model.flowEngine.logger, 'debug').mockImplementation(() => {});
2878
2906
 
2879
2907
  model.createFork();
2880
2908
  model.setProps({ testProp: 'value' });
@@ -2882,7 +2910,7 @@ describe('FlowModel', () => {
2882
2910
  try {
2883
2911
  expect(() => model.remove()).not.toThrow();
2884
2912
  } finally {
2885
- consoleSpy.mockRestore();
2913
+ loggerSpy.mockRestore();
2886
2914
  }
2887
2915
  });
2888
2916