@nocobase/flow-engine 3.0.0-alpha.7 → 3.0.0-alpha.8
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/components/FlowContextSelector.js +7 -1
- package/lib/flowContext.d.ts +5 -1
- package/lib/flowContext.js +8 -2
- package/lib/utils/dateVariable.d.ts +22 -0
- package/lib/utils/dateVariable.js +123 -16
- package/lib/utils/index.d.ts +1 -1
- package/lib/utils/index.js +4 -0
- package/lib/utils/params-resolvers.d.ts +1 -0
- package/lib/utils/params-resolvers.js +1 -0
- package/lib/views/createViewMeta.d.ts +1 -0
- package/lib/views/createViewMeta.js +35 -6
- package/package.json +4 -4
- package/src/__tests__/createViewMeta.popup.test.ts +84 -1
- package/src/__tests__/flowContext.test.ts +8 -0
- package/src/__tests__/objectVariable.test.ts +6 -1
- package/src/components/FlowContextSelector.tsx +7 -1
- package/src/components/variables/__tests__/FlowContextSelector.test.tsx +35 -0
- package/src/flowContext.ts +19 -3
- package/src/utils/__tests__/dateVariable.test.ts +57 -4
- package/src/utils/dateVariable.ts +145 -18
- package/src/utils/index.ts +6 -0
- package/src/utils/params-resolvers.ts +2 -0
- package/src/views/createViewMeta.ts +33 -0
|
@@ -247,6 +247,7 @@ const FlowContextSelectorComponent = /* @__PURE__ */ __name(({
|
|
|
247
247
|
}, [active, cascaderProps.disabled, currentPath]);
|
|
248
248
|
const handleChange = (0, import_react.useCallback)(
|
|
249
249
|
(selectedValues, selectedOptions) => {
|
|
250
|
+
var _a;
|
|
250
251
|
const lastOption = selectedOptions == null ? void 0 : selectedOptions[selectedOptions.length - 1];
|
|
251
252
|
if (!selectedValues || selectedValues.length === 0) {
|
|
252
253
|
onChange == null ? void 0 : onChange("", lastOption == null ? void 0 : lastOption.meta);
|
|
@@ -256,6 +257,7 @@ const FlowContextSelectorComponent = /* @__PURE__ */ __name(({
|
|
|
256
257
|
const path = selectedValues.map(String);
|
|
257
258
|
const pathString = path.join(".");
|
|
258
259
|
const isLeaf = lastOption == null ? void 0 : lastOption.isLeaf;
|
|
260
|
+
const isSelectable = ((_a = lastOption == null ? void 0 : lastOption.meta) == null ? void 0 : _a.selectable) !== false;
|
|
259
261
|
const now = Date.now();
|
|
260
262
|
let formattedValue;
|
|
261
263
|
if (customFormatPathToValue) {
|
|
@@ -267,12 +269,16 @@ const FlowContextSelectorComponent = /* @__PURE__ */ __name(({
|
|
|
267
269
|
formattedValue = (0, import_utils.formatPathToValue)(lastOption == null ? void 0 : lastOption.meta);
|
|
268
270
|
}
|
|
269
271
|
if (isLeaf) {
|
|
272
|
+
if (!isSelectable) {
|
|
273
|
+
setTempSelectedPath(path);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
270
276
|
onChange == null ? void 0 : onChange(formattedValue, lastOption == null ? void 0 : lastOption.meta);
|
|
271
277
|
setTempSelectedPath([]);
|
|
272
278
|
return;
|
|
273
279
|
}
|
|
274
280
|
const lastSelected = lastSelectedRef.current;
|
|
275
|
-
const isDoubleClick = !onlyLeafSelectable && (lastSelected == null ? void 0 : lastSelected.path) === pathString && now - lastSelected.time < 300;
|
|
281
|
+
const isDoubleClick = isSelectable && !onlyLeafSelectable && (lastSelected == null ? void 0 : lastSelected.path) === pathString && now - lastSelected.time < 300;
|
|
276
282
|
if (isDoubleClick) {
|
|
277
283
|
onChange == null ? void 0 : onChange(formattedValue, lastOption == null ? void 0 : lastOption.meta);
|
|
278
284
|
lastSelectedRef.current = null;
|
package/lib/flowContext.d.ts
CHANGED
|
@@ -27,6 +27,9 @@ import type { RecordRef } from './utils/serverContextParams';
|
|
|
27
27
|
import { FlowView, FlowViewer } from './views/FlowView';
|
|
28
28
|
import { type RunJSVersion } from './runjs-context/registry';
|
|
29
29
|
type Getter<T = any> = (ctx: FlowContext) => T | Promise<T>;
|
|
30
|
+
export type ResolveJsonTemplateOptions = {
|
|
31
|
+
contractModelUid?: string | number | null;
|
|
32
|
+
};
|
|
30
33
|
export type FlowContextDocRef = string | {
|
|
31
34
|
url: string;
|
|
32
35
|
title?: string;
|
|
@@ -77,6 +80,7 @@ export interface MetaTreeNode {
|
|
|
77
80
|
hidden?: boolean | (() => boolean);
|
|
78
81
|
disabled?: boolean | (() => boolean);
|
|
79
82
|
disabledReason?: string | (() => string | undefined);
|
|
83
|
+
selectable?: boolean;
|
|
80
84
|
children?: MetaTreeNode[] | (() => Promise<MetaTreeNode[]>);
|
|
81
85
|
}
|
|
82
86
|
export interface PropertyMeta {
|
|
@@ -343,7 +347,7 @@ declare class BaseFlowEngineContext extends FlowContext {
|
|
|
343
347
|
* @deprecated use `resolveJsonTemplate` instead
|
|
344
348
|
*/
|
|
345
349
|
renderJson: (template: JSONValue) => Promise<any>;
|
|
346
|
-
resolveJsonTemplate: (template: JSONValue) => Promise<any>;
|
|
350
|
+
resolveJsonTemplate: (template: JSONValue, options?: ResolveJsonTemplateOptions) => Promise<any>;
|
|
347
351
|
getVar: (path: string) => Promise<any>;
|
|
348
352
|
request: (options: RequestOptions) => Promise<any>;
|
|
349
353
|
runjs: (code: string, variables?: Record<string, any>, options?: JSRunnerOptions) => Promise<any>;
|
package/lib/flowContext.js
CHANGED
|
@@ -2383,7 +2383,7 @@ const _FlowEngineContext = class _FlowEngineContext extends BaseFlowEngineContex
|
|
|
2383
2383
|
this.defineMethod("renderJson", function(template) {
|
|
2384
2384
|
return this.resolveJsonTemplate(template);
|
|
2385
2385
|
});
|
|
2386
|
-
|
|
2386
|
+
const resolveJsonTemplate = /* @__PURE__ */ __name(async function(template, options) {
|
|
2387
2387
|
var _a, _b, _c;
|
|
2388
2388
|
const used = (0, import_utils.extractUsedVariablePaths)(template);
|
|
2389
2389
|
const usedVarNames = Object.keys(used || {});
|
|
@@ -2516,7 +2516,12 @@ const _FlowEngineContext = class _FlowEngineContext extends BaseFlowEngineContex
|
|
|
2516
2516
|
}
|
|
2517
2517
|
if (this.api) {
|
|
2518
2518
|
try {
|
|
2519
|
+
const contractRd = (0, import_params_resolvers.buildFlowModelResolveDescriptor)(
|
|
2520
|
+
this,
|
|
2521
|
+
options == null ? void 0 : options.contractModelUid
|
|
2522
|
+
);
|
|
2519
2523
|
serverResolved = await (0, import_params_resolvers.enqueueVariablesResolve)(this, {
|
|
2524
|
+
...contractRd ? { contractRd } : {},
|
|
2520
2525
|
rd: (0, import_params_resolvers.buildFlowModelResolveDescriptor)(this, (_a = this.model) == null ? void 0 : _a.uid),
|
|
2521
2526
|
template,
|
|
2522
2527
|
contextParams: autoContextParams || {}
|
|
@@ -2528,7 +2533,8 @@ const _FlowEngineContext = class _FlowEngineContext extends BaseFlowEngineContex
|
|
|
2528
2533
|
}
|
|
2529
2534
|
}
|
|
2530
2535
|
return (0, import_utils.resolveExpressions)(serverResolved, this);
|
|
2531
|
-
});
|
|
2536
|
+
}, "resolveJsonTemplate");
|
|
2537
|
+
this.defineMethod("resolveJsonTemplate", resolveJsonTemplate);
|
|
2532
2538
|
this.defineMethod(
|
|
2533
2539
|
"getVar",
|
|
2534
2540
|
async function(varPath) {
|
|
@@ -6,11 +6,33 @@
|
|
|
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
|
+
declare const PRESET_KEY_LIST: readonly ["today", "now", "yesterday", "tomorrow", "thisWeek", "lastWeek", "nextWeek", "thisMonth", "lastMonth", "nextMonth", "thisQuarter", "lastQuarter", "nextQuarter", "thisYear", "lastYear", "nextYear"];
|
|
10
|
+
export type CtxDatePreset = (typeof PRESET_KEY_LIST)[number];
|
|
11
|
+
export type CtxDateRelativeDirection = 'next' | 'past';
|
|
12
|
+
export type CtxDateRelativeUnit = 'day' | 'week' | 'month' | 'year';
|
|
13
|
+
export type CtxDateExpressionConfig = {
|
|
14
|
+
kind: 'exact';
|
|
15
|
+
value: string | [string, string];
|
|
16
|
+
format?: string;
|
|
17
|
+
} | {
|
|
18
|
+
kind: 'relative';
|
|
19
|
+
direction: CtxDateRelativeDirection;
|
|
20
|
+
amount: number;
|
|
21
|
+
unit: CtxDateRelativeUnit;
|
|
22
|
+
format?: string;
|
|
23
|
+
} | {
|
|
24
|
+
kind: 'preset';
|
|
25
|
+
preset: CtxDatePreset;
|
|
26
|
+
format?: string;
|
|
27
|
+
};
|
|
9
28
|
export declare function isCtxDatePathPrefix(pathSegments: string[]): boolean;
|
|
10
29
|
export declare function encodeBase64Url(input: string): string;
|
|
11
30
|
export declare function decodeBase64Url(input: string): string | undefined;
|
|
12
31
|
export declare function isCtxDateExpression(value: unknown): value is string;
|
|
13
32
|
export declare function isCompleteCtxDatePath(pathSegments: string[]): boolean;
|
|
14
33
|
export declare function parseCtxDateExpression(value: unknown): any;
|
|
34
|
+
export declare function parseCtxDateExpressionConfig(value: unknown): CtxDateExpressionConfig | undefined;
|
|
35
|
+
export declare function serializeCtxDateExpressionConfig(config: CtxDateExpressionConfig): string | undefined;
|
|
15
36
|
export declare function serializeCtxDateValue(value: unknown): string | undefined;
|
|
16
37
|
export declare function resolveCtxDatePath(pathSegments: string[]): any;
|
|
38
|
+
export {};
|
|
@@ -43,13 +43,15 @@ __export(dateVariable_exports, {
|
|
|
43
43
|
isCtxDateExpression: () => isCtxDateExpression,
|
|
44
44
|
isCtxDatePathPrefix: () => isCtxDatePathPrefix,
|
|
45
45
|
parseCtxDateExpression: () => parseCtxDateExpression,
|
|
46
|
+
parseCtxDateExpressionConfig: () => parseCtxDateExpressionConfig,
|
|
46
47
|
resolveCtxDatePath: () => resolveCtxDatePath,
|
|
48
|
+
serializeCtxDateExpressionConfig: () => serializeCtxDateExpressionConfig,
|
|
47
49
|
serializeCtxDateValue: () => serializeCtxDateValue
|
|
48
50
|
});
|
|
49
51
|
module.exports = __toCommonJS(dateVariable_exports);
|
|
50
52
|
var import_dayjs = __toESM(require("dayjs"));
|
|
51
53
|
const CTX_DATE_REGEX = /^\{\{\s*ctx\.date(?:\.(.+?))?\s*\}\}$/;
|
|
52
|
-
const
|
|
54
|
+
const PRESET_KEY_LIST = [
|
|
53
55
|
"today",
|
|
54
56
|
"now",
|
|
55
57
|
"yesterday",
|
|
@@ -66,9 +68,11 @@ const PRESET_KEYS = /* @__PURE__ */ new Set([
|
|
|
66
68
|
"thisYear",
|
|
67
69
|
"lastYear",
|
|
68
70
|
"nextYear"
|
|
69
|
-
]
|
|
71
|
+
];
|
|
72
|
+
const PRESET_KEYS = new Set(PRESET_KEY_LIST);
|
|
70
73
|
const RELATIVE_DIRECTIONS = /* @__PURE__ */ new Set(["next", "past"]);
|
|
71
74
|
const RELATIVE_UNITS = /* @__PURE__ */ new Set(["day", "week", "month", "year"]);
|
|
75
|
+
const MAX_DATE_FORMAT_LENGTH = 128;
|
|
72
76
|
function parseCtxDateSegments(value) {
|
|
73
77
|
if (typeof value !== "string") return null;
|
|
74
78
|
const trimmed = value.trim();
|
|
@@ -79,8 +83,7 @@ function parseCtxDateSegments(value) {
|
|
|
79
83
|
return rawPath.split(".").map((seg) => seg.trim()).filter(Boolean);
|
|
80
84
|
}
|
|
81
85
|
__name(parseCtxDateSegments, "parseCtxDateSegments");
|
|
82
|
-
function
|
|
83
|
-
const segments = withDatePrefix((pathSegments || []).map((seg) => String(seg)));
|
|
86
|
+
function isBaseCtxDatePathPrefix(segments) {
|
|
84
87
|
if (segments[0] !== "date") return false;
|
|
85
88
|
if (segments.length === 1) return true;
|
|
86
89
|
if (segments[1] === "preset") {
|
|
@@ -117,6 +120,34 @@ function isCtxDatePathPrefix(pathSegments) {
|
|
|
117
120
|
}
|
|
118
121
|
return false;
|
|
119
122
|
}
|
|
123
|
+
__name(isBaseCtxDatePathPrefix, "isBaseCtxDatePathPrefix");
|
|
124
|
+
function decodeFormatToken(token) {
|
|
125
|
+
const raw = String(token || "");
|
|
126
|
+
if (!raw.startsWith("v")) return void 0;
|
|
127
|
+
const decoded = decodeBase64Url(raw.slice(1));
|
|
128
|
+
if (!decoded || decoded.length > MAX_DATE_FORMAT_LENGTH) return void 0;
|
|
129
|
+
return decoded;
|
|
130
|
+
}
|
|
131
|
+
__name(decodeFormatToken, "decodeFormatToken");
|
|
132
|
+
function splitFormattedDateSegments(segments) {
|
|
133
|
+
if (segments[0] !== "date") return null;
|
|
134
|
+
if (segments[1] !== "format") return { baseSegments: segments };
|
|
135
|
+
if (segments.length < 4) return null;
|
|
136
|
+
const format = decodeFormatToken(segments[2]);
|
|
137
|
+
if (!format) return null;
|
|
138
|
+
return { baseSegments: ["date", ...segments.slice(3)], format };
|
|
139
|
+
}
|
|
140
|
+
__name(splitFormattedDateSegments, "splitFormattedDateSegments");
|
|
141
|
+
function isCtxDatePathPrefix(pathSegments) {
|
|
142
|
+
const segments = withDatePrefix((pathSegments || []).map((seg) => String(seg)));
|
|
143
|
+
if (segments[0] !== "date") return false;
|
|
144
|
+
if (segments.length === 1) return true;
|
|
145
|
+
if (segments[1] !== "format") return isBaseCtxDatePathPrefix(segments);
|
|
146
|
+
if (segments.length === 2) return true;
|
|
147
|
+
if (segments.length === 3) return typeof decodeFormatToken(segments[2]) === "string";
|
|
148
|
+
const formatted = splitFormattedDateSegments(segments);
|
|
149
|
+
return formatted ? isBaseCtxDatePathPrefix(formatted.baseSegments) : false;
|
|
150
|
+
}
|
|
120
151
|
__name(isCtxDatePathPrefix, "isCtxDatePathPrefix");
|
|
121
152
|
function withDatePrefix(pathSegments) {
|
|
122
153
|
if (pathSegments[0] === "date") {
|
|
@@ -225,26 +256,31 @@ __name(isCtxDateExpression, "isCtxDateExpression");
|
|
|
225
256
|
function isCompleteCtxDatePath(pathSegments) {
|
|
226
257
|
if (!isCtxDatePathPrefix(pathSegments)) return false;
|
|
227
258
|
const segments = withDatePrefix((pathSegments || []).map((seg) => String(seg)));
|
|
228
|
-
|
|
229
|
-
if (
|
|
230
|
-
|
|
259
|
+
const formatted = splitFormattedDateSegments(segments);
|
|
260
|
+
if (!formatted) return false;
|
|
261
|
+
const baseSegments = formatted.baseSegments;
|
|
262
|
+
if (baseSegments[1] === "preset") {
|
|
263
|
+
return baseSegments.length === 3 && PRESET_KEYS.has(baseSegments[2]);
|
|
231
264
|
}
|
|
232
|
-
if (
|
|
233
|
-
if (
|
|
234
|
-
return RELATIVE_DIRECTIONS.has(
|
|
265
|
+
if (baseSegments[1] === "relative") {
|
|
266
|
+
if (baseSegments.length !== 5) return false;
|
|
267
|
+
return RELATIVE_DIRECTIONS.has(baseSegments[2]) && RELATIVE_UNITS.has(baseSegments[3]) && typeof parseNumberToken(baseSegments[4]) === "number";
|
|
235
268
|
}
|
|
236
|
-
if (
|
|
237
|
-
return
|
|
269
|
+
if (baseSegments[1] === "exact" && baseSegments[2] === "single" && baseSegments[3] === "date") {
|
|
270
|
+
return baseSegments.length === 5 && /^v.+/.test(baseSegments[4]);
|
|
238
271
|
}
|
|
239
|
-
if (
|
|
240
|
-
return
|
|
272
|
+
if (baseSegments[1] === "exact" && baseSegments[2] === "range" && baseSegments[3] === "date") {
|
|
273
|
+
return baseSegments.length === 6 && /^v.+/.test(baseSegments[4]) && /^v.+/.test(baseSegments[5]);
|
|
241
274
|
}
|
|
242
275
|
return false;
|
|
243
276
|
}
|
|
244
277
|
__name(isCompleteCtxDatePath, "isCompleteCtxDatePath");
|
|
245
278
|
function parseCtxDateExpression(value) {
|
|
246
279
|
if (!isCtxDateExpression(value)) return void 0;
|
|
247
|
-
const
|
|
280
|
+
const rawSegments = withDatePrefix(parseCtxDateSegments(value) || []);
|
|
281
|
+
const formatted = splitFormattedDateSegments(rawSegments);
|
|
282
|
+
if (!formatted) return void 0;
|
|
283
|
+
const segments = formatted.baseSegments;
|
|
248
284
|
if (segments[1] === "preset" && segments.length === 3 && PRESET_KEYS.has(segments[2])) {
|
|
249
285
|
return { type: segments[2] };
|
|
250
286
|
}
|
|
@@ -272,6 +308,60 @@ function parseCtxDateExpression(value) {
|
|
|
272
308
|
return void 0;
|
|
273
309
|
}
|
|
274
310
|
__name(parseCtxDateExpression, "parseCtxDateExpression");
|
|
311
|
+
function parseCtxDateExpressionConfig(value) {
|
|
312
|
+
if (!isCtxDateExpression(value)) return void 0;
|
|
313
|
+
const segments = withDatePrefix(parseCtxDateSegments(value) || []);
|
|
314
|
+
const formatted = splitFormattedDateSegments(segments);
|
|
315
|
+
if (!formatted) return void 0;
|
|
316
|
+
const parsed = parseCtxDateExpression(value);
|
|
317
|
+
const formatConfig = formatted.format ? { format: formatted.format } : {};
|
|
318
|
+
if (typeof parsed === "string") {
|
|
319
|
+
return { kind: "exact", value: parsed, ...formatConfig };
|
|
320
|
+
}
|
|
321
|
+
if (Array.isArray(parsed) && parsed.length === 2 && typeof parsed[0] === "string" && typeof parsed[1] === "string") {
|
|
322
|
+
return { kind: "exact", value: [parsed[0], parsed[1]], ...formatConfig };
|
|
323
|
+
}
|
|
324
|
+
if (!parsed || typeof parsed !== "object") return void 0;
|
|
325
|
+
const typed = parsed;
|
|
326
|
+
if (typed.type === "past" || typed.type === "next") {
|
|
327
|
+
if (typeof typed.unit !== "string" || !RELATIVE_UNITS.has(typed.unit) || typeof typed.number !== "number") {
|
|
328
|
+
return void 0;
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
kind: "relative",
|
|
332
|
+
direction: typed.type,
|
|
333
|
+
amount: typed.number,
|
|
334
|
+
unit: typed.unit,
|
|
335
|
+
...formatConfig
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
if (typeof typed.type === "string" && PRESET_KEYS.has(typed.type)) {
|
|
339
|
+
return { kind: "preset", preset: typed.type, ...formatConfig };
|
|
340
|
+
}
|
|
341
|
+
return void 0;
|
|
342
|
+
}
|
|
343
|
+
__name(parseCtxDateExpressionConfig, "parseCtxDateExpressionConfig");
|
|
344
|
+
function serializeCtxDateExpressionConfig(config) {
|
|
345
|
+
let legacyValue;
|
|
346
|
+
if (config.kind === "preset") {
|
|
347
|
+
if (!PRESET_KEYS.has(config.preset)) return void 0;
|
|
348
|
+
legacyValue = { type: config.preset };
|
|
349
|
+
} else if (config.kind === "relative") {
|
|
350
|
+
if (!RELATIVE_DIRECTIONS.has(config.direction) || !RELATIVE_UNITS.has(config.unit)) return void 0;
|
|
351
|
+
const amount = Math.floor(Number(config.amount));
|
|
352
|
+
if (!Number.isFinite(amount) || amount <= 0) return void 0;
|
|
353
|
+
legacyValue = { type: config.direction, unit: config.unit, number: amount };
|
|
354
|
+
} else {
|
|
355
|
+
legacyValue = config.value;
|
|
356
|
+
}
|
|
357
|
+
const expression = serializeCtxDateValue(legacyValue);
|
|
358
|
+
if (!expression || !config.format) return expression;
|
|
359
|
+
const format = String(config.format);
|
|
360
|
+
if (!format.trim() || format.length > MAX_DATE_FORMAT_LENGTH) return void 0;
|
|
361
|
+
const segments = withDatePrefix(parseCtxDateSegments(expression) || []);
|
|
362
|
+
return toCtxDateExpression(["date", "format", `v${encodeBase64Url(format)}`, ...segments.slice(1)]);
|
|
363
|
+
}
|
|
364
|
+
__name(serializeCtxDateExpressionConfig, "serializeCtxDateExpressionConfig");
|
|
275
365
|
function serializeCtxDateValue(value) {
|
|
276
366
|
if (isCtxDateExpression(value)) {
|
|
277
367
|
return String(value).trim();
|
|
@@ -317,8 +407,23 @@ function serializeCtxDateValue(value) {
|
|
|
317
407
|
return void 0;
|
|
318
408
|
}
|
|
319
409
|
__name(serializeCtxDateValue, "serializeCtxDateValue");
|
|
410
|
+
function formatResolvedDateValue(value, format) {
|
|
411
|
+
const formatValue = /* @__PURE__ */ __name((item) => {
|
|
412
|
+
if (typeof item !== "string") return item;
|
|
413
|
+
const parsed = (0, import_dayjs.default)(item);
|
|
414
|
+
return parsed.isValid() ? parsed.format(format) : item;
|
|
415
|
+
}, "formatValue");
|
|
416
|
+
return Array.isArray(value) ? value.map(formatValue) : formatValue(value);
|
|
417
|
+
}
|
|
418
|
+
__name(formatResolvedDateValue, "formatResolvedDateValue");
|
|
320
419
|
function resolveCtxDatePath(pathSegments) {
|
|
321
|
-
const
|
|
420
|
+
const rawSegments = withDatePrefix((pathSegments || []).map((seg) => String(seg)));
|
|
421
|
+
const formatted = splitFormattedDateSegments(rawSegments);
|
|
422
|
+
if (!formatted) return void 0;
|
|
423
|
+
if (formatted.format) {
|
|
424
|
+
return formatResolvedDateValue(resolveCtxDatePath(formatted.baseSegments), formatted.format);
|
|
425
|
+
}
|
|
426
|
+
const segments = formatted.baseSegments;
|
|
322
427
|
if (segments[0] !== "date") return void 0;
|
|
323
428
|
if (segments[1] === "preset" && segments.length === 3) {
|
|
324
429
|
const key = segments[2];
|
|
@@ -375,6 +480,8 @@ __name(resolveCtxDatePath, "resolveCtxDatePath");
|
|
|
375
480
|
isCtxDateExpression,
|
|
376
481
|
isCtxDatePathPrefix,
|
|
377
482
|
parseCtxDateExpression,
|
|
483
|
+
parseCtxDateExpressionConfig,
|
|
378
484
|
resolveCtxDatePath,
|
|
485
|
+
serializeCtxDateExpressionConfig,
|
|
379
486
|
serializeCtxDateValue
|
|
380
487
|
});
|
package/lib/utils/index.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export { extractPropertyPath, formatPathToVariable, isVariableExpression } from
|
|
|
21
21
|
export { clearAutoFlowError, getAutoFlowError, setAutoFlowError, type AutoFlowError } from './autoFlowError';
|
|
22
22
|
export { parsePathnameToViewParams, type ViewParam } from './parsePathnameToViewParams';
|
|
23
23
|
export { createOpenViewRouteState, decodeOpenViewRouteState, encodeOpenViewRouteState, isOpenViewRouteStateToken, RUNJS_OPEN_VIEW_ROUTE_STATE, type OpenViewRouteMode, type OpenViewRouteSize, type OpenViewRouteState, } from './openViewRouteState';
|
|
24
|
-
export { decodeBase64Url, encodeBase64Url, isCompleteCtxDatePath, isCtxDatePathPrefix, isCtxDateExpression, parseCtxDateExpression, resolveCtxDatePath, serializeCtxDateValue, } from './dateVariable';
|
|
24
|
+
export { decodeBase64Url, encodeBase64Url, isCompleteCtxDatePath, isCtxDatePathPrefix, isCtxDateExpression, parseCtxDateExpression, parseCtxDateExpressionConfig, resolveCtxDatePath, serializeCtxDateExpressionConfig, serializeCtxDateValue, type CtxDateExpressionConfig, type CtxDatePreset, type CtxDateRelativeDirection, type CtxDateRelativeUnit, } from './dateVariable';
|
|
25
25
|
export { isRunJSValue, normalizeRunJSValue, extractUsedVariablePathsFromRunJS, type RunJSValue } from './runjsValue';
|
|
26
26
|
export { resolveRunJSObjectValues } from './resolveRunJSObjectValues';
|
|
27
27
|
export { prepareRunJsCode, preprocessRunJsTemplates } from './runjsTemplateCompat';
|
package/lib/utils/index.js
CHANGED
|
@@ -73,6 +73,7 @@ __export(utils_exports, {
|
|
|
73
73
|
isVariableExpression: () => import_context.isVariableExpression,
|
|
74
74
|
normalizeRunJSValue: () => import_runjsValue.normalizeRunJSValue,
|
|
75
75
|
parseCtxDateExpression: () => import_dateVariable.parseCtxDateExpression,
|
|
76
|
+
parseCtxDateExpressionConfig: () => import_dateVariable.parseCtxDateExpressionConfig,
|
|
76
77
|
parsePathnameToViewParams: () => import_parsePathnameToViewParams.parsePathnameToViewParams,
|
|
77
78
|
prepareRunJsCode: () => import_runjsTemplateCompat.prepareRunJsCode,
|
|
78
79
|
preprocessRunJsTemplates: () => import_runjsTemplateCompat.preprocessRunJsTemplates,
|
|
@@ -87,6 +88,7 @@ __export(utils_exports, {
|
|
|
87
88
|
resolveStepDisabledInSettings: () => import_schema_utils.resolveStepDisabledInSettings,
|
|
88
89
|
resolveStepUiSchema: () => import_schema_utils.resolveStepUiSchema,
|
|
89
90
|
resolveUiMode: () => import_schema_utils.resolveUiMode,
|
|
91
|
+
serializeCtxDateExpressionConfig: () => import_dateVariable.serializeCtxDateExpressionConfig,
|
|
90
92
|
serializeCtxDateValue: () => import_dateVariable.serializeCtxDateValue,
|
|
91
93
|
setAutoFlowError: () => import_autoFlowError.setAutoFlowError,
|
|
92
94
|
setupRuntimeContextSteps: () => import_setupRuntimeContextSteps.setupRuntimeContextSteps,
|
|
@@ -168,6 +170,7 @@ var import_randomId = require("./randomId");
|
|
|
168
170
|
isVariableExpression,
|
|
169
171
|
normalizeRunJSValue,
|
|
170
172
|
parseCtxDateExpression,
|
|
173
|
+
parseCtxDateExpressionConfig,
|
|
171
174
|
parsePathnameToViewParams,
|
|
172
175
|
prepareRunJsCode,
|
|
173
176
|
preprocessRunJsTemplates,
|
|
@@ -182,6 +185,7 @@ var import_randomId = require("./randomId");
|
|
|
182
185
|
resolveStepDisabledInSettings,
|
|
183
186
|
resolveStepUiSchema,
|
|
184
187
|
resolveUiMode,
|
|
188
|
+
serializeCtxDateExpressionConfig,
|
|
185
189
|
serializeCtxDateValue,
|
|
186
190
|
setAutoFlowError,
|
|
187
191
|
setupRuntimeContextSteps,
|
|
@@ -146,6 +146,7 @@ function enqueueVariablesResolve(ctx, payload) {
|
|
|
146
146
|
try {
|
|
147
147
|
const batch = items.map((it) => ({
|
|
148
148
|
id: it.id,
|
|
149
|
+
contractRd: it.payload.contractRd,
|
|
149
150
|
rd: it.payload.rd,
|
|
150
151
|
template: it.payload.template,
|
|
151
152
|
contextParams: it.payload.contextParams || {}
|
|
@@ -28,6 +28,7 @@ interface PopupNodeResource {
|
|
|
28
28
|
interface PopupNode {
|
|
29
29
|
uid?: string;
|
|
30
30
|
resource: PopupNodeResource;
|
|
31
|
+
sourceRecord?: unknown;
|
|
31
32
|
parent?: PopupNode;
|
|
32
33
|
}
|
|
33
34
|
export declare function buildPopupRuntime(ctx: FlowContext, view: FlowView): Promise<PopupNode | undefined>;
|
|
@@ -428,10 +428,11 @@ function createPopupMeta(ctx, anchorView) {
|
|
|
428
428
|
}
|
|
429
429
|
__name(createPopupMeta, "createPopupMeta");
|
|
430
430
|
async function buildPopupRuntime(ctx, view) {
|
|
431
|
-
var _a;
|
|
431
|
+
var _a, _b, _c;
|
|
432
432
|
const stack = getViewStack(view);
|
|
433
433
|
const currentIndex = getAnchoredViewStackIndex(view, stack);
|
|
434
|
-
const
|
|
434
|
+
const sourceRecord = (_b = (_a = view == null ? void 0 : view.inputArgs) == null ? void 0 : _a.parentItem) == null ? void 0 : _b.value;
|
|
435
|
+
const openerUids = (_c = view == null ? void 0 : view.inputArgs) == null ? void 0 : _c.openerUids;
|
|
435
436
|
const hasOpener = Array.isArray(openerUids) && openerUids.length > 0;
|
|
436
437
|
const hasStackPopup = currentIndex >= 1;
|
|
437
438
|
const isPopup = hasStackPopup || hasOpener;
|
|
@@ -448,16 +449,17 @@ async function buildPopupRuntime(ctx, view) {
|
|
|
448
449
|
associationName: args.associationName,
|
|
449
450
|
filterByTk: args.filterByTk,
|
|
450
451
|
sourceId: args.sourceId
|
|
451
|
-
}
|
|
452
|
+
},
|
|
453
|
+
...typeof sourceRecord !== "undefined" ? { sourceRecord } : {}
|
|
452
454
|
};
|
|
453
455
|
}
|
|
454
456
|
const buildNode = /* @__PURE__ */ __name(async (idx) => {
|
|
455
|
-
var _a2,
|
|
457
|
+
var _a2, _b2, _c2, _d, _e, _f;
|
|
456
458
|
if (idx < 0 || !((_a2 = stack[idx]) == null ? void 0 : _a2.viewUid)) return void 0;
|
|
457
459
|
const viewUid = stack[idx].viewUid;
|
|
458
|
-
let model = (
|
|
460
|
+
let model = (_b2 = ctx.engine) == null ? void 0 : _b2.getModel(viewUid, true);
|
|
459
461
|
if (!model) {
|
|
460
|
-
model = await ((
|
|
462
|
+
model = await ((_c2 = ctx.engine) == null ? void 0 : _c2.loadModel({ uid: viewUid }));
|
|
461
463
|
}
|
|
462
464
|
const p = ((_d = model == null ? void 0 : model.getStepParams) == null ? void 0 : _d.call(model, "popupSettings", "openView")) || {};
|
|
463
465
|
const collectionName = p == null ? void 0 : p.collectionName;
|
|
@@ -477,16 +479,43 @@ async function buildPopupRuntime(ctx, view) {
|
|
|
477
479
|
return node;
|
|
478
480
|
}, "buildNode");
|
|
479
481
|
const currentNode = await buildNode(currentIndex);
|
|
482
|
+
if (currentNode && typeof sourceRecord !== "undefined") {
|
|
483
|
+
currentNode.sourceRecord = sourceRecord;
|
|
484
|
+
}
|
|
480
485
|
return currentNode;
|
|
481
486
|
}
|
|
482
487
|
__name(buildPopupRuntime, "buildPopupRuntime");
|
|
483
488
|
function registerPopupVariable(ctx, view) {
|
|
484
489
|
const POPUP_SERVER_PATH_RE = /^(?:record|sourceRecord)(?:\.|$)|^parent(?:\.parent)*(?:\.(?:record|sourceRecord))(?:\.|$)/;
|
|
490
|
+
const shouldResolveSourceRecordOnServer = /* @__PURE__ */ __name((path) => {
|
|
491
|
+
var _a, _b;
|
|
492
|
+
if (path !== "sourceRecord" && !path.startsWith("sourceRecord.")) return false;
|
|
493
|
+
const parentItem = (_a = view == null ? void 0 : view.inputArgs) == null ? void 0 : _a.parentItem;
|
|
494
|
+
if (typeof (parentItem == null ? void 0 : parentItem.value) === "undefined") return true;
|
|
495
|
+
const sourcePath = path === "sourceRecord" ? "" : path.slice("sourceRecord.".length);
|
|
496
|
+
if (!sourcePath) return false;
|
|
497
|
+
const parentItemResolver = (_b = view == null ? void 0 : view.inputArgs) == null ? void 0 : _b.parentItemResolver;
|
|
498
|
+
if (typeof parentItemResolver === "function") {
|
|
499
|
+
return parentItemResolver(`value.${sourcePath}`);
|
|
500
|
+
}
|
|
501
|
+
const segments = sourcePath.split(".").filter(Boolean);
|
|
502
|
+
let current = parentItem.value;
|
|
503
|
+
for (const segment of segments) {
|
|
504
|
+
if (current === null || typeof current !== "object" || !(segment in current)) {
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
current = current[segment];
|
|
508
|
+
}
|
|
509
|
+
return false;
|
|
510
|
+
}, "shouldResolveSourceRecordOnServer");
|
|
485
511
|
ctx.defineProperty("popup", {
|
|
486
512
|
get: /* @__PURE__ */ __name(async () => buildPopupRuntime(ctx, view), "get"),
|
|
487
513
|
meta: createPopupMeta(ctx, view),
|
|
488
514
|
resolveOnServer: /* @__PURE__ */ __name((p) => {
|
|
489
515
|
try {
|
|
516
|
+
if (p === "sourceRecord" || p.startsWith("sourceRecord.")) {
|
|
517
|
+
return shouldResolveSourceRecordOnServer(p);
|
|
518
|
+
}
|
|
490
519
|
return !!p && POPUP_SERVER_PATH_RE.test(p);
|
|
491
520
|
} catch (_) {
|
|
492
521
|
return false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/flow-engine",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.8",
|
|
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": "3.0.0-alpha.
|
|
12
|
-
"@nocobase/shared": "3.0.0-alpha.
|
|
11
|
+
"@nocobase/sdk": "3.0.0-alpha.8",
|
|
12
|
+
"@nocobase/shared": "3.0.0-alpha.8",
|
|
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": "ce017f3deb8b2414c6b13818042c4187270d1d8a"
|
|
41
41
|
}
|
|
@@ -11,7 +11,7 @@ import { describe, it, expect, vi } from 'vitest';
|
|
|
11
11
|
import { FlowContext } from '../flowContext';
|
|
12
12
|
import { FlowEngine } from '../flowEngine';
|
|
13
13
|
import type { FlowView } from '../views/FlowView';
|
|
14
|
-
import { buildPopupRuntime, createPopupMeta } from '../views/createViewMeta';
|
|
14
|
+
import { buildPopupRuntime, createPopupMeta, registerPopupVariable } from '../views/createViewMeta';
|
|
15
15
|
|
|
16
16
|
describe('createPopupMeta - popup variables', () => {
|
|
17
17
|
function makeCtx() {
|
|
@@ -167,6 +167,7 @@ describe('createPopupMeta - popup variables', () => {
|
|
|
167
167
|
it('buildPopupRuntime anchors current popup even when the navigation stack already has a child popup', async () => {
|
|
168
168
|
const { engine, ctx } = makeCtx();
|
|
169
169
|
const parentView = makeNestedPopupView('parent-popup-uid', 13);
|
|
170
|
+
parentView.inputArgs.parentItem = { value: { id: 13, phone: '9999' } };
|
|
170
171
|
mockNestedPopupModels(engine);
|
|
171
172
|
|
|
172
173
|
const popup = await buildPopupRuntime(ctx, parentView);
|
|
@@ -179,6 +180,7 @@ describe('createPopupMeta - popup variables', () => {
|
|
|
179
180
|
filterByTk: 13,
|
|
180
181
|
sourceId: 13,
|
|
181
182
|
});
|
|
183
|
+
expect(popup?.sourceRecord).toEqual({ id: 13, phone: '9999' });
|
|
182
184
|
});
|
|
183
185
|
|
|
184
186
|
it('buildVariablesParams(record) keeps the parent view record when a child popup is open', async () => {
|
|
@@ -314,4 +316,85 @@ describe('createPopupMeta - popup variables', () => {
|
|
|
314
316
|
const props = typeof meta.properties === 'function' ? await (meta.properties as any)() : meta.properties || {};
|
|
315
317
|
expect(props.record).toBeUndefined();
|
|
316
318
|
});
|
|
319
|
+
|
|
320
|
+
it('uses the current parent item value for popup sourceRecord fields', async () => {
|
|
321
|
+
const { ctx } = makeCtx();
|
|
322
|
+
const parentItemResolver = vi.fn(() => false);
|
|
323
|
+
const anchorView: FlowView = {
|
|
324
|
+
type: 'dialog',
|
|
325
|
+
inputArgs: {
|
|
326
|
+
openerUids: ['opener-uid-1'],
|
|
327
|
+
viewUid: 'popup-uid',
|
|
328
|
+
dataSourceKey: 'main',
|
|
329
|
+
collectionName: 'roles',
|
|
330
|
+
associationName: 'users.roles',
|
|
331
|
+
sourceId: 1,
|
|
332
|
+
parentItem: { value: { id: 1, phone: '9999' } },
|
|
333
|
+
parentItemResolver,
|
|
334
|
+
},
|
|
335
|
+
Header: null,
|
|
336
|
+
Footer: null,
|
|
337
|
+
close: () => void 0,
|
|
338
|
+
update: () => void 0,
|
|
339
|
+
} as any;
|
|
340
|
+
|
|
341
|
+
registerPopupVariable(ctx, anchorView);
|
|
342
|
+
|
|
343
|
+
await expect(ctx.popup).resolves.toMatchObject({
|
|
344
|
+
sourceRecord: { id: 1, phone: '9999' },
|
|
345
|
+
});
|
|
346
|
+
expect(ctx.getPropertyOptions('popup')?.resolveOnServer?.('sourceRecord.phone')).toBe(false);
|
|
347
|
+
expect(parentItemResolver).toHaveBeenCalledWith('value.phone');
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('resolves a popup sourceRecord field from the current parent item without a server request', async () => {
|
|
351
|
+
const engine = new FlowEngine();
|
|
352
|
+
const ctx = engine.context;
|
|
353
|
+
const anchorView: FlowView = {
|
|
354
|
+
type: 'dialog',
|
|
355
|
+
inputArgs: {
|
|
356
|
+
openerUids: ['opener-uid-1'],
|
|
357
|
+
viewUid: 'popup-uid',
|
|
358
|
+
dataSourceKey: 'main',
|
|
359
|
+
collectionName: 'roles',
|
|
360
|
+
associationName: 'users.roles',
|
|
361
|
+
sourceId: 1,
|
|
362
|
+
parentItem: { value: { id: 1, phone: '9999' } },
|
|
363
|
+
parentItemResolver: () => false,
|
|
364
|
+
},
|
|
365
|
+
Header: null,
|
|
366
|
+
Footer: null,
|
|
367
|
+
close: () => void 0,
|
|
368
|
+
update: () => void 0,
|
|
369
|
+
} as any;
|
|
370
|
+
|
|
371
|
+
registerPopupVariable(ctx, anchorView);
|
|
372
|
+
|
|
373
|
+
await expect(ctx.resolveJsonTemplate('{{ ctx.popup.sourceRecord.phone }}')).resolves.toBe('9999');
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('keeps server resolution for popup sourceRecord association subpaths', async () => {
|
|
377
|
+
const { ctx } = makeCtx();
|
|
378
|
+
const anchorView: FlowView = {
|
|
379
|
+
type: 'dialog',
|
|
380
|
+
inputArgs: {
|
|
381
|
+
openerUids: ['opener-uid-1'],
|
|
382
|
+
viewUid: 'popup-uid',
|
|
383
|
+
dataSourceKey: 'main',
|
|
384
|
+
collectionName: 'roles',
|
|
385
|
+
associationName: 'users.roles',
|
|
386
|
+
sourceId: 1,
|
|
387
|
+
parentItem: { value: { id: 1, departmentId: 2 } },
|
|
388
|
+
parentItemResolver: (path: string) => path === 'value.department.title',
|
|
389
|
+
},
|
|
390
|
+
Header: null,
|
|
391
|
+
Footer: null,
|
|
392
|
+
close: () => void 0,
|
|
393
|
+
update: () => void 0,
|
|
394
|
+
} as any;
|
|
395
|
+
|
|
396
|
+
registerPopupVariable(ctx, anchorView);
|
|
397
|
+
|
|
398
|
+
expect(ctx.getPropertyOptions('popup')?.resolveOnServer?.('sourceRecord.department.title')).toBe(true);
|
|
399
|
+
});
|
|
317
400
|
});
|
|
@@ -16,6 +16,7 @@ import { RunJSContextRegistry } from '../runjs-context/registry';
|
|
|
16
16
|
import { setupRunJSContexts } from '../runjs-context/setup';
|
|
17
17
|
import { createViewScopedEngine } from '../ViewScopedFlowEngine';
|
|
18
18
|
import { DATA_SOURCE_DIRTY_EVENT } from '../views/viewEvents';
|
|
19
|
+
import { serializeCtxDateExpressionConfig } from '../utils/dateVariable';
|
|
19
20
|
|
|
20
21
|
describe('FlowContext properties and methods', () => {
|
|
21
22
|
it('should return static property value', () => {
|
|
@@ -2068,10 +2069,16 @@ describe('getPropertyMetaTree with deep delegate meta', () => {
|
|
|
2068
2069
|
describe('FlowContext resolveOnServer selective server resolution', () => {
|
|
2069
2070
|
it('resolves ctx.date expressions on client context', async () => {
|
|
2070
2071
|
const engine = new FlowEngine();
|
|
2072
|
+
const formattedToday = serializeCtxDateExpressionConfig({
|
|
2073
|
+
kind: 'preset',
|
|
2074
|
+
preset: 'today',
|
|
2075
|
+
format: 'YYYY/MM/DD',
|
|
2076
|
+
});
|
|
2071
2077
|
const out = await (engine.context as any).resolveJsonTemplate({
|
|
2072
2078
|
today: '{{ ctx.date.preset.today }}',
|
|
2073
2079
|
next12: '{{ ctx.date.relative.next.day.n12 }}',
|
|
2074
2080
|
now: '{{ ctx.date.preset.now }}',
|
|
2081
|
+
formattedToday,
|
|
2075
2082
|
});
|
|
2076
2083
|
|
|
2077
2084
|
expect(typeof out.today).toBe('string');
|
|
@@ -2080,6 +2087,7 @@ describe('FlowContext resolveOnServer selective server resolution', () => {
|
|
|
2080
2087
|
expect(out.next12).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
|
2081
2088
|
expect(typeof out.now).toBe('string');
|
|
2082
2089
|
expect(out.now.length).toBeGreaterThan(0);
|
|
2090
|
+
expect(out.formattedToday).toMatch(/^\d{4}\/\d{2}\/\d{2}$/);
|
|
2083
2091
|
});
|
|
2084
2092
|
|
|
2085
2093
|
it('does not call server by default (no resolveOnServer set)', async () => {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { describe, expect, it, vi } from 'vitest';
|
|
11
|
+
import { generateFlowModelRdFromToken } from '@nocobase/utils/client';
|
|
11
12
|
import { FlowContext } from '../flowContext';
|
|
12
13
|
import { FlowEngine } from '../flowEngine';
|
|
13
14
|
import {
|
|
@@ -122,7 +123,10 @@ describe('objectVariable utilities', () => {
|
|
|
122
123
|
|
|
123
124
|
// Provide API stub to intercept variables:resolve
|
|
124
125
|
const calls: any[] = [];
|
|
126
|
+
const payload = Buffer.from(JSON.stringify({ userId: 1, signInTime: 'contract-owner-test' })).toString('base64url');
|
|
127
|
+
const token = `test.${payload}.sig`;
|
|
125
128
|
(ctx as any).api = {
|
|
129
|
+
auth: { token },
|
|
126
130
|
request: vi.fn(async ({ url, data, method }) => {
|
|
127
131
|
calls.push({ url, data, method });
|
|
128
132
|
const batch = (data?.values?.batch as any[]) || [];
|
|
@@ -146,13 +150,14 @@ describe('objectVariable utilities', () => {
|
|
|
146
150
|
});
|
|
147
151
|
|
|
148
152
|
const template = { x: '{{ ctx.obj.author.name }}' } as any;
|
|
149
|
-
await (ctx as any).resolveJsonTemplate(template);
|
|
153
|
+
await (ctx as any).resolveJsonTemplate(template, { contractModelUid: 'form-grid' });
|
|
150
154
|
|
|
151
155
|
// Assert variables:resolve was called with proper flattened contextParams
|
|
152
156
|
expect((ctx as any).api.request).toHaveBeenCalled();
|
|
153
157
|
const call = calls.find((c) => c.url === 'variables:resolve');
|
|
154
158
|
expect(call).toBeTruthy();
|
|
155
159
|
const batch0 = call.data?.values?.batch?.[0];
|
|
160
|
+
expect(batch0?.contractRd).toBe(generateFlowModelRdFromToken('form-grid', token));
|
|
156
161
|
expect(batch0?.contextParams).toBeTruthy();
|
|
157
162
|
// Flattened key should be 'obj.author'
|
|
158
163
|
const cp = batch0.contextParams as Record<string, any>;
|
|
@@ -311,6 +311,7 @@ const FlowContextSelectorComponent: React.FC<FlowContextSelectorProps> = ({
|
|
|
311
311
|
const path = selectedValues.map(String);
|
|
312
312
|
const pathString = path.join('.');
|
|
313
313
|
const isLeaf = lastOption?.isLeaf;
|
|
314
|
+
const isSelectable = lastOption?.meta?.selectable !== false;
|
|
314
315
|
const now = Date.now();
|
|
315
316
|
|
|
316
317
|
// 使用自定义格式化函数或默认函数
|
|
@@ -325,6 +326,10 @@ const FlowContextSelectorComponent: React.FC<FlowContextSelectorProps> = ({
|
|
|
325
326
|
}
|
|
326
327
|
|
|
327
328
|
if (isLeaf) {
|
|
329
|
+
if (!isSelectable) {
|
|
330
|
+
setTempSelectedPath(path);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
328
333
|
onChange?.(formattedValue, lastOption?.meta);
|
|
329
334
|
// 选中叶子节点后,可清空内部临时路径(外部 value 将驱动级联)
|
|
330
335
|
setTempSelectedPath([]);
|
|
@@ -333,7 +338,8 @@ const FlowContextSelectorComponent: React.FC<FlowContextSelectorProps> = ({
|
|
|
333
338
|
|
|
334
339
|
// 非叶子节点:检查双击
|
|
335
340
|
const lastSelected = lastSelectedRef.current;
|
|
336
|
-
const isDoubleClick =
|
|
341
|
+
const isDoubleClick =
|
|
342
|
+
isSelectable && !onlyLeafSelectable && lastSelected?.path === pathString && now - lastSelected.time < 300;
|
|
337
343
|
|
|
338
344
|
if (isDoubleClick) {
|
|
339
345
|
// 双击:选中非叶子节点
|
|
@@ -864,4 +864,39 @@ describe('FlowContextSelector', () => {
|
|
|
864
864
|
// It should only expand the node, not select it
|
|
865
865
|
expect(onChange).not.toHaveBeenCalled();
|
|
866
866
|
});
|
|
867
|
+
|
|
868
|
+
it('should expand but never select a node marked selectable=false', async () => {
|
|
869
|
+
const onChange = vi.fn();
|
|
870
|
+
const flowContext = createTestFlowContext();
|
|
871
|
+
const metaTree = [
|
|
872
|
+
{
|
|
873
|
+
name: 'date',
|
|
874
|
+
title: 'Date',
|
|
875
|
+
type: 'date',
|
|
876
|
+
paths: ['date'],
|
|
877
|
+
selectable: false,
|
|
878
|
+
children: [{ name: 'today', title: 'Today', type: 'date', paths: ['date', 'today'] }],
|
|
879
|
+
},
|
|
880
|
+
];
|
|
881
|
+
|
|
882
|
+
render(
|
|
883
|
+
<TestFlowContextWrapper context={flowContext}>
|
|
884
|
+
<FlowContextSelector metaTree={metaTree} onChange={onChange} />
|
|
885
|
+
</TestFlowContextWrapper>,
|
|
886
|
+
);
|
|
887
|
+
|
|
888
|
+
fireEvent.click(screen.getByRole('button'));
|
|
889
|
+
await waitFor(() => expect(screen.getByText('Date')).toBeInTheDocument());
|
|
890
|
+
|
|
891
|
+
fireEvent.click(screen.getByText('Date'));
|
|
892
|
+
fireEvent.click(screen.getByText('Date'));
|
|
893
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
894
|
+
|
|
895
|
+
await waitFor(() => expect(screen.getByText('Today')).toBeInTheDocument());
|
|
896
|
+
fireEvent.click(screen.getByText('Today'));
|
|
897
|
+
expect(onChange).toHaveBeenCalledWith(
|
|
898
|
+
'{{ ctx.date.today }}',
|
|
899
|
+
expect.objectContaining({ paths: ['date', 'today'] }),
|
|
900
|
+
);
|
|
901
|
+
});
|
|
867
902
|
});
|
package/src/flowContext.ts
CHANGED
|
@@ -165,6 +165,10 @@ function inferSelectsFromUsage(paths: string[] = []): { generatedAppends?: strin
|
|
|
165
165
|
|
|
166
166
|
type Getter<T = any> = (ctx: FlowContext) => T | Promise<T>;
|
|
167
167
|
|
|
168
|
+
export type ResolveJsonTemplateOptions = {
|
|
169
|
+
contractModelUid?: string | number | null;
|
|
170
|
+
};
|
|
171
|
+
|
|
168
172
|
export type FlowContextDocRef = string | { url: string; title?: string };
|
|
169
173
|
|
|
170
174
|
export type FlowDeprecationDoc =
|
|
@@ -221,6 +225,8 @@ export interface MetaTreeNode {
|
|
|
221
225
|
// 变量禁用状态与原因(用于变量选择器 UI 展示)
|
|
222
226
|
disabled?: boolean | (() => boolean);
|
|
223
227
|
disabledReason?: string | (() => string | undefined);
|
|
228
|
+
// 允许节点仅用于展开子级,而不能作为变量值被选中
|
|
229
|
+
selectable?: boolean;
|
|
224
230
|
children?: MetaTreeNode[] | (() => Promise<MetaTreeNode[]>);
|
|
225
231
|
}
|
|
226
232
|
|
|
@@ -3044,7 +3050,7 @@ class BaseFlowEngineContext extends FlowContext {
|
|
|
3044
3050
|
* @deprecated use `resolveJsonTemplate` instead
|
|
3045
3051
|
*/
|
|
3046
3052
|
declare renderJson: (template: JSONValue) => Promise<any>;
|
|
3047
|
-
declare resolveJsonTemplate: (template: JSONValue) => Promise<any>;
|
|
3053
|
+
declare resolveJsonTemplate: (template: JSONValue, options?: ResolveJsonTemplateOptions) => Promise<any>;
|
|
3048
3054
|
declare getVar: (path: string) => Promise<any>;
|
|
3049
3055
|
declare request: (options: RequestOptions) => Promise<any>;
|
|
3050
3056
|
declare runjs: (code: string, variables?: Record<string, any>, options?: JSRunnerOptions) => Promise<any>;
|
|
@@ -3227,7 +3233,11 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
3227
3233
|
this.defineMethod('renderJson', function (template: any) {
|
|
3228
3234
|
return this.resolveJsonTemplate(template);
|
|
3229
3235
|
});
|
|
3230
|
-
|
|
3236
|
+
const resolveJsonTemplate = async function (
|
|
3237
|
+
this: BaseFlowEngineContext,
|
|
3238
|
+
template: any,
|
|
3239
|
+
options?: ResolveJsonTemplateOptions,
|
|
3240
|
+
) {
|
|
3231
3241
|
// 提取模板使用到的变量及其子路径
|
|
3232
3242
|
const used = extractUsedVariablePaths(template);
|
|
3233
3243
|
const usedVarNames = Object.keys(used || {});
|
|
@@ -3396,7 +3406,12 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
3396
3406
|
|
|
3397
3407
|
if (this.api) {
|
|
3398
3408
|
try {
|
|
3409
|
+
const contractRd = buildFlowModelResolveDescriptor(
|
|
3410
|
+
this as FlowRuntimeContext<FlowModel>,
|
|
3411
|
+
options?.contractModelUid,
|
|
3412
|
+
);
|
|
3399
3413
|
serverResolved = await enqueueVariablesResolve(this as FlowRuntimeContext<FlowModel>, {
|
|
3414
|
+
...(contractRd ? { contractRd } : {}),
|
|
3400
3415
|
rd: buildFlowModelResolveDescriptor(this as FlowRuntimeContext<FlowModel>, this.model?.uid),
|
|
3401
3416
|
template,
|
|
3402
3417
|
contextParams: autoContextParams || {},
|
|
@@ -3409,7 +3424,8 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
3409
3424
|
}
|
|
3410
3425
|
|
|
3411
3426
|
return resolveExpressions(serverResolved, this);
|
|
3412
|
-
}
|
|
3427
|
+
};
|
|
3428
|
+
this.defineMethod('resolveJsonTemplate', resolveJsonTemplate);
|
|
3413
3429
|
|
|
3414
3430
|
// Helper: resolve a single ctx expression value via resolveJsonTemplate behavior.
|
|
3415
3431
|
// Example: await ctx.getVar('ctx.record.id')
|
|
@@ -12,9 +12,12 @@ import {
|
|
|
12
12
|
decodeBase64Url,
|
|
13
13
|
encodeBase64Url,
|
|
14
14
|
isCompleteCtxDatePath,
|
|
15
|
+
isCtxDatePathPrefix,
|
|
15
16
|
isCtxDateExpression,
|
|
16
17
|
parseCtxDateExpression,
|
|
18
|
+
parseCtxDateExpressionConfig,
|
|
17
19
|
resolveCtxDatePath,
|
|
20
|
+
serializeCtxDateExpressionConfig,
|
|
18
21
|
serializeCtxDateValue,
|
|
19
22
|
} from '../dateVariable';
|
|
20
23
|
|
|
@@ -54,13 +57,60 @@ describe('dateVariable utils', () => {
|
|
|
54
57
|
number: 2,
|
|
55
58
|
});
|
|
56
59
|
|
|
57
|
-
const singleExpr = serializeCtxDateValue('2026-02-12')
|
|
60
|
+
const singleExpr = serializeCtxDateValue('2026-02-12');
|
|
61
|
+
if (!singleExpr) throw new Error('Expected exact date expression');
|
|
58
62
|
expect(parseCtxDateExpression(singleExpr)).toBe('2026-02-12');
|
|
59
63
|
|
|
60
|
-
const rangeExpr = serializeCtxDateValue(['2026-02-12', '2026-02-20'])
|
|
64
|
+
const rangeExpr = serializeCtxDateValue(['2026-02-12', '2026-02-20']);
|
|
65
|
+
if (!rangeExpr) throw new Error('Expected exact date range expression');
|
|
61
66
|
expect(parseCtxDateExpression(rangeExpr)).toEqual(['2026-02-12', '2026-02-20']);
|
|
62
67
|
});
|
|
63
68
|
|
|
69
|
+
it('serializes, parses and resolves formatted expressions', () => {
|
|
70
|
+
const expression = serializeCtxDateExpressionConfig({
|
|
71
|
+
kind: 'preset',
|
|
72
|
+
preset: 'today',
|
|
73
|
+
format: 'YYYY/MM/DD',
|
|
74
|
+
});
|
|
75
|
+
if (!expression) throw new Error('Expected formatted date expression');
|
|
76
|
+
|
|
77
|
+
expect(expression).toMatch(/^\{\{ ctx\.date\.format\.v[A-Za-z0-9_-]+\.preset\.today \}\}$/);
|
|
78
|
+
expect(parseCtxDateExpressionConfig(expression)).toEqual({
|
|
79
|
+
kind: 'preset',
|
|
80
|
+
preset: 'today',
|
|
81
|
+
format: 'YYYY/MM/DD',
|
|
82
|
+
});
|
|
83
|
+
// Keep the legacy parser contract for filter-form consumers.
|
|
84
|
+
expect(parseCtxDateExpression(expression)).toEqual({ type: 'today' });
|
|
85
|
+
|
|
86
|
+
const path = expression.replace('{{ ctx.', '').replace(' }}', '').split('.');
|
|
87
|
+
expect(resolveCtxDatePath(path)).toMatch(/^\d{4}\/\d{2}\/\d{2}$/);
|
|
88
|
+
expect(isCompleteCtxDatePath(path)).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('preserves significant whitespace in a custom Format', () => {
|
|
92
|
+
const expression = serializeCtxDateExpressionConfig({
|
|
93
|
+
kind: 'preset',
|
|
94
|
+
preset: 'today',
|
|
95
|
+
format: 'YYYY-MM-DD ',
|
|
96
|
+
});
|
|
97
|
+
if (!expression) throw new Error('Expected formatted date expression');
|
|
98
|
+
|
|
99
|
+
expect(parseCtxDateExpressionConfig(expression)?.format).toBe('YYYY-MM-DD ');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('formats exact ranges element by element', () => {
|
|
103
|
+
const expression = serializeCtxDateExpressionConfig({
|
|
104
|
+
kind: 'exact',
|
|
105
|
+
value: ['2026-02-12', '2026-02-20'],
|
|
106
|
+
format: 'YYYYMMDD',
|
|
107
|
+
});
|
|
108
|
+
if (!expression) throw new Error('Expected formatted date range expression');
|
|
109
|
+
const path = expression.replace('{{ ctx.', '').replace(' }}', '').split('.');
|
|
110
|
+
|
|
111
|
+
expect(resolveCtxDatePath(path)).toEqual(['20260212', '20260220']);
|
|
112
|
+
});
|
|
113
|
+
|
|
64
114
|
it('resolves preset/relative/exact path', () => {
|
|
65
115
|
expect(typeof resolveCtxDatePath(['date', 'preset', 'now'])).toBe('string');
|
|
66
116
|
|
|
@@ -72,11 +122,13 @@ describe('dateVariable utils', () => {
|
|
|
72
122
|
expect(typeof rel).toBe('string');
|
|
73
123
|
expect(rel).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
|
74
124
|
|
|
75
|
-
const singleExpr = serializeCtxDateValue('2026-02-12')
|
|
125
|
+
const singleExpr = serializeCtxDateValue('2026-02-12');
|
|
126
|
+
if (!singleExpr) throw new Error('Expected exact date expression');
|
|
76
127
|
const token = singleExpr.replace('{{ ctx.date.exact.single.date.', '').replace(' }}', '');
|
|
77
128
|
expect(resolveCtxDatePath(['date', 'exact', 'single', 'date', token])).toBe('2026-02-12');
|
|
78
129
|
|
|
79
|
-
const rangeExpr = serializeCtxDateValue(['2026-02-12', '2026-02-20'])
|
|
130
|
+
const rangeExpr = serializeCtxDateValue(['2026-02-12', '2026-02-20']);
|
|
131
|
+
if (!rangeExpr) throw new Error('Expected exact date range expression');
|
|
80
132
|
const parts = rangeExpr.replace('{{ ctx.date.exact.range.date.', '').replace(' }}', '').split('.');
|
|
81
133
|
expect(resolveCtxDatePath(['date', 'exact', 'range', 'date', parts[0], parts[1]])).toEqual([
|
|
82
134
|
'2026-02-12',
|
|
@@ -90,6 +142,7 @@ describe('dateVariable utils', () => {
|
|
|
90
142
|
expect(isCompleteCtxDatePath(['date', 'exact', 'single', 'date', 'vabc'])).toBe(true);
|
|
91
143
|
expect(isCompleteCtxDatePath(['date', 'exact', 'range', 'date', 'vabc', 'vdef'])).toBe(true);
|
|
92
144
|
expect(isCompleteCtxDatePath(['date', 'relative', 'next', 'day'])).toBe(false);
|
|
145
|
+
expect(isCtxDatePathPrefix(['date', 'format'])).toBe(true);
|
|
93
146
|
expect(isCompleteCtxDatePath(['user', 'name'])).toBe(false);
|
|
94
147
|
});
|
|
95
148
|
|
|
@@ -11,7 +11,7 @@ import dayjs from 'dayjs';
|
|
|
11
11
|
|
|
12
12
|
const CTX_DATE_REGEX = /^\{\{\s*ctx\.date(?:\.(.+?))?\s*\}\}$/;
|
|
13
13
|
|
|
14
|
-
const
|
|
14
|
+
const PRESET_KEY_LIST = [
|
|
15
15
|
'today',
|
|
16
16
|
'now',
|
|
17
17
|
'yesterday',
|
|
@@ -28,10 +28,28 @@ const PRESET_KEYS = new Set([
|
|
|
28
28
|
'thisYear',
|
|
29
29
|
'lastYear',
|
|
30
30
|
'nextYear',
|
|
31
|
-
]
|
|
31
|
+
] as const;
|
|
32
|
+
|
|
33
|
+
export type CtxDatePreset = (typeof PRESET_KEY_LIST)[number];
|
|
34
|
+
export type CtxDateRelativeDirection = 'next' | 'past';
|
|
35
|
+
export type CtxDateRelativeUnit = 'day' | 'week' | 'month' | 'year';
|
|
36
|
+
|
|
37
|
+
export type CtxDateExpressionConfig =
|
|
38
|
+
| { kind: 'exact'; value: string | [string, string]; format?: string }
|
|
39
|
+
| {
|
|
40
|
+
kind: 'relative';
|
|
41
|
+
direction: CtxDateRelativeDirection;
|
|
42
|
+
amount: number;
|
|
43
|
+
unit: CtxDateRelativeUnit;
|
|
44
|
+
format?: string;
|
|
45
|
+
}
|
|
46
|
+
| { kind: 'preset'; preset: CtxDatePreset; format?: string };
|
|
47
|
+
|
|
48
|
+
const PRESET_KEYS = new Set<string>(PRESET_KEY_LIST);
|
|
32
49
|
|
|
33
50
|
const RELATIVE_DIRECTIONS = new Set(['next', 'past']);
|
|
34
51
|
const RELATIVE_UNITS = new Set(['day', 'week', 'month', 'year']);
|
|
52
|
+
const MAX_DATE_FORMAT_LENGTH = 128;
|
|
35
53
|
|
|
36
54
|
function parseCtxDateSegments(value: string): string[] | null {
|
|
37
55
|
if (typeof value !== 'string') return null;
|
|
@@ -46,8 +64,7 @@ function parseCtxDateSegments(value: string): string[] | null {
|
|
|
46
64
|
.filter(Boolean);
|
|
47
65
|
}
|
|
48
66
|
|
|
49
|
-
|
|
50
|
-
const segments = withDatePrefix((pathSegments || []).map((seg) => String(seg)));
|
|
67
|
+
function isBaseCtxDatePathPrefix(segments: string[]): boolean {
|
|
51
68
|
if (segments[0] !== 'date') return false;
|
|
52
69
|
if (segments.length === 1) return true;
|
|
53
70
|
|
|
@@ -96,6 +113,36 @@ export function isCtxDatePathPrefix(pathSegments: string[]): boolean {
|
|
|
96
113
|
return false;
|
|
97
114
|
}
|
|
98
115
|
|
|
116
|
+
function decodeFormatToken(token: string): string | undefined {
|
|
117
|
+
const raw = String(token || '');
|
|
118
|
+
if (!raw.startsWith('v')) return undefined;
|
|
119
|
+
const decoded = decodeBase64Url(raw.slice(1));
|
|
120
|
+
if (!decoded || decoded.length > MAX_DATE_FORMAT_LENGTH) return undefined;
|
|
121
|
+
return decoded;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function splitFormattedDateSegments(segments: string[]): { baseSegments: string[]; format?: string } | null {
|
|
125
|
+
if (segments[0] !== 'date') return null;
|
|
126
|
+
if (segments[1] !== 'format') return { baseSegments: segments };
|
|
127
|
+
if (segments.length < 4) return null;
|
|
128
|
+
|
|
129
|
+
const format = decodeFormatToken(segments[2]);
|
|
130
|
+
if (!format) return null;
|
|
131
|
+
return { baseSegments: ['date', ...segments.slice(3)], format };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function isCtxDatePathPrefix(pathSegments: string[]): boolean {
|
|
135
|
+
const segments = withDatePrefix((pathSegments || []).map((seg) => String(seg)));
|
|
136
|
+
if (segments[0] !== 'date') return false;
|
|
137
|
+
if (segments.length === 1) return true;
|
|
138
|
+
if (segments[1] !== 'format') return isBaseCtxDatePathPrefix(segments);
|
|
139
|
+
if (segments.length === 2) return true;
|
|
140
|
+
if (segments.length === 3) return typeof decodeFormatToken(segments[2]) === 'string';
|
|
141
|
+
|
|
142
|
+
const formatted = splitFormattedDateSegments(segments);
|
|
143
|
+
return formatted ? isBaseCtxDatePathPrefix(formatted.baseSegments) : false;
|
|
144
|
+
}
|
|
145
|
+
|
|
99
146
|
function withDatePrefix(pathSegments: string[]): string[] {
|
|
100
147
|
if (pathSegments[0] === 'date') {
|
|
101
148
|
return pathSegments;
|
|
@@ -210,27 +257,29 @@ export function isCtxDateExpression(value: unknown): value is string {
|
|
|
210
257
|
export function isCompleteCtxDatePath(pathSegments: string[]): boolean {
|
|
211
258
|
if (!isCtxDatePathPrefix(pathSegments)) return false;
|
|
212
259
|
const segments = withDatePrefix((pathSegments || []).map((seg) => String(seg)));
|
|
213
|
-
|
|
260
|
+
const formatted = splitFormattedDateSegments(segments);
|
|
261
|
+
if (!formatted) return false;
|
|
262
|
+
const baseSegments = formatted.baseSegments;
|
|
214
263
|
|
|
215
|
-
if (
|
|
216
|
-
return
|
|
264
|
+
if (baseSegments[1] === 'preset') {
|
|
265
|
+
return baseSegments.length === 3 && PRESET_KEYS.has(baseSegments[2]);
|
|
217
266
|
}
|
|
218
267
|
|
|
219
|
-
if (
|
|
220
|
-
if (
|
|
268
|
+
if (baseSegments[1] === 'relative') {
|
|
269
|
+
if (baseSegments.length !== 5) return false;
|
|
221
270
|
return (
|
|
222
|
-
RELATIVE_DIRECTIONS.has(
|
|
223
|
-
RELATIVE_UNITS.has(
|
|
224
|
-
typeof parseNumberToken(
|
|
271
|
+
RELATIVE_DIRECTIONS.has(baseSegments[2]) &&
|
|
272
|
+
RELATIVE_UNITS.has(baseSegments[3]) &&
|
|
273
|
+
typeof parseNumberToken(baseSegments[4]) === 'number'
|
|
225
274
|
);
|
|
226
275
|
}
|
|
227
276
|
|
|
228
|
-
if (
|
|
229
|
-
return
|
|
277
|
+
if (baseSegments[1] === 'exact' && baseSegments[2] === 'single' && baseSegments[3] === 'date') {
|
|
278
|
+
return baseSegments.length === 5 && /^v.+/.test(baseSegments[4]);
|
|
230
279
|
}
|
|
231
280
|
|
|
232
|
-
if (
|
|
233
|
-
return
|
|
281
|
+
if (baseSegments[1] === 'exact' && baseSegments[2] === 'range' && baseSegments[3] === 'date') {
|
|
282
|
+
return baseSegments.length === 6 && /^v.+/.test(baseSegments[4]) && /^v.+/.test(baseSegments[5]);
|
|
234
283
|
}
|
|
235
284
|
|
|
236
285
|
return false;
|
|
@@ -238,7 +287,10 @@ export function isCompleteCtxDatePath(pathSegments: string[]): boolean {
|
|
|
238
287
|
|
|
239
288
|
export function parseCtxDateExpression(value: unknown): any {
|
|
240
289
|
if (!isCtxDateExpression(value)) return undefined;
|
|
241
|
-
const
|
|
290
|
+
const rawSegments = withDatePrefix(parseCtxDateSegments(value as string) || []);
|
|
291
|
+
const formatted = splitFormattedDateSegments(rawSegments);
|
|
292
|
+
if (!formatted) return undefined;
|
|
293
|
+
const segments = formatted.baseSegments;
|
|
242
294
|
|
|
243
295
|
if (segments[1] === 'preset' && segments.length === 3 && PRESET_KEYS.has(segments[2])) {
|
|
244
296
|
return { type: segments[2] };
|
|
@@ -276,6 +328,66 @@ export function parseCtxDateExpression(value: unknown): any {
|
|
|
276
328
|
return undefined;
|
|
277
329
|
}
|
|
278
330
|
|
|
331
|
+
export function parseCtxDateExpressionConfig(value: unknown): CtxDateExpressionConfig | undefined {
|
|
332
|
+
if (!isCtxDateExpression(value)) return undefined;
|
|
333
|
+
const segments = withDatePrefix(parseCtxDateSegments(value) || []);
|
|
334
|
+
const formatted = splitFormattedDateSegments(segments);
|
|
335
|
+
if (!formatted) return undefined;
|
|
336
|
+
|
|
337
|
+
const parsed = parseCtxDateExpression(value);
|
|
338
|
+
const formatConfig = formatted.format ? { format: formatted.format } : {};
|
|
339
|
+
if (typeof parsed === 'string') {
|
|
340
|
+
return { kind: 'exact', value: parsed, ...formatConfig };
|
|
341
|
+
}
|
|
342
|
+
if (Array.isArray(parsed) && parsed.length === 2 && typeof parsed[0] === 'string' && typeof parsed[1] === 'string') {
|
|
343
|
+
return { kind: 'exact', value: [parsed[0], parsed[1]], ...formatConfig };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (!parsed || typeof parsed !== 'object') return undefined;
|
|
347
|
+
const typed = parsed as { type?: unknown; unit?: unknown; number?: unknown };
|
|
348
|
+
if (typed.type === 'past' || typed.type === 'next') {
|
|
349
|
+
if (typeof typed.unit !== 'string' || !RELATIVE_UNITS.has(typed.unit) || typeof typed.number !== 'number') {
|
|
350
|
+
return undefined;
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
kind: 'relative',
|
|
354
|
+
direction: typed.type,
|
|
355
|
+
amount: typed.number,
|
|
356
|
+
unit: typed.unit as CtxDateRelativeUnit,
|
|
357
|
+
...formatConfig,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (typeof typed.type === 'string' && PRESET_KEYS.has(typed.type)) {
|
|
362
|
+
return { kind: 'preset', preset: typed.type as CtxDatePreset, ...formatConfig };
|
|
363
|
+
}
|
|
364
|
+
return undefined;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function serializeCtxDateExpressionConfig(config: CtxDateExpressionConfig): string | undefined {
|
|
368
|
+
let legacyValue: unknown;
|
|
369
|
+
|
|
370
|
+
if (config.kind === 'preset') {
|
|
371
|
+
if (!PRESET_KEYS.has(config.preset)) return undefined;
|
|
372
|
+
legacyValue = { type: config.preset };
|
|
373
|
+
} else if (config.kind === 'relative') {
|
|
374
|
+
if (!RELATIVE_DIRECTIONS.has(config.direction) || !RELATIVE_UNITS.has(config.unit)) return undefined;
|
|
375
|
+
const amount = Math.floor(Number(config.amount));
|
|
376
|
+
if (!Number.isFinite(amount) || amount <= 0) return undefined;
|
|
377
|
+
legacyValue = { type: config.direction, unit: config.unit, number: amount };
|
|
378
|
+
} else {
|
|
379
|
+
legacyValue = config.value;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const expression = serializeCtxDateValue(legacyValue);
|
|
383
|
+
if (!expression || !config.format) return expression;
|
|
384
|
+
|
|
385
|
+
const format = String(config.format);
|
|
386
|
+
if (!format.trim() || format.length > MAX_DATE_FORMAT_LENGTH) return undefined;
|
|
387
|
+
const segments = withDatePrefix(parseCtxDateSegments(expression) || []);
|
|
388
|
+
return toCtxDateExpression(['date', 'format', `v${encodeBase64Url(format)}`, ...segments.slice(1)]);
|
|
389
|
+
}
|
|
390
|
+
|
|
279
391
|
export function serializeCtxDateValue(value: unknown): string | undefined {
|
|
280
392
|
if (isCtxDateExpression(value)) {
|
|
281
393
|
return String(value).trim();
|
|
@@ -327,8 +439,23 @@ export function serializeCtxDateValue(value: unknown): string | undefined {
|
|
|
327
439
|
return undefined;
|
|
328
440
|
}
|
|
329
441
|
|
|
442
|
+
function formatResolvedDateValue(value: unknown, format: string): unknown {
|
|
443
|
+
const formatValue = (item: unknown) => {
|
|
444
|
+
if (typeof item !== 'string') return item;
|
|
445
|
+
const parsed = dayjs(item);
|
|
446
|
+
return parsed.isValid() ? parsed.format(format) : item;
|
|
447
|
+
};
|
|
448
|
+
return Array.isArray(value) ? value.map(formatValue) : formatValue(value);
|
|
449
|
+
}
|
|
450
|
+
|
|
330
451
|
export function resolveCtxDatePath(pathSegments: string[]): any {
|
|
331
|
-
const
|
|
452
|
+
const rawSegments = withDatePrefix((pathSegments || []).map((seg) => String(seg)));
|
|
453
|
+
const formatted = splitFormattedDateSegments(rawSegments);
|
|
454
|
+
if (!formatted) return undefined;
|
|
455
|
+
if (formatted.format) {
|
|
456
|
+
return formatResolvedDateValue(resolveCtxDatePath(formatted.baseSegments), formatted.format);
|
|
457
|
+
}
|
|
458
|
+
const segments = formatted.baseSegments;
|
|
332
459
|
if (segments[0] !== 'date') return undefined;
|
|
333
460
|
|
|
334
461
|
if (segments[1] === 'preset' && segments.length === 3) {
|
package/src/utils/index.ts
CHANGED
|
@@ -92,8 +92,14 @@ export {
|
|
|
92
92
|
isCtxDatePathPrefix,
|
|
93
93
|
isCtxDateExpression,
|
|
94
94
|
parseCtxDateExpression,
|
|
95
|
+
parseCtxDateExpressionConfig,
|
|
95
96
|
resolveCtxDatePath,
|
|
97
|
+
serializeCtxDateExpressionConfig,
|
|
96
98
|
serializeCtxDateValue,
|
|
99
|
+
type CtxDateExpressionConfig,
|
|
100
|
+
type CtxDatePreset,
|
|
101
|
+
type CtxDateRelativeDirection,
|
|
102
|
+
type CtxDateRelativeUnit,
|
|
97
103
|
} from './dateVariable';
|
|
98
104
|
|
|
99
105
|
// RunJS value helpers
|
|
@@ -79,6 +79,7 @@ export type JSONValue = string | { [key: string]: JSONValue } | JSONValue[];
|
|
|
79
79
|
// =========================
|
|
80
80
|
|
|
81
81
|
type BatchPayload = {
|
|
82
|
+
contractRd?: string;
|
|
82
83
|
rd?: string;
|
|
83
84
|
template: JSONValue;
|
|
84
85
|
contextParams?: ServerContextParams | undefined;
|
|
@@ -172,6 +173,7 @@ export function enqueueVariablesResolve(ctx: FlowRuntimeContext, payload: BatchP
|
|
|
172
173
|
try {
|
|
173
174
|
const batch = items.map((it) => ({
|
|
174
175
|
id: it.id,
|
|
176
|
+
contractRd: it.payload.contractRd,
|
|
175
177
|
rd: it.payload.rd,
|
|
176
178
|
template: it.payload.template,
|
|
177
179
|
contextParams: it.payload.contextParams || {},
|
|
@@ -475,12 +475,14 @@ interface PopupNodeResource {
|
|
|
475
475
|
interface PopupNode {
|
|
476
476
|
uid?: string;
|
|
477
477
|
resource: PopupNodeResource;
|
|
478
|
+
sourceRecord?: unknown;
|
|
478
479
|
parent?: PopupNode;
|
|
479
480
|
}
|
|
480
481
|
|
|
481
482
|
export async function buildPopupRuntime(ctx: FlowContext, view: FlowView): Promise<PopupNode | undefined> {
|
|
482
483
|
const stack = getViewStack(view);
|
|
483
484
|
const currentIndex = getAnchoredViewStackIndex(view, stack);
|
|
485
|
+
const sourceRecord = view?.inputArgs?.parentItem?.value;
|
|
484
486
|
|
|
485
487
|
const openerUids = view?.inputArgs?.openerUids;
|
|
486
488
|
const hasOpener = Array.isArray(openerUids) && openerUids.length > 0;
|
|
@@ -503,6 +505,7 @@ export async function buildPopupRuntime(ctx: FlowContext, view: FlowView): Promi
|
|
|
503
505
|
filterByTk: args.filterByTk,
|
|
504
506
|
sourceId: args.sourceId,
|
|
505
507
|
},
|
|
508
|
+
...(typeof sourceRecord !== 'undefined' ? { sourceRecord } : {}),
|
|
506
509
|
};
|
|
507
510
|
}
|
|
508
511
|
|
|
@@ -531,6 +534,9 @@ export async function buildPopupRuntime(ctx: FlowContext, view: FlowView): Promi
|
|
|
531
534
|
return node;
|
|
532
535
|
};
|
|
533
536
|
const currentNode = await buildNode(currentIndex);
|
|
537
|
+
if (currentNode && typeof sourceRecord !== 'undefined') {
|
|
538
|
+
currentNode.sourceRecord = sourceRecord;
|
|
539
|
+
}
|
|
534
540
|
return currentNode;
|
|
535
541
|
}
|
|
536
542
|
|
|
@@ -542,6 +548,30 @@ export function registerPopupVariable(ctx: FlowContext, view: FlowView) {
|
|
|
542
548
|
// - 任意层级 parent.parent... 下的 record / sourceRecord 及其子字段
|
|
543
549
|
const POPUP_SERVER_PATH_RE =
|
|
544
550
|
/^(?:record|sourceRecord)(?:\.|$)|^parent(?:\.parent)*(?:\.(?:record|sourceRecord))(?:\.|$)/;
|
|
551
|
+
const shouldResolveSourceRecordOnServer = (path: string): boolean => {
|
|
552
|
+
if (path !== 'sourceRecord' && !path.startsWith('sourceRecord.')) return false;
|
|
553
|
+
|
|
554
|
+
const parentItem = view?.inputArgs?.parentItem;
|
|
555
|
+
if (typeof parentItem?.value === 'undefined') return true;
|
|
556
|
+
|
|
557
|
+
const sourcePath = path === 'sourceRecord' ? '' : path.slice('sourceRecord.'.length);
|
|
558
|
+
if (!sourcePath) return false;
|
|
559
|
+
|
|
560
|
+
const parentItemResolver = view?.inputArgs?.parentItemResolver;
|
|
561
|
+
if (typeof parentItemResolver === 'function') {
|
|
562
|
+
return parentItemResolver(`value.${sourcePath}`);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const segments = sourcePath.split('.').filter(Boolean);
|
|
566
|
+
let current = parentItem.value;
|
|
567
|
+
for (const segment of segments) {
|
|
568
|
+
if (current === null || typeof current !== 'object' || !(segment in current)) {
|
|
569
|
+
return true;
|
|
570
|
+
}
|
|
571
|
+
current = current[segment];
|
|
572
|
+
}
|
|
573
|
+
return false;
|
|
574
|
+
};
|
|
545
575
|
// 始终注册 popup 变量:
|
|
546
576
|
// - 若当前视图无可推断记录,仅在元信息中不呈现 record 字段;
|
|
547
577
|
// - 但仍可依据 navigation 推断并展示上级弹窗信息。
|
|
@@ -550,6 +580,9 @@ export function registerPopupVariable(ctx: FlowContext, view: FlowView) {
|
|
|
550
580
|
meta: createPopupMeta(ctx, view),
|
|
551
581
|
resolveOnServer: (p: string) => {
|
|
552
582
|
try {
|
|
583
|
+
if (p === 'sourceRecord' || p.startsWith('sourceRecord.')) {
|
|
584
|
+
return shouldResolveSourceRecordOnServer(p);
|
|
585
|
+
}
|
|
553
586
|
return !!p && POPUP_SERVER_PATH_RE.test(p);
|
|
554
587
|
} catch (_) {
|
|
555
588
|
return false;
|