@nocobase/flow-engine 2.1.0-alpha.4 → 2.1.0-alpha.45

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 (209) hide show
  1. package/LICENSE +201 -661
  2. package/README.md +79 -10
  3. package/lib/FlowContextProvider.d.ts +5 -1
  4. package/lib/FlowContextProvider.js +9 -2
  5. package/lib/JSRunner.d.ts +10 -1
  6. package/lib/JSRunner.js +50 -5
  7. package/lib/ViewScopedFlowEngine.js +5 -1
  8. package/lib/components/FieldModelRenderer.js +2 -2
  9. package/lib/components/FlowModelRenderer.d.ts +3 -1
  10. package/lib/components/FlowModelRenderer.js +12 -6
  11. package/lib/components/FormItem.d.ts +6 -0
  12. package/lib/components/FormItem.js +11 -3
  13. package/lib/components/MobilePopup.js +6 -5
  14. package/lib/components/dnd/gridDragPlanner.d.ts +59 -2
  15. package/lib/components/dnd/gridDragPlanner.js +613 -21
  16. package/lib/components/dnd/index.d.ts +31 -2
  17. package/lib/components/dnd/index.js +244 -23
  18. package/lib/components/settings/wrappers/component/SelectWithTitle.d.ts +2 -1
  19. package/lib/components/settings/wrappers/component/SelectWithTitle.js +14 -12
  20. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.d.ts +3 -0
  21. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +76 -11
  22. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +23 -43
  23. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +352 -295
  24. package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +16 -2
  25. package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.d.ts +36 -0
  26. package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.js +274 -0
  27. package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.d.ts +30 -0
  28. package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +315 -0
  29. package/lib/components/subModel/AddSubModelButton.js +27 -1
  30. package/lib/components/subModel/LazyDropdown.js +293 -52
  31. package/lib/components/subModel/index.d.ts +1 -0
  32. package/lib/components/subModel/index.js +19 -0
  33. package/lib/components/subModel/utils.d.ts +1 -1
  34. package/lib/components/subModel/utils.js +9 -3
  35. package/lib/components/variables/VariableHybridInput.d.ts +27 -0
  36. package/lib/components/variables/VariableHybridInput.js +499 -0
  37. package/lib/components/variables/index.d.ts +2 -0
  38. package/lib/components/variables/index.js +3 -0
  39. package/lib/data-source/index.d.ts +84 -0
  40. package/lib/data-source/index.js +259 -5
  41. package/lib/executor/FlowExecutor.js +32 -9
  42. package/lib/flow-registry/DetachedFlowRegistry.d.ts +21 -0
  43. package/lib/flow-registry/DetachedFlowRegistry.js +80 -0
  44. package/lib/flow-registry/index.d.ts +1 -0
  45. package/lib/flow-registry/index.js +3 -1
  46. package/lib/flowContext.d.ts +3 -0
  47. package/lib/flowContext.js +46 -1
  48. package/lib/flowEngine.d.ts +151 -1
  49. package/lib/flowEngine.js +392 -18
  50. package/lib/flowI18n.js +2 -1
  51. package/lib/flowSettings.d.ts +14 -6
  52. package/lib/flowSettings.js +34 -6
  53. package/lib/index.d.ts +2 -0
  54. package/lib/index.js +7 -0
  55. package/lib/lazy-helper.d.ts +14 -0
  56. package/lib/lazy-helper.js +71 -0
  57. package/lib/locale/en-US.json +1 -0
  58. package/lib/locale/index.d.ts +2 -0
  59. package/lib/locale/zh-CN.json +1 -0
  60. package/lib/models/DisplayItemModel.d.ts +1 -1
  61. package/lib/models/EditableItemModel.d.ts +1 -1
  62. package/lib/models/FilterableItemModel.d.ts +1 -1
  63. package/lib/models/flowModel.d.ts +13 -10
  64. package/lib/models/flowModel.js +81 -21
  65. package/lib/provider.js +38 -23
  66. package/lib/reactive/observer.js +46 -16
  67. package/lib/runjs-context/registry.d.ts +1 -1
  68. package/lib/runjs-context/setup.js +20 -12
  69. package/lib/runjs-context/snippets/index.js +13 -2
  70. package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.d.ts +11 -0
  71. package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.js +50 -0
  72. package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.d.ts +11 -0
  73. package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.js +54 -0
  74. package/lib/scheduler/ModelOperationScheduler.d.ts +5 -1
  75. package/lib/scheduler/ModelOperationScheduler.js +3 -2
  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/parsePathnameToViewParams.d.ts +5 -1
  82. package/lib/utils/parsePathnameToViewParams.js +29 -5
  83. package/lib/utils/randomId.d.ts +39 -0
  84. package/lib/utils/randomId.js +45 -0
  85. package/lib/utils/runjsTemplateCompat.js +1 -1
  86. package/lib/utils/runjsValue.js +41 -11
  87. package/lib/utils/schema-utils.d.ts +7 -1
  88. package/lib/utils/schema-utils.js +19 -0
  89. package/lib/views/FlowView.d.ts +7 -1
  90. package/lib/views/FlowView.js +11 -1
  91. package/lib/views/PageComponent.js +8 -6
  92. package/lib/views/ViewNavigation.d.ts +12 -2
  93. package/lib/views/ViewNavigation.js +28 -9
  94. package/lib/views/createViewMeta.js +114 -50
  95. package/lib/views/inheritLayoutContext.d.ts +10 -0
  96. package/lib/views/inheritLayoutContext.js +50 -0
  97. package/lib/views/runViewBeforeClose.d.ts +10 -0
  98. package/lib/views/runViewBeforeClose.js +45 -0
  99. package/lib/views/useDialog.d.ts +2 -1
  100. package/lib/views/useDialog.js +22 -3
  101. package/lib/views/useDrawer.d.ts +2 -1
  102. package/lib/views/useDrawer.js +22 -3
  103. package/lib/views/usePage.d.ts +5 -11
  104. package/lib/views/usePage.js +304 -144
  105. package/package.json +6 -5
  106. package/src/FlowContextProvider.tsx +9 -1
  107. package/src/JSRunner.ts +68 -4
  108. package/src/ViewScopedFlowEngine.ts +4 -0
  109. package/src/__tests__/JSRunner.test.ts +27 -1
  110. package/src/__tests__/createViewMeta.popup.test.ts +115 -1
  111. package/src/__tests__/flow-engine.test.ts +166 -0
  112. package/src/__tests__/flowContext.test.ts +82 -1
  113. package/src/__tests__/flowEngine.modelLoaders.test.ts +245 -0
  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 +16 -0
  120. package/src/__tests__/runjsContextRuntime.test.ts +2 -0
  121. package/src/__tests__/runjsPreprocessDefault.test.ts +23 -0
  122. package/src/__tests__/runjsSnippets.test.ts +21 -0
  123. package/src/__tests__/viewScopedFlowEngine.test.ts +3 -3
  124. package/src/components/FieldModelRenderer.tsx +2 -1
  125. package/src/components/FlowModelRenderer.tsx +18 -6
  126. package/src/components/FormItem.tsx +7 -1
  127. package/src/components/MobilePopup.tsx +4 -2
  128. package/src/components/__tests__/FlowModelRenderer.test.tsx +65 -2
  129. package/src/components/__tests__/FormItem.test.tsx +25 -0
  130. package/src/components/__tests__/dnd.test.ts +44 -0
  131. package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +20 -10
  132. package/src/components/__tests__/gridDragPlanner.test.ts +558 -3
  133. package/src/components/dnd/__tests__/DndProvider.test.tsx +98 -0
  134. package/src/components/dnd/gridDragPlanner.ts +758 -19
  135. package/src/components/dnd/index.tsx +305 -28
  136. package/src/components/settings/wrappers/component/SelectWithTitle.tsx +21 -9
  137. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +99 -11
  138. package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +487 -440
  139. package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +18 -2
  140. package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +194 -5
  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 +32 -2
  145. package/src/components/subModel/LazyDropdown.tsx +332 -56
  146. package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +522 -37
  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 +7 -1
  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 +68 -1
  154. package/src/data-source/index.ts +322 -6
  155. package/src/executor/FlowExecutor.ts +35 -10
  156. package/src/executor/__tests__/flowExecutor.test.ts +85 -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 +50 -3
  161. package/src/flowEngine.ts +449 -14
  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__/dispatchEvent.when.test.ts +214 -0
  172. package/src/models/__tests__/flowEngine.resolveUse.test.ts +0 -15
  173. package/src/models/__tests__/flowModel.test.ts +80 -37
  174. package/src/models/flowModel.tsx +122 -36
  175. package/src/provider.tsx +41 -25
  176. package/src/reactive/__tests__/observer.test.tsx +82 -0
  177. package/src/reactive/observer.tsx +87 -25
  178. package/src/runjs-context/registry.ts +1 -1
  179. package/src/runjs-context/setup.ts +22 -12
  180. package/src/runjs-context/snippets/index.ts +12 -1
  181. package/src/runjs-context/snippets/scene/detail/set-field-style.snippet.ts +30 -0
  182. package/src/runjs-context/snippets/scene/table/set-cell-style.snippet.ts +34 -0
  183. package/src/scheduler/ModelOperationScheduler.ts +14 -3
  184. package/src/types.ts +62 -0
  185. package/src/utils/__tests__/createCollectionContextMeta.test.ts +48 -0
  186. package/src/utils/__tests__/parsePathnameToViewParams.test.ts +28 -0
  187. package/src/utils/__tests__/runjsValue.test.ts +11 -0
  188. package/src/utils/__tests__/utils.test.ts +62 -0
  189. package/src/utils/createCollectionContextMeta.ts +6 -2
  190. package/src/utils/index.ts +5 -1
  191. package/src/utils/parsePathnameToViewParams.ts +47 -7
  192. package/src/utils/randomId.ts +48 -0
  193. package/src/utils/runjsTemplateCompat.ts +1 -1
  194. package/src/utils/runjsValue.ts +50 -11
  195. package/src/utils/schema-utils.ts +30 -1
  196. package/src/views/FlowView.tsx +22 -2
  197. package/src/views/PageComponent.tsx +7 -4
  198. package/src/views/ViewNavigation.ts +46 -9
  199. package/src/views/__tests__/FlowView.usePage.test.tsx +243 -3
  200. package/src/views/__tests__/ViewNavigation.test.ts +52 -0
  201. package/src/views/__tests__/inheritLayoutContext.test.ts +53 -0
  202. package/src/views/__tests__/runViewBeforeClose.test.ts +30 -0
  203. package/src/views/__tests__/useDialog.closeDestroy.test.tsx +13 -12
  204. package/src/views/createViewMeta.ts +106 -34
  205. package/src/views/inheritLayoutContext.ts +26 -0
  206. package/src/views/runViewBeforeClose.ts +19 -0
  207. package/src/views/useDialog.tsx +27 -3
  208. package/src/views/useDrawer.tsx +27 -3
  209. package/src/views/usePage.tsx +367 -179
@@ -370,15 +370,17 @@ describe('FlowModel', () => {
370
370
  };
371
371
 
372
372
  TestFlowModel.registerFlow(exitFlow);
373
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
373
+ const loggerSpy = vi.spyOn(model.flowEngine.logger, 'debug').mockImplementation(() => {});
374
374
 
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]'));
375
+ try {
376
+ const result = await model.applyFlow('exitFlow');
380
377
 
381
- consoleSpy.mockRestore();
378
+ expect(result).toBeInstanceOf(FlowExitAllException);
379
+ expect(exitFlow.steps.step2.handler).not.toHaveBeenCalled();
380
+ expect(loggerSpy).toHaveBeenCalledWith(expect.stringContaining('[FlowModel]'));
381
+ } finally {
382
+ loggerSpy.mockRestore();
383
+ }
382
384
  });
383
385
 
384
386
  test('should handle ctx.exit() as FlowExitAllException in beforeRender dispatch', async () => {
@@ -474,15 +476,17 @@ describe('FlowModel', () => {
474
476
  };
475
477
 
476
478
  TestFlowModel.registerFlow(exitFlow);
477
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
479
+ const loggerSpy = vi.spyOn(model.flowEngine.logger, 'debug').mockImplementation(() => {});
478
480
 
479
- const result = await model.applyFlow('exitFlow');
480
-
481
- expect(result).toBeInstanceOf(FlowExitAllException);
482
- expect(exitFlow.steps.step2.handler).not.toHaveBeenCalled();
483
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('[FlowModel]'));
481
+ try {
482
+ const result = await model.applyFlow('exitFlow');
484
483
 
485
- consoleSpy.mockRestore();
484
+ expect(result).toBeInstanceOf(FlowExitAllException);
485
+ expect(exitFlow.steps.step2.handler).not.toHaveBeenCalled();
486
+ expect(loggerSpy).toHaveBeenCalledWith(expect.stringContaining('[FlowModel]'));
487
+ } finally {
488
+ loggerSpy.mockRestore();
489
+ }
486
490
  });
487
491
 
488
492
  test('should propagate step execution errors', async () => {
@@ -546,6 +550,34 @@ describe('FlowModel', () => {
546
550
 
547
551
  loggerSpy.mockRestore();
548
552
  });
553
+
554
+ test('should warn and skip step when use and handler are both missing', async () => {
555
+ const warnSpy = vi.spyOn(model.context.logger, 'warn').mockImplementation(() => {});
556
+ const errorSpy = vi.spyOn(model.context.logger, 'error').mockImplementation(() => {});
557
+
558
+ TestFlowModel.registerFlow({
559
+ key: 'settingsOnlyFlow',
560
+ steps: {
561
+ edit: {
562
+ title: 'Edit',
563
+ uiSchema: {},
564
+ },
565
+ },
566
+ });
567
+
568
+ const result = await model.applyFlow('settingsOnlyFlow');
569
+
570
+ expect(result).toEqual({});
571
+ expect(warnSpy).toHaveBeenCalledWith(
572
+ expect.stringContaining("Step 'edit' in flow 'settingsOnlyFlow' has neither 'use' nor 'handler'"),
573
+ );
574
+ expect(errorSpy).not.toHaveBeenCalledWith(
575
+ expect.stringContaining("Step 'edit' in flow 'settingsOnlyFlow' has neither 'use' nor 'handler'"),
576
+ );
577
+
578
+ warnSpy.mockRestore();
579
+ errorSpy.mockRestore();
580
+ });
549
581
  });
550
582
 
551
583
  describe('beforeRender flows', () => {
@@ -768,7 +800,7 @@ describe('FlowModel', () => {
768
800
  const eventFlow = createEventFlowDefinition('testEvent');
769
801
  TestFlowModel.registerFlow(eventFlow);
770
802
 
771
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
803
+ const loggerSpy = vi.spyOn(model.flowEngine.logger, 'debug').mockImplementation(() => {});
772
804
 
773
805
  try {
774
806
  model.dispatchEvent('testEvent', { data: 'payload' });
@@ -776,7 +808,7 @@ describe('FlowModel', () => {
776
808
  // Use a more reliable approach than arbitrary timeout
777
809
  await new Promise((resolve) => setTimeout(resolve, 0));
778
810
 
779
- expect(consoleSpy).toHaveBeenCalledWith(
811
+ expect(loggerSpy).toHaveBeenCalledWith(
780
812
  expect.stringContaining('[FlowModel] dispatchEvent: uid=test-model-uid, event=testEvent'),
781
813
  );
782
814
  expect(eventFlow.steps.eventStep.handler).toHaveBeenCalledWith(
@@ -786,7 +818,7 @@ describe('FlowModel', () => {
786
818
  expect.any(Object),
787
819
  );
788
820
  } finally {
789
- consoleSpy.mockRestore();
821
+ loggerSpy.mockRestore();
790
822
  }
791
823
  });
792
824
 
@@ -1597,7 +1629,7 @@ describe('FlowModel', () => {
1597
1629
  fork1.dispose = vi.fn();
1598
1630
  fork2.dispose = vi.fn();
1599
1631
 
1600
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1632
+ const loggerSpy = vi.spyOn(model.flowEngine.logger, 'debug').mockImplementation(() => {});
1601
1633
 
1602
1634
  try {
1603
1635
  model.clearForks();
@@ -1606,19 +1638,19 @@ describe('FlowModel', () => {
1606
1638
  expect(fork2.dispose).toHaveBeenCalled();
1607
1639
  expect(model.forks.size).toBe(0);
1608
1640
  } finally {
1609
- consoleSpy.mockRestore();
1641
+ loggerSpy.mockRestore();
1610
1642
  }
1611
1643
  });
1612
1644
 
1613
1645
  test('should handle empty forks collection when clearing', () => {
1614
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1646
+ const loggerSpy = vi.spyOn(model.flowEngine.logger, 'debug').mockImplementation(() => {});
1615
1647
 
1616
1648
  try {
1617
1649
  model.clearForks();
1618
1650
 
1619
1651
  expect(model.forks.size).toBe(0);
1620
1652
  } finally {
1621
- consoleSpy.mockRestore();
1653
+ loggerSpy.mockRestore();
1622
1654
  }
1623
1655
  });
1624
1656
  });
@@ -1746,7 +1778,7 @@ describe('FlowModel', () => {
1746
1778
  test('should clean up resources on remove', () => {
1747
1779
  model.createFork();
1748
1780
  model.createFork();
1749
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1781
+ const loggerSpy = vi.spyOn(model.flowEngine.logger, 'debug').mockImplementation(() => {});
1750
1782
 
1751
1783
  // Mock removeModel to simulate proper fork cleanup
1752
1784
  flowEngine.removeModel = vi.fn().mockImplementation(() => {
@@ -1763,7 +1795,7 @@ describe('FlowModel', () => {
1763
1795
  expect(model.forks.size).toBe(0);
1764
1796
  expect(flowEngine.removeModel).toHaveBeenCalledWith(model.uid);
1765
1797
  } finally {
1766
- consoleSpy.mockRestore();
1798
+ loggerSpy.mockRestore();
1767
1799
  }
1768
1800
  });
1769
1801
  });
@@ -1840,22 +1872,17 @@ describe('FlowModel', () => {
1840
1872
  });
1841
1873
 
1842
1874
  test('should rerender triggers beforeRender without cache', async () => {
1843
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1844
1875
  model.dispatchEvent = vi.fn().mockResolvedValue(undefined) as any;
1845
1876
 
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
- }
1877
+ await expect(model.rerender()).resolves.not.toThrow();
1878
+ expect(model.dispatchEvent).toHaveBeenCalledWith('beforeRender', undefined, {
1879
+ useCache: false,
1880
+ });
1854
1881
  });
1855
1882
  });
1856
1883
 
1857
1884
  describe('serialization', () => {
1858
- test('should serialize basic model data, excluding props and flowEngine', () => {
1885
+ test('should serialize basic model data with the latest props, excluding flowEngine', () => {
1859
1886
  model.sortIndex = 5;
1860
1887
  model.setProps({ name: 'Test Model', value: 42 });
1861
1888
  model.setStepParams({
@@ -1867,13 +1894,12 @@ describe('FlowModel', () => {
1867
1894
  expect(serialized).toEqual(
1868
1895
  expect.objectContaining({
1869
1896
  uid: model.uid,
1897
+ props: expect.objectContaining({ name: 'Test Model', value: 42 }),
1870
1898
  stepParams: expect.objectContaining({ flow1: { step1: { param1: 'value1' } } }),
1871
1899
  sortIndex: 5,
1872
1900
  subModels: expect.any(Object),
1873
1901
  }),
1874
1902
  );
1875
- // props should be excluded from serialization
1876
- expect(serialized.props).toBeUndefined();
1877
1903
  expect(serialized.flowEngine).toBeUndefined();
1878
1904
  });
1879
1905
 
@@ -1892,6 +1918,7 @@ describe('FlowModel', () => {
1892
1918
  expect(serialized).toEqual(
1893
1919
  expect.objectContaining({
1894
1920
  uid: 'empty-model',
1921
+ props: expect.objectContaining({ foo: 'bar' }),
1895
1922
  stepParams: expect.any(Object),
1896
1923
  sortIndex: expect.any(Number),
1897
1924
  subModels: expect.any(Object),
@@ -1899,6 +1926,22 @@ describe('FlowModel', () => {
1899
1926
  );
1900
1927
  expect(serialized.flowEngine).toBeUndefined();
1901
1928
  });
1929
+
1930
+ test('should serialize the latest props after multiple updates', () => {
1931
+ model.setProps({ fieldNames: { title: 'name' }, searchable: true });
1932
+ model.setProps({ fieldNames: { title: 'age' } });
1933
+ model.setProps('defaultExpandAll', false);
1934
+
1935
+ const serialized = model.serialize();
1936
+
1937
+ expect(serialized.props).toEqual(
1938
+ expect.objectContaining({
1939
+ fieldNames: { title: 'age' },
1940
+ searchable: true,
1941
+ defaultExpandAll: false,
1942
+ }),
1943
+ );
1944
+ });
1902
1945
  });
1903
1946
  });
1904
1947
 
@@ -2874,7 +2917,7 @@ describe('FlowModel', () => {
2874
2917
  describe('Edge Cases & Error Handling', () => {
2875
2918
  test('should handle model destruction gracefully', () => {
2876
2919
  const model = new FlowModel(modelOptions);
2877
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
2920
+ const loggerSpy = vi.spyOn(model.flowEngine.logger, 'debug').mockImplementation(() => {});
2878
2921
 
2879
2922
  model.createFork();
2880
2923
  model.setProps({ testProp: 'value' });
@@ -2882,7 +2925,7 @@ describe('FlowModel', () => {
2882
2925
  try {
2883
2926
  expect(() => model.remove()).not.toThrow();
2884
2927
  } finally {
2885
- consoleSpy.mockRestore();
2928
+ loggerSpy.mockRestore();
2886
2929
  }
2887
2930
  });
2888
2931
 
@@ -11,8 +11,6 @@ import { batch, define, observable, observe } from '@formily/reactive';
11
11
  import _ from 'lodash';
12
12
  import React from 'react';
13
13
  import { uid } from 'uid/secure';
14
- import { openRequiredParamsStepFormDialog as openRequiredParamsStepFormDialogFn } from '../components/settings/wrappers/contextual/StepRequiredSettingsDialog';
15
- import { openStepSettingsDialog as openStepSettingsDialogFn } from '../components/settings/wrappers/contextual/StepSettingsDialog';
16
14
  import { Emitter } from '../emitter';
17
15
  import { InstanceFlowRegistry } from '../flow-registry/InstanceFlowRegistry';
18
16
  import { FlowContext, FlowModelContext, FlowRuntimeContext } from '../flowContext';
@@ -36,7 +34,9 @@ import type {
36
34
  import { IModelComponentProps, ReadonlyModelProps } from '../types';
37
35
  import { isInheritedFrom, setupRuntimeContextSteps } from '../utils';
38
36
  // import { FlowExitAllException } from '../utils/exceptions';
39
- import { Typography } from 'antd/lib';
37
+ import { Typography } from 'antd';
38
+ import type { MenuProps } from 'antd';
39
+ import { observer } from '..';
40
40
  import { ModelActionRegistry } from '../action-registry/ModelActionRegistry';
41
41
  import { buildSubModelItem } from '../components/subModel/utils';
42
42
  import { ModelEventRegistry } from '../event-registry/ModelEventRegistry';
@@ -46,8 +46,6 @@ import { FlowSettingsOpenOptions } from '../flowSettings';
46
46
  import type { ScheduleOptions } from '../scheduler/ModelOperationScheduler';
47
47
  import type { DispatchEventOptions, EventDefinition } from '../types';
48
48
  import { ForkFlowModel } from './forkFlowModel';
49
- import type { MenuProps } from 'antd';
50
- import { observer } from '..';
51
49
 
52
50
  // 使用 WeakMap 为每个类缓存一个 ModelActionRegistry 实例
53
51
  const classActionRegistries = new WeakMap<typeof FlowModel, ModelActionRegistry>();
@@ -62,16 +60,22 @@ const modelMetas = new WeakMap<typeof FlowModel, FlowModelMeta>();
62
60
  const modelGlobalRegistries = new WeakMap<typeof FlowModel, GlobalFlowRegistry>();
63
61
 
64
62
  type BaseMenuItem = NonNullable<MenuProps['items']>[number];
65
- type MenuLeafItem = Exclude<BaseMenuItem, { children: MenuProps['items'] }>;
63
+ type MenuBaseItem = Omit<Exclude<BaseMenuItem, null>, 'key' | 'children'>;
66
64
 
67
- export type FlowModelExtraMenuItem = Omit<MenuLeafItem, 'key'> & {
65
+ export type FlowModelExtraMenuItem = MenuBaseItem & {
68
66
  key: React.Key;
69
67
  group?: string;
70
68
  sort?: number;
69
+ label?: React.ReactNode;
70
+ disabled?: boolean;
71
71
  onClick?: () => void;
72
+ children?: FlowModelExtraMenuItem[];
72
73
  };
73
74
 
74
- type FlowModelExtraMenuItemInput = Omit<FlowModelExtraMenuItem, 'key'> & { key?: React.Key };
75
+ type FlowModelExtraMenuItemInput = Omit<FlowModelExtraMenuItem, 'key' | 'children'> & {
76
+ key?: React.Key;
77
+ children?: FlowModelExtraMenuItemInput[];
78
+ };
75
79
 
76
80
  type ExtraMenuItemEntry = {
77
81
  group?: string;
@@ -88,6 +92,66 @@ type ExtraMenuItemEntry = {
88
92
 
89
93
  const classMenuExtensions = new WeakMap<typeof FlowModel, Set<ExtraMenuItemEntry>>();
90
94
 
95
+ const sortExtraMenuItems = (items: FlowModelExtraMenuItem[]) => {
96
+ return [...items].sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0));
97
+ };
98
+
99
+ const isFlowModelExtraMenuItem = (item: FlowModelExtraMenuItem | null): item is FlowModelExtraMenuItem => {
100
+ return item !== null;
101
+ };
102
+
103
+ const normalizeExtraMenuItem = (
104
+ item: FlowModelExtraMenuItemInput,
105
+ {
106
+ group,
107
+ sort,
108
+ prefix,
109
+ path,
110
+ }: {
111
+ group: string;
112
+ sort: number;
113
+ prefix: string;
114
+ path: string;
115
+ },
116
+ ): FlowModelExtraMenuItem | null => {
117
+ if (!item) {
118
+ return null;
119
+ }
120
+
121
+ const normalizedGroup = item.group || group;
122
+ const normalizedSort = typeof item.sort === 'number' ? item.sort : sort;
123
+ const normalizedChildren = sortExtraMenuItems(
124
+ (item.children || [])
125
+ .map((child, index) =>
126
+ normalizeExtraMenuItem(child, {
127
+ group: normalizedGroup,
128
+ sort: normalizedSort,
129
+ prefix,
130
+ path: `${path}-${index}`,
131
+ }),
132
+ )
133
+ .filter(isFlowModelExtraMenuItem),
134
+ );
135
+
136
+ return {
137
+ ...item,
138
+ key: item.key ?? `${prefix}-${normalizedGroup}-${path}`,
139
+ group: normalizedGroup,
140
+ sort: normalizedSort,
141
+ children: normalizedChildren.length ? normalizedChildren : undefined,
142
+ };
143
+ };
144
+
145
+ async function loadOpenStepSettingsDialog() {
146
+ const mod = await import('../components/settings/wrappers/contextual/StepSettingsDialog');
147
+ return mod.openStepSettingsDialog;
148
+ }
149
+
150
+ async function loadOpenRequiredParamsStepFormDialog() {
151
+ const mod = await import('../components/settings/wrappers/contextual/StepRequiredSettingsDialog');
152
+ return mod.openRequiredParamsStepFormDialog;
153
+ }
154
+
91
155
  export enum ModelRenderMode {
92
156
  ReactElement = 'reactElement',
93
157
  RenderFunction = 'renderFunction',
@@ -208,11 +272,14 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
208
272
  if (changed.type === 'set' && _.isEqual(changed.value, changed.oldValue)) {
209
273
  return;
210
274
  }
275
+ const hasLastAutoRun = !!this._lastAutoRunParams;
211
276
 
212
277
  if (this.flowEngine) {
213
278
  this.invalidateFlowCache('beforeRender');
214
279
  }
215
- this._rerunLastAutoRun();
280
+ if (hasLastAutoRun) {
281
+ this._rerunLastAutoRun();
282
+ }
216
283
  this.forks.forEach((fork) => {
217
284
  fork.rerender();
218
285
  });
@@ -695,6 +762,8 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
695
762
  } else {
696
763
  this.props = { ...this.props, ...props };
697
764
  }
765
+
766
+ this._options.props = { ...this.props };
698
767
  }
699
768
 
700
769
  getProps(): ReadonlyModelProps {
@@ -754,7 +823,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
754
823
  }
755
824
  const isFork = (this as any).isFork === true;
756
825
  const target = this;
757
- console.log(
826
+ currentFlowEngine.logger.debug(
758
827
  `[FlowModel] applyFlow: uid=${this.uid}, flowKey=${flowKey}, isFork=${isFork}, cleanRun=${
759
828
  this.cleanRun
760
829
  }, targetIsFork=${(target as any)?.isFork === true}`,
@@ -774,7 +843,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
774
843
  }
775
844
  const isFork = (this as any).isFork === true;
776
845
  const target = this;
777
- console.log(
846
+ currentFlowEngine.logger.debug(
778
847
  `[FlowModel] dispatchEvent: uid=${this.uid}, event=${eventName}, isFork=${isFork}, cleanRun=${
779
848
  this.cleanRun
780
849
  }, targetIsFork=${(target as any)?.isFork === true}`,
@@ -858,6 +927,11 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
858
927
  }
859
928
  }, 100);
860
929
 
930
+ private resetAutoRunState(): void {
931
+ this._rerunLastAutoRun?.cancel?.();
932
+ this._lastAutoRunParams = null;
933
+ }
934
+
861
935
  /**
862
936
  * 通用事件分发钩子:开始
863
937
  * 子类可覆盖;beforeRender 事件可通过抛出 FlowExitException 提前终止。
@@ -951,7 +1025,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
951
1025
  }
952
1026
 
953
1027
  // 创建缓存的响应式包装器组件工厂(只创建一次)
954
- const createReactiveWrapper = (modelInstance: any) => {
1028
+ const createReactiveWrapper = (modelInstance: FlowModel) => {
955
1029
  const ReactiveWrapper = observer(() => {
956
1030
  // 触发响应式更新的关键属性访问(读取 run/渲染目标的 props)
957
1031
  const renderTarget = modelInstance;
@@ -977,6 +1051,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
977
1051
  model: renderTarget,
978
1052
  });
979
1053
  return () => {
1054
+ renderTarget.resetAutoRunState();
980
1055
  if (typeof renderTarget.onUnmount === 'function') {
981
1056
  renderTarget.onUnmount();
982
1057
  }
@@ -1177,17 +1252,17 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
1177
1252
  return model;
1178
1253
  }
1179
1254
 
1180
- filterSubModels<K extends keyof Structure['subModels'], R>(
1255
+ filterSubModels<K extends keyof NonNullable<Structure['subModels']>, R>(
1181
1256
  subKey: K,
1182
- callback: (model: ArrayElementType<Structure['subModels'][K]>, index: number) => boolean,
1183
- ): ArrayElementType<Structure['subModels'][K]>[] {
1257
+ callback: (model: ArrayElementType<NonNullable<Structure['subModels']>[K]>, index: number) => boolean,
1258
+ ): ArrayElementType<NonNullable<Structure['subModels']>[K]>[] {
1184
1259
  const model = (this.subModels as any)[subKey as string];
1185
1260
 
1186
1261
  if (!model) {
1187
1262
  return [];
1188
1263
  }
1189
1264
 
1190
- const results: ArrayElementType<Structure['subModels'][K]>[] = [];
1265
+ const results: ArrayElementType<NonNullable<Structure['subModels']>[K]>[] = [];
1191
1266
 
1192
1267
  _.castArray(model)
1193
1268
  .sort((a, b) => (a.sortIndex || 0) - (b.sortIndex || 0))
@@ -1201,9 +1276,9 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
1201
1276
  return results;
1202
1277
  }
1203
1278
 
1204
- mapSubModels<K extends keyof Structure['subModels'], R>(
1279
+ mapSubModels<K extends keyof NonNullable<Structure['subModels']>, R>(
1205
1280
  subKey: K,
1206
- callback: (model: ArrayElementType<Structure['subModels'][K]>, index: number) => R,
1281
+ callback: (model: ArrayElementType<NonNullable<Structure['subModels']>[K]>, index: number) => R,
1207
1282
  ): R[] {
1208
1283
  const model = (this.subModels as any)[subKey as string];
1209
1284
 
@@ -1223,7 +1298,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
1223
1298
  return results;
1224
1299
  }
1225
1300
 
1226
- hasSubModel<K extends keyof Structure['subModels']>(subKey: K) {
1301
+ hasSubModel<K extends keyof NonNullable<Structure['subModels']>>(subKey: K) {
1227
1302
  const subModel = (this.subModels as any)[subKey as string];
1228
1303
  if (!subModel) {
1229
1304
  return false;
@@ -1231,10 +1306,10 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
1231
1306
  return _.castArray(subModel).length > 0;
1232
1307
  }
1233
1308
 
1234
- findSubModel<K extends keyof Structure['subModels'], R>(
1309
+ findSubModel<K extends keyof NonNullable<Structure['subModels']>, R>(
1235
1310
  subKey: K,
1236
- callback: (model: ArrayElementType<Structure['subModels'][K]>) => R,
1237
- ): ArrayElementType<Structure['subModels'][K]> | null {
1311
+ callback: (model: ArrayElementType<NonNullable<Structure['subModels']>[K]>) => R,
1312
+ ): ArrayElementType<NonNullable<Structure['subModels']>[K]> | null {
1238
1313
  const model = (this.subModels as any)[subKey as string];
1239
1314
 
1240
1315
  if (!model) {
@@ -1244,7 +1319,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
1244
1319
  return (
1245
1320
  (_.castArray(model).find((item) => {
1246
1321
  return (callback as (model: any) => R)(item);
1247
- }) as ArrayElementType<Structure['subModels'][K]> | undefined) || null
1322
+ }) as ArrayElementType<NonNullable<Structure['subModels']>[K]> | undefined) || null
1248
1323
  );
1249
1324
  }
1250
1325
 
@@ -1304,7 +1379,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
1304
1379
  }
1305
1380
 
1306
1381
  clearForks() {
1307
- console.log(`FlowModel ${this.uid} clearing all forks.`);
1382
+ this.flowEngine.logger.debug(`FlowModel ${this.uid} clearing all forks.`);
1308
1383
  // 主动使所有 fork 失效
1309
1384
  if (this.forks?.size) {
1310
1385
  this.forks.forEach((fork) => fork.dispose());
@@ -1369,7 +1444,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
1369
1444
  * @param {string} stepKey 步骤的唯一标识符
1370
1445
  * @returns {void}
1371
1446
  */
1372
- openStepSettingsDialog(flowKey: string, stepKey: string) {
1447
+ async openStepSettingsDialog(flowKey: string, stepKey: string) {
1373
1448
  // 创建流程运行时上下文
1374
1449
  const flow = this.getFlow(flowKey);
1375
1450
  const step = flow?.steps?.[stepKey];
@@ -1383,7 +1458,9 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
1383
1458
  setupRuntimeContextSteps(ctx, flow.steps, this, flowKey);
1384
1459
  ctx.defineProperty('currentStep', { value: step });
1385
1460
 
1386
- return openStepSettingsDialogFn({
1461
+ const openStepSettingsDialog = await loadOpenStepSettingsDialog();
1462
+
1463
+ return openStepSettingsDialog({
1387
1464
  model: this,
1388
1465
  flowKey,
1389
1466
  stepKey,
@@ -1399,7 +1476,9 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
1399
1476
  * @returns {Promise<any>} 返回表单提交的值
1400
1477
  */
1401
1478
  async configureRequiredSteps(dialogWidth?: number | string, dialogTitle?: string) {
1402
- return openRequiredParamsStepFormDialogFn({
1479
+ const openRequiredParamsStepFormDialog = await loadOpenRequiredParamsStepFormDialog();
1480
+
1481
+ return openRequiredParamsStepFormDialog({
1403
1482
  model: this,
1404
1483
  dialogWidth,
1405
1484
  dialogTitle,
@@ -1432,6 +1511,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
1432
1511
  const data = {
1433
1512
  uid: this.uid,
1434
1513
  ..._.omit(this._options, ['flowEngine']),
1514
+ props: { ...this.props },
1435
1515
  stepParams: this.stepParams,
1436
1516
  sortIndex: this.sortIndex,
1437
1517
  flowRegistry: {},
@@ -1586,6 +1666,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
1586
1666
  seen.add(Cls);
1587
1667
  const reg = classMenuExtensions.get(Cls);
1588
1668
  if (reg) {
1669
+ let entryIndex = 0;
1589
1670
  for (const entry of reg) {
1590
1671
  if (entry.matcher && !entry.matcher(model)) continue;
1591
1672
  const items =
@@ -1593,16 +1674,21 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
1593
1674
  const group = entry.group || 'common-actions';
1594
1675
  const sort = entry.sort ?? 0;
1595
1676
  const prefix = entry.keyPrefix || Cls.name || 'extra';
1596
- (items || []).forEach((it, idx: number) => {
1597
- if (!it) return;
1598
- const key = it.key ?? `${prefix}-${group}-${idx}-${Math.random().toString(36).slice(2, 6)}`;
1599
- collected.push({
1600
- ...it,
1601
- key,
1602
- group: it.group || group,
1603
- sort: typeof it.sort === 'number' ? it.sort : sort,
1604
- });
1677
+ sortExtraMenuItems(
1678
+ (items || [])
1679
+ .map((it, idx: number) =>
1680
+ normalizeExtraMenuItem(it, {
1681
+ group,
1682
+ sort,
1683
+ prefix,
1684
+ path: `${entryIndex}-${idx}`,
1685
+ }),
1686
+ )
1687
+ .filter(isFlowModelExtraMenuItem),
1688
+ ).forEach((it) => {
1689
+ collected.push(it);
1605
1690
  });
1691
+ entryIndex += 1;
1606
1692
  }
1607
1693
  }
1608
1694
  const ParentClass = Object.getPrototypeOf(Cls) as typeof FlowModel;
package/src/provider.tsx CHANGED
@@ -45,34 +45,50 @@ export const FlowEngineGlobalsContextProvider: React.FC<{ children: React.ReactN
45
45
  const engine = useFlowEngine();
46
46
  const config = useContext(ConfigProvider.ConfigContext);
47
47
  const { token } = theme.useToken();
48
+ const isDarkTheme = React.useMemo(() => {
49
+ const algorithm = config?.theme?.algorithm;
50
+ if (Array.isArray(algorithm)) {
51
+ return algorithm.includes(theme.darkAlgorithm);
52
+ }
53
+ return algorithm === theme.darkAlgorithm;
54
+ }, [config]);
48
55
 
49
- useEffect(() => {
50
- const context = {
51
- antdConfig: config,
52
- // themeToken 改为可观察的 getter,在下方单独 define
53
- modal,
54
- message,
55
- notification,
56
- };
57
- engine.context.defineProperty('viewer', {
58
- cache: false,
59
- get: (ctx) => new FlowViewer(ctx, { drawer, embed, popover, dialog }),
60
- });
61
- for (const item of Object.entries(context)) {
62
- const [key, value] = item;
63
- if (value) {
64
- engine.context.defineProperty(key, { value });
65
- }
56
+ // 这些全局能力需要在 children 首次渲染前就可读,不能等到 effect 后再挂到上下文。
57
+ engine.context.defineProperty('viewer', {
58
+ cache: false,
59
+ get: (ctx) => new FlowViewer(ctx, { drawer, embed, popover, dialog }),
60
+ });
61
+ for (const item of Object.entries({
62
+ antdConfig: config,
63
+ modal,
64
+ message,
65
+ notification,
66
+ })) {
67
+ const [key, value] = item;
68
+ if (value) {
69
+ engine.context.defineProperty(key, { value });
66
70
  }
67
- // 将 themeToken 定义为 observable, 使组件能够响应主题的变更
68
- // NOTE: 必须在 antdConfig 写入后再更新 themeToken;否则会读取到旧 antdConfig 的值。
69
- engine.context.defineProperty('themeToken', {
70
- get: () => token,
71
- observable: true,
72
- cache: true,
73
- });
71
+ }
72
+ // themeToken 定义为 observable, 使组件能够响应主题的变更。
73
+ engine.context.defineProperty('themeToken', {
74
+ get: () => token,
75
+ observable: true,
76
+ cache: true,
77
+ });
78
+ // 统一把暗色模式暴露到 Flow 上下文,避免 flow 侧继续依赖 global-theme。
79
+ engine.context.defineProperty('isDarkTheme', {
80
+ get: () => isDarkTheme,
81
+ observable: true,
82
+ cache: true,
83
+ info: {
84
+ description: 'Whether current theme algorithm is dark mode.',
85
+ detail: 'boolean',
86
+ },
87
+ });
88
+
89
+ useEffect(() => {
74
90
  engine.reactView.refresh();
75
- }, [engine, drawer, modal, message, notification, config, popover, token, dialog, embed]);
91
+ }, [engine, drawer, modal, message, notification, config, popover, token, dialog, embed, isDarkTheme]);
76
92
 
77
93
  return (
78
94
  <ConfigProvider {...config} locale={engine.context.locales?.antd} popupMatchSelectWidth={false}>