@nocobase/flow-engine 2.0.0-beta.8 → 2.0.0

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 (245) hide show
  1. package/lib/BlockScopedFlowEngine.js +0 -1
  2. package/lib/FlowDefinition.d.ts +2 -0
  3. package/lib/JSRunner.d.ts +6 -0
  4. package/lib/JSRunner.js +32 -2
  5. package/lib/ViewScopedFlowEngine.js +3 -0
  6. package/lib/acl/Acl.js +13 -3
  7. package/lib/components/FlowContextSelector.js +155 -10
  8. package/lib/components/settings/wrappers/component/SwitchWithTitle.js +2 -1
  9. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +76 -15
  10. package/lib/components/settings/wrappers/contextual/FlowsContextMenu.js +24 -4
  11. package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +5 -1
  12. package/lib/components/variables/VariableInput.js +9 -4
  13. package/lib/components/variables/VariableTag.js +46 -39
  14. package/lib/components/variables/utils.d.ts +7 -0
  15. package/lib/components/variables/utils.js +42 -2
  16. package/lib/data-source/index.d.ts +7 -27
  17. package/lib/data-source/index.js +81 -51
  18. package/lib/executor/FlowExecutor.d.ts +2 -1
  19. package/lib/executor/FlowExecutor.js +163 -22
  20. package/lib/flowContext.d.ts +230 -7
  21. package/lib/flowContext.js +2267 -148
  22. package/lib/flowEngine.d.ts +21 -0
  23. package/lib/flowEngine.js +56 -8
  24. package/lib/flowI18n.js +6 -4
  25. package/lib/flowSettings.js +17 -11
  26. package/lib/index.d.ts +7 -1
  27. package/lib/index.js +21 -0
  28. package/lib/locale/en-US.json +9 -2
  29. package/lib/locale/index.d.ts +14 -0
  30. package/lib/locale/zh-CN.json +8 -1
  31. package/lib/models/CollectionFieldModel.d.ts +1 -0
  32. package/lib/models/CollectionFieldModel.js +3 -2
  33. package/lib/models/flowModel.js +12 -1
  34. package/lib/provider.js +5 -5
  35. package/lib/resources/baseRecordResource.d.ts +5 -0
  36. package/lib/resources/baseRecordResource.js +24 -0
  37. package/lib/resources/multiRecordResource.d.ts +1 -0
  38. package/lib/resources/multiRecordResource.js +11 -4
  39. package/lib/resources/singleRecordResource.js +2 -0
  40. package/lib/resources/sqlResource.d.ts +4 -3
  41. package/lib/resources/sqlResource.js +8 -3
  42. package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +12 -2
  43. package/lib/runjs-context/contexts/JSBlockRunJSContext.js +2 -2
  44. package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.d.ts +16 -0
  45. package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.js +125 -0
  46. package/lib/runjs-context/contexts/JSItemRunJSContext.js +12 -2
  47. package/lib/runjs-context/contexts/base.js +706 -41
  48. package/lib/runjs-context/contributions.d.ts +33 -0
  49. package/lib/runjs-context/contributions.js +88 -0
  50. package/lib/runjs-context/helpers.js +12 -1
  51. package/lib/runjs-context/setup.js +6 -0
  52. package/lib/runjs-context/snippets/global/api-request.snippet.js +3 -3
  53. package/lib/runjs-context/snippets/global/import-esm.snippet.js +2 -3
  54. package/lib/runjs-context/snippets/global/query-selector.snippet.js +8 -3
  55. package/lib/runjs-context/snippets/global/require-amd.snippet.js +1 -1
  56. package/lib/runjs-context/snippets/index.d.ts +11 -1
  57. package/lib/runjs-context/snippets/index.js +61 -40
  58. package/lib/runjs-context/snippets/scene/block/add-event-listener.snippet.js +10 -7
  59. package/lib/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.js +3 -3
  60. package/lib/runjs-context/snippets/scene/block/chartjs-bar.snippet.js +2 -2
  61. package/lib/runjs-context/snippets/scene/block/echarts-init.snippet.js +2 -2
  62. package/lib/runjs-context/snippets/scene/block/render-iframe.snippet.js +2 -2
  63. package/lib/runjs-context/snippets/scene/block/render-react.snippet.js +1 -1
  64. package/lib/runjs-context/snippets/scene/block/render-statistics.snippet.js +1 -1
  65. package/lib/runjs-context/snippets/scene/block/render-timeline.snippet.js +1 -1
  66. package/lib/runjs-context/snippets/scene/block/resource-example.snippet.js +5 -5
  67. package/lib/runjs-context/snippets/scene/block/three-users-orbit.snippet.js +6 -6
  68. package/lib/runjs-context/snippets/scene/block/vue-component.snippet.js +3 -4
  69. package/lib/runjs-context/snippets/scene/detail/color-by-value.snippet.js +1 -1
  70. package/lib/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.js +20 -3
  71. package/lib/runjs-context/snippets/scene/detail/format-number.snippet.js +1 -1
  72. package/lib/runjs-context/snippets/scene/detail/innerHTML-value.snippet.js +1 -1
  73. package/lib/runjs-context/snippets/scene/detail/percentage-bar.snippet.js +3 -3
  74. package/lib/runjs-context/snippets/scene/detail/relative-time.snippet.js +3 -3
  75. package/lib/runjs-context/snippets/scene/detail/status-tag.snippet.js +2 -2
  76. package/lib/runjs-context/snippets/scene/form/cascade-select.snippet.js +1 -1
  77. package/lib/runjs-context/snippets/scene/form/render-basic.snippet.js +2 -2
  78. package/lib/runjs-context/snippets/scene/table/cell-open-dialog.snippet.js +6 -3
  79. package/lib/runjs-context/snippets/scene/table/concat-fields.snippet.js +3 -1
  80. package/lib/runjsLibs.d.ts +28 -0
  81. package/lib/runjsLibs.js +532 -0
  82. package/lib/scheduler/ModelOperationScheduler.d.ts +2 -0
  83. package/lib/scheduler/ModelOperationScheduler.js +25 -21
  84. package/lib/types.d.ts +27 -0
  85. package/lib/utils/associationObjectVariable.d.ts +2 -2
  86. package/lib/utils/createCollectionContextMeta.js +1 -0
  87. package/lib/utils/createEphemeralContext.js +2 -2
  88. package/lib/utils/dateVariable.d.ts +16 -0
  89. package/lib/utils/dateVariable.js +380 -0
  90. package/lib/utils/exceptions.d.ts +7 -0
  91. package/lib/utils/exceptions.js +10 -0
  92. package/lib/utils/index.d.ts +8 -3
  93. package/lib/utils/index.js +45 -0
  94. package/lib/utils/params-resolvers.js +16 -9
  95. package/lib/utils/resolveModuleUrl.d.ts +58 -0
  96. package/lib/utils/resolveModuleUrl.js +65 -0
  97. package/lib/utils/resolveRunJSObjectValues.d.ts +16 -0
  98. package/lib/utils/resolveRunJSObjectValues.js +61 -0
  99. package/lib/utils/runjsModuleLoader.d.ts +58 -0
  100. package/lib/utils/runjsModuleLoader.js +422 -0
  101. package/lib/utils/runjsTemplateCompat.d.ts +35 -0
  102. package/lib/utils/runjsTemplateCompat.js +743 -0
  103. package/lib/utils/runjsValue.d.ts +29 -0
  104. package/lib/utils/runjsValue.js +275 -0
  105. package/lib/utils/safeGlobals.d.ts +18 -8
  106. package/lib/utils/safeGlobals.js +164 -17
  107. package/lib/utils/schema-utils.d.ts +10 -0
  108. package/lib/utils/schema-utils.js +61 -0
  109. package/lib/views/createViewMeta.d.ts +0 -7
  110. package/lib/views/createViewMeta.js +19 -70
  111. package/lib/views/index.d.ts +1 -2
  112. package/lib/views/index.js +4 -3
  113. package/lib/views/useDialog.js +7 -2
  114. package/lib/views/useDrawer.js +7 -2
  115. package/lib/views/usePage.d.ts +4 -0
  116. package/lib/views/usePage.js +43 -6
  117. package/lib/views/usePopover.js +4 -1
  118. package/lib/views/viewEvents.d.ts +17 -0
  119. package/lib/views/viewEvents.js +90 -0
  120. package/package.json +4 -4
  121. package/src/BlockScopedFlowEngine.ts +2 -5
  122. package/src/JSRunner.ts +44 -2
  123. package/src/ViewScopedFlowEngine.ts +4 -0
  124. package/src/__tests__/JSRunner.test.ts +64 -0
  125. package/src/__tests__/createViewMeta.popup.test.ts +62 -1
  126. package/src/__tests__/flowContext.test.ts +693 -1
  127. package/src/__tests__/flowEngine.dataSourceDirty.test.ts +63 -0
  128. package/src/__tests__/flowModel.openView.navigation.test.ts +28 -0
  129. package/src/__tests__/flowRunJSContextDefine.test.ts +63 -0
  130. package/src/__tests__/flowRuntimeContext.test.ts +2 -1
  131. package/src/__tests__/flowSettings.open.test.tsx +123 -19
  132. package/src/__tests__/runjsContext.test.ts +10 -7
  133. package/src/__tests__/runjsContextImplementations.test.ts +34 -3
  134. package/src/__tests__/runjsContextRuntime.test.ts +3 -3
  135. package/src/__tests__/runjsContributions.test.ts +89 -0
  136. package/src/__tests__/runjsExternalLibs.test.ts +242 -0
  137. package/src/__tests__/runjsLibsLazyLoading.test.ts +44 -0
  138. package/src/__tests__/runjsLocales.test.ts +4 -1
  139. package/src/__tests__/runjsPreprocessDefault.test.ts +49 -0
  140. package/src/__tests__/runjsRuntimeFeatures.test.ts +166 -0
  141. package/src/__tests__/runjsSnippets.test.ts +40 -3
  142. package/src/acl/Acl.tsx +3 -3
  143. package/src/components/FlowContextSelector.tsx +208 -12
  144. package/src/components/settings/wrappers/component/SwitchWithTitle.tsx +2 -1
  145. package/src/components/settings/wrappers/component/__tests__/InlineControls.test.tsx +74 -0
  146. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +109 -16
  147. package/src/components/settings/wrappers/contextual/FlowsContextMenu.tsx +41 -7
  148. package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +13 -2
  149. package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +157 -5
  150. package/src/components/variables/VariableInput.tsx +12 -4
  151. package/src/components/variables/VariableTag.tsx +54 -45
  152. package/src/components/variables/__tests__/FlowContextSelector.test.tsx +260 -3
  153. package/src/components/variables/__tests__/VariableTag.test.tsx +50 -0
  154. package/src/components/variables/__tests__/utils.test.ts +81 -3
  155. package/src/components/variables/utils.ts +67 -6
  156. package/src/data-source/index.ts +85 -110
  157. package/src/executor/FlowExecutor.ts +200 -23
  158. package/src/executor/__tests__/flowExecutor.test.ts +66 -0
  159. package/src/flowContext.ts +2986 -211
  160. package/src/flowEngine.ts +59 -8
  161. package/src/flowI18n.ts +7 -5
  162. package/src/flowSettings.ts +18 -12
  163. package/src/index.ts +14 -1
  164. package/src/locale/en-US.json +9 -2
  165. package/src/locale/zh-CN.json +8 -1
  166. package/src/models/CollectionFieldModel.tsx +3 -1
  167. package/src/models/__tests__/dispatchEvent.when.test.ts +554 -0
  168. package/src/models/__tests__/flowModel.test.ts +20 -4
  169. package/src/models/flowModel.tsx +13 -1
  170. package/src/provider.tsx +7 -6
  171. package/src/resources/__tests__/multiRecordResource.test.ts +44 -0
  172. package/src/resources/__tests__/sqlResource.test.ts +60 -0
  173. package/src/resources/baseRecordResource.ts +31 -0
  174. package/src/resources/multiRecordResource.ts +11 -4
  175. package/src/resources/singleRecordResource.ts +3 -0
  176. package/src/resources/sqlResource.ts +11 -6
  177. package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +10 -0
  178. package/src/runjs-context/contexts/JSBlockRunJSContext.ts +6 -2
  179. package/src/runjs-context/contexts/JSEditableFieldRunJSContext.ts +106 -0
  180. package/src/runjs-context/contexts/JSItemRunJSContext.ts +10 -0
  181. package/src/runjs-context/contexts/base.ts +715 -44
  182. package/src/runjs-context/contributions.ts +88 -0
  183. package/src/runjs-context/helpers.ts +11 -1
  184. package/src/runjs-context/setup.ts +6 -0
  185. package/src/runjs-context/snippets/global/api-request.snippet.ts +3 -3
  186. package/src/runjs-context/snippets/global/import-esm.snippet.ts +2 -3
  187. package/src/runjs-context/snippets/global/query-selector.snippet.ts +8 -3
  188. package/src/runjs-context/snippets/global/require-amd.snippet.ts +1 -1
  189. package/src/runjs-context/snippets/index.ts +75 -41
  190. package/src/runjs-context/snippets/scene/block/add-event-listener.snippet.ts +11 -13
  191. package/src/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.ts +3 -3
  192. package/src/runjs-context/snippets/scene/block/chartjs-bar.snippet.ts +2 -2
  193. package/src/runjs-context/snippets/scene/block/echarts-init.snippet.ts +2 -2
  194. package/src/runjs-context/snippets/scene/block/render-iframe.snippet.ts +2 -2
  195. package/src/runjs-context/snippets/scene/block/render-react.snippet.ts +1 -1
  196. package/src/runjs-context/snippets/scene/block/render-statistics.snippet.ts +1 -1
  197. package/src/runjs-context/snippets/scene/block/render-timeline.snippet.ts +1 -1
  198. package/src/runjs-context/snippets/scene/block/resource-example.snippet.ts +6 -11
  199. package/src/runjs-context/snippets/scene/block/three-users-orbit.snippet.ts +6 -6
  200. package/src/runjs-context/snippets/scene/block/vue-component.snippet.ts +3 -4
  201. package/src/runjs-context/snippets/scene/detail/color-by-value.snippet.ts +1 -1
  202. package/src/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.ts +20 -3
  203. package/src/runjs-context/snippets/scene/detail/format-number.snippet.ts +1 -1
  204. package/src/runjs-context/snippets/scene/detail/innerHTML-value.snippet.ts +1 -1
  205. package/src/runjs-context/snippets/scene/detail/percentage-bar.snippet.ts +3 -3
  206. package/src/runjs-context/snippets/scene/detail/relative-time.snippet.ts +3 -3
  207. package/src/runjs-context/snippets/scene/detail/status-tag.snippet.ts +2 -2
  208. package/src/runjs-context/snippets/scene/form/cascade-select.snippet.ts +1 -1
  209. package/src/runjs-context/snippets/scene/form/render-basic.snippet.ts +3 -8
  210. package/src/runjs-context/snippets/scene/table/cell-open-dialog.snippet.ts +6 -3
  211. package/src/runjs-context/snippets/scene/table/concat-fields.snippet.ts +3 -1
  212. package/src/runjsLibs.ts +622 -0
  213. package/src/scheduler/ModelOperationScheduler.ts +27 -21
  214. package/src/types.ts +38 -1
  215. package/src/utils/__tests__/dateVariable.test.ts +101 -0
  216. package/src/utils/__tests__/params-resolvers.test.ts +40 -0
  217. package/src/utils/__tests__/runjsRequireAsyncAutoWhitelist.test.ts +38 -0
  218. package/src/utils/__tests__/runjsTemplateCompat.test.ts +159 -0
  219. package/src/utils/__tests__/runjsValue.test.ts +44 -0
  220. package/src/utils/__tests__/safeGlobals.test.ts +57 -2
  221. package/src/utils/__tests__/utils.test.ts +95 -0
  222. package/src/utils/associationObjectVariable.ts +2 -2
  223. package/src/utils/createCollectionContextMeta.ts +1 -0
  224. package/src/utils/createEphemeralContext.ts +5 -4
  225. package/src/utils/dateVariable.ts +397 -0
  226. package/src/utils/exceptions.ts +11 -0
  227. package/src/utils/index.ts +37 -3
  228. package/src/utils/params-resolvers.ts +23 -9
  229. package/src/utils/resolveModuleUrl.ts +91 -0
  230. package/src/utils/resolveRunJSObjectValues.ts +46 -0
  231. package/src/utils/runjsModuleLoader.ts +553 -0
  232. package/src/utils/runjsTemplateCompat.ts +828 -0
  233. package/src/utils/runjsValue.ts +287 -0
  234. package/src/utils/safeGlobals.ts +188 -17
  235. package/src/utils/schema-utils.ts +79 -0
  236. package/src/views/__tests__/FlowView.usePage.test.tsx +54 -1
  237. package/src/views/__tests__/useDialog.closeDestroy.test.tsx +35 -8
  238. package/src/views/__tests__/viewEvents.resolveOpenerEngine.test.ts +28 -0
  239. package/src/views/createViewMeta.ts +22 -75
  240. package/src/views/index.tsx +1 -2
  241. package/src/views/useDialog.tsx +8 -1
  242. package/src/views/useDrawer.tsx +8 -1
  243. package/src/views/usePage.tsx +51 -5
  244. package/src/views/usePopover.tsx +4 -1
  245. package/src/views/viewEvents.ts +55 -0
@@ -0,0 +1,554 @@
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 { describe, test, expect } from 'vitest';
11
+ import { FlowEngine } from '../../flowEngine';
12
+ import { FlowModel } from '../flowModel';
13
+
14
+ describe('dispatchEvent dynamic event flow phase (scheduleModelOperation integration)', () => {
15
+ test('default (phase undefined): instance flows run before static flows', async () => {
16
+ const engine = new FlowEngine();
17
+ class M extends FlowModel {}
18
+ engine.registerModels({ M });
19
+
20
+ const calls: string[] = [];
21
+
22
+ M.registerFlow({
23
+ key: 'S',
24
+ on: { eventName: 'go' },
25
+ steps: {
26
+ a: { handler: async () => void calls.push('static-a') } as any,
27
+ },
28
+ });
29
+
30
+ const model = engine.createModel({ use: 'M' });
31
+ model.registerFlow('D', {
32
+ on: { eventName: 'go' },
33
+ steps: {
34
+ d: { handler: async () => void calls.push('dynamic') } as any,
35
+ },
36
+ });
37
+
38
+ await model.dispatchEvent('go');
39
+ expect(calls).toEqual(['dynamic', 'static-a']);
40
+ });
41
+
42
+ test('default (phase undefined): ctx.exitAll() stops static flows (beforeAllFlows regression)', async () => {
43
+ const engine = new FlowEngine();
44
+ class M extends FlowModel {}
45
+ engine.registerModels({ M });
46
+
47
+ const calls: string[] = [];
48
+
49
+ M.registerFlow({
50
+ key: 'S',
51
+ on: { eventName: 'go' },
52
+ steps: {
53
+ a: { handler: async () => void calls.push('static-a') } as any,
54
+ },
55
+ });
56
+ M.registerFlow({
57
+ key: 'T',
58
+ on: { eventName: 'go' },
59
+ steps: {
60
+ t: { handler: async () => void calls.push('static-t') } as any,
61
+ },
62
+ });
63
+
64
+ const model = engine.createModel({ use: 'M' });
65
+ model.registerFlow('D', {
66
+ on: { eventName: 'go' },
67
+ steps: {
68
+ d: {
69
+ handler: async (ctx: any) => {
70
+ calls.push('dynamic');
71
+ ctx.exitAll();
72
+ },
73
+ } as any,
74
+ },
75
+ });
76
+
77
+ await model.dispatchEvent('go');
78
+ expect(calls).toEqual(['dynamic']);
79
+ });
80
+
81
+ test("phase='afterAllFlows': instance flow runs after static flows", async () => {
82
+ const engine = new FlowEngine();
83
+ class M extends FlowModel {}
84
+ engine.registerModels({ M });
85
+
86
+ const calls: string[] = [];
87
+
88
+ M.registerFlow({
89
+ key: 'S',
90
+ on: { eventName: 'go' },
91
+ steps: {
92
+ a: { handler: async () => void calls.push('static-a') } as any,
93
+ },
94
+ });
95
+
96
+ const model = engine.createModel({ use: 'M' });
97
+ model.registerFlow('D', {
98
+ on: { eventName: 'go', phase: 'afterAllFlows' },
99
+ steps: {
100
+ d: { handler: async () => void calls.push('dynamic') } as any,
101
+ },
102
+ });
103
+
104
+ await model.dispatchEvent('go');
105
+ expect(calls).toEqual(['static-a', 'dynamic']);
106
+ });
107
+
108
+ test("phase='beforeFlow': instance flow runs before the target static flow", 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
+ b: { handler: async () => void calls.push('static-b') } as any,
121
+ },
122
+ });
123
+
124
+ const model = engine.createModel({ use: 'M' });
125
+ model.registerFlow('D', {
126
+ on: { eventName: 'go', phase: 'beforeFlow', flowKey: 'S' },
127
+ steps: {
128
+ d: { handler: async () => void calls.push('dynamic') } as any,
129
+ },
130
+ });
131
+
132
+ await model.dispatchEvent('go');
133
+ expect(calls).toEqual(['dynamic', 'static-a', 'static-b']);
134
+ });
135
+
136
+ test("phase='afterFlow': instance flow runs after the target static flow", async () => {
137
+ const engine = new FlowEngine();
138
+ class M extends FlowModel {}
139
+ engine.registerModels({ M });
140
+
141
+ const calls: string[] = [];
142
+
143
+ M.registerFlow({
144
+ key: 'S',
145
+ on: { eventName: 'go' },
146
+ steps: {
147
+ a: { handler: async () => void calls.push('static-a') } as any,
148
+ b: { handler: async () => void calls.push('static-b') } as any,
149
+ },
150
+ });
151
+
152
+ const model = engine.createModel({ use: 'M' });
153
+ model.registerFlow('D', {
154
+ on: { eventName: 'go', phase: 'afterFlow', flowKey: 'S' },
155
+ steps: {
156
+ d: { handler: async () => void calls.push('dynamic') } as any,
157
+ },
158
+ });
159
+
160
+ await model.dispatchEvent('go');
161
+ expect(calls).toEqual(['static-a', 'static-b', 'dynamic']);
162
+ });
163
+
164
+ test("phase='beforeStep': instance flow runs before the target static step", async () => {
165
+ const engine = new FlowEngine();
166
+ class M extends FlowModel {}
167
+ engine.registerModels({ M });
168
+
169
+ const calls: string[] = [];
170
+
171
+ M.registerFlow({
172
+ key: 'S',
173
+ on: { eventName: 'go' },
174
+ steps: {
175
+ a: { handler: async () => void calls.push('static-a') } as any,
176
+ b: { handler: async () => void calls.push('static-b') } as any,
177
+ },
178
+ });
179
+
180
+ const model = engine.createModel({ use: 'M' });
181
+ model.registerFlow('D', {
182
+ on: { eventName: 'go', phase: 'beforeStep', flowKey: 'S', stepKey: 'a' },
183
+ steps: {
184
+ d: { handler: async () => void calls.push('dynamic') } as any,
185
+ },
186
+ });
187
+
188
+ await model.dispatchEvent('go');
189
+ expect(calls).toEqual(['dynamic', 'static-a', 'static-b']);
190
+ });
191
+
192
+ test("phase='afterStep': instance flow runs after the target static step", async () => {
193
+ const engine = new FlowEngine();
194
+ class M extends FlowModel {}
195
+ engine.registerModels({ M });
196
+
197
+ const calls: string[] = [];
198
+
199
+ M.registerFlow({
200
+ key: 'S',
201
+ on: { eventName: 'go' },
202
+ steps: {
203
+ a: { handler: async () => void calls.push('static-a') } as any,
204
+ b: { handler: async () => void calls.push('static-b') } as any,
205
+ },
206
+ });
207
+
208
+ const model = engine.createModel({ use: 'M' });
209
+ model.registerFlow('D', {
210
+ on: { eventName: 'go', phase: 'afterStep', flowKey: 'S', stepKey: 'a' },
211
+ steps: {
212
+ d: { handler: async () => void calls.push('dynamic') } as any,
213
+ },
214
+ });
215
+
216
+ await model.dispatchEvent('go');
217
+ expect(calls).toEqual(['static-a', 'dynamic', 'static-b']);
218
+ });
219
+
220
+ test("phase='beforeFlow': ctx.exitAll() stops anchor flow and subsequent flows", async () => {
221
+ const engine = new FlowEngine();
222
+ class M extends FlowModel {}
223
+ engine.registerModels({ M });
224
+
225
+ const calls: string[] = [];
226
+
227
+ M.registerFlow({
228
+ key: 'S',
229
+ on: { eventName: 'go' },
230
+ steps: {
231
+ a: { handler: async () => void calls.push('static-a') } as any,
232
+ },
233
+ });
234
+ M.registerFlow({
235
+ key: 'T',
236
+ on: { eventName: 'go' },
237
+ steps: {
238
+ t: { handler: async () => void calls.push('static-t') } as any,
239
+ },
240
+ });
241
+
242
+ const model = engine.createModel({ use: 'M' });
243
+ model.registerFlow('D', {
244
+ on: { eventName: 'go', phase: 'beforeFlow', flowKey: 'S' },
245
+ steps: {
246
+ d: {
247
+ handler: async (ctx: any) => {
248
+ calls.push('dynamic');
249
+ ctx.exitAll();
250
+ },
251
+ } as any,
252
+ },
253
+ });
254
+
255
+ await model.dispatchEvent('go');
256
+ expect(calls).toEqual(['dynamic']);
257
+ });
258
+
259
+ test("phase='beforeStep': ctx.exitAll() stops anchor step and subsequent flows", async () => {
260
+ const engine = new FlowEngine();
261
+ class M extends FlowModel {}
262
+ engine.registerModels({ M });
263
+
264
+ const calls: string[] = [];
265
+
266
+ M.registerFlow({
267
+ key: 'S',
268
+ on: { eventName: 'go' },
269
+ steps: {
270
+ a: { handler: async () => void calls.push('static-a') } as any,
271
+ b: { handler: async () => void calls.push('static-b') } as any,
272
+ },
273
+ });
274
+ M.registerFlow({
275
+ key: 'T',
276
+ on: { eventName: 'go' },
277
+ steps: {
278
+ t: { handler: async () => void calls.push('static-t') } as any,
279
+ },
280
+ });
281
+
282
+ const model = engine.createModel({ use: 'M' });
283
+ model.registerFlow('D', {
284
+ on: { eventName: 'go', phase: 'beforeStep', flowKey: 'S', stepKey: 'a' },
285
+ steps: {
286
+ d: {
287
+ handler: async (ctx: any) => {
288
+ calls.push('dynamic');
289
+ ctx.exitAll();
290
+ },
291
+ } as any,
292
+ },
293
+ });
294
+
295
+ await model.dispatchEvent('go');
296
+ expect(calls).toEqual(['dynamic']);
297
+ });
298
+
299
+ test("phase='afterStep': ctx.exitAll() stops subsequent steps and subsequent flows", async () => {
300
+ const engine = new FlowEngine();
301
+ class M extends FlowModel {}
302
+ engine.registerModels({ M });
303
+
304
+ const calls: string[] = [];
305
+
306
+ M.registerFlow({
307
+ key: 'S',
308
+ on: { eventName: 'go' },
309
+ steps: {
310
+ a: { handler: async () => void calls.push('static-a') } as any,
311
+ b: { handler: async () => void calls.push('static-b') } as any,
312
+ },
313
+ });
314
+ M.registerFlow({
315
+ key: 'T',
316
+ on: { eventName: 'go' },
317
+ steps: {
318
+ t: { handler: async () => void calls.push('static-t') } as any,
319
+ },
320
+ });
321
+
322
+ const model = engine.createModel({ use: 'M' });
323
+ model.registerFlow('D', {
324
+ on: { eventName: 'go', phase: 'afterStep', flowKey: 'S', stepKey: 'a' },
325
+ steps: {
326
+ d: {
327
+ handler: async (ctx: any) => {
328
+ calls.push('dynamic');
329
+ ctx.exitAll();
330
+ },
331
+ } as any,
332
+ },
333
+ });
334
+
335
+ await model.dispatchEvent('go');
336
+ expect(calls).toEqual(['static-a', 'dynamic']);
337
+ });
338
+
339
+ test("phase='afterFlow': ctx.exitAll() stops subsequent flows", async () => {
340
+ const engine = new FlowEngine();
341
+ class M extends FlowModel {}
342
+ engine.registerModels({ M });
343
+
344
+ const calls: string[] = [];
345
+
346
+ M.registerFlow({
347
+ key: 'S',
348
+ on: { eventName: 'go' },
349
+ steps: {
350
+ a: { handler: async () => void calls.push('static-a') } as any,
351
+ b: { handler: async () => void calls.push('static-b') } as any,
352
+ },
353
+ });
354
+ M.registerFlow({
355
+ key: 'T',
356
+ on: { eventName: 'go' },
357
+ steps: {
358
+ t: { handler: async () => void calls.push('static-t') } as any,
359
+ },
360
+ });
361
+
362
+ const model = engine.createModel({ use: 'M' });
363
+ model.registerFlow('D', {
364
+ on: { eventName: 'go', phase: 'afterFlow', flowKey: 'S' },
365
+ steps: {
366
+ d: {
367
+ handler: async (ctx: any) => {
368
+ calls.push('dynamic');
369
+ ctx.exitAll();
370
+ },
371
+ } as any,
372
+ },
373
+ });
374
+
375
+ await model.dispatchEvent('go');
376
+ expect(calls).toEqual(['static-a', 'static-b', 'dynamic']);
377
+ });
378
+
379
+ test("phase='beforeFlow' missing flow: falls back to afterAllFlows", async () => {
380
+ const engine = new FlowEngine();
381
+ class M extends FlowModel {}
382
+ engine.registerModels({ M });
383
+
384
+ const calls: string[] = [];
385
+
386
+ M.registerFlow({
387
+ key: 'S',
388
+ on: { eventName: 'go' },
389
+ steps: {
390
+ a: { handler: async () => void calls.push('static-a') } as any,
391
+ },
392
+ });
393
+
394
+ const model = engine.createModel({ use: 'M' });
395
+ model.registerFlow('D', {
396
+ on: { eventName: 'go', phase: 'beforeFlow', flowKey: 'missing' },
397
+ steps: {
398
+ d: { handler: async () => void calls.push('dynamic') } as any,
399
+ },
400
+ });
401
+
402
+ await model.dispatchEvent('go');
403
+ expect(calls).toEqual(['static-a', 'dynamic']);
404
+ });
405
+
406
+ test("phase='beforeStep' missing step: falls back to afterAllFlows", async () => {
407
+ const engine = new FlowEngine();
408
+ class M extends FlowModel {}
409
+ engine.registerModels({ M });
410
+
411
+ const calls: string[] = [];
412
+
413
+ M.registerFlow({
414
+ key: 'S',
415
+ on: { eventName: 'go' },
416
+ steps: {
417
+ a: { handler: async () => void calls.push('static-a') } as any,
418
+ },
419
+ });
420
+
421
+ const model = engine.createModel({ use: 'M' });
422
+ model.registerFlow('D', {
423
+ on: { eventName: 'go', phase: 'beforeStep', flowKey: 'S', stepKey: 'missing' },
424
+ steps: {
425
+ d: { handler: async () => void calls.push('dynamic') } as any,
426
+ },
427
+ });
428
+
429
+ await model.dispatchEvent('go');
430
+ expect(calls).toEqual(['static-a', 'dynamic']);
431
+ });
432
+
433
+ test('multiple flows on same anchor: executes by flow.sort asc (stable)', async () => {
434
+ const engine = new FlowEngine();
435
+ class M extends FlowModel {}
436
+ engine.registerModels({ M });
437
+
438
+ const calls: string[] = [];
439
+
440
+ M.registerFlow({
441
+ key: 'S',
442
+ on: { eventName: 'go' },
443
+ steps: {
444
+ a: { handler: async () => void calls.push('static-a') } as any,
445
+ },
446
+ });
447
+
448
+ const model = engine.createModel({ use: 'M' });
449
+ model.registerFlow('D5', {
450
+ sort: 5,
451
+ on: { eventName: 'go', phase: 'beforeStep', flowKey: 'S', stepKey: 'a' },
452
+ steps: {
453
+ d: { handler: async () => void calls.push('dynamic-5') } as any,
454
+ },
455
+ });
456
+ model.registerFlow('D0', {
457
+ sort: 0,
458
+ on: { eventName: 'go', phase: 'beforeStep', flowKey: 'S', stepKey: 'a' },
459
+ steps: {
460
+ d: { handler: async () => void calls.push('dynamic-0') } as any,
461
+ },
462
+ });
463
+
464
+ await model.dispatchEvent('go');
465
+ expect(calls).toEqual(['dynamic-0', 'dynamic-5', 'static-a']);
466
+ });
467
+ });
468
+
469
+ describe('dispatchEvent static flow phase (scheduleModelOperation integration)', () => {
470
+ test("phase='beforeFlow': static flow runs before the target static flow", async () => {
471
+ const engine = new FlowEngine();
472
+ class M extends FlowModel {}
473
+ engine.registerModels({ M });
474
+
475
+ const calls: string[] = [];
476
+
477
+ M.registerFlow({
478
+ key: 'S',
479
+ on: { eventName: 'go' },
480
+ steps: {
481
+ a: { handler: async () => void calls.push('static-a') } as any,
482
+ },
483
+ });
484
+
485
+ M.registerFlow({
486
+ key: 'P',
487
+ on: { eventName: 'go', phase: 'beforeFlow', flowKey: 'S' },
488
+ steps: {
489
+ p: { handler: async () => void calls.push('phase') } as any,
490
+ },
491
+ });
492
+
493
+ const model = engine.createModel({ use: 'M' });
494
+ await model.dispatchEvent('go');
495
+ expect(calls).toEqual(['phase', 'static-a']);
496
+ });
497
+
498
+ test("phase='afterStep': static flow runs after the target static step", async () => {
499
+ const engine = new FlowEngine();
500
+ class M extends FlowModel {}
501
+ engine.registerModels({ M });
502
+
503
+ const calls: string[] = [];
504
+
505
+ M.registerFlow({
506
+ key: 'S',
507
+ on: { eventName: 'go' },
508
+ steps: {
509
+ a: { handler: async () => void calls.push('static-a') } as any,
510
+ b: { handler: async () => void calls.push('static-b') } as any,
511
+ },
512
+ });
513
+
514
+ M.registerFlow({
515
+ key: 'P',
516
+ on: { eventName: 'go', phase: 'afterStep', flowKey: 'S', stepKey: 'a' },
517
+ steps: {
518
+ p: { handler: async () => void calls.push('phase') } as any,
519
+ },
520
+ });
521
+
522
+ const model = engine.createModel({ use: 'M' });
523
+ await model.dispatchEvent('go');
524
+ expect(calls).toEqual(['static-a', 'phase', 'static-b']);
525
+ });
526
+
527
+ test("phase='afterAllFlows': static flow runs after static flows", async () => {
528
+ const engine = new FlowEngine();
529
+ class M extends FlowModel {}
530
+ engine.registerModels({ M });
531
+
532
+ const calls: string[] = [];
533
+
534
+ M.registerFlow({
535
+ key: 'S',
536
+ on: { eventName: 'go' },
537
+ steps: {
538
+ a: { handler: async () => void calls.push('static-a') } as any,
539
+ },
540
+ });
541
+
542
+ M.registerFlow({
543
+ key: 'P',
544
+ on: { eventName: 'go', phase: 'afterAllFlows' },
545
+ steps: {
546
+ p: { handler: async () => void calls.push('phase') } as any,
547
+ },
548
+ });
549
+
550
+ const model = engine.createModel({ use: 'M' });
551
+ await model.dispatchEvent('go');
552
+ expect(calls).toEqual(['static-a', 'phase']);
553
+ });
554
+ });
@@ -353,7 +353,7 @@ describe('FlowModel', () => {
353
353
  }).toThrow('FlowModel must be initialized with a FlowEngine instance.');
354
354
  });
355
355
 
356
- test('should handle FlowExitException correctly', async () => {
356
+ test('should handle ctx.exit() as FlowExitAllException in applyFlow', async () => {
357
357
  const exitFlow: FlowDefinitionOptions = {
358
358
  key: 'exitFlow',
359
359
  steps: {
@@ -374,14 +374,14 @@ describe('FlowModel', () => {
374
374
 
375
375
  const result = await model.applyFlow('exitFlow');
376
376
 
377
- expect(result).toEqual({});
377
+ expect(result).toBeInstanceOf(FlowExitAllException);
378
378
  expect(exitFlow.steps.step2.handler).not.toHaveBeenCalled();
379
379
  expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('[FlowModel]'));
380
380
 
381
381
  consoleSpy.mockRestore();
382
382
  });
383
383
 
384
- test('should handle FlowExitException correctly', async () => {
384
+ test('should handle ctx.exit() as FlowExitAllException in beforeRender dispatch', async () => {
385
385
  const exitFlow: FlowDefinitionOptions = {
386
386
  key: 'exitFlow',
387
387
  steps: {
@@ -413,7 +413,7 @@ describe('FlowModel', () => {
413
413
  await model.dispatchEvent('beforeRender');
414
414
 
415
415
  expect(exitFlow.steps.step2.handler).not.toHaveBeenCalled();
416
- expect(exitFlow2.steps.step2.handler).toHaveBeenCalled();
416
+ expect(exitFlow2.steps.step2.handler).not.toHaveBeenCalled();
417
417
  expect(loggerSpy).toHaveBeenCalledWith(expect.stringContaining('[FlowEngine]'));
418
418
 
419
419
  loggerSpy.mockRestore();
@@ -1558,6 +1558,22 @@ describe('FlowModel', () => {
1558
1558
  expect(model.forks.size).toBe(1);
1559
1559
  });
1560
1560
 
1561
+ test('should recreate cached fork after dispose to avoid state leakage', () => {
1562
+ const fork1 = model.createFork({ foo: 'bar' }, 'cacheKey');
1563
+ fork1.hidden = true;
1564
+ fork1.setProps({ disabled: true });
1565
+
1566
+ fork1.dispose();
1567
+
1568
+ expect(model.getFork('cacheKey')).toBeUndefined();
1569
+
1570
+ const fork2 = model.createFork({}, 'cacheKey');
1571
+
1572
+ expect(fork2).not.toBe(fork1);
1573
+ expect(fork2.hidden).toBe(false);
1574
+ expect(fork2.localProps).toEqual({});
1575
+ });
1576
+
1561
1577
  test('should create different instances for different keys', () => {
1562
1578
  const fork1 = model.createFork({}, 'key1');
1563
1579
  const fork2 = model.createFork({}, 'key2');
@@ -424,7 +424,19 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
424
424
  const meta = Cls.meta as any;
425
425
  const metaCreate = meta?.createModelOptions;
426
426
  if (metaCreate && typeof metaCreate === 'object' && metaCreate.subModels) {
427
- mergedSubModels = _.merge({}, _.cloneDeep(metaCreate.subModels || {}), _.cloneDeep(subModels || {}));
427
+ const replaceArrays = (objValue: unknown, srcValue: unknown) => {
428
+ if (Array.isArray(objValue) && Array.isArray(srcValue)) {
429
+ // Arrays should be replaced, not merged by index.
430
+ return srcValue;
431
+ }
432
+ return undefined;
433
+ };
434
+ mergedSubModels = _.mergeWith(
435
+ {},
436
+ _.cloneDeep(metaCreate.subModels || {}),
437
+ _.cloneDeep(subModels || {}),
438
+ replaceArrays,
439
+ );
428
440
  }
429
441
  } catch (e) {
430
442
  // Fallback silently if meta defaults resolution fails
package/src/provider.tsx CHANGED
@@ -58,18 +58,19 @@ export const FlowEngineGlobalsContextProvider: React.FC<{ children: React.ReactN
58
58
  cache: false,
59
59
  get: (ctx) => new FlowViewer(ctx, { drawer, embed, popover, dialog }),
60
60
  });
61
- // 将 themeToken 定义为 observable, 使组件能够响应主题的变更
62
- engine.context.defineProperty('themeToken', {
63
- get: () => token,
64
- observable: true,
65
- cache: true,
66
- });
67
61
  for (const item of Object.entries(context)) {
68
62
  const [key, value] = item;
69
63
  if (value) {
70
64
  engine.context.defineProperty(key, { value });
71
65
  }
72
66
  }
67
+ // 将 themeToken 定义为 observable, 使组件能够响应主题的变更
68
+ // NOTE: 必须在 antdConfig 写入后再更新 themeToken;否则会读取到旧 antdConfig 的值。
69
+ engine.context.defineProperty('themeToken', {
70
+ get: () => token,
71
+ observable: true,
72
+ cache: true,
73
+ });
73
74
  engine.reactView.refresh();
74
75
  }, [engine, drawer, modal, message, notification, config, popover, token, dialog, embed]);
75
76
 
@@ -0,0 +1,44 @@
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 { describe, expect, it, vi } from 'vitest';
11
+ import { FlowEngine } from '../../flowEngine';
12
+ import { MultiRecordResource } from '../multiRecordResource';
13
+
14
+ function createMultiRecordResource() {
15
+ const engine = new FlowEngine();
16
+ return engine.createResource(MultiRecordResource);
17
+ }
18
+
19
+ describe('MultiRecordResource - refresh', () => {
20
+ it('should coalesce multiple refresh calls and settle all awaiters', async () => {
21
+ vi.useFakeTimers();
22
+ try {
23
+ const r = createMultiRecordResource();
24
+ const api = {
25
+ request: vi.fn().mockResolvedValue({
26
+ data: { data: [], meta: { count: 0, page: 1, pageSize: 20 } },
27
+ }),
28
+ };
29
+
30
+ r.setAPIClient(api as any);
31
+ r.setResourceName('posts');
32
+
33
+ const p1 = r.refresh();
34
+ const p2 = r.refresh();
35
+
36
+ await vi.runAllTimersAsync();
37
+ await expect(p1).resolves.toBeUndefined();
38
+ await expect(p2).resolves.toBeUndefined();
39
+ expect(api.request).toHaveBeenCalledTimes(1);
40
+ } finally {
41
+ vi.useRealTimers();
42
+ }
43
+ });
44
+ });