@nocobase/flow-engine 2.0.0-beta.2 → 2.0.0-beta.20

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 (124) hide show
  1. package/lib/BlockScopedFlowEngine.js +0 -1
  2. package/lib/JSRunner.d.ts +6 -0
  3. package/lib/JSRunner.js +2 -1
  4. package/lib/ViewScopedFlowEngine.js +3 -0
  5. package/lib/acl/Acl.js +13 -3
  6. package/lib/components/dnd/gridDragPlanner.d.ts +1 -0
  7. package/lib/components/dnd/gridDragPlanner.js +53 -1
  8. package/lib/components/settings/wrappers/component/SwitchWithTitle.js +2 -1
  9. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +11 -3
  10. package/lib/components/variables/VariableInput.js +8 -2
  11. package/lib/data-source/index.js +6 -0
  12. package/lib/executor/FlowExecutor.d.ts +2 -1
  13. package/lib/executor/FlowExecutor.js +156 -22
  14. package/lib/flowContext.d.ts +4 -1
  15. package/lib/flowContext.js +176 -107
  16. package/lib/flowEngine.d.ts +21 -0
  17. package/lib/flowEngine.js +38 -0
  18. package/lib/flowSettings.js +12 -10
  19. package/lib/index.d.ts +3 -0
  20. package/lib/index.js +16 -0
  21. package/lib/models/CollectionFieldModel.d.ts +1 -0
  22. package/lib/models/CollectionFieldModel.js +3 -2
  23. package/lib/models/flowModel.d.ts +7 -0
  24. package/lib/models/flowModel.js +66 -1
  25. package/lib/provider.js +7 -6
  26. package/lib/resources/baseRecordResource.d.ts +5 -0
  27. package/lib/resources/baseRecordResource.js +24 -0
  28. package/lib/resources/multiRecordResource.d.ts +1 -0
  29. package/lib/resources/multiRecordResource.js +11 -4
  30. package/lib/resources/singleRecordResource.js +2 -0
  31. package/lib/resources/sqlResource.d.ts +1 -0
  32. package/lib/resources/sqlResource.js +8 -3
  33. package/lib/runjs-context/contexts/base.js +10 -4
  34. package/lib/runjsLibs.d.ts +28 -0
  35. package/lib/runjsLibs.js +532 -0
  36. package/lib/scheduler/ModelOperationScheduler.d.ts +2 -0
  37. package/lib/scheduler/ModelOperationScheduler.js +21 -21
  38. package/lib/types.d.ts +15 -0
  39. package/lib/utils/createCollectionContextMeta.js +1 -0
  40. package/lib/utils/index.d.ts +2 -0
  41. package/lib/utils/index.js +10 -0
  42. package/lib/utils/params-resolvers.js +16 -9
  43. package/lib/utils/resolveModuleUrl.d.ts +58 -0
  44. package/lib/utils/resolveModuleUrl.js +65 -0
  45. package/lib/utils/runjsModuleLoader.d.ts +58 -0
  46. package/lib/utils/runjsModuleLoader.js +422 -0
  47. package/lib/utils/runjsTemplateCompat.d.ts +35 -0
  48. package/lib/utils/runjsTemplateCompat.js +743 -0
  49. package/lib/utils/safeGlobals.d.ts +5 -9
  50. package/lib/utils/safeGlobals.js +129 -17
  51. package/lib/views/createViewMeta.d.ts +0 -7
  52. package/lib/views/createViewMeta.js +19 -70
  53. package/lib/views/index.d.ts +1 -2
  54. package/lib/views/index.js +4 -3
  55. package/lib/views/useDialog.js +8 -3
  56. package/lib/views/useDrawer.js +7 -2
  57. package/lib/views/usePage.d.ts +4 -0
  58. package/lib/views/usePage.js +43 -6
  59. package/lib/views/usePopover.js +4 -1
  60. package/lib/views/viewEvents.d.ts +17 -0
  61. package/lib/views/viewEvents.js +90 -0
  62. package/package.json +4 -4
  63. package/src/BlockScopedFlowEngine.ts +2 -5
  64. package/src/JSRunner.ts +8 -1
  65. package/src/ViewScopedFlowEngine.ts +4 -0
  66. package/src/__tests__/createViewMeta.popup.test.ts +62 -1
  67. package/src/__tests__/flowEngine.dataSourceDirty.test.ts +63 -0
  68. package/src/__tests__/flowSettings.open.test.tsx +69 -15
  69. package/src/__tests__/provider.test.tsx +0 -5
  70. package/src/__tests__/runjsExternalLibs.test.ts +242 -0
  71. package/src/__tests__/runjsLibsLazyLoading.test.ts +44 -0
  72. package/src/__tests__/runjsPreprocessDefault.test.ts +49 -0
  73. package/src/acl/Acl.tsx +3 -3
  74. package/src/components/__tests__/gridDragPlanner.test.ts +141 -1
  75. package/src/components/dnd/gridDragPlanner.ts +60 -0
  76. package/src/components/settings/wrappers/component/SwitchWithTitle.tsx +2 -1
  77. package/src/components/settings/wrappers/component/__tests__/InlineControls.test.tsx +74 -0
  78. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +11 -3
  79. package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +63 -4
  80. package/src/components/variables/VariableInput.tsx +8 -2
  81. package/src/data-source/index.ts +6 -0
  82. package/src/executor/FlowExecutor.ts +193 -23
  83. package/src/executor/__tests__/flowExecutor.test.ts +66 -0
  84. package/src/flowContext.ts +234 -118
  85. package/src/flowEngine.ts +41 -0
  86. package/src/flowSettings.ts +12 -11
  87. package/src/index.ts +10 -0
  88. package/src/models/CollectionFieldModel.tsx +3 -1
  89. package/src/models/__tests__/dispatchEvent.when.test.ts +356 -0
  90. package/src/models/__tests__/flowModel.clone.test.ts +416 -0
  91. package/src/models/__tests__/flowModel.test.ts +16 -0
  92. package/src/models/flowModel.tsx +94 -1
  93. package/src/provider.tsx +9 -7
  94. package/src/resources/__tests__/multiRecordResource.test.ts +44 -0
  95. package/src/resources/__tests__/sqlResource.test.ts +60 -0
  96. package/src/resources/baseRecordResource.ts +31 -0
  97. package/src/resources/multiRecordResource.ts +11 -4
  98. package/src/resources/singleRecordResource.ts +3 -0
  99. package/src/resources/sqlResource.ts +8 -3
  100. package/src/runjs-context/contexts/base.ts +9 -2
  101. package/src/runjsLibs.ts +622 -0
  102. package/src/scheduler/ModelOperationScheduler.ts +23 -21
  103. package/src/types.ts +26 -1
  104. package/src/utils/__tests__/params-resolvers.test.ts +40 -0
  105. package/src/utils/__tests__/runjsRequireAsyncAutoWhitelist.test.ts +38 -0
  106. package/src/utils/__tests__/runjsTemplateCompat.test.ts +159 -0
  107. package/src/utils/__tests__/safeGlobals.test.ts +49 -2
  108. package/src/utils/createCollectionContextMeta.ts +1 -0
  109. package/src/utils/index.ts +6 -0
  110. package/src/utils/params-resolvers.ts +23 -9
  111. package/src/utils/resolveModuleUrl.ts +91 -0
  112. package/src/utils/runjsModuleLoader.ts +553 -0
  113. package/src/utils/runjsTemplateCompat.ts +828 -0
  114. package/src/utils/safeGlobals.ts +133 -16
  115. package/src/views/__tests__/FlowView.usePage.test.tsx +54 -1
  116. package/src/views/__tests__/useDialog.closeDestroy.test.tsx +35 -8
  117. package/src/views/__tests__/viewEvents.resolveOpenerEngine.test.ts +28 -0
  118. package/src/views/createViewMeta.ts +22 -75
  119. package/src/views/index.tsx +1 -2
  120. package/src/views/useDialog.tsx +9 -2
  121. package/src/views/useDrawer.tsx +8 -1
  122. package/src/views/usePage.tsx +51 -5
  123. package/src/views/usePopover.tsx +4 -1
  124. package/src/views/viewEvents.ts +55 -0
@@ -15,6 +15,66 @@
15
15
  * - 不允许随意访问未声明的属性,最小权限原则
16
16
  */
17
17
 
18
+ type RunJSSafeGlobalsRegistry = {
19
+ windowAllow: Set<string>;
20
+ documentAllow: Set<string>;
21
+ };
22
+
23
+ function getRunJSSafeGlobalsRegistry(): RunJSSafeGlobalsRegistry {
24
+ const g: any = globalThis as any;
25
+ if (g.__nocobaseRunJSSafeGlobalsRegistry?.windowAllow && g.__nocobaseRunJSSafeGlobalsRegistry?.documentAllow) {
26
+ return g.__nocobaseRunJSSafeGlobalsRegistry as RunJSSafeGlobalsRegistry;
27
+ }
28
+ const reg: RunJSSafeGlobalsRegistry = {
29
+ windowAllow: new Set<string>(),
30
+ documentAllow: new Set<string>(),
31
+ };
32
+ g.__nocobaseRunJSSafeGlobalsRegistry = reg;
33
+ return reg;
34
+ }
35
+
36
+ export function registerRunJSSafeWindowGlobals(keys: Iterable<string> | null | undefined): void {
37
+ if (!keys) return;
38
+ const reg = getRunJSSafeGlobalsRegistry();
39
+ for (const k of keys) {
40
+ if (typeof k !== 'string') continue;
41
+ const key = k.trim();
42
+ if (!key) continue;
43
+ reg.windowAllow.add(key);
44
+ }
45
+ }
46
+
47
+ export function registerRunJSSafeDocumentGlobals(keys: Iterable<string> | null | undefined): void {
48
+ if (!keys) return;
49
+ const reg = getRunJSSafeGlobalsRegistry();
50
+ for (const k of keys) {
51
+ if (typeof k !== 'string') continue;
52
+ const key = k.trim();
53
+ if (!key) continue;
54
+ reg.documentAllow.add(key);
55
+ }
56
+ }
57
+
58
+ export function __resetRunJSSafeGlobalsRegistryForTests(): void {
59
+ const g: any = globalThis as any;
60
+ if (g.__nocobaseRunJSSafeGlobalsRegistry) {
61
+ try {
62
+ g.__nocobaseRunJSSafeGlobalsRegistry.windowAllow?.clear?.();
63
+ g.__nocobaseRunJSSafeGlobalsRegistry.documentAllow?.clear?.();
64
+ } catch {
65
+ // ignore
66
+ }
67
+ }
68
+ }
69
+
70
+ function isAllowedDynamicWindowKey(key: string): boolean {
71
+ return getRunJSSafeGlobalsRegistry().windowAllow.has(key);
72
+ }
73
+
74
+ function isAllowedDynamicDocumentKey(key: string): boolean {
75
+ return getRunJSSafeGlobalsRegistry().documentAllow.has(key);
76
+ }
77
+
18
78
  export function createSafeWindow(extra?: Record<string, any>) {
19
79
  // 解析相对 URL 使用脱敏 base(不含 query/hash),避免在解析时泄露敏感信息
20
80
  const getSafeBaseHref = () => `${window.location.origin}${window.location.pathname}`;
@@ -160,15 +220,44 @@ export function createSafeWindow(extra?: Record<string, any>) {
160
220
  ...(extra || {}),
161
221
  };
162
222
 
163
- return new Proxy(
164
- {},
165
- {
166
- get(_target, prop: string) {
167
- if (prop in allowedGlobals) return allowedGlobals[prop];
168
- throw new Error(`Access to global property "${prop}" is not allowed.`);
169
- },
223
+ const target: Record<string, any> = Object.create(null);
224
+
225
+ return new Proxy(target, {
226
+ get(t, prop: string | symbol) {
227
+ if (typeof prop !== 'string') {
228
+ return Reflect.get(t, prop);
229
+ }
230
+
231
+ if (prop in allowedGlobals) return allowedGlobals[prop];
232
+ if (Object.prototype.hasOwnProperty.call(t, prop)) return (t as any)[prop];
233
+ if (isAllowedDynamicWindowKey(prop)) {
234
+ const v = (window as any)[prop];
235
+ // Bind functions to the real window to avoid Illegal invocation
236
+ if (typeof v === 'function') return v.bind(window);
237
+ return v;
238
+ }
239
+
240
+ throw new Error(`Access to global property "${prop}" is not allowed.`);
170
241
  },
171
- );
242
+ set(t, prop: string | symbol, value: any) {
243
+ if (typeof prop !== 'string') {
244
+ Reflect.set(t, prop, value);
245
+ return true;
246
+ }
247
+ if (prop in allowedGlobals) {
248
+ throw new Error(`Mutation of global property "${prop}" is not allowed.`);
249
+ }
250
+ (t as any)[prop] = value;
251
+ return true;
252
+ },
253
+ has(t, prop: string | symbol) {
254
+ if (typeof prop !== 'string') return Reflect.has(t, prop);
255
+ if (prop in allowedGlobals) return true;
256
+ if (Object.prototype.hasOwnProperty.call(t, prop)) return true;
257
+ if (isAllowedDynamicWindowKey(prop)) return true;
258
+ return false;
259
+ },
260
+ });
172
261
  }
173
262
 
174
263
  export function createSafeDocument(extra?: Record<string, any>) {
@@ -178,15 +267,43 @@ export function createSafeDocument(extra?: Record<string, any>) {
178
267
  querySelectorAll: document.querySelectorAll.bind(document),
179
268
  ...(extra || {}),
180
269
  };
181
- return new Proxy(
182
- {},
183
- {
184
- get(_target, prop: string) {
185
- if (prop in allowed) return allowed[prop];
186
- throw new Error(`Access to document property "${prop}" is not allowed.`);
187
- },
270
+ const target: Record<string, any> = Object.create(null);
271
+ return new Proxy(target, {
272
+ get(t, prop: string | symbol) {
273
+ if (typeof prop !== 'string') {
274
+ return Reflect.get(t, prop);
275
+ }
276
+
277
+ if (prop in allowed) return allowed[prop];
278
+ if (Object.prototype.hasOwnProperty.call(t, prop)) return (t as any)[prop];
279
+ if (isAllowedDynamicDocumentKey(prop)) {
280
+ const v = (document as any)[prop];
281
+ // Bind functions to the real document to avoid Illegal invocation
282
+ if (typeof v === 'function') return v.bind(document);
283
+ return v;
284
+ }
285
+
286
+ throw new Error(`Access to document property "${prop}" is not allowed.`);
188
287
  },
189
- );
288
+ set(t, prop: string | symbol, value: any) {
289
+ if (typeof prop !== 'string') {
290
+ Reflect.set(t, prop, value);
291
+ return true;
292
+ }
293
+ if (prop in allowed) {
294
+ throw new Error(`Mutation of document property "${prop}" is not allowed.`);
295
+ }
296
+ (t as any)[prop] = value;
297
+ return true;
298
+ },
299
+ has(t, prop: string | symbol) {
300
+ if (typeof prop !== 'string') return Reflect.has(t, prop);
301
+ if (prop in allowed) return true;
302
+ if (Object.prototype.hasOwnProperty.call(t, prop)) return true;
303
+ if (isAllowedDynamicDocumentKey(prop)) return true;
304
+ return false;
305
+ },
306
+ });
190
307
  }
191
308
 
192
309
  export function createSafeNavigator(extra?: Record<string, any>) {
@@ -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
  });
@@ -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 变量:弹窗记录/数据源/上级弹窗链(去重封装)
@@ -164,9 +171,9 @@ export function useDialog() {
164
171
  className="nb-dialog-overflow-hidden"
165
172
  ref={dialogRef}
166
173
  hidden={config.inputArgs?.hidden?.value}
167
- {...config}
168
174
  footer={currentFooter}
169
175
  header={currentHeader}
176
+ {...config}
170
177
  onCancel={() => {
171
178
  currentDialog.close(config.result);
172
179
  }}
@@ -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 变量:弹窗记录/数据源/上级弹窗链(去重封装)