@nocobase/flow-engine 2.1.0-alpha.1 → 2.1.0-alpha.10

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 (283) hide show
  1. package/LICENSE +201 -661
  2. package/README.md +79 -10
  3. package/lib/BlockScopedFlowEngine.js +0 -1
  4. package/lib/FlowDefinition.d.ts +2 -0
  5. package/lib/JSRunner.d.ts +15 -0
  6. package/lib/JSRunner.js +82 -7
  7. package/lib/ViewScopedFlowEngine.js +8 -1
  8. package/lib/acl/Acl.js +13 -3
  9. package/lib/components/FlowContextSelector.js +155 -10
  10. package/lib/components/MobilePopup.js +6 -5
  11. package/lib/components/dnd/gridDragPlanner.d.ts +1 -0
  12. package/lib/components/dnd/gridDragPlanner.js +59 -3
  13. package/lib/components/settings/wrappers/component/SwitchWithTitle.js +2 -1
  14. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +76 -15
  15. package/lib/components/settings/wrappers/contextual/FlowsContextMenu.js +24 -4
  16. package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +21 -3
  17. package/lib/components/subModel/AddSubModelButton.js +16 -1
  18. package/lib/components/subModel/utils.js +2 -2
  19. package/lib/components/variables/VariableInput.js +9 -4
  20. package/lib/components/variables/VariableTag.js +46 -39
  21. package/lib/components/variables/utils.d.ts +7 -0
  22. package/lib/components/variables/utils.js +42 -2
  23. package/lib/data-source/index.d.ts +7 -27
  24. package/lib/data-source/index.js +84 -51
  25. package/lib/executor/FlowExecutor.d.ts +2 -1
  26. package/lib/executor/FlowExecutor.js +190 -26
  27. package/lib/flowContext.d.ts +230 -7
  28. package/lib/flowContext.js +2270 -148
  29. package/lib/flowEngine.d.ts +160 -1
  30. package/lib/flowEngine.js +383 -26
  31. package/lib/flowI18n.js +6 -4
  32. package/lib/flowSettings.d.ts +14 -6
  33. package/lib/flowSettings.js +51 -17
  34. package/lib/index.d.ts +7 -1
  35. package/lib/index.js +21 -0
  36. package/lib/lazy-helper.d.ts +14 -0
  37. package/lib/lazy-helper.js +71 -0
  38. package/lib/locale/en-US.json +9 -2
  39. package/lib/locale/index.d.ts +14 -0
  40. package/lib/locale/zh-CN.json +8 -1
  41. package/lib/models/CollectionFieldModel.d.ts +1 -0
  42. package/lib/models/CollectionFieldModel.js +3 -2
  43. package/lib/models/flowModel.d.ts +7 -0
  44. package/lib/models/flowModel.js +83 -8
  45. package/lib/provider.js +7 -6
  46. package/lib/resources/baseRecordResource.d.ts +5 -0
  47. package/lib/resources/baseRecordResource.js +24 -0
  48. package/lib/resources/multiRecordResource.d.ts +1 -0
  49. package/lib/resources/multiRecordResource.js +11 -4
  50. package/lib/resources/singleRecordResource.js +2 -0
  51. package/lib/resources/sqlResource.d.ts +4 -3
  52. package/lib/resources/sqlResource.js +8 -3
  53. package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +12 -2
  54. package/lib/runjs-context/contexts/JSBlockRunJSContext.js +2 -2
  55. package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.d.ts +16 -0
  56. package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.js +125 -0
  57. package/lib/runjs-context/contexts/JSItemRunJSContext.js +12 -2
  58. package/lib/runjs-context/contexts/base.js +706 -41
  59. package/lib/runjs-context/contributions.d.ts +33 -0
  60. package/lib/runjs-context/contributions.js +88 -0
  61. package/lib/runjs-context/helpers.js +12 -1
  62. package/lib/runjs-context/registry.d.ts +1 -1
  63. package/lib/runjs-context/setup.js +22 -9
  64. package/lib/runjs-context/snippets/global/api-request.snippet.js +3 -3
  65. package/lib/runjs-context/snippets/global/import-esm.snippet.js +2 -3
  66. package/lib/runjs-context/snippets/global/query-selector.snippet.js +8 -3
  67. package/lib/runjs-context/snippets/global/require-amd.snippet.js +1 -1
  68. package/lib/runjs-context/snippets/index.d.ts +11 -1
  69. package/lib/runjs-context/snippets/index.js +61 -40
  70. package/lib/runjs-context/snippets/scene/block/add-event-listener.snippet.js +10 -7
  71. package/lib/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.js +3 -3
  72. package/lib/runjs-context/snippets/scene/block/chartjs-bar.snippet.js +2 -2
  73. package/lib/runjs-context/snippets/scene/block/echarts-init.snippet.js +2 -2
  74. package/lib/runjs-context/snippets/scene/block/render-iframe.snippet.js +2 -2
  75. package/lib/runjs-context/snippets/scene/block/render-react.snippet.js +1 -1
  76. package/lib/runjs-context/snippets/scene/block/render-statistics.snippet.js +1 -1
  77. package/lib/runjs-context/snippets/scene/block/render-timeline.snippet.js +1 -1
  78. package/lib/runjs-context/snippets/scene/block/resource-example.snippet.js +5 -5
  79. package/lib/runjs-context/snippets/scene/block/three-users-orbit.snippet.js +6 -6
  80. package/lib/runjs-context/snippets/scene/block/vue-component.snippet.js +3 -4
  81. package/lib/runjs-context/snippets/scene/detail/color-by-value.snippet.js +1 -1
  82. package/lib/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.js +20 -3
  83. package/lib/runjs-context/snippets/scene/detail/format-number.snippet.js +1 -1
  84. package/lib/runjs-context/snippets/scene/detail/innerHTML-value.snippet.js +1 -1
  85. package/lib/runjs-context/snippets/scene/detail/percentage-bar.snippet.js +3 -3
  86. package/lib/runjs-context/snippets/scene/detail/relative-time.snippet.js +3 -3
  87. package/lib/runjs-context/snippets/scene/detail/status-tag.snippet.js +2 -2
  88. package/lib/runjs-context/snippets/scene/form/cascade-select.snippet.js +1 -1
  89. package/lib/runjs-context/snippets/scene/form/render-basic.snippet.js +2 -2
  90. package/lib/runjs-context/snippets/scene/table/cell-open-dialog.snippet.js +6 -3
  91. package/lib/runjs-context/snippets/scene/table/concat-fields.snippet.js +3 -1
  92. package/lib/runjsLibs.d.ts +28 -0
  93. package/lib/runjsLibs.js +532 -0
  94. package/lib/scheduler/ModelOperationScheduler.d.ts +7 -1
  95. package/lib/scheduler/ModelOperationScheduler.js +28 -23
  96. package/lib/types.d.ts +63 -1
  97. package/lib/utils/associationObjectVariable.d.ts +2 -2
  98. package/lib/utils/createCollectionContextMeta.js +1 -0
  99. package/lib/utils/createEphemeralContext.js +2 -2
  100. package/lib/utils/dateVariable.d.ts +16 -0
  101. package/lib/utils/dateVariable.js +380 -0
  102. package/lib/utils/exceptions.d.ts +7 -0
  103. package/lib/utils/exceptions.js +10 -0
  104. package/lib/utils/index.d.ts +8 -3
  105. package/lib/utils/index.js +49 -0
  106. package/lib/utils/params-resolvers.js +16 -9
  107. package/lib/utils/parsePathnameToViewParams.js +1 -1
  108. package/lib/utils/resolveModuleUrl.d.ts +58 -0
  109. package/lib/utils/resolveModuleUrl.js +65 -0
  110. package/lib/utils/resolveRunJSObjectValues.d.ts +16 -0
  111. package/lib/utils/resolveRunJSObjectValues.js +61 -0
  112. package/lib/utils/runjsModuleLoader.d.ts +58 -0
  113. package/lib/utils/runjsModuleLoader.js +422 -0
  114. package/lib/utils/runjsTemplateCompat.d.ts +35 -0
  115. package/lib/utils/runjsTemplateCompat.js +743 -0
  116. package/lib/utils/runjsValue.d.ts +29 -0
  117. package/lib/utils/runjsValue.js +275 -0
  118. package/lib/utils/safeGlobals.d.ts +18 -8
  119. package/lib/utils/safeGlobals.js +164 -17
  120. package/lib/utils/schema-utils.d.ts +17 -1
  121. package/lib/utils/schema-utils.js +80 -0
  122. package/lib/views/FlowView.d.ts +7 -1
  123. package/lib/views/createViewMeta.d.ts +0 -7
  124. package/lib/views/createViewMeta.js +19 -70
  125. package/lib/views/index.d.ts +1 -2
  126. package/lib/views/index.js +4 -3
  127. package/lib/views/runViewBeforeClose.d.ts +10 -0
  128. package/lib/views/runViewBeforeClose.js +45 -0
  129. package/lib/views/useDialog.d.ts +2 -1
  130. package/lib/views/useDialog.js +28 -6
  131. package/lib/views/useDrawer.d.ts +2 -1
  132. package/lib/views/useDrawer.js +27 -5
  133. package/lib/views/usePage.d.ts +6 -1
  134. package/lib/views/usePage.js +53 -9
  135. package/lib/views/usePopover.js +4 -1
  136. package/lib/views/viewEvents.d.ts +17 -0
  137. package/lib/views/viewEvents.js +90 -0
  138. package/package.json +5 -5
  139. package/src/BlockScopedFlowEngine.ts +2 -5
  140. package/src/JSRunner.ts +111 -5
  141. package/src/ViewScopedFlowEngine.ts +8 -0
  142. package/src/__tests__/JSRunner.test.ts +91 -1
  143. package/src/__tests__/createViewMeta.popup.test.ts +62 -1
  144. package/src/__tests__/flowContext.test.ts +693 -1
  145. package/src/__tests__/flowEngine.dataSourceDirty.test.ts +63 -0
  146. package/src/__tests__/flowEngine.modelLoaders.test.ts +245 -0
  147. package/src/__tests__/flowModel.openView.navigation.test.ts +28 -0
  148. package/src/__tests__/flowRunJSContextDefine.test.ts +63 -0
  149. package/src/__tests__/flowRuntimeContext.test.ts +2 -1
  150. package/src/__tests__/flowSettings.open.test.tsx +123 -19
  151. package/src/__tests__/flowSettings.test.ts +94 -15
  152. package/src/__tests__/provider.test.tsx +0 -5
  153. package/src/__tests__/renderHiddenInConfig.test.tsx +6 -6
  154. package/src/__tests__/runjsContext.test.ts +23 -7
  155. package/src/__tests__/runjsContextImplementations.test.ts +34 -3
  156. package/src/__tests__/runjsContextRuntime.test.ts +3 -3
  157. package/src/__tests__/runjsContributions.test.ts +89 -0
  158. package/src/__tests__/runjsExternalLibs.test.ts +242 -0
  159. package/src/__tests__/runjsLibsLazyLoading.test.ts +44 -0
  160. package/src/__tests__/runjsLocales.test.ts +4 -1
  161. package/src/__tests__/runjsPreprocessDefault.test.ts +72 -0
  162. package/src/__tests__/runjsRuntimeFeatures.test.ts +166 -0
  163. package/src/__tests__/runjsSnippets.test.ts +40 -3
  164. package/src/__tests__/viewScopedFlowEngine.test.ts +3 -3
  165. package/src/acl/Acl.tsx +3 -3
  166. package/src/components/FlowContextSelector.tsx +208 -12
  167. package/src/components/MobilePopup.tsx +4 -2
  168. package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +3 -3
  169. package/src/components/__tests__/gridDragPlanner.test.ts +229 -1
  170. package/src/components/dnd/gridDragPlanner.ts +68 -2
  171. package/src/components/settings/wrappers/component/SwitchWithTitle.tsx +2 -1
  172. package/src/components/settings/wrappers/component/__tests__/InlineControls.test.tsx +74 -0
  173. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +109 -16
  174. package/src/components/settings/wrappers/contextual/FlowsContextMenu.tsx +41 -7
  175. package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +31 -4
  176. package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +157 -5
  177. package/src/components/subModel/AddSubModelButton.tsx +17 -1
  178. package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +142 -32
  179. package/src/components/subModel/utils.ts +1 -1
  180. package/src/components/variables/VariableInput.tsx +12 -4
  181. package/src/components/variables/VariableTag.tsx +54 -45
  182. package/src/components/variables/__tests__/FlowContextSelector.test.tsx +260 -3
  183. package/src/components/variables/__tests__/VariableTag.test.tsx +50 -0
  184. package/src/components/variables/__tests__/utils.test.ts +81 -3
  185. package/src/components/variables/utils.ts +67 -6
  186. package/src/data-source/index.ts +88 -110
  187. package/src/executor/FlowExecutor.ts +230 -28
  188. package/src/executor/__tests__/flowExecutor.test.ts +123 -0
  189. package/src/flowContext.ts +2989 -212
  190. package/src/flowEngine.ts +427 -22
  191. package/src/flowI18n.ts +7 -5
  192. package/src/flowSettings.ts +58 -18
  193. package/src/index.ts +14 -1
  194. package/src/lazy-helper.tsx +57 -0
  195. package/src/locale/en-US.json +9 -2
  196. package/src/locale/zh-CN.json +8 -1
  197. package/src/models/CollectionFieldModel.tsx +3 -1
  198. package/src/models/__tests__/dispatchEvent.when.test.ts +768 -0
  199. package/src/models/__tests__/flowModel.clone.test.ts +416 -0
  200. package/src/models/__tests__/flowModel.test.ts +20 -4
  201. package/src/models/flowModel.tsx +112 -7
  202. package/src/provider.tsx +9 -7
  203. package/src/resources/__tests__/multiRecordResource.test.ts +44 -0
  204. package/src/resources/__tests__/sqlResource.test.ts +60 -0
  205. package/src/resources/baseRecordResource.ts +31 -0
  206. package/src/resources/multiRecordResource.ts +11 -4
  207. package/src/resources/singleRecordResource.ts +3 -0
  208. package/src/resources/sqlResource.ts +11 -6
  209. package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +10 -0
  210. package/src/runjs-context/contexts/JSBlockRunJSContext.ts +6 -2
  211. package/src/runjs-context/contexts/JSEditableFieldRunJSContext.ts +106 -0
  212. package/src/runjs-context/contexts/JSItemRunJSContext.ts +10 -0
  213. package/src/runjs-context/contexts/base.ts +715 -44
  214. package/src/runjs-context/contributions.ts +88 -0
  215. package/src/runjs-context/helpers.ts +11 -1
  216. package/src/runjs-context/registry.ts +1 -1
  217. package/src/runjs-context/setup.ts +24 -9
  218. package/src/runjs-context/snippets/global/api-request.snippet.ts +3 -3
  219. package/src/runjs-context/snippets/global/import-esm.snippet.ts +2 -3
  220. package/src/runjs-context/snippets/global/query-selector.snippet.ts +8 -3
  221. package/src/runjs-context/snippets/global/require-amd.snippet.ts +1 -1
  222. package/src/runjs-context/snippets/index.ts +75 -41
  223. package/src/runjs-context/snippets/scene/block/add-event-listener.snippet.ts +11 -13
  224. package/src/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.ts +3 -3
  225. package/src/runjs-context/snippets/scene/block/chartjs-bar.snippet.ts +2 -2
  226. package/src/runjs-context/snippets/scene/block/echarts-init.snippet.ts +2 -2
  227. package/src/runjs-context/snippets/scene/block/render-iframe.snippet.ts +2 -2
  228. package/src/runjs-context/snippets/scene/block/render-react.snippet.ts +1 -1
  229. package/src/runjs-context/snippets/scene/block/render-statistics.snippet.ts +1 -1
  230. package/src/runjs-context/snippets/scene/block/render-timeline.snippet.ts +1 -1
  231. package/src/runjs-context/snippets/scene/block/resource-example.snippet.ts +6 -11
  232. package/src/runjs-context/snippets/scene/block/three-users-orbit.snippet.ts +6 -6
  233. package/src/runjs-context/snippets/scene/block/vue-component.snippet.ts +3 -4
  234. package/src/runjs-context/snippets/scene/detail/color-by-value.snippet.ts +1 -1
  235. package/src/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.ts +20 -3
  236. package/src/runjs-context/snippets/scene/detail/format-number.snippet.ts +1 -1
  237. package/src/runjs-context/snippets/scene/detail/innerHTML-value.snippet.ts +1 -1
  238. package/src/runjs-context/snippets/scene/detail/percentage-bar.snippet.ts +3 -3
  239. package/src/runjs-context/snippets/scene/detail/relative-time.snippet.ts +3 -3
  240. package/src/runjs-context/snippets/scene/detail/status-tag.snippet.ts +2 -2
  241. package/src/runjs-context/snippets/scene/form/cascade-select.snippet.ts +1 -1
  242. package/src/runjs-context/snippets/scene/form/render-basic.snippet.ts +3 -8
  243. package/src/runjs-context/snippets/scene/table/cell-open-dialog.snippet.ts +6 -3
  244. package/src/runjs-context/snippets/scene/table/concat-fields.snippet.ts +3 -1
  245. package/src/runjsLibs.ts +622 -0
  246. package/src/scheduler/ModelOperationScheduler.ts +41 -24
  247. package/src/types.ts +86 -1
  248. package/src/utils/__tests__/dateVariable.test.ts +101 -0
  249. package/src/utils/__tests__/params-resolvers.test.ts +40 -0
  250. package/src/utils/__tests__/parsePathnameToViewParams.test.ts +7 -0
  251. package/src/utils/__tests__/runjsRequireAsyncAutoWhitelist.test.ts +38 -0
  252. package/src/utils/__tests__/runjsTemplateCompat.test.ts +159 -0
  253. package/src/utils/__tests__/runjsValue.test.ts +44 -0
  254. package/src/utils/__tests__/safeGlobals.test.ts +57 -2
  255. package/src/utils/__tests__/utils.test.ts +157 -0
  256. package/src/utils/associationObjectVariable.ts +2 -2
  257. package/src/utils/createCollectionContextMeta.ts +1 -0
  258. package/src/utils/createEphemeralContext.ts +5 -4
  259. package/src/utils/dateVariable.ts +397 -0
  260. package/src/utils/exceptions.ts +11 -0
  261. package/src/utils/index.ts +38 -3
  262. package/src/utils/params-resolvers.ts +23 -9
  263. package/src/utils/parsePathnameToViewParams.ts +2 -2
  264. package/src/utils/resolveModuleUrl.ts +91 -0
  265. package/src/utils/resolveRunJSObjectValues.ts +46 -0
  266. package/src/utils/runjsModuleLoader.ts +553 -0
  267. package/src/utils/runjsTemplateCompat.ts +828 -0
  268. package/src/utils/runjsValue.ts +287 -0
  269. package/src/utils/safeGlobals.ts +188 -17
  270. package/src/utils/schema-utils.ts +109 -1
  271. package/src/views/FlowView.tsx +11 -1
  272. package/src/views/__tests__/FlowView.usePage.test.tsx +54 -1
  273. package/src/views/__tests__/runViewBeforeClose.test.ts +30 -0
  274. package/src/views/__tests__/useDialog.closeDestroy.test.tsx +44 -16
  275. package/src/views/__tests__/viewEvents.resolveOpenerEngine.test.ts +28 -0
  276. package/src/views/createViewMeta.ts +22 -75
  277. package/src/views/index.tsx +1 -2
  278. package/src/views/runViewBeforeClose.ts +19 -0
  279. package/src/views/useDialog.tsx +34 -5
  280. package/src/views/useDrawer.tsx +33 -4
  281. package/src/views/usePage.tsx +63 -8
  282. package/src/views/usePopover.tsx +4 -1
  283. package/src/views/viewEvents.ts +55 -0
@@ -13,7 +13,7 @@ import { render, act, waitFor, screen } from '@testing-library/react';
13
13
  import { FlowEngine } from '../../flowEngine';
14
14
  import { FlowEngineProvider } from '../../provider';
15
15
  import { FlowViewer } from '../FlowView';
16
- import { usePage } from '../usePage';
16
+ import { usePage, GLOBAL_EMBED_CONTAINER_ID } from '../usePage';
17
17
  import { App, ConfigProvider } from 'antd';
18
18
 
19
19
  describe('FlowViewer zIndex with usePage', () => {
@@ -130,4 +130,57 @@ describe('FlowViewer zIndex with usePage', () => {
130
130
 
131
131
  unmount();
132
132
  });
133
+
134
+ it('replaces previous embed view when using global #nocobase-embed-container target', async () => {
135
+ let api: { open: (config: any, flowContext: any) => any } | undefined;
136
+
137
+ function TestApp({ onReady }: { onReady: (page: any) => void }) {
138
+ const [page, pageHolder] = usePage() as [{ open: (config: any, flowContext: any) => any }, React.ReactNode];
139
+
140
+ React.useEffect(() => {
141
+ onReady(page);
142
+ }, [page, onReady]);
143
+
144
+ return <>{pageHolder}</>;
145
+ }
146
+
147
+ const Wrapper: React.FC<{ onReady: (page: any) => void }> = ({ onReady }) => (
148
+ <ConfigProvider>
149
+ <App>
150
+ <FlowEngineProvider engine={engine}>
151
+ <TestApp onReady={onReady} />
152
+ </FlowEngineProvider>
153
+ </App>
154
+ </ConfigProvider>
155
+ );
156
+
157
+ const target = document.createElement('div');
158
+ target.id = GLOBAL_EMBED_CONTAINER_ID;
159
+ document.body.appendChild(target);
160
+
161
+ const { unmount } = render(
162
+ <Wrapper
163
+ onReady={(page) => {
164
+ api = page;
165
+ }}
166
+ />,
167
+ );
168
+
169
+ await waitFor(() => expect(api).toBeDefined());
170
+
171
+ await act(async () => {
172
+ api!.open({ target, content: <div data-testid="page1">Page 1</div> }, engine.context);
173
+ });
174
+ await waitFor(() => expect(screen.getByTestId('page1')).toBeInTheDocument());
175
+
176
+ // Opening page2 into the global embed container should destroy page1 (replace behavior).
177
+ await act(async () => {
178
+ api!.open({ target, content: <div data-testid="page2">Page 2</div> }, engine.context);
179
+ });
180
+ await waitFor(() => expect(screen.getByTestId('page2')).toBeInTheDocument());
181
+ expect(screen.queryByTestId('page1')).not.toBeInTheDocument();
182
+
183
+ unmount();
184
+ document.body.removeChild(target);
185
+ });
133
186
  });
@@ -0,0 +1,30 @@
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 { runViewBeforeClose } from '../runViewBeforeClose';
12
+
13
+ describe('runViewBeforeClose', () => {
14
+ it('returns true when no beforeClose handler is configured', async () => {
15
+ await expect(runViewBeforeClose({} as any, { force: false })).resolves.toBe(true);
16
+ });
17
+
18
+ it('skips beforeClose handler for force close', async () => {
19
+ const beforeClose = vi.fn();
20
+
21
+ await expect(runViewBeforeClose({ beforeClose } as any, { force: true })).resolves.toBe(true);
22
+ expect(beforeClose).not.toHaveBeenCalled();
23
+ });
24
+
25
+ it('returns false when beforeClose handler blocks the close', async () => {
26
+ const beforeClose = vi.fn().mockResolvedValue(false);
27
+
28
+ await expect(runViewBeforeClose({ beforeClose } as any, { force: false })).resolves.toBe(false);
29
+ });
30
+ });
@@ -14,30 +14,29 @@ import { useDialog } from '../useDialog';
14
14
  import { FlowContext } from '../../flowContext';
15
15
 
16
16
  // Mock dependencies
17
- vi.mock('../provider', () => ({
17
+ vi.mock('../../provider', () => ({
18
18
  FlowEngineProvider: ({ children }) => children,
19
19
  }));
20
20
 
21
- vi.mock('../FlowContextProvider', () => ({
21
+ vi.mock('../../FlowContextProvider', () => ({
22
22
  FlowViewContextProvider: ({ children }) => children,
23
23
  }));
24
24
 
25
- vi.mock('../ViewScopedFlowEngine', () => ({
25
+ vi.mock('../../ViewScopedFlowEngine', () => ({
26
26
  createViewScopedEngine: (engine) => ({
27
27
  context: new FlowContext(),
28
28
  unlinkFromStack: vi.fn(),
29
+ setDestroyView: vi.fn(),
30
+ // mimic real view stack linkage: previousEngine points to the last engine in chain
31
+ previousEngine: (engine as any)?.nextEngine || engine,
29
32
  }),
30
33
  }));
31
34
 
32
- vi.mock('../utils/variablesParams', () => ({
35
+ vi.mock('../../utils/variablesParams', () => ({
33
36
  createViewRecordResolveOnServer: vi.fn(),
34
37
  getViewRecordFromParent: vi.fn(),
35
38
  }));
36
39
 
37
- vi.mock('../createViewMeta', () => ({
38
- registerPopupVariable: vi.fn(),
39
- }));
40
-
41
40
  vi.mock('../DialogComponent', () => ({
42
41
  default: ({ children }) => <div>{children}</div>,
43
42
  }));
@@ -52,8 +51,12 @@ vi.mock('../usePatchElement', () => ({
52
51
  describe('useDialog - close/destroy logic', () => {
53
52
  const createMockFlowContext = () => {
54
53
  const ctx = new FlowContext();
54
+ ctx.defineMethod('t', (key: string) => key);
55
55
  ctx.engine = {
56
56
  context: new FlowContext(),
57
+ emitter: {
58
+ emit: vi.fn(),
59
+ },
57
60
  };
58
61
  return ctx;
59
62
  };
@@ -73,40 +76,40 @@ describe('useDialog - close/destroy logic', () => {
73
76
  return api;
74
77
  };
75
78
 
76
- it('should call destroy (and thus closeFunc) when close is called without preventClose', () => {
79
+ it('should call destroy (and thus closeFunc) when close is called without preventClose', async () => {
77
80
  const api = renderUseDialog();
78
81
  const flowContext = createMockFlowContext();
79
82
 
80
83
  const dialog = api.open({}, flowContext);
81
84
 
82
- dialog.close();
85
+ await dialog.close();
83
86
 
84
87
  expect(mockCloseFunc).toHaveBeenCalled();
85
88
  });
86
89
 
87
- it('should not call destroy (and thus closeFunc) when close is called with preventClose=true', () => {
90
+ it('should not call destroy (and thus closeFunc) when close is called with preventClose=true', async () => {
88
91
  const api = renderUseDialog();
89
92
  const flowContext = createMockFlowContext();
90
93
 
91
94
  const dialog = api.open({ preventClose: true }, flowContext);
92
95
 
93
- dialog.close();
96
+ await dialog.close();
94
97
 
95
98
  expect(mockCloseFunc).not.toHaveBeenCalled();
96
99
  });
97
100
 
98
- it('should call destroy (and thus closeFunc) when close is called with preventClose=true but force=true', () => {
101
+ it('should call destroy (and thus closeFunc) when close is called with preventClose=true but force=true', async () => {
99
102
  const api = renderUseDialog();
100
103
  const flowContext = createMockFlowContext();
101
104
 
102
105
  const dialog = api.open({ preventClose: true }, flowContext);
103
106
 
104
- dialog.close(undefined, true);
107
+ await dialog.close(undefined, true);
105
108
 
106
109
  expect(mockCloseFunc).toHaveBeenCalled();
107
110
  });
108
111
 
109
- it('should delegate to navigation.back when triggerByRouter is true', () => {
112
+ it('should delegate to navigation.back when triggerByRouter is true', async () => {
110
113
  const api = renderUseDialog();
111
114
  const flowContext = createMockFlowContext();
112
115
  const backMock = vi.fn();
@@ -123,10 +126,35 @@ describe('useDialog - close/destroy logic', () => {
123
126
  flowContext,
124
127
  );
125
128
 
126
- dialog.close();
129
+ await dialog.close();
127
130
 
128
131
  expect(backMock).toHaveBeenCalled();
129
132
  // Should not call destroy directly, let router handle it
130
133
  expect(mockCloseFunc).not.toHaveBeenCalled();
131
134
  });
135
+
136
+ it('should emit view activated event on opener engine', async () => {
137
+ const api = renderUseDialog();
138
+ const flowContext = createMockFlowContext();
139
+ const emitSpy = flowContext.engine.emitter.emit;
140
+
141
+ const dialog = api.open({ inputArgs: { viewUid: 'child-view' } }, flowContext);
142
+
143
+ await dialog.close();
144
+ expect(emitSpy).toHaveBeenCalledWith('view:activated', expect.anything());
145
+ });
146
+
147
+ it('should emit view events on immediate opener engine (previousEngine) when present', async () => {
148
+ const api = renderUseDialog();
149
+ const flowContext = createMockFlowContext();
150
+ const rootEmitSpy = flowContext.engine.emitter.emit;
151
+ const openerEmitSpy = vi.fn();
152
+ (flowContext.engine as any).nextEngine = { emitter: { emit: openerEmitSpy }, __NOCOBASE_ENGINE_SCOPE__: 'view' };
153
+
154
+ const dialog = api.open({ inputArgs: { viewUid: 'child-view' } }, flowContext);
155
+
156
+ await dialog.close();
157
+ expect(openerEmitSpy).toHaveBeenCalledWith('view:activated', expect.anything());
158
+ expect(rootEmitSpy).not.toHaveBeenCalledWith('view:activated', expect.anything());
159
+ });
132
160
  });
@@ -0,0 +1,28 @@
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 } from 'vitest';
11
+ import { FlowEngine } from '../../flowEngine';
12
+ import { createViewScopedEngine } from '../../ViewScopedFlowEngine';
13
+ import { resolveOpenerEngine } from '../viewEvents';
14
+
15
+ describe('viewEvents.resolveOpenerEngine', () => {
16
+ it('prefers the parent view engine even when it is not the stack tail (cached page scenario)', () => {
17
+ const root = new FlowEngine();
18
+ const pageA = createViewScopedEngine(root);
19
+ createViewScopedEngine(root); // pageB appended after pageA
20
+
21
+ // Open a dialog from pageA while another kept-alive view exists after it.
22
+ // view scoped engines always link to the tail, so the dialog's previousEngine will be pageB.
23
+ const dialog = createViewScopedEngine(pageA);
24
+
25
+ const opener = resolveOpenerEngine(pageA, dialog);
26
+ expect(opener).toBe(pageA);
27
+ });
28
+ });
@@ -70,66 +70,6 @@ function makeMetaFromValue(value: any, title?: string, seen?: WeakSet<any>): any
70
70
  return { type: 'any', title };
71
71
  }
72
72
 
73
- /**
74
- * Create a meta factory for ctx.view that includes:
75
- * - buildVariablesParams: { record } via inferRecordRef
76
- * - properties.record: full collection meta via buildRecordMeta
77
- * - type/preventClose/inputArgs/navigation fields for better variable selection UX
78
- */
79
- export function createViewMeta(ctx: FlowContext): PropertyMetaFactory {
80
- const viewTitle = ctx.t('当前视图');
81
- const factory: PropertyMetaFactory = async () => {
82
- const view = ctx.view;
83
- return {
84
- type: 'object',
85
- title: ctx.t('当前视图'),
86
- buildVariablesParams: (c) => {
87
- const params = inferViewRecordRef(c);
88
- if (params) {
89
- return {
90
- record: params,
91
- };
92
- }
93
- return undefined;
94
- },
95
- properties: async () => {
96
- const props: Record<string, any> = {};
97
- // 仅当能推断到当前记录引用时,才暴露“当前视图记录”,避免出现空子菜单
98
- const refNow = inferViewRecordRef(ctx);
99
- if (refNow && refNow.collection) {
100
- const recordFactory: PropertyMetaFactory = async () => {
101
- try {
102
- const ref = inferViewRecordRef(ctx);
103
- if (!ref?.collection) return null;
104
- const dsKey = ref.dataSourceKey || 'main';
105
- const ds = ctx.dataSourceManager?.getDataSource?.(dsKey);
106
- const col = ds?.collectionManager?.getCollection?.(ref.collection);
107
- if (!col) return null;
108
- return (await buildRecordMeta(
109
- () => col,
110
- ctx.t('当前视图记录'),
111
- (c) => inferViewRecordRef(c),
112
- )) as PropertyMeta;
113
- } catch (e) {
114
- return null;
115
- }
116
- };
117
- recordFactory.title = ctx.t('当前视图记录');
118
- recordFactory.hasChildren = true;
119
- props.record = recordFactory;
120
- }
121
- props.type = { type: 'string', title: ctx.t?.('类型') || '类型' };
122
- props.preventClose = { type: 'boolean', title: ctx.t?.('是否允许关闭') || '是否允许关闭' };
123
- props.inputArgs = makeMetaFromValue(view?.inputArgs, ctx.t?.('输入参数') || '输入参数');
124
- return props;
125
- },
126
- } as PropertyMeta;
127
- };
128
- // 设置工厂函数的 title,让未加载前的占位标题就是“当前视图”
129
- factory.title = viewTitle;
130
- return factory;
131
- }
132
-
133
73
  /**
134
74
  * 为 ctx.popup 构建元信息:
135
75
  * - popup.record:当前弹窗记录(服务端解析)
@@ -142,14 +82,16 @@ export function createPopupMeta(ctx: FlowContext, anchorView?: FlowView): Proper
142
82
  const isPopupView = (view?: FlowView): boolean => {
143
83
  if (!view) return false;
144
84
  const stack = Array.isArray(view.navigation?.viewStack) ? view.navigation.viewStack : [];
145
- return stack.length >= 2;
85
+ const openerUids = view?.inputArgs?.openerUids;
86
+ const hasOpener = Array.isArray(openerUids) && openerUids.length > 0;
87
+ return stack.length >= 2 || hasOpener;
146
88
  };
147
89
 
148
90
  const hasPopupNow = (): boolean => isPopupView(anchorView ?? ctx.view);
149
91
 
150
92
  // 统一解析锚定视图下的 RecordRef,避免在设置弹窗等二级视图中被误导
151
93
  const resolveRecordRef = async (flowCtx: FlowContext): Promise<RecordRef | undefined> => {
152
- const view = anchorView ?? (flowCtx.view as any);
94
+ const view = anchorView ?? flowCtx.view;
153
95
  if (!view || !isPopupView(view)) return undefined;
154
96
 
155
97
  const base = await buildPopupRuntime(flowCtx, view);
@@ -353,19 +295,24 @@ export function createPopupMeta(ctx: FlowContext, anchorView?: FlowView): Proper
353
295
  const props: Record<string, any> = {};
354
296
  // 当前弹窗 UID(纯前端变量)
355
297
  props.uid = { type: 'string', title: t('Popup uid') };
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;
298
+ // 仅当存在 filterByTk(可推断具体记录)时才提供“当前弹窗记录”变量;
299
+ // 对于新增/选择类弹窗(无 filterByTk),不应展示该变量以避免误导。
300
+ const recordRef = await resolveRecordRef(ctx);
301
+ if (recordRef) {
302
+ // 基于锚定视图计算“当前弹窗记录”的集合与 RecordRef
303
+ const recordFactory: PropertyMetaFactory = async () => {
304
+ const col = await getCurrentCollection();
305
+ if (!col) return null;
306
+ return await buildRecordMeta(
307
+ () => col,
308
+ t('Current popup record'),
309
+ (c) => resolveRecordRef(c),
310
+ );
311
+ };
312
+ recordFactory.title = t('Current popup record');
313
+ recordFactory.hasChildren = true;
314
+ props.record = recordFactory;
315
+ }
369
316
  // 当 view.inputArgs 带有 sourceId + associationName 时,提供“上级记录”变量(基于 sourceId 推断)
370
317
  try {
371
318
  const inputArgs = ctx.view?.inputArgs;
@@ -9,7 +9,6 @@
9
9
 
10
10
  export { useDialog } from './useDialog';
11
11
  export { useDrawer } from './useDrawer';
12
- export { usePage } from './usePage';
12
+ export { usePage, GLOBAL_EMBED_CONTAINER_ID, EMBED_REPLACING_DATA_KEY } from './usePage';
13
13
  export { usePopover } from './usePopover';
14
14
  export { ViewNavigation } from './ViewNavigation';
15
- export { createViewMeta } from './createViewMeta';
@@ -0,0 +1,19 @@
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 type { FlowView, FlowViewBeforeClosePayload } from './FlowView';
11
+
12
+ export async function runViewBeforeClose(view: FlowView, payload: FlowViewBeforeClosePayload): Promise<boolean> {
13
+ if (payload.force) {
14
+ return true;
15
+ }
16
+
17
+ const result = await view.beforeClose?.(payload);
18
+ return result !== false;
19
+ }
@@ -15,9 +15,11 @@ import { FlowViewContextProvider } from '../FlowContextProvider';
15
15
  import { registerPopupVariable } from './createViewMeta';
16
16
  import DialogComponent from './DialogComponent';
17
17
  import usePatchElement from './usePatchElement';
18
+ import { VIEW_ACTIVATED_EVENT, bumpViewActivatedVersion, resolveOpenerEngine } from './viewEvents';
18
19
  import { FlowEngineProvider } from '../provider';
19
20
  import { createViewScopedEngine } from '../ViewScopedFlowEngine';
20
21
  import { createViewRecordResolveOnServer, getViewRecordFromParent } from '../utils/variablesParams';
22
+ import { runViewBeforeClose } from './runViewBeforeClose';
21
23
 
22
24
  let uuid = 0;
23
25
 
@@ -25,6 +27,7 @@ export function useDialog() {
25
27
  const holderRef = React.useRef(null);
26
28
 
27
29
  const open = (config, flowContext) => {
30
+ const parentEngine = flowContext?.engine;
28
31
  uuid += 1;
29
32
  const dialogRef = React.createRef<{
30
33
  destroy: () => void;
@@ -77,6 +80,8 @@ export function useDialog() {
77
80
  const ctx = new FlowContext();
78
81
  // 为当前视图创建作用域引擎(隔离实例与缓存)
79
82
  const scopedEngine = createViewScopedEngine(flowContext.engine);
83
+ const openerEngine = resolveOpenerEngine(parentEngine, scopedEngine);
84
+
80
85
  ctx.defineProperty('engine', { value: scopedEngine });
81
86
  ctx.addDelegate(scopedEngine.context);
82
87
  if (config.inheritContext !== false) {
@@ -85,32 +90,48 @@ export function useDialog() {
85
90
  ctx.addDelegate(flowContext.engine.context);
86
91
  }
87
92
 
93
+ // 幂等保护:防止 FlowPage 路由清理时二次调用 destroy
94
+ let destroyed = false;
95
+
88
96
  // 构造 currentDialog 实例
89
97
  const currentDialog = {
90
98
  type: 'dialog' as const,
91
99
  inputArgs: config.inputArgs || {},
92
100
  preventClose: !!config.preventClose,
101
+ beforeClose: undefined,
93
102
  destroy: (result?: any) => {
103
+ if (destroyed) return;
104
+ destroyed = true;
94
105
  config.onClose?.();
95
106
  dialogRef.current?.destroy();
96
107
  closeFunc?.();
97
108
  resolvePromise?.(result);
109
+ // Notify opener view that it becomes active again.
110
+ const openerEmitter = openerEngine?.emitter;
111
+ bumpViewActivatedVersion(openerEmitter);
112
+ openerEmitter?.emit?.(VIEW_ACTIVATED_EVENT, { type: 'dialog', viewUid: currentDialog?.inputArgs?.viewUid });
98
113
  // 关闭时修正 previous/next 指针
99
114
  scopedEngine.unlinkFromStack();
100
115
  },
101
116
  update: (newConfig) => dialogRef.current?.update(newConfig),
102
- close: (result?: any, force?: boolean) => {
117
+ close: async (result?: any, force?: boolean) => {
103
118
  if (config.preventClose && !force) {
104
- return;
119
+ return false;
120
+ }
121
+
122
+ const shouldClose = await runViewBeforeClose(currentDialog, { result, force });
123
+ if (!shouldClose) {
124
+ return false;
105
125
  }
106
126
 
107
127
  if (config.triggerByRouter && config.inputArgs?.navigation?.back) {
108
128
  // 交由路由系统来销毁当前视图
109
129
  config.inputArgs.navigation.back();
110
- return;
130
+ return true;
111
131
  }
112
132
 
113
133
  currentDialog.destroy(result);
134
+ return true;
114
135
  },
115
136
  Footer: FooterComponent,
116
137
  Header: HeaderComponent,
@@ -130,9 +151,17 @@ export function useDialog() {
130
151
 
131
152
  ctx.defineProperty('view', {
132
153
  get: () => currentDialog,
133
- // meta: createViewMeta(ctx),
134
154
  resolveOnServer: createViewRecordResolveOnServer(ctx, () => getViewRecordFromParent(flowContext, ctx)),
135
155
  });
156
+ // 注册视图销毁回调,供外部(如 afterSuccess)通过引擎栈遍历来关闭多层弹窗。
157
+ // 对路由触发的弹窗:先 navigation.back() 清理 URL(replace 方式),再 destroy() 立即移除元素;
158
+ // 对非路由弹窗:直接 destroy()。destroy() 已做幂等保护,FlowPage 后续清理不会重复执行。
159
+ scopedEngine.setDestroyView(() => {
160
+ if (config.triggerByRouter && config.inputArgs?.navigation?.back) {
161
+ config.inputArgs.navigation.back();
162
+ }
163
+ currentDialog.destroy();
164
+ });
136
165
  // 顶层 popup 变量:弹窗记录/数据源/上级弹窗链(去重封装)
137
166
  registerPopupVariable(ctx, currentDialog);
138
167
  // 内部组件,在 Provider 内部计算 content
@@ -164,9 +193,9 @@ export function useDialog() {
164
193
  className="nb-dialog-overflow-hidden"
165
194
  ref={dialogRef}
166
195
  hidden={config.inputArgs?.hidden?.value}
167
- {...config}
168
196
  footer={currentFooter}
169
197
  header={currentHeader}
198
+ {...config}
170
199
  onCancel={() => {
171
200
  currentDialog.close(config.result);
172
201
  }}
@@ -15,9 +15,11 @@ import { FlowViewContextProvider } from '../FlowContextProvider';
15
15
  import { registerPopupVariable } from './createViewMeta';
16
16
  import DrawerComponent from './DrawerComponent';
17
17
  import usePatchElement from './usePatchElement';
18
+ import { VIEW_ACTIVATED_EVENT, bumpViewActivatedVersion, resolveOpenerEngine } from './viewEvents';
18
19
  import { FlowEngineProvider } from '../provider';
19
20
  import { createViewScopedEngine } from '../ViewScopedFlowEngine';
20
21
  import { createViewRecordResolveOnServer, getViewRecordFromParent } from '../utils/variablesParams';
22
+ import { runViewBeforeClose } from './runViewBeforeClose';
21
23
 
22
24
  export function useDrawer() {
23
25
  const holderRef = React.useRef(null);
@@ -54,6 +56,7 @@ export function useDrawer() {
54
56
  RenderNestedDrawer.displayName = 'RenderNestedDrawer';
55
57
 
56
58
  const open = (config, flowContext: FlowEngineContext) => {
59
+ const parentEngine = flowContext.engine;
57
60
  const drawerRef = React.createRef<{
58
61
  destroy: () => void;
59
62
  update: (config: any) => void;
@@ -105,6 +108,8 @@ export function useDrawer() {
105
108
  const ctx = new FlowContext();
106
109
  // 为当前视图创建作用域引擎(隔离实例与缓存)
107
110
  const scopedEngine = createViewScopedEngine(flowContext.engine);
111
+ const openerEngine = resolveOpenerEngine(parentEngine, scopedEngine);
112
+
108
113
  // 先将引擎暴露给视图上下文,再按需继承父上下文
109
114
  ctx.defineProperty('engine', { value: scopedEngine });
110
115
  ctx.addDelegate(scopedEngine.context);
@@ -114,32 +119,48 @@ export function useDrawer() {
114
119
  ctx.addDelegate(flowContext.engine.context);
115
120
  }
116
121
 
122
+ // 幂等保护:防止 FlowPage 路由清理时二次调用 destroy
123
+ let destroyed = false;
124
+
117
125
  // 构造 currentDrawer 实例
118
126
  const currentDrawer = {
119
127
  type: 'drawer' as const,
120
128
  inputArgs: config.inputArgs || {},
121
129
  preventClose: !!config.preventClose,
130
+ beforeClose: undefined,
122
131
  destroy: (result?: any) => {
132
+ if (destroyed) return;
133
+ destroyed = true;
123
134
  config.onClose?.();
124
135
  drawerRef.current?.destroy();
125
136
  closeFunc?.();
126
137
  resolvePromise?.(result);
138
+ // Notify opener view that it becomes active again.
139
+ const openerEmitter = openerEngine?.emitter;
140
+ bumpViewActivatedVersion(openerEmitter);
141
+ openerEmitter?.emit?.(VIEW_ACTIVATED_EVENT, { type: 'drawer', viewUid: currentDrawer?.inputArgs?.viewUid });
127
142
  // 关闭时修正 previous/next 指针
128
143
  scopedEngine.unlinkFromStack();
129
144
  },
130
145
  update: (newConfig) => drawerRef.current?.update(newConfig),
131
- close: (result?: any, force?: boolean) => {
146
+ close: async (result?: any, force?: boolean) => {
132
147
  if (config.preventClose && !force) {
133
- return;
148
+ return false;
149
+ }
150
+
151
+ const shouldClose = await runViewBeforeClose(currentDrawer, { result, force });
152
+ if (!shouldClose) {
153
+ return false;
134
154
  }
135
155
 
136
156
  if (config.triggerByRouter && config.inputArgs?.navigation?.back) {
137
157
  // 交由路由系统来销毁当前视图
138
158
  config.inputArgs.navigation.back();
139
- return;
159
+ return true;
140
160
  }
141
161
 
142
162
  currentDrawer.destroy(result);
163
+ return true;
143
164
  },
144
165
  Footer: FooterComponent,
145
166
  Header: HeaderComponent,
@@ -159,9 +180,17 @@ export function useDrawer() {
159
180
 
160
181
  ctx.defineProperty('view', {
161
182
  get: () => currentDrawer,
162
- // meta: createViewMeta(ctx),
163
183
  resolveOnServer: createViewRecordResolveOnServer(ctx, () => getViewRecordFromParent(flowContext, ctx)),
164
184
  });
185
+ // 注册视图销毁回调,供外部(如 afterSuccess)通过引擎栈遍历来关闭多层弹窗。
186
+ // 对路由触发的弹窗:先 navigation.back() 清理 URL(replace 方式),再 destroy() 立即移除元素;
187
+ // 对非路由弹窗:直接 destroy()。destroy() 已做幂等保护,FlowPage 后续清理不会重复执行。
188
+ scopedEngine.setDestroyView(() => {
189
+ if (config.triggerByRouter && config.inputArgs?.navigation?.back) {
190
+ config.inputArgs.navigation.back();
191
+ }
192
+ currentDrawer.destroy();
193
+ });
165
194
  // 顶层 popup 变量:弹窗记录/数据源/上级弹窗链(去重封装)
166
195
  registerPopupVariable(ctx, currentDrawer);
167
196