@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 +23 -1
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +3 -3
- package/lib/data-source/index.d.ts +7 -0
- package/lib/data-source/index.js +72 -0
- package/lib/flowContext.js +12 -3
- package/lib/flowEngine.js +18 -8
- package/lib/resources/sqlResource.d.ts +3 -3
- package/lib/utils/safeGlobals.js +2 -0
- package/package.json +4 -4
- package/src/JSRunner.ts +29 -1
- package/src/__tests__/JSRunner.test.ts +64 -0
- package/src/__tests__/flowContext.test.ts +78 -0
- package/src/__tests__/flowModel.openView.navigation.test.ts +28 -0
- package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +3 -3
- package/src/data-source/index.ts +78 -0
- package/src/flowContext.ts +12 -2
- package/src/flowEngine.ts +18 -8
- package/src/resources/sqlResource.ts +3 -3
- package/src/utils/__tests__/safeGlobals.test.ts +8 -0
- package/src/utils/safeGlobals.ts +3 -1
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
|
-
...
|
|
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:
|
|
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:
|
|
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:
|
|
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>;
|
package/lib/data-source/index.js
CHANGED
|
@@ -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;
|
package/lib/flowContext.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
|
766
|
-
if (
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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
|
-
|
|
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:
|
|
24
|
-
constructor(ctx:
|
|
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>;
|
package/lib/utils/safeGlobals.js
CHANGED
|
@@ -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.
|
|
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.
|
|
12
|
-
"@nocobase/shared": "2.0.0-alpha.
|
|
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": "
|
|
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
|
-
...
|
|
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={
|
|
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:
|
|
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:
|
|
674
|
+
label: stepInfo.title,
|
|
675
675
|
});
|
|
676
676
|
});
|
|
677
677
|
});
|
package/src/data-source/index.ts
CHANGED
|
@@ -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
|
package/src/flowContext.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
|
911
|
-
if (
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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
|
-
|
|
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:
|
|
30
|
+
protected ctx: FlowContext;
|
|
31
31
|
|
|
32
|
-
constructor(ctx:
|
|
33
|
-
this.ctx = new FlowContext()
|
|
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
|
});
|
package/src/utils/safeGlobals.ts
CHANGED
|
@@ -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 代理
|