@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
@@ -22,28 +22,26 @@ describe('ViewNavigation', () => {
22
22
  pathname: '/admin/test',
23
23
  },
24
24
  };
25
+ // Mock window.location
26
+ Object.defineProperty(window, 'location', {
27
+ value: {
28
+ pathname: '/admin',
29
+ },
30
+ writable: true,
31
+ });
25
32
  });
26
33
 
27
34
  describe('changeTo', () => {
28
- it('should replace current view when viewStack is empty', () => {
29
- viewNavigation = new ViewNavigation(mockCtx, []);
30
-
31
- viewNavigation.changeTo({ viewUid: 'new-view' });
32
-
33
- expect(viewNavigation.viewStack).toEqual([{ viewUid: 'new-view' }]);
34
- expect(mockCtx.router.navigate).toHaveBeenCalledWith('/admin/new-view', { replace: true });
35
- });
36
-
37
- it('should replace last view in viewStack', () => {
35
+ it('should keep viewStack unchanged', () => {
38
36
  viewNavigation = new ViewNavigation(mockCtx, [{ viewUid: 'old-view' }]);
39
37
 
40
38
  viewNavigation.changeTo({ viewUid: 'new-view', tabUid: 'tab1' });
41
39
 
42
- expect(viewNavigation.viewStack).toEqual([{ viewUid: 'new-view', tabUid: 'tab1' }]);
40
+ expect(viewNavigation.viewStack).toEqual([{ viewUid: 'old-view' }]);
43
41
  expect(mockCtx.router.navigate).toHaveBeenCalledWith('/admin/new-view/tab/tab1', { replace: true });
44
42
  });
45
43
 
46
- it('should replace last view with complex parameters', () => {
44
+ it('should keep viewStack unchanged with complex parameters', () => {
47
45
  viewNavigation = new ViewNavigation(mockCtx, [{ viewUid: 'view1' }, { viewUid: 'view2', tabUid: 'tab1' }]);
48
46
 
49
47
  viewNavigation.changeTo({
@@ -53,10 +51,7 @@ describe('ViewNavigation', () => {
53
51
  sourceId: 'source1',
54
52
  });
55
53
 
56
- expect(viewNavigation.viewStack).toEqual([
57
- { viewUid: 'view1' },
58
- { viewUid: 'new-view', tabUid: 'new-tab', filterByTk: '123', sourceId: 'source1' },
59
- ]);
54
+ expect(viewNavigation.viewStack).toEqual([{ viewUid: 'view1' }, { viewUid: 'view2', tabUid: 'tab1' }]); // keep unchanged
60
55
  expect(mockCtx.router.navigate).toHaveBeenCalledWith(
61
56
  '/admin/view1/view/new-view/tab/new-tab/filterbytk/123/sourceid/source1',
62
57
  { replace: true },
@@ -68,13 +63,15 @@ describe('ViewNavigation', () => {
68
63
 
69
64
  viewNavigation.changeTo({ tabUid: 'new-tab' });
70
65
 
71
- expect(viewNavigation.viewStack).toEqual([{ viewUid: 'view1', tabUid: 'new-tab' }]);
66
+ expect(viewNavigation.viewStack).toEqual([{ viewUid: 'view1' }]);
67
+ expect(mockCtx.router.navigate).toHaveBeenCalledWith('/admin/view1/tab/new-tab', { replace: true });
72
68
  });
73
69
  });
74
70
 
75
71
  describe('navigateTo', () => {
76
72
  it('should navigate to new view', () => {
77
73
  viewNavigation = new ViewNavigation(mockCtx, [{ viewUid: 'current-view' }]);
74
+ window.location.pathname = '/admin/current-view';
78
75
 
79
76
  viewNavigation.navigateTo({ viewUid: 'new-view' });
80
77
 
@@ -85,20 +82,9 @@ describe('ViewNavigation', () => {
85
82
  expect(call[1]).toBeUndefined();
86
83
  });
87
84
 
88
- it('should navigate back when pathname is the same', () => {
89
- viewNavigation = new ViewNavigation(mockCtx, [{ viewUid: 'view1' }]);
90
- // set browser location to match the generated pathname
91
- window.history.pushState({}, '', '/admin/view1/view/view2');
92
-
93
- viewNavigation.navigateTo({ viewUid: 'view2' });
94
-
95
- // when same pathname, navigate(-1) to avoid "no reaction" UX
96
- expect(mockCtx.router.navigate).toHaveBeenCalledWith(-1);
97
- expect(viewNavigation.viewStack).toEqual([{ viewUid: 'view1' }]);
98
- });
99
-
100
85
  it('should navigate with complex parameters', () => {
101
86
  viewNavigation = new ViewNavigation(mockCtx, [{ viewUid: 'view1', tabUid: 'tab1' }]);
87
+ window.location.pathname = '/admin/view1/tab/tab1';
102
88
 
103
89
  viewNavigation.navigateTo({
104
90
  viewUid: 'view2',
@@ -116,25 +102,49 @@ describe('ViewNavigation', () => {
116
102
 
117
103
  it('should navigate from empty viewStack', () => {
118
104
  viewNavigation = new ViewNavigation(mockCtx, []);
105
+ window.location.pathname = '/admin';
119
106
 
120
107
  viewNavigation.navigateTo({ viewUid: 'first-view' });
121
108
 
122
- expect(viewNavigation.viewStack).toEqual([{ viewUid: 'first-view' }]);
109
+ expect(viewNavigation.viewStack).toEqual([]);
123
110
  const call = (mockCtx.router.navigate as any).mock.calls[0];
124
111
  expect(call[0]).toBe('/admin/first-view');
125
112
  expect(call[1]).toBeUndefined();
126
113
  });
114
+
115
+ it('should pass options to router.navigate', () => {
116
+ viewNavigation = new ViewNavigation(mockCtx, [{ viewUid: 'view1' }]);
117
+ window.location.pathname = '/admin/view1';
118
+
119
+ viewNavigation.navigateTo({ viewUid: 'view2' }, { replace: true });
120
+
121
+ expect(mockCtx.router.navigate).toHaveBeenCalledWith('/admin/view1/view/view2', { replace: true });
122
+ });
127
123
  });
128
124
 
129
125
  describe('back', () => {
130
- it('should call router navigate with -1', () => {
126
+ it('should navigate to parent path', () => {
127
+ viewNavigation = new ViewNavigation(mockCtx, [{ viewUid: 'view1' }, { viewUid: 'view2' }]);
128
+
129
+ viewNavigation.back();
130
+
131
+ expect(mockCtx.router.navigate).toHaveBeenCalledWith('/admin/view1', { replace: true });
132
+ });
133
+
134
+ it('should navigate to root if stack has only one item', () => {
131
135
  viewNavigation = new ViewNavigation(mockCtx, [{ viewUid: 'view1' }]);
132
- // set browser location to current stack pathname so back() triggers
133
- window.history.pushState({}, '', '/admin/view1');
134
136
 
135
137
  viewNavigation.back();
136
138
 
137
- expect(mockCtx.router.navigate).toHaveBeenCalledWith(-1);
139
+ expect(mockCtx.router.navigate).toHaveBeenCalledWith('/admin', { replace: true });
140
+ });
141
+
142
+ it('should navigate to root if stack is empty', () => {
143
+ viewNavigation = new ViewNavigation(mockCtx, []);
144
+
145
+ viewNavigation.back();
146
+
147
+ expect(mockCtx.router.navigate).toHaveBeenCalledWith('/admin', { replace: true });
138
148
  });
139
149
  });
140
150
  });
@@ -173,6 +183,16 @@ describe('generatePathnameFromViewParams', () => {
173
183
  );
174
184
  });
175
185
 
186
+ it('should encode object filterByTk as encoded key-value string', () => {
187
+ const path = generatePathnameFromViewParams([{ viewUid: 'xxx', filterByTk: { id: 1, tenant: 'ac' } }]);
188
+ expect(path).toBe('/admin/xxx/filterbytk/' + encodeURIComponent('id=1&tenant=ac'));
189
+ });
190
+
191
+ it('should omit filterByTk segment for empty string or when absent', () => {
192
+ expect(generatePathnameFromViewParams([{ viewUid: 'xxx' }])).toBe('/admin/xxx');
193
+ expect(generatePathnameFromViewParams([{ viewUid: 'xxx', filterByTk: '' }])).toBe('/admin/xxx');
194
+ });
195
+
176
196
  it('should match parsePathnameToViewParams test cases', () => {
177
197
  // Test cases from parsePathnameToViewParams.test.ts
178
198
  expect(generatePathnameFromViewParams([{ viewUid: 'xxx' }])).toBe('/admin/xxx');
@@ -0,0 +1,132 @@
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 { render } from '@testing-library/react';
12
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
13
+ import { useDialog } from '../useDialog';
14
+ import { FlowContext } from '../../flowContext';
15
+
16
+ // Mock dependencies
17
+ vi.mock('../provider', () => ({
18
+ FlowEngineProvider: ({ children }) => children,
19
+ }));
20
+
21
+ vi.mock('../FlowContextProvider', () => ({
22
+ FlowViewContextProvider: ({ children }) => children,
23
+ }));
24
+
25
+ vi.mock('../ViewScopedFlowEngine', () => ({
26
+ createViewScopedEngine: (engine) => ({
27
+ context: new FlowContext(),
28
+ unlinkFromStack: vi.fn(),
29
+ }),
30
+ }));
31
+
32
+ vi.mock('../utils/variablesParams', () => ({
33
+ createViewRecordResolveOnServer: vi.fn(),
34
+ getViewRecordFromParent: vi.fn(),
35
+ }));
36
+
37
+ vi.mock('../createViewMeta', () => ({
38
+ registerPopupVariable: vi.fn(),
39
+ }));
40
+
41
+ vi.mock('../DialogComponent', () => ({
42
+ default: ({ children }) => <div>{children}</div>,
43
+ }));
44
+
45
+ // Mock usePatchElement to return a mock close function
46
+ const mockCloseFunc = vi.fn();
47
+ const mockPatchElement = vi.fn(() => mockCloseFunc);
48
+ vi.mock('../usePatchElement', () => ({
49
+ default: () => [[], mockPatchElement],
50
+ }));
51
+
52
+ describe('useDialog - close/destroy logic', () => {
53
+ const createMockFlowContext = () => {
54
+ const ctx = new FlowContext();
55
+ ctx.engine = {
56
+ context: new FlowContext(),
57
+ };
58
+ return ctx;
59
+ };
60
+
61
+ beforeEach(() => {
62
+ vi.clearAllMocks();
63
+ });
64
+
65
+ const renderUseDialog = () => {
66
+ let api: any;
67
+ const TestComponent = () => {
68
+ const [dialogApi, contextHolder] = useDialog();
69
+ api = dialogApi;
70
+ return contextHolder as any;
71
+ };
72
+ render(<TestComponent />);
73
+ return api;
74
+ };
75
+
76
+ it('should call destroy (and thus closeFunc) when close is called without preventClose', () => {
77
+ const api = renderUseDialog();
78
+ const flowContext = createMockFlowContext();
79
+
80
+ const dialog = api.open({}, flowContext);
81
+
82
+ dialog.close();
83
+
84
+ expect(mockCloseFunc).toHaveBeenCalled();
85
+ });
86
+
87
+ it('should not call destroy (and thus closeFunc) when close is called with preventClose=true', () => {
88
+ const api = renderUseDialog();
89
+ const flowContext = createMockFlowContext();
90
+
91
+ const dialog = api.open({ preventClose: true }, flowContext);
92
+
93
+ dialog.close();
94
+
95
+ expect(mockCloseFunc).not.toHaveBeenCalled();
96
+ });
97
+
98
+ it('should call destroy (and thus closeFunc) when close is called with preventClose=true but force=true', () => {
99
+ const api = renderUseDialog();
100
+ const flowContext = createMockFlowContext();
101
+
102
+ const dialog = api.open({ preventClose: true }, flowContext);
103
+
104
+ dialog.close(undefined, true);
105
+
106
+ expect(mockCloseFunc).toHaveBeenCalled();
107
+ });
108
+
109
+ it('should delegate to navigation.back when triggerByRouter is true', () => {
110
+ const api = renderUseDialog();
111
+ const flowContext = createMockFlowContext();
112
+ const backMock = vi.fn();
113
+
114
+ const dialog = api.open(
115
+ {
116
+ triggerByRouter: true,
117
+ inputArgs: {
118
+ navigation: {
119
+ back: backMock,
120
+ },
121
+ },
122
+ },
123
+ flowContext,
124
+ );
125
+
126
+ dialog.close();
127
+
128
+ expect(backMock).toHaveBeenCalled();
129
+ // Should not call destroy directly, let router handle it
130
+ expect(mockCloseFunc).not.toHaveBeenCalled();
131
+ });
132
+ });
@@ -13,6 +13,8 @@ import type { RecordRef } from '../utils/serverContextParams';
13
13
  import type { Collection } from '../data-source';
14
14
  import type { FlowView } from './FlowView';
15
15
 
16
+ type PopupModelLike = { getStepParams?: (a: string, b: string) => any } | undefined;
17
+
16
18
  // 判断是否为普通对象(Plain Object),避免对类实例/代理等进行深度遍历
17
19
  function isPlainObject(val: any) {
18
20
  if (val === null || typeof val !== 'object') return false;
@@ -134,24 +136,48 @@ export function createViewMeta(ctx: FlowContext): PropertyMetaFactory {
134
136
  * - popup.resource:数据源信息(前端解析)
135
137
  * - popup.parent:上级弹窗(无限级,前端解析;不存在则禁用/为空)
136
138
  */
137
- export function createPopupMeta(ctx: FlowContext): PropertyMetaFactory {
139
+ export function createPopupMeta(ctx: FlowContext, anchorView?: FlowView): PropertyMetaFactory {
138
140
  const t = (k: string) => ctx.t(k);
139
141
 
140
- const getCurrentCollection = (): Collection | null => {
141
- try {
142
- const ref = inferViewRecordRef(ctx);
143
- if (!ref?.filterByTk) return null;
144
- const ds = ctx.dataSourceManager?.getDataSource?.(ref.dataSourceKey || 'main');
145
- return ds?.collectionManager?.getCollection?.(ref.collection) || null;
146
- } catch (_) {
147
- return null;
142
+ const isPopupView = (view?: FlowView): boolean => {
143
+ if (!view) return false;
144
+ const stack = Array.isArray(view.navigation?.viewStack) ? view.navigation.viewStack : [];
145
+ return stack.length >= 2;
146
+ };
147
+
148
+ const hasPopupNow = (): boolean => isPopupView(anchorView ?? ctx.view);
149
+
150
+ // 统一解析锚定视图下的 RecordRef,避免在设置弹窗等二级视图中被误导
151
+ const resolveRecordRef = async (flowCtx: FlowContext): Promise<RecordRef | undefined> => {
152
+ const view = anchorView ?? (flowCtx.view as any);
153
+ if (!view || !isPopupView(view)) return undefined;
154
+
155
+ const base = await buildPopupRuntime(flowCtx, view);
156
+ const res = base?.resource;
157
+ if (res?.collectionName && res.filterByTk != null) {
158
+ return {
159
+ collection: res.collectionName,
160
+ dataSourceKey: res.dataSourceKey || 'main',
161
+ filterByTk: res.filterByTk,
162
+ associationName: res.associationName,
163
+ sourceId: res.sourceId,
164
+ };
148
165
  }
166
+ return inferViewRecordRef(flowCtx);
167
+ };
168
+
169
+ const getCurrentCollection = async (): Promise<Collection | null> => {
170
+ const ref = await resolveRecordRef(ctx);
171
+ if (!ref?.collection) return null;
172
+ const ds = ctx.dataSourceManager?.getDataSource?.(ref.dataSourceKey || 'main');
173
+ return ds?.collectionManager?.getCollection?.(ref.collection) || null;
149
174
  };
150
175
 
151
176
  // 从视图堆栈推断 level 级父弹窗(level=1 上一层)
152
- const getParentRecordRef = async (level: number): Promise<RecordRef | undefined> => {
177
+ const getParentRecordRef = async (level: number, flowCtx?: FlowContext): Promise<RecordRef | undefined> => {
153
178
  try {
154
- const nav = ctx.view?.navigation;
179
+ const useCtx = flowCtx || ctx;
180
+ const nav = useCtx.view?.navigation;
155
181
  const stack = Array.isArray(nav?.viewStack) ? nav.viewStack : [];
156
182
  if (stack.length < 2 || level < 1) return undefined;
157
183
  const idx = stack.length - 1 - level;
@@ -159,12 +185,12 @@ export function createPopupMeta(ctx: FlowContext): PropertyMetaFactory {
159
185
  const parent = stack[idx];
160
186
  if (!parent?.viewUid) return undefined;
161
187
 
162
- let model: any = ctx.engine?.getModel?.(parent.viewUid);
163
- if (!model && typeof ctx.engine?.loadModel === 'function') {
188
+ let model = useCtx.engine?.getModel(parent.viewUid, true) as PopupModelLike;
189
+ if (!model) {
164
190
  try {
165
- model = await ctx.engine.loadModel({ uid: parent.viewUid });
191
+ model = (await useCtx.engine.loadModel({ uid: parent.viewUid })) as PopupModelLike;
166
192
  } catch (e) {
167
- console.warn('[FlowEngine] popup.getParentRecordRef loadModel failed:', e);
193
+ (useCtx.logger || ctx.logger)?.warn?.({ err: e }, '[FlowEngine] popup.getParentRecordRef loadModel failed');
168
194
  }
169
195
  }
170
196
  const params = model?.getStepParams?.('popupSettings', 'openView') || {};
@@ -172,16 +198,23 @@ export function createPopupMeta(ctx: FlowContext): PropertyMetaFactory {
172
198
  const dataSourceKey = params?.dataSourceKey || 'main';
173
199
  const filterByTk = parent?.filterByTk ?? parent?.sourceId;
174
200
  if (!collection || typeof filterByTk === 'undefined' || filterByTk === null) return undefined;
175
- return { collection, dataSourceKey, filterByTk };
201
+ const ref: RecordRef = {
202
+ collection,
203
+ dataSourceKey,
204
+ filterByTk,
205
+ sourceId: parent?.sourceId,
206
+ associationName: params?.associationName,
207
+ };
208
+ return ref;
176
209
  } catch (e) {
177
- console.warn('[FlowEngine] popup.getParentRecordRef failed:', e);
210
+ (flowCtx?.logger || ctx.logger)?.warn?.({ err: e }, '[FlowEngine] popup.getParentRecordRef failed');
178
211
  return undefined;
179
212
  }
180
213
  };
181
214
 
182
215
  const hasParentNow = (level: number): boolean => {
183
216
  try {
184
- const nav = ctx.view?.navigation;
217
+ const nav = (anchorView ?? ctx.view)?.navigation;
185
218
  const stack = Array.isArray(nav?.viewStack) ? nav.viewStack : [];
186
219
  return stack.length >= level + 1; // level=1 需要至少2层
187
220
  } catch (_) {
@@ -196,6 +229,7 @@ export function createPopupMeta(ctx: FlowContext): PropertyMetaFactory {
196
229
  title: t('Parent popup'),
197
230
  disabled: () => !hasParentNow(level),
198
231
  disabledReason: () => (!hasParentNow(level) ? t('No parent popup') : undefined),
232
+ hidden: () => !hasParentNow(level),
199
233
  properties: async () => {
200
234
  const parentRef = await getParentRecordRef(level);
201
235
  const props: Record<string, any> = {};
@@ -229,6 +263,7 @@ export function createPopupMeta(ctx: FlowContext): PropertyMetaFactory {
229
263
  properties: async () => ({
230
264
  dataSourceKey: { type: 'string', title: t('Data source key') },
231
265
  collectionName: { type: 'string', title: t('Collection name') },
266
+ associationName: { type: 'string', title: t('Association name') },
232
267
  filterByTk: { type: 'string', title: t('filterByTk') },
233
268
  sourceId: { type: 'string', title: t('sourceId') },
234
269
  }),
@@ -251,10 +286,49 @@ export function createPopupMeta(ctx: FlowContext): PropertyMetaFactory {
251
286
  const meta: PropertyMeta = {
252
287
  type: 'object',
253
288
  title: t('Current popup'),
254
- buildVariablesParams: (c) => {
255
- const ref = inferViewRecordRef(c);
256
- const inputArgs = (c?.view as any)?.inputArgs || {};
257
- const out: Record<string, any> = { record: ref };
289
+ disabled: () => !hasPopupNow(),
290
+ hidden: () => !hasPopupNow(),
291
+ buildVariablesParams: async (c) => {
292
+ if (!hasPopupNow()) return undefined;
293
+ const ref = await resolveRecordRef(c);
294
+ const inputArgs = c.view?.inputArgs;
295
+ type PopupVariableParams = {
296
+ record?: RecordRef;
297
+ sourceRecord?: RecordRef;
298
+ parent?: PopupVariableParams;
299
+ };
300
+ const params: PopupVariableParams = {};
301
+ if (ref) {
302
+ const merged: RecordRef = { ...ref };
303
+ if (!merged.associationName && inputArgs?.associationName) {
304
+ merged.associationName = inputArgs.associationName;
305
+ }
306
+ if (typeof merged.sourceId === 'undefined' && typeof inputArgs?.sourceId !== 'undefined') {
307
+ merged.sourceId = inputArgs?.sourceId;
308
+ }
309
+ params.record = merged;
310
+ }
311
+
312
+ // 构建 parent 链(用于服务端解析 ctx.popup.parent[.parent...].record.*)
313
+ try {
314
+ const nav = c.view?.navigation;
315
+ const stack = Array.isArray(nav?.viewStack) ? nav.viewStack : [];
316
+ if (stack.length >= 2) {
317
+ let cur: Record<string, any> = params;
318
+ let level = 1;
319
+ let parentRef = await getParentRecordRef(level, c);
320
+ while (parentRef) {
321
+ if (!cur.parent) cur.parent = {};
322
+ cur.parent.record = parentRef;
323
+ cur = cur.parent;
324
+ level += 1;
325
+ parentRef = await getParentRecordRef(level, c);
326
+ }
327
+ }
328
+ } catch (err) {
329
+ c.logger?.debug?.({ err }, '[FlowEngine] buildVariablesParams: build parent-chain failed');
330
+ }
331
+
258
332
  try {
259
333
  const srcId = inputArgs?.sourceId;
260
334
  const assoc: string | undefined = inputArgs?.associationName;
@@ -263,32 +337,58 @@ export function createPopupMeta(ctx: FlowContext): PropertyMetaFactory {
263
337
  // associationName 形如 `posts.comments`,父级集合为 `posts`
264
338
  const parentCollectionName = String(assoc).split('.')[0];
265
339
  if (parentCollectionName) {
266
- out.sourceRecord = {
340
+ params.sourceRecord = {
267
341
  collection: parentCollectionName,
268
342
  dataSourceKey: dsKey,
269
343
  filterByTk: srcId,
270
344
  };
271
345
  }
272
346
  }
273
- } catch (_) {
274
- // 忽略异常,保持 record 正常返回
347
+ } catch (err) {
348
+ c.logger?.debug?.({ err }, '[FlowEngine] buildVariablesParams: infer sourceRecord failed');
275
349
  }
276
- return out;
350
+ return params;
277
351
  },
278
352
  properties: async () => {
279
353
  const props: Record<string, any> = {};
280
354
  // 当前弹窗 UID(纯前端变量)
281
355
  props.uid = { type: 'string', title: t('Popup uid') };
282
- const base = await buildRecordMeta(getCurrentCollection, t('Current popup record'), (c) =>
283
- inferViewRecordRef(c),
284
- );
285
- if (base) props.record = base;
356
+ // 基于锚定视图计算“当前弹窗记录”的集合与 RecordRef
357
+ const recordFactory: PropertyMetaFactory = async () => {
358
+ const col = await getCurrentCollection();
359
+ if (!col) return null;
360
+ return await buildRecordMeta(
361
+ () => col,
362
+ t('Current popup record'),
363
+ (c) => resolveRecordRef(c),
364
+ );
365
+ };
366
+ recordFactory.title = t('Current popup record');
367
+ recordFactory.hasChildren = true;
368
+ props.record = recordFactory;
286
369
  // 当 view.inputArgs 带有 sourceId + associationName 时,提供“上级记录”变量(基于 sourceId 推断)
287
370
  try {
288
- const inputArgs = (ctx.view as any)?.inputArgs || {};
371
+ const inputArgs = ctx.view?.inputArgs;
289
372
  const srcId = inputArgs?.sourceId;
290
- const assoc: string | undefined = inputArgs?.associationName;
291
- const dsKey: string = inputArgs?.dataSourceKey || 'main';
373
+ let assoc: string | undefined = inputArgs?.associationName;
374
+ let dsKey: string = inputArgs?.dataSourceKey || 'main';
375
+
376
+ // 兜底:若 associationName 缺失或不含“.”,尝试从当前视图模型的 openView 参数推断
377
+ if (!assoc || typeof assoc !== 'string' || !assoc.includes('.')) {
378
+ const nav = ctx.view?.navigation;
379
+ const stack = Array.isArray(nav?.viewStack) ? nav.viewStack : [];
380
+ const last = stack?.[stack.length - 1];
381
+ if (last?.viewUid) {
382
+ let model = ctx?.engine?.getModel(last.viewUid, true) as PopupModelLike;
383
+ if (!model) {
384
+ model = (await ctx.engine.loadModel({ uid: last.viewUid })) as PopupModelLike;
385
+ }
386
+ const p = model?.getStepParams?.('popupSettings', 'openView') || {};
387
+ assoc = p?.associationName || assoc;
388
+ dsKey = p?.dataSourceKey || dsKey;
389
+ }
390
+ }
391
+
292
392
  if (srcId != null && srcId !== '' && assoc && typeof assoc === 'string') {
293
393
  const parentCollectionName = String(assoc).includes('.') ? String(assoc).split('.')[0] : undefined;
294
394
  if (parentCollectionName) {
@@ -310,8 +410,8 @@ export function createPopupMeta(ctx: FlowContext): PropertyMetaFactory {
310
410
  }
311
411
  }
312
412
  }
313
- } catch (_) {
314
- // ignore
413
+ } catch (err) {
414
+ ctx.logger?.debug?.({ err }, '[FlowEngine] popup.properties: build sourceRecord failed');
315
415
  }
316
416
  const resourceMeta: PropertyMeta = {
317
417
  type: 'object',
@@ -319,6 +419,7 @@ export function createPopupMeta(ctx: FlowContext): PropertyMetaFactory {
319
419
  properties: async () => ({
320
420
  dataSourceKey: { type: 'string', title: t('Data source key') },
321
421
  collectionName: { type: 'string', title: t('Collection name') },
422
+ associationName: { type: 'string', title: t('Association name') },
322
423
  filterByTk: { type: 'string', title: t('filterByTk') },
323
424
  sourceId: { type: 'string', title: t('sourceId') },
324
425
  }),
@@ -346,6 +447,7 @@ export function createPopupMeta(ctx: FlowContext): PropertyMetaFactory {
346
447
  interface PopupNodeResource {
347
448
  dataSourceKey: string;
348
449
  collectionName?: string;
450
+ associationName?: string;
349
451
  filterByTk?: any;
350
452
  sourceId?: any;
351
453
  }
@@ -356,15 +458,40 @@ interface PopupNode {
356
458
  parent?: PopupNode;
357
459
  }
358
460
 
359
- export async function buildPopupRuntime(ctx: FlowContext, view: FlowView): Promise<PopupNode> {
461
+ export async function buildPopupRuntime(ctx: FlowContext, view: FlowView): Promise<PopupNode | undefined> {
360
462
  const nav = view?.navigation;
361
463
  const stack = Array.isArray(nav?.viewStack) ? nav.viewStack : [];
464
+
465
+ const openerUids = view?.inputArgs?.openerUids;
466
+ const hasOpener = Array.isArray(openerUids) && openerUids.length > 0;
467
+ const hasStackPopup = stack.length >= 2;
468
+ const isPopup = hasStackPopup || hasOpener;
469
+ if (!isPopup) return undefined;
470
+
471
+ // 当没有 navigation 堆栈时,退回当前视图的 inputArgs 作为单节点弹窗上下文
472
+ if (!stack.length) {
473
+ const args = view?.inputArgs || {};
474
+ const hasAny =
475
+ args.collectionName || args.filterByTk != null || args.sourceId != null || args.associationName || args.viewUid;
476
+ if (!hasAny) return undefined;
477
+ return {
478
+ uid: args.viewUid,
479
+ resource: {
480
+ dataSourceKey: args.dataSourceKey || 'main',
481
+ collectionName: args.collectionName,
482
+ associationName: args.associationName,
483
+ filterByTk: args.filterByTk,
484
+ sourceId: args.sourceId,
485
+ },
486
+ };
487
+ }
488
+
362
489
  const buildNode = async (idx: number): Promise<PopupNode | undefined> => {
363
490
  if (idx < 0 || !stack[idx]?.viewUid) return undefined;
364
491
  const viewUid = stack[idx].viewUid;
365
- let model: any = ctx.engine?.getModel?.(viewUid);
366
- if (!model && typeof ctx.engine?.loadModel === 'function') {
367
- model = await ctx.engine?.loadModel({ uid: viewUid });
492
+ let model = ctx.engine?.getModel(viewUid, true) as PopupModelLike;
493
+ if (!model) {
494
+ model = (await ctx.engine?.loadModel({ uid: viewUid })) as PopupModelLike;
368
495
  }
369
496
  const p = model?.getStepParams?.('popupSettings', 'openView') || {};
370
497
  const collectionName = p?.collectionName;
@@ -374,6 +501,7 @@ export async function buildPopupRuntime(ctx: FlowContext, view: FlowView): Promi
374
501
  resource: {
375
502
  dataSourceKey,
376
503
  collectionName,
504
+ associationName: p?.associationName,
377
505
  filterByTk: stack[idx]?.filterByTk,
378
506
  sourceId: stack[idx]?.sourceId,
379
507
  },
@@ -390,13 +518,22 @@ export async function buildPopupRuntime(ctx: FlowContext, view: FlowView): Promi
390
518
  * 在视图上下文中注册 popup 变量(统一消除重复)
391
519
  */
392
520
  export function registerPopupVariable(ctx: FlowContext, view: FlowView) {
521
+ // - 顶层 record / sourceRecord 及其子字段
522
+ // - 任意层级 parent.parent... 下的 record / sourceRecord 及其子字段
523
+ const POPUP_SERVER_PATH_RE =
524
+ /^(?:record|sourceRecord)(?:\.|$)|^parent(?:\.parent)*(?:\.(?:record|sourceRecord))(?:\.|$)/;
393
525
  // 始终注册 popup 变量:
394
526
  // - 若当前视图无可推断记录,仅在元信息中不呈现 record 字段;
395
527
  // - 但仍可依据 navigation 推断并展示上级弹窗信息。
396
528
  ctx.defineProperty('popup', {
397
529
  get: async () => buildPopupRuntime(ctx, view),
398
- meta: createPopupMeta(ctx),
399
- resolveOnServer: (p: string) =>
400
- p === 'record' || p?.startsWith('record.') || p === 'sourceRecord' || p?.startsWith('sourceRecord.'),
530
+ meta: createPopupMeta(ctx, view),
531
+ resolveOnServer: (p: string) => {
532
+ try {
533
+ return !!p && POPUP_SERVER_PATH_RE.test(p);
534
+ } catch (_) {
535
+ return false;
536
+ }
537
+ },
401
538
  });
402
539
  }