@nocobase/flow-engine 2.2.0-alpha.1 → 2.2.0-alpha.2
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/components/FieldModelRenderer.js +44 -31
- package/lib/components/FlowContextSelector.js +24 -3
- package/lib/components/dnd/index.js +9 -2
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +86 -32
- package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +20 -0
- package/lib/components/variables/VariableHybridInput.d.ts +8 -0
- package/lib/components/variables/VariableHybridInput.js +128 -12
- package/lib/components/variables/types.d.ts +8 -0
- package/lib/flowContext.d.ts +1 -1
- package/lib/flowContext.js +87 -36
- package/lib/flowI18n.js +3 -3
- package/lib/flowSettings.d.ts +5 -1
- package/lib/flowSettings.js +70 -0
- package/lib/locale/en-US.json +1 -0
- package/lib/locale/index.d.ts +2 -0
- package/lib/locale/zh-CN.json +1 -0
- package/lib/resources/apiResource.js +2 -1
- package/lib/resources/baseRecordResource.js +6 -17
- package/lib/resources/multiRecordResource.js +13 -3
- package/lib/resources/singleRecordResource.js +7 -2
- package/lib/runjs-context/helpers.js +12 -5
- package/lib/types.d.ts +12 -0
- package/lib/utils/dataSourceDirty.d.ts +20 -0
- package/lib/utils/dataSourceDirty.js +139 -0
- package/lib/utils/dirtyAwareApiClient.d.ts +11 -0
- package/lib/utils/dirtyAwareApiClient.js +378 -0
- package/lib/utils/index.d.ts +1 -1
- package/lib/utils/index.js +11 -11
- package/lib/utils/openViewRouteState.d.ts +28 -0
- package/lib/utils/openViewRouteState.js +125 -0
- package/lib/utils/parsePathnameToViewParams.d.ts +3 -0
- package/lib/utils/parsePathnameToViewParams.js +18 -1
- package/lib/utils/resolveRunJSObjectValues.js +3 -2
- package/lib/utils/runjsModuleLoader.js +0 -30
- package/lib/views/ViewNavigation.js +5 -0
- package/package.json +4 -4
- package/src/JSRunner.ts +112 -25
- package/src/__tests__/JSRunner.test.ts +4 -5
- package/src/__tests__/flowContext.test.ts +131 -0
- package/src/__tests__/flowEngine.dataSourceDirty.test.ts +51 -0
- package/src/__tests__/flowI18n.test.ts +11 -0
- package/src/__tests__/flowModel.openView.navigation.test.ts +28 -0
- package/src/__tests__/flowSettings.test.ts +72 -0
- package/src/__tests__/runjsRuntimeFeatures.test.ts +15 -2
- package/src/components/FieldModelRenderer.tsx +50 -36
- package/src/components/FlowContextSelector.tsx +33 -2
- package/src/components/__tests__/FieldModelRenderer.test.tsx +165 -0
- package/src/components/dnd/index.tsx +11 -2
- package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +105 -35
- package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +381 -12
- package/src/components/settings/wrappers/contextual/useFloatToolbarVisibility.ts +28 -0
- package/src/components/variables/VariableHybridInput.tsx +166 -9
- package/src/components/variables/__tests__/VariableHybridInput.test.tsx +178 -0
- package/src/components/variables/types.ts +8 -0
- package/src/flowContext.ts +100 -33
- package/src/flowI18n.ts +8 -3
- package/src/flowSettings.ts +85 -1
- package/src/locale/en-US.json +1 -0
- package/src/locale/zh-CN.json +1 -0
- package/src/resources/apiResource.ts +2 -1
- package/src/resources/baseRecordResource.ts +6 -23
- package/src/resources/multiRecordResource.ts +13 -3
- package/src/resources/singleRecordResource.ts +6 -1
- package/src/runjs-context/helpers.ts +12 -6
- package/src/types.ts +14 -0
- package/src/utils/__tests__/dirtyAwareApiClient.test.ts +392 -0
- package/src/utils/__tests__/openViewRouteState.test.ts +40 -0
- package/src/utils/__tests__/parsePathnameToViewParams.test.ts +36 -0
- package/src/utils/dataSourceDirty.ts +126 -0
- package/src/utils/dirtyAwareApiClient.ts +430 -0
- package/src/utils/index.ts +10 -9
- package/src/utils/openViewRouteState.ts +107 -0
- package/src/utils/parsePathnameToViewParams.ts +23 -1
- package/src/utils/resolveRunJSObjectValues.ts +5 -2
- package/src/utils/runjsModuleLoader.ts +0 -32
- package/src/views/ViewNavigation.ts +6 -1
- package/src/views/__tests__/ViewNavigation.test.ts +15 -0
- 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
|
@@ -31,8 +31,8 @@ __export(resolveRunJSObjectValues_exports, {
|
|
|
31
31
|
});
|
|
32
32
|
module.exports = __toCommonJS(resolveRunJSObjectValues_exports);
|
|
33
33
|
var import_runjsValue = require("./runjsValue");
|
|
34
|
-
var import_safeGlobals = require("./safeGlobals");
|
|
35
34
|
async function resolveRunJSObjectValues(ctx, raw) {
|
|
35
|
+
var _a;
|
|
36
36
|
const out = {};
|
|
37
37
|
if (!raw || typeof raw !== "object") return out;
|
|
38
38
|
if (Array.isArray(raw)) return out;
|
|
@@ -41,7 +41,8 @@ async function resolveRunJSObjectValues(ctx, raw) {
|
|
|
41
41
|
if ((0, import_runjsValue.isRunJSValue)(value)) {
|
|
42
42
|
const { code, version } = (0, import_runjsValue.normalizeRunJSValue)(value);
|
|
43
43
|
if (!code.trim()) continue;
|
|
44
|
-
const
|
|
44
|
+
const runjsCtx = ctx;
|
|
45
|
+
const ret = await ((_a = runjsCtx == null ? void 0 : runjsCtx.runjs) == null ? void 0 : _a.call(runjsCtx, code, void 0, { version }));
|
|
45
46
|
if (!(ret == null ? void 0 : ret.success)) {
|
|
46
47
|
throw new Error(`RunJS execution failed for "${key}"`);
|
|
47
48
|
}
|
|
@@ -34,27 +34,6 @@ __export(runjsModuleLoader_exports, {
|
|
|
34
34
|
module.exports = __toCommonJS(runjsModuleLoader_exports);
|
|
35
35
|
var import_runjsLibs = require("../runjsLibs");
|
|
36
36
|
var import_resolveModuleUrl = require("./resolveModuleUrl");
|
|
37
|
-
var import_safeGlobals = require("./safeGlobals");
|
|
38
|
-
function snapshotOwnKeys(obj) {
|
|
39
|
-
try {
|
|
40
|
-
if (!obj || typeof obj !== "object" && typeof obj !== "function") return [];
|
|
41
|
-
return Object.getOwnPropertyNames(obj);
|
|
42
|
-
} catch (_) {
|
|
43
|
-
return [];
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
__name(snapshotOwnKeys, "snapshotOwnKeys");
|
|
47
|
-
function diffAddedKeys(afterKeys, beforeKeys) {
|
|
48
|
-
if (!afterKeys.length) return [];
|
|
49
|
-
if (!beforeKeys.length) return [...afterKeys];
|
|
50
|
-
const beforeSet = new Set(beforeKeys);
|
|
51
|
-
const added = [];
|
|
52
|
-
for (const k of afterKeys) {
|
|
53
|
-
if (!beforeSet.has(k)) added.push(k);
|
|
54
|
-
}
|
|
55
|
-
return added;
|
|
56
|
-
}
|
|
57
|
-
__name(diffAddedKeys, "diffAddedKeys");
|
|
58
37
|
async function withRunjsModuleLoadLock(task) {
|
|
59
38
|
const g = globalThis;
|
|
60
39
|
g.__nocobaseRunjsModuleLoadLock = (g.__nocobaseRunjsModuleLoadLock || Promise.resolve()).catch(() => {
|
|
@@ -222,8 +201,6 @@ async function prefetchEsmModule(url, options) {
|
|
|
222
201
|
__name(prefetchEsmModule, "prefetchEsmModule");
|
|
223
202
|
async function runjsRequireAsync(requirejs, url) {
|
|
224
203
|
return await withRunjsModuleLoadLock(async () => {
|
|
225
|
-
const beforeWinKeys = typeof window !== "undefined" ? snapshotOwnKeys(window) : [];
|
|
226
|
-
const beforeDocKeys = typeof document !== "undefined" ? snapshotOwnKeys(document) : [];
|
|
227
204
|
let result;
|
|
228
205
|
let error;
|
|
229
206
|
try {
|
|
@@ -242,13 +219,6 @@ async function runjsRequireAsync(requirejs, url) {
|
|
|
242
219
|
});
|
|
243
220
|
} catch (e) {
|
|
244
221
|
error = e;
|
|
245
|
-
} finally {
|
|
246
|
-
const afterWinKeys = typeof window !== "undefined" ? snapshotOwnKeys(window) : [];
|
|
247
|
-
const afterDocKeys = typeof document !== "undefined" ? snapshotOwnKeys(document) : [];
|
|
248
|
-
const addedWinKeys = diffAddedKeys(afterWinKeys, beforeWinKeys);
|
|
249
|
-
const addedDocKeys = diffAddedKeys(afterDocKeys, beforeDocKeys);
|
|
250
|
-
(0, import_safeGlobals.registerRunJSSafeWindowGlobals)(addedWinKeys);
|
|
251
|
-
(0, import_safeGlobals.registerRunJSSafeDocumentGlobals)(addedDocKeys);
|
|
252
222
|
}
|
|
253
223
|
if (error) throw error;
|
|
254
224
|
return result;
|
|
@@ -32,6 +32,7 @@ __export(ViewNavigation_exports, {
|
|
|
32
32
|
});
|
|
33
33
|
module.exports = __toCommonJS(ViewNavigation_exports);
|
|
34
34
|
var import_reactive = require("../reactive");
|
|
35
|
+
var import_utils = require("../utils");
|
|
35
36
|
function encodeFilterByTk(val) {
|
|
36
37
|
if (val === void 0 || val === null) return "";
|
|
37
38
|
if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
@@ -63,6 +64,10 @@ function generatePathnameFromViewParams(viewParams, options = {}) {
|
|
|
63
64
|
segments.push("view");
|
|
64
65
|
}
|
|
65
66
|
segments.push(viewParam.viewUid);
|
|
67
|
+
const openViewRouteStateToken = (0, import_utils.encodeOpenViewRouteState)(viewParam.viewUid, viewParam.openViewRouteState);
|
|
68
|
+
if (openViewRouteStateToken) {
|
|
69
|
+
segments.push("opts", openViewRouteStateToken);
|
|
70
|
+
}
|
|
66
71
|
if (viewParam.tabUid) {
|
|
67
72
|
segments.push("tab", viewParam.tabUid);
|
|
68
73
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/flow-engine",
|
|
3
|
-
"version": "2.2.0-alpha.
|
|
3
|
+
"version": "2.2.0-alpha.2",
|
|
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.2.0-alpha.
|
|
12
|
-
"@nocobase/shared": "2.2.0-alpha.
|
|
11
|
+
"@nocobase/sdk": "2.2.0-alpha.2",
|
|
12
|
+
"@nocobase/shared": "2.2.0-alpha.2",
|
|
13
13
|
"ahooks": "^3.7.2",
|
|
14
14
|
"axios": "^1.7.0",
|
|
15
15
|
"dayjs": "^1.11.9",
|
|
@@ -37,5 +37,5 @@
|
|
|
37
37
|
],
|
|
38
38
|
"author": "NocoBase Team",
|
|
39
39
|
"license": "Apache-2.0",
|
|
40
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "9dd5224bb8f6c1ac12d766b9f175bdc29bddd845"
|
|
41
41
|
}
|
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
|
});
|
|
@@ -14,6 +14,8 @@ import { FlowEngine } from '../flowEngine';
|
|
|
14
14
|
import { FlowModel } from '../models/flowModel';
|
|
15
15
|
import { RunJSContextRegistry } from '../runjs-context/registry';
|
|
16
16
|
import { setupRunJSContexts } from '../runjs-context/setup';
|
|
17
|
+
import { createViewScopedEngine } from '../ViewScopedFlowEngine';
|
|
18
|
+
import { DATA_SOURCE_DIRTY_EVENT } from '../views/viewEvents';
|
|
17
19
|
|
|
18
20
|
describe('FlowContext properties and methods', () => {
|
|
19
21
|
it('should return static property value', () => {
|
|
@@ -177,6 +179,49 @@ describe('FlowContext properties and methods', () => {
|
|
|
177
179
|
});
|
|
178
180
|
});
|
|
179
181
|
|
|
182
|
+
it('should expose current role as a top-level variable', async () => {
|
|
183
|
+
const engine = new FlowEngine();
|
|
184
|
+
const ctx = engine.context;
|
|
185
|
+
ctx.defineProperty('api', { value: { auth: { role: 'admin' } } });
|
|
186
|
+
|
|
187
|
+
expect(ctx.role).toBe('admin');
|
|
188
|
+
await expect(ctx.resolveJsonTemplate('{{ ctx.role }}')).resolves.toBe('admin');
|
|
189
|
+
|
|
190
|
+
const roleNode = ctx.getPropertyMetaTree().find((node) => node.name === 'role');
|
|
191
|
+
expect(roleNode).toMatchObject({
|
|
192
|
+
name: 'role',
|
|
193
|
+
title: '{{t("Current role")}}',
|
|
194
|
+
paths: ['role'],
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should expose actual role names for union role mode', async () => {
|
|
199
|
+
const engine = new FlowEngine();
|
|
200
|
+
const ctx = engine.context;
|
|
201
|
+
ctx.defineProperty('api', { value: { auth: { role: '__union__' } } });
|
|
202
|
+
ctx.defineProperty('user', {
|
|
203
|
+
value: {
|
|
204
|
+
roles: [
|
|
205
|
+
{ name: 'admin', title: 'Admin' },
|
|
206
|
+
{ name: 'member', title: 'Member' },
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
expect(ctx.role).toEqual(['admin', 'member']);
|
|
212
|
+
await expect(ctx.resolveJsonTemplate('{{ ctx.role }}')).resolves.toEqual(['admin', 'member']);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('should expose an empty role list for union role mode without user roles', async () => {
|
|
216
|
+
const engine = new FlowEngine();
|
|
217
|
+
const ctx = engine.context;
|
|
218
|
+
ctx.defineProperty('api', { value: { auth: { role: '__union__' } } });
|
|
219
|
+
ctx.defineProperty('user', { value: {} });
|
|
220
|
+
|
|
221
|
+
expect(ctx.role).toEqual([]);
|
|
222
|
+
await expect(ctx.resolveJsonTemplate('{{ ctx.role }}')).resolves.toEqual([]);
|
|
223
|
+
});
|
|
224
|
+
|
|
180
225
|
it('should throw sync error in get', () => {
|
|
181
226
|
const ctx = new FlowContext();
|
|
182
227
|
ctx.defineProperty('error', {
|
|
@@ -1386,6 +1431,92 @@ describe('FlowEngine context', () => {
|
|
|
1386
1431
|
expect(engine.context.appName).toBe('NocoBase');
|
|
1387
1432
|
});
|
|
1388
1433
|
|
|
1434
|
+
it('ctx.api should return a dirty-aware wrapper for static api properties', async () => {
|
|
1435
|
+
const engine = new FlowEngine();
|
|
1436
|
+
const update = vi.fn(async () => ({ data: { data: { id: 1 } } }));
|
|
1437
|
+
const api = {
|
|
1438
|
+
auth: { locale: 'zh-CN' },
|
|
1439
|
+
request: vi.fn(async () => ({ data: { ok: true } })),
|
|
1440
|
+
resource: vi.fn(() => ({ update })),
|
|
1441
|
+
};
|
|
1442
|
+
engine.context.defineProperty('api', { value: api });
|
|
1443
|
+
|
|
1444
|
+
await engine.context.api.resource('posts').update({ filterByTk: 1, values: { title: 't' } });
|
|
1445
|
+
|
|
1446
|
+
expect(update).toHaveBeenCalledTimes(1);
|
|
1447
|
+
expect(engine.getDataSourceDirtyVersion('main', 'posts')).toBe(1);
|
|
1448
|
+
expect(engine.context.api).toBe(engine.context.api);
|
|
1449
|
+
});
|
|
1450
|
+
|
|
1451
|
+
it('ctx.api should stay dirty-aware when resolved from a scoped context delegate', async () => {
|
|
1452
|
+
const root = new FlowEngine();
|
|
1453
|
+
const scoped = createViewScopedEngine(root);
|
|
1454
|
+
const update = vi.fn(async () => ({ data: { data: { id: 1 } } }));
|
|
1455
|
+
root.context.defineProperty('api', {
|
|
1456
|
+
value: {
|
|
1457
|
+
auth: { locale: 'zh-CN' },
|
|
1458
|
+
request: vi.fn(async () => ({ data: { ok: true } })),
|
|
1459
|
+
resource: vi.fn(() => ({ update })),
|
|
1460
|
+
},
|
|
1461
|
+
});
|
|
1462
|
+
|
|
1463
|
+
await scoped.context.api.resource('posts').update({ filterByTk: 1, values: { title: 't' } });
|
|
1464
|
+
|
|
1465
|
+
expect(update).toHaveBeenCalledTimes(1);
|
|
1466
|
+
expect(root.getDataSourceDirtyVersion('main', 'posts')).toBe(1);
|
|
1467
|
+
expect(scoped.context.engine.getDataSourceDirtyVersion('main', 'posts')).toBe(1);
|
|
1468
|
+
});
|
|
1469
|
+
|
|
1470
|
+
it('ctx.request should use the dirty-aware api wrapper', async () => {
|
|
1471
|
+
const engine = new FlowEngine();
|
|
1472
|
+
const request = vi.fn(async () => ({ data: { ok: true } }));
|
|
1473
|
+
engine.context.defineProperty('api', {
|
|
1474
|
+
value: {
|
|
1475
|
+
auth: { locale: 'zh-CN' },
|
|
1476
|
+
request,
|
|
1477
|
+
resource: vi.fn(),
|
|
1478
|
+
},
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1481
|
+
await engine.context.request({
|
|
1482
|
+
resource: 'posts',
|
|
1483
|
+
action: 'update',
|
|
1484
|
+
headers: { 'X-Data-Source': 'analytics' },
|
|
1485
|
+
params: { filterByTk: 1 },
|
|
1486
|
+
} as any);
|
|
1487
|
+
|
|
1488
|
+
expect(request).toHaveBeenCalledTimes(1);
|
|
1489
|
+
expect(engine.getDataSourceDirtyVersion('analytics', 'posts')).toBe(1);
|
|
1490
|
+
});
|
|
1491
|
+
|
|
1492
|
+
it('ctx.request should use the caller context when resolved through a scoped delegate', async () => {
|
|
1493
|
+
const root = new FlowEngine();
|
|
1494
|
+
const scoped = createViewScopedEngine(root);
|
|
1495
|
+
const callerCtx = new FlowContext();
|
|
1496
|
+
const dirtyEvents: Array<{ dataSourceKey: string; resourceNames: string[] }> = [];
|
|
1497
|
+
const request = vi.fn(async () => ({ data: { ok: true } }));
|
|
1498
|
+
root.context.defineProperty('api', {
|
|
1499
|
+
value: {
|
|
1500
|
+
auth: { locale: 'zh-CN' },
|
|
1501
|
+
request,
|
|
1502
|
+
resource: vi.fn(),
|
|
1503
|
+
},
|
|
1504
|
+
});
|
|
1505
|
+
callerCtx.addDelegate(scoped.context);
|
|
1506
|
+
scoped.context.engine.emitter.on(DATA_SOURCE_DIRTY_EVENT, (event) => dirtyEvents.push(event));
|
|
1507
|
+
|
|
1508
|
+
await callerCtx.request({
|
|
1509
|
+
resource: 'posts',
|
|
1510
|
+
action: 'update',
|
|
1511
|
+
params: { filterByTk: 1 },
|
|
1512
|
+
} as any);
|
|
1513
|
+
|
|
1514
|
+
expect(request).toHaveBeenCalledTimes(1);
|
|
1515
|
+
expect(root.getDataSourceDirtyVersion('main', 'posts')).toBe(1);
|
|
1516
|
+
expect(scoped.context.engine.getDataSourceDirtyVersion('main', 'posts')).toBe(1);
|
|
1517
|
+
expect(dirtyEvents).toEqual([{ dataSourceKey: 'main', resourceNames: ['posts'] }]);
|
|
1518
|
+
});
|
|
1519
|
+
|
|
1389
1520
|
it('ctx.sql should resolve template variables from caller context in delegate chain', async () => {
|
|
1390
1521
|
const engine = new FlowEngine();
|
|
1391
1522
|
const request = vi.fn(async () => ({ data: { data: [] } }));
|
|
@@ -11,6 +11,7 @@ import { describe, expect, it, vi } from 'vitest';
|
|
|
11
11
|
import { FlowEngine } from '../flowEngine';
|
|
12
12
|
import { MultiRecordResource } from '../resources/multiRecordResource';
|
|
13
13
|
import { SingleRecordResource } from '../resources/singleRecordResource';
|
|
14
|
+
import { DATA_SOURCE_DIRTY_EVENT } from '../views/viewEvents';
|
|
14
15
|
|
|
15
16
|
describe('FlowEngine dataSource dirty registry', () => {
|
|
16
17
|
it('tracks versions per dataSourceKey + resourceName', () => {
|
|
@@ -60,4 +61,54 @@ describe('FlowEngine dataSource dirty registry', () => {
|
|
|
60
61
|
// plus root collection (safety)
|
|
61
62
|
expect(markSpy).toHaveBeenCalledWith('main', 'users');
|
|
62
63
|
});
|
|
64
|
+
|
|
65
|
+
it('marks dirty once for record write helpers when using the dirty-aware context api', async () => {
|
|
66
|
+
const engine = new FlowEngine();
|
|
67
|
+
const request = vi.fn(async () => ({ data: { data: { id: 1 }, meta: {} } }));
|
|
68
|
+
const dirtyEvents: Array<{ dataSourceKey: string; resourceNames: string[] }> = [];
|
|
69
|
+
engine.context.defineProperty('api', {
|
|
70
|
+
value: {
|
|
71
|
+
auth: { locale: 'zh-CN' },
|
|
72
|
+
request,
|
|
73
|
+
resource: vi.fn(),
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
engine.emitter.on(DATA_SOURCE_DIRTY_EVENT, (event) => dirtyEvents.push(event));
|
|
77
|
+
|
|
78
|
+
const multi = engine.createResource(MultiRecordResource);
|
|
79
|
+
multi.setDataSourceKey('main').setResourceName('posts');
|
|
80
|
+
await multi.create({ title: 't' } as any, { refresh: false });
|
|
81
|
+
|
|
82
|
+
expect(request).toHaveBeenCalledTimes(1);
|
|
83
|
+
expect(engine.getDataSourceDirtyVersion('main', 'posts')).toBe(1);
|
|
84
|
+
expect(dirtyEvents).toEqual([{ dataSourceKey: 'main', resourceNames: ['posts'] }]);
|
|
85
|
+
|
|
86
|
+
const single = engine.createResource(SingleRecordResource);
|
|
87
|
+
single.setDataSourceKey('main').setResourceName('posts').setFilterByTk(1);
|
|
88
|
+
await single.save({ title: 'u' } as any, { refresh: false });
|
|
89
|
+
|
|
90
|
+
expect(request).toHaveBeenCalledTimes(2);
|
|
91
|
+
expect(engine.getDataSourceDirtyVersion('main', 'posts')).toBe(2);
|
|
92
|
+
expect(dirtyEvents).toEqual([
|
|
93
|
+
{ dataSourceKey: 'main', resourceNames: ['posts'] },
|
|
94
|
+
{ dataSourceKey: 'main', resourceNames: ['posts'] },
|
|
95
|
+
]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('still marks dirty for direct runAction writes', async () => {
|
|
99
|
+
const engine = new FlowEngine();
|
|
100
|
+
engine.context.defineProperty('api', {
|
|
101
|
+
value: {
|
|
102
|
+
auth: { locale: 'zh-CN' },
|
|
103
|
+
request: vi.fn(async () => ({ data: { data: { id: 1 }, meta: {} } })),
|
|
104
|
+
resource: vi.fn(),
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const multi = engine.createResource(MultiRecordResource);
|
|
109
|
+
multi.setDataSourceKey('main').setResourceName('posts');
|
|
110
|
+
await multi.runAction('create', { data: { title: 't' } });
|
|
111
|
+
|
|
112
|
+
expect(engine.getDataSourceDirtyVersion('main', 'posts')).toBe(1);
|
|
113
|
+
});
|
|
63
114
|
});
|
|
@@ -17,6 +17,17 @@ describe('FlowI18n', () => {
|
|
|
17
17
|
expect(i18n.translate("{{ t('Hello') }}")).toBe('你好');
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
+
it('keeps embedded quotes of a different type inside the key', () => {
|
|
21
|
+
// A single-quoted key whose text contains double quotes (and vice versa) must not be truncated at the first inner
|
|
22
|
+
// quote.
|
|
23
|
+
const key = 'Unlike "Post-action event", it listens for data changes.';
|
|
24
|
+
const table: Record<string, string> = { [key]: '与“操作后事件”不同,它监听数据变动。' };
|
|
25
|
+
const i18n = new FlowI18n({ i18n: { t: (k: string) => table[k] ?? k } });
|
|
26
|
+
|
|
27
|
+
expect(i18n.translate(`{{t('${key}', { ns: "workflow" })}}`)).toBe(table[key]);
|
|
28
|
+
expect(i18n.translate(`{{t("It's here", { ns: "workflow" })}}`)).toBe("It's here");
|
|
29
|
+
});
|
|
30
|
+
|
|
20
31
|
it('template compile ignores malformed options', () => {
|
|
21
32
|
const spy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
|
|
22
33
|
const i18n = new FlowI18n({ i18n: { t: (k: string) => k } });
|
|
@@ -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');
|