@nocobase/flow-engine 2.0.0-alpha.70 → 2.0.0-alpha.71

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.
package/lib/JSRunner.js CHANGED
@@ -35,6 +35,7 @@ const _JSRunner = class _JSRunner {
35
35
  globals;
36
36
  timeoutMs;
37
37
  constructor(options = {}) {
38
+ var _a, _b;
38
39
  const bindWindowFn = /* @__PURE__ */ __name((key) => {
39
40
  if (typeof window !== "undefined" && typeof window[key] === "function") {
40
41
  return window[key].bind(window);
@@ -42,13 +43,34 @@ const _JSRunner = class _JSRunner {
42
43
  const fn = globalThis[key];
43
44
  return typeof fn === "function" ? fn.bind(globalThis) : fn;
44
45
  }, "bindWindowFn");
46
+ const providedGlobals = options.globals || {};
47
+ const liftedGlobals = {};
48
+ if (!Object.prototype.hasOwnProperty.call(providedGlobals, "Blob")) {
49
+ try {
50
+ const blobCtor = (_a = providedGlobals.window) == null ? void 0 : _a.Blob;
51
+ if (typeof blobCtor !== "undefined") {
52
+ liftedGlobals.Blob = blobCtor;
53
+ }
54
+ } catch {
55
+ }
56
+ }
57
+ if (!Object.prototype.hasOwnProperty.call(providedGlobals, "URL")) {
58
+ try {
59
+ const urlCtor = (_b = providedGlobals.window) == null ? void 0 : _b.URL;
60
+ if (typeof urlCtor !== "undefined") {
61
+ liftedGlobals.URL = urlCtor;
62
+ }
63
+ } catch {
64
+ }
65
+ }
45
66
  this.globals = {
46
67
  console,
47
68
  setTimeout: bindWindowFn("setTimeout"),
48
69
  clearTimeout: bindWindowFn("clearTimeout"),
49
70
  setInterval: bindWindowFn("setInterval"),
50
71
  clearInterval: bindWindowFn("clearInterval"),
51
- ...options.globals || {}
72
+ ...liftedGlobals,
73
+ ...providedGlobals
52
74
  };
53
75
  this.timeoutMs = options.timeoutMs ?? 5e3;
54
76
  }
@@ -521,7 +521,7 @@ const DefaultSettingsIcon = /* @__PURE__ */ __name(({
521
521
  };
522
522
  items.push({
523
523
  key: uniqueKey,
524
- label: /* @__PURE__ */ import_react.default.createElement(MenuLabelItem, { title: t(stepInfo.title), uiMode, itemProps })
524
+ label: /* @__PURE__ */ import_react.default.createElement(MenuLabelItem, { title: stepInfo.title, uiMode, itemProps })
525
525
  });
526
526
  });
527
527
  if (flow.options.divider === "bottom") {
@@ -555,7 +555,7 @@ const DefaultSettingsIcon = /* @__PURE__ */ __name(({
555
555
  const uniqueKey = generateUniqueKey(`${flow.key}:${stepInfo.stepKey}`);
556
556
  items.push({
557
557
  key: uniqueKey,
558
- label: t(stepInfo.title)
558
+ label: stepInfo.title
559
559
  });
560
560
  });
561
561
  });
@@ -567,7 +567,7 @@ const DefaultSettingsIcon = /* @__PURE__ */ __name(({
567
567
  const uniqueKey = generateUniqueKey(`${modelKey}:${flow.key}:${stepInfo.stepKey}`);
568
568
  subMenuChildren.push({
569
569
  key: uniqueKey,
570
- label: t(stepInfo.title)
570
+ label: stepInfo.title
571
571
  });
572
572
  });
573
573
  });
@@ -66,6 +66,11 @@ export interface CollectionOptions {
66
66
  export declare class CollectionManager {
67
67
  dataSource: DataSource;
68
68
  collections: Map<string, Collection>;
69
+ allCollectionsInheritChain: string[];
70
+ protected childrenCollectionsName: {
71
+ supportView?: string[];
72
+ notSupportView?: string[];
73
+ };
69
74
  constructor(dataSource: DataSource);
70
75
  get flowEngine(): FlowEngine;
71
76
  addCollection(collection: Collection | CollectionOptions): void;
@@ -83,6 +88,8 @@ export declare class CollectionManager {
83
88
  clearCollections(): void;
84
89
  getAssociation(associationName: string): CollectionField | undefined;
85
90
  getChildrenCollections(name: any): any[];
91
+ getChildrenCollectionsName(name: any, isSupportView?: boolean): string[];
92
+ getAllCollectionsInheritChain(name: any): string[];
86
93
  }
87
94
  export declare class Collection {
88
95
  fields: Map<string, CollectionField>;
@@ -188,6 +188,8 @@ const _CollectionManager = class _CollectionManager {
188
188
  this.collections = import_reactive.observable.shallow(/* @__PURE__ */ new Map());
189
189
  }
190
190
  collections;
191
+ allCollectionsInheritChain;
192
+ childrenCollectionsName = {};
191
193
  get flowEngine() {
192
194
  return this.dataSource.flowEngine;
193
195
  }
@@ -317,6 +319,76 @@ const _CollectionManager = class _CollectionManager {
317
319
  }, "getChildrens");
318
320
  return getChildrens(name);
319
321
  }
322
+ getChildrenCollectionsName(name, isSupportView = false) {
323
+ const cacheKey = isSupportView ? "supportView" : "notSupportView";
324
+ if (this.childrenCollectionsName[cacheKey]) {
325
+ return this.childrenCollectionsName[cacheKey].slice();
326
+ }
327
+ const children = [];
328
+ const collections = [...this.getCollections()];
329
+ const getChildrenCollectionsInner = /* @__PURE__ */ __name((collectionName) => {
330
+ const inheritCollections = collections.filter((v) => {
331
+ var _a;
332
+ return (_a = [...v.inherits]) == null ? void 0 : _a.includes(collectionName);
333
+ });
334
+ inheritCollections.forEach((v) => {
335
+ const collectionKey = v.name;
336
+ children.push(collectionKey);
337
+ return getChildrenCollectionsInner(collectionKey);
338
+ });
339
+ if (isSupportView) {
340
+ const sourceCollections = collections.filter((v) => {
341
+ var _a;
342
+ return ((_a = [...v.sources]) == null ? void 0 : _a.length) === 1 && (v == null ? void 0 : v.sources[0]) === collectionName;
343
+ });
344
+ sourceCollections.forEach((v) => {
345
+ const collectionKey = v.name;
346
+ children.push(v.name);
347
+ return getChildrenCollectionsInner(collectionKey);
348
+ });
349
+ }
350
+ return import_lodash.default.uniq(children);
351
+ }, "getChildrenCollectionsInner");
352
+ this.childrenCollectionsName[cacheKey] = getChildrenCollectionsInner(name);
353
+ return this.childrenCollectionsName[cacheKey];
354
+ }
355
+ getAllCollectionsInheritChain(name) {
356
+ if (this.allCollectionsInheritChain) {
357
+ return this.allCollectionsInheritChain.slice();
358
+ }
359
+ const collectionsInheritChain = [name];
360
+ const getInheritChain = /* @__PURE__ */ __name((name2) => {
361
+ const collection = this.getCollection(name2);
362
+ if (collection) {
363
+ const { inherits } = collection;
364
+ const children = this.getChildrenCollectionsName(name2);
365
+ if (inherits) {
366
+ for (let index = 0; index < inherits.length; index++) {
367
+ const collectionKey = inherits[index];
368
+ if (collectionsInheritChain.includes(collectionKey)) {
369
+ continue;
370
+ }
371
+ collectionsInheritChain.push(collectionKey);
372
+ getInheritChain(collectionKey);
373
+ }
374
+ }
375
+ if (children) {
376
+ for (let index = 0; index < children.length; index++) {
377
+ const collection2 = this.getCollection(children[index]);
378
+ const collectionKey = collection2.name;
379
+ if (collectionsInheritChain.includes(collectionKey)) {
380
+ continue;
381
+ }
382
+ collectionsInheritChain.push(collectionKey);
383
+ getInheritChain(collectionKey);
384
+ }
385
+ }
386
+ }
387
+ return collectionsInheritChain;
388
+ }, "getInheritChain");
389
+ this.allCollectionsInheritChain = getInheritChain(name);
390
+ return this.allCollectionsInheritChain || [];
391
+ }
320
392
  };
321
393
  __name(_CollectionManager, "CollectionManager");
322
394
  let CollectionManager = _CollectionManager;
@@ -830,7 +830,8 @@ const _FlowEngineContext = class _FlowEngineContext extends BaseFlowEngineContex
830
830
  value: this.engine
831
831
  });
832
832
  this.defineProperty("sql", {
833
- get: /* @__PURE__ */ __name(() => new import_resources.FlowSQLRepository(this), "get")
833
+ get: /* @__PURE__ */ __name((ctx) => new import_resources.FlowSQLRepository(ctx), "get"),
834
+ cache: false
834
835
  });
835
836
  this.defineProperty("dataSourceManager", {
836
837
  value: dataSourceManager
@@ -1207,7 +1208,7 @@ const _FlowModelContext = class _FlowModelContext extends BaseFlowModelContext {
1207
1208
  }, "get")
1208
1209
  });
1209
1210
  this.defineMethod("openView", async function(uid, options) {
1210
- var _a, _b, _c, _d;
1211
+ var _a, _b, _c, _d, _e;
1211
1212
  const opts = { ...options };
1212
1213
  if (opts.defineProperties || opts.defineMethods) {
1213
1214
  opts.navigation = false;
@@ -1257,8 +1258,16 @@ const _FlowModelContext = class _FlowModelContext extends BaseFlowModelContext {
1257
1258
  engineCtx: this.engine.context
1258
1259
  };
1259
1260
  model2.context.defineProperty("view", { value: pendingView });
1261
+ const popupFlow = (_e = model2.getFlow) == null ? void 0 : _e.call(model2, "popupSettings");
1262
+ const on = popupFlow == null ? void 0 : popupFlow.on;
1263
+ let openEventName = "click";
1264
+ if (typeof on === "string" && on) {
1265
+ openEventName = on;
1266
+ } else if (on && typeof on === "object" && typeof on.eventName === "string" && on.eventName) {
1267
+ openEventName = on.eventName;
1268
+ }
1260
1269
  await model2.dispatchEvent(
1261
- "click",
1270
+ openEventName,
1262
1271
  {
1263
1272
  // navigation: false, // TODO: 路由模式有bug,不支持多层同样viewId的弹窗,因此这里默认先用false
1264
1273
  // ...this.model?.['getInputArgs']?.(), // 避免部分关系字段信息丢失, 仿照 ClickableCollectionField 做法
package/lib/flowEngine.js CHANGED
@@ -762,16 +762,26 @@ const _FlowEngine = class _FlowEngine {
762
762
  */
763
763
  async loadModel(options) {
764
764
  if (!this.ensureModelRepository()) return;
765
- const model = this.findModelByParentId(options.parentId, options.subKey);
766
- if (model) {
767
- return model;
768
- }
769
- const hydrated = this.hydrateModelFromPreviousEngines(options);
770
- if (hydrated) {
771
- return hydrated;
765
+ const refresh = !!(options == null ? void 0 : options.refresh);
766
+ if (!refresh) {
767
+ const model = this.findModelByParentId(options.parentId, options.subKey);
768
+ if (model) {
769
+ return model;
770
+ }
771
+ const hydrated = this.hydrateModelFromPreviousEngines(options);
772
+ if (hydrated) {
773
+ return hydrated;
774
+ }
772
775
  }
773
776
  const data = await this._modelRepository.findOne(options);
774
- return (data == null ? void 0 : data.uid) ? this.createModel(data) : null;
777
+ if (!(data == null ? void 0 : data.uid)) return null;
778
+ if (refresh) {
779
+ const existing = this.getModel(data.uid);
780
+ if (existing) {
781
+ this.removeModelWithSubModels(existing.uid);
782
+ }
783
+ }
784
+ return this.createModel(data);
775
785
  }
776
786
  /**
777
787
  * Find a sub-model by parent model ID and subKey.
@@ -6,7 +6,7 @@
6
6
  * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
- import { FlowEngineContext } from '../flowContext';
9
+ import { FlowContext, FlowEngineContext } from '../flowContext';
10
10
  import { BaseRecordResource } from './baseRecordResource';
11
11
  type SQLRunOptions = {
12
12
  bind?: Record<string, any>;
@@ -20,8 +20,8 @@ type SQLSaveOptions = {
20
20
  dataSourceKey?: string;
21
21
  };
22
22
  export declare class FlowSQLRepository {
23
- protected ctx: FlowEngineContext;
24
- constructor(ctx: FlowEngineContext);
23
+ protected ctx: FlowContext;
24
+ constructor(ctx: FlowContext);
25
25
  run(sql: string, options?: SQLRunOptions): Promise<any>;
26
26
  save(data: SQLSaveOptions): Promise<void>;
27
27
  runById(uid: string, options?: SQLRunOptions): Promise<any>;
@@ -196,6 +196,8 @@ function createSafeWindow(extra) {
196
196
  Math,
197
197
  Date,
198
198
  FormData,
199
+ ...typeof Blob !== "undefined" ? { Blob } : {},
200
+ ...typeof URL !== "undefined" ? { URL } : {},
199
201
  // 事件侦听仅绑定到真实 window,便于少量需要的全局监听
200
202
  addEventListener: addEventListener.bind(window),
201
203
  // 安全的 window.open 代理
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/flow-engine",
3
- "version": "2.0.0-alpha.70",
3
+ "version": "2.0.0-alpha.71",
4
4
  "private": false,
5
5
  "description": "A standalone flow engine for NocoBase, managing workflows, models, and actions.",
6
6
  "main": "lib/index.js",
@@ -8,8 +8,8 @@
8
8
  "dependencies": {
9
9
  "@formily/antd-v5": "1.x",
10
10
  "@formily/reactive": "2.x",
11
- "@nocobase/sdk": "2.0.0-alpha.70",
12
- "@nocobase/shared": "2.0.0-alpha.70",
11
+ "@nocobase/sdk": "2.0.0-alpha.71",
12
+ "@nocobase/shared": "2.0.0-alpha.71",
13
13
  "ahooks": "^3.7.2",
14
14
  "dayjs": "^1.11.9",
15
15
  "dompurify": "^3.0.2",
@@ -36,5 +36,5 @@
36
36
  ],
37
37
  "author": "NocoBase Team",
38
38
  "license": "AGPL-3.0",
39
- "gitHead": "42556ed4b29d8410e104ffd8af4fa72c9b1bae7f"
39
+ "gitHead": "b6fc484eb698fa12fba02dd468a04e39079b1e79"
40
40
  }
package/src/JSRunner.ts CHANGED
@@ -34,13 +34,41 @@ export class JSRunner {
34
34
  return typeof fn === 'function' ? fn.bind(globalThis) : fn;
35
35
  };
36
36
 
37
+ const providedGlobals = options.globals || {};
38
+ const liftedGlobals: Record<string, any> = {};
39
+
40
+ // Auto-lift selected globals from safe window into top-level sandbox globals
41
+ // so user code can access them directly (e.g. `new Blob(...)`).
42
+ if (!Object.prototype.hasOwnProperty.call(providedGlobals, 'Blob')) {
43
+ try {
44
+ const blobCtor = (providedGlobals as any).window?.Blob;
45
+ if (typeof blobCtor !== 'undefined') {
46
+ liftedGlobals.Blob = blobCtor;
47
+ }
48
+ } catch {
49
+ // ignore when window proxy blocks property access
50
+ }
51
+ }
52
+
53
+ if (!Object.prototype.hasOwnProperty.call(providedGlobals, 'URL')) {
54
+ try {
55
+ const urlCtor = (providedGlobals as any).window?.URL;
56
+ if (typeof urlCtor !== 'undefined') {
57
+ liftedGlobals.URL = urlCtor;
58
+ }
59
+ } catch {
60
+ // ignore when window proxy blocks property access
61
+ }
62
+ }
63
+
37
64
  this.globals = {
38
65
  console,
39
66
  setTimeout: bindWindowFn('setTimeout'),
40
67
  clearTimeout: bindWindowFn('clearTimeout'),
41
68
  setInterval: bindWindowFn('setInterval'),
42
69
  clearInterval: bindWindowFn('clearInterval'),
43
- ...(options.globals || {}),
70
+ ...liftedGlobals,
71
+ ...providedGlobals,
44
72
  };
45
73
  this.timeoutMs = options.timeoutMs ?? 5000; // 默认 5 秒超时
46
74
  }
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
11
11
  import { JSRunner } from '../JSRunner';
12
+ import { createSafeWindow } from '../utils';
12
13
 
13
14
  describe('JSRunner', () => {
14
15
  let originalSearch: string;
@@ -48,6 +49,69 @@ describe('JSRunner', () => {
48
49
  expect(res2.value).toBe('baz');
49
50
  });
50
51
 
52
+ it('auto-lifts Blob from injected window to top-level globals', async () => {
53
+ if (typeof Blob === 'undefined') {
54
+ return;
55
+ }
56
+
57
+ const runner = new JSRunner({
58
+ globals: {
59
+ window: createSafeWindow(),
60
+ },
61
+ });
62
+
63
+ const result = await runner.run('return new Blob(["x"]).size');
64
+ expect(result.success).toBe(true);
65
+ expect(result.value).toBe(1);
66
+ });
67
+
68
+ it('keeps explicit globals.Blob higher priority than auto-lifted Blob', async () => {
69
+ const explicitBlob = function ExplicitBlob(this: any, chunks: any[]) {
70
+ this.size = Array.isArray(chunks) ? chunks.length : 0;
71
+ } as any;
72
+
73
+ const runner = new JSRunner({
74
+ globals: {
75
+ window: createSafeWindow(),
76
+ Blob: explicitBlob,
77
+ },
78
+ });
79
+
80
+ const result = await runner.run('const b = new Blob([1,2,3]); return b.size;');
81
+ expect(result.success).toBe(true);
82
+ expect(result.value).toBe(3);
83
+ });
84
+
85
+ it('auto-lifts URL from injected window to top-level globals', async () => {
86
+ const runner = new JSRunner({
87
+ globals: {
88
+ window: createSafeWindow(),
89
+ },
90
+ });
91
+
92
+ const result = await runner.run('return typeof URL.createObjectURL === "function"');
93
+ expect(result.success).toBe(true);
94
+ expect(result.value).toBe(true);
95
+ });
96
+
97
+ it('keeps explicit globals.URL higher priority than auto-lifted URL', async () => {
98
+ const explicitURL = {
99
+ createObjectURL: () => 'explicit://url',
100
+ revokeObjectURL: (_url: string) => undefined,
101
+ };
102
+
103
+ const runner = new JSRunner({
104
+ globals: {
105
+ window: createSafeWindow(),
106
+ URL: explicitURL,
107
+ },
108
+ });
109
+
110
+ const result = await runner.run('return URL.createObjectURL(new Blob(["x"]))');
111
+ expect(result.success).toBe(true);
112
+ expect(result.value).toBe('explicit://url');
113
+ });
114
+
51
115
  it('exposes console in sandbox by default', async () => {
52
116
  const runner = new JSRunner();
53
117
  const result = await runner.run('return typeof console !== "undefined"');
@@ -759,6 +759,84 @@ describe('FlowEngine context', () => {
759
759
  expect(engine.context.appName).toBe('NocoBase');
760
760
  });
761
761
 
762
+ it('ctx.sql should resolve template variables from caller context in delegate chain', async () => {
763
+ const engine = new FlowEngine();
764
+ const request = vi.fn(async () => ({ data: { data: [] } }));
765
+ engine.context.defineProperty('api', {
766
+ value: { request },
767
+ });
768
+ engine.context.defineProperty('minId', {
769
+ get: () => 999,
770
+ cache: false,
771
+ });
772
+
773
+ const callerCtx = new FlowContext();
774
+ callerCtx.addDelegate(engine.context);
775
+ callerCtx.defineProperty('minId', {
776
+ get: () => 1,
777
+ cache: false,
778
+ });
779
+
780
+ await callerCtx.sql.run('SELECT * FROM users WHERE id > {{ctx.minId}}', { type: 'selectRows' });
781
+
782
+ expect(request).toHaveBeenCalledTimes(1);
783
+ const config = request.mock.calls[0]?.[0];
784
+ expect(config?.url).toBe('flowSql:run');
785
+ expect(config?.data.bind.__var1).toBe(1);
786
+ });
787
+
788
+ it('ctx.sql should not share repository instance between different caller contexts', async () => {
789
+ const engine = new FlowEngine();
790
+ const request = vi.fn(async () => ({ data: { data: [] } }));
791
+ engine.context.defineProperty('api', {
792
+ value: { request },
793
+ });
794
+
795
+ const caller1 = new FlowContext();
796
+ caller1.addDelegate(engine.context);
797
+ caller1.defineProperty('minId', {
798
+ get: () => 1,
799
+ cache: false,
800
+ });
801
+
802
+ const caller2 = new FlowContext();
803
+ caller2.addDelegate(engine.context);
804
+ caller2.defineProperty('minId', {
805
+ get: () => 2,
806
+ cache: false,
807
+ });
808
+
809
+ expect(caller1.sql).not.toBe(caller2.sql);
810
+
811
+ await caller1.sql.run('SELECT * FROM users WHERE id > {{ctx.minId}}', { type: 'selectRows' });
812
+ await caller2.sql.run('SELECT * FROM users WHERE id > {{ctx.minId}}', { type: 'selectRows' });
813
+
814
+ expect(request).toHaveBeenCalledTimes(2);
815
+ const config1 = request.mock.calls[0]?.[0];
816
+ const config2 = request.mock.calls[1]?.[0];
817
+ expect(config1?.data.bind.__var1).toBe(1);
818
+ expect(config2?.data.bind.__var1).toBe(2);
819
+ });
820
+
821
+ it('engine.context.sql should keep working when accessed directly', async () => {
822
+ const engine = new FlowEngine();
823
+ const request = vi.fn(async () => ({ data: { data: [] } }));
824
+ engine.context.defineProperty('api', {
825
+ value: { request },
826
+ });
827
+ engine.context.defineProperty('minId', {
828
+ get: () => 3,
829
+ cache: false,
830
+ });
831
+
832
+ await engine.context.sql.run('SELECT * FROM users WHERE id > {{ctx.minId}}', { type: 'selectRows' });
833
+
834
+ expect(request).toHaveBeenCalledTimes(1);
835
+ const config = request.mock.calls[0]?.[0];
836
+ expect(config?.url).toBe('flowSql:run');
837
+ expect(config?.data.bind.__var1).toBe(3);
838
+ });
839
+
762
840
  it('engine.context.getVar should resolve variable by path', async () => {
763
841
  const engine = new FlowEngine();
764
842
  engine.context.defineProperty('foo', { value: { bar: 1 } });
@@ -76,4 +76,32 @@ describe('FlowModelContext.openView - navigation enforcement', () => {
76
76
  const dispatchedParams = child.dispatchEvent.mock.calls[0][1];
77
77
  expect(dispatchedParams.navigation).toBe(false);
78
78
  });
79
+
80
+ it('dispatches the popupSettings bound event (object form) when opening external popup', async () => {
81
+ const { parent, child } = setup();
82
+
83
+ child.getFlow = vi.fn((key: string) => {
84
+ if (key !== 'popupSettings') return undefined;
85
+ return { on: { eventName: 'openDuplicatePopup' } };
86
+ });
87
+
88
+ await (parent.context as any).openView('child-uid', { mode: 'drawer' });
89
+
90
+ expect(child.dispatchEvent).toHaveBeenCalledTimes(1);
91
+ expect(child.dispatchEvent.mock.calls[0][0]).toBe('openDuplicatePopup');
92
+ });
93
+
94
+ it('falls back to click when popupSettings has no explicit on event', async () => {
95
+ const { parent, child } = setup();
96
+
97
+ child.getFlow = vi.fn((key: string) => {
98
+ if (key !== 'popupSettings') return undefined;
99
+ return { on: undefined };
100
+ });
101
+
102
+ await (parent.context as any).openView('child-uid', { mode: 'drawer' });
103
+
104
+ expect(child.dispatchEvent).toHaveBeenCalledTimes(1);
105
+ expect(child.dispatchEvent.mock.calls[0][0]).toBe('click');
106
+ });
79
107
  });
@@ -614,7 +614,7 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
614
614
  };
615
615
  items.push({
616
616
  key: uniqueKey,
617
- label: <MenuLabelItem title={t(stepInfo.title)} uiMode={uiMode} itemProps={itemProps} />,
617
+ label: <MenuLabelItem title={stepInfo.title} uiMode={uiMode} itemProps={itemProps} />,
618
618
  });
619
619
  });
620
620
  if (flow.options.divider === 'bottom') {
@@ -656,7 +656,7 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
656
656
 
657
657
  items.push({
658
658
  key: uniqueKey,
659
- label: t(stepInfo.title),
659
+ label: stepInfo.title,
660
660
  });
661
661
  });
662
662
  });
@@ -671,7 +671,7 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
671
671
 
672
672
  subMenuChildren.push({
673
673
  key: uniqueKey,
674
- label: t(stepInfo.title),
674
+ label: stepInfo.title,
675
675
  });
676
676
  });
677
677
  });
@@ -187,6 +187,9 @@ export interface CollectionOptions {
187
187
  export class CollectionManager {
188
188
  collections: Map<string, Collection>;
189
189
 
190
+ allCollectionsInheritChain: string[];
191
+ protected childrenCollectionsName: { supportView?: string[]; notSupportView?: string[] } = {};
192
+
190
193
  constructor(public dataSource: DataSource) {
191
194
  this.collections = observable.shallow<Map<string, Collection>>(new Map());
192
195
  }
@@ -339,6 +342,81 @@ export class CollectionManager {
339
342
  };
340
343
  return getChildrens(name);
341
344
  }
345
+ getChildrenCollectionsName(name, isSupportView = false) {
346
+ const cacheKey = isSupportView ? 'supportView' : 'notSupportView';
347
+ if (this.childrenCollectionsName[cacheKey]) {
348
+ return this.childrenCollectionsName[cacheKey].slice();
349
+ }
350
+
351
+ const children: string[] = [];
352
+ const collections = [...this.getCollections()];
353
+ const getChildrenCollectionsInner = (collectionName: string) => {
354
+ const inheritCollections = collections.filter((v: any) => {
355
+ return [...v.inherits]?.includes(collectionName);
356
+ });
357
+ inheritCollections.forEach((v) => {
358
+ const collectionKey = v.name;
359
+ children.push(collectionKey);
360
+ return getChildrenCollectionsInner(collectionKey);
361
+ });
362
+ if (isSupportView) {
363
+ const sourceCollections = collections.filter((v: any) => {
364
+ return [...v.sources]?.length === 1 && v?.sources[0] === collectionName;
365
+ });
366
+ sourceCollections.forEach((v) => {
367
+ const collectionKey = v.name;
368
+ children.push(v.name);
369
+ return getChildrenCollectionsInner(collectionKey);
370
+ });
371
+ }
372
+ return _.uniq(children);
373
+ };
374
+
375
+ this.childrenCollectionsName[cacheKey] = getChildrenCollectionsInner(name);
376
+ return this.childrenCollectionsName[cacheKey];
377
+ }
378
+
379
+ getAllCollectionsInheritChain(name) {
380
+ if (this.allCollectionsInheritChain) {
381
+ return this.allCollectionsInheritChain.slice();
382
+ }
383
+
384
+ const collectionsInheritChain = [name];
385
+ const getInheritChain = (name: string) => {
386
+ const collection = this.getCollection(name);
387
+ if (collection) {
388
+ const { inherits } = collection as any;
389
+ const children = this.getChildrenCollectionsName(name);
390
+ // 搜寻祖先表
391
+ if (inherits) {
392
+ for (let index = 0; index < inherits.length; index++) {
393
+ const collectionKey = inherits[index];
394
+ if (collectionsInheritChain.includes(collectionKey)) {
395
+ continue;
396
+ }
397
+ collectionsInheritChain.push(collectionKey);
398
+ getInheritChain(collectionKey);
399
+ }
400
+ }
401
+ // 搜寻后代表
402
+ if (children) {
403
+ for (let index = 0; index < children.length; index++) {
404
+ const collection = this.getCollection(children[index]);
405
+ const collectionKey = collection.name;
406
+ if (collectionsInheritChain.includes(collectionKey)) {
407
+ continue;
408
+ }
409
+ collectionsInheritChain.push(collectionKey);
410
+ getInheritChain(collectionKey);
411
+ }
412
+ }
413
+ }
414
+ return collectionsInheritChain;
415
+ };
416
+
417
+ this.allCollectionsInheritChain = getInheritChain(name);
418
+ return this.allCollectionsInheritChain || [];
419
+ }
342
420
  }
343
421
 
344
422
  // Collection 负责管理自己的 Field
@@ -1144,7 +1144,8 @@ export class FlowEngineContext extends BaseFlowEngineContext {
1144
1144
  value: this.engine,
1145
1145
  });
1146
1146
  this.defineProperty('sql', {
1147
- get: () => new FlowSQLRepository(this),
1147
+ get: (ctx) => new FlowSQLRepository(ctx),
1148
+ cache: false,
1148
1149
  });
1149
1150
  this.defineProperty('dataSourceManager', {
1150
1151
  value: dataSourceManager,
@@ -1678,8 +1679,17 @@ export class FlowModelContext extends BaseFlowModelContext {
1678
1679
  engineCtx: this.engine.context,
1679
1680
  };
1680
1681
  model.context.defineProperty('view', { value: pendingView });
1682
+ // 默认按 click 打开,但兼容 popupSettings 绑定到其他事件(例如 DuplicateActionModel 监听 openDuplicatePopup)。
1683
+ const popupFlow = model.getFlow?.('popupSettings');
1684
+ const on = (popupFlow as any)?.on;
1685
+ let openEventName = 'click';
1686
+ if (typeof on === 'string' && on) {
1687
+ openEventName = on;
1688
+ } else if (on && typeof on === 'object' && typeof (on as any).eventName === 'string' && (on as any).eventName) {
1689
+ openEventName = (on as any).eventName;
1690
+ }
1681
1691
  await model.dispatchEvent(
1682
- 'click',
1692
+ openEventName,
1683
1693
  {
1684
1694
  // navigation: false, // TODO: 路由模式有bug,不支持多层同样viewId的弹窗,因此这里默认先用false
1685
1695
  // ...this.model?.['getInputArgs']?.(), // 避免部分关系字段信息丢失, 仿照 ClickableCollectionField 做法
package/src/flowEngine.ts CHANGED
@@ -907,16 +907,26 @@ export class FlowEngine {
907
907
  */
908
908
  async loadModel<T extends FlowModel = FlowModel>(options): Promise<T | null> {
909
909
  if (!this.ensureModelRepository()) return;
910
- const model = this.findModelByParentId(options.parentId, options.subKey);
911
- if (model) {
912
- return model as T;
913
- }
914
- const hydrated = this.hydrateModelFromPreviousEngines<T>(options);
915
- if (hydrated) {
916
- return hydrated as T;
910
+ const refresh = !!options?.refresh;
911
+ if (!refresh) {
912
+ const model = this.findModelByParentId(options.parentId, options.subKey);
913
+ if (model) {
914
+ return model as T;
915
+ }
916
+ const hydrated = this.hydrateModelFromPreviousEngines<T>(options);
917
+ if (hydrated) {
918
+ return hydrated as T;
919
+ }
917
920
  }
918
921
  const data = await this._modelRepository.findOne(options);
919
- return data?.uid ? this.createModel<T>(data as any) : null;
922
+ if (!data?.uid) return null;
923
+ if (refresh) {
924
+ const existing = this.getModel(data.uid);
925
+ if (existing) {
926
+ this.removeModelWithSubModels(existing.uid);
927
+ }
928
+ }
929
+ return this.createModel<T>(data as any);
920
930
  }
921
931
 
922
932
  /**
@@ -27,10 +27,10 @@ type SQLSaveOptions = {
27
27
  };
28
28
 
29
29
  export class FlowSQLRepository {
30
- protected ctx: FlowEngineContext;
30
+ protected ctx: FlowContext;
31
31
 
32
- constructor(ctx: FlowEngineContext) {
33
- this.ctx = new FlowContext() as FlowEngineContext;
32
+ constructor(ctx: FlowContext) {
33
+ this.ctx = new FlowContext();
34
34
  this.ctx.addDelegate(ctx);
35
35
  this.ctx.defineProperty('offset', {
36
36
  get: () => 0,
@@ -28,6 +28,14 @@ describe('safeGlobals', () => {
28
28
  expect(win.console).toBeDefined();
29
29
  expect(win.foo).toBe(123);
30
30
  expect(new win.FormData()).toBeInstanceOf(window.FormData);
31
+ if (typeof window.Blob !== 'undefined') {
32
+ expect(typeof win.Blob).toBe('function');
33
+ expect(new win.Blob(['x'])).toBeInstanceOf(window.Blob);
34
+ }
35
+ if (typeof window.URL !== 'undefined') {
36
+ expect(win.URL).toBe(window.URL);
37
+ expect(typeof win.URL.createObjectURL).toBe('function');
38
+ }
31
39
  // access to location proxy is allowed, but sensitive props throw
32
40
  expect(() => win.location.href).toThrow(/not allowed/);
33
41
  });
@@ -9,7 +9,7 @@
9
9
 
10
10
  /**
11
11
  * 统一的安全全局对象代理:window/document/navigator
12
- * - window:仅允许常用的定时器、console、Math、Date、FormData、addEventListener、open(安全包装)、location(安全代理)
12
+ * - window:仅允许常用的定时器、console、Math、Date、FormData、Blob、URL、addEventListener、open(安全包装)、location(安全代理)
13
13
  * - document:仅允许 createElement/querySelector/querySelectorAll
14
14
  * - navigator:仅提供极少量低风险能力(clipboard.writeText、onLine、language、languages)
15
15
  * - 不允许随意访问未声明的属性,最小权限原则
@@ -211,6 +211,8 @@ export function createSafeWindow(extra?: Record<string, any>) {
211
211
  Math,
212
212
  Date,
213
213
  FormData,
214
+ ...(typeof Blob !== 'undefined' ? { Blob } : {}),
215
+ ...(typeof URL !== 'undefined' ? { URL } : {}),
214
216
  // 事件侦听仅绑定到真实 window,便于少量需要的全局监听
215
217
  addEventListener: addEventListener.bind(window),
216
218
  // 安全的 window.open 代理