@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,63 @@
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 '../resources/multiRecordResource';
13
+ import { SingleRecordResource } from '../resources/singleRecordResource';
14
+
15
+ describe('FlowEngine dataSource dirty registry', () => {
16
+ it('tracks versions per dataSourceKey + resourceName', () => {
17
+ const engine = new FlowEngine();
18
+
19
+ expect(engine.getDataSourceDirtyVersion('main', 'posts')).toBe(0);
20
+ expect(engine.markDataSourceDirty('main', 'posts')).toBe(1);
21
+ expect(engine.getDataSourceDirtyVersion('main', 'posts')).toBe(1);
22
+
23
+ expect(engine.markDataSourceDirty('main', 'posts')).toBe(2);
24
+ expect(engine.getDataSourceDirtyVersion('main', 'posts')).toBe(2);
25
+
26
+ // different resource
27
+ expect(engine.getDataSourceDirtyVersion('main', 'users')).toBe(0);
28
+ expect(engine.markDataSourceDirty('main', 'users')).toBe(1);
29
+ expect(engine.getDataSourceDirtyVersion('main', 'users')).toBe(1);
30
+
31
+ // different data source
32
+ expect(engine.getDataSourceDirtyVersion('ds2', 'posts')).toBe(0);
33
+ expect(engine.markDataSourceDirty('ds2', 'posts')).toBe(1);
34
+ expect(engine.getDataSourceDirtyVersion('ds2', 'posts')).toBe(1);
35
+ // main unchanged
36
+ expect(engine.getDataSourceDirtyVersion('main', 'posts')).toBe(2);
37
+ });
38
+
39
+ it('marks dirty on record write operations (single & multi)', async () => {
40
+ const engine = new FlowEngine();
41
+ const markSpy = vi.spyOn(engine, 'markDataSourceDirty');
42
+
43
+ const single = engine.createResource(SingleRecordResource);
44
+ single.setDataSourceKey('main');
45
+ single.setResourceName('posts');
46
+ // avoid network: stub runAction + refresh
47
+ (single as any).runAction = vi.fn().mockResolvedValue({ data: {}, meta: {} });
48
+ (single as any).refresh = vi.fn().mockResolvedValue(undefined);
49
+ await single.save({ title: 't' } as any, { refresh: false });
50
+ expect(markSpy).toHaveBeenCalledWith('main', 'posts');
51
+
52
+ const multi = engine.createResource(MultiRecordResource);
53
+ multi.setDataSourceKey('main');
54
+ multi.setResourceName('users.profile');
55
+ (multi as any).runAction = vi.fn().mockResolvedValue({ data: [], meta: {} });
56
+ (multi as any).refresh = vi.fn().mockResolvedValue(undefined);
57
+ await multi.create({ name: 'n' } as any, { refresh: false });
58
+ // exact association
59
+ expect(markSpy).toHaveBeenCalledWith('main', 'users.profile');
60
+ // plus root collection (safety)
61
+ expect(markSpy).toHaveBeenCalledWith('main', 'users');
62
+ });
63
+ });
@@ -76,4 +76,32 @@ describe('FlowModelContext.openView - navigation enforcement', () => {
76
76
  const dispatchedParams = child.dispatchEvent.mock.calls[0][1];
77
77
  expect(dispatchedParams.navigation).toBe(false);
78
78
  });
79
+
80
+ it('dispatches the popupSettings bound event (object form) when opening external popup', async () => {
81
+ const { parent, child } = setup();
82
+
83
+ child.getFlow = vi.fn((key: string) => {
84
+ if (key !== 'popupSettings') return undefined;
85
+ return { on: { eventName: 'openDuplicatePopup' } };
86
+ });
87
+
88
+ await (parent.context as any).openView('child-uid', { mode: 'drawer' });
89
+
90
+ expect(child.dispatchEvent).toHaveBeenCalledTimes(1);
91
+ expect(child.dispatchEvent.mock.calls[0][0]).toBe('openDuplicatePopup');
92
+ });
93
+
94
+ it('falls back to click when popupSettings has no explicit on event', async () => {
95
+ const { parent, child } = setup();
96
+
97
+ child.getFlow = vi.fn((key: string) => {
98
+ if (key !== 'popupSettings') return undefined;
99
+ return { on: undefined };
100
+ });
101
+
102
+ await (parent.context as any).openView('child-uid', { mode: 'drawer' });
103
+
104
+ expect(child.dispatchEvent).toHaveBeenCalledTimes(1);
105
+ expect(child.dispatchEvent.mock.calls[0][0]).toBe('click');
106
+ });
79
107
  });
@@ -61,6 +61,69 @@ describe('FlowRunJSContext.define() and getDoc() deep tests', () => {
61
61
  expect(apiProp.properties?.auth?.description).toBe('Auth info');
62
62
  });
63
63
 
64
+ it('should treat string doc as description when overriding object docs', () => {
65
+ class BaseDocContext extends FlowRunJSContext {}
66
+ class ChildDocContext extends BaseDocContext {}
67
+
68
+ BaseDocContext.define({
69
+ properties: {
70
+ foo: {
71
+ description: 'Base foo',
72
+ detail: 'FooDetail',
73
+ hidden: true,
74
+ completion: { insertText: 'ctx.foo' },
75
+ properties: {
76
+ bar: { description: 'Bar desc' },
77
+ },
78
+ },
79
+ },
80
+ });
81
+
82
+ ChildDocContext.define({
83
+ properties: {
84
+ foo: 'Child foo',
85
+ },
86
+ });
87
+
88
+ const doc = ChildDocContext.getDoc();
89
+ const foo: any = doc.properties?.foo;
90
+ expect(foo).toBeTruthy();
91
+ expect(typeof foo).toBe('object');
92
+ expect(foo.description).toBe('Child foo');
93
+ expect(foo.hidden).toBe(true);
94
+ expect(foo.detail).toBe('FooDetail');
95
+ expect(foo.completion?.insertText).toBe('ctx.foo');
96
+ expect(foo.properties?.bar?.description).toBe('Bar desc');
97
+ });
98
+
99
+ it('should preserve base string description when patch adds object doc', () => {
100
+ class BaseStringDocContext extends FlowRunJSContext {}
101
+ class ChildObjectDocContext extends BaseStringDocContext {}
102
+
103
+ BaseStringDocContext.define({
104
+ properties: {
105
+ foo: 'Base foo',
106
+ },
107
+ });
108
+
109
+ ChildObjectDocContext.define({
110
+ properties: {
111
+ foo: {
112
+ properties: {
113
+ bar: 'Bar',
114
+ },
115
+ },
116
+ },
117
+ });
118
+
119
+ const doc = ChildObjectDocContext.getDoc();
120
+ const foo: any = doc.properties?.foo;
121
+ expect(foo).toBeTruthy();
122
+ expect(typeof foo).toBe('object');
123
+ expect(foo.description).toBe('Base foo');
124
+ expect(foo.properties?.bar).toBe('Bar');
125
+ });
126
+
64
127
  it('should deep merge method documentation', () => {
65
128
  class MethodMergeContext extends FlowRunJSContext {}
66
129
 
@@ -11,6 +11,7 @@ import { ContextPathProxy } from '../ContextPathProxy';
11
11
  import { FlowRuntimeContext } from '../flowContext';
12
12
  import { FlowEngine } from '../flowEngine';
13
13
  import { FlowModel } from '../models/flowModel';
14
+ import { FlowExitAllException } from '../utils/exceptions';
14
15
 
15
16
  describe('FlowRuntimeContext', () => {
16
17
  let engine: FlowEngine;
@@ -46,7 +47,7 @@ describe('FlowRuntimeContext', () => {
46
47
 
47
48
  it('should throw on exit()', () => {
48
49
  const ctx = new FlowRuntimeContext(model, 'flow1', 'runtime');
49
- expect(() => ctx.exit()).toThrow();
50
+ expect(() => ctx.exit()).toThrow(FlowExitAllException);
50
51
  });
51
52
  });
52
53
 
@@ -12,6 +12,7 @@ import { screen } from '@testing-library/react';
12
12
  import { FlowSettings } from '../flowSettings';
13
13
  import { FlowModel } from '../models';
14
14
  import { FlowEngine } from '../flowEngine';
15
+ import { GLOBAL_EMBED_CONTAINER_ID } from '../views';
15
16
 
16
17
  // We will stub viewer directly on model.context in tests
17
18
 
@@ -1087,18 +1088,18 @@ describe('FlowSettings.open rendering behavior', () => {
1087
1088
 
1088
1089
  // Create mock DOM element for embed target
1089
1090
  const mockTarget = document.createElement('div');
1090
- mockTarget.id = 'nocobase-embed-container';
1091
+ mockTarget.id = GLOBAL_EMBED_CONTAINER_ID;
1091
1092
  mockTarget.style.width = 'auto';
1092
1093
  mockTarget.style.maxWidth = 'none';
1093
1094
  document.body.appendChild(mockTarget);
1094
1095
 
1095
1096
  // Mock querySelector to return our mock element
1096
- const originalQuerySelector = document.querySelector;
1097
- document.querySelector = vi.fn((selector) => {
1098
- if (selector === '#nocobase-embed-container') {
1097
+ const originalQuerySelector = document.querySelector.bind(document);
1098
+ const querySelectorSpy = vi.spyOn(document, 'querySelector').mockImplementation((selector: string) => {
1099
+ if (selector === `#${GLOBAL_EMBED_CONTAINER_ID}`) {
1099
1100
  return mockTarget;
1100
1101
  }
1101
- return originalQuerySelector.call(document, selector);
1102
+ return originalQuerySelector(selector);
1102
1103
  });
1103
1104
 
1104
1105
  const M = model.constructor as any;
@@ -1164,7 +1165,61 @@ describe('FlowSettings.open rendering behavior', () => {
1164
1165
 
1165
1166
  // Cleanup
1166
1167
  document.body.removeChild(mockTarget);
1167
- document.querySelector = originalQuerySelector;
1168
+ querySelectorSpy.mockRestore();
1169
+ });
1170
+
1171
+ it('does not clear embed target DOM before opening (avoids portal unmount errors)', async () => {
1172
+ const engine = new FlowEngine();
1173
+ const flowSettings = new FlowSettings(engine);
1174
+ const model = new FlowModel({ uid: 'm-embed-no-clear', flowEngine: engine });
1175
+
1176
+ const mockTarget = document.createElement('div');
1177
+ mockTarget.id = GLOBAL_EMBED_CONTAINER_ID;
1178
+ mockTarget.innerHTML = '<div data-testid="existing">Existing</div>';
1179
+ document.body.appendChild(mockTarget);
1180
+
1181
+ const originalQuerySelector = document.querySelector.bind(document);
1182
+ const querySelectorSpy = vi.spyOn(document, 'querySelector').mockImplementation((selector: string) => {
1183
+ if (selector === `#${GLOBAL_EMBED_CONTAINER_ID}`) {
1184
+ return mockTarget;
1185
+ }
1186
+ return originalQuerySelector(selector);
1187
+ });
1188
+
1189
+ const M = model.constructor as any;
1190
+ M.registerFlow({
1191
+ key: 'embedNoClearFlow',
1192
+ steps: {
1193
+ step: {
1194
+ title: 'Step',
1195
+ uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
1196
+ },
1197
+ },
1198
+ });
1199
+
1200
+ const embed = vi.fn((opts: any) => {
1201
+ // The existing DOM should not be wiped out before opening the embed view.
1202
+ expect(mockTarget.querySelector('[data-testid="existing"]')).toBeTruthy();
1203
+ const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
1204
+ if (typeof opts.content === 'function') opts.content(dlg, { defineMethod: vi.fn() });
1205
+ return dlg;
1206
+ });
1207
+
1208
+ model.context.defineProperty('viewer', { value: { embed } });
1209
+ model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
1210
+
1211
+ await flowSettings.open({
1212
+ model,
1213
+ flowKey: 'embedNoClearFlow',
1214
+ stepKey: 'step',
1215
+ uiMode: 'embed',
1216
+ } as any);
1217
+
1218
+ expect(embed).toHaveBeenCalledTimes(1);
1219
+ expect(mockTarget.querySelector('[data-testid="existing"]')).toBeTruthy();
1220
+
1221
+ document.body.removeChild(mockTarget);
1222
+ querySelectorSpy.mockRestore();
1168
1223
  });
1169
1224
 
1170
1225
  it('uses embed uiMode with default props when target element exists', async () => {
@@ -1174,16 +1229,16 @@ describe('FlowSettings.open rendering behavior', () => {
1174
1229
 
1175
1230
  // Create mock DOM element for embed target
1176
1231
  const mockTarget = document.createElement('div');
1177
- mockTarget.id = 'nocobase-embed-container';
1232
+ mockTarget.id = GLOBAL_EMBED_CONTAINER_ID;
1178
1233
  document.body.appendChild(mockTarget);
1179
1234
 
1180
1235
  // Mock querySelector
1181
- const originalQuerySelector = document.querySelector;
1182
- document.querySelector = vi.fn((selector) => {
1183
- if (selector === '#nocobase-embed-container') {
1236
+ const originalQuerySelector = document.querySelector.bind(document);
1237
+ const querySelectorSpy = vi.spyOn(document, 'querySelector').mockImplementation((selector: string) => {
1238
+ if (selector === `#${GLOBAL_EMBED_CONTAINER_ID}`) {
1184
1239
  return mockTarget;
1185
1240
  }
1186
- return originalQuerySelector.call(document, selector);
1241
+ return originalQuerySelector(selector);
1187
1242
  });
1188
1243
 
1189
1244
  const M = model.constructor as any;
@@ -1223,7 +1278,7 @@ describe('FlowSettings.open rendering behavior', () => {
1223
1278
 
1224
1279
  // Cleanup
1225
1280
  document.body.removeChild(mockTarget);
1226
- document.querySelector = originalQuerySelector;
1281
+ querySelectorSpy.mockRestore();
1227
1282
  });
1228
1283
 
1229
1284
  it('handles embed uiMode when target element is not found', async () => {
@@ -1232,8 +1287,7 @@ describe('FlowSettings.open rendering behavior', () => {
1232
1287
  const model = new FlowModel({ uid: 'm-embed-no-target', flowEngine: engine });
1233
1288
 
1234
1289
  // Mock querySelector to return null (target not found)
1235
- const originalQuerySelector = document.querySelector;
1236
- document.querySelector = vi.fn(() => null);
1290
+ const querySelectorSpy = vi.spyOn(document, 'querySelector').mockReturnValue(null);
1237
1291
 
1238
1292
  const M = model.constructor as any;
1239
1293
  M.registerFlow({
@@ -1266,7 +1320,7 @@ describe('FlowSettings.open rendering behavior', () => {
1266
1320
  expect(embed).toHaveBeenCalledTimes(1);
1267
1321
 
1268
1322
  // Restore querySelector
1269
- document.querySelector = originalQuerySelector;
1323
+ querySelectorSpy.mockRestore();
1270
1324
  });
1271
1325
 
1272
1326
  it('handles error in function-based step uiMode gracefully', async () => {
@@ -1718,8 +1772,8 @@ describe('FlowSettings.open rendering behavior', () => {
1718
1772
  expect(capturedDialog.close).toHaveBeenCalled();
1719
1773
  });
1720
1774
 
1721
- it('submit method handles FlowExitException by closing dialog without error message', async () => {
1722
- const { FlowExitException } = await import('../utils/exceptions');
1775
+ it('submit method handles FlowExitAllException by closing dialog without error message', async () => {
1776
+ const { FlowExitAllException } = await import('../utils/exceptions');
1723
1777
 
1724
1778
  const engine = new FlowEngine();
1725
1779
  const flowSettings = new FlowSettings(engine);
@@ -1732,7 +1786,7 @@ describe('FlowSettings.open rendering behavior', () => {
1732
1786
  step: {
1733
1787
  title: 'Step',
1734
1788
  beforeParamsSave: () => {
1735
- throw new FlowExitException('exitFlow', 'm-submit-exit', 'Exit requested');
1789
+ throw new FlowExitAllException('exitFlow', 'm-submit-exit', 'Exit requested');
1736
1790
  },
1737
1791
  uiSchema: { field: { type: 'string', 'x-component': 'Input' } },
1738
1792
  },
@@ -1764,12 +1818,62 @@ describe('FlowSettings.open rendering behavior', () => {
1764
1818
  // Call submit method
1765
1819
  await capturedDialog.submit();
1766
1820
 
1767
- // Verify FlowExitException handling
1821
+ // Verify FlowExitAllException handling
1768
1822
  expect(error).not.toHaveBeenCalled(); // Should not show error message
1769
1823
  expect(success).not.toHaveBeenCalled(); // Should not show success message
1770
1824
  expect(capturedDialog.close).toHaveBeenCalled(); // Should close dialog
1771
1825
  });
1772
1826
 
1827
+ it('submit method keeps dialog open on FlowCancelSaveException', async () => {
1828
+ const { FlowCancelSaveException } = await import('../utils/exceptions');
1829
+
1830
+ const engine = new FlowEngine();
1831
+ const flowSettings = new FlowSettings(engine);
1832
+ const TestFlowModel = createIsolatedFlowModel('test-submit-cancel-save');
1833
+ const model = new TestFlowModel({ uid: 'm-submit-cancel-save', flowEngine: engine });
1834
+
1835
+ TestFlowModel.registerFlow({
1836
+ key: 'cancelSaveFlow',
1837
+ steps: {
1838
+ step: {
1839
+ title: 'Step',
1840
+ beforeParamsSave: () => {
1841
+ throw new FlowCancelSaveException();
1842
+ },
1843
+ uiSchema: { field: { type: 'string', 'x-component': 'Input' } },
1844
+ },
1845
+ },
1846
+ });
1847
+
1848
+ const info = vi.fn();
1849
+ const error = vi.fn();
1850
+ const success = vi.fn();
1851
+ model.context.defineProperty('message', { value: { info, error, success } });
1852
+
1853
+ const saveStepParams = vi.spyOn(model as any, 'saveStepParams').mockResolvedValue(undefined);
1854
+
1855
+ let capturedDialog: any;
1856
+ model.context.defineProperty('viewer', {
1857
+ value: {
1858
+ dialog: ({ content }) => {
1859
+ capturedDialog = { close: vi.fn(), Footer: (p: any) => null };
1860
+ if (typeof content === 'function') {
1861
+ content(capturedDialog, { defineMethod: vi.fn() });
1862
+ }
1863
+ return capturedDialog;
1864
+ },
1865
+ },
1866
+ });
1867
+
1868
+ await flowSettings.open({ model, flowKey: 'cancelSaveFlow', stepKey: 'step' } as any);
1869
+ await capturedDialog.submit();
1870
+
1871
+ expect(capturedDialog.close).not.toHaveBeenCalled();
1872
+ expect(saveStepParams).not.toHaveBeenCalled();
1873
+ expect(error).not.toHaveBeenCalled();
1874
+ expect(success).not.toHaveBeenCalled();
1875
+ });
1876
+
1773
1877
  it('submit method handles general errors by showing error message and keeping dialog open', async () => {
1774
1878
  const engine = new FlowEngine();
1775
1879
  const flowSettings = new FlowSettings(engine);
@@ -7,17 +7,17 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { describe, it, expect, beforeAll } from 'vitest';
10
+ import { beforeAll, describe, expect, it } from 'vitest';
11
11
  import {
12
12
  RunJSContextRegistry,
13
- getRunJSDocFor,
14
13
  createJSRunnerWithVersion,
15
- getRunJSScenesForModel,
14
+ getRunJSDocFor,
16
15
  getRunJSScenesForContext,
16
+ getRunJSScenesForModel,
17
17
  } from '..';
18
- import { setupRunJSContexts } from '../runjs-context/setup';
19
18
  import { FlowContext } from '../flowContext';
20
19
  import { JSRunner } from '../JSRunner';
20
+ import { setupRunJSContexts } from '../runjs-context/setup';
21
21
 
22
22
  describe('flowRunJSContext registry and doc', () => {
23
23
  beforeAll(async () => {
@@ -80,7 +80,10 @@ describe('flowRunJSContext registry and doc', () => {
80
80
  (ctx as any).defineProperty('model', { value: { constructor: { name: 'JSFieldModel' } } });
81
81
  (ctx as any).defineProperty('api', { value: { auth: { locale: 'zh-CN' } } });
82
82
  const doc = getRunJSDocFor(ctx as any, { version: 'v1' });
83
- expect(doc?.properties?.message).toMatch(/Ant Design 全局消息/);
83
+ const message = doc?.properties?.message;
84
+ const messageText =
85
+ typeof message === 'string' ? message : (message as any)?.description ?? (message as any)?.detail ?? '';
86
+ expect(String(messageText)).toMatch(/Ant Design 全局消息/);
84
87
  });
85
88
 
86
89
  it('should fallback to English when locale is not found', () => {
@@ -208,10 +211,10 @@ describe('flowRunJSContext registry and doc', () => {
208
211
  expect(doc?.properties?.message).toBeTruthy();
209
212
  });
210
213
 
211
- it('should have api property in base context', () => {
214
+ it('should have request method in base context', () => {
212
215
  const ctx: any = { model: { constructor: { name: '*' } } };
213
216
  const doc = getRunJSDocFor(ctx as any, { version: 'v1' });
214
- expect(doc?.properties?.api).toBeTruthy();
217
+ expect(doc?.methods?.request).toBeTruthy();
215
218
  });
216
219
 
217
220
  it('should have t method in base context', () => {
@@ -7,10 +7,10 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { describe, it, expect, beforeAll } from 'vitest';
10
+ import { beforeAll, describe, expect, it } from 'vitest';
11
11
  import { RunJSContextRegistry, getRunJSDocFor } from '..';
12
- import { setupRunJSContexts } from '../runjs-context/setup';
13
12
  import { FlowContext } from '../flowContext';
13
+ import { setupRunJSContexts } from '../runjs-context/setup';
14
14
 
15
15
  describe('Specific RunJSContext implementations', () => {
16
16
  beforeAll(async () => {
@@ -34,7 +34,8 @@ describe('Specific RunJSContext implementations', () => {
34
34
  const ctx: any = { model: { constructor: { name: 'JSColumnModel' } } };
35
35
  const doc = getRunJSDocFor(ctx as any, { version: 'v1' });
36
36
  expect(doc?.properties?.record).toBeTruthy();
37
- expect(doc?.properties?.record).toContain('row record');
37
+ const recordDoc: any = doc?.properties?.record;
38
+ expect(String(recordDoc?.description ?? recordDoc ?? '')).toContain('row record');
38
39
  });
39
40
 
40
41
  it('should have recordIndex property in doc', () => {
@@ -86,6 +87,14 @@ describe('Specific RunJSContext implementations', () => {
86
87
  expect(doc?.properties?.antd).toBeTruthy();
87
88
  });
88
89
 
90
+ it('should have ctx.auth.locale / ctx.viewer.drawer in doc', () => {
91
+ const ctx: any = { model: { constructor: { name: 'JSBlockModel' } } };
92
+ const doc = getRunJSDocFor(ctx as any, { version: 'v1' });
93
+
94
+ expect(doc?.properties?.auth?.properties?.locale).toBeTruthy();
95
+ expect(doc?.properties?.viewer?.properties?.drawer).toBeTruthy();
96
+ });
97
+
89
98
  it('should have element property', () => {
90
99
  const ctx: any = { model: { constructor: { name: 'JSBlockModel' } } };
91
100
  const doc = getRunJSDocFor(ctx as any, { version: 'v1' });
@@ -214,4 +223,26 @@ describe('Specific RunJSContext implementations', () => {
214
223
  expect(doc?.label).toMatch(/表单 JS 字段项/);
215
224
  });
216
225
  });
226
+
227
+ describe('JSEditableFieldRunJSContext', () => {
228
+ it('should be registered for JSEditableFieldModel', () => {
229
+ const ctor = RunJSContextRegistry['resolve']('v1' as any, 'JSEditableFieldModel');
230
+ expect(ctor).toBeTruthy();
231
+ });
232
+
233
+ it('should have getValue/setValue methods in doc', () => {
234
+ const ctx: any = { model: { constructor: { name: 'JSEditableFieldModel' } } };
235
+ const doc = getRunJSDocFor(ctx as any, { version: 'v1' });
236
+ expect(doc?.methods?.getValue).toBeTruthy();
237
+ expect(doc?.methods?.setValue).toBeTruthy();
238
+ });
239
+
240
+ it('should support zh-CN locale', () => {
241
+ const ctx = new FlowContext();
242
+ (ctx as any).defineProperty('model', { value: { constructor: { name: 'JSEditableFieldModel' } } });
243
+ (ctx as any).defineProperty('api', { value: { auth: { locale: 'zh-CN' } } });
244
+ const doc = getRunJSDocFor(ctx as any, { version: 'v1' });
245
+ expect(doc?.label).toMatch(/可编辑字段/);
246
+ });
247
+ });
217
248
  });
@@ -7,9 +7,9 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { describe, it, expect, beforeAll } from 'vitest';
11
- import { FlowContext } from '../flowContext';
10
+ import { beforeAll, describe, expect, it } from 'vitest';
12
11
  import { createJSRunnerWithVersion, getRunJSDocFor } from '..';
12
+ import { FlowContext } from '../flowContext';
13
13
  import { setupRunJSContexts } from '../runjs-context/setup';
14
14
 
15
15
  describe('RunJS Context Runtime Behavior', () => {
@@ -224,7 +224,7 @@ describe('RunJS Context Runtime Behavior', () => {
224
224
  // Base properties from FlowRunJSContext
225
225
  expect(doc?.properties?.logger).toBeTruthy();
226
226
  expect(doc?.properties?.message).toBeTruthy();
227
- expect(doc?.properties?.api).toBeTruthy();
227
+ expect(doc?.methods?.request).toBeTruthy();
228
228
  expect(doc?.methods?.t).toBeTruthy();
229
229
  expect(doc?.methods?.requireAsync).toBeTruthy();
230
230
  }
@@ -0,0 +1,89 @@
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, it, expect, vi } from 'vitest';
11
+
12
+ describe('RunJS context contributions', () => {
13
+ it('should apply contribution during setupRunJSContexts()', async () => {
14
+ vi.resetModules();
15
+ const mod: any = await import('..');
16
+ const {
17
+ registerRunJSContextContribution,
18
+ setupRunJSContexts,
19
+ RunJSContextRegistry,
20
+ getRunJSDocFor,
21
+ FlowRunJSContext,
22
+ } = mod;
23
+
24
+ registerRunJSContextContribution(({ version, RunJSContextRegistry: Registry, FlowRunJSContext: BaseCtx }: any) => {
25
+ if (version !== 'v1') return;
26
+ class PluginTestRunJSContext extends BaseCtx {}
27
+ PluginTestRunJSContext.define({
28
+ properties: {
29
+ plugin: { description: 'plugin namespace', detail: 'object' },
30
+ },
31
+ });
32
+ Registry.register('v1', 'PluginTestModel', PluginTestRunJSContext, { scenes: ['block'] });
33
+ });
34
+
35
+ await setupRunJSContexts();
36
+
37
+ const ctor = RunJSContextRegistry.resolve('v1', 'PluginTestModel');
38
+ expect(ctor).toBeTruthy();
39
+ expect((ctor as any).name).toBe('PluginTestRunJSContext');
40
+
41
+ const ctx: any = { model: { constructor: { name: 'PluginTestModel' } } };
42
+ const doc = getRunJSDocFor(ctx, { version: 'v1' });
43
+ expect(doc?.properties?.plugin).toBeTruthy();
44
+
45
+ // Ensure FlowRunJSContext stays usable
46
+ expect(typeof FlowRunJSContext.getDoc).toBe('function');
47
+ });
48
+
49
+ it('should apply late contribution immediately after setup', async () => {
50
+ vi.resetModules();
51
+ const mod: any = await import('..');
52
+ const { registerRunJSContextContribution, setupRunJSContexts, getRunJSDocFor, FlowContext } = mod;
53
+
54
+ await setupRunJSContexts();
55
+
56
+ registerRunJSContextContribution(({ version, FlowRunJSContext }: any) => {
57
+ if (version !== 'v1') return;
58
+ FlowRunJSContext.define({
59
+ properties: {
60
+ pluginLate: { description: 'late-added', detail: 'string' },
61
+ },
62
+ });
63
+ });
64
+
65
+ const ctx = new FlowContext();
66
+ const doc = getRunJSDocFor(ctx as any, { version: 'v1' });
67
+ expect(doc?.properties?.pluginLate).toBeTruthy();
68
+ });
69
+
70
+ it('should run each contribution at most once per version', async () => {
71
+ vi.resetModules();
72
+ const mod: any = await import('..');
73
+ const { registerRunJSContextContribution, setupRunJSContexts } = mod;
74
+
75
+ let count = 0;
76
+ const fn = ({ version }: any) => {
77
+ if (version !== 'v1') return;
78
+ count += 1;
79
+ };
80
+
81
+ registerRunJSContextContribution(fn);
82
+ registerRunJSContextContribution(fn); // duplicate registration should be ignored
83
+
84
+ await setupRunJSContexts();
85
+ await setupRunJSContexts();
86
+
87
+ expect(count).toBe(1);
88
+ });
89
+ });