@nocobase/flow-engine 2.0.0-alpha.9 → 2.0.0-beta.2

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 (305) hide show
  1. package/lib/BlockScopedFlowEngine.d.ts +23 -0
  2. package/lib/BlockScopedFlowEngine.js +92 -0
  3. package/lib/FlowDefinition.d.ts +6 -4
  4. package/lib/JSRunner.js +3 -0
  5. package/lib/ViewScopedFlowEngine.js +15 -1
  6. package/lib/acl/Acl.d.ts +12 -12
  7. package/lib/acl/Acl.js +78 -30
  8. package/lib/components/DynamicFlowsEditor.js +2 -4
  9. package/lib/components/FieldModelRenderer.js +10 -8
  10. package/lib/components/FieldSkeleton.d.ts +10 -0
  11. package/lib/components/FieldSkeleton.js +64 -0
  12. package/lib/components/FlowContextSelector.js +19 -3
  13. package/lib/components/FlowModelRenderer.d.ts +2 -1
  14. package/lib/components/FlowModelRenderer.js +34 -12
  15. package/lib/components/FormItem.js +5 -1
  16. package/lib/components/MobilePopup.d.ts +20 -0
  17. package/lib/components/MobilePopup.js +102 -0
  18. package/lib/components/MobilePopup.style.d.ts +17 -0
  19. package/lib/components/MobilePopup.style.js +186 -0
  20. package/lib/components/common/withFlowDesignMode.d.ts +1 -1
  21. package/lib/components/common/withFlowDesignMode.js +5 -5
  22. package/lib/components/index.d.ts +1 -0
  23. package/lib/components/index.js +3 -1
  24. package/lib/components/settings/independents/dropdown/FlowsDropdownButton.js +71 -53
  25. package/lib/components/settings/wrappers/component/SelectWithTitle.d.ts +19 -0
  26. package/lib/components/settings/wrappers/component/SelectWithTitle.js +136 -0
  27. package/lib/components/settings/wrappers/component/SwitchWithTitle.d.ts +10 -0
  28. package/lib/components/settings/wrappers/component/SwitchWithTitle.js +110 -0
  29. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +221 -93
  30. package/lib/components/settings/wrappers/contextual/FlowsContextMenu.js +71 -54
  31. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +2 -2
  32. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +63 -23
  33. package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +11 -6
  34. package/lib/components/settings/wrappers/embedded/FlowSettings.js +42 -28
  35. package/lib/components/settings/wrappers/embedded/FlowsSettings.js +3 -3
  36. package/lib/components/settings/wrappers/embedded/FlowsSettingsContent.js +52 -32
  37. package/lib/components/subModel/AddSubModelButton.d.ts +7 -0
  38. package/lib/components/subModel/AddSubModelButton.js +78 -8
  39. package/lib/components/subModel/LazyDropdown.js +14 -15
  40. package/lib/components/subModel/utils.d.ts +1 -1
  41. package/lib/components/subModel/utils.js +21 -11
  42. package/lib/components/variables/VariableInput.js +5 -3
  43. package/lib/components/variables/types.d.ts +2 -0
  44. package/lib/components/variables/utils.js +4 -2
  45. package/lib/data-source/index.d.ts +43 -4
  46. package/lib/data-source/index.js +104 -11
  47. package/lib/data-source/jioToJoiSchema.js +1 -0
  48. package/lib/emitter.d.ts +6 -0
  49. package/lib/emitter.js +12 -0
  50. package/lib/executor/FlowExecutor.js +48 -7
  51. package/lib/flow-registry/GlobalFlowRegistry.d.ts +1 -0
  52. package/lib/flow-registry/GlobalFlowRegistry.js +3 -0
  53. package/lib/flow-registry/InstanceFlowRegistry.d.ts +1 -0
  54. package/lib/flow-registry/InstanceFlowRegistry.js +3 -0
  55. package/lib/flowContext.d.ts +6 -0
  56. package/lib/flowContext.js +111 -30
  57. package/lib/flowEngine.d.ts +49 -0
  58. package/lib/flowEngine.js +265 -10
  59. package/lib/flowSettings.d.ts +4 -3
  60. package/lib/flowSettings.js +33 -11
  61. package/lib/hooks/useApplyAutoFlows.d.ts +1 -0
  62. package/lib/hooks/useApplyAutoFlows.js +2 -2
  63. package/lib/index.d.ts +4 -2
  64. package/lib/index.js +11 -5
  65. package/lib/locale/de-DE.json +62 -0
  66. package/lib/locale/en-US.json +57 -45
  67. package/lib/locale/es-ES.json +62 -0
  68. package/lib/locale/fr-FR.json +62 -0
  69. package/lib/locale/hu-HU.json +62 -0
  70. package/lib/locale/id-ID.json +62 -0
  71. package/lib/locale/index.d.ts +114 -90
  72. package/lib/locale/it-IT.json +62 -0
  73. package/lib/locale/ja-JP.json +62 -0
  74. package/lib/locale/ko-KR.json +62 -0
  75. package/lib/locale/nl-NL.json +62 -0
  76. package/lib/locale/pt-BR.json +62 -0
  77. package/lib/locale/ru-RU.json +62 -0
  78. package/lib/locale/tr-TR.json +62 -0
  79. package/lib/locale/uk-UA.json +62 -0
  80. package/lib/locale/vi-VN.json +62 -0
  81. package/lib/locale/zh-CN.json +58 -46
  82. package/lib/locale/zh-TW.json +62 -0
  83. package/lib/models/CollectionFieldModel.d.ts +6 -2
  84. package/lib/models/CollectionFieldModel.js +60 -14
  85. package/lib/models/flowModel.d.ts +43 -4
  86. package/lib/models/flowModel.js +128 -26
  87. package/lib/models/forkFlowModel.d.ts +6 -2
  88. package/lib/models/forkFlowModel.js +9 -2
  89. package/lib/provider.d.ts +3 -1
  90. package/lib/provider.js +4 -3
  91. package/lib/reactive/index.d.ts +10 -0
  92. package/lib/reactive/index.js +41 -0
  93. package/lib/reactive/observer.d.ts +19 -0
  94. package/lib/reactive/observer.js +109 -0
  95. package/lib/resources/baseRecordResource.d.ts +1 -0
  96. package/lib/resources/baseRecordResource.js +14 -3
  97. package/lib/resources/multiRecordResource.d.ts +4 -2
  98. package/lib/resources/multiRecordResource.js +15 -6
  99. package/lib/resources/singleRecordResource.js +6 -3
  100. package/lib/resources/sqlResource.d.ts +1 -0
  101. package/lib/resources/sqlResource.js +22 -25
  102. package/lib/runjs-context/contexts/base.js +42 -6
  103. package/lib/runjs-context/snippets/global/clipboard-copy-text.snippet.d.ts +11 -0
  104. package/lib/runjs-context/snippets/global/clipboard-copy-text.snippet.js +61 -0
  105. package/lib/runjs-context/snippets/index.js +3 -0
  106. package/lib/runjs-context/snippets/scene/block/render-antd-icons.snippet.d.ts +11 -0
  107. package/lib/runjs-context/snippets/scene/block/render-antd-icons.snippet.js +65 -0
  108. package/lib/runjs-context/snippets/scene/block/render-button-handler.snippet.js +6 -4
  109. package/lib/runjs-context/snippets/scene/block/render-info-card.snippet.js +15 -16
  110. package/lib/runjs-context/snippets/scene/block/render-react-jsx.snippet.d.ts +11 -0
  111. package/lib/runjs-context/snippets/scene/block/render-react-jsx.snippet.js +58 -0
  112. package/lib/runjs-context/snippets/scene/block/render-react.snippet.js +7 -7
  113. package/lib/runjs-context/snippets/scene/block/render-statistics.snippet.js +24 -29
  114. package/lib/runjs-context/snippets/scene/block/render-timeline.snippet.js +20 -21
  115. package/lib/scheduler/ModelOperationScheduler.d.ts +51 -0
  116. package/lib/scheduler/ModelOperationScheduler.js +262 -0
  117. package/lib/types.d.ts +42 -7
  118. package/lib/types.js +4 -3
  119. package/lib/utils/associationObjectVariable.d.ts +32 -0
  120. package/lib/utils/associationObjectVariable.js +157 -0
  121. package/lib/utils/createCollectionContextMeta.d.ts +1 -1
  122. package/lib/utils/createCollectionContextMeta.js +8 -4
  123. package/lib/utils/createEphemeralContext.d.ts +13 -0
  124. package/lib/utils/createEphemeralContext.js +140 -0
  125. package/lib/utils/flows.d.ts +10 -0
  126. package/lib/utils/flows.js +48 -0
  127. package/lib/utils/index.d.ts +7 -3
  128. package/lib/utils/index.js +20 -0
  129. package/lib/utils/jsxTransform.d.ts +15 -0
  130. package/lib/utils/jsxTransform.js +68 -0
  131. package/lib/utils/params-resolvers.js +3 -3
  132. package/lib/utils/parsePathnameToViewParams.d.ts +1 -1
  133. package/lib/utils/parsePathnameToViewParams.js +41 -5
  134. package/lib/utils/pruneFilter.d.ts +21 -0
  135. package/lib/utils/pruneFilter.js +52 -0
  136. package/lib/utils/safeGlobals.d.ts +5 -3
  137. package/lib/utils/safeGlobals.js +42 -1
  138. package/lib/utils/schema-utils.d.ts +6 -0
  139. package/lib/utils/schema-utils.js +71 -6
  140. package/lib/utils/serverContextParams.d.ts +3 -0
  141. package/lib/utils/serverContextParams.js +2 -0
  142. package/lib/utils/translation.d.ts +4 -1
  143. package/lib/utils/translation.js +6 -2
  144. package/lib/utils/variablesParams.d.ts +21 -5
  145. package/lib/utils/variablesParams.js +103 -34
  146. package/lib/views/DialogComponent.js +1 -5
  147. package/lib/views/DrawerComponent.js +18 -9
  148. package/lib/views/PageComponent.js +3 -4
  149. package/lib/views/ViewNavigation.d.ts +11 -15
  150. package/lib/views/ViewNavigation.js +37 -19
  151. package/lib/views/createViewMeta.d.ts +3 -2
  152. package/lib/views/createViewMeta.js +164 -53
  153. package/lib/views/useDialog.d.ts +2 -1
  154. package/lib/views/useDialog.js +36 -30
  155. package/lib/views/useDrawer.d.ts +2 -1
  156. package/lib/views/useDrawer.js +33 -26
  157. package/lib/views/usePage.d.ts +2 -1
  158. package/lib/views/usePage.js +40 -29
  159. package/package.json +6 -3
  160. package/src/BlockScopedFlowEngine.ts +88 -0
  161. package/src/JSRunner.ts +3 -0
  162. package/src/ViewScopedFlowEngine.ts +16 -0
  163. package/src/__tests__/JSRunner.test.ts +62 -53
  164. package/src/__tests__/blockScopedFlowEngine.test.ts +154 -0
  165. package/src/__tests__/createViewMeta.popup.test.ts +142 -0
  166. package/src/__tests__/flow-engine.test.ts +3 -0
  167. package/src/__tests__/flowContext.test.ts +70 -0
  168. package/src/__tests__/flowEngine.destroyModel.test.ts +74 -0
  169. package/src/__tests__/flowEngine.moveModel.test.ts +43 -0
  170. package/src/__tests__/flowEngine.removeModel.test.ts +72 -0
  171. package/src/__tests__/flowEngine.saveModel.test.ts +4 -0
  172. package/src/__tests__/flowModel.openView.navigation.test.ts +3 -2
  173. package/src/__tests__/flowSettings.open.test.tsx +2 -0
  174. package/src/__tests__/flowSettings.test.ts +2 -0
  175. package/src/__tests__/globalFlowRegistry.test.ts +1 -1
  176. package/src/__tests__/modelOperationScheduler.test.ts +346 -0
  177. package/src/__tests__/objectVariable.test.ts +464 -0
  178. package/src/__tests__/runjsRuntimeFeatures.test.ts +12 -0
  179. package/src/__tests__/viewScopedFlowEngine.test.ts +98 -0
  180. package/src/acl/Acl.tsx +85 -31
  181. package/src/acl/__tests__/Acl.test.tsx +43 -1
  182. package/src/components/DynamicFlowsEditor.tsx +0 -10
  183. package/src/components/FieldModelRenderer.tsx +15 -8
  184. package/src/components/FieldSkeleton.tsx +27 -0
  185. package/src/components/FlowContextSelector.tsx +20 -2
  186. package/src/components/FlowModelRenderer.tsx +46 -12
  187. package/src/components/FormItem.tsx +8 -1
  188. package/src/components/MobilePopup.style.ts +220 -0
  189. package/src/components/MobilePopup.tsx +86 -0
  190. package/src/components/__tests__/FlowModelRenderer.test.tsx +89 -0
  191. package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +1 -1
  192. package/src/components/common/withFlowDesignMode.tsx +5 -5
  193. package/src/components/index.ts +1 -0
  194. package/src/components/settings/independents/dropdown/FlowsDropdownButton.tsx +34 -17
  195. package/src/components/settings/wrappers/component/SelectWithTitle.tsx +110 -0
  196. package/src/components/settings/wrappers/component/SwitchWithTitle.tsx +82 -0
  197. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +260 -121
  198. package/src/components/settings/wrappers/contextual/FlowsContextMenu.tsx +34 -18
  199. package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +56 -18
  200. package/src/components/settings/wrappers/contextual/StepSettings.tsx +1 -2
  201. package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +12 -6
  202. package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +565 -0
  203. package/src/components/settings/wrappers/embedded/FlowSettings.tsx +47 -35
  204. package/src/components/settings/wrappers/embedded/FlowsSettings.tsx +1 -1
  205. package/src/components/settings/wrappers/embedded/FlowsSettingsContent.tsx +64 -42
  206. package/src/components/subModel/AddSubModelButton.tsx +104 -9
  207. package/src/components/subModel/LazyDropdown.tsx +14 -14
  208. package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +168 -7
  209. package/src/components/subModel/__tests__/utils.test.ts +12 -12
  210. package/src/components/subModel/utils.ts +25 -6
  211. package/src/components/variables/VariableInput.tsx +5 -3
  212. package/src/components/variables/types.ts +2 -0
  213. package/src/components/variables/utils.ts +7 -3
  214. package/src/data-source/index.ts +169 -11
  215. package/src/data-source/jioToJoiSchema.ts +1 -0
  216. package/src/emitter.ts +14 -0
  217. package/src/executor/FlowExecutor.ts +56 -8
  218. package/src/executor/__tests__/ctx-defs-injection.test.ts +197 -0
  219. package/src/flow-registry/GlobalFlowRegistry.ts +1 -0
  220. package/src/flow-registry/InstanceFlowRegistry.ts +1 -0
  221. package/src/flow-registry/__tests__/globalFlowRegistry.test.ts +54 -0
  222. package/src/flowContext.ts +144 -29
  223. package/src/flowEngine.ts +328 -8
  224. package/src/flowSettings.ts +47 -19
  225. package/src/hooks/useApplyAutoFlows.ts +3 -3
  226. package/src/index.ts +4 -2
  227. package/src/locale/de-DE.json +62 -0
  228. package/src/locale/en-US.json +57 -45
  229. package/src/locale/es-ES.json +62 -0
  230. package/src/locale/fr-FR.json +62 -0
  231. package/src/locale/hu-HU.json +62 -0
  232. package/src/locale/id-ID.json +62 -0
  233. package/src/locale/it-IT.json +62 -0
  234. package/src/locale/ja-JP.json +62 -0
  235. package/src/locale/ko-KR.json +62 -0
  236. package/src/locale/nl-NL.json +62 -0
  237. package/src/locale/pt-BR.json +62 -0
  238. package/src/locale/ru-RU.json +62 -0
  239. package/src/locale/tr-TR.json +62 -0
  240. package/src/locale/uk-UA.json +62 -0
  241. package/src/locale/vi-VN.json +62 -0
  242. package/src/locale/zh-CN.json +58 -46
  243. package/src/locale/zh-TW.json +62 -0
  244. package/src/models/CollectionFieldModel.tsx +79 -17
  245. package/src/models/__tests__/dispatchEvent.behavior.test.ts +169 -0
  246. package/src/models/__tests__/flowEngine.resolveUse.test.ts +170 -0
  247. package/src/models/__tests__/flowModel.getFlows.sort.test.ts +29 -5
  248. package/src/models/__tests__/flowModel.scheduleModelOperation.test.tsx +129 -0
  249. package/src/models/__tests__/flowModel.test.ts +65 -27
  250. package/src/models/__tests__/forkFlowModel.test.ts +40 -7
  251. package/src/models/flowModel.tsx +192 -30
  252. package/src/models/forkFlowModel.ts +11 -3
  253. package/src/provider.tsx +5 -5
  254. package/src/reactive/__tests__/observer.test.tsx +211 -0
  255. package/src/reactive/index.ts +11 -0
  256. package/src/reactive/observer.tsx +101 -0
  257. package/src/resources/baseRecordResource.ts +15 -3
  258. package/src/resources/multiRecordResource.ts +17 -8
  259. package/src/resources/singleRecordResource.ts +6 -3
  260. package/src/resources/sqlResource.ts +22 -26
  261. package/src/runjs-context/contexts/base.ts +47 -6
  262. package/src/runjs-context/snippets/global/clipboard-copy-text.snippet.ts +42 -0
  263. package/src/runjs-context/snippets/index.ts +3 -0
  264. package/src/runjs-context/snippets/scene/block/render-antd-icons.snippet.ts +46 -0
  265. package/src/runjs-context/snippets/scene/block/render-button-handler.snippet.ts +6 -4
  266. package/src/runjs-context/snippets/scene/block/render-info-card.snippet.ts +15 -16
  267. package/src/runjs-context/snippets/scene/block/render-react-jsx.snippet.ts +39 -0
  268. package/src/runjs-context/snippets/scene/block/render-react.snippet.ts +7 -7
  269. package/src/runjs-context/snippets/scene/block/render-statistics.snippet.ts +24 -29
  270. package/src/runjs-context/snippets/scene/block/render-timeline.snippet.ts +20 -21
  271. package/src/scheduler/ModelOperationScheduler.ts +304 -0
  272. package/src/types.ts +50 -4
  273. package/src/utils/__tests__/createCollectionContextMeta.test.ts +51 -0
  274. package/src/utils/__tests__/flows.test.ts +65 -0
  275. package/src/utils/__tests__/jsxTransform.test.ts +38 -0
  276. package/src/utils/__tests__/parsePathnameToViewParams.test.ts +25 -0
  277. package/src/utils/__tests__/pruneFilter.test.ts +38 -0
  278. package/src/utils/__tests__/safeGlobals.test.ts +23 -1
  279. package/src/utils/__tests__/utils.test.ts +114 -15
  280. package/src/utils/__tests__/variablesParams.test.ts +120 -0
  281. package/src/utils/associationObjectVariable.ts +180 -0
  282. package/src/utils/createCollectionContextMeta.ts +8 -3
  283. package/src/utils/createEphemeralContext.ts +142 -0
  284. package/src/utils/flows.ts +23 -0
  285. package/src/utils/index.ts +11 -2
  286. package/src/utils/jsxTransform.ts +39 -0
  287. package/src/utils/params-resolvers.ts +2 -2
  288. package/src/utils/parsePathnameToViewParams.ts +50 -6
  289. package/src/utils/pruneFilter.ts +41 -0
  290. package/src/utils/safeGlobals.ts +51 -4
  291. package/src/utils/schema-utils.ts +81 -3
  292. package/src/utils/serverContextParams.ts +5 -0
  293. package/src/utils/translation.ts +7 -2
  294. package/src/utils/variablesParams.ts +125 -42
  295. package/src/views/DialogComponent.tsx +1 -4
  296. package/src/views/DrawerComponent.tsx +19 -7
  297. package/src/views/PageComponent.tsx +2 -4
  298. package/src/views/ViewNavigation.ts +49 -43
  299. package/src/views/__tests__/FlowView.usePage.test.tsx +133 -0
  300. package/src/views/__tests__/ViewNavigation.test.ts +54 -34
  301. package/src/views/__tests__/useDialog.closeDestroy.test.tsx +132 -0
  302. package/src/views/createViewMeta.ts +179 -42
  303. package/src/views/useDialog.tsx +36 -24
  304. package/src/views/useDrawer.tsx +37 -24
  305. package/src/views/usePage.tsx +46 -27
@@ -0,0 +1,565 @@
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 from 'react';
11
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
12
+ import { render, cleanup, waitFor, act } from '@testing-library/react';
13
+ import { App, ConfigProvider } from 'antd';
14
+
15
+ import { FlowEngine } from '../../../../../flowEngine';
16
+ import { FlowModel } from '../../../../../models/flowModel';
17
+ import { DefaultSettingsIcon } from '../DefaultSettingsIcon';
18
+
19
+ // ---- Mock antd to capture Dropdown menu props ----
20
+ const dropdownMenus: any[] = [];
21
+ vi.mock('antd', async (importOriginal) => {
22
+ const messageApi = {
23
+ success: vi.fn(),
24
+ error: vi.fn(),
25
+ info: vi.fn(),
26
+ };
27
+ const modalApi = {
28
+ confirm: (opts: any) => {
29
+ if (opts && typeof opts.onOk === 'function') return opts.onOk();
30
+ },
31
+ error: vi.fn(),
32
+ };
33
+ const appApi = { message: messageApi, modal: modalApi };
34
+
35
+ const Dropdown = (props: any) => {
36
+ (globalThis as any).__lastDropdownMenu = props.menu;
37
+ (globalThis as any).__lastDropdownOnOpenChange = props.onOpenChange;
38
+ dropdownMenus.push(props.menu);
39
+ return React.createElement('span', { 'data-testid': 'dropdown' }, props.children);
40
+ };
41
+
42
+ const App = Object.assign(({ children }: any) => React.createElement(React.Fragment, null, children), {
43
+ useApp: () => appApi,
44
+ });
45
+
46
+ const ConfigProvider = ({ children }: any) => React.createElement(React.Fragment, null, children);
47
+ const Modal = {
48
+ confirm: (opts: any) => {
49
+ if (opts && typeof opts.onOk === 'function') return opts.onOk();
50
+ },
51
+ error: vi.fn(),
52
+ };
53
+ const Typography = {
54
+ Paragraph: ({ children }: any) => React.createElement('p', null, children ?? 'Paragraph'),
55
+ Text: ({ children }: any) => React.createElement('span', null, children ?? 'Text'),
56
+ };
57
+ const Collapse = Object.assign((props: any) => React.createElement('div', null, props.children ?? 'Collapse'), {
58
+ Panel: (props: any) => React.createElement('div', null, props.children ?? 'Panel'),
59
+ });
60
+ const Space = ({ children }: any) => React.createElement('div', null, children);
61
+ const FormItem = (props: any) => React.createElement('div', null, props.children ?? 'FormItem');
62
+ const Form = Object.assign((props: any) => React.createElement('form', null, props.children ?? 'Form'), {
63
+ Item: FormItem,
64
+ useForm: () => [{ setFieldsValue: (_: any) => {} }],
65
+ });
66
+ const Input: any = (props: any) => React.createElement('input', props);
67
+ Input.TextArea = (props: any) => React.createElement('textarea', props);
68
+ const InputNumber = (props: any) => React.createElement('input', { ...props, type: 'number' });
69
+ const Select = (props: any) => React.createElement('select', props);
70
+ const Switch = (props: any) => React.createElement('input', { ...props, type: 'checkbox' });
71
+ const Alert = (props: any) => React.createElement('div', { role: 'alert' }, props.message ?? 'Alert');
72
+ const Button = (props: any) => React.createElement('button', props, props.children ?? 'Button');
73
+ const Result = (props: any) => React.createElement('div', null, props.children ?? 'Result');
74
+
75
+ // Keep other components from original mock/default
76
+ return {
77
+ Dropdown,
78
+ App,
79
+ ConfigProvider,
80
+ Modal,
81
+ Typography,
82
+ Collapse,
83
+ Space,
84
+ Form,
85
+ Input,
86
+ InputNumber,
87
+ Select,
88
+ Switch,
89
+ Alert,
90
+ Button,
91
+ Result,
92
+ theme: { useToken: () => ({}) },
93
+ };
94
+ });
95
+
96
+ describe('DefaultSettingsIcon - only static flows are shown', () => {
97
+ beforeEach(() => {
98
+ dropdownMenus.length = 0;
99
+ (globalThis as any).__lastDropdownMenu = undefined;
100
+ (globalThis as any).__lastDropdownOnOpenChange = undefined;
101
+ });
102
+
103
+ afterEach(() => {
104
+ cleanup();
105
+ vi.clearAllMocks();
106
+ });
107
+
108
+ it('excludes instance (dynamic) flows from the settings menu', async () => {
109
+ class TestFlowModel extends FlowModel {}
110
+
111
+ const engine = new FlowEngine();
112
+ const model = new TestFlowModel({ uid: 'model-static-only', flowEngine: engine });
113
+
114
+ // register one static flow with a visible step
115
+ TestFlowModel.registerFlow({
116
+ key: 'static1',
117
+ title: 'Static Flow',
118
+ steps: {
119
+ general: {
120
+ title: 'General',
121
+ uiSchema: {
122
+ field: { type: 'string', 'x-component': 'Input' },
123
+ },
124
+ },
125
+ },
126
+ });
127
+
128
+ // add a dynamic (instance) flow which should NOT appear in menu
129
+ model.flowRegistry.addFlow('dyn1', {
130
+ title: 'Dynamic Flow',
131
+ steps: {
132
+ general: {
133
+ title: 'General (Dyn)',
134
+ uiSchema: {
135
+ field: { type: 'string', 'x-component': 'Input' },
136
+ },
137
+ },
138
+ },
139
+ });
140
+
141
+ render(
142
+ React.createElement(
143
+ ConfigProvider as any,
144
+ null,
145
+ React.createElement(
146
+ App as any,
147
+ null,
148
+ React.createElement(DefaultSettingsIcon as any, {
149
+ model,
150
+ // 关闭常用操作,避免干扰断言
151
+ showDeleteButton: false,
152
+ showCopyUidButton: false,
153
+ }),
154
+ ),
155
+ ),
156
+ );
157
+
158
+ // 等待菜单渲染完成,并且只包含静态流的步骤
159
+ await waitFor(() => {
160
+ const menu = (globalThis as any).__lastDropdownMenu;
161
+ expect(menu).toBeTruthy();
162
+ const items = (menu?.items || []) as any[];
163
+ const keys = items.map((it) => String(it.key || ''));
164
+ expect(keys.some((k) => k.startsWith('static1:'))).toBe(true);
165
+ expect(keys.some((k) => k.startsWith('dyn1:'))).toBe(false);
166
+ });
167
+
168
+ const menu = (globalThis as any).__lastDropdownMenu;
169
+ const items = (menu?.items || []) as any[];
170
+
171
+ // 静态流的 step 存在(key: `${flowKey}:${stepKey}`),动态流 step 不存在
172
+ expect(items.some((it) => String(it.key || '').startsWith('static1:'))).toBe(true);
173
+ expect(items.some((it) => String(it.key || '').startsWith('dyn1:'))).toBe(false);
174
+ });
175
+
176
+ it('filters out steps with hideInSettings and keeps visible ones', async () => {
177
+ class TestFlowModel extends FlowModel {}
178
+ const engine = new FlowEngine();
179
+ const model = new TestFlowModel({ uid: 'm-hide', flowEngine: engine });
180
+
181
+ TestFlowModel.registerFlow({
182
+ key: 'flowA',
183
+ title: 'Flow A',
184
+ steps: {
185
+ hidden: { title: 'Hidden', hideInSettings: true, uiSchema: { a: { type: 'string', 'x-component': 'Input' } } },
186
+ visible: { title: 'Visible', uiSchema: { b: { type: 'string', 'x-component': 'Input' } } },
187
+ },
188
+ });
189
+
190
+ render(
191
+ React.createElement(
192
+ ConfigProvider as any,
193
+ null,
194
+ React.createElement(App as any, null, React.createElement(DefaultSettingsIcon as any, { model })),
195
+ ),
196
+ );
197
+
198
+ await waitFor(() => {
199
+ const menu = (globalThis as any).__lastDropdownMenu;
200
+ const items = (menu?.items || []) as any[];
201
+ expect(items.some((it) => String(it.key || '') === 'flowA:visible')).toBe(true);
202
+ expect(items.some((it) => String(it.key || '') === 'flowA:hidden')).toBe(false);
203
+ });
204
+ });
205
+
206
+ it('includes step when uiSchema provided by action (step.use)', async () => {
207
+ class TestFlowModel extends FlowModel {}
208
+ const engine = new FlowEngine();
209
+ const model = new TestFlowModel({ uid: 'm-action', flowEngine: engine });
210
+
211
+ // Step has no uiSchema but uses an action that provides uiSchema
212
+ TestFlowModel.registerFlow({
213
+ key: 'flowB',
214
+ title: 'Flow B',
215
+ steps: {
216
+ useAction: { title: 'Use Action', use: 'act' },
217
+ },
218
+ });
219
+
220
+ // Stub getAction to provide uiSchema
221
+ (model as any).getAction = vi.fn().mockReturnValue({
222
+ name: 'act',
223
+ title: 'Action Title',
224
+ uiSchema: { x: { type: 'string', 'x-component': 'Input' } },
225
+ });
226
+
227
+ render(
228
+ React.createElement(
229
+ ConfigProvider as any,
230
+ null,
231
+ React.createElement(App as any, null, React.createElement(DefaultSettingsIcon as any, { model })),
232
+ ),
233
+ );
234
+
235
+ await waitFor(() => {
236
+ const menu = (globalThis as any).__lastDropdownMenu;
237
+ const items = (menu?.items || []) as any[];
238
+ expect(items.some((it) => String(it.key || '') === 'flowB:useAction')).toBe(true);
239
+ });
240
+ });
241
+
242
+ it('clicking a step item opens flow settings with correct args', async () => {
243
+ class TestFlowModel extends FlowModel {}
244
+ const engine = new FlowEngine();
245
+ const model = new TestFlowModel({ uid: 'm-open', flowEngine: engine });
246
+ const openSpy = vi.spyOn(model, 'openFlowSettings').mockResolvedValue(undefined as any);
247
+
248
+ TestFlowModel.registerFlow({
249
+ key: 'flowC',
250
+ title: 'Flow C',
251
+ steps: {
252
+ general: { title: 'General', uiSchema: { f: { type: 'string', 'x-component': 'Input' } } },
253
+ },
254
+ });
255
+
256
+ render(
257
+ React.createElement(
258
+ ConfigProvider as any,
259
+ null,
260
+ React.createElement(App as any, null, React.createElement(DefaultSettingsIcon as any, { model })),
261
+ ),
262
+ );
263
+
264
+ await waitFor(() => {
265
+ expect((globalThis as any).__lastDropdownMenu).toBeTruthy();
266
+ });
267
+ const menu = (globalThis as any).__lastDropdownMenu;
268
+ menu.onClick?.({ key: 'flowC:general' });
269
+ expect(openSpy).toHaveBeenCalledWith({ flowKey: 'flowC', stepKey: 'general' });
270
+ });
271
+
272
+ it('copy UID action writes model uid to clipboard', async () => {
273
+ class TestFlowModel extends FlowModel {}
274
+ const engine = new FlowEngine();
275
+ const model = new TestFlowModel({ uid: 'm-copy', flowEngine: engine });
276
+
277
+ TestFlowModel.registerFlow({
278
+ key: 'flowD',
279
+ title: 'Flow D',
280
+ steps: { s: { title: 'S', uiSchema: { f: { type: 'string', 'x-component': 'Input' } } } },
281
+ });
282
+
283
+ // mock clipboard
284
+ Object.defineProperty(window.navigator, 'clipboard', {
285
+ value: { writeText: vi.fn().mockResolvedValue(undefined) },
286
+ configurable: true,
287
+ });
288
+
289
+ render(
290
+ React.createElement(
291
+ ConfigProvider as any,
292
+ null,
293
+ React.createElement(App as any, null, React.createElement(DefaultSettingsIcon as any, { model })),
294
+ ),
295
+ );
296
+
297
+ await waitFor(() => {
298
+ expect((globalThis as any).__lastDropdownMenu).toBeTruthy();
299
+ });
300
+ const menu = (globalThis as any).__lastDropdownMenu;
301
+ menu.onClick?.({ key: 'copy-uid' });
302
+ expect((navigator as any).clipboard.writeText).toHaveBeenCalledWith('m-copy');
303
+ });
304
+
305
+ it('delete action calls model.destroy()', async () => {
306
+ class TestFlowModel extends FlowModel {}
307
+ const engine = new FlowEngine();
308
+ const model = new TestFlowModel({ uid: 'm-del', flowEngine: engine });
309
+ const destroySpy = vi.spyOn(model, 'destroy').mockResolvedValue(undefined as any);
310
+
311
+ TestFlowModel.registerFlow({
312
+ key: 'flowE',
313
+ title: 'Flow E',
314
+ steps: { s: { title: 'S', uiSchema: { f: { type: 'string', 'x-component': 'Input' } } } },
315
+ });
316
+
317
+ render(
318
+ React.createElement(
319
+ ConfigProvider as any,
320
+ null,
321
+ React.createElement(App as any, null, React.createElement(DefaultSettingsIcon as any, { model })),
322
+ ),
323
+ );
324
+
325
+ await waitFor(() => {
326
+ expect((globalThis as any).__lastDropdownMenu).toBeTruthy();
327
+ });
328
+ const menu = (globalThis as any).__lastDropdownMenu;
329
+ menu.onClick?.({ key: 'delete' });
330
+ expect(destroySpy).toHaveBeenCalled();
331
+ });
332
+
333
+ it('shows sub-model steps with modelKey when flattenSubMenus=false and menuLevels=2', async () => {
334
+ class Parent extends FlowModel {}
335
+ class Child extends FlowModel {}
336
+ const engine = new FlowEngine();
337
+ const parent = new Parent({ uid: 'parent-1', flowEngine: engine });
338
+ const child = new Child({ uid: 'child-1', flowEngine: engine });
339
+
340
+ // child static flow
341
+ Child.registerFlow({
342
+ key: 'childFlow',
343
+ title: 'Child Flow',
344
+ steps: { cstep: { title: 'C', uiSchema: { x: { type: 'string', 'x-component': 'Input' } } } },
345
+ });
346
+
347
+ parent.addSubModel('items', child);
348
+
349
+ render(
350
+ React.createElement(
351
+ ConfigProvider as any,
352
+ null,
353
+ React.createElement(
354
+ App as any,
355
+ null,
356
+ React.createElement(DefaultSettingsIcon as any, {
357
+ model: parent,
358
+ menuLevels: 2,
359
+ flattenSubMenus: false,
360
+ }),
361
+ ),
362
+ ),
363
+ );
364
+
365
+ await waitFor(() => {
366
+ const menu = (globalThis as any).__lastDropdownMenu;
367
+ expect(menu).toBeTruthy();
368
+ const items = (menu?.items || []) as any[];
369
+ const subMenu = items.find((it) => Array.isArray(it?.children));
370
+ expect(subMenu).toBeTruthy();
371
+ expect(subMenu!.children.some((it: any) => String(it.key).startsWith('items[0]:childFlow:cstep'))).toBe(true);
372
+ });
373
+ });
374
+
375
+ it('refreshes menu when current model step params change', async () => {
376
+ class TestFlowModel extends FlowModel {}
377
+ const engine = new FlowEngine();
378
+ const model = new TestFlowModel({
379
+ uid: 'step-params-current',
380
+ flowEngine: engine,
381
+ stepParams: {
382
+ dynamicFlow: { dynamicStep: { hidden: true } },
383
+ },
384
+ });
385
+
386
+ TestFlowModel.registerFlow({
387
+ key: 'dynamicFlow',
388
+ title: 'Dynamic Flow',
389
+ steps: {
390
+ dynamicStep: {
391
+ title: 'Dynamic Step',
392
+ hideInSettings: (ctx) => !!ctx.getStepParams('dynamicStep')?.hidden,
393
+ uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
394
+ },
395
+ },
396
+ });
397
+
398
+ render(
399
+ React.createElement(
400
+ ConfigProvider as any,
401
+ null,
402
+ React.createElement(
403
+ App as any,
404
+ null,
405
+ React.createElement(DefaultSettingsIcon as any, {
406
+ model,
407
+ showCopyUidButton: true,
408
+ showDeleteButton: false,
409
+ }),
410
+ ),
411
+ ),
412
+ );
413
+
414
+ await waitFor(() => {
415
+ const menu = (globalThis as any).__lastDropdownMenu;
416
+ expect(menu).toBeTruthy();
417
+ const items = (menu?.items || []) as any[];
418
+ expect(items.some((it) => String(it.key || '') === 'dynamicFlow:dynamicStep')).toBe(false);
419
+ });
420
+
421
+ await act(async () => {
422
+ model.setStepParams('dynamicFlow', 'dynamicStep', { hidden: false });
423
+ });
424
+
425
+ await waitFor(() => {
426
+ const menu = (globalThis as any).__lastDropdownMenu;
427
+ const items = (menu?.items || []) as any[];
428
+ expect(items.some((it) => String(it.key || '') === 'dynamicFlow:dynamicStep')).toBe(true);
429
+ });
430
+ });
431
+
432
+ it('refreshes submenu when child model step params change', async () => {
433
+ class ParentModel extends FlowModel {}
434
+ class ChildModel extends FlowModel {}
435
+ const engine = new FlowEngine();
436
+ const child = new ChildModel({
437
+ uid: 'child-step-params',
438
+ flowEngine: engine,
439
+ stepParams: { childFlow: { childStep: { hidden: true } } },
440
+ });
441
+ const parent = new ParentModel({ uid: 'parent-step-params', flowEngine: engine });
442
+
443
+ ChildModel.registerFlow({
444
+ key: 'childFlow',
445
+ title: 'Child Flow',
446
+ steps: {
447
+ childStep: {
448
+ title: 'Child Step',
449
+ hideInSettings: (ctx) => !!ctx.getStepParams('childStep')?.hidden,
450
+ uiSchema: { x: { type: 'string', 'x-component': 'Input' } },
451
+ },
452
+ },
453
+ });
454
+
455
+ parent.addSubModel('items', child);
456
+
457
+ render(
458
+ React.createElement(
459
+ ConfigProvider as any,
460
+ null,
461
+ React.createElement(
462
+ App as any,
463
+ null,
464
+ React.createElement(DefaultSettingsIcon as any, {
465
+ model: parent,
466
+ menuLevels: 2,
467
+ flattenSubMenus: true,
468
+ showCopyUidButton: true,
469
+ showDeleteButton: false,
470
+ }),
471
+ ),
472
+ ),
473
+ );
474
+
475
+ await waitFor(() => {
476
+ const menu = (globalThis as any).__lastDropdownMenu;
477
+ expect(menu).toBeTruthy();
478
+ const items = (menu?.items || []) as any[];
479
+ expect(items.some((it) => String(it.key || '').startsWith('items[0]:childFlow:childStep'))).toBe(false);
480
+ });
481
+
482
+ await act(async () => {
483
+ child.setStepParams('childFlow', 'childStep', { hidden: false });
484
+ });
485
+
486
+ await waitFor(() => {
487
+ const menu = (globalThis as any).__lastDropdownMenu;
488
+ const items = (menu?.items || []) as any[];
489
+ expect(items.some((it) => String(it.key || '').startsWith('items[0]:childFlow:childStep'))).toBe(true);
490
+ });
491
+ });
492
+ });
493
+
494
+ describe('DefaultSettingsIcon - extra menu items', () => {
495
+ beforeEach(() => {
496
+ dropdownMenus.length = 0;
497
+ (globalThis as any).__lastDropdownMenu = undefined;
498
+ (globalThis as any).__lastDropdownOnOpenChange = undefined;
499
+ });
500
+
501
+ afterEach(() => {
502
+ cleanup();
503
+ vi.clearAllMocks();
504
+ });
505
+
506
+ it('renders and triggers extra menu items registered on FlowModel class', async () => {
507
+ const onClick = vi.fn();
508
+
509
+ class TestFlowModel extends FlowModel {}
510
+ const dispose = TestFlowModel.registerExtraMenuItems({
511
+ group: 'common-actions',
512
+ sort: 10,
513
+ items: [{ key: 'extra-action', label: 'Extra Action', onClick }],
514
+ });
515
+
516
+ const engine = new FlowEngine();
517
+ const model = new TestFlowModel({ uid: 'm-extra', flowEngine: engine });
518
+
519
+ TestFlowModel.registerFlow({
520
+ key: 'flow',
521
+ title: 'Flow',
522
+ steps: { s: { title: 'S', uiSchema: { f: { type: 'string', 'x-component': 'Input' } } } },
523
+ });
524
+
525
+ try {
526
+ render(
527
+ React.createElement(
528
+ ConfigProvider as any,
529
+ null,
530
+ React.createElement(
531
+ App as any,
532
+ null,
533
+ React.createElement(DefaultSettingsIcon as any, {
534
+ model,
535
+ showCopyUidButton: false,
536
+ showDeleteButton: false,
537
+ }),
538
+ ),
539
+ ),
540
+ );
541
+
542
+ await waitFor(() => {
543
+ expect((globalThis as any).__lastDropdownMenu).toBeTruthy();
544
+ expect((globalThis as any).__lastDropdownOnOpenChange).toBeTruthy();
545
+ });
546
+
547
+ // extra menu items are loaded when dropdown becomes visible
548
+ await act(async () => {
549
+ (globalThis as any).__lastDropdownOnOpenChange?.(true, { source: 'trigger' });
550
+ });
551
+
552
+ await waitFor(() => {
553
+ const menu = (globalThis as any).__lastDropdownMenu;
554
+ const items = (menu?.items || []) as any[];
555
+ expect(items.some((it) => String(it.key || '') === 'extra-action')).toBe(true);
556
+ });
557
+
558
+ const menu = (globalThis as any).__lastDropdownMenu;
559
+ menu.onClick?.({ key: 'extra-action' });
560
+ expect(onClick).toHaveBeenCalled();
561
+ } finally {
562
+ dispose?.();
563
+ }
564
+ });
565
+ });
@@ -8,13 +8,12 @@
8
8
  */
9
9
 
10
10
  import { Alert, Form, Input, InputNumber, Select, Switch } from 'antd';
11
- import React, { useCallback, useEffect } from 'react';
12
- // TODO: ISchema may need to be imported from a different package or refactored.
13
- import { observer } from '@formily/react';
11
+ import React, { useCallback, useEffect, useState } from 'react';
14
12
  import { FlowRuntimeContext } from '../../../../flowContext';
15
13
  import { useFlowModelById } from '../../../../hooks';
16
14
  import { FlowModel } from '../../../../models';
17
- import { resolveDefaultParams, setupRuntimeContextSteps } from '../../../../utils';
15
+ import { resolveDefaultParams, setupRuntimeContextSteps, shouldHideStepInSettings } from '../../../../utils';
16
+ import { observer } from '../../../../reactive';
18
17
 
19
18
  const { Item: FormItem } = Form;
20
19
 
@@ -89,46 +88,59 @@ const FlowSettingsContent: React.FC<FlowSettingsContentProps> = observer(({ mode
89
88
  const flows = model.getFlows();
90
89
  const flow = flows.get(flowKey);
91
90
 
92
- // 获取可配置的步骤
93
- const configurableSteps = Object.entries(flow?.steps || {})
94
- .map(([stepKey, actionStep]) => {
95
- // 如果步骤设置了 hideInSettings: true,则跳过此步骤
96
- if (actionStep.hideInSettings) {
97
- return null;
98
- }
91
+ const [configurableSteps, setConfigurableSteps] = useState<{ stepKey: string; step: any; uiSchema: any }[]>([]);
92
+
93
+ // 获取可配置的步骤(支持动态 hideInSettings)
94
+ useEffect(() => {
95
+ let mounted = true;
96
+ const buildConfigurableSteps = async () => {
97
+ const steps: { stepKey: string; step: any; uiSchema: any }[] = [];
98
+ for (const [stepKey, actionStep] of Object.entries(flow?.steps || {})) {
99
+ if (await shouldHideStepInSettings(model, flow, actionStep as any)) {
100
+ continue;
101
+ }
99
102
 
100
- // 从step获取uiSchema(如果存在)
101
- const stepUiSchema = actionStep.uiSchema || {};
103
+ // 从step获取uiSchema(如果存在)
104
+ const stepUiSchema = actionStep.uiSchema || {};
102
105
 
103
- // 如果step使用了action,也获取action的uiSchema
104
- let actionUiSchema = {};
105
- if (actionStep.use) {
106
- const action = model.flowEngine?.getAction?.(actionStep.use);
107
- if (action && action.uiSchema) {
108
- actionUiSchema = action.uiSchema;
106
+ // 如果step使用了action,也获取action的uiSchema
107
+ let actionUiSchema = {};
108
+ if (actionStep.use) {
109
+ const action = model.flowEngine?.getAction?.(actionStep.use);
110
+ if (action && action.uiSchema) {
111
+ actionUiSchema = action.uiSchema;
112
+ }
109
113
  }
110
- }
111
114
 
112
- // 合并uiSchema,确保step的uiSchema优先级更高
113
- const mergedUiSchema = { ...actionUiSchema };
115
+ // 合并uiSchema,确保step的uiSchema优先级更高
116
+ const mergedUiSchema = { ...actionUiSchema };
114
117
 
115
- // 将stepUiSchema中的字段合并到mergedUiSchema
116
- Object.entries(stepUiSchema).forEach(([fieldKey, schema]) => {
117
- if (mergedUiSchema[fieldKey]) {
118
- mergedUiSchema[fieldKey] = { ...mergedUiSchema[fieldKey], ...schema };
119
- } else {
120
- mergedUiSchema[fieldKey] = schema;
118
+ // 将stepUiSchema中的字段合并到mergedUiSchema
119
+ Object.entries(stepUiSchema).forEach(([fieldKey, schema]) => {
120
+ if (mergedUiSchema[fieldKey]) {
121
+ mergedUiSchema[fieldKey] = { ...mergedUiSchema[fieldKey], ...schema };
122
+ } else {
123
+ mergedUiSchema[fieldKey] = schema;
124
+ }
125
+ });
126
+
127
+ // 如果没有可配置的UI Schema,跳过
128
+ if (Object.keys(mergedUiSchema).length === 0) {
129
+ continue;
121
130
  }
122
- });
123
131
 
124
- // 如果没有可配置的UI Schema,返回null
125
- if (Object.keys(mergedUiSchema).length === 0) {
126
- return null;
132
+ steps.push({ stepKey, step: actionStep, uiSchema: mergedUiSchema });
127
133
  }
134
+ if (mounted) {
135
+ setConfigurableSteps(steps);
136
+ }
137
+ };
128
138
 
129
- return { stepKey, step: actionStep, uiSchema: mergedUiSchema };
130
- })
131
- .filter(Boolean);
139
+ buildConfigurableSteps();
140
+ return () => {
141
+ mounted = false;
142
+ };
143
+ }, [model, flow]);
132
144
 
133
145
  // 获取当前流程的参数 - 从model中获取实际参数
134
146
  const getCurrentParams = useCallback(async () => {
@@ -10,8 +10,8 @@
10
10
  import React from 'react';
11
11
  import { Alert } from 'antd';
12
12
  import { useFlowModelById } from '../../../../hooks';
13
- import { observer } from '@formily/react';
14
13
  import FlowsSettingsContent from './FlowsSettingsContent';
14
+ import { observer } from '../../../../reactive';
15
15
 
16
16
  // 简单的流程设置组件接口
17
17
  interface ModelProvidedProps {