@nocobase/flow-engine 2.3.0-alpha.1 → 2.3.0-beta.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/FlowDefinition.d.ts +1 -0
- package/lib/FlowDefinition.js +3 -0
- package/lib/acl/Acl.d.ts +2 -1
- package/lib/acl/Acl.js +28 -0
- package/lib/components/FlowContextSelector.js +7 -1
- package/lib/components/MobilePopup.js +14 -3
- package/lib/components/subModel/LazyDropdown.js +62 -33
- package/lib/flow-registry/BaseFlowRegistry.d.ts +2 -0
- package/lib/flow-registry/InstanceFlowRegistry.d.ts +1 -1
- package/lib/flowContext.d.ts +13 -1
- package/lib/flowContext.js +52 -6
- 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/models/CollectionFieldModel.d.ts +1 -1
- package/lib/models/CollectionFieldModel.js +6 -3
- package/lib/resources/flowResource.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 +15 -2
- package/lib/utils/index.d.ts +3 -3
- package/lib/utils/index.js +8 -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/FlowDefinition.ts +4 -0
- package/src/__tests__/createViewMeta.popup.test.ts +84 -1
- package/src/__tests__/flowContext.test.ts +8 -0
- package/src/__tests__/objectVariable.test.ts +43 -2
- package/src/__tests__/runjsFormSubmit.test.ts +138 -0
- package/src/acl/Acl.tsx +36 -1
- package/src/acl/__tests__/Acl.test.tsx +70 -0
- package/src/components/FlowContextSelector.tsx +7 -1
- package/src/components/MobilePopup.tsx +16 -4
- package/src/components/__tests__/MobilePopup.test.tsx +42 -1
- 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/__tests__/FlowContextSelector.test.tsx +35 -0
- package/src/flow-registry/BaseFlowRegistry.ts +2 -0
- package/src/flow-registry/InstanceFlowRegistry.ts +1 -1
- package/src/flowContext.ts +85 -6
- 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/models/CollectionFieldModel.tsx +7 -4
- package/src/models/__tests__/CollectionFieldModel.test.ts +5 -0
- package/src/resources/__tests__/flowResource.test.ts +3 -0
- package/src/resources/flowResource.ts +1 -0
- package/src/utils/__tests__/dateVariable.test.ts +57 -4
- 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 +25 -2
- package/src/utils/index.ts +17 -2
- package/src/utils/params-resolvers.ts +12 -0
- package/src/utils/variablesParams.ts +10 -0
- package/src/views/createViewMeta.ts +52 -18
|
@@ -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
|
});
|
|
@@ -10,10 +10,12 @@
|
|
|
10
10
|
import { FlowDefinitionOptions } from '../types';
|
|
11
11
|
import { FlowDefinition } from '../FlowDefinition';
|
|
12
12
|
import { observable } from '@formily/reactive';
|
|
13
|
+
import type { FlowModel } from '../models';
|
|
13
14
|
|
|
14
15
|
type FlowKey = string;
|
|
15
16
|
|
|
16
17
|
export interface IFlowRepository {
|
|
18
|
+
readonly model?: FlowModel;
|
|
17
19
|
addFlows(flowDefs: Record<string, Omit<FlowDefinitionOptions, 'key'>>): void;
|
|
18
20
|
addFlow(flowKey: string, flowOptions: Omit<FlowDefinitionOptions, 'key'>): FlowDefinition | void;
|
|
19
21
|
removeFlow(flowKey: string): void;
|
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,8 @@ 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>;
|
|
3054
|
+
declare variableContractModelUid?: string;
|
|
3048
3055
|
declare getVar: (path: string) => Promise<any>;
|
|
3049
3056
|
declare request: (options: RequestOptions) => Promise<any>;
|
|
3050
3057
|
declare runjs: (code: string, variables?: Record<string, any>, options?: JSRunnerOptions) => Promise<any>;
|
|
@@ -3227,7 +3234,11 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
3227
3234
|
this.defineMethod('renderJson', function (template: any) {
|
|
3228
3235
|
return this.resolveJsonTemplate(template);
|
|
3229
3236
|
});
|
|
3230
|
-
|
|
3237
|
+
const resolveJsonTemplate = async function (
|
|
3238
|
+
this: BaseFlowEngineContext,
|
|
3239
|
+
template: any,
|
|
3240
|
+
options?: ResolveJsonTemplateOptions,
|
|
3241
|
+
) {
|
|
3231
3242
|
// 提取模板使用到的变量及其子路径
|
|
3232
3243
|
const used = extractUsedVariablePaths(template);
|
|
3233
3244
|
const usedVarNames = Object.keys(used || {});
|
|
@@ -3316,6 +3327,15 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
3316
3327
|
const inputFromMeta = await collectFromMeta();
|
|
3317
3328
|
const autoInput = { ...inputFromMeta } as Record<string, any>;
|
|
3318
3329
|
|
|
3330
|
+
const viewPaths = serverVarPaths.view || [];
|
|
3331
|
+
if (
|
|
3332
|
+
!autoInput.view &&
|
|
3333
|
+
viewPaths.some((path) => path === 'record' || path.startsWith('record.') || path.startsWith('record['))
|
|
3334
|
+
) {
|
|
3335
|
+
const recordRef = inferViewRecordRef(this);
|
|
3336
|
+
if (recordRef) autoInput.view = { record: recordRef };
|
|
3337
|
+
}
|
|
3338
|
+
|
|
3319
3339
|
// Special-case: formValues
|
|
3320
3340
|
// If server needs to resolve some formValues paths but meta params only cover association anchors
|
|
3321
3341
|
// (e.g. formValues.customer) and some top-level paths are missing (e.g. formValues.status),
|
|
@@ -3387,7 +3407,13 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
3387
3407
|
|
|
3388
3408
|
if (this.api) {
|
|
3389
3409
|
try {
|
|
3410
|
+
const contractRd = buildFlowModelResolveDescriptor(
|
|
3411
|
+
this as FlowRuntimeContext<FlowModel>,
|
|
3412
|
+
options?.contractModelUid ?? this.variableContractModelUid,
|
|
3413
|
+
);
|
|
3390
3414
|
serverResolved = await enqueueVariablesResolve(this as FlowRuntimeContext<FlowModel>, {
|
|
3415
|
+
...(contractRd ? { contractRd } : {}),
|
|
3416
|
+
rd: buildFlowModelResolveDescriptor(this as FlowRuntimeContext<FlowModel>, this.model?.uid),
|
|
3391
3417
|
template,
|
|
3392
3418
|
contextParams: autoContextParams || {},
|
|
3393
3419
|
});
|
|
@@ -3399,7 +3425,8 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
3399
3425
|
}
|
|
3400
3426
|
|
|
3401
3427
|
return resolveExpressions(serverResolved, this);
|
|
3402
|
-
}
|
|
3428
|
+
};
|
|
3429
|
+
this.defineMethod('resolveJsonTemplate', resolveJsonTemplate);
|
|
3403
3430
|
|
|
3404
3431
|
// Helper: resolve a single ctx expression value via resolveJsonTemplate behavior.
|
|
3405
3432
|
// Example: await ctx.getVar('ctx.record.id')
|
|
@@ -3917,6 +3944,11 @@ export class FlowRuntimeContext<
|
|
|
3917
3944
|
) {
|
|
3918
3945
|
super();
|
|
3919
3946
|
this.addDelegate(this.model.context);
|
|
3947
|
+
const owner = model.getFlow?.(flowKey)?.model;
|
|
3948
|
+
if (owner && owner.uid !== model.uid) {
|
|
3949
|
+
// A forwarded instance flow keeps its configuration owner, without changing the runtime model.
|
|
3950
|
+
this.defineProperty('variableContractModelUid', { value: owner.uid });
|
|
3951
|
+
}
|
|
3920
3952
|
this.defineMethod('getStepParams', (stepKey: string) => {
|
|
3921
3953
|
return model.getStepParams(flowKey, stepKey) || {};
|
|
3922
3954
|
});
|
|
@@ -4582,9 +4614,56 @@ function __mergeRunJSDocMeta(base: any, patch: any): RunJSDocMeta {
|
|
|
4582
4614
|
return out as RunJSDocMeta;
|
|
4583
4615
|
}
|
|
4584
4616
|
export class FlowRunJSContext extends FlowContext {
|
|
4617
|
+
[PREPARE_CONTEXT_RESOURCE_ACTION_PARAMS](
|
|
4618
|
+
action: { actionName: string; dataSourceKey?: string; resourceName: string; resourceOf?: unknown },
|
|
4619
|
+
params: Record<string, unknown> | undefined,
|
|
4620
|
+
) {
|
|
4621
|
+
if (
|
|
4622
|
+
action.actionName.toLowerCase() !== 'create' ||
|
|
4623
|
+
!params ||
|
|
4624
|
+
Array.isArray(params) ||
|
|
4625
|
+
Object.prototype.hasOwnProperty.call(params, 'updateAssociationValues') ||
|
|
4626
|
+
!this.form ||
|
|
4627
|
+
typeof this.blockModel?.submitFromRunJs !== 'function'
|
|
4628
|
+
) {
|
|
4629
|
+
return params;
|
|
4630
|
+
}
|
|
4631
|
+
|
|
4632
|
+
const resource = this.resource;
|
|
4633
|
+
const currentResourceName = resource?.getResourceName?.();
|
|
4634
|
+
const currentDataSourceKey = resource?.getDataSourceKey?.() || 'main';
|
|
4635
|
+
if (action.resourceName !== currentResourceName || (action.dataSourceKey || 'main') !== currentDataSourceKey) {
|
|
4636
|
+
return params;
|
|
4637
|
+
}
|
|
4638
|
+
|
|
4639
|
+
const currentSourceId = resource?.getSourceId?.();
|
|
4640
|
+
if (
|
|
4641
|
+
currentResourceName?.includes('.') &&
|
|
4642
|
+
currentSourceId !== null &&
|
|
4643
|
+
typeof currentSourceId !== 'undefined' &&
|
|
4644
|
+
String(action.resourceOf ?? '') !== String(currentSourceId)
|
|
4645
|
+
) {
|
|
4646
|
+
return params;
|
|
4647
|
+
}
|
|
4648
|
+
|
|
4649
|
+
const updateAssociationValues = resource?.getUpdateAssociationValues?.();
|
|
4650
|
+
if (!Array.isArray(updateAssociationValues) || updateAssociationValues.length === 0) {
|
|
4651
|
+
return params;
|
|
4652
|
+
}
|
|
4653
|
+
|
|
4654
|
+
return {
|
|
4655
|
+
...params,
|
|
4656
|
+
updateAssociationValues: [...updateAssociationValues],
|
|
4657
|
+
};
|
|
4658
|
+
}
|
|
4659
|
+
|
|
4585
4660
|
constructor(delegate: FlowContext) {
|
|
4586
4661
|
super();
|
|
4587
4662
|
this.addDelegate(delegate);
|
|
4663
|
+
const submit = delegate.blockModel?.submitFromRunJs?.bind(delegate.blockModel);
|
|
4664
|
+
if (delegate.form && submit) {
|
|
4665
|
+
this.defineProperty('form', { value: { ...delegate.form, submit } });
|
|
4666
|
+
}
|
|
4588
4667
|
this.defineProperty('React', { value: React });
|
|
4589
4668
|
this.defineProperty('antd', { value: antd });
|
|
4590
4669
|
this.defineProperty('dayjs', {
|
|
@@ -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": "步骤配置",
|
|
@@ -51,7 +51,7 @@ export function FieldDeletePlaceholder() {
|
|
|
51
51
|
const dataSourcePrefix = dataSource ? `${t(dataSource.displayName || dataSource.key)} > ` : '';
|
|
52
52
|
const collectionPrefix = collection ? `${t(collection.title) || collection.name || collection.tableName} > ` : '';
|
|
53
53
|
return `${dataSourcePrefix}${collectionPrefix}${name}`;
|
|
54
|
-
}, []);
|
|
54
|
+
}, [collection, dataSource, name, t]);
|
|
55
55
|
return (
|
|
56
56
|
<Form.Item>
|
|
57
57
|
<div
|
|
@@ -80,7 +80,7 @@ function FieldWithoutPermissionPlaceholder() {
|
|
|
80
80
|
const dataSourcePrefix = `${t(dataSource.displayName || dataSource.key)} > `;
|
|
81
81
|
const collectionPrefix = collection ? `${t(collection.title) || collection.name || collection.tableName} > ` : '';
|
|
82
82
|
return `${dataSourcePrefix}${collectionPrefix}${name}`;
|
|
83
|
-
}, []);
|
|
83
|
+
}, [collection, dataSource.displayName, dataSource.key, name, t]);
|
|
84
84
|
const { actionName } = model.forbidden;
|
|
85
85
|
const messageValue = useMemo(() => {
|
|
86
86
|
return t(
|
|
@@ -90,7 +90,7 @@ function FieldWithoutPermissionPlaceholder() {
|
|
|
90
90
|
actionName: t(_.capitalize(actionName)),
|
|
91
91
|
},
|
|
92
92
|
).replaceAll('>', '>');
|
|
93
|
-
}, [nameValue, t]);
|
|
93
|
+
}, [actionName, nameValue, t]);
|
|
94
94
|
|
|
95
95
|
return (
|
|
96
96
|
<Tooltip title={messageValue}>
|
|
@@ -198,13 +198,16 @@ export class CollectionFieldModel<T extends DefaultStructure = DefaultStructure>
|
|
|
198
198
|
|
|
199
199
|
static getDefaultBindingByField(
|
|
200
200
|
ctx: FlowEngineContext,
|
|
201
|
-
collectionField: CollectionField,
|
|
201
|
+
collectionField: CollectionField | null | undefined,
|
|
202
202
|
options: {
|
|
203
203
|
useStrict?: boolean;
|
|
204
204
|
fallbackToTargetTitleField?: boolean;
|
|
205
205
|
targetCollectionTitleField?: CollectionField;
|
|
206
206
|
} = {},
|
|
207
207
|
): BindingOptions | null {
|
|
208
|
+
if (!collectionField) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
208
211
|
if (options.fallbackToTargetTitleField) {
|
|
209
212
|
const binding = this.getDefaultBindingByField(ctx, collectionField, { useStrict: true });
|
|
210
213
|
if (!binding) {
|
|
@@ -108,6 +108,11 @@ describe('CollectionFieldModel', () => {
|
|
|
108
108
|
expect(defaultBinding).toBeNull();
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
+
it('should return null when the collection field has been deleted', () => {
|
|
112
|
+
const defaultBinding = TestModel.getDefaultBindingByField(mockContext, undefined);
|
|
113
|
+
expect(defaultBinding).toBeNull();
|
|
114
|
+
});
|
|
115
|
+
|
|
111
116
|
it('should return null if no bindings exist for the interface', () => {
|
|
112
117
|
class Test1Model extends CollectionFieldModel {}
|
|
113
118
|
class Test2Model extends Test1Model {}
|
|
@@ -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);
|
|
@@ -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
|
|
|
@@ -34,7 +34,8 @@ describe('variablesParams helpers', () => {
|
|
|
34
34
|
|
|
35
35
|
it('inferRecordRef fallback to collection.getFilterByTK when resource has no filterByTk', () => {
|
|
36
36
|
const engine = new FlowEngine();
|
|
37
|
-
const ds = engine.context.dataSourceManager.getDataSource('main')
|
|
37
|
+
const ds = engine.context.dataSourceManager.getDataSource('main');
|
|
38
|
+
if (!ds) throw new Error('main data source is required');
|
|
38
39
|
ds.addCollection({
|
|
39
40
|
name: 'users',
|
|
40
41
|
filterTargetKey: 'id',
|
|
@@ -108,6 +109,32 @@ describe('variablesParams helpers', () => {
|
|
|
108
109
|
});
|
|
109
110
|
});
|
|
110
111
|
|
|
112
|
+
it('collectContextParamsForTemplate infers view.record when its meta has no descriptor', async () => {
|
|
113
|
+
const ctx: any = {
|
|
114
|
+
getPropertyOptions: () => undefined,
|
|
115
|
+
view: {
|
|
116
|
+
inputArgs: {
|
|
117
|
+
collectionName: 'posts',
|
|
118
|
+
dataSourceKey: 'main',
|
|
119
|
+
filterByTk: 3,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const res = await collectContextParamsForTemplate(ctx, {
|
|
125
|
+
recordId: '{{ ctx.view.record.id }}',
|
|
126
|
+
viewType: '{{ ctx.view.type }}',
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
expect(res).toEqual({
|
|
130
|
+
'view.record': {
|
|
131
|
+
collection: 'posts',
|
|
132
|
+
dataSourceKey: 'main',
|
|
133
|
+
filterByTk: 3,
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
111
138
|
it('createRecordResolveOnServerWithLocal: no local record => always use server', () => {
|
|
112
139
|
const resolver = createRecordResolveOnServerWithLocal(
|
|
113
140
|
() => ({ name: 'posts', dataSourceKey: 'main' }) as any,
|
|
@@ -51,13 +51,14 @@ function findFieldByName(collection: Collection | null | undefined, name?: strin
|
|
|
51
51
|
* @param primaryKey 主键字段名
|
|
52
52
|
* @returns 解析出的主键值,无法解析时返回 undefined
|
|
53
53
|
*/
|
|
54
|
-
function
|
|
54
|
+
export function getAssociationFilterByTk(value: unknown, primaryKey: string | string[]) {
|
|
55
55
|
if (value == null) return undefined;
|
|
56
56
|
if (Array.isArray(primaryKey)) {
|
|
57
57
|
if (typeof value !== 'object' || !value) return undefined;
|
|
58
|
-
const
|
|
58
|
+
const record = value as Record<string, unknown>;
|
|
59
|
+
const out: Record<string, unknown> = {};
|
|
59
60
|
for (const k of primaryKey) {
|
|
60
|
-
const v =
|
|
61
|
+
const v = record[k];
|
|
61
62
|
if (typeof v === 'undefined' || v === null) return undefined;
|
|
62
63
|
out[k] = v;
|
|
63
64
|
}
|
|
@@ -65,7 +66,7 @@ function toFilterByTk(value: unknown, primaryKey: string | string[]) {
|
|
|
65
66
|
}
|
|
66
67
|
if (typeof value === 'string' || typeof value === 'number') return value;
|
|
67
68
|
if (typeof value === 'object') {
|
|
68
|
-
return (value as
|
|
69
|
+
return (value as Record<string, unknown>)[primaryKey];
|
|
69
70
|
}
|
|
70
71
|
return undefined;
|
|
71
72
|
}
|
|
@@ -149,7 +150,9 @@ export function createAssociationAwareObjectMetaFactory(
|
|
|
149
150
|
if (associationValue == null) continue;
|
|
150
151
|
|
|
151
152
|
if (Array.isArray(associationValue)) {
|
|
152
|
-
const ids = associationValue
|
|
153
|
+
const ids = associationValue
|
|
154
|
+
.map((item) => getAssociationFilterByTk(item, primaryKey))
|
|
155
|
+
.filter((v) => v != null);
|
|
153
156
|
if (ids.length) {
|
|
154
157
|
params[name] = {
|
|
155
158
|
collection: target,
|
|
@@ -158,7 +161,7 @@ export function createAssociationAwareObjectMetaFactory(
|
|
|
158
161
|
};
|
|
159
162
|
}
|
|
160
163
|
} else {
|
|
161
|
-
const id =
|
|
164
|
+
const id = getAssociationFilterByTk(associationValue, primaryKey);
|
|
162
165
|
if (id != null) {
|
|
163
166
|
params[name] = {
|
|
164
167
|
collection: target,
|