@nocobase/flow-engine 2.1.31 → 2.2.0-alpha.10
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/FlowContextSelector.js +55 -12
- package/lib/components/FormItem.js +11 -7
- package/lib/components/MobilePopup.style.js +16 -5
- package/lib/components/variables/VariableHybridInput.d.ts +9 -0
- package/lib/components/variables/VariableHybridInput.js +146 -17
- package/lib/components/variables/VariableInput.js +19 -7
- package/lib/components/variables/VariableTag.js +48 -36
- package/lib/components/variables/types.d.ts +21 -0
- package/lib/flowContext.js +11 -1
- package/lib/flowI18n.js +3 -3
- package/lib/runjs-context/helpers.js +12 -5
- package/lib/types.d.ts +3 -1
- package/lib/types.js +1 -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__/flowI18n.test.ts +11 -0
- package/src/components/FlowContextSelector.tsx +66 -11
- package/src/components/FormItem.tsx +12 -7
- package/src/components/MobilePopup.style.ts +22 -6
- package/src/components/__tests__/FormItem.test.tsx +17 -2
- package/src/components/__tests__/MobilePopup.style.test.tsx +103 -0
- package/src/components/variables/VariableHybridInput.tsx +185 -14
- package/src/components/variables/VariableInput.tsx +32 -7
- package/src/components/variables/VariableTag.tsx +51 -37
- package/src/components/variables/__tests__/FlowContextSelector.test.tsx +60 -3
- package/src/components/variables/__tests__/VariableHybridInput.test.tsx +212 -0
- package/src/components/variables/__tests__/VariableInput.test.tsx +202 -6
- package/src/components/variables/__tests__/VariableTag.test.tsx +80 -0
- package/src/components/variables/types.ts +21 -0
- package/src/flowContext.ts +11 -1
- package/src/flowI18n.ts +8 -3
- package/src/runjs-context/helpers.ts +12 -6
- package/src/types.ts +2 -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/lib/utils/index.js
CHANGED
|
@@ -46,10 +46,6 @@ __export(utils_exports, {
|
|
|
46
46
|
createOpenViewRouteState: () => import_openViewRouteState.createOpenViewRouteState,
|
|
47
47
|
createRecordMetaFactory: () => import_variablesParams.createRecordMetaFactory,
|
|
48
48
|
createRecordResolveOnServerWithLocal: () => import_variablesParams.createRecordResolveOnServerWithLocal,
|
|
49
|
-
createSafeDocument: () => import_safeGlobals.createSafeDocument,
|
|
50
|
-
createSafeNavigator: () => import_safeGlobals.createSafeNavigator,
|
|
51
|
-
createSafeRunJSGlobals: () => import_safeGlobals.createSafeRunJSGlobals,
|
|
52
|
-
createSafeWindow: () => import_safeGlobals.createSafeWindow,
|
|
53
49
|
decodeBase64Url: () => import_dateVariable.decodeBase64Url,
|
|
54
50
|
decodeOpenViewRouteState: () => import_openViewRouteState.decodeOpenViewRouteState,
|
|
55
51
|
defineAction: () => import_flow_definitions.defineAction,
|
|
@@ -89,7 +85,6 @@ __export(utils_exports, {
|
|
|
89
85
|
resolveStepDisabledInSettings: () => import_schema_utils.resolveStepDisabledInSettings,
|
|
90
86
|
resolveStepUiSchema: () => import_schema_utils.resolveStepUiSchema,
|
|
91
87
|
resolveUiMode: () => import_schema_utils.resolveUiMode,
|
|
92
|
-
runjsWithSafeGlobals: () => import_safeGlobals.runjsWithSafeGlobals,
|
|
93
88
|
serializeCtxDateValue: () => import_dateVariable.serializeCtxDateValue,
|
|
94
89
|
setAutoFlowError: () => import_autoFlowError.setAutoFlowError,
|
|
95
90
|
setupRuntimeContextSteps: () => import_setupRuntimeContextSteps.setupRuntimeContextSteps,
|
|
@@ -114,7 +109,6 @@ var import_autoFlowError = require("./autoFlowError");
|
|
|
114
109
|
var import_parsePathnameToViewParams = require("./parsePathnameToViewParams");
|
|
115
110
|
var import_openViewRouteState = require("./openViewRouteState");
|
|
116
111
|
var import_dateVariable = require("./dateVariable");
|
|
117
|
-
var import_safeGlobals = require("./safeGlobals");
|
|
118
112
|
var import_runjsValue = require("./runjsValue");
|
|
119
113
|
var import_resolveRunJSObjectValues = require("./resolveRunJSObjectValues");
|
|
120
114
|
var import_runjsTemplateCompat = require("./runjsTemplateCompat");
|
|
@@ -145,10 +139,6 @@ var import_randomId = require("./randomId");
|
|
|
145
139
|
createOpenViewRouteState,
|
|
146
140
|
createRecordMetaFactory,
|
|
147
141
|
createRecordResolveOnServerWithLocal,
|
|
148
|
-
createSafeDocument,
|
|
149
|
-
createSafeNavigator,
|
|
150
|
-
createSafeRunJSGlobals,
|
|
151
|
-
createSafeWindow,
|
|
152
142
|
decodeBase64Url,
|
|
153
143
|
decodeOpenViewRouteState,
|
|
154
144
|
defineAction,
|
|
@@ -188,7 +178,6 @@ var import_randomId = require("./randomId");
|
|
|
188
178
|
resolveStepDisabledInSettings,
|
|
189
179
|
resolveStepUiSchema,
|
|
190
180
|
resolveUiMode,
|
|
191
|
-
runjsWithSafeGlobals,
|
|
192
181
|
serializeCtxDateValue,
|
|
193
182
|
setAutoFlowError,
|
|
194
183
|
setupRuntimeContextSteps,
|
|
@@ -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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/flow-engine",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0-alpha.10",
|
|
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.
|
|
12
|
-
"@nocobase/shared": "2.
|
|
11
|
+
"@nocobase/sdk": "2.2.0-alpha.10",
|
|
12
|
+
"@nocobase/shared": "2.2.0-alpha.10",
|
|
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": "b5a3f131bb0a4a2fbfd96852896bccadc933d834"
|
|
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
|
});
|
|
@@ -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 } });
|
|
@@ -40,6 +40,18 @@ type SelectedPathInfo = {
|
|
|
40
40
|
meta?: ContextSelectorItem['meta'];
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
+
type MetaNodeTooltipOptions = { tooltip?: React.ReactNode };
|
|
44
|
+
|
|
45
|
+
function getMetaNodeTooltip(meta?: ContextSelectorItem['meta']): React.ReactNode {
|
|
46
|
+
if (!meta) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const metaWithTooltip = meta as ContextSelectorItem['meta'] & MetaNodeTooltipOptions;
|
|
51
|
+
const options = meta.options as MetaNodeTooltipOptions | undefined;
|
|
52
|
+
return metaWithTooltip.tooltip ?? options?.tooltip;
|
|
53
|
+
}
|
|
54
|
+
|
|
43
55
|
const normalizePath = (path: unknown): string[] | undefined => {
|
|
44
56
|
if (!Array.isArray(path)) {
|
|
45
57
|
return undefined;
|
|
@@ -85,6 +97,7 @@ const FlowContextSelectorComponent: React.FC<FlowContextSelectorProps> = ({
|
|
|
85
97
|
value,
|
|
86
98
|
onChange,
|
|
87
99
|
children,
|
|
100
|
+
active,
|
|
88
101
|
metaTree,
|
|
89
102
|
showSearch = false,
|
|
90
103
|
parseValueToPath: customParseValueToPath = parseValueToPath,
|
|
@@ -92,6 +105,7 @@ const FlowContextSelectorComponent: React.FC<FlowContextSelectorProps> = ({
|
|
|
92
105
|
open,
|
|
93
106
|
onlyLeafSelectable = false,
|
|
94
107
|
ignoreFieldNames,
|
|
108
|
+
dropdownFooter,
|
|
95
109
|
...cascaderProps
|
|
96
110
|
}) => {
|
|
97
111
|
const { token } = theme.useToken();
|
|
@@ -119,17 +133,28 @@ const FlowContextSelectorComponent: React.FC<FlowContextSelectorProps> = ({
|
|
|
119
133
|
|
|
120
134
|
// 文本国际化:仅当 label 为字符串时进行翻译
|
|
121
135
|
const baseLabel = typeof o.label === 'string' ? flowCtx.t(o.label) : o.label;
|
|
136
|
+
const labelText = typeof baseLabel === 'string' ? baseLabel : String(o.value);
|
|
137
|
+
const tooltip = getMetaNodeTooltip(meta);
|
|
138
|
+
const tooltipTitle = disabled
|
|
139
|
+
? disabledReason || tooltip || flowCtx.t('This variable is not available')
|
|
140
|
+
: tooltip;
|
|
122
141
|
|
|
123
|
-
const label =
|
|
142
|
+
const label = tooltipTitle ? (
|
|
124
143
|
<span>
|
|
125
144
|
{baseLabel}
|
|
126
145
|
<Tooltip
|
|
127
|
-
title={
|
|
128
|
-
placement="
|
|
129
|
-
|
|
146
|
+
title={typeof tooltipTitle === 'string' ? flowCtx.t(tooltipTitle) : tooltipTitle}
|
|
147
|
+
placement="top"
|
|
148
|
+
classNames={{ root: 'flow-variable-tip' }}
|
|
130
149
|
destroyTooltipOnHide
|
|
131
150
|
>
|
|
132
|
-
<QuestionCircleOutlined
|
|
151
|
+
<QuestionCircleOutlined
|
|
152
|
+
aria-label={`${labelText} tooltip`}
|
|
153
|
+
style={{
|
|
154
|
+
marginLeft: token.marginXXS,
|
|
155
|
+
color: disabled ? token.colorTextDisabled : token.colorTextDescription,
|
|
156
|
+
}}
|
|
157
|
+
/>
|
|
133
158
|
</Tooltip>
|
|
134
159
|
</span>
|
|
135
160
|
) : (
|
|
@@ -144,7 +169,7 @@ const FlowContextSelectorComponent: React.FC<FlowContextSelectorProps> = ({
|
|
|
144
169
|
};
|
|
145
170
|
});
|
|
146
171
|
},
|
|
147
|
-
[flowCtx],
|
|
172
|
+
[flowCtx, token.colorTextDescription, token.colorTextDisabled, token.marginXXS],
|
|
148
173
|
);
|
|
149
174
|
|
|
150
175
|
// 用于强制重新渲染的状态
|
|
@@ -265,13 +290,13 @@ const FlowContextSelectorComponent: React.FC<FlowContextSelectorProps> = ({
|
|
|
265
290
|
|
|
266
291
|
// 默认按钮组件
|
|
267
292
|
const defaultChildren = useMemo(() => {
|
|
268
|
-
const hasSelected = currentPath && currentPath.length > 0;
|
|
293
|
+
const hasSelected = active ?? Boolean(currentPath && currentPath.length > 0);
|
|
269
294
|
return (
|
|
270
|
-
<Button type={hasSelected ? 'primary' : 'default'} style={defaultButtonStyle}>
|
|
295
|
+
<Button type={hasSelected ? 'primary' : 'default'} style={defaultButtonStyle} disabled={cascaderProps.disabled}>
|
|
271
296
|
x
|
|
272
297
|
</Button>
|
|
273
298
|
);
|
|
274
|
-
}, [currentPath]);
|
|
299
|
+
}, [active, cascaderProps.disabled, currentPath]);
|
|
275
300
|
|
|
276
301
|
// 处理选择变化事件
|
|
277
302
|
const handleChange = useCallback(
|
|
@@ -360,12 +385,41 @@ const FlowContextSelectorComponent: React.FC<FlowContextSelectorProps> = ({
|
|
|
360
385
|
[cascaderOnDropdownVisibleChange, open],
|
|
361
386
|
);
|
|
362
387
|
|
|
388
|
+
// Footer hint at the bottom of the dropdown. Defaults to the "double click to choose entire object" hint whenever
|
|
389
|
+
// non-leaf selection is allowed (double-clicking a non-leaf selects the whole object). Callers can override with
|
|
390
|
+
// their own node, or pass `null` to hide it.
|
|
391
|
+
const footerNode = useMemo(() => {
|
|
392
|
+
if (dropdownFooter !== undefined) {
|
|
393
|
+
return dropdownFooter;
|
|
394
|
+
}
|
|
395
|
+
if (onlyLeafSelectable) {
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
return (
|
|
399
|
+
<div
|
|
400
|
+
className={css`
|
|
401
|
+
padding: 6px 12px;
|
|
402
|
+
color: ${token.colorTextDescription};
|
|
403
|
+
border-top: 1px solid ${token.colorSplit};
|
|
404
|
+
font-size: ${token.fontSizeSM}px;
|
|
405
|
+
`}
|
|
406
|
+
>
|
|
407
|
+
{flowCtx.t('Double click to choose entire object')}
|
|
408
|
+
</div>
|
|
409
|
+
);
|
|
410
|
+
}, [dropdownFooter, onlyLeafSelectable, token, flowCtx]);
|
|
411
|
+
|
|
363
412
|
const renderDropdown = useCallback(
|
|
364
413
|
(menu: React.ReactElement) => {
|
|
365
414
|
const cascaderMenuNode = cascaderDropdownRender ? cascaderDropdownRender(menu) : menu;
|
|
366
415
|
const cascaderMenu = React.isValidElement(cascaderMenuNode) ? cascaderMenuNode : <>{cascaderMenuNode}</>;
|
|
367
416
|
if (!isSearchEnabled || children === null) {
|
|
368
|
-
return
|
|
417
|
+
return (
|
|
418
|
+
<>
|
|
419
|
+
{cascaderMenu}
|
|
420
|
+
{footerNode}
|
|
421
|
+
</>
|
|
422
|
+
);
|
|
369
423
|
}
|
|
370
424
|
|
|
371
425
|
return (
|
|
@@ -381,10 +435,11 @@ const FlowContextSelectorComponent: React.FC<FlowContextSelectorProps> = ({
|
|
|
381
435
|
/>
|
|
382
436
|
</div>
|
|
383
437
|
{cascaderMenu}
|
|
438
|
+
{footerNode}
|
|
384
439
|
</>
|
|
385
440
|
);
|
|
386
441
|
},
|
|
387
|
-
[cascaderDropdownRender, cascaderSearchInputClassName, children, flowCtx, isSearchEnabled, searchText],
|
|
442
|
+
[cascaderDropdownRender, cascaderSearchInputClassName, children, flowCtx, isSearchEnabled, searchText, footerNode],
|
|
388
443
|
);
|
|
389
444
|
|
|
390
445
|
const inlinePlaceholder =
|
|
@@ -54,15 +54,20 @@ const formItemPropKeys: (keyof ExtendedFormItemProps)[] = [
|
|
|
54
54
|
'showLabel',
|
|
55
55
|
];
|
|
56
56
|
|
|
57
|
+
const modelInternalPropKeys = ['globalSort'];
|
|
58
|
+
|
|
57
59
|
export const FormItem = ({
|
|
58
60
|
children,
|
|
59
61
|
showLabel = true,
|
|
60
62
|
labelWidth,
|
|
61
63
|
...rest
|
|
62
64
|
}: ExtendedFormItemProps & ChildExtraProps) => {
|
|
65
|
+
const forwardedRest = Object.fromEntries(
|
|
66
|
+
Object.entries(rest).filter(([key]) => !modelInternalPropKeys.includes(key)),
|
|
67
|
+
) as ExtendedFormItemProps & ChildExtraProps;
|
|
63
68
|
// 过滤掉 Form.Item 专用 props,只保留要传给子组件的
|
|
64
69
|
const childProps = Object.fromEntries(
|
|
65
|
-
Object.entries(
|
|
70
|
+
Object.entries(forwardedRest).filter(([key]) => !formItemPropKeys.includes(key as keyof ExtendedFormItemProps)),
|
|
66
71
|
);
|
|
67
72
|
|
|
68
73
|
const processedChildren =
|
|
@@ -74,7 +79,7 @@ export const FormItem = ({
|
|
|
74
79
|
}
|
|
75
80
|
return child;
|
|
76
81
|
});
|
|
77
|
-
const { label, labelWrap, colon = true, layout } =
|
|
82
|
+
const { label, labelWrap, colon = true, layout } = forwardedRest;
|
|
78
83
|
const effectiveLabelWrap = !layout || layout === 'vertical' ? true : labelWrap;
|
|
79
84
|
const labelColStyle =
|
|
80
85
|
layout === 'vertical' ? { width: labelWidth, ...verticalFormItemLabelStyle } : { width: labelWidth };
|
|
@@ -122,17 +127,17 @@ export const FormItem = ({
|
|
|
122
127
|
};
|
|
123
128
|
return (
|
|
124
129
|
<Form.Item
|
|
125
|
-
{...
|
|
126
|
-
style={{ ...formItemStyle, ...
|
|
130
|
+
{...forwardedRest}
|
|
131
|
+
style={{ ...formItemStyle, ...forwardedRest.style }}
|
|
127
132
|
labelCol={{ style: labelColStyle }}
|
|
128
133
|
layout={layout}
|
|
129
134
|
label={renderLabel()}
|
|
130
135
|
colon={false}
|
|
131
|
-
extra={
|
|
136
|
+
extra={forwardedRest.extra && <span style={{ whiteSpace: 'pre-wrap' }}>{forwardedRest.extra}</span>}
|
|
132
137
|
tooltip={
|
|
133
|
-
|
|
138
|
+
forwardedRest.tooltip &&
|
|
134
139
|
({
|
|
135
|
-
title:
|
|
140
|
+
title: forwardedRest.tooltip,
|
|
136
141
|
overlayInnerStyle: { whiteSpace: 'pre-line' },
|
|
137
142
|
} as TooltipProps)
|
|
138
143
|
}
|
|
@@ -11,7 +11,7 @@ import type { CSSInterpolation, CSSObject } from '@ant-design/cssinjs';
|
|
|
11
11
|
import { useStyleRegister } from '@ant-design/cssinjs';
|
|
12
12
|
import { merge } from '@formily/shared';
|
|
13
13
|
import type { ComponentTokenMap } from 'antd/es/theme/interface';
|
|
14
|
-
import {
|
|
14
|
+
import { useContext, useRef } from 'react';
|
|
15
15
|
import { ConfigProvider, theme } from 'antd';
|
|
16
16
|
|
|
17
17
|
const usePrefixCls = (
|
|
@@ -83,6 +83,10 @@ type UseComponentStyleResult = {
|
|
|
83
83
|
componentCls: string;
|
|
84
84
|
rootPrefixCls: string;
|
|
85
85
|
};
|
|
86
|
+
type WrapSSRCache = {
|
|
87
|
+
deps: unknown[];
|
|
88
|
+
wrapSSR: ReturnType<typeof useStyleRegister>;
|
|
89
|
+
};
|
|
86
90
|
|
|
87
91
|
const genStyleHook = <ComponentName extends OverrideComponent>(
|
|
88
92
|
component: ComponentName,
|
|
@@ -122,9 +126,18 @@ const genStyleHook = <ComponentName extends OverrideComponent>(
|
|
|
122
126
|
|
|
123
127
|
// useStyleRegister 有 BUG,会导致重复渲染,所以这里做了一层缓存
|
|
124
128
|
// 等 https://github.com/ant-design/cssinjs/pull/176 合并后,可以去掉这层缓存
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
129
|
+
const wrapSSRDeps = [theme, token, hashId, prefixCls, iconPrefixCls, rootPrefixCls, props];
|
|
130
|
+
const wrapSSRCacheRef = useRef<WrapSSRCache>();
|
|
131
|
+
const currentCache = wrapSSRCacheRef.current;
|
|
132
|
+
let memoizedWrapSSR: ReturnType<typeof useStyleRegister> = currentCache?.wrapSSR || wrapSSR;
|
|
133
|
+
|
|
134
|
+
if (!currentCache || wrapSSRDeps.some((dep, index) => dep !== currentCache.deps[index])) {
|
|
135
|
+
wrapSSRCacheRef.current = {
|
|
136
|
+
deps: wrapSSRDeps,
|
|
137
|
+
wrapSSR,
|
|
138
|
+
};
|
|
139
|
+
memoizedWrapSSR = wrapSSR;
|
|
140
|
+
}
|
|
128
141
|
|
|
129
142
|
return {
|
|
130
143
|
wrapSSR: memoizedWrapSSR,
|
|
@@ -140,7 +153,7 @@ export const useMobileActionDrawerStyle = genStyleHook('nb-mobile-action-drawer'
|
|
|
140
153
|
return {
|
|
141
154
|
[componentCls]: {
|
|
142
155
|
'.nb-mobile-action-drawer-header': {
|
|
143
|
-
height: 'var(--nb-mobile-page-header-height)',
|
|
156
|
+
height: 'var(--nb-mobile-page-header-height, 46px)',
|
|
144
157
|
display: 'flex',
|
|
145
158
|
alignItems: 'center',
|
|
146
159
|
justifyContent: 'space-between',
|
|
@@ -182,7 +195,10 @@ export const useMobileActionDrawerStyle = genStyleHook('nb-mobile-action-drawer'
|
|
|
182
195
|
'.nb-mobile-action-drawer-body': {
|
|
183
196
|
borderTopLeftRadius: 8,
|
|
184
197
|
borderTopRightRadius: 8,
|
|
185
|
-
maxHeight: 'calc(
|
|
198
|
+
maxHeight: 'calc(100vh - var(--nb-mobile-page-header-height, 46px))',
|
|
199
|
+
'@supports (height: 100dvh)': {
|
|
200
|
+
maxHeight: 'calc(100dvh - var(--nb-mobile-page-header-height, 46px))',
|
|
201
|
+
},
|
|
186
202
|
overflowY: 'auto',
|
|
187
203
|
overflowX: 'hidden',
|
|
188
204
|
backgroundColor: token.colorBgLayout,
|
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
10
|
+
import { render } from '@testing-library/react';
|
|
11
|
+
import React from 'react';
|
|
12
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
13
|
+
import { FormItem, formItemStyle, verticalFormItemLabelStyle } from '../FormItem';
|
|
12
14
|
|
|
13
15
|
describe('FormItem', () => {
|
|
14
16
|
it('keeps vertical label-to-value spacing consistent with v1', () => {
|
|
@@ -22,4 +24,17 @@ describe('FormItem', () => {
|
|
|
22
24
|
marginBottom: 12,
|
|
23
25
|
});
|
|
24
26
|
});
|
|
27
|
+
|
|
28
|
+
it('does not forward model-internal globalSort props to field children', () => {
|
|
29
|
+
const Field = vi.fn(() => <input aria-label="field" />);
|
|
30
|
+
|
|
31
|
+
render(
|
|
32
|
+
<FormItem globalSort={['title']} placeholder="Title">
|
|
33
|
+
<Field />
|
|
34
|
+
</FormItem>,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
expect(Field).toHaveBeenCalledWith(expect.not.objectContaining({ globalSort: expect.anything() }), {});
|
|
38
|
+
expect(Field).toHaveBeenCalledWith(expect.objectContaining({ placeholder: 'Title' }), {});
|
|
39
|
+
});
|
|
25
40
|
});
|