@nocobase/client-v2 2.1.8 → 2.1.9
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/es/components/form/JsonTextArea.d.ts +2 -1
- package/es/components/form/VariableJsonTextArea.d.ts +19 -0
- package/es/components/form/index.d.ts +1 -0
- package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
- package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +5 -0
- package/es/flow/models/blocks/form/FormBlockModel.d.ts +4 -0
- package/es/flow/models/blocks/form/value-runtime/rules.d.ts +21 -0
- package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +21 -0
- package/es/flow/models/blocks/form/value-runtime/types.d.ts +2 -2
- package/es/index.mjs +102 -76
- package/lib/index.js +77 -51
- package/package.json +7 -7
- package/src/components/form/JsonTextArea.tsx +21 -11
- package/src/components/form/VariableJsonTextArea.tsx +175 -0
- package/src/components/form/__tests__/JsonTextArea.test.tsx +43 -0
- package/src/components/form/__tests__/VariableJsonTextArea.test.tsx +86 -0
- package/src/components/form/index.tsx +1 -0
- package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +150 -1
- package/src/flow/actions/__tests__/linkageAssignField.legacy.test.ts +135 -0
- package/src/flow/actions/linkageRules.tsx +75 -16
- package/src/flow/components/FieldAssignRulesEditor.tsx +62 -12
- package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +81 -4
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +30 -1
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +77 -0
- package/src/flow/models/blocks/form/EditFormModel.tsx +1 -0
- package/src/flow/models/blocks/form/FormBlockModel.tsx +7 -0
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +880 -102
- package/src/flow/models/blocks/form/value-runtime/rules.ts +443 -11
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +257 -13
- package/src/flow/models/blocks/form/value-runtime/types.ts +2 -2
- package/src/flow/models/fields/InputFieldModel.tsx +14 -22
- package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +17 -0
|
@@ -69,6 +69,72 @@ describe('linkage assign actions - legacy params', () => {
|
|
|
69
69
|
expect(setProps).not.toHaveBeenCalledWith(fieldModel, { value: 'default-in-update' });
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
+
it('linkageAssignField should mark override mode for field targets', async () => {
|
|
73
|
+
const fieldModel: any = { uid: 'f-override', fieldPath: 'a.b' };
|
|
74
|
+
const ctx: any = {
|
|
75
|
+
model: {
|
|
76
|
+
subModels: { grid: { subModels: { items: [fieldModel] } } },
|
|
77
|
+
},
|
|
78
|
+
engine: { getModel: vi.fn(() => ({ fieldPath: 'a.b' })) },
|
|
79
|
+
app: { jsonLogic: { apply: vi.fn() } },
|
|
80
|
+
};
|
|
81
|
+
const setProps = vi.fn();
|
|
82
|
+
|
|
83
|
+
await linkageAssignField.handler(ctx, {
|
|
84
|
+
value: [
|
|
85
|
+
{
|
|
86
|
+
key: 'r-override',
|
|
87
|
+
enable: true,
|
|
88
|
+
targetPath: 'a.b',
|
|
89
|
+
mode: 'override',
|
|
90
|
+
condition: { logic: '$and', items: [] },
|
|
91
|
+
value: 'override-value',
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
setProps,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
expect(setProps).toHaveBeenCalledTimes(1);
|
|
98
|
+
const [, props] = setProps.mock.calls[0];
|
|
99
|
+
expect(props).toMatchObject({
|
|
100
|
+
value: 'override-value',
|
|
101
|
+
__linkageAssignMode: 'override',
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('linkageAssignField should emit override patches for targets without form item models', async () => {
|
|
106
|
+
const ctx: any = {
|
|
107
|
+
model: { subModels: { grid: { subModels: { items: [] } } } },
|
|
108
|
+
engine: { getModel: vi.fn(() => null) },
|
|
109
|
+
app: { jsonLogic: { apply: vi.fn() } },
|
|
110
|
+
};
|
|
111
|
+
const setProps = vi.fn();
|
|
112
|
+
const addFormValuePatch = vi.fn();
|
|
113
|
+
|
|
114
|
+
await linkageAssignField.handler(ctx, {
|
|
115
|
+
value: [
|
|
116
|
+
{
|
|
117
|
+
key: 'r-override-direct',
|
|
118
|
+
enable: true,
|
|
119
|
+
targetPath: 'user.name',
|
|
120
|
+
mode: 'override',
|
|
121
|
+
condition: { logic: '$and', items: [] },
|
|
122
|
+
value: 'override-direct',
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
setProps,
|
|
126
|
+
addFormValuePatch,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
expect(setProps).not.toHaveBeenCalled();
|
|
130
|
+
expect(addFormValuePatch).toHaveBeenCalledWith({
|
|
131
|
+
path: 'user.name',
|
|
132
|
+
value: 'override-direct',
|
|
133
|
+
whenEmpty: false,
|
|
134
|
+
mode: 'override',
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
72
138
|
it('subFormLinkageAssignField should apply legacy object params at runtime', () => {
|
|
73
139
|
const forkModel: any = {
|
|
74
140
|
uid: 'f1',
|
|
@@ -381,6 +447,75 @@ describe('linkage assign actions - legacy params', () => {
|
|
|
381
447
|
expect(addFormValuePatch).toHaveBeenCalledTimes(1);
|
|
382
448
|
});
|
|
383
449
|
|
|
450
|
+
it('subFormLinkageAssignField should skip override patch after user edits the target path', () => {
|
|
451
|
+
const ctx: any = {
|
|
452
|
+
model: {
|
|
453
|
+
uid: 'sub-form-override-path',
|
|
454
|
+
getAclActionName: vi.fn(() => 'create'),
|
|
455
|
+
context: {},
|
|
456
|
+
parent: {
|
|
457
|
+
getStepParams: (flowKey: string, stepKey: string) => {
|
|
458
|
+
if (flowKey === 'fieldSettings' && stepKey === 'init') {
|
|
459
|
+
return { fieldPath: 'M2M.M2M' };
|
|
460
|
+
}
|
|
461
|
+
return {};
|
|
462
|
+
},
|
|
463
|
+
},
|
|
464
|
+
subModels: { grid: { subModels: { items: [] } } },
|
|
465
|
+
},
|
|
466
|
+
engine: {
|
|
467
|
+
getModel: vi.fn(() => null),
|
|
468
|
+
},
|
|
469
|
+
app: { jsonLogic: { apply: vi.fn() } },
|
|
470
|
+
};
|
|
471
|
+
const addFormValuePatch = vi.fn();
|
|
472
|
+
|
|
473
|
+
subFormLinkageAssignField.handler(ctx, {
|
|
474
|
+
value: [
|
|
475
|
+
{
|
|
476
|
+
key: 'r-override-path-1',
|
|
477
|
+
enable: true,
|
|
478
|
+
targetPath: 'Name',
|
|
479
|
+
mode: 'override',
|
|
480
|
+
condition: { logic: '$and', items: [] },
|
|
481
|
+
value: 'override-1',
|
|
482
|
+
},
|
|
483
|
+
],
|
|
484
|
+
addFormValuePatch,
|
|
485
|
+
setProps: vi.fn(),
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
expect(addFormValuePatch).toHaveBeenCalledTimes(1);
|
|
489
|
+
expect(addFormValuePatch).toHaveBeenNthCalledWith(1, {
|
|
490
|
+
path: 'M2M.M2M.Name',
|
|
491
|
+
value: 'override-1',
|
|
492
|
+
whenEmpty: false,
|
|
493
|
+
mode: 'override',
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
ctx.inputArgs = {
|
|
497
|
+
source: 'user',
|
|
498
|
+
changedPaths: [['M2M', 'M2M', 'Name']],
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
subFormLinkageAssignField.handler(ctx, {
|
|
502
|
+
value: [
|
|
503
|
+
{
|
|
504
|
+
key: 'r-override-path-2',
|
|
505
|
+
enable: true,
|
|
506
|
+
targetPath: 'Name',
|
|
507
|
+
mode: 'override',
|
|
508
|
+
condition: { logic: '$and', items: [] },
|
|
509
|
+
value: 'override-2',
|
|
510
|
+
},
|
|
511
|
+
],
|
|
512
|
+
addFormValuePatch,
|
|
513
|
+
setProps: vi.fn(),
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
expect(addFormValuePatch).toHaveBeenCalledTimes(1);
|
|
517
|
+
});
|
|
518
|
+
|
|
384
519
|
it('linkageAssignField should evaluate RunJS value before assign', async () => {
|
|
385
520
|
const fieldModel: any = { uid: 'f-runjs-assign', fieldPath: 'a.b' };
|
|
386
521
|
const runjs = vi.fn(async () => ({ success: true, value: 'from-runjs' }));
|
|
@@ -40,7 +40,7 @@ import { FilterGroup } from '../components/filter/FilterGroup';
|
|
|
40
40
|
import { LinkageFilterItem } from '../components/filter';
|
|
41
41
|
import { CodeEditor } from '../components/code-editor';
|
|
42
42
|
import { FieldAssignRulesEditor } from '../components/FieldAssignRulesEditor';
|
|
43
|
-
import type { FieldAssignRuleItem } from '../components/FieldAssignRulesEditor';
|
|
43
|
+
import type { AssignMode, FieldAssignRuleItem } from '../components/FieldAssignRulesEditor';
|
|
44
44
|
import { collectFieldAssignCascaderOptions } from '../components/fieldAssignOptions';
|
|
45
45
|
import { useAssociationTitleFieldSync } from '../components/useAssociationTitleFieldSync';
|
|
46
46
|
import _ from 'lodash';
|
|
@@ -888,6 +888,20 @@ type ArrayFieldComponentProps = {
|
|
|
888
888
|
|
|
889
889
|
const LEGACY_ASSIGN_RULE = { mode: 'assign', valueKey: 'assignValue' } as const;
|
|
890
890
|
const LEGACY_DEFAULT_RULE = { mode: 'default', valueKey: 'initialValue' } as const;
|
|
891
|
+
const LINKAGE_ASSIGN_MODE_PROP = '__linkageAssignMode';
|
|
892
|
+
|
|
893
|
+
function normalizeLinkageAssignMode(mode: unknown): AssignMode {
|
|
894
|
+
if (mode === 'default') return 'default';
|
|
895
|
+
if (mode === 'override') return 'override';
|
|
896
|
+
return 'assign';
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
type LinkageValuePatch = {
|
|
900
|
+
path: Array<string | number>;
|
|
901
|
+
value: unknown;
|
|
902
|
+
whenEmpty?: boolean;
|
|
903
|
+
mode?: AssignMode;
|
|
904
|
+
};
|
|
891
905
|
|
|
892
906
|
const FieldAssignRulesActionComponent: React.FC<
|
|
893
907
|
ArrayFieldComponentProps & {
|
|
@@ -1002,16 +1016,24 @@ export const linkageAssignField = defineAction({
|
|
|
1002
1016
|
continue;
|
|
1003
1017
|
}
|
|
1004
1018
|
|
|
1005
|
-
const mode = it?.mode
|
|
1019
|
+
const mode = normalizeLinkageAssignMode(it?.mode);
|
|
1006
1020
|
if (fieldModel) {
|
|
1007
1021
|
if (mode === 'default') {
|
|
1008
1022
|
setProps(fieldModel as FlowModel, { initialValue: finalValue });
|
|
1009
1023
|
} else {
|
|
1010
|
-
setProps(fieldModel as FlowModel, {
|
|
1024
|
+
setProps(fieldModel as FlowModel, {
|
|
1025
|
+
value: finalValue,
|
|
1026
|
+
...(mode === 'override' ? { [LINKAGE_ASSIGN_MODE_PROP]: mode } : {}),
|
|
1027
|
+
});
|
|
1011
1028
|
}
|
|
1012
1029
|
} else if (typeof addFormValuePatch === 'function') {
|
|
1013
1030
|
// 对关联字段子属性(如 user.name)等没有独立 FormItemModel 的目标,直接写入表单值
|
|
1014
|
-
addFormValuePatch({
|
|
1031
|
+
addFormValuePatch({
|
|
1032
|
+
path: targetPath,
|
|
1033
|
+
value: finalValue,
|
|
1034
|
+
whenEmpty: mode === 'default',
|
|
1035
|
+
...(mode === 'override' ? { mode } : {}),
|
|
1036
|
+
});
|
|
1015
1037
|
}
|
|
1016
1038
|
}
|
|
1017
1039
|
} catch (error) {
|
|
@@ -1177,7 +1199,7 @@ export const subFormLinkageAssignField = defineAction({
|
|
|
1177
1199
|
continue;
|
|
1178
1200
|
}
|
|
1179
1201
|
|
|
1180
|
-
const mode = it?.mode
|
|
1202
|
+
const mode = normalizeLinkageAssignMode(it?.mode);
|
|
1181
1203
|
const actionName = (ctx.model as any)?.getAclActionName?.() ?? (ctx.model as any)?.context?.actionName;
|
|
1182
1204
|
const isEditForm = actionName === 'update';
|
|
1183
1205
|
const isNewItem = (ctx as any)?.item?.__is_new__ === true;
|
|
@@ -1207,12 +1229,21 @@ export const subFormLinkageAssignField = defineAction({
|
|
|
1207
1229
|
continue;
|
|
1208
1230
|
}
|
|
1209
1231
|
|
|
1232
|
+
if (mode === 'override' && hasExplicitPathHit(targetPath)) {
|
|
1233
|
+
continue;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1210
1236
|
if (!fieldUid) {
|
|
1211
1237
|
if (mode === 'default' && hasExplicitPathHit(targetPath)) {
|
|
1212
1238
|
continue;
|
|
1213
1239
|
}
|
|
1214
1240
|
if (typeof addFormValuePatch === 'function') {
|
|
1215
|
-
addFormValuePatch({
|
|
1241
|
+
addFormValuePatch({
|
|
1242
|
+
path: targetPath,
|
|
1243
|
+
value: finalValue,
|
|
1244
|
+
whenEmpty: mode === 'default',
|
|
1245
|
+
...(mode === 'override' ? { mode } : {}),
|
|
1246
|
+
});
|
|
1216
1247
|
}
|
|
1217
1248
|
continue;
|
|
1218
1249
|
}
|
|
@@ -1223,7 +1254,10 @@ export const subFormLinkageAssignField = defineAction({
|
|
|
1223
1254
|
if (mode === 'default') {
|
|
1224
1255
|
setProps(model, { initialValue: finalValue });
|
|
1225
1256
|
} else {
|
|
1226
|
-
setProps(model, {
|
|
1257
|
+
setProps(model, {
|
|
1258
|
+
value: finalValue,
|
|
1259
|
+
...(mode === 'override' ? { [LINKAGE_ASSIGN_MODE_PROP]: mode } : {}),
|
|
1260
|
+
});
|
|
1227
1261
|
}
|
|
1228
1262
|
}
|
|
1229
1263
|
} catch (error) {
|
|
@@ -1781,7 +1815,7 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
|
|
|
1781
1815
|
const modelsToApply = new Set<FlowModel>(allModels);
|
|
1782
1816
|
const patchPropsByModel = new Map<FlowModel, any>();
|
|
1783
1817
|
const clearValueOnHiddenModelUids = new Set<string>();
|
|
1784
|
-
const directValuePatches:
|
|
1818
|
+
const directValuePatches: LinkageValuePatch[] = [];
|
|
1785
1819
|
const rootCollection = getCollectionFromModel((ctx.model as any)?.context?.blockModel ?? ctx.model);
|
|
1786
1820
|
const isSafeToWriteAssociationSubpath = (namePath: any): boolean => {
|
|
1787
1821
|
if (!Array.isArray(namePath) || !namePath.length) return true;
|
|
@@ -1836,13 +1870,18 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
|
|
|
1836
1870
|
}
|
|
1837
1871
|
|
|
1838
1872
|
for (const patch of lastPatchByPathKey.values()) {
|
|
1839
|
-
if (!patch.whenEmpty) continue;
|
|
1873
|
+
if (!patch.whenEmpty && patch.mode !== 'default') continue;
|
|
1840
1874
|
runtime.recordDefaultValuePatch(patch.path, patch.value);
|
|
1841
1875
|
}
|
|
1842
1876
|
};
|
|
1843
|
-
const
|
|
1877
|
+
const getPatchMode = (patch: { mode?: unknown; whenEmpty?: boolean }): AssignMode => {
|
|
1878
|
+
if (patch?.mode === 'default') return 'default';
|
|
1879
|
+
if (patch?.mode === 'override') return 'override';
|
|
1880
|
+
return patch?.whenEmpty ? 'default' : 'assign';
|
|
1881
|
+
};
|
|
1882
|
+
const addFormValuePatch = (patch: { path: unknown; value: unknown; whenEmpty?: boolean; mode?: AssignMode }) => {
|
|
1844
1883
|
if (!patch) return;
|
|
1845
|
-
const path =
|
|
1884
|
+
const path = patch.path;
|
|
1846
1885
|
if (!path) return;
|
|
1847
1886
|
const resolvedPath = resolveNamePathForPatch(path);
|
|
1848
1887
|
if (!resolvedPath) {
|
|
@@ -1862,8 +1901,9 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
|
|
|
1862
1901
|
});
|
|
1863
1902
|
return;
|
|
1864
1903
|
}
|
|
1865
|
-
const
|
|
1866
|
-
const
|
|
1904
|
+
const mode = getPatchMode(patch);
|
|
1905
|
+
const whenEmpty = mode === 'default';
|
|
1906
|
+
const value = patch.value;
|
|
1867
1907
|
try {
|
|
1868
1908
|
const form = ctx.model?.context?.form;
|
|
1869
1909
|
const current = form?.getFieldValue?.(resolvedPath);
|
|
@@ -1878,6 +1918,15 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
|
|
|
1878
1918
|
return;
|
|
1879
1919
|
}
|
|
1880
1920
|
}
|
|
1921
|
+
if (mode === 'override') {
|
|
1922
|
+
const runtime = getDefaultPatchRuntime();
|
|
1923
|
+
if (
|
|
1924
|
+
typeof runtime?.canApplyOverrideValuePatch === 'function' &&
|
|
1925
|
+
!runtime.canApplyOverrideValuePatch(resolvedPath)
|
|
1926
|
+
) {
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1881
1930
|
if (_.isEqual(current, value)) {
|
|
1882
1931
|
return;
|
|
1883
1932
|
}
|
|
@@ -1889,6 +1938,7 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
|
|
|
1889
1938
|
path: resolvedPath,
|
|
1890
1939
|
value,
|
|
1891
1940
|
...(whenEmpty ? { whenEmpty: true } : {}),
|
|
1941
|
+
...(mode === 'override' ? { mode } : {}),
|
|
1892
1942
|
});
|
|
1893
1943
|
};
|
|
1894
1944
|
const removePendingFormValuePatches = (path: any) => {
|
|
@@ -2116,6 +2166,14 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
|
|
|
2116
2166
|
|
|
2117
2167
|
for (const action of actions) {
|
|
2118
2168
|
const setProps = (model: FlowModel & { __originalProps?: any; __shouldReset?: boolean }, props: any) => {
|
|
2169
|
+
const normalizedProps =
|
|
2170
|
+
props && typeof props === 'object' && Object.prototype.hasOwnProperty.call(props, 'value')
|
|
2171
|
+
? {
|
|
2172
|
+
...props,
|
|
2173
|
+
[LINKAGE_ASSIGN_MODE_PROP]: normalizeLinkageAssignMode(props?.[LINKAGE_ASSIGN_MODE_PROP]),
|
|
2174
|
+
}
|
|
2175
|
+
: props;
|
|
2176
|
+
|
|
2119
2177
|
// 存储原始值,用于恢复
|
|
2120
2178
|
if (!model.__originalProps) {
|
|
2121
2179
|
model.__originalProps = {
|
|
@@ -2131,7 +2189,7 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
|
|
|
2131
2189
|
// 临时存起来,遍历完所有规则后,再统一处理
|
|
2132
2190
|
patchPropsByModel.set(model, {
|
|
2133
2191
|
...(patchPropsByModel.get(model) || {}),
|
|
2134
|
-
...
|
|
2192
|
+
...normalizedProps,
|
|
2135
2193
|
});
|
|
2136
2194
|
|
|
2137
2195
|
if (
|
|
@@ -2185,7 +2243,7 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
|
|
|
2185
2243
|
const prevHidden = !!model.hidden;
|
|
2186
2244
|
const nextHidden = !!newProps.hiddenModel;
|
|
2187
2245
|
|
|
2188
|
-
model.setProps(_.omit(newProps, ['hiddenModel', 'value', 'hiddenText']));
|
|
2246
|
+
model.setProps(_.omit(newProps, ['hiddenModel', 'value', 'hiddenText', LINKAGE_ASSIGN_MODE_PROP]));
|
|
2189
2247
|
syncFieldOptionsToForks(model, patchProps);
|
|
2190
2248
|
if (typeof model.setHidden === 'function') {
|
|
2191
2249
|
model.setHidden(nextHidden);
|
|
@@ -2224,7 +2282,8 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
|
|
|
2224
2282
|
targetUid: model?.uid,
|
|
2225
2283
|
});
|
|
2226
2284
|
} else {
|
|
2227
|
-
|
|
2285
|
+
const mode = normalizeLinkageAssignMode(patchProps?.[LINKAGE_ASSIGN_MODE_PROP]);
|
|
2286
|
+
addFormValuePatch({ path: targetPath, value: newProps.value, ...(mode === 'override' ? { mode } : {}) });
|
|
2228
2287
|
}
|
|
2229
2288
|
}
|
|
2230
2289
|
|
|
@@ -28,7 +28,7 @@ import { buildFieldAssignCascaderOptionsFromCollection, type FieldAssignCascader
|
|
|
28
28
|
import { isRunJSValue, isVariableExpression, type MetaTreeNode } from '@nocobase/flow-engine';
|
|
29
29
|
import { isToManyAssociationField } from '../internal/utils/modelUtils';
|
|
30
30
|
|
|
31
|
-
export type AssignMode = 'default' | 'assign';
|
|
31
|
+
export type AssignMode = 'default' | 'assign' | 'override';
|
|
32
32
|
|
|
33
33
|
type CollectionFieldLike = {
|
|
34
34
|
name?: unknown;
|
|
@@ -89,6 +89,8 @@ export interface FieldAssignRulesEditorProps {
|
|
|
89
89
|
defaultMode?: AssignMode;
|
|
90
90
|
/** 固定 mode:用于“仅默认值/仅赋值”的场景 */
|
|
91
91
|
fixedMode?: AssignMode;
|
|
92
|
+
/** 可选 mode 列表;fixedMode 优先级更高 */
|
|
93
|
+
allowedModes?: AssignMode[];
|
|
92
94
|
/** 是否显示 condition */
|
|
93
95
|
showCondition?: boolean;
|
|
94
96
|
/** 是否显示 enable 开关 */
|
|
@@ -109,6 +111,23 @@ export interface FieldAssignRulesEditorProps {
|
|
|
109
111
|
maxAssociationFieldDepth?: number;
|
|
110
112
|
}
|
|
111
113
|
|
|
114
|
+
const ALL_ASSIGN_MODES: AssignMode[] = ['default', 'override', 'assign'];
|
|
115
|
+
|
|
116
|
+
function isAssignMode(mode: unknown): mode is AssignMode {
|
|
117
|
+
return mode === 'default' || mode === 'override' || mode === 'assign';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function normalizeAssignModes(modes?: AssignMode[]): AssignMode[] {
|
|
121
|
+
const source = Array.isArray(modes) && modes.length ? modes : ALL_ASSIGN_MODES;
|
|
122
|
+
const out: AssignMode[] = [];
|
|
123
|
+
for (const mode of source) {
|
|
124
|
+
if (!isAssignMode(mode)) continue;
|
|
125
|
+
if (out.includes(mode)) continue;
|
|
126
|
+
out.push(mode);
|
|
127
|
+
}
|
|
128
|
+
return out.length ? out : ALL_ASSIGN_MODES;
|
|
129
|
+
}
|
|
130
|
+
|
|
112
131
|
export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (props) => {
|
|
113
132
|
const {
|
|
114
133
|
t,
|
|
@@ -118,6 +137,7 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
118
137
|
onChange,
|
|
119
138
|
defaultMode = 'assign',
|
|
120
139
|
fixedMode,
|
|
140
|
+
allowedModes: rawAllowedModes,
|
|
121
141
|
showCondition = true,
|
|
122
142
|
showEnable = true,
|
|
123
143
|
showValueEditorWhenNoField = false,
|
|
@@ -129,6 +149,28 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
129
149
|
} = props;
|
|
130
150
|
|
|
131
151
|
const value = React.useMemo(() => (Array.isArray(rawValue) ? rawValue : []), [rawValue]);
|
|
152
|
+
const allowedModes = React.useMemo(
|
|
153
|
+
() => normalizeAssignModes(fixedMode ? [fixedMode] : rawAllowedModes),
|
|
154
|
+
[fixedMode, rawAllowedModes],
|
|
155
|
+
);
|
|
156
|
+
const getDefaultMode = React.useCallback((): AssignMode => {
|
|
157
|
+
if (fixedMode) return fixedMode;
|
|
158
|
+
if (isAssignMode(defaultMode) && allowedModes.includes(defaultMode)) return defaultMode;
|
|
159
|
+
return allowedModes[0] || 'assign';
|
|
160
|
+
}, [allowedModes, defaultMode, fixedMode]);
|
|
161
|
+
const normalizeItemMode = React.useCallback(
|
|
162
|
+
(item: FieldAssignRuleItem): FieldAssignRuleItem => {
|
|
163
|
+
const mode = isAssignMode(item?.mode) && allowedModes.includes(item.mode) ? item.mode : getDefaultMode();
|
|
164
|
+
return item?.mode === mode ? item : { ...item, mode };
|
|
165
|
+
},
|
|
166
|
+
[allowedModes, getDefaultMode],
|
|
167
|
+
);
|
|
168
|
+
const emitChange = React.useCallback(
|
|
169
|
+
(next: FieldAssignRuleItem[]) => {
|
|
170
|
+
onChange?.(next.map((item) => normalizeItemMode(item)));
|
|
171
|
+
},
|
|
172
|
+
[normalizeItemMode, onChange],
|
|
173
|
+
);
|
|
132
174
|
const [cascaderOptions, setCascaderOptions] = React.useState<FieldAssignCascaderOption[]>(() =>
|
|
133
175
|
Array.isArray(fieldOptions) ? (fieldOptions as FieldAssignCascaderOption[]) : [],
|
|
134
176
|
);
|
|
@@ -312,7 +354,7 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
312
354
|
|
|
313
355
|
const patchItem = (index: number, patch: Partial<FieldAssignRuleItem>) => {
|
|
314
356
|
const next = value.map((it, i) => (i === index ? { ...it, ...patch } : it));
|
|
315
|
-
|
|
357
|
+
emitChange(next);
|
|
316
358
|
};
|
|
317
359
|
|
|
318
360
|
const removeItem = (index: number) => {
|
|
@@ -334,7 +376,7 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
334
376
|
setSyncingRuleKey(null);
|
|
335
377
|
}
|
|
336
378
|
const next = value.filter((_, i) => i !== index);
|
|
337
|
-
|
|
379
|
+
emitChange(next);
|
|
338
380
|
};
|
|
339
381
|
|
|
340
382
|
const moveItem = (index: number, direction: 'up' | 'down') => {
|
|
@@ -343,7 +385,7 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
343
385
|
if (targetIndex < 0 || targetIndex >= next.length) return;
|
|
344
386
|
const [item] = next.splice(index, 1);
|
|
345
387
|
next.splice(targetIndex, 0, item);
|
|
346
|
-
|
|
388
|
+
emitChange(next);
|
|
347
389
|
};
|
|
348
390
|
|
|
349
391
|
const addItem = () => {
|
|
@@ -352,29 +394,33 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
352
394
|
{
|
|
353
395
|
key: uid(),
|
|
354
396
|
enable: true,
|
|
355
|
-
mode:
|
|
397
|
+
mode: getDefaultMode(),
|
|
356
398
|
condition: { logic: '$and', items: [] },
|
|
357
399
|
targetPath: undefined,
|
|
358
400
|
value: undefined,
|
|
359
401
|
},
|
|
360
402
|
];
|
|
361
|
-
|
|
403
|
+
emitChange(next);
|
|
362
404
|
};
|
|
363
405
|
|
|
364
406
|
const getEffectiveMode = (item: FieldAssignRuleItem): AssignMode => {
|
|
365
407
|
if (fixedMode) return fixedMode;
|
|
366
|
-
|
|
408
|
+
if (isAssignMode(item?.mode) && allowedModes.includes(item.mode)) return item.mode;
|
|
409
|
+
return getDefaultMode();
|
|
367
410
|
};
|
|
368
411
|
|
|
369
412
|
const renderAssignModeLabel = React.useCallback(
|
|
370
413
|
(mode: AssignMode) => {
|
|
371
|
-
const modeText =
|
|
414
|
+
const modeText =
|
|
415
|
+
mode === 'default' ? t('Default value') : mode === 'override' ? t('Override value') : t('Fixed value');
|
|
372
416
|
const modeHelpText =
|
|
373
417
|
mode === 'default'
|
|
374
418
|
? t(
|
|
375
419
|
'A pre-filled value. Editable, for new entries only, and won’t affect existing data (including empty values).',
|
|
376
420
|
)
|
|
377
|
-
:
|
|
421
|
+
: mode === 'override'
|
|
422
|
+
? t('Overrides existing values until the user edits it.')
|
|
423
|
+
: t('A system-set value. Read-only.');
|
|
378
424
|
|
|
379
425
|
const preventModeToggle = (e: React.MouseEvent<HTMLElement>) => {
|
|
380
426
|
e.preventDefault();
|
|
@@ -627,7 +673,8 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
627
673
|
|
|
628
674
|
const renderPanelHeader = (item: FieldAssignRuleItem, index: number) => {
|
|
629
675
|
const mode = getEffectiveMode(item);
|
|
630
|
-
const modeText =
|
|
676
|
+
const modeText =
|
|
677
|
+
mode === 'default' ? t('Default value') : mode === 'override' ? t('Override value') : t('Fixed value');
|
|
631
678
|
const fieldLabel = getFieldLabel(item.targetPath);
|
|
632
679
|
const title = fieldLabel ? String(fieldLabel) : t('Please select field');
|
|
633
680
|
|
|
@@ -935,8 +982,11 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
935
982
|
onChange={(event) => patchItem(index, { mode: event.target.value as AssignMode })}
|
|
936
983
|
>
|
|
937
984
|
<Space size={16}>
|
|
938
|
-
|
|
939
|
-
|
|
985
|
+
{allowedModes.map((allowedMode) => (
|
|
986
|
+
<Radio key={allowedMode} value={allowedMode}>
|
|
987
|
+
{renderAssignModeLabel(allowedMode)}
|
|
988
|
+
</Radio>
|
|
989
|
+
))}
|
|
940
990
|
</Space>
|
|
941
991
|
</Radio.Group>
|
|
942
992
|
</div>
|
|
@@ -795,15 +795,23 @@ describe('FieldAssignRulesEditor', () => {
|
|
|
795
795
|
);
|
|
796
796
|
|
|
797
797
|
expect(screen.getByLabelText('Default value')).toBeInTheDocument();
|
|
798
|
+
expect(screen.getByLabelText('Override value')).toBeInTheDocument();
|
|
798
799
|
expect(screen.getByLabelText('Fixed value')).toBeInTheDocument();
|
|
799
800
|
expect(screen.getByLabelText('Fixed value')).toBeChecked();
|
|
800
801
|
await userEvent.click(screen.getByLabelText('Default value'));
|
|
801
802
|
await waitFor(() => {
|
|
802
803
|
expect(onChange).toHaveBeenCalled();
|
|
803
804
|
});
|
|
804
|
-
|
|
805
|
-
const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1]?.[0] as FieldAssignRuleItem[];
|
|
805
|
+
let lastCall = onChange.mock.calls[onChange.mock.calls.length - 1]?.[0] as FieldAssignRuleItem[];
|
|
806
806
|
expect(lastCall?.[0]?.mode).toBe('default');
|
|
807
|
+
|
|
808
|
+
onChange.mockClear();
|
|
809
|
+
await userEvent.click(screen.getByLabelText('Override value'));
|
|
810
|
+
await waitFor(() => {
|
|
811
|
+
expect(onChange).toHaveBeenCalled();
|
|
812
|
+
});
|
|
813
|
+
lastCall = onChange.mock.calls[onChange.mock.calls.length - 1]?.[0] as FieldAssignRuleItem[];
|
|
814
|
+
expect(lastCall?.[0]?.mode).toBe('override');
|
|
807
815
|
});
|
|
808
816
|
|
|
809
817
|
it('shows assignment mode tooltips for each option', async () => {
|
|
@@ -821,7 +829,7 @@ describe('FieldAssignRulesEditor', () => {
|
|
|
821
829
|
);
|
|
822
830
|
|
|
823
831
|
const questionIcons = Array.from(container.querySelectorAll('.anticon-question-circle'));
|
|
824
|
-
expect(questionIcons).toHaveLength(
|
|
832
|
+
expect(questionIcons).toHaveLength(3);
|
|
825
833
|
|
|
826
834
|
await userEvent.hover(questionIcons[0] as Element);
|
|
827
835
|
await waitFor(() => {
|
|
@@ -835,11 +843,80 @@ describe('FieldAssignRulesEditor', () => {
|
|
|
835
843
|
await userEvent.unhover(questionIcons[0] as Element);
|
|
836
844
|
|
|
837
845
|
await userEvent.hover(questionIcons[1] as Element);
|
|
846
|
+
await waitFor(() => {
|
|
847
|
+
expect(screen.getByText('Overrides existing values until the user edits it.')).toBeInTheDocument();
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
await userEvent.unhover(questionIcons[1] as Element);
|
|
851
|
+
|
|
852
|
+
await userEvent.hover(questionIcons[2] as Element);
|
|
838
853
|
await waitFor(() => {
|
|
839
854
|
expect(screen.getByText('A system-set value. Read-only.')).toBeInTheDocument();
|
|
840
855
|
});
|
|
841
856
|
});
|
|
842
857
|
|
|
858
|
+
it('honors allowed assignment modes', () => {
|
|
859
|
+
const value: FieldAssignRuleItem[] = [
|
|
860
|
+
{
|
|
861
|
+
key: '1',
|
|
862
|
+
enable: true,
|
|
863
|
+
targetPath: 'title',
|
|
864
|
+
mode: 'override',
|
|
865
|
+
},
|
|
866
|
+
];
|
|
867
|
+
|
|
868
|
+
render(
|
|
869
|
+
wrap(
|
|
870
|
+
<FieldAssignRulesEditor
|
|
871
|
+
t={t}
|
|
872
|
+
fieldOptions={[]}
|
|
873
|
+
value={value}
|
|
874
|
+
allowedModes={['default', 'assign']}
|
|
875
|
+
showCondition={false}
|
|
876
|
+
showEnable={false}
|
|
877
|
+
/>,
|
|
878
|
+
),
|
|
879
|
+
);
|
|
880
|
+
|
|
881
|
+
expect(screen.getByLabelText('Default value')).toBeInTheDocument();
|
|
882
|
+
expect(screen.getByLabelText('Fixed value')).toBeInTheDocument();
|
|
883
|
+
expect(screen.queryByLabelText('Override value')).toBeNull();
|
|
884
|
+
expect(screen.getByLabelText('Fixed value')).toBeChecked();
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
it('normalizes disallowed assignment mode before emitting changes', async () => {
|
|
888
|
+
const onChange = vi.fn();
|
|
889
|
+
const value: FieldAssignRuleItem[] = [
|
|
890
|
+
{
|
|
891
|
+
key: '1',
|
|
892
|
+
enable: true,
|
|
893
|
+
targetPath: 'title',
|
|
894
|
+
mode: 'override',
|
|
895
|
+
},
|
|
896
|
+
];
|
|
897
|
+
|
|
898
|
+
render(
|
|
899
|
+
wrap(
|
|
900
|
+
<FieldAssignRulesEditor
|
|
901
|
+
t={t}
|
|
902
|
+
fieldOptions={[]}
|
|
903
|
+
value={value}
|
|
904
|
+
onChange={onChange}
|
|
905
|
+
allowedModes={['default', 'assign']}
|
|
906
|
+
showCondition={false}
|
|
907
|
+
/>,
|
|
908
|
+
),
|
|
909
|
+
);
|
|
910
|
+
|
|
911
|
+
await userEvent.click(screen.getByRole('switch'));
|
|
912
|
+
|
|
913
|
+
await waitFor(() => {
|
|
914
|
+
expect(onChange).toHaveBeenCalled();
|
|
915
|
+
});
|
|
916
|
+
const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1]?.[0] as FieldAssignRuleItem[];
|
|
917
|
+
expect(lastCall?.[0]?.mode).toBe('assign');
|
|
918
|
+
});
|
|
919
|
+
|
|
843
920
|
it('merges extra item tree into base item while keeping order', () => {
|
|
844
921
|
const base: MetaTreeNode[] = [
|
|
845
922
|
{ name: 'formValues', title: 'Current form', type: 'object', paths: ['formValues'] },
|
|
@@ -897,7 +974,7 @@ describe('FieldAssignRulesEditor', () => {
|
|
|
897
974
|
|
|
898
975
|
const itemNode = merged.find((node) => node.name === 'item');
|
|
899
976
|
expect(itemNode).toBeTruthy();
|
|
900
|
-
const parent = (itemNode
|
|
977
|
+
const parent = ((itemNode?.children as MetaTreeNode[]) || []).find((node) => node.name === 'parentItem');
|
|
901
978
|
expect(parent).toBeTruthy();
|
|
902
979
|
|
|
903
980
|
const nestedParent = ((parent?.children as MetaTreeNode[]) || []).find((node) => node.name === 'parentItem');
|