@nocobase/flow-engine 2.2.0-beta.3 → 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.
- package/lib/JSRunner.d.ts +1 -0
- package/lib/JSRunner.js +110 -20
- package/lib/flowContext.js +55 -28
- package/lib/flowSettings.d.ts +5 -1
- package/lib/flowSettings.js +70 -0
- package/lib/runjs-context/helpers.js +12 -5
- package/lib/types.d.ts +12 -0
- package/lib/utils/index.d.ts +0 -1
- package/lib/utils/index.js +0 -11
- package/lib/utils/resolveRunJSObjectValues.js +3 -2
- package/lib/utils/runjsModuleLoader.js +0 -30
- package/package.json +4 -4
- package/src/JSRunner.ts +112 -25
- package/src/__tests__/JSRunner.test.ts +4 -5
- package/src/__tests__/flowModel.openView.navigation.test.ts +28 -0
- package/src/__tests__/flowSettings.test.ts +72 -0
- package/src/flowContext.ts +55 -25
- package/src/flowSettings.ts +85 -1
- package/src/runjs-context/helpers.ts +12 -6
- package/src/types.ts +14 -0
- package/src/utils/index.ts +0 -9
- package/src/utils/resolveRunJSObjectValues.ts +5 -2
- package/src/utils/runjsModuleLoader.ts +0 -32
- package/lib/utils/safeGlobals.d.ts +0 -28
- package/lib/utils/safeGlobals.js +0 -367
- package/src/utils/__tests__/runjsRequireAsyncAutoWhitelist.test.ts +0 -38
- package/src/utils/__tests__/safeGlobals.test.ts +0 -106
- package/src/utils/safeGlobals.ts +0 -406
package/src/JSRunner.ts
CHANGED
|
@@ -39,6 +39,117 @@ export function shouldPreprocessRunJSTemplates(
|
|
|
39
39
|
return options?.version !== 'v2';
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
const RUNJS_BROWSER_GLOBAL_NAMES = [
|
|
43
|
+
'fetch',
|
|
44
|
+
'localStorage',
|
|
45
|
+
'sessionStorage',
|
|
46
|
+
'XMLHttpRequest',
|
|
47
|
+
'WebSocket',
|
|
48
|
+
'Worker',
|
|
49
|
+
'SharedWorker',
|
|
50
|
+
'ServiceWorker',
|
|
51
|
+
'BroadcastChannel',
|
|
52
|
+
'EventSource',
|
|
53
|
+
'indexedDB',
|
|
54
|
+
'caches',
|
|
55
|
+
'Function',
|
|
56
|
+
'eval',
|
|
57
|
+
'globalThis',
|
|
58
|
+
'Intl',
|
|
59
|
+
'Blob',
|
|
60
|
+
'URL',
|
|
61
|
+
'location',
|
|
62
|
+
] as const;
|
|
63
|
+
|
|
64
|
+
export const RUNJS_ALLOWED_BARE_GLOBAL_NAMES = [
|
|
65
|
+
'ctx',
|
|
66
|
+
'console',
|
|
67
|
+
'window',
|
|
68
|
+
'document',
|
|
69
|
+
'navigator',
|
|
70
|
+
'setTimeout',
|
|
71
|
+
'clearTimeout',
|
|
72
|
+
'setInterval',
|
|
73
|
+
'clearInterval',
|
|
74
|
+
'Array',
|
|
75
|
+
'ArrayBuffer',
|
|
76
|
+
'BigInt',
|
|
77
|
+
'BigInt64Array',
|
|
78
|
+
'BigUint64Array',
|
|
79
|
+
'Boolean',
|
|
80
|
+
'DataView',
|
|
81
|
+
'Date',
|
|
82
|
+
'Error',
|
|
83
|
+
'EvalError',
|
|
84
|
+
'FinalizationRegistry',
|
|
85
|
+
'Float32Array',
|
|
86
|
+
'Float64Array',
|
|
87
|
+
'Int8Array',
|
|
88
|
+
'Int16Array',
|
|
89
|
+
'Int32Array',
|
|
90
|
+
'Map',
|
|
91
|
+
'Math',
|
|
92
|
+
'Number',
|
|
93
|
+
'Object',
|
|
94
|
+
'Promise',
|
|
95
|
+
'Proxy',
|
|
96
|
+
'RangeError',
|
|
97
|
+
'ReferenceError',
|
|
98
|
+
'Reflect',
|
|
99
|
+
'RegExp',
|
|
100
|
+
'Set',
|
|
101
|
+
'String',
|
|
102
|
+
'Symbol',
|
|
103
|
+
'SyntaxError',
|
|
104
|
+
'TypeError',
|
|
105
|
+
'URIError',
|
|
106
|
+
'Uint8Array',
|
|
107
|
+
'Uint8ClampedArray',
|
|
108
|
+
'Uint16Array',
|
|
109
|
+
'Uint32Array',
|
|
110
|
+
'WeakMap',
|
|
111
|
+
'WeakRef',
|
|
112
|
+
'WeakSet',
|
|
113
|
+
'JSON',
|
|
114
|
+
'decodeURI',
|
|
115
|
+
'decodeURIComponent',
|
|
116
|
+
'encodeURI',
|
|
117
|
+
'encodeURIComponent',
|
|
118
|
+
'isFinite',
|
|
119
|
+
'isNaN',
|
|
120
|
+
'parseFloat',
|
|
121
|
+
'parseInt',
|
|
122
|
+
'undefined',
|
|
123
|
+
'NaN',
|
|
124
|
+
'Infinity',
|
|
125
|
+
...RUNJS_BROWSER_GLOBAL_NAMES,
|
|
126
|
+
] as const;
|
|
127
|
+
|
|
128
|
+
function collectRunJSBrowserGlobals(providedGlobals: Record<string, unknown> = {}) {
|
|
129
|
+
const windowGlobal = providedGlobals.window;
|
|
130
|
+
if (!windowGlobal || typeof windowGlobal !== 'object') {
|
|
131
|
+
return {};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const windowRecord = windowGlobal as Record<string, unknown>;
|
|
135
|
+
const globals: Record<string, unknown> = {};
|
|
136
|
+
RUNJS_BROWSER_GLOBAL_NAMES.forEach((name) => {
|
|
137
|
+
if (Object.prototype.hasOwnProperty.call(providedGlobals, name)) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const value = windowRecord[name];
|
|
142
|
+
if (typeof value === 'undefined') {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
globals[name] = name === 'fetch' && typeof value === 'function' ? value.bind(windowGlobal) : value;
|
|
146
|
+
} catch {
|
|
147
|
+
// Ignore browser globals that cannot be read in the current environment.
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
return globals;
|
|
151
|
+
}
|
|
152
|
+
|
|
42
153
|
// Heuristic: detect likely bare `{{ctx.xxx}}` usage in executable positions (not quoted string literals).
|
|
43
154
|
const BARE_CTX_TEMPLATE_RE = /(^|[=(:,[\s)])(\{\{\s*(ctx(?:\.|\[|\?\.)[^}]*)\s*\}\})/m;
|
|
44
155
|
|
|
@@ -98,31 +209,7 @@ export class JSRunner {
|
|
|
98
209
|
};
|
|
99
210
|
|
|
100
211
|
const providedGlobals = options.globals || {};
|
|
101
|
-
const liftedGlobals
|
|
102
|
-
|
|
103
|
-
// Auto-lift selected globals from safe window into top-level sandbox globals
|
|
104
|
-
// so user code can access them directly (e.g. `new Blob(...)`).
|
|
105
|
-
if (!Object.prototype.hasOwnProperty.call(providedGlobals, 'Blob')) {
|
|
106
|
-
try {
|
|
107
|
-
const blobCtor = (providedGlobals as any).window?.Blob;
|
|
108
|
-
if (typeof blobCtor !== 'undefined') {
|
|
109
|
-
liftedGlobals.Blob = blobCtor;
|
|
110
|
-
}
|
|
111
|
-
} catch {
|
|
112
|
-
// ignore when window proxy blocks property access
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (!Object.prototype.hasOwnProperty.call(providedGlobals, 'URL')) {
|
|
117
|
-
try {
|
|
118
|
-
const urlCtor = (providedGlobals as any).window?.URL;
|
|
119
|
-
if (typeof urlCtor !== 'undefined') {
|
|
120
|
-
liftedGlobals.URL = urlCtor;
|
|
121
|
-
}
|
|
122
|
-
} catch {
|
|
123
|
-
// ignore when window proxy blocks property access
|
|
124
|
-
}
|
|
125
|
-
}
|
|
212
|
+
const liftedGlobals = collectRunJSBrowserGlobals(providedGlobals);
|
|
126
213
|
|
|
127
214
|
this.globals = {
|
|
128
215
|
console,
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
});
|
|
@@ -557,6 +557,78 @@ describe('FlowSettings', () => {
|
|
|
557
557
|
});
|
|
558
558
|
});
|
|
559
559
|
|
|
560
|
+
describe('Dynamic Flow Source Providers', () => {
|
|
561
|
+
test('should return current model as the default dynamic flow source', async () => {
|
|
562
|
+
const model = new FlowModel({ uid: 'source-model', flowEngine: engine });
|
|
563
|
+
|
|
564
|
+
const sources = await flowSettings.getDynamicFlowSources(model);
|
|
565
|
+
|
|
566
|
+
expect(sources).toHaveLength(1);
|
|
567
|
+
expect(sources[0].key).toBe('self');
|
|
568
|
+
expect(sources[0].label).toBe('Current block');
|
|
569
|
+
expect(sources[0].model).toBe(model);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
test('should register, resolve, and dispose dynamic flow source providers', async () => {
|
|
573
|
+
const model = new FlowModel({ uid: 'source-model', flowEngine: engine });
|
|
574
|
+
const target = new FlowModel({ uid: 'target-model', flowEngine: engine });
|
|
575
|
+
|
|
576
|
+
const dispose = flowSettings.registerDynamicFlowSourceProvider({
|
|
577
|
+
key: 'test-provider',
|
|
578
|
+
visible: (m) => m.uid === model.uid,
|
|
579
|
+
getSources: () => [{ key: 'target', label: 'Target model', model: target, sort: 10 }],
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
expect(flowSettings.hasDynamicFlowSourceProvider(model)).toBe(true);
|
|
583
|
+
expect(flowSettings.hasDynamicFlowSourceProvider(target)).toBe(false);
|
|
584
|
+
|
|
585
|
+
const sources = await flowSettings.getDynamicFlowSources(model);
|
|
586
|
+
expect(sources.map((source) => source.key)).toEqual(['self', 'target']);
|
|
587
|
+
|
|
588
|
+
dispose();
|
|
589
|
+
|
|
590
|
+
expect(flowSettings.hasDynamicFlowSourceProvider(model)).toBe(false);
|
|
591
|
+
await expect(flowSettings.getDynamicFlowSources(model)).resolves.toHaveLength(1);
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
test('should skip duplicate dynamic flow source keys and model uids', async () => {
|
|
595
|
+
const model = new FlowModel({ uid: 'source-model', flowEngine: engine });
|
|
596
|
+
const target = new FlowModel({ uid: 'target-model', flowEngine: engine });
|
|
597
|
+
|
|
598
|
+
flowSettings.registerDynamicFlowSourceProvider({
|
|
599
|
+
key: 'test-provider',
|
|
600
|
+
getSources: () => [
|
|
601
|
+
{ key: 'target', label: 'Target model', model: target },
|
|
602
|
+
{ key: 'target', label: 'Duplicate key', model: new FlowModel({ uid: 'other-model', flowEngine: engine }) },
|
|
603
|
+
{ key: 'duplicate-uid', label: 'Duplicate uid', model: target },
|
|
604
|
+
],
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
const sources = await flowSettings.getDynamicFlowSources(model);
|
|
608
|
+
|
|
609
|
+
expect(sources.map((source) => source.key)).toEqual(['self', 'target']);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
test('should ignore failing dynamic flow source providers', async () => {
|
|
613
|
+
const model = new FlowModel({ uid: 'source-model', flowEngine: engine });
|
|
614
|
+
|
|
615
|
+
flowSettings.registerDynamicFlowSourceProvider({
|
|
616
|
+
key: 'failing-provider',
|
|
617
|
+
getSources: () => {
|
|
618
|
+
throw new Error('provider failed');
|
|
619
|
+
},
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
const sources = await flowSettings.getDynamicFlowSources(model);
|
|
623
|
+
|
|
624
|
+
expect(sources.map((source) => source.key)).toEqual(['self']);
|
|
625
|
+
expect(consoleSpy.warn).toHaveBeenCalledWith(
|
|
626
|
+
"FlowSettings: Dynamic flow source provider 'failing-provider' failed.",
|
|
627
|
+
expect.any(Error),
|
|
628
|
+
);
|
|
629
|
+
});
|
|
630
|
+
});
|
|
631
|
+
|
|
560
632
|
describe('Step Settings Dialog', () => {
|
|
561
633
|
test('should call openStepSettingsDialog with correct parameters', async () => {
|
|
562
634
|
const { openStepSettingsDialog } = await import('../components/settings/wrappers/contextual/StepSettingsDialog');
|
package/src/flowContext.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
...
|
|
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
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
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();
|
package/src/flowSettings.ts
CHANGED
|
@@ -20,7 +20,13 @@ import { FlowRuntimeContext } from './flowContext';
|
|
|
20
20
|
import { FlowEngine, untracked } from '.';
|
|
21
21
|
import { FlowSettingsContextProvider, useFlowSettingsContext } from './hooks/useFlowSettingsContext';
|
|
22
22
|
import type { FlowModel } from './models';
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
DynamicFlowSource,
|
|
25
|
+
DynamicFlowSourceProvider,
|
|
26
|
+
ParamObject,
|
|
27
|
+
StepSettingsDialogProps,
|
|
28
|
+
ToolbarItemConfig,
|
|
29
|
+
} from './types';
|
|
24
30
|
import {
|
|
25
31
|
compileUiSchema,
|
|
26
32
|
FlowCancelSaveException,
|
|
@@ -128,6 +134,7 @@ export class FlowSettings {
|
|
|
128
134
|
private engine: FlowEngine;
|
|
129
135
|
#forceEnabled = false; // 强制启用状态,主要用于设计模式下的强制启用
|
|
130
136
|
public toolbarItems: ToolbarItemConfig[] = [];
|
|
137
|
+
private dynamicFlowSourceProviders: DynamicFlowSourceProvider[] = [];
|
|
131
138
|
#emitter: Emitter = new Emitter();
|
|
132
139
|
|
|
133
140
|
constructor(engine: FlowEngine) {
|
|
@@ -463,6 +470,83 @@ export class FlowSettings {
|
|
|
463
470
|
return [...this.toolbarItems];
|
|
464
471
|
}
|
|
465
472
|
|
|
473
|
+
public registerDynamicFlowSourceProvider(provider: DynamicFlowSourceProvider): () => void {
|
|
474
|
+
const existingIndex = this.dynamicFlowSourceProviders.findIndex((item) => item.key === provider.key);
|
|
475
|
+
if (existingIndex !== -1) {
|
|
476
|
+
console.warn(
|
|
477
|
+
`FlowSettings: Dynamic flow source provider with key '${provider.key}' already exists and will be replaced.`,
|
|
478
|
+
);
|
|
479
|
+
this.dynamicFlowSourceProviders[existingIndex] = provider;
|
|
480
|
+
} else {
|
|
481
|
+
this.dynamicFlowSourceProviders.push(provider);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
this.dynamicFlowSourceProviders.sort((a, b) => (a.sort || 0) - (b.sort || 0));
|
|
485
|
+
|
|
486
|
+
return () => {
|
|
487
|
+
const index = this.dynamicFlowSourceProviders.indexOf(provider);
|
|
488
|
+
if (index !== -1) {
|
|
489
|
+
this.dynamicFlowSourceProviders.splice(index, 1);
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
public hasDynamicFlowSourceProvider(model: FlowModel): boolean {
|
|
495
|
+
return this.dynamicFlowSourceProviders.some((provider) => {
|
|
496
|
+
try {
|
|
497
|
+
return provider.visible ? provider.visible(model) : true;
|
|
498
|
+
} catch (error) {
|
|
499
|
+
console.warn(`FlowSettings: Dynamic flow source provider '${provider.key}' visibility check failed.`, error);
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
public async getDynamicFlowSources(model: FlowModel): Promise<DynamicFlowSource[]> {
|
|
506
|
+
const t = getT(model);
|
|
507
|
+
const selfSource: DynamicFlowSource = {
|
|
508
|
+
key: 'self',
|
|
509
|
+
label: t('Current block'),
|
|
510
|
+
model,
|
|
511
|
+
sort: -1000,
|
|
512
|
+
};
|
|
513
|
+
const sources: DynamicFlowSource[] = [];
|
|
514
|
+
const seenKeys = new Set<string>(['self']);
|
|
515
|
+
const seenModelUids = new Set<string>([model.uid]);
|
|
516
|
+
|
|
517
|
+
for (const provider of this.dynamicFlowSourceProviders) {
|
|
518
|
+
try {
|
|
519
|
+
if (provider.visible && !provider.visible(model)) {
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const providerSources = await provider.getSources(model);
|
|
524
|
+
for (const source of providerSources || []) {
|
|
525
|
+
if (!source?.key || !source.model) {
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
const key = String(source.key);
|
|
529
|
+
const uid = source.model.uid;
|
|
530
|
+
if (seenKeys.has(key) || seenModelUids.has(uid)) {
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
seenKeys.add(key);
|
|
534
|
+
seenModelUids.add(uid);
|
|
535
|
+
sources.push({
|
|
536
|
+
...source,
|
|
537
|
+
key,
|
|
538
|
+
label: source.label || key,
|
|
539
|
+
sort: source.sort || 0,
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
} catch (error) {
|
|
543
|
+
console.warn(`FlowSettings: Dynamic flow source provider '${provider.key}' failed.`, error);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return [selfSource, ...sources.sort((a, b) => (a.sort || 0) - (b.sort || 0))];
|
|
548
|
+
}
|
|
549
|
+
|
|
466
550
|
/**
|
|
467
551
|
* 清空所有工具栏项目
|
|
468
552
|
* @example
|
|
@@ -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
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (typeof
|
|
53
|
-
|
|
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
|
-
|
|
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[] {
|
package/src/types.ts
CHANGED
|
@@ -612,6 +612,20 @@ export interface ToolbarItemConfig {
|
|
|
612
612
|
sort?: number;
|
|
613
613
|
}
|
|
614
614
|
|
|
615
|
+
export interface DynamicFlowSource {
|
|
616
|
+
key: string;
|
|
617
|
+
label: React.ReactNode;
|
|
618
|
+
model: FlowModel;
|
|
619
|
+
sort?: number;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
export interface DynamicFlowSourceProvider {
|
|
623
|
+
key: string;
|
|
624
|
+
sort?: number;
|
|
625
|
+
visible?: (model: FlowModel) => boolean;
|
|
626
|
+
getSources: (model: FlowModel) => DynamicFlowSource[] | Promise<DynamicFlowSource[]>;
|
|
627
|
+
}
|
|
628
|
+
|
|
615
629
|
export interface ApplyFlowCacheEntry {
|
|
616
630
|
status: 'pending' | 'resolved' | 'rejected';
|
|
617
631
|
promise: Promise<any>;
|
package/src/utils/index.ts
CHANGED
|
@@ -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
|
|
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
|
}
|