@nocobase/flow-engine 2.0.0-beta.9 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/lib/BlockScopedFlowEngine.js +0 -1
  2. package/lib/FlowDefinition.d.ts +2 -0
  3. package/lib/JSRunner.d.ts +6 -0
  4. package/lib/JSRunner.js +32 -2
  5. package/lib/ViewScopedFlowEngine.js +3 -0
  6. package/lib/acl/Acl.js +13 -3
  7. package/lib/components/FlowContextSelector.js +155 -10
  8. package/lib/components/settings/wrappers/component/SwitchWithTitle.js +2 -1
  9. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +76 -15
  10. package/lib/components/settings/wrappers/contextual/FlowsContextMenu.js +24 -4
  11. package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +5 -1
  12. package/lib/components/variables/VariableInput.js +9 -4
  13. package/lib/components/variables/VariableTag.js +46 -39
  14. package/lib/components/variables/utils.d.ts +7 -0
  15. package/lib/components/variables/utils.js +42 -2
  16. package/lib/data-source/index.d.ts +7 -27
  17. package/lib/data-source/index.js +81 -51
  18. package/lib/executor/FlowExecutor.d.ts +2 -1
  19. package/lib/executor/FlowExecutor.js +163 -22
  20. package/lib/flowContext.d.ts +230 -7
  21. package/lib/flowContext.js +2267 -148
  22. package/lib/flowEngine.d.ts +21 -0
  23. package/lib/flowEngine.js +56 -8
  24. package/lib/flowI18n.js +6 -4
  25. package/lib/flowSettings.js +17 -11
  26. package/lib/index.d.ts +7 -1
  27. package/lib/index.js +21 -0
  28. package/lib/locale/en-US.json +9 -2
  29. package/lib/locale/index.d.ts +14 -0
  30. package/lib/locale/zh-CN.json +8 -1
  31. package/lib/models/CollectionFieldModel.d.ts +1 -0
  32. package/lib/models/CollectionFieldModel.js +3 -2
  33. package/lib/models/flowModel.js +12 -1
  34. package/lib/provider.js +5 -5
  35. package/lib/resources/baseRecordResource.d.ts +5 -0
  36. package/lib/resources/baseRecordResource.js +24 -0
  37. package/lib/resources/multiRecordResource.d.ts +1 -0
  38. package/lib/resources/multiRecordResource.js +11 -4
  39. package/lib/resources/singleRecordResource.js +2 -0
  40. package/lib/resources/sqlResource.d.ts +4 -3
  41. package/lib/resources/sqlResource.js +8 -3
  42. package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +12 -2
  43. package/lib/runjs-context/contexts/JSBlockRunJSContext.js +2 -2
  44. package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.d.ts +16 -0
  45. package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.js +125 -0
  46. package/lib/runjs-context/contexts/JSItemRunJSContext.js +12 -2
  47. package/lib/runjs-context/contexts/base.js +706 -41
  48. package/lib/runjs-context/contributions.d.ts +33 -0
  49. package/lib/runjs-context/contributions.js +88 -0
  50. package/lib/runjs-context/helpers.js +12 -1
  51. package/lib/runjs-context/setup.js +6 -0
  52. package/lib/runjs-context/snippets/global/api-request.snippet.js +3 -3
  53. package/lib/runjs-context/snippets/global/import-esm.snippet.js +2 -3
  54. package/lib/runjs-context/snippets/global/query-selector.snippet.js +8 -3
  55. package/lib/runjs-context/snippets/global/require-amd.snippet.js +1 -1
  56. package/lib/runjs-context/snippets/index.d.ts +11 -1
  57. package/lib/runjs-context/snippets/index.js +61 -40
  58. package/lib/runjs-context/snippets/scene/block/add-event-listener.snippet.js +10 -7
  59. package/lib/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.js +3 -3
  60. package/lib/runjs-context/snippets/scene/block/chartjs-bar.snippet.js +2 -2
  61. package/lib/runjs-context/snippets/scene/block/echarts-init.snippet.js +2 -2
  62. package/lib/runjs-context/snippets/scene/block/render-iframe.snippet.js +2 -2
  63. package/lib/runjs-context/snippets/scene/block/render-react.snippet.js +1 -1
  64. package/lib/runjs-context/snippets/scene/block/render-statistics.snippet.js +1 -1
  65. package/lib/runjs-context/snippets/scene/block/render-timeline.snippet.js +1 -1
  66. package/lib/runjs-context/snippets/scene/block/resource-example.snippet.js +5 -5
  67. package/lib/runjs-context/snippets/scene/block/three-users-orbit.snippet.js +6 -6
  68. package/lib/runjs-context/snippets/scene/block/vue-component.snippet.js +3 -4
  69. package/lib/runjs-context/snippets/scene/detail/color-by-value.snippet.js +1 -1
  70. package/lib/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.js +20 -3
  71. package/lib/runjs-context/snippets/scene/detail/format-number.snippet.js +1 -1
  72. package/lib/runjs-context/snippets/scene/detail/innerHTML-value.snippet.js +1 -1
  73. package/lib/runjs-context/snippets/scene/detail/percentage-bar.snippet.js +3 -3
  74. package/lib/runjs-context/snippets/scene/detail/relative-time.snippet.js +3 -3
  75. package/lib/runjs-context/snippets/scene/detail/status-tag.snippet.js +2 -2
  76. package/lib/runjs-context/snippets/scene/form/cascade-select.snippet.js +1 -1
  77. package/lib/runjs-context/snippets/scene/form/render-basic.snippet.js +2 -2
  78. package/lib/runjs-context/snippets/scene/table/cell-open-dialog.snippet.js +6 -3
  79. package/lib/runjs-context/snippets/scene/table/concat-fields.snippet.js +3 -1
  80. package/lib/runjsLibs.d.ts +28 -0
  81. package/lib/runjsLibs.js +532 -0
  82. package/lib/scheduler/ModelOperationScheduler.d.ts +2 -0
  83. package/lib/scheduler/ModelOperationScheduler.js +25 -21
  84. package/lib/types.d.ts +27 -0
  85. package/lib/utils/associationObjectVariable.d.ts +2 -2
  86. package/lib/utils/createCollectionContextMeta.js +1 -0
  87. package/lib/utils/createEphemeralContext.js +2 -2
  88. package/lib/utils/dateVariable.d.ts +16 -0
  89. package/lib/utils/dateVariable.js +380 -0
  90. package/lib/utils/exceptions.d.ts +7 -0
  91. package/lib/utils/exceptions.js +10 -0
  92. package/lib/utils/index.d.ts +8 -3
  93. package/lib/utils/index.js +45 -0
  94. package/lib/utils/params-resolvers.js +16 -9
  95. package/lib/utils/resolveModuleUrl.d.ts +58 -0
  96. package/lib/utils/resolveModuleUrl.js +65 -0
  97. package/lib/utils/resolveRunJSObjectValues.d.ts +16 -0
  98. package/lib/utils/resolveRunJSObjectValues.js +61 -0
  99. package/lib/utils/runjsModuleLoader.d.ts +58 -0
  100. package/lib/utils/runjsModuleLoader.js +422 -0
  101. package/lib/utils/runjsTemplateCompat.d.ts +35 -0
  102. package/lib/utils/runjsTemplateCompat.js +743 -0
  103. package/lib/utils/runjsValue.d.ts +29 -0
  104. package/lib/utils/runjsValue.js +275 -0
  105. package/lib/utils/safeGlobals.d.ts +18 -8
  106. package/lib/utils/safeGlobals.js +164 -17
  107. package/lib/utils/schema-utils.d.ts +10 -0
  108. package/lib/utils/schema-utils.js +61 -0
  109. package/lib/views/createViewMeta.d.ts +0 -7
  110. package/lib/views/createViewMeta.js +19 -70
  111. package/lib/views/index.d.ts +1 -2
  112. package/lib/views/index.js +4 -3
  113. package/lib/views/useDialog.js +7 -2
  114. package/lib/views/useDrawer.js +7 -2
  115. package/lib/views/usePage.d.ts +4 -0
  116. package/lib/views/usePage.js +43 -6
  117. package/lib/views/usePopover.js +4 -1
  118. package/lib/views/viewEvents.d.ts +17 -0
  119. package/lib/views/viewEvents.js +90 -0
  120. package/package.json +4 -4
  121. package/src/BlockScopedFlowEngine.ts +2 -5
  122. package/src/JSRunner.ts +44 -2
  123. package/src/ViewScopedFlowEngine.ts +4 -0
  124. package/src/__tests__/JSRunner.test.ts +64 -0
  125. package/src/__tests__/createViewMeta.popup.test.ts +62 -1
  126. package/src/__tests__/flowContext.test.ts +693 -1
  127. package/src/__tests__/flowEngine.dataSourceDirty.test.ts +63 -0
  128. package/src/__tests__/flowModel.openView.navigation.test.ts +28 -0
  129. package/src/__tests__/flowRunJSContextDefine.test.ts +63 -0
  130. package/src/__tests__/flowRuntimeContext.test.ts +2 -1
  131. package/src/__tests__/flowSettings.open.test.tsx +123 -19
  132. package/src/__tests__/runjsContext.test.ts +10 -7
  133. package/src/__tests__/runjsContextImplementations.test.ts +34 -3
  134. package/src/__tests__/runjsContextRuntime.test.ts +3 -3
  135. package/src/__tests__/runjsContributions.test.ts +89 -0
  136. package/src/__tests__/runjsExternalLibs.test.ts +242 -0
  137. package/src/__tests__/runjsLibsLazyLoading.test.ts +44 -0
  138. package/src/__tests__/runjsLocales.test.ts +4 -1
  139. package/src/__tests__/runjsPreprocessDefault.test.ts +49 -0
  140. package/src/__tests__/runjsRuntimeFeatures.test.ts +166 -0
  141. package/src/__tests__/runjsSnippets.test.ts +40 -3
  142. package/src/acl/Acl.tsx +3 -3
  143. package/src/components/FlowContextSelector.tsx +208 -12
  144. package/src/components/settings/wrappers/component/SwitchWithTitle.tsx +2 -1
  145. package/src/components/settings/wrappers/component/__tests__/InlineControls.test.tsx +74 -0
  146. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +109 -16
  147. package/src/components/settings/wrappers/contextual/FlowsContextMenu.tsx +41 -7
  148. package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +13 -2
  149. package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +157 -5
  150. package/src/components/variables/VariableInput.tsx +12 -4
  151. package/src/components/variables/VariableTag.tsx +54 -45
  152. package/src/components/variables/__tests__/FlowContextSelector.test.tsx +260 -3
  153. package/src/components/variables/__tests__/VariableTag.test.tsx +50 -0
  154. package/src/components/variables/__tests__/utils.test.ts +81 -3
  155. package/src/components/variables/utils.ts +67 -6
  156. package/src/data-source/index.ts +85 -110
  157. package/src/executor/FlowExecutor.ts +200 -23
  158. package/src/executor/__tests__/flowExecutor.test.ts +66 -0
  159. package/src/flowContext.ts +2986 -211
  160. package/src/flowEngine.ts +59 -8
  161. package/src/flowI18n.ts +7 -5
  162. package/src/flowSettings.ts +18 -12
  163. package/src/index.ts +14 -1
  164. package/src/locale/en-US.json +9 -2
  165. package/src/locale/zh-CN.json +8 -1
  166. package/src/models/CollectionFieldModel.tsx +3 -1
  167. package/src/models/__tests__/dispatchEvent.when.test.ts +554 -0
  168. package/src/models/__tests__/flowModel.test.ts +20 -4
  169. package/src/models/flowModel.tsx +13 -1
  170. package/src/provider.tsx +7 -6
  171. package/src/resources/__tests__/multiRecordResource.test.ts +44 -0
  172. package/src/resources/__tests__/sqlResource.test.ts +60 -0
  173. package/src/resources/baseRecordResource.ts +31 -0
  174. package/src/resources/multiRecordResource.ts +11 -4
  175. package/src/resources/singleRecordResource.ts +3 -0
  176. package/src/resources/sqlResource.ts +11 -6
  177. package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +10 -0
  178. package/src/runjs-context/contexts/JSBlockRunJSContext.ts +6 -2
  179. package/src/runjs-context/contexts/JSEditableFieldRunJSContext.ts +106 -0
  180. package/src/runjs-context/contexts/JSItemRunJSContext.ts +10 -0
  181. package/src/runjs-context/contexts/base.ts +715 -44
  182. package/src/runjs-context/contributions.ts +88 -0
  183. package/src/runjs-context/helpers.ts +11 -1
  184. package/src/runjs-context/setup.ts +6 -0
  185. package/src/runjs-context/snippets/global/api-request.snippet.ts +3 -3
  186. package/src/runjs-context/snippets/global/import-esm.snippet.ts +2 -3
  187. package/src/runjs-context/snippets/global/query-selector.snippet.ts +8 -3
  188. package/src/runjs-context/snippets/global/require-amd.snippet.ts +1 -1
  189. package/src/runjs-context/snippets/index.ts +75 -41
  190. package/src/runjs-context/snippets/scene/block/add-event-listener.snippet.ts +11 -13
  191. package/src/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.ts +3 -3
  192. package/src/runjs-context/snippets/scene/block/chartjs-bar.snippet.ts +2 -2
  193. package/src/runjs-context/snippets/scene/block/echarts-init.snippet.ts +2 -2
  194. package/src/runjs-context/snippets/scene/block/render-iframe.snippet.ts +2 -2
  195. package/src/runjs-context/snippets/scene/block/render-react.snippet.ts +1 -1
  196. package/src/runjs-context/snippets/scene/block/render-statistics.snippet.ts +1 -1
  197. package/src/runjs-context/snippets/scene/block/render-timeline.snippet.ts +1 -1
  198. package/src/runjs-context/snippets/scene/block/resource-example.snippet.ts +6 -11
  199. package/src/runjs-context/snippets/scene/block/three-users-orbit.snippet.ts +6 -6
  200. package/src/runjs-context/snippets/scene/block/vue-component.snippet.ts +3 -4
  201. package/src/runjs-context/snippets/scene/detail/color-by-value.snippet.ts +1 -1
  202. package/src/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.ts +20 -3
  203. package/src/runjs-context/snippets/scene/detail/format-number.snippet.ts +1 -1
  204. package/src/runjs-context/snippets/scene/detail/innerHTML-value.snippet.ts +1 -1
  205. package/src/runjs-context/snippets/scene/detail/percentage-bar.snippet.ts +3 -3
  206. package/src/runjs-context/snippets/scene/detail/relative-time.snippet.ts +3 -3
  207. package/src/runjs-context/snippets/scene/detail/status-tag.snippet.ts +2 -2
  208. package/src/runjs-context/snippets/scene/form/cascade-select.snippet.ts +1 -1
  209. package/src/runjs-context/snippets/scene/form/render-basic.snippet.ts +3 -8
  210. package/src/runjs-context/snippets/scene/table/cell-open-dialog.snippet.ts +6 -3
  211. package/src/runjs-context/snippets/scene/table/concat-fields.snippet.ts +3 -1
  212. package/src/runjsLibs.ts +622 -0
  213. package/src/scheduler/ModelOperationScheduler.ts +27 -21
  214. package/src/types.ts +38 -1
  215. package/src/utils/__tests__/dateVariable.test.ts +101 -0
  216. package/src/utils/__tests__/params-resolvers.test.ts +40 -0
  217. package/src/utils/__tests__/runjsRequireAsyncAutoWhitelist.test.ts +38 -0
  218. package/src/utils/__tests__/runjsTemplateCompat.test.ts +159 -0
  219. package/src/utils/__tests__/runjsValue.test.ts +44 -0
  220. package/src/utils/__tests__/safeGlobals.test.ts +57 -2
  221. package/src/utils/__tests__/utils.test.ts +95 -0
  222. package/src/utils/associationObjectVariable.ts +2 -2
  223. package/src/utils/createCollectionContextMeta.ts +1 -0
  224. package/src/utils/createEphemeralContext.ts +5 -4
  225. package/src/utils/dateVariable.ts +397 -0
  226. package/src/utils/exceptions.ts +11 -0
  227. package/src/utils/index.ts +37 -3
  228. package/src/utils/params-resolvers.ts +23 -9
  229. package/src/utils/resolveModuleUrl.ts +91 -0
  230. package/src/utils/resolveRunJSObjectValues.ts +46 -0
  231. package/src/utils/runjsModuleLoader.ts +553 -0
  232. package/src/utils/runjsTemplateCompat.ts +828 -0
  233. package/src/utils/runjsValue.ts +287 -0
  234. package/src/utils/safeGlobals.ts +188 -17
  235. package/src/utils/schema-utils.ts +79 -0
  236. package/src/views/__tests__/FlowView.usePage.test.tsx +54 -1
  237. package/src/views/__tests__/useDialog.closeDestroy.test.tsx +35 -8
  238. package/src/views/__tests__/viewEvents.resolveOpenerEngine.test.ts +28 -0
  239. package/src/views/createViewMeta.ts +22 -75
  240. package/src/views/index.tsx +1 -2
  241. package/src/views/useDialog.tsx +8 -1
  242. package/src/views/useDrawer.tsx +8 -1
  243. package/src/views/usePage.tsx +51 -5
  244. package/src/views/usePopover.tsx +4 -1
  245. package/src/views/viewEvents.ts +55 -0
@@ -14,30 +14,28 @@ 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
+ // mimic real view stack linkage: previousEngine points to the last engine in chain
30
+ previousEngine: (engine as any)?.nextEngine || engine,
29
31
  }),
30
32
  }));
31
33
 
32
- vi.mock('../utils/variablesParams', () => ({
34
+ vi.mock('../../utils/variablesParams', () => ({
33
35
  createViewRecordResolveOnServer: vi.fn(),
34
36
  getViewRecordFromParent: vi.fn(),
35
37
  }));
36
38
 
37
- vi.mock('../createViewMeta', () => ({
38
- registerPopupVariable: vi.fn(),
39
- }));
40
-
41
39
  vi.mock('../DialogComponent', () => ({
42
40
  default: ({ children }) => <div>{children}</div>,
43
41
  }));
@@ -52,8 +50,12 @@ vi.mock('../usePatchElement', () => ({
52
50
  describe('useDialog - close/destroy logic', () => {
53
51
  const createMockFlowContext = () => {
54
52
  const ctx = new FlowContext();
53
+ ctx.defineMethod('t', (key: string) => key);
55
54
  ctx.engine = {
56
55
  context: new FlowContext(),
56
+ emitter: {
57
+ emit: vi.fn(),
58
+ },
57
59
  };
58
60
  return ctx;
59
61
  };
@@ -129,4 +131,29 @@ describe('useDialog - close/destroy logic', () => {
129
131
  // Should not call destroy directly, let router handle it
130
132
  expect(mockCloseFunc).not.toHaveBeenCalled();
131
133
  });
134
+
135
+ it('should emit view activated event on opener engine', () => {
136
+ const api = renderUseDialog();
137
+ const flowContext = createMockFlowContext();
138
+ const emitSpy = flowContext.engine.emitter.emit;
139
+
140
+ const dialog = api.open({ inputArgs: { viewUid: 'child-view' } }, flowContext);
141
+
142
+ dialog.close();
143
+ expect(emitSpy).toHaveBeenCalledWith('view:activated', expect.anything());
144
+ });
145
+
146
+ it('should emit view events on immediate opener engine (previousEngine) when present', () => {
147
+ const api = renderUseDialog();
148
+ const flowContext = createMockFlowContext();
149
+ const rootEmitSpy = flowContext.engine.emitter.emit;
150
+ const openerEmitSpy = vi.fn();
151
+ (flowContext.engine as any).nextEngine = { emitter: { emit: openerEmitSpy }, __NOCOBASE_ENGINE_SCOPE__: 'view' };
152
+
153
+ const dialog = api.open({ inputArgs: { viewUid: 'child-view' } }, flowContext);
154
+
155
+ dialog.close();
156
+ expect(openerEmitSpy).toHaveBeenCalledWith('view:activated', expect.anything());
157
+ expect(rootEmitSpy).not.toHaveBeenCalledWith('view:activated', expect.anything());
158
+ });
132
159
  });
@@ -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';
@@ -15,6 +15,7 @@ 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';
@@ -25,6 +26,7 @@ export function useDialog() {
25
26
  const holderRef = React.useRef(null);
26
27
 
27
28
  const open = (config, flowContext) => {
29
+ const parentEngine = flowContext?.engine;
28
30
  uuid += 1;
29
31
  const dialogRef = React.createRef<{
30
32
  destroy: () => void;
@@ -77,6 +79,8 @@ export function useDialog() {
77
79
  const ctx = new FlowContext();
78
80
  // 为当前视图创建作用域引擎(隔离实例与缓存)
79
81
  const scopedEngine = createViewScopedEngine(flowContext.engine);
82
+ const openerEngine = resolveOpenerEngine(parentEngine, scopedEngine);
83
+
80
84
  ctx.defineProperty('engine', { value: scopedEngine });
81
85
  ctx.addDelegate(scopedEngine.context);
82
86
  if (config.inheritContext !== false) {
@@ -95,6 +99,10 @@ export function useDialog() {
95
99
  dialogRef.current?.destroy();
96
100
  closeFunc?.();
97
101
  resolvePromise?.(result);
102
+ // Notify opener view that it becomes active again.
103
+ const openerEmitter = openerEngine?.emitter;
104
+ bumpViewActivatedVersion(openerEmitter);
105
+ openerEmitter?.emit?.(VIEW_ACTIVATED_EVENT, { type: 'dialog', viewUid: currentDialog?.inputArgs?.viewUid });
98
106
  // 关闭时修正 previous/next 指针
99
107
  scopedEngine.unlinkFromStack();
100
108
  },
@@ -130,7 +138,6 @@ export function useDialog() {
130
138
 
131
139
  ctx.defineProperty('view', {
132
140
  get: () => currentDialog,
133
- // meta: createViewMeta(ctx),
134
141
  resolveOnServer: createViewRecordResolveOnServer(ctx, () => getViewRecordFromParent(flowContext, ctx)),
135
142
  });
136
143
  // 顶层 popup 变量:弹窗记录/数据源/上级弹窗链(去重封装)
@@ -15,6 +15,7 @@ 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';
@@ -54,6 +55,7 @@ export function useDrawer() {
54
55
  RenderNestedDrawer.displayName = 'RenderNestedDrawer';
55
56
 
56
57
  const open = (config, flowContext: FlowEngineContext) => {
58
+ const parentEngine = flowContext.engine;
57
59
  const drawerRef = React.createRef<{
58
60
  destroy: () => void;
59
61
  update: (config: any) => void;
@@ -105,6 +107,8 @@ export function useDrawer() {
105
107
  const ctx = new FlowContext();
106
108
  // 为当前视图创建作用域引擎(隔离实例与缓存)
107
109
  const scopedEngine = createViewScopedEngine(flowContext.engine);
110
+ const openerEngine = resolveOpenerEngine(parentEngine, scopedEngine);
111
+
108
112
  // 先将引擎暴露给视图上下文,再按需继承父上下文
109
113
  ctx.defineProperty('engine', { value: scopedEngine });
110
114
  ctx.addDelegate(scopedEngine.context);
@@ -124,6 +128,10 @@ export function useDrawer() {
124
128
  drawerRef.current?.destroy();
125
129
  closeFunc?.();
126
130
  resolvePromise?.(result);
131
+ // Notify opener view that it becomes active again.
132
+ const openerEmitter = openerEngine?.emitter;
133
+ bumpViewActivatedVersion(openerEmitter);
134
+ openerEmitter?.emit?.(VIEW_ACTIVATED_EVENT, { type: 'drawer', viewUid: currentDrawer?.inputArgs?.viewUid });
127
135
  // 关闭时修正 previous/next 指针
128
136
  scopedEngine.unlinkFromStack();
129
137
  },
@@ -159,7 +167,6 @@ export function useDrawer() {
159
167
 
160
168
  ctx.defineProperty('view', {
161
169
  get: () => currentDrawer,
162
- // meta: createViewMeta(ctx),
163
170
  resolveOnServer: createViewRecordResolveOnServer(ctx, () => getViewRecordFromParent(flowContext, ctx)),
164
171
  });
165
172
  // 顶层 popup 变量:弹窗记录/数据源/上级弹窗链(去重封装)
@@ -15,26 +15,33 @@ import { FlowViewContextProvider } from '../FlowContextProvider';
15
15
  import { registerPopupVariable } from './createViewMeta';
16
16
  import { PageComponent } from './PageComponent';
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';
21
22
 
22
23
  let uuid = 0;
23
24
 
25
+ /** Global embed container element ID */
26
+ export const GLOBAL_EMBED_CONTAINER_ID = 'nocobase-embed-container';
27
+ /** Dataset key used to signal embed replacement in progress (skip style reset on close) */
28
+ export const EMBED_REPLACING_DATA_KEY = 'nocobaseEmbedReplacing';
29
+
24
30
  // 稳定的 Holder 组件,避免在父组件重渲染时更换组件类型导致卸载, 否则切换主题时会丢失所有页面内容
25
31
  const PageElementsHolder = React.memo(
26
32
  React.forwardRef((props: any, ref: any) => {
27
33
  const [elements, patchElement] = usePatchElement();
28
34
  React.useImperativeHandle(ref, () => ({ patchElement }), [patchElement]);
29
- console.log('[NocoBase] Rendering PageElementsHolder with elements count:', elements.length);
30
35
  return <>{elements}</>;
31
36
  }),
32
37
  );
33
38
 
34
39
  export function usePage() {
35
40
  const holderRef = React.useRef(null);
41
+ const globalEmbedActiveRef = React.useRef<null | { destroy: () => void }>(null);
36
42
 
37
43
  const open = (config, flowContext) => {
44
+ const parentEngine = flowContext?.engine;
38
45
  uuid += 1;
39
46
  const pageRef = React.createRef<{
40
47
  destroy: () => void;
@@ -75,11 +82,33 @@ export function usePage() {
75
82
  return null; // Header 组件本身不渲染内容
76
83
  };
77
84
 
78
- const { target, content, preventClose, inheritContext = true, inputArgs, ...restConfig } = config;
85
+ const {
86
+ target,
87
+ content,
88
+ preventClose,
89
+ inheritContext = true,
90
+ inputArgs: viewInputArgs = {},
91
+ ...restConfig
92
+ } = config;
93
+ const isGlobalEmbedContainer = target instanceof HTMLElement && target.id === GLOBAL_EMBED_CONTAINER_ID;
94
+
95
+ // Global embed container uses "replace" behavior: opening a new view destroys the previous one.
96
+ if (isGlobalEmbedContainer && globalEmbedActiveRef.current) {
97
+ try {
98
+ // Avoid style "reset flicker" when replacing: tell embed wrappers to skip resetting container styles.
99
+ target.dataset[EMBED_REPLACING_DATA_KEY] = '1';
100
+ globalEmbedActiveRef.current.destroy();
101
+ } finally {
102
+ delete target.dataset[EMBED_REPLACING_DATA_KEY];
103
+ globalEmbedActiveRef.current = null;
104
+ }
105
+ }
79
106
 
80
107
  const ctx = new FlowContext();
81
108
  // 为当前视图创建作用域引擎(隔离实例与缓存)
82
109
  const scopedEngine = createViewScopedEngine(flowContext.engine);
110
+ const openerEngine = resolveOpenerEngine(parentEngine, scopedEngine);
111
+
83
112
  ctx.defineProperty('engine', { value: scopedEngine });
84
113
  ctx.addDelegate(scopedEngine.context);
85
114
  if (inheritContext) {
@@ -91,13 +120,27 @@ export function usePage() {
91
120
  // 构造 currentPage 实例
92
121
  const currentPage = {
93
122
  type: 'embed' as const,
94
- inputArgs: config.inputArgs || {},
123
+ inputArgs: viewInputArgs,
95
124
  preventClose: !!config.preventClose,
96
125
  destroy: (result?: any) => {
97
126
  config.onClose?.();
98
127
  resolvePromise?.(result);
99
128
  pageRef.current?.destroy();
100
129
  closeFunc?.();
130
+
131
+ if (isGlobalEmbedContainer) {
132
+ globalEmbedActiveRef.current = null;
133
+ }
134
+
135
+ // Notify opener view that it becomes active again.
136
+ const isReplacing =
137
+ isGlobalEmbedContainer && target instanceof HTMLElement && target.dataset?.[EMBED_REPLACING_DATA_KEY] === '1';
138
+ if (!isReplacing) {
139
+ const openerEmitter = openerEngine?.emitter;
140
+ bumpViewActivatedVersion(openerEmitter);
141
+ openerEmitter?.emit?.(VIEW_ACTIVATED_EVENT, { type: 'embed', viewUid: currentPage?.inputArgs?.viewUid });
142
+ }
143
+
101
144
  // 关闭时修正 previous/next 指针
102
145
  scopedEngine.unlinkFromStack();
103
146
  },
@@ -132,7 +175,6 @@ export function usePage() {
132
175
 
133
176
  ctx.defineProperty('view', {
134
177
  get: () => currentPage,
135
- // meta: createViewMeta(ctx),
136
178
  // 仅当访问关联字段或前端无本地记录数据时,才交给服务端解析
137
179
  resolveOnServer: createViewRecordResolveOnServer(ctx, () => getViewRecordFromParent(flowContext, ctx)),
138
180
  });
@@ -180,7 +222,7 @@ export function usePage() {
180
222
  },
181
223
  );
182
224
 
183
- const key = inputArgs?.viewUid || `page-${uuid}`;
225
+ const key = viewInputArgs?.viewUid || `page-${uuid}`;
184
226
  const page = (
185
227
  <FlowEngineProvider key={key} engine={scopedEngine}>
186
228
  <FlowViewContextProvider context={ctx}>
@@ -195,6 +237,10 @@ export function usePage() {
195
237
  closeFunc = holderRef.current?.patchElement(page);
196
238
  }
197
239
 
240
+ if (isGlobalEmbedContainer) {
241
+ globalEmbedActiveRef.current = { destroy: currentPage.destroy };
242
+ }
243
+
198
244
  return Object.assign(promise, currentPage);
199
245
  };
200
246
 
@@ -40,8 +40,11 @@ const PopoverComponent = React.forwardRef<any, any>(({ afterClose, content, plac
40
40
  destroyTooltipOnHide
41
41
  content={config.content}
42
42
  placement={config.placement}
43
- getPopupContainer={() => document.body}
43
+ getPopupContainer={() => document.querySelector('#nocobase-app-container') || document.body}
44
44
  onOpenChange={(nextOpen) => {
45
+ if (!nextOpen && config.preventClose) {
46
+ return;
47
+ }
45
48
  setVisible(nextOpen);
46
49
  if (!nextOpen) {
47
50
  afterClose?.();
@@ -0,0 +1,55 @@
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 { FlowEngine } from '../flowEngine';
11
+
12
+ export const VIEW_ACTIVATED_VERSION = Symbol.for('__NOCOBASE_VIEW_ACTIVATED_VERSION__');
13
+
14
+ export const VIEW_ACTIVATED_EVENT = 'view:activated' as const;
15
+ export const DATA_SOURCE_DIRTY_EVENT = 'dataSource:dirty' as const;
16
+
17
+ export const ENGINE_SCOPE_KEY = '__NOCOBASE_ENGINE_SCOPE__' as const;
18
+ export const VIEW_ENGINE_SCOPE = 'view' as const;
19
+
20
+ export function getEmitterViewActivatedVersion(emitter): number {
21
+ const raw = Reflect.get(emitter, VIEW_ACTIVATED_VERSION);
22
+ const num = typeof raw === 'number' ? raw : Number(raw);
23
+ return Number.isFinite(num) && num > 0 ? num : 0;
24
+ }
25
+
26
+ export function bumpViewActivatedVersion(emitter): number {
27
+ const current = getEmitterViewActivatedVersion(emitter);
28
+ if (!Object.isExtensible(emitter)) return current;
29
+ const next = current + 1;
30
+ Reflect.set(emitter, VIEW_ACTIVATED_VERSION, next);
31
+ return next;
32
+ }
33
+
34
+ function isViewEngine(engine: FlowEngine): boolean {
35
+ return Reflect.get(engine, ENGINE_SCOPE_KEY) === VIEW_ENGINE_SCOPE;
36
+ }
37
+
38
+ function findNearestViewEngine(engine: FlowEngine | undefined): FlowEngine | undefined {
39
+ let cur: FlowEngine | undefined = engine;
40
+ let guard = 0;
41
+ while (cur && guard++ < 50) {
42
+ if (isViewEngine(cur)) return cur;
43
+ cur = (cur as { previousEngine?: FlowEngine | undefined }).previousEngine;
44
+ }
45
+ }
46
+
47
+ export function resolveOpenerEngine(parentEngine: FlowEngine, scopedEngine: FlowEngine): FlowEngine | undefined {
48
+ if (!parentEngine) return undefined;
49
+ const parentViewEngine = findNearestViewEngine(parentEngine);
50
+ if (parentViewEngine) return parentViewEngine;
51
+
52
+ // Fallback: resolve from previous engine in the stack (historical behavior).
53
+ const previousEngine = scopedEngine?.previousEngine;
54
+ return findNearestViewEngine(previousEngine) || parentEngine;
55
+ }