@nocobase/flow-engine 2.2.0-beta.5 → 2.2.0-beta.6

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.
@@ -9,7 +9,6 @@
9
9
 
10
10
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
11
11
  import { JSRunner, shouldPreprocessRunJSTemplates } from '../JSRunner';
12
- import { createSafeWindow } from '../utils';
13
12
 
14
13
  describe('JSRunner', () => {
15
14
  let originalSearch: string;
@@ -68,7 +67,7 @@ describe('JSRunner', () => {
68
67
 
69
68
  const runner = new JSRunner({
70
69
  globals: {
71
- window: createSafeWindow(),
70
+ window,
72
71
  },
73
72
  });
74
73
 
@@ -84,7 +83,7 @@ describe('JSRunner', () => {
84
83
 
85
84
  const runner = new JSRunner({
86
85
  globals: {
87
- window: createSafeWindow(),
86
+ window,
88
87
  Blob: explicitBlob,
89
88
  },
90
89
  });
@@ -97,7 +96,7 @@ describe('JSRunner', () => {
97
96
  it('auto-lifts URL from injected window to top-level globals', async () => {
98
97
  const runner = new JSRunner({
99
98
  globals: {
100
- window: createSafeWindow(),
99
+ window,
101
100
  },
102
101
  });
103
102
 
@@ -114,7 +113,7 @@ describe('JSRunner', () => {
114
113
 
115
114
  const runner = new JSRunner({
116
115
  globals: {
117
- window: createSafeWindow(),
116
+ window,
118
117
  URL: explicitURL,
119
118
  },
120
119
  });
@@ -104,4 +104,32 @@ describe('FlowModelContext.openView - navigation enforcement', () => {
104
104
  expect(child.dispatchEvent).toHaveBeenCalledTimes(1);
105
105
  expect(child.dispatchEvent.mock.calls[0][0]).toBe('click');
106
106
  });
107
+
108
+ it('inherits current model input args when opening an external popup', async () => {
109
+ const { parent, child } = setup();
110
+ parent['getInputArgs'] = vi.fn(() => ({
111
+ filterByTk: 2,
112
+ sourceId: 10,
113
+ defaultInputKeys: ['filterByTk', 'sourceId'],
114
+ }));
115
+
116
+ await (parent.context as any).openView('child-uid', { mode: 'dialog' });
117
+
118
+ expect(child.dispatchEvent).toHaveBeenCalledTimes(1);
119
+ expect(child.dispatchEvent.mock.calls[0][1]).toMatchObject({
120
+ mode: 'dialog',
121
+ filterByTk: 2,
122
+ sourceId: 10,
123
+ });
124
+ expect(child.dispatchEvent.mock.calls[0][1]).not.toHaveProperty('defaultInputKeys');
125
+ });
126
+
127
+ it('does not debounce external popup dispatches', async () => {
128
+ const { parent, child } = setup();
129
+
130
+ await (parent.context as any).openView('child-uid', { mode: 'dialog', filterByTk: 1 });
131
+
132
+ expect(child.dispatchEvent).toHaveBeenCalledTimes(1);
133
+ expect(child.dispatchEvent.mock.calls[0][2]).toBeUndefined();
134
+ });
107
135
  });
@@ -3139,6 +3139,37 @@ class BaseFlowModelContext extends BaseFlowEngineContext {
3139
3139
  declare makeResource: <T extends FlowResource = FlowResource>(resourceType: ResourceType<T>) => T;
3140
3140
  }
3141
3141
 
3142
+ const OPEN_VIEW_INHERITED_INPUT_ARG_KEYS = [
3143
+ 'dataSourceKey',
3144
+ 'collectionName',
3145
+ 'associationName',
3146
+ 'filterByTk',
3147
+ 'sourceId',
3148
+ 'tabUid',
3149
+ ];
3150
+
3151
+ function pickDefinedKeys(source: Record<string, unknown> | null | undefined, keys: string[]) {
3152
+ const res: Record<string, unknown> = {};
3153
+ for (const key of keys) {
3154
+ if (typeof source?.[key] !== 'undefined') {
3155
+ res[key] = source[key];
3156
+ }
3157
+ }
3158
+ return res;
3159
+ }
3160
+
3161
+ function pickDefinedOpenViewInputArgs(source?: Record<string, unknown> | null) {
3162
+ return pickDefinedKeys(source, OPEN_VIEW_INHERITED_INPUT_ARG_KEYS);
3163
+ }
3164
+
3165
+ function applyDefinedDefaults(target: Record<string, unknown>, defaults: Record<string, unknown>) {
3166
+ for (const [key, value] of Object.entries(defaults)) {
3167
+ if (typeof target[key] === 'undefined') {
3168
+ target[key] = value;
3169
+ }
3170
+ }
3171
+ }
3172
+
3142
3173
  export class FlowEngineContext extends BaseFlowEngineContext {
3143
3174
  // public dataSourceManager: DataSourceManager;
3144
3175
  constructor(public engine: FlowEngine) {
@@ -3561,7 +3592,17 @@ export class FlowEngineContext extends BaseFlowEngineContext {
3561
3592
  doc = {};
3562
3593
  }
3563
3594
  const deprecatedCtx = createRunJSDeprecationProxy(runCtx, { doc });
3564
- const globals: Record<string, any> = { ctx: deprecatedCtx, ...(options?.globals || {}) };
3595
+ const browserGlobals: Record<string, any> = {};
3596
+ if (typeof window !== 'undefined') {
3597
+ browserGlobals.window = window;
3598
+ if (typeof navigator !== 'undefined') {
3599
+ browserGlobals.navigator = navigator;
3600
+ }
3601
+ }
3602
+ if (typeof document !== 'undefined') {
3603
+ browserGlobals.document = document;
3604
+ }
3605
+ const globals: Record<string, any> = { ctx: deprecatedCtx, ...browserGlobals, ...(options?.globals || {}) };
3565
3606
  const { timeoutMs } = options || {};
3566
3607
  return new JSRunner({ globals, timeoutMs });
3567
3608
  });
@@ -3687,7 +3728,14 @@ export class FlowModelContext extends BaseFlowModelContext {
3687
3728
  },
3688
3729
  });
3689
3730
  this.defineMethod('openView', async function (uid: string, options) {
3690
- const opts = { ...options };
3731
+ const inheritedInputArgs = {
3732
+ ...(typeof this.model?.['getInputArgs'] === 'function'
3733
+ ? pickDefinedOpenViewInputArgs(this.model['getInputArgs']())
3734
+ : {}),
3735
+ ...pickDefinedOpenViewInputArgs(this.inputArgs),
3736
+ };
3737
+ const opts = { ...(options || {}) };
3738
+ applyDefinedDefaults(opts, inheritedInputArgs);
3691
3739
  // NOTE: when custom context is passed, route navigation must be disabled to avoid losing it after refresh.
3692
3740
  if (opts.defineProperties || opts.defineMethods) {
3693
3741
  opts.navigation = false; // 强制不使用路由导航, 避免刷新页面时丢失上下文
@@ -3695,15 +3743,6 @@ export class FlowModelContext extends BaseFlowModelContext {
3695
3743
  let model: FlowModel | null = null;
3696
3744
  model = await this.engine.loadModel({ uid });
3697
3745
  if (!model) {
3698
- const pickDefined = (src: Record<string, any>, keys: string[]) => {
3699
- const res: Record<string, any> = {};
3700
- for (const k of keys) {
3701
- if (typeof src?.[k] !== 'undefined') {
3702
- res[k] = src[k];
3703
- }
3704
- }
3705
- return res;
3706
- };
3707
3746
  model = this.engine.createModel({
3708
3747
  uid, // 注意: 新建的 model 应该使用 ${parentModel.uid}-xxx 形式的 uid
3709
3748
  use: 'PopupActionModel',
@@ -3714,7 +3753,7 @@ export class FlowModelContext extends BaseFlowModelContext {
3714
3753
  popupSettings: {
3715
3754
  openView: {
3716
3755
  // 仅在创建时持久化一份默认配置;运行时以本次 opts 为准,避免多个 opener 互相覆盖。
3717
- ...pickDefined(opts, ['dataSourceKey', 'collectionName', 'associationName', 'mode', 'size']),
3756
+ ...pickDefinedKeys(opts, ['dataSourceKey', 'collectionName', 'associationName', 'mode', 'size']),
3718
3757
  },
3719
3758
  },
3720
3759
  },
@@ -3734,8 +3773,6 @@ export class FlowModelContext extends BaseFlowModelContext {
3734
3773
  // 统一语义:为即将打开的外部视图定义一个 PendingView(占位视图)
3735
3774
  const pendingType = (opts?.isMobileLayout ? 'embed' : opts?.mode || 'drawer') as any;
3736
3775
  const pendingInputArgs = { ...opts, viewUid, navigation: opts.navigation };
3737
- pendingInputArgs.filterByTk = pendingInputArgs.filterByTk || this.inputArgs?.filterByTk;
3738
- pendingInputArgs.sourceId = pendingInputArgs.sourceId || this.inputArgs?.sourceId;
3739
3776
 
3740
3777
  const pendingView = {
3741
3778
  type: pendingType,
@@ -3754,17 +3791,10 @@ export class FlowModelContext extends BaseFlowModelContext {
3754
3791
  } else if (on && typeof on === 'object' && typeof (on as any).eventName === 'string' && (on as any).eventName) {
3755
3792
  openEventName = (on as any).eventName;
3756
3793
  }
3757
- await model.dispatchEvent(
3758
- openEventName,
3759
- {
3760
- // navigation: false, // TODO: 路由模式有bug,不支持多层同样viewId的弹窗,因此这里默认先用false
3761
- // ...this.model?.['getInputArgs']?.(), // 避免部分关系字段信息丢失, 仿照 ClickableCollectionField 做法
3762
- ...opts,
3763
- },
3764
- {
3765
- debounce: true,
3766
- },
3767
- );
3794
+ await model.dispatchEvent(openEventName, {
3795
+ // navigation: false, // TODO: 路由模式有bug,不支持多层同样viewId的弹窗,因此这里默认先用false
3796
+ ...opts,
3797
+ });
3768
3798
  });
3769
3799
  this.defineMethod('getEvents', function (this: BaseFlowModelContext) {
3770
3800
  return this.model.getEvents();
@@ -46,12 +46,17 @@ export function createJSRunnerWithVersion(this: FlowContext, options?: JSRunnerO
46
46
  doc = {};
47
47
  }
48
48
  const deprecatedCtx = createRunJSDeprecationProxy(runCtx, { doc });
49
- const globals: Record<string, any> = { ctx: deprecatedCtx, ...(options?.globals || {}) };
50
- // 对字段/区块类上下文,默认注入 window/document 以支持在沙箱中访问 DOM API
51
- if (modelClass === 'JSFieldModel' || modelClass === 'JSBlockModel') {
52
- if (typeof window !== 'undefined') globals.window = window as any;
53
- if (typeof document !== 'undefined') globals.document = document as any;
49
+ const browserGlobals: Record<string, any> = {};
50
+ if (typeof window !== 'undefined') {
51
+ browserGlobals.window = window;
52
+ if (typeof navigator !== 'undefined') {
53
+ browserGlobals.navigator = navigator;
54
+ }
54
55
  }
56
+ if (typeof document !== 'undefined') {
57
+ browserGlobals.document = document;
58
+ }
59
+ const globals: Record<string, any> = { ctx: deprecatedCtx, ...browserGlobals, ...(options?.globals || {}) };
55
60
  // 透传 JSRunnerOptions 其余配置(如 timeoutMs)
56
61
  const { timeoutMs } = options || {};
57
62
  return new JSRunner({ globals, timeoutMs });
@@ -59,7 +64,8 @@ export function createJSRunnerWithVersion(this: FlowContext, options?: JSRunnerO
59
64
 
60
65
  export function getRunJSScenesForModel(modelClass: string, version: RunJSVersion = 'v1'): string[] {
61
66
  const meta = RunJSContextRegistry.getMeta(version, modelClass);
62
- return Array.isArray(meta?.scenes) ? [...meta!.scenes!] : [];
67
+ const scenes = meta?.scenes;
68
+ return Array.isArray(scenes) ? [...scenes] : [];
63
69
  }
64
70
 
65
71
  export function getRunJSScenesForContext(ctx: FlowContext, { version = 'v1' as RunJSVersion } = {}): string[] {
@@ -77,15 +77,6 @@ export {
77
77
  serializeCtxDateValue,
78
78
  } from './dateVariable';
79
79
 
80
- // 安全全局对象(window/document)
81
- export {
82
- createSafeDocument,
83
- createSafeWindow,
84
- createSafeNavigator,
85
- createSafeRunJSGlobals,
86
- runjsWithSafeGlobals,
87
- } from './safeGlobals';
88
-
89
80
  // RunJS value helpers
90
81
  export { isRunJSValue, normalizeRunJSValue, extractUsedVariablePathsFromRunJS, type RunJSValue } from './runjsValue';
91
82
 
@@ -8,7 +8,6 @@
8
8
  */
9
9
 
10
10
  import { isRunJSValue, normalizeRunJSValue } from './runjsValue';
11
- import { runjsWithSafeGlobals } from './safeGlobals';
12
11
 
13
12
  /**
14
13
  * Resolve an object's values, executing any RunJSValue entries via ctx.runjs.
@@ -29,7 +28,11 @@ export async function resolveRunJSObjectValues(ctx: unknown, raw: unknown): Prom
29
28
  if (isRunJSValue(value)) {
30
29
  const { code, version } = normalizeRunJSValue(value);
31
30
  if (!code.trim()) continue;
32
- const ret = await runjsWithSafeGlobals(ctx, code, { version });
31
+ const runjsCtx = ctx as
32
+ | { runjs?: (code: string, variables?: Record<string, any>, options?: Record<string, any>) => Promise<any> }
33
+ | undefined
34
+ | null;
35
+ const ret = await runjsCtx?.runjs?.(code, undefined, { version });
33
36
  if (!ret?.success) {
34
37
  throw new Error(`RunJS execution failed for "${key}"`);
35
38
  }
@@ -9,7 +9,6 @@
9
9
 
10
10
  import { setRunJSLibOverride } from '../runjsLibs';
11
11
  import { resolveModuleUrl } from './resolveModuleUrl';
12
- import { registerRunJSSafeDocumentGlobals, registerRunJSSafeWindowGlobals } from './safeGlobals';
13
12
 
14
13
  /**
15
14
  * RunJS 外部模块加载辅助(浏览器侧)。
@@ -38,26 +37,6 @@ type ParsedPackageSpecifier = {
38
37
  subpath?: string;
39
38
  };
40
39
 
41
- function snapshotOwnKeys(obj: any): string[] {
42
- try {
43
- if (!obj || (typeof obj !== 'object' && typeof obj !== 'function')) return [];
44
- return Object.getOwnPropertyNames(obj);
45
- } catch (_) {
46
- return [];
47
- }
48
- }
49
-
50
- function diffAddedKeys(afterKeys: string[], beforeKeys: string[]): string[] {
51
- if (!afterKeys.length) return [];
52
- if (!beforeKeys.length) return [...afterKeys];
53
- const beforeSet = new Set(beforeKeys);
54
- const added: string[] = [];
55
- for (const k of afterKeys) {
56
- if (!beforeSet.has(k)) added.push(k);
57
- }
58
- return added;
59
- }
60
-
61
40
  /**
62
41
  * 使用全局 Promise 链实现“互斥锁”:
63
42
  * - 锁存放在 `globalThis.__nocobaseRunjsModuleLoadLock`;
@@ -298,9 +277,6 @@ async function prefetchEsmModule(url: string, options?: { timeoutMs?: number }):
298
277
  */
299
278
  export async function runjsRequireAsync(requirejs: RequireJsLike, url: string): Promise<any> {
300
279
  return await withRunjsModuleLoadLock(async () => {
301
- const beforeWinKeys = typeof window !== 'undefined' ? snapshotOwnKeys(window) : [];
302
- const beforeDocKeys = typeof document !== 'undefined' ? snapshotOwnKeys(document) : [];
303
-
304
280
  let result: any;
305
281
  let error: any;
306
282
  try {
@@ -319,14 +295,6 @@ export async function runjsRequireAsync(requirejs: RequireJsLike, url: string):
319
295
  });
320
296
  } catch (e) {
321
297
  error = e;
322
- } finally {
323
- const afterWinKeys = typeof window !== 'undefined' ? snapshotOwnKeys(window) : [];
324
- const afterDocKeys = typeof document !== 'undefined' ? snapshotOwnKeys(document) : [];
325
- const addedWinKeys = diffAddedKeys(afterWinKeys, beforeWinKeys);
326
- const addedDocKeys = diffAddedKeys(afterDocKeys, beforeDocKeys);
327
- // Best-effort: allow RunJS safe window/document to access globals introduced by this module load.
328
- registerRunJSSafeWindowGlobals(addedWinKeys);
329
- registerRunJSSafeDocumentGlobals(addedDocKeys);
330
298
  }
331
299
 
332
300
  if (error) throw error;
@@ -1,28 +0,0 @@
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
- export declare function registerRunJSSafeWindowGlobals(keys: Iterable<string> | null | undefined): void;
10
- export declare function registerRunJSSafeDocumentGlobals(keys: Iterable<string> | null | undefined): void;
11
- export declare function __resetRunJSSafeGlobalsRegistryForTests(): void;
12
- export declare function createSafeWindow(extra?: Record<string, any>): Record<string, any>;
13
- export declare function createSafeDocument(extra?: Record<string, any>): Record<string, any>;
14
- export declare function createSafeNavigator(extra?: Record<string, any>): {};
15
- /**
16
- * Create a safe globals object for RunJS execution.
17
- *
18
- * - Always tries to provide `navigator`
19
- * - Best-effort provides `window` and `document` in browser environments
20
- * - Never throws (so callers can decide how to handle missing globals)
21
- */
22
- export declare function createSafeRunJSGlobals(extraGlobals?: Record<string, any>): Record<string, any>;
23
- /**
24
- * Execute RunJS with safe globals (window/document/navigator).
25
- *
26
- * Keeps `this` binding by calling `ctx.runjs(...)` instead of passing bare function references.
27
- */
28
- export declare function runjsWithSafeGlobals(ctx: unknown, code: string, options?: any, extraGlobals?: Record<string, any>): Promise<any>;