@nocobase/flow-engine 2.1.0-beta.8 → 2.1.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 (215) hide show
  1. package/lib/FlowContextProvider.d.ts +5 -1
  2. package/lib/FlowContextProvider.js +9 -2
  3. package/lib/components/FieldModelRenderer.js +2 -2
  4. package/lib/components/FlowModelRenderer.d.ts +3 -1
  5. package/lib/components/FlowModelRenderer.js +12 -6
  6. package/lib/components/FormItem.d.ts +6 -0
  7. package/lib/components/FormItem.js +11 -3
  8. package/lib/components/MobilePopup.js +6 -5
  9. package/lib/components/dnd/gridDragPlanner.d.ts +59 -2
  10. package/lib/components/dnd/gridDragPlanner.js +607 -19
  11. package/lib/components/dnd/index.d.ts +31 -2
  12. package/lib/components/dnd/index.js +244 -23
  13. package/lib/components/settings/wrappers/component/SelectWithTitle.d.ts +2 -1
  14. package/lib/components/settings/wrappers/component/SelectWithTitle.js +14 -12
  15. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.d.ts +3 -0
  16. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +152 -42
  17. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +23 -43
  18. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +352 -295
  19. package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.d.ts +36 -0
  20. package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.js +274 -0
  21. package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.d.ts +30 -0
  22. package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +315 -0
  23. package/lib/components/subModel/AddSubModelButton.js +12 -1
  24. package/lib/components/subModel/LazyDropdown.js +301 -52
  25. package/lib/components/subModel/index.d.ts +1 -0
  26. package/lib/components/subModel/index.js +19 -0
  27. package/lib/components/subModel/utils.d.ts +2 -1
  28. package/lib/components/subModel/utils.js +15 -5
  29. package/lib/components/variables/VariableHybridInput.d.ts +27 -0
  30. package/lib/components/variables/VariableHybridInput.js +499 -0
  31. package/lib/components/variables/index.d.ts +2 -0
  32. package/lib/components/variables/index.js +3 -0
  33. package/lib/data-source/index.d.ts +84 -0
  34. package/lib/data-source/index.js +269 -7
  35. package/lib/executor/FlowExecutor.js +6 -3
  36. package/lib/flow-registry/DetachedFlowRegistry.d.ts +21 -0
  37. package/lib/flow-registry/DetachedFlowRegistry.js +80 -0
  38. package/lib/flow-registry/index.d.ts +1 -0
  39. package/lib/flow-registry/index.js +3 -1
  40. package/lib/flowContext.d.ts +9 -1
  41. package/lib/flowContext.js +77 -6
  42. package/lib/flowEngine.d.ts +136 -4
  43. package/lib/flowEngine.js +429 -51
  44. package/lib/flowI18n.js +2 -1
  45. package/lib/flowSettings.d.ts +14 -6
  46. package/lib/flowSettings.js +34 -6
  47. package/lib/index.d.ts +2 -0
  48. package/lib/index.js +7 -0
  49. package/lib/lazy-helper.d.ts +14 -0
  50. package/lib/lazy-helper.js +71 -0
  51. package/lib/locale/en-US.json +1 -0
  52. package/lib/locale/index.d.ts +2 -0
  53. package/lib/locale/zh-CN.json +1 -0
  54. package/lib/models/DisplayItemModel.d.ts +1 -1
  55. package/lib/models/EditableItemModel.d.ts +1 -1
  56. package/lib/models/FilterableItemModel.d.ts +1 -1
  57. package/lib/models/flowModel.d.ts +13 -10
  58. package/lib/models/flowModel.js +126 -34
  59. package/lib/provider.js +38 -23
  60. package/lib/reactive/observer.js +46 -16
  61. package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +4 -3
  62. package/lib/runjs-context/contexts/JSBlockRunJSContext.js +4 -15
  63. package/lib/runjs-context/contexts/JSColumnRunJSContext.js +5 -2
  64. package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.js +5 -8
  65. package/lib/runjs-context/contexts/JSFieldRunJSContext.js +4 -3
  66. package/lib/runjs-context/contexts/JSItemRunJSContext.js +4 -3
  67. package/lib/runjs-context/contexts/base.js +464 -29
  68. package/lib/runjs-context/contexts/elementDoc.d.ts +11 -0
  69. package/lib/runjs-context/contexts/elementDoc.js +152 -0
  70. package/lib/runjs-context/setup.js +1 -0
  71. package/lib/runjs-context/snippets/index.js +13 -2
  72. package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.d.ts +11 -0
  73. package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.js +50 -0
  74. package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.d.ts +11 -0
  75. package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.js +54 -0
  76. package/lib/types.d.ts +50 -2
  77. package/lib/types.js +1 -0
  78. package/lib/utils/createCollectionContextMeta.js +6 -2
  79. package/lib/utils/index.d.ts +3 -2
  80. package/lib/utils/index.js +7 -0
  81. package/lib/utils/loadedPageCache.d.ts +24 -0
  82. package/lib/utils/loadedPageCache.js +139 -0
  83. package/lib/utils/parsePathnameToViewParams.d.ts +5 -1
  84. package/lib/utils/parsePathnameToViewParams.js +28 -4
  85. package/lib/utils/randomId.d.ts +39 -0
  86. package/lib/utils/randomId.js +45 -0
  87. package/lib/utils/runjsTemplateCompat.js +1 -1
  88. package/lib/utils/runjsValue.js +41 -11
  89. package/lib/utils/schema-utils.d.ts +7 -1
  90. package/lib/utils/schema-utils.js +19 -0
  91. package/lib/views/FlowView.d.ts +7 -1
  92. package/lib/views/FlowView.js +11 -1
  93. package/lib/views/PageComponent.js +8 -6
  94. package/lib/views/ViewNavigation.d.ts +12 -2
  95. package/lib/views/ViewNavigation.js +28 -9
  96. package/lib/views/createViewMeta.js +114 -50
  97. package/lib/views/inheritLayoutContext.d.ts +10 -0
  98. package/lib/views/inheritLayoutContext.js +50 -0
  99. package/lib/views/runViewBeforeClose.d.ts +10 -0
  100. package/lib/views/runViewBeforeClose.js +45 -0
  101. package/lib/views/useDialog.d.ts +2 -1
  102. package/lib/views/useDialog.js +12 -3
  103. package/lib/views/useDrawer.d.ts +2 -1
  104. package/lib/views/useDrawer.js +12 -3
  105. package/lib/views/usePage.d.ts +5 -11
  106. package/lib/views/usePage.js +304 -144
  107. package/package.json +5 -4
  108. package/src/FlowContextProvider.tsx +9 -1
  109. package/src/__tests__/createViewMeta.popup.test.ts +115 -1
  110. package/src/__tests__/flow-engine.test.ts +166 -0
  111. package/src/__tests__/flowContext.test.ts +105 -1
  112. package/src/__tests__/flowEngine.modelLoaders.test.ts +245 -0
  113. package/src/__tests__/flowEngine.moveModel.test.ts +81 -1
  114. package/src/__tests__/flowEngine.removeModel.test.ts +47 -3
  115. package/src/__tests__/flowSettings.test.ts +94 -15
  116. package/src/__tests__/objectVariable.test.ts +24 -0
  117. package/src/__tests__/provider.test.tsx +24 -2
  118. package/src/__tests__/renderHiddenInConfig.test.tsx +6 -6
  119. package/src/__tests__/runjsContext.test.ts +21 -0
  120. package/src/__tests__/runjsContextImplementations.test.ts +9 -2
  121. package/src/__tests__/runjsContextRuntime.test.ts +2 -0
  122. package/src/__tests__/runjsLocales.test.ts +6 -5
  123. package/src/__tests__/runjsSnippets.test.ts +21 -0
  124. package/src/__tests__/viewScopedFlowEngine.test.ts +136 -3
  125. package/src/components/FieldModelRenderer.tsx +2 -1
  126. package/src/components/FlowModelRenderer.tsx +18 -6
  127. package/src/components/FormItem.tsx +7 -1
  128. package/src/components/MobilePopup.tsx +4 -2
  129. package/src/components/__tests__/FlowModelRenderer.test.tsx +65 -2
  130. package/src/components/__tests__/FormItem.test.tsx +25 -0
  131. package/src/components/__tests__/dnd.test.ts +44 -0
  132. package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +20 -10
  133. package/src/components/__tests__/gridDragPlanner.test.ts +472 -5
  134. package/src/components/dnd/__tests__/DndProvider.test.tsx +98 -0
  135. package/src/components/dnd/gridDragPlanner.ts +750 -17
  136. package/src/components/dnd/index.tsx +305 -28
  137. package/src/components/settings/wrappers/component/SelectWithTitle.tsx +21 -9
  138. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +178 -48
  139. package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +487 -440
  140. package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +344 -8
  141. package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +778 -0
  142. package/src/components/settings/wrappers/contextual/useFloatToolbarPortal.ts +360 -0
  143. package/src/components/settings/wrappers/contextual/useFloatToolbarVisibility.ts +361 -0
  144. package/src/components/subModel/AddSubModelButton.tsx +16 -2
  145. package/src/components/subModel/LazyDropdown.tsx +341 -56
  146. package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +524 -38
  147. package/src/components/subModel/__tests__/utils.test.ts +24 -0
  148. package/src/components/subModel/index.ts +1 -0
  149. package/src/components/subModel/utils.ts +13 -2
  150. package/src/components/variables/VariableHybridInput.tsx +531 -0
  151. package/src/components/variables/index.ts +2 -0
  152. package/src/data-source/__tests__/collection.test.ts +41 -2
  153. package/src/data-source/__tests__/index.test.ts +69 -2
  154. package/src/data-source/index.ts +332 -8
  155. package/src/executor/FlowExecutor.ts +6 -3
  156. package/src/executor/__tests__/flowExecutor.test.ts +57 -0
  157. package/src/flow-registry/DetachedFlowRegistry.ts +46 -0
  158. package/src/flow-registry/__tests__/detachedFlowRegistry.test.ts +47 -0
  159. package/src/flow-registry/index.ts +1 -0
  160. package/src/flowContext.ts +85 -6
  161. package/src/flowEngine.ts +484 -45
  162. package/src/flowI18n.ts +2 -1
  163. package/src/flowSettings.ts +40 -6
  164. package/src/index.ts +2 -0
  165. package/src/lazy-helper.tsx +57 -0
  166. package/src/locale/en-US.json +1 -0
  167. package/src/locale/zh-CN.json +1 -0
  168. package/src/models/DisplayItemModel.tsx +1 -1
  169. package/src/models/EditableItemModel.tsx +1 -1
  170. package/src/models/FilterableItemModel.tsx +1 -1
  171. package/src/models/__tests__/flowEngine.resolveUse.test.ts +0 -15
  172. package/src/models/__tests__/flowModel.test.ts +65 -37
  173. package/src/models/flowModel.tsx +184 -65
  174. package/src/provider.tsx +41 -25
  175. package/src/reactive/__tests__/observer.test.tsx +82 -0
  176. package/src/reactive/observer.tsx +87 -25
  177. package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +4 -3
  178. package/src/runjs-context/contexts/JSBlockRunJSContext.ts +4 -15
  179. package/src/runjs-context/contexts/JSColumnRunJSContext.ts +4 -2
  180. package/src/runjs-context/contexts/JSEditableFieldRunJSContext.ts +5 -9
  181. package/src/runjs-context/contexts/JSFieldRunJSContext.ts +4 -3
  182. package/src/runjs-context/contexts/JSItemRunJSContext.ts +4 -3
  183. package/src/runjs-context/contexts/base.ts +467 -31
  184. package/src/runjs-context/contexts/elementDoc.ts +130 -0
  185. package/src/runjs-context/setup.ts +1 -0
  186. package/src/runjs-context/snippets/index.ts +12 -1
  187. package/src/runjs-context/snippets/scene/detail/set-field-style.snippet.ts +30 -0
  188. package/src/runjs-context/snippets/scene/table/set-cell-style.snippet.ts +34 -0
  189. package/src/types.ts +62 -0
  190. package/src/utils/__tests__/createCollectionContextMeta.test.ts +48 -0
  191. package/src/utils/__tests__/parsePathnameToViewParams.test.ts +21 -0
  192. package/src/utils/__tests__/runjsValue.test.ts +11 -0
  193. package/src/utils/__tests__/utils.test.ts +62 -0
  194. package/src/utils/createCollectionContextMeta.ts +6 -2
  195. package/src/utils/index.ts +5 -1
  196. package/src/utils/loadedPageCache.ts +147 -0
  197. package/src/utils/parsePathnameToViewParams.ts +45 -5
  198. package/src/utils/randomId.ts +48 -0
  199. package/src/utils/runjsTemplateCompat.ts +1 -1
  200. package/src/utils/runjsValue.ts +50 -11
  201. package/src/utils/schema-utils.ts +30 -1
  202. package/src/views/FlowView.tsx +22 -2
  203. package/src/views/PageComponent.tsx +7 -4
  204. package/src/views/ViewNavigation.ts +46 -9
  205. package/src/views/__tests__/FlowView.usePage.test.tsx +243 -3
  206. package/src/views/__tests__/ViewNavigation.test.ts +52 -0
  207. package/src/views/__tests__/inheritLayoutContext.test.ts +53 -0
  208. package/src/views/__tests__/runViewBeforeClose.test.ts +30 -0
  209. package/src/views/__tests__/useDialog.closeDestroy.test.tsx +12 -12
  210. package/src/views/createViewMeta.ts +106 -34
  211. package/src/views/inheritLayoutContext.ts +26 -0
  212. package/src/views/runViewBeforeClose.ts +19 -0
  213. package/src/views/useDialog.tsx +13 -3
  214. package/src/views/useDrawer.tsx +13 -3
  215. package/src/views/usePage.tsx +367 -180
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { beforeEach, describe, expect, it } from 'vitest';
10
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
11
11
  import { FlowEngine } from '../flowEngine';
12
12
  import { FlowModel } from '../models';
13
13
 
@@ -20,6 +20,7 @@ describe('FlowEngine removeModel', () => {
20
20
  });
21
21
 
22
22
  it('removeModel should remove model but keep sub-models in cache (current behavior)', () => {
23
+ const loggerSpy = vi.spyOn(engine.logger, 'debug').mockImplementation(() => {});
23
24
  const parent = engine.createModel({ uid: 'parent', use: 'FlowModel' });
24
25
  const child = engine.createModel({
25
26
  uid: 'child',
@@ -32,14 +33,53 @@ describe('FlowEngine removeModel', () => {
32
33
  expect(engine.getModel('parent')).toBe(parent);
33
34
  expect(engine.getModel('child')).toBe(child);
34
35
 
35
- engine.removeModel('parent');
36
+ try {
37
+ engine.removeModel('parent');
38
+ } finally {
39
+ loggerSpy.mockRestore();
40
+ }
36
41
 
37
42
  expect(engine.getModel('parent')).toBeUndefined();
38
43
  // Current behavior: child is still in cache
39
44
  expect(engine.getModel('child')).toBeDefined();
40
45
  });
41
46
 
47
+ it('removeModel should log missing models at debug level', () => {
48
+ const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
49
+ const loggerSpy = vi.spyOn(engine.logger, 'debug').mockImplementation(() => {});
50
+
51
+ try {
52
+ expect(engine.removeModel('missing')).toBe(false);
53
+ expect(consoleWarnSpy).not.toHaveBeenCalled();
54
+ expect(loggerSpy).toHaveBeenCalledWith("FlowEngine: Model with UID 'missing' does not exist.");
55
+ } finally {
56
+ consoleWarnSpy.mockRestore();
57
+ loggerSpy.mockRestore();
58
+ }
59
+ });
60
+
61
+ it('should reduce default logger verbosity in production', () => {
62
+ const originalNodeEnv = process.env.NODE_ENV;
63
+
64
+ try {
65
+ process.env.NODE_ENV = 'production';
66
+
67
+ const productionEngine = new FlowEngine();
68
+
69
+ expect(productionEngine.logger.level).toBe('warn');
70
+ expect(productionEngine.logger.isLevelEnabled('debug')).toBe(false);
71
+ expect(productionEngine.logger.isLevelEnabled('warn')).toBe(true);
72
+ } finally {
73
+ if (originalNodeEnv === undefined) {
74
+ delete process.env.NODE_ENV;
75
+ } else {
76
+ process.env.NODE_ENV = originalNodeEnv;
77
+ }
78
+ }
79
+ });
80
+
42
81
  it('removeModelWithSubModels should remove model and all sub-models from cache', () => {
82
+ const loggerSpy = vi.spyOn(engine.logger, 'debug').mockImplementation(() => {});
43
83
  const parent = engine.createModel({ uid: 'parent', use: 'FlowModel' });
44
84
  const child = engine.createModel({
45
85
  uid: 'child',
@@ -63,7 +103,11 @@ describe('FlowEngine removeModel', () => {
63
103
  expect(engine.getModel('child')).toBe(child);
64
104
  expect(engine.getModel('grandChild')).toBe(grandChild);
65
105
 
66
- engine.removeModelWithSubModels('parent');
106
+ try {
107
+ engine.removeModelWithSubModels('parent');
108
+ } finally {
109
+ loggerSpy.mockRestore();
110
+ }
67
111
 
68
112
  expect(engine.getModel('parent')).toBeUndefined();
69
113
  expect(engine.getModel('child')).toBeUndefined();
@@ -8,6 +8,10 @@
8
8
  */
9
9
 
10
10
  import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest';
11
+ import React from 'react';
12
+ import { createForm } from '@formily/core';
13
+ import { createSchemaField, FormProvider } from '@formily/react';
14
+ import { render, screen } from '@testing-library/react';
11
15
  import { FlowSettings } from '../flowSettings';
12
16
  import { DefaultSettingsIcon } from '../components/settings/wrappers/contextual/DefaultSettingsIcon';
13
17
  import { FlowModel } from '../models';
@@ -142,10 +146,10 @@ describe('FlowSettings', () => {
142
146
  expect(settingsItem?.sort).toBe(0);
143
147
  });
144
148
 
145
- test('should set up observable properties', () => {
149
+ test('should set up observable properties', async () => {
146
150
  // Test that enabled property is reactive
147
151
  const initialEnabled = flowSettings.enabled;
148
- flowSettings.enable();
152
+ await flowSettings.enable();
149
153
  expect(flowSettings.enabled).not.toBe(initialEnabled);
150
154
  expect(flowSettings.enabled).toBe(true);
151
155
  });
@@ -186,6 +190,43 @@ describe('FlowSettings', () => {
186
190
  flowSettings.registerComponents({});
187
191
  expect(Object.keys(flowSettings.components)).toHaveLength(0);
188
192
  });
193
+
194
+ test('should register component loaders and load component on render', async () => {
195
+ const loader = vi.fn(async () => ({
196
+ default: () => React.createElement('div', null, 'Lazy Flow Settings Component'),
197
+ }));
198
+
199
+ flowSettings.registerComponentLoaders({
200
+ DemoFlowSettingsLazyField: loader,
201
+ });
202
+
203
+ expect(loader).not.toHaveBeenCalled();
204
+
205
+ const SchemaField = createSchemaField();
206
+ const form = createForm();
207
+
208
+ render(
209
+ React.createElement(
210
+ FormProvider,
211
+ { form },
212
+ React.createElement(SchemaField, {
213
+ schema: {
214
+ type: 'object',
215
+ properties: {
216
+ demo: {
217
+ type: 'void',
218
+ 'x-component': 'DemoFlowSettingsLazyField',
219
+ },
220
+ },
221
+ },
222
+ components: flowSettings.components,
223
+ }),
224
+ ),
225
+ );
226
+
227
+ expect(await screen.findByText('Lazy Flow Settings Component')).toBeInTheDocument();
228
+ expect(loader).toHaveBeenCalledTimes(1);
229
+ });
189
230
  });
190
231
 
191
232
  describe('Scope Registration', () => {
@@ -228,30 +269,68 @@ describe('FlowSettings', () => {
228
269
  });
229
270
 
230
271
  describe('Enable/Disable Functionality', () => {
231
- test('should enable flow settings', () => {
272
+ test('should enable flow settings', async () => {
232
273
  expect(flowSettings.enabled).toBe(false);
233
274
 
234
- flowSettings.enable();
275
+ await flowSettings.enable();
235
276
 
236
277
  expect(flowSettings.enabled).toBe(true);
237
278
  });
238
279
 
239
- test('should disable flow settings', () => {
240
- flowSettings.enable();
280
+ test('should preload model loaders before enabling flow settings', async () => {
281
+ const preloadSpy = vi.spyOn(engine, 'preloadModelLoaders').mockResolvedValue({
282
+ requested: [],
283
+ loaded: [],
284
+ failed: [],
285
+ });
286
+
287
+ await flowSettings.enable();
288
+
289
+ expect(preloadSpy).toHaveBeenCalledTimes(1);
241
290
  expect(flowSettings.enabled).toBe(true);
291
+ });
242
292
 
243
- flowSettings.disable();
293
+ test('should preload model loaders before force enabling flow settings', async () => {
294
+ const preloadSpy = vi.spyOn(engine, 'preloadModelLoaders').mockResolvedValue({
295
+ requested: [],
296
+ loaded: [],
297
+ failed: [],
298
+ });
299
+
300
+ await flowSettings.forceEnable();
301
+
302
+ expect(preloadSpy).toHaveBeenCalledTimes(1);
303
+ expect(flowSettings.enabled).toBe(true);
304
+ });
305
+
306
+ test('should disable flow settings', async () => {
307
+ await flowSettings.enable();
308
+ expect(flowSettings.enabled).toBe(true);
309
+
310
+ await flowSettings.disable();
244
311
 
245
312
  expect(flowSettings.enabled).toBe(false);
246
313
  });
247
314
 
248
- test('should handle multiple enable/disable calls', () => {
249
- flowSettings.enable();
250
- flowSettings.enable();
315
+ test('should handle multiple enable/disable calls', async () => {
316
+ await flowSettings.enable();
317
+ await flowSettings.enable();
251
318
  expect(flowSettings.enabled).toBe(true);
252
319
 
253
- flowSettings.disable();
254
- flowSettings.disable();
320
+ await flowSettings.disable();
321
+ await flowSettings.disable();
322
+ expect(flowSettings.enabled).toBe(false);
323
+ });
324
+
325
+ test('forceDisable should clear force-enabled state and disable flow settings', async () => {
326
+ await flowSettings.forceEnable();
327
+ expect(flowSettings.enabled).toBe(true);
328
+
329
+ await flowSettings.forceDisable();
330
+
331
+ expect(flowSettings.enabled).toBe(false);
332
+
333
+ await flowSettings.disable();
255
334
  expect(flowSettings.enabled).toBe(false);
256
335
  });
257
336
  });
@@ -512,7 +591,7 @@ describe('FlowSettings', () => {
512
591
  });
513
592
 
514
593
  describe('Complex Integration Scenarios', () => {
515
- test('should maintain state consistency during multiple operations', () => {
594
+ test('should maintain state consistency during multiple operations', async () => {
516
595
  // Initialize with components and scopes
517
596
  const TestComponent = () => 'TestComponent';
518
597
  const testScope = () => 'testScope';
@@ -528,7 +607,7 @@ describe('FlowSettings', () => {
528
607
  });
529
608
 
530
609
  // Enable/disable
531
- flowSettings.enable();
610
+ await flowSettings.enable();
532
611
  expect(flowSettings.enabled).toBe(true);
533
612
 
534
613
  // Verify all state is maintained
@@ -536,7 +615,7 @@ describe('FlowSettings', () => {
536
615
  expect(flowSettings.scopes.testScope).toBe(testScope);
537
616
  expect(flowSettings.getToolbarItems().find((item) => item.key === 'integration-test')).toBeDefined();
538
617
 
539
- flowSettings.disable();
618
+ await flowSettings.disable();
540
619
  expect(flowSettings.enabled).toBe(false);
541
620
 
542
621
  // State should still be maintained after disable
@@ -26,6 +26,7 @@ function setupEngineWithCollections() {
26
26
  fields: [
27
27
  { name: 'id', type: 'integer', interface: 'number' },
28
28
  { name: 'name', type: 'string', interface: 'text' },
29
+ { name: 'rawUserPayload', type: 'json', filterable: true },
29
30
  ],
30
31
  });
31
32
  ds.addCollection({
@@ -41,6 +42,8 @@ function setupEngineWithCollections() {
41
42
  filterTargetKey: 'id',
42
43
  fields: [
43
44
  { name: 'title', type: 'string', interface: 'text' },
45
+ { name: 'internalName', type: 'string', interface: 'text' },
46
+ { name: 'rawPostPayload', type: 'json', filterable: true },
44
47
  { name: 'author', type: 'belongsTo', target: 'users', interface: 'm2o' },
45
48
  { name: 'tags', type: 'belongsToMany', target: 'tags', interface: 'm2m' },
46
49
  ],
@@ -91,6 +94,27 @@ describe('objectVariable utilities', () => {
91
94
  });
92
95
  });
93
96
 
97
+ it('createAssociationAwareObjectMetaFactory should hide fields without interface from object variable meta', async () => {
98
+ const { collection } = setupEngineWithCollections();
99
+ const obj = { title: 'hello', internalName: 'internal', rawPostPayload: { secret: true }, author: 1 };
100
+ const metaFactory = createAssociationAwareObjectMetaFactory(
101
+ () => collection,
102
+ 'Current object',
103
+ () => obj,
104
+ );
105
+
106
+ const meta = await metaFactory();
107
+ const props = await (meta?.properties as any)?.();
108
+ const authorFields = await props?.author?.properties?.();
109
+
110
+ expect(props).toHaveProperty('title');
111
+ expect(props).toHaveProperty('internalName');
112
+ expect(props).toHaveProperty('author');
113
+ expect(props).not.toHaveProperty('rawPostPayload');
114
+ expect(authorFields).toHaveProperty('name');
115
+ expect(authorFields).not.toHaveProperty('rawUserPayload');
116
+ });
117
+
94
118
  it('integrates with FlowContext.resolveJsonTemplate to call variables:resolve with flattened contextParams', async () => {
95
119
  const { engine, collection } = setupEngineWithCollections();
96
120
  const obj = { author: 1 };
@@ -9,9 +9,10 @@
9
9
 
10
10
  import React from 'react';
11
11
  import { describe, expect, it } from 'vitest';
12
- import { renderHook } from '@testing-library/react';
12
+ import { render, renderHook } from '@testing-library/react';
13
+ import { App, ConfigProvider, theme } from 'antd';
13
14
  import { FlowEngine } from '../flowEngine';
14
- import { FlowEngineProvider, useFlowEngine } from '../provider';
15
+ import { FlowEngineGlobalsContextProvider, FlowEngineProvider, useFlowEngine } from '../provider';
15
16
 
16
17
  describe('FlowEngineProvider/useFlowEngine', () => {
17
18
  it('returns engine within provider', () => {
@@ -20,4 +21,25 @@ describe('FlowEngineProvider/useFlowEngine', () => {
20
21
  const { result } = renderHook(() => useFlowEngine(), { wrapper });
21
22
  expect(result.current).toBe(engine);
22
23
  });
24
+
25
+ it('registers isDarkTheme within globals provider before first child render', () => {
26
+ const engine = new FlowEngine();
27
+ const reads: boolean[] = [];
28
+ const Reader = () => {
29
+ reads.push(engine.context.isDarkTheme);
30
+ return null;
31
+ };
32
+ const wrapper = ({ children }: any) => (
33
+ <ConfigProvider theme={{ algorithm: theme.darkAlgorithm }}>
34
+ <App>
35
+ <FlowEngineProvider engine={engine}>
36
+ <FlowEngineGlobalsContextProvider>{children}</FlowEngineGlobalsContextProvider>
37
+ </FlowEngineProvider>
38
+ </App>
39
+ </ConfigProvider>
40
+ );
41
+ render(<Reader />, { wrapper });
42
+ expect(reads[0]).toBe(true);
43
+ expect(engine.context.isDarkTheme).toBe(true);
44
+ });
23
45
  });
@@ -14,7 +14,7 @@ import { FlowEngine } from '../flowEngine';
14
14
  import { FlowModel, ModelRenderMode } from '../models/flowModel';
15
15
 
16
16
  describe('FlowModel.renderHiddenInConfig', () => {
17
- it('renders via renderHiddenInConfig when hidden and config enabled (React element mode, mounted)', () => {
17
+ it('renders via renderHiddenInConfig when hidden and config enabled (React element mode, mounted)', async () => {
18
18
  class ElemModel extends FlowModel {
19
19
  render() {
20
20
  return <div data-testid="content">Content</div>;
@@ -28,14 +28,14 @@ describe('FlowModel.renderHiddenInConfig', () => {
28
28
  const model = new ElemModel({ uid: 'elem-1', flowEngine: engine });
29
29
 
30
30
  // runtime hidden => mounted result should be empty (no content/hidden)
31
- engine.flowSettings.disable();
31
+ await engine.flowSettings.disable();
32
32
  model.setHidden(true);
33
33
  const { container, unmount, rerender } = render(model.render() as React.ReactElement);
34
34
  expect(screen.queryByTestId('content')).toBeNull();
35
35
  expect(screen.queryByTestId('hidden')).toBeNull();
36
36
 
37
37
  // config enabled + hidden => should show renderHiddenInConfig result
38
- engine.flowSettings.enable();
38
+ await engine.flowSettings.enable();
39
39
  rerender(model.render() as React.ReactElement);
40
40
  expect(screen.getByTestId('hidden').textContent).toBe('HiddenViaAPI');
41
41
 
@@ -46,7 +46,7 @@ describe('FlowModel.renderHiddenInConfig', () => {
46
46
  unmount();
47
47
  cleanup();
48
48
  });
49
- it('returns a render function when hidden and config enabled (RenderFunction mode)', () => {
49
+ it('returns a render function when hidden and config enabled (RenderFunction mode)', async () => {
50
50
  class FuncModel extends FlowModel {
51
51
  static override renderMode = ModelRenderMode.RenderFunction;
52
52
  render() {
@@ -63,13 +63,13 @@ describe('FlowModel.renderHiddenInConfig', () => {
63
63
  const model = engine.createModel({ use: 'FuncModel' }) as FuncModel;
64
64
 
65
65
  // runtime hidden => null
66
- engine.flowSettings.disable();
66
+ await engine.flowSettings.disable();
67
67
  model.setHidden(true);
68
68
  const runtimeHidden = model.render();
69
69
  expect(runtimeHidden).toBeNull();
70
70
 
71
71
  // config enabled + hidden => renderHiddenInConfig (function)
72
- engine.flowSettings.enable();
72
+ await engine.flowSettings.enable();
73
73
  const cfgHidden = model.render();
74
74
  expect(typeof cfgHidden).toBe('function');
75
75
  const cellNode = (cfgHidden as any)();
@@ -38,6 +38,7 @@ describe('flowRunJSContext registry and doc', () => {
38
38
  'JSBlockModel',
39
39
  'JSFieldModel',
40
40
  'JSItemModel',
41
+ 'JSItemActionModel',
41
42
  'JSColumnModel',
42
43
  'FormJSFieldItemModel',
43
44
  'JSRecordActionModel',
@@ -58,8 +59,10 @@ describe('flowRunJSContext registry and doc', () => {
58
59
  it('should expose scene metadata for contexts', () => {
59
60
  expect(getRunJSScenesForModel('JSBlockModel', 'v1')).toEqual(['block']);
60
61
  expect(getRunJSScenesForModel('JSFieldModel', 'v1')).toEqual(['detail']);
62
+ expect(getRunJSScenesForModel('JSItemActionModel', 'v1')).toEqual(['table']);
61
63
  expect(getRunJSScenesForModel('JSBlockModel', 'v2')).toEqual(['block']);
62
64
  expect(getRunJSScenesForModel('JSFieldModel', 'v2')).toEqual(['detail']);
65
+ expect(getRunJSScenesForModel('JSItemActionModel', 'v2')).toEqual(['table']);
63
66
  expect(getRunJSScenesForModel('UnknownModel', 'v1')).toEqual([]);
64
67
  expect(getRunJSScenesForModel('UnknownModel', 'v2')).toEqual([]);
65
68
  });
@@ -85,6 +88,19 @@ describe('flowRunJSContext registry and doc', () => {
85
88
  const doc = getRunJSDocFor(ctx as any, { version: 'v1' });
86
89
  expect(doc).toBeTruthy();
87
90
  expect(doc?.label).toMatch(/RunJS base/);
91
+ expect(doc?.properties?.element).toBeUndefined();
92
+ });
93
+
94
+ it('should mark element-dependent base completions with element requirement', () => {
95
+ const ctx: any = { model: { constructor: { name: 'UnknownModel' } } };
96
+ const doc = getRunJSDocFor(ctx as any, { version: 'v1' });
97
+
98
+ expect((doc?.methods?.render as any)?.completion?.requires).toContain('element');
99
+ expect((doc?.properties?.viewer as any)?.properties?.popover?.completion?.requires).toContain('element');
100
+ expect((doc?.properties?.viewer as any)?.properties?.embed?.completion?.requires).toContain('element');
101
+ expect(
102
+ (doc?.properties?.libs as any)?.properties?.ReactDOM?.properties?.createRoot?.completion?.requires,
103
+ ).toContain('element');
88
104
  });
89
105
 
90
106
  it('should support locale-specific doc', () => {
@@ -96,6 +112,11 @@ describe('flowRunJSContext registry and doc', () => {
96
112
  const messageText =
97
113
  typeof message === 'string' ? message : (message as any)?.description ?? (message as any)?.detail ?? '';
98
114
  expect(String(messageText)).toMatch(/Ant Design 全局消息/);
115
+ expect((doc?.methods?.render as any)?.completion?.requires).toContain('element');
116
+ expect((doc?.properties?.viewer as any)?.properties?.popover?.completion?.requires).toContain('element');
117
+ expect(
118
+ (doc?.properties?.libs as any)?.properties?.ReactDOM?.properties?.createRoot?.completion?.requires,
119
+ ).toContain('element');
99
120
  });
100
121
 
101
122
  it('should fallback to English when locale is not found', () => {
@@ -27,7 +27,10 @@ describe('Specific RunJSContext implementations', () => {
27
27
  const ctx: any = { model: { constructor: { name: 'JSColumnModel' } } };
28
28
  const doc = getRunJSDocFor(ctx as any, { version: 'v1' });
29
29
  expect(doc?.properties?.element).toBeTruthy();
30
- expect(doc?.properties?.element).toContain('ElementProxy');
30
+ const elementDoc: any = doc?.properties?.element;
31
+ expect(elementDoc?.detail).toContain('ElementProxy');
32
+ expect(elementDoc?.properties?.setAttribute).toBeTruthy();
33
+ expect(elementDoc?.properties?.querySelector).toBeTruthy();
31
34
  });
32
35
 
33
36
  it('should have record property in doc', () => {
@@ -68,7 +71,9 @@ describe('Specific RunJSContext implementations', () => {
68
71
  (ctx as any).defineProperty('api', { value: { auth: { locale: 'zh-CN' } } });
69
72
  const doc = getRunJSDocFor(ctx as any, { version: 'v1' });
70
73
  expect(doc?.label).toMatch(/JS 列/);
71
- expect(doc?.properties?.element).toContain('表格单元格');
74
+ const elementDoc: any = doc?.properties?.element;
75
+ expect(elementDoc?.description).toContain('表格单元格');
76
+ expect(elementDoc?.properties?.addEventListener).toBeTruthy();
72
77
  });
73
78
 
74
79
  it('should create instance successfully', () => {
@@ -162,6 +167,7 @@ describe('Specific RunJSContext implementations', () => {
162
167
  const ctx: any = { model: { constructor: { name: 'JSRecordActionModel' } } };
163
168
  const doc = getRunJSDocFor(ctx as any, { version: 'v1' });
164
169
  expect(doc?.properties?.record).toBeTruthy();
170
+ expect(doc?.properties?.element).toBeUndefined();
165
171
  });
166
172
 
167
173
  it('should have filterByTk property', () => {
@@ -184,6 +190,7 @@ describe('Specific RunJSContext implementations', () => {
184
190
  const ctx: any = { model: { constructor: { name: 'JSCollectionActionModel' } } };
185
191
  const doc = getRunJSDocFor(ctx as any, { version: 'v1' });
186
192
  expect(doc?.properties?.resource).toBeTruthy();
193
+ expect(doc?.properties?.element).toBeUndefined();
187
194
  });
188
195
 
189
196
  it('should support zh-CN locale', () => {
@@ -186,6 +186,7 @@ describe('RunJS Context Runtime Behavior', () => {
186
186
  'JSBlockModel',
187
187
  'JSFieldModel',
188
188
  'JSItemModel',
189
+ 'JSItemActionModel',
189
190
  'JSColumnModel',
190
191
  'FormJSFieldItemModel',
191
192
  'JSRecordActionModel',
@@ -237,6 +238,7 @@ describe('RunJS Context Runtime Behavior', () => {
237
238
  'JSBlockModel',
238
239
  'JSFieldModel',
239
240
  'JSItemModel',
241
+ 'JSItemActionModel',
240
242
  'JSColumnModel',
241
243
  'FormJSFieldItemModel',
242
244
  'JSRecordActionModel',
@@ -12,6 +12,10 @@ import { getRunJSDocFor } from '..';
12
12
  import { setupRunJSContexts } from '../runjs-context/setup';
13
13
  import { FlowContext } from '../flowContext';
14
14
 
15
+ function getRunJSDocText(doc: unknown) {
16
+ return typeof doc === 'string' ? doc : (doc as any)?.description ?? (doc as any)?.detail ?? '';
17
+ }
18
+
15
19
  describe('RunJS locales patch (engine doc)', () => {
16
20
  beforeAll(async () => {
17
21
  await setupRunJSContexts();
@@ -30,10 +34,7 @@ describe('RunJS locales patch (engine doc)', () => {
30
34
  (ctx as any).defineProperty('model', { value: { constructor: { name: 'JSBlockModel' } } });
31
35
  (ctx as any).defineProperty('api', { value: { auth: { locale: 'zh-CN' } } });
32
36
  const doc = getRunJSDocFor(ctx as any, { version: 'v1' });
33
- const message = doc?.properties?.message;
34
- const messageText =
35
- typeof message === 'string' ? message : (message as any)?.description ?? (message as any)?.detail ?? '';
36
- expect(String(messageText)).toMatch(/Ant Design 全局消息 API/);
37
- expect(String(doc?.methods?.t || '')).toMatch(/国际化函数/);
37
+ expect(String(getRunJSDocText(doc?.properties?.message))).toMatch(/Ant Design 全局消息 API/);
38
+ expect(String(getRunJSDocText(doc?.methods?.t))).toMatch(/国际化函数/);
38
39
  });
39
40
  });
@@ -112,6 +112,27 @@ describe('RunJS Snippets', () => {
112
112
  expect(multiScene?.scenes).toEqual(expect.arrayContaining(['detail', 'table']));
113
113
  expect(multiScene?.groups).toEqual(expect.arrayContaining(['scene/detail', 'scene/table']));
114
114
  });
115
+
116
+ it('should expose new style snippets for matching contexts', async () => {
117
+ const tableSnippets = await listSnippetsForContext('JSColumnRunJSContext', 'v1', 'zh-CN');
118
+ const fieldSnippets = await listSnippetsForContext('FormJSFieldItemRunJSContext', 'v1', 'zh-CN');
119
+ const detailEventSnippets = await listSnippetsForContext('DetailsItemModel', 'v1', 'zh-CN');
120
+ const tableEventSnippets = await listSnippetsForContext('TableColumnModel', 'v1', 'zh-CN');
121
+
122
+ const tableStyle = tableSnippets.find((s) => s.ref === 'scene/table/set-cell-style');
123
+ expect(tableStyle?.name).toBe('表格字段样式设置');
124
+ expect(tableStyle?.body).toContain('ctx.model.props.onCell');
125
+ expect(tableStyle?.scenes).toEqual(['tableFieldEvent']);
126
+
127
+ const fieldStyle = fieldSnippets.find((s) => s.ref === 'scene/detail/set-field-style');
128
+ expect(fieldStyle?.name).toBe('设置表单项/详情项样式');
129
+ expect(fieldStyle?.body).toContain('ctx.model.props.style');
130
+ expect(fieldStyle?.scenes).toEqual(expect.arrayContaining(['detailFieldEvent', 'formFieldEvent']));
131
+ expect(fieldStyle?.groups).toEqual(expect.arrayContaining(['scene/detail', 'scene/form']));
132
+
133
+ expect(detailEventSnippets.some((s) => s.ref === 'scene/detail/set-field-style')).toBe(true);
134
+ expect(tableEventSnippets.some((s) => s.ref === 'scene/table/set-cell-style')).toBe(true);
135
+ });
115
136
  });
116
137
 
117
138
  describe('New snippets', () => {