@nocobase/flow-engine 2.2.0-beta.8 → 2.2.0
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/acl/Acl.d.ts +2 -1
- package/lib/acl/Acl.js +28 -0
- package/lib/components/FlowContextSelector.js +62 -13
- package/lib/components/FormItem.js +11 -7
- package/lib/components/MobilePopup.js +39 -10
- package/lib/components/MobilePopup.style.js +11 -1
- package/lib/components/subModel/LazyDropdown.js +62 -33
- 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.d.ts +12 -1
- package/lib/flowContext.js +57 -12
- package/lib/flowEngine.js +6 -0
- package/lib/flowI18n.js +3 -3
- package/lib/locale/en-US.json +2 -0
- package/lib/locale/index.d.ts +4 -0
- package/lib/locale/zh-CN.json +2 -0
- package/lib/resources/flowResource.js +1 -0
- package/lib/types.d.ts +3 -1
- package/lib/types.js +1 -0
- package/lib/utils/associationObjectVariable.d.ts +10 -0
- package/lib/utils/associationObjectVariable.js +10 -7
- package/lib/utils/dateVariable.d.ts +22 -0
- package/lib/utils/dateVariable.js +123 -16
- package/lib/utils/dirtyAwareApiClient.d.ts +1 -0
- package/lib/utils/dirtyAwareApiClient.js +280 -13
- package/lib/utils/index.d.ts +3 -3
- package/lib/utils/index.js +8 -0
- package/lib/utils/loadedPageCache.d.ts +1 -0
- package/lib/utils/loadedPageCache.js +6 -0
- package/lib/utils/params-resolvers.d.ts +3 -0
- package/lib/utils/params-resolvers.js +10 -0
- package/lib/utils/variablesParams.js +5 -0
- package/lib/views/createViewMeta.d.ts +1 -0
- package/lib/views/createViewMeta.js +53 -22
- package/package.json +4 -4
- package/src/__tests__/createViewMeta.popup.test.ts +84 -1
- package/src/__tests__/flowContext.test.ts +31 -0
- package/src/__tests__/flowI18n.test.ts +11 -0
- package/src/__tests__/objectVariable.test.ts +6 -1
- package/src/__tests__/runjsFormSubmit.test.ts +138 -0
- package/src/__tests__/viewScopedFlowEngine.test.ts +72 -6
- package/src/acl/Acl.tsx +36 -1
- package/src/acl/__tests__/Acl.test.tsx +70 -0
- package/src/components/FlowContextSelector.tsx +73 -12
- package/src/components/FormItem.tsx +12 -7
- package/src/components/MobilePopup.style.ts +12 -1
- package/src/components/MobilePopup.tsx +42 -10
- package/src/components/__tests__/FormItem.test.tsx +17 -2
- package/src/components/__tests__/MobilePopup.test.tsx +150 -0
- package/src/components/subModel/LazyDropdown.tsx +71 -38
- package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +85 -2
- package/src/components/subModel/__tests__/LazyDropdown.test.tsx +202 -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 +95 -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 +83 -9
- package/src/flowEngine.ts +6 -0
- package/src/flowI18n.ts +8 -3
- package/src/locale/__tests__/index.test.ts +21 -0
- package/src/locale/en-US.json +2 -0
- package/src/locale/zh-CN.json +2 -0
- package/src/resources/__tests__/flowResource.test.ts +3 -0
- package/src/resources/flowResource.ts +1 -0
- package/src/types.ts +2 -0
- package/src/utils/__tests__/dateVariable.test.ts +57 -4
- package/src/utils/__tests__/dirtyAwareApiClient.test.ts +321 -0
- package/src/utils/__tests__/variablesParams.test.ts +28 -1
- package/src/utils/associationObjectVariable.ts +9 -6
- package/src/utils/dateVariable.ts +145 -18
- package/src/utils/dirtyAwareApiClient.ts +348 -13
- package/src/utils/index.ts +17 -2
- package/src/utils/loadedPageCache.ts +7 -0
- package/src/utils/params-resolvers.ts +12 -0
- package/src/utils/variablesParams.ts +10 -0
- package/src/views/createViewMeta.ts +52 -18
|
@@ -16,12 +16,30 @@ export interface FlowContextSelectorProps
|
|
|
16
16
|
value?: string;
|
|
17
17
|
onChange?: (value: string, metaTreeNode?: MetaTreeNode) => void;
|
|
18
18
|
children?: CascaderProps<ContextSelectorItem>['children'];
|
|
19
|
+
/**
|
|
20
|
+
* Controls whether the default `x` trigger button is rendered as active
|
|
21
|
+
* (`type="primary"`). When omitted, the selector falls back to its parsed
|
|
22
|
+
* `value` path (`true` iff a valid variable path is currently selected).
|
|
23
|
+
*
|
|
24
|
+
* Use this when callers intentionally feed synthetic paths such as
|
|
25
|
+
* `['constant']` / `['null']` into the cascader to keep menu state aligned,
|
|
26
|
+
* but only want real variable references to show the blue active button.
|
|
27
|
+
*/
|
|
28
|
+
active?: boolean;
|
|
19
29
|
metaTree?: MetaTreeNode[] | (() => MetaTreeNode[] | Promise<MetaTreeNode[]>);
|
|
20
30
|
parseValueToPath?: (value: string) => string[] | undefined;
|
|
21
31
|
formatPathToValue?: (item: MetaTreeNode) => string;
|
|
22
32
|
open?: boolean;
|
|
23
33
|
onlyLeafSelectable?: boolean;
|
|
24
34
|
ignoreFieldNames?: string[];
|
|
35
|
+
/**
|
|
36
|
+
* Footer rendered at the bottom of the dropdown. Defaults to a muted
|
|
37
|
+
* "Double click to choose entire object" hint when non-leaf selection is
|
|
38
|
+
* allowed (`onlyLeafSelectable` is false) — since double-clicking a non-leaf
|
|
39
|
+
* node selects the whole object. Pass an explicit node to override, or `null`
|
|
40
|
+
* to hide it.
|
|
41
|
+
*/
|
|
42
|
+
dropdownFooter?: React.ReactNode;
|
|
25
43
|
}
|
|
26
44
|
|
|
27
45
|
export interface ContextSelectorItem {
|
|
@@ -76,7 +94,10 @@ export interface VariableInputProps {
|
|
|
76
94
|
|
|
77
95
|
export interface VariableTagProps {
|
|
78
96
|
value?: string;
|
|
97
|
+
resolvedPath?: Array<string | number>;
|
|
79
98
|
onClear?: () => void;
|
|
99
|
+
disabled?: boolean;
|
|
100
|
+
allowCustomTagInput?: boolean;
|
|
80
101
|
className?: string;
|
|
81
102
|
style?: React.CSSProperties;
|
|
82
103
|
metaTreeNode?: MetaTreeNode | null;
|
package/src/flowContext.ts
CHANGED
|
@@ -50,11 +50,11 @@ import {
|
|
|
50
50
|
resolveModuleUrl,
|
|
51
51
|
} from './utils';
|
|
52
52
|
import { FlowExitAllException } from './utils/exceptions';
|
|
53
|
-
import { enqueueVariablesResolve, JSONValue } from './utils/params-resolvers';
|
|
53
|
+
import { buildFlowModelResolveDescriptor, enqueueVariablesResolve, JSONValue } from './utils/params-resolvers';
|
|
54
54
|
import type { RecordRef } from './utils/serverContextParams';
|
|
55
55
|
import { buildServerContextParams as _buildServerContextParams } from './utils/serverContextParams';
|
|
56
|
-
import { getDirtyAwareApiClient } from './utils/dirtyAwareApiClient';
|
|
57
|
-
import { inferRecordRef } from './utils/variablesParams';
|
|
56
|
+
import { getDirtyAwareApiClient, PREPARE_CONTEXT_RESOURCE_ACTION_PARAMS } from './utils/dirtyAwareApiClient';
|
|
57
|
+
import { inferRecordRef, inferViewRecordRef } from './utils/variablesParams';
|
|
58
58
|
import { FlowView, FlowViewer } from './views/FlowView';
|
|
59
59
|
import { RunJSContextRegistry, getModelClassName, type RunJSVersion } from './runjs-context/registry';
|
|
60
60
|
import { createEphemeralContext } from './utils/createEphemeralContext';
|
|
@@ -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 || {});
|
|
@@ -3316,6 +3326,15 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
3316
3326
|
const inputFromMeta = await collectFromMeta();
|
|
3317
3327
|
const autoInput = { ...inputFromMeta } as Record<string, any>;
|
|
3318
3328
|
|
|
3329
|
+
const viewPaths = serverVarPaths.view || [];
|
|
3330
|
+
if (
|
|
3331
|
+
!autoInput.view &&
|
|
3332
|
+
viewPaths.some((path) => path === 'record' || path.startsWith('record.') || path.startsWith('record['))
|
|
3333
|
+
) {
|
|
3334
|
+
const recordRef = inferViewRecordRef(this);
|
|
3335
|
+
if (recordRef) autoInput.view = { record: recordRef };
|
|
3336
|
+
}
|
|
3337
|
+
|
|
3319
3338
|
// Special-case: formValues
|
|
3320
3339
|
// If server needs to resolve some formValues paths but meta params only cover association anchors
|
|
3321
3340
|
// (e.g. formValues.customer) and some top-level paths are missing (e.g. formValues.status),
|
|
@@ -3387,7 +3406,13 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
3387
3406
|
|
|
3388
3407
|
if (this.api) {
|
|
3389
3408
|
try {
|
|
3409
|
+
const contractRd = buildFlowModelResolveDescriptor(
|
|
3410
|
+
this as FlowRuntimeContext<FlowModel>,
|
|
3411
|
+
options?.contractModelUid,
|
|
3412
|
+
);
|
|
3390
3413
|
serverResolved = await enqueueVariablesResolve(this as FlowRuntimeContext<FlowModel>, {
|
|
3414
|
+
...(contractRd ? { contractRd } : {}),
|
|
3415
|
+
rd: buildFlowModelResolveDescriptor(this as FlowRuntimeContext<FlowModel>, this.model?.uid),
|
|
3391
3416
|
template,
|
|
3392
3417
|
contextParams: autoContextParams || {},
|
|
3393
3418
|
});
|
|
@@ -3399,7 +3424,8 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
3399
3424
|
}
|
|
3400
3425
|
|
|
3401
3426
|
return resolveExpressions(serverResolved, this);
|
|
3402
|
-
}
|
|
3427
|
+
};
|
|
3428
|
+
this.defineMethod('resolveJsonTemplate', resolveJsonTemplate);
|
|
3403
3429
|
|
|
3404
3430
|
// Helper: resolve a single ctx expression value via resolveJsonTemplate behavior.
|
|
3405
3431
|
// Example: await ctx.getVar('ctx.record.id')
|
|
@@ -3504,11 +3530,12 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
3504
3530
|
});
|
|
3505
3531
|
this.defineProperty('auth', {
|
|
3506
3532
|
get: () => ({
|
|
3507
|
-
roleName: this.api
|
|
3508
|
-
locale: this.api
|
|
3509
|
-
token: this.api
|
|
3533
|
+
roleName: this.api?.auth?.role,
|
|
3534
|
+
locale: this.api?.auth?.locale,
|
|
3535
|
+
token: this.api?.auth?.token,
|
|
3510
3536
|
user: this.user,
|
|
3511
3537
|
}),
|
|
3538
|
+
cache: false,
|
|
3512
3539
|
});
|
|
3513
3540
|
this.defineProperty('date', {
|
|
3514
3541
|
get: () => {
|
|
@@ -4581,9 +4608,56 @@ function __mergeRunJSDocMeta(base: any, patch: any): RunJSDocMeta {
|
|
|
4581
4608
|
return out as RunJSDocMeta;
|
|
4582
4609
|
}
|
|
4583
4610
|
export class FlowRunJSContext extends FlowContext {
|
|
4611
|
+
[PREPARE_CONTEXT_RESOURCE_ACTION_PARAMS](
|
|
4612
|
+
action: { actionName: string; dataSourceKey?: string; resourceName: string; resourceOf?: unknown },
|
|
4613
|
+
params: Record<string, unknown> | undefined,
|
|
4614
|
+
) {
|
|
4615
|
+
if (
|
|
4616
|
+
action.actionName.toLowerCase() !== 'create' ||
|
|
4617
|
+
!params ||
|
|
4618
|
+
Array.isArray(params) ||
|
|
4619
|
+
Object.prototype.hasOwnProperty.call(params, 'updateAssociationValues') ||
|
|
4620
|
+
!this.form ||
|
|
4621
|
+
typeof this.blockModel?.submitFromRunJs !== 'function'
|
|
4622
|
+
) {
|
|
4623
|
+
return params;
|
|
4624
|
+
}
|
|
4625
|
+
|
|
4626
|
+
const resource = this.resource;
|
|
4627
|
+
const currentResourceName = resource?.getResourceName?.();
|
|
4628
|
+
const currentDataSourceKey = resource?.getDataSourceKey?.() || 'main';
|
|
4629
|
+
if (action.resourceName !== currentResourceName || (action.dataSourceKey || 'main') !== currentDataSourceKey) {
|
|
4630
|
+
return params;
|
|
4631
|
+
}
|
|
4632
|
+
|
|
4633
|
+
const currentSourceId = resource?.getSourceId?.();
|
|
4634
|
+
if (
|
|
4635
|
+
currentResourceName?.includes('.') &&
|
|
4636
|
+
currentSourceId !== null &&
|
|
4637
|
+
typeof currentSourceId !== 'undefined' &&
|
|
4638
|
+
String(action.resourceOf ?? '') !== String(currentSourceId)
|
|
4639
|
+
) {
|
|
4640
|
+
return params;
|
|
4641
|
+
}
|
|
4642
|
+
|
|
4643
|
+
const updateAssociationValues = resource?.getUpdateAssociationValues?.();
|
|
4644
|
+
if (!Array.isArray(updateAssociationValues) || updateAssociationValues.length === 0) {
|
|
4645
|
+
return params;
|
|
4646
|
+
}
|
|
4647
|
+
|
|
4648
|
+
return {
|
|
4649
|
+
...params,
|
|
4650
|
+
updateAssociationValues: [...updateAssociationValues],
|
|
4651
|
+
};
|
|
4652
|
+
}
|
|
4653
|
+
|
|
4584
4654
|
constructor(delegate: FlowContext) {
|
|
4585
4655
|
super();
|
|
4586
4656
|
this.addDelegate(delegate);
|
|
4657
|
+
const submit = delegate.blockModel?.submitFromRunJs?.bind(delegate.blockModel);
|
|
4658
|
+
if (delegate.form && submit) {
|
|
4659
|
+
this.defineProperty('form', { value: { ...delegate.form, submit } });
|
|
4660
|
+
}
|
|
4587
4661
|
this.defineProperty('React', { value: React });
|
|
4588
4662
|
this.defineProperty('antd', { value: antd });
|
|
4589
4663
|
this.defineProperty('dayjs', {
|
package/src/flowEngine.ts
CHANGED
|
@@ -1343,6 +1343,9 @@ export class FlowEngine {
|
|
|
1343
1343
|
if (!this.ensureModelRepository()) return;
|
|
1344
1344
|
const refresh = !!options?.refresh;
|
|
1345
1345
|
const bypassLoadedPageCache = this._loadedPageCache.shouldBypass(options, () => this.context.flowSettingsEnabled);
|
|
1346
|
+
if (this.context.flowSettingsEnabled) {
|
|
1347
|
+
this._loadedPageCache.markDirtyForOptions(options);
|
|
1348
|
+
}
|
|
1346
1349
|
if (!refresh && !bypassLoadedPageCache) {
|
|
1347
1350
|
const model = this.findModelByParentId(options.parentId, options.subKey);
|
|
1348
1351
|
if (model) {
|
|
@@ -1412,6 +1415,9 @@ export class FlowEngine {
|
|
|
1412
1415
|
if (!this.ensureModelRepository()) return;
|
|
1413
1416
|
const { uid, parentId, subKey } = options;
|
|
1414
1417
|
const bypassLoadedPageCache = this._loadedPageCache.shouldBypass(options, () => this.context.flowSettingsEnabled);
|
|
1418
|
+
if (this.context.flowSettingsEnabled) {
|
|
1419
|
+
this._loadedPageCache.markDirtyForOptions(options);
|
|
1420
|
+
}
|
|
1415
1421
|
if (uid && !bypassLoadedPageCache && this._modelInstances.has(uid)) {
|
|
1416
1422
|
return this._modelInstances.get(uid) as T;
|
|
1417
1423
|
}
|
package/src/flowI18n.ts
CHANGED
|
@@ -64,7 +64,9 @@ export class FlowI18n {
|
|
|
64
64
|
* @private
|
|
65
65
|
*/
|
|
66
66
|
private isTemplate(str: string): boolean {
|
|
67
|
-
|
|
67
|
+
// The closing quote is a backreference to the opening one (group 1) so an embedded quote of a different type — e.g.
|
|
68
|
+
// {{t('… "Post-action event" …')}} — does not terminate the key early.
|
|
69
|
+
return /\{\{\s*t\s*\(\s*(["'`])(?:\\.|(?!\1).)*?\1\s*(?:,\s*.*?)?\s*\)\s*\}\}/.test(str);
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
/**
|
|
@@ -72,9 +74,12 @@ export class FlowI18n {
|
|
|
72
74
|
* @private
|
|
73
75
|
*/
|
|
74
76
|
private compileTemplate(template: string): string {
|
|
77
|
+
// `(["'`])` captures the opening quote; the key allows escaped chars (`\\.`) and any char that is not that same
|
|
78
|
+
// quote (`(?!\1).`), and `\1` closes on the matching quote. This keeps embedded quotes of a different type inside
|
|
79
|
+
// the key instead of truncating it at the first quote of any kind.
|
|
75
80
|
return template.replace(
|
|
76
|
-
/\{\{\s*t\s*\(\s*["'`](
|
|
77
|
-
(match, key, optionsStr) => {
|
|
81
|
+
/\{\{\s*t\s*\(\s*(["'`])((?:\\.|(?!\1).)*?)\1\s*(?:,\s*((?:[^{}]|\{[^}]*\})*?))?\s*\)\s*\}\}/g,
|
|
82
|
+
(match, _quote, key, optionsStr) => {
|
|
78
83
|
try {
|
|
79
84
|
let templateOptions = {};
|
|
80
85
|
if (optionsStr) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it } from 'vitest';
|
|
11
|
+
|
|
12
|
+
import { getFlowEngineTranslation } from '../index';
|
|
13
|
+
|
|
14
|
+
describe('flow engine locale', () => {
|
|
15
|
+
it('translates the default secondary confirmation text into Chinese', () => {
|
|
16
|
+
expect(getFlowEngineTranslation('Please Confirm', 'zh-CN')).toBe('请确认');
|
|
17
|
+
expect(getFlowEngineTranslation('Are you sure you want to perform the action?', 'zh-CN')).toBe(
|
|
18
|
+
'确定要执行此操作吗?',
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
});
|
package/src/locale/en-US.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"Add": "Add",
|
|
3
|
+
"Are you sure you want to perform the action?": "Are you sure you want to perform the action?",
|
|
3
4
|
"Are you sure you want to delete this item? This action cannot be undone.": "Are you sure you want to delete this item? This action cannot be undone.",
|
|
4
5
|
"Are you sure to convert this template block to copy mode?": "Are you sure you want to convert this template block to copy mode?",
|
|
5
6
|
"Array index out of bounds": "Array index {{index}} out of bounds for '{{subKey}}'",
|
|
@@ -53,6 +54,7 @@
|
|
|
53
54
|
"Other blocks": "Other blocks",
|
|
54
55
|
"Parent not found, cannot replace block": "Parent not found, cannot replace block",
|
|
55
56
|
"Previous step": "Previous step",
|
|
57
|
+
"Please Confirm": "Please Confirm",
|
|
56
58
|
"Replace current block with template?": "Replace current block with template?",
|
|
57
59
|
"Replaced with template block": "Replaced with template block",
|
|
58
60
|
"Render failed": "Render failed",
|
package/src/locale/zh-CN.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"Add": "添加",
|
|
3
|
+
"Are you sure you want to perform the action?": "确定要执行此操作吗?",
|
|
3
4
|
"Are you sure you want to delete this item? This action cannot be undone.": "确定要删除此项吗?此操作不可撤销。",
|
|
4
5
|
"Are you sure to convert this template block to copy mode?": "确定将该模板区块转换为复制模式吗?",
|
|
5
6
|
"Array index out of bounds": "数组索引 {{index}} 超出 '{{subKey}}' 的边界",
|
|
@@ -59,6 +60,7 @@
|
|
|
59
60
|
"OK": "确定",
|
|
60
61
|
"Other blocks": "其他区块",
|
|
61
62
|
"Previous step": "上一步",
|
|
63
|
+
"Please Confirm": "请确认",
|
|
62
64
|
"Render failed": "渲染失败",
|
|
63
65
|
"Response record": "响应结果记录",
|
|
64
66
|
"Step configuration": "步骤配置",
|
|
@@ -79,6 +79,9 @@ describe('FlowResource - error handling', () => {
|
|
|
79
79
|
expect(r.getError()).toBeNull();
|
|
80
80
|
|
|
81
81
|
const err = new ResourceError({ response: { data: { error: { message: 'boom', code: 'X' } } } });
|
|
82
|
+
expect(err.data).toEqual({ message: 'boom', code: 'X' });
|
|
83
|
+
expect(err.message).toBe('boom');
|
|
84
|
+
expect(err.code).toBe('X');
|
|
82
85
|
const ret = r.setError(err);
|
|
83
86
|
expect(ret).toBe(r);
|
|
84
87
|
expect(r.error).toBe(err);
|
package/src/types.ts
CHANGED
|
@@ -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
|
|