@nocobase/client-v2 2.1.9 → 2.1.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/es/flow-compat/FieldValidation.d.ts +2 -1
- package/es/index.mjs +65 -65
- package/lib/index.js +30 -30
- package/package.json +7 -7
- package/src/flow/actions/validation.tsx +62 -30
- package/src/flow/models/base/PageModel/PageModel.tsx +4 -1
- package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +28 -0
- package/src/flow-compat/FieldValidation.tsx +122 -60
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/client-v2",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.10",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "es/index.mjs",
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
"@formily/antd-v5": "1.2.3",
|
|
28
28
|
"@formily/react": "^2.2.27",
|
|
29
29
|
"@formily/shared": "^2.2.27",
|
|
30
|
-
"@nocobase/evaluators": "2.1.
|
|
31
|
-
"@nocobase/flow-engine": "2.1.
|
|
32
|
-
"@nocobase/sdk": "2.1.
|
|
33
|
-
"@nocobase/shared": "2.1.
|
|
34
|
-
"@nocobase/utils": "2.1.
|
|
30
|
+
"@nocobase/evaluators": "2.1.10",
|
|
31
|
+
"@nocobase/flow-engine": "2.1.10",
|
|
32
|
+
"@nocobase/sdk": "2.1.10",
|
|
33
|
+
"@nocobase/shared": "2.1.10",
|
|
34
|
+
"@nocobase/utils": "2.1.10",
|
|
35
35
|
"ahooks": "^3.7.2",
|
|
36
36
|
"antd": "5.24.2",
|
|
37
37
|
"antd-style": "3.7.1",
|
|
@@ -46,5 +46,5 @@
|
|
|
46
46
|
"react-i18next": "^11.15.1",
|
|
47
47
|
"react-router-dom": "^6.30.1"
|
|
48
48
|
},
|
|
49
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "65d5bc38d72d5a6fca8bcf9798a0b976e382cac9"
|
|
50
50
|
}
|
|
@@ -10,6 +10,56 @@
|
|
|
10
10
|
import { defineAction, jioToJoiSchema, tExpr } from '@nocobase/flow-engine';
|
|
11
11
|
import { FieldValidation } from '../../flow-compat';
|
|
12
12
|
|
|
13
|
+
type ValidationType = 'string' | 'number' | 'array' | 'boolean' | 'any';
|
|
14
|
+
|
|
15
|
+
interface ValidationRule {
|
|
16
|
+
name: string;
|
|
17
|
+
args?: any;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface ValidationData {
|
|
21
|
+
type: ValidationType;
|
|
22
|
+
rules?: ValidationRule[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface ValidationContext {
|
|
26
|
+
model: {
|
|
27
|
+
props: {
|
|
28
|
+
label?: string;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
t: (key: string, options?: Record<string, unknown>) => string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function buildValidationRules(ctx: ValidationContext, validation: ValidationData) {
|
|
35
|
+
const rules = [];
|
|
36
|
+
const schema = jioToJoiSchema(validation);
|
|
37
|
+
const label = ctx.model.props.label;
|
|
38
|
+
rules.push({
|
|
39
|
+
validator: (_: unknown, value: unknown) => {
|
|
40
|
+
const { error } = schema.validate(value, {
|
|
41
|
+
abortEarly: false,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (error) {
|
|
45
|
+
const messages = error.details.map((d: { type: string; context?: Record<string, unknown> }) => {
|
|
46
|
+
return ctx.t(`${d.type}`, {
|
|
47
|
+
...d.context,
|
|
48
|
+
ns: 'data-source-main',
|
|
49
|
+
label,
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
const div = document.createElement('div');
|
|
53
|
+
div.innerHTML = messages.join('; ');
|
|
54
|
+
return Promise.reject(div.textContent);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return Promise.resolve();
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
return rules;
|
|
61
|
+
}
|
|
62
|
+
|
|
13
63
|
export const validation = defineAction({
|
|
14
64
|
title: tExpr('Validation'),
|
|
15
65
|
name: 'validation',
|
|
@@ -29,6 +79,7 @@ export const validation = defineAction({
|
|
|
29
79
|
type: targetInterface.validationType,
|
|
30
80
|
availableValidationOptions: [...new Set(targetInterface.availableValidationOptions)],
|
|
31
81
|
excludeValidationOptions: [...new Set(targetInterface.excludeValidationOptions)],
|
|
82
|
+
inheritedValue: ctx.model.collectionField.validation,
|
|
32
83
|
isAssociation: targetInterface.isAssociation,
|
|
33
84
|
},
|
|
34
85
|
},
|
|
@@ -36,40 +87,21 @@ export const validation = defineAction({
|
|
|
36
87
|
},
|
|
37
88
|
handler(ctx, params) {
|
|
38
89
|
if (params.validation) {
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (error) {
|
|
49
|
-
const messages = error.details.map((d) => {
|
|
50
|
-
return ctx.t(`${d.type}`, {
|
|
51
|
-
...d.context,
|
|
52
|
-
ns: 'data-source-main',
|
|
53
|
-
label,
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
const div = document.createElement('div');
|
|
57
|
-
div.innerHTML = messages.join('; ');
|
|
58
|
-
return Promise.reject(div.textContent);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return Promise.resolve();
|
|
62
|
-
},
|
|
63
|
-
});
|
|
64
|
-
const hasRequiredInCollection = params.validation.rules.some((rule) => rule.name === 'required');
|
|
65
|
-
if (hasRequiredInCollection) {
|
|
90
|
+
const collectionValidation = ctx.model.collectionField?.validation;
|
|
91
|
+
const collectionRules = ctx.model.collectionField?.getComponentProps?.().rules || [];
|
|
92
|
+
const uiRules = params.validation.rules?.length
|
|
93
|
+
? buildValidationRules(ctx as unknown as ValidationContext, params.validation)
|
|
94
|
+
: [];
|
|
95
|
+
const hasRequiredInValidation = [collectionValidation, params.validation].some(
|
|
96
|
+
(validation) => validation?.rules?.some((rule) => rule.name === 'required'),
|
|
97
|
+
);
|
|
98
|
+
if (hasRequiredInValidation) {
|
|
66
99
|
ctx.model.setProps({
|
|
67
|
-
required:
|
|
100
|
+
required: hasRequiredInValidation,
|
|
68
101
|
});
|
|
69
102
|
}
|
|
70
|
-
console.log(rules);
|
|
71
103
|
ctx.model.setProps({
|
|
72
|
-
rules,
|
|
104
|
+
rules: [...collectionRules, ...uiRules],
|
|
73
105
|
validation: params.validation,
|
|
74
106
|
});
|
|
75
107
|
}
|
|
@@ -375,12 +375,15 @@ export class PageModel extends FlowModel<PageModelStructure> {
|
|
|
375
375
|
tabBarStyle={this.props.tabBarStyle}
|
|
376
376
|
items={this.mapTabs()}
|
|
377
377
|
onChange={(activeKey) => {
|
|
378
|
+
const previousActiveKey = this.props.tabActiveKey || this.getActiveTabKey();
|
|
378
379
|
this.context.view.navigation?.changeTo?.({
|
|
379
380
|
tabUid: activeKey,
|
|
380
381
|
});
|
|
381
382
|
|
|
382
383
|
this.invokeTabModelLifecycleMethod(activeKey, 'onActive');
|
|
383
|
-
|
|
384
|
+
if (previousActiveKey && previousActiveKey !== activeKey) {
|
|
385
|
+
this.invokeTabModelLifecycleMethod(previousActiveKey, 'onInactive');
|
|
386
|
+
}
|
|
384
387
|
this.setProps('tabActiveKey', activeKey);
|
|
385
388
|
}}
|
|
386
389
|
// destroyInactiveTabPane
|
|
@@ -452,6 +452,34 @@ describe('PageModel', () => {
|
|
|
452
452
|
marginInlineEnd: 24,
|
|
453
453
|
});
|
|
454
454
|
});
|
|
455
|
+
|
|
456
|
+
it('should deactivate the previous tab when navigation updates active tab synchronously', () => {
|
|
457
|
+
pageModel.props = { tabActiveKey: 'tab-old' } as any;
|
|
458
|
+
const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod').mockImplementation(() => undefined);
|
|
459
|
+
const changeTo = vi.fn((params: { tabUid: string }) => {
|
|
460
|
+
pageModel.props.tabActiveKey = params.tabUid;
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
pageModel.context.view = {
|
|
464
|
+
navigation: {
|
|
465
|
+
viewParams: {
|
|
466
|
+
tabUid: 'tab-old',
|
|
467
|
+
},
|
|
468
|
+
changeTo,
|
|
469
|
+
},
|
|
470
|
+
} as any;
|
|
471
|
+
|
|
472
|
+
const result = pageModel.renderTabs() as any;
|
|
473
|
+
const tabsElement = result.props.children;
|
|
474
|
+
|
|
475
|
+
tabsElement.props.onChange('tab-new');
|
|
476
|
+
|
|
477
|
+
expect(changeTo).toHaveBeenCalledWith({ tabUid: 'tab-new' });
|
|
478
|
+
expect(invokeSpy).toHaveBeenNthCalledWith(1, 'tab-new', 'onActive');
|
|
479
|
+
expect(invokeSpy).toHaveBeenNthCalledWith(2, 'tab-old', 'onInactive');
|
|
480
|
+
expect(invokeSpy).not.toHaveBeenCalledWith('tab-new', 'onInactive');
|
|
481
|
+
expect(pageModel.props.tabActiveKey).toBe('tab-new');
|
|
482
|
+
});
|
|
455
483
|
});
|
|
456
484
|
|
|
457
485
|
describe('render header spacing with tabs', () => {
|
|
@@ -18,7 +18,7 @@ import { useTranslation } from 'react-i18next';
|
|
|
18
18
|
import { FIELDS_VALIDATION_OPTIONS, REQUIRED_RULE_KEY } from './fieldValidationConstants';
|
|
19
19
|
|
|
20
20
|
interface ValidationRule {
|
|
21
|
-
key
|
|
21
|
+
key?: string;
|
|
22
22
|
name: string;
|
|
23
23
|
args?: {
|
|
24
24
|
[key: string]: any;
|
|
@@ -34,14 +34,23 @@ interface ValidationData {
|
|
|
34
34
|
interface FieldValidationProps {
|
|
35
35
|
value?: ValidationData;
|
|
36
36
|
onChange?: (value: ValidationData) => void;
|
|
37
|
+
inheritedValue?: ValidationData;
|
|
37
38
|
type?: string;
|
|
38
39
|
availableValidationOptions?: string[];
|
|
39
40
|
excludeValidationOptions?: string[];
|
|
40
41
|
isAssociation?: boolean;
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
function formatFallbackRuleLabel(name: string) {
|
|
45
|
+
if (!name) {
|
|
46
|
+
return name;
|
|
47
|
+
}
|
|
48
|
+
return `${name.slice(0, 1).toUpperCase()}${name.slice(1)}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
43
51
|
export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
44
|
-
const { value, onChange, type, availableValidationOptions, excludeValidationOptions, isAssociation } =
|
|
52
|
+
const { value, onChange, inheritedValue, type, availableValidationOptions, excludeValidationOptions, isAssociation } =
|
|
53
|
+
props;
|
|
45
54
|
const { t } = useTranslation();
|
|
46
55
|
const { token } = theme.useToken();
|
|
47
56
|
const noCascadeCls = css`
|
|
@@ -57,8 +66,9 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
|
57
66
|
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
|
|
58
67
|
const formilyField: any = useField?.() as any;
|
|
59
68
|
|
|
60
|
-
const validationType = value?.type || type;
|
|
61
|
-
const rules = value?.rules || [];
|
|
69
|
+
const validationType = value?.type || inheritedValue?.type || type;
|
|
70
|
+
const rules = useMemo(() => value?.rules || [], [value?.rules]);
|
|
71
|
+
const inheritedRules = useMemo(() => inheritedValue?.rules || [], [inheritedValue?.rules]);
|
|
62
72
|
|
|
63
73
|
const normalizeArgsForDomainLike = useCallback((ruleName: string, args: Record<string, any> | undefined) => {
|
|
64
74
|
if (!args) return args;
|
|
@@ -163,19 +173,19 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
|
163
173
|
let hasError = false;
|
|
164
174
|
const invalidRuleKeys: string[] = [];
|
|
165
175
|
try {
|
|
166
|
-
rules.forEach((rule) => {
|
|
176
|
+
rules.forEach((rule, index) => {
|
|
167
177
|
const opt = validationOptions.find((o) => o.key === rule.name);
|
|
168
178
|
if (!opt || !opt.params) return;
|
|
169
179
|
let ruleInvalid = false;
|
|
170
180
|
opt.params.forEach((param) => {
|
|
171
181
|
if (!param.required) return;
|
|
172
|
-
const currentValue =
|
|
173
|
-
if (isValueEmpty(param.componentType
|
|
182
|
+
const currentValue = rule.args?.[param.key] ?? param.defaultValue;
|
|
183
|
+
if (isValueEmpty(param.componentType, currentValue)) {
|
|
174
184
|
hasError = true;
|
|
175
185
|
ruleInvalid = true;
|
|
176
186
|
}
|
|
177
187
|
});
|
|
178
|
-
if (ruleInvalid) invalidRuleKeys.push(rule
|
|
188
|
+
if (ruleInvalid) invalidRuleKeys.push(getRuleKey(rule, index, 'ui'));
|
|
179
189
|
});
|
|
180
190
|
} catch (err) {
|
|
181
191
|
hasError = true;
|
|
@@ -198,17 +208,18 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
|
198
208
|
|
|
199
209
|
const handleAddRule = (ruleType: string) => {
|
|
200
210
|
const option = validationOptions.find((opt) => opt.key === ruleType);
|
|
211
|
+
const args: Record<string, any> = {};
|
|
201
212
|
const newRule: ValidationRule = {
|
|
202
213
|
key: `r_${Date.now()}`,
|
|
203
214
|
name: ruleType,
|
|
204
|
-
args
|
|
215
|
+
args,
|
|
205
216
|
paramsType: option?.paramsType,
|
|
206
217
|
};
|
|
207
218
|
|
|
208
219
|
if (option?.params) {
|
|
209
220
|
option.params.forEach((param) => {
|
|
210
221
|
if (param.defaultValue !== undefined) {
|
|
211
|
-
|
|
222
|
+
args[param.key] = param.defaultValue;
|
|
212
223
|
}
|
|
213
224
|
});
|
|
214
225
|
}
|
|
@@ -225,7 +236,7 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
|
225
236
|
};
|
|
226
237
|
|
|
227
238
|
const handleRemoveRule = (ruleKey: string) => {
|
|
228
|
-
const newRules = rules.filter((rule) => rule
|
|
239
|
+
const newRules = rules.filter((rule, index) => getRuleKey(rule, index, 'ui') !== ruleKey);
|
|
229
240
|
onChange?.({
|
|
230
241
|
type: validationType,
|
|
231
242
|
rules: newRules,
|
|
@@ -233,8 +244,8 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
|
233
244
|
};
|
|
234
245
|
|
|
235
246
|
const handleRuleValueChange = (ruleKey: string, argKey: string, newValue: any) => {
|
|
236
|
-
const newRules = rules.map((rule) => {
|
|
237
|
-
if (rule
|
|
247
|
+
const newRules = rules.map((rule, index) => {
|
|
248
|
+
if (getRuleKey(rule, index, 'ui') === ruleKey) {
|
|
238
249
|
let nextArgs = { ...rule.args, [argKey]: newValue } as Record<string, any>;
|
|
239
250
|
nextArgs = normalizeArgsForDomainLike(rule.name, nextArgs);
|
|
240
251
|
const option = validationOptions.find((item) => item.key === rule.name);
|
|
@@ -261,7 +272,10 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
|
261
272
|
}
|
|
262
273
|
};
|
|
263
274
|
|
|
264
|
-
const
|
|
275
|
+
const getRuleKey = (rule: ValidationRule, index: number, source: 'field' | 'ui') =>
|
|
276
|
+
rule.key || `${source}-${rule.name}-${index}`;
|
|
277
|
+
|
|
278
|
+
const renderValidationForm = (rule: ValidationRule, ruleKey: string, readOnly: boolean) => {
|
|
265
279
|
const option = validationOptions.find((opt) => opt.key === rule.name);
|
|
266
280
|
if (!option) return null;
|
|
267
281
|
|
|
@@ -282,7 +296,7 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
|
282
296
|
) {
|
|
283
297
|
currentValue = deriveTldsModeFromArgs(rule.args);
|
|
284
298
|
}
|
|
285
|
-
const isInvalid = !!param.required && isValueEmpty(param.componentType
|
|
299
|
+
const isInvalid = !readOnly && !!param.required && isValueEmpty(param.componentType, currentValue);
|
|
286
300
|
|
|
287
301
|
return (
|
|
288
302
|
<Form.Item
|
|
@@ -296,33 +310,35 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
|
296
310
|
>
|
|
297
311
|
{param.componentType === 'checkbox' ? (
|
|
298
312
|
<Checkbox
|
|
313
|
+
disabled={readOnly}
|
|
299
314
|
checked={currentValue || false}
|
|
300
|
-
onChange={(e) => handleRuleValueChange(
|
|
315
|
+
onChange={(e) => handleRuleValueChange(ruleKey, param.key, e.target.checked)}
|
|
301
316
|
>
|
|
302
317
|
{t(param.label)}
|
|
303
318
|
</Checkbox>
|
|
304
319
|
) : param.componentType === 'radio' ? (
|
|
305
320
|
<div>
|
|
306
321
|
<Radio.Group
|
|
322
|
+
disabled={readOnly}
|
|
307
323
|
value={currentValue}
|
|
308
324
|
onChange={(e) => {
|
|
309
325
|
if (param.key === 'tlds' && (option.key === 'email' || option.key === 'domain')) {
|
|
310
326
|
const val = e.target.value as 'iana' | 'disable' | 'allow' | 'deny';
|
|
311
327
|
if (val === 'iana') {
|
|
312
|
-
handleRuleValueChange(
|
|
328
|
+
handleRuleValueChange(ruleKey, 'tlds', { allow: true });
|
|
313
329
|
} else if (val === 'disable') {
|
|
314
|
-
handleRuleValueChange(
|
|
330
|
+
handleRuleValueChange(ruleKey, 'tlds', false);
|
|
315
331
|
} else if (val === 'allow') {
|
|
316
332
|
const prev = rule.args?.tlds;
|
|
317
333
|
const arr = Array.isArray(prev?.allow) ? prev.allow : [];
|
|
318
|
-
handleRuleValueChange(
|
|
334
|
+
handleRuleValueChange(ruleKey, 'tlds', { allow: arr });
|
|
319
335
|
} else if (val === 'deny') {
|
|
320
336
|
const prev = rule.args?.tlds;
|
|
321
337
|
const arr = Array.isArray(prev?.deny) ? prev.deny : [];
|
|
322
|
-
handleRuleValueChange(
|
|
338
|
+
handleRuleValueChange(ruleKey, 'tlds', { deny: arr });
|
|
323
339
|
}
|
|
324
340
|
} else {
|
|
325
|
-
handleRuleValueChange(
|
|
341
|
+
handleRuleValueChange(ruleKey, param.key, e.target.value);
|
|
326
342
|
}
|
|
327
343
|
}}
|
|
328
344
|
>
|
|
@@ -349,6 +365,7 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
|
349
365
|
: [];
|
|
350
366
|
return (
|
|
351
367
|
<Select
|
|
368
|
+
disabled={readOnly}
|
|
352
369
|
mode="tags"
|
|
353
370
|
value={arrValue}
|
|
354
371
|
onChange={(vals) => {
|
|
@@ -356,9 +373,9 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
|
356
373
|
new Set((vals as string[]).map((s) => String(s).trim()).filter(Boolean)),
|
|
357
374
|
);
|
|
358
375
|
if (item.value === 'allow') {
|
|
359
|
-
handleRuleValueChange(
|
|
376
|
+
handleRuleValueChange(ruleKey, 'tlds', { allow: cleaned });
|
|
360
377
|
} else {
|
|
361
|
-
handleRuleValueChange(
|
|
378
|
+
handleRuleValueChange(ruleKey, 'tlds', { deny: cleaned });
|
|
362
379
|
}
|
|
363
380
|
}}
|
|
364
381
|
tokenSeparators={[',', ' ', '\n', '\t']}
|
|
@@ -370,9 +387,10 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
|
370
387
|
|
|
371
388
|
return (
|
|
372
389
|
<Input
|
|
390
|
+
disabled={readOnly}
|
|
373
391
|
value=""
|
|
374
392
|
onChange={(e) =>
|
|
375
|
-
handleRuleValueChange(
|
|
393
|
+
handleRuleValueChange(ruleKey, `${param.key}_${item.value}`, e.target.value)
|
|
376
394
|
}
|
|
377
395
|
style={{ width: '100%' }}
|
|
378
396
|
/>
|
|
@@ -380,30 +398,33 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
|
380
398
|
})()
|
|
381
399
|
) : item.componentType === 'inputNumber' ? (
|
|
382
400
|
<InputNumber
|
|
401
|
+
disabled={readOnly}
|
|
383
402
|
precision={allowDecimalInputNumber ? undefined : 0}
|
|
384
403
|
step={allowDecimalInputNumber ? 0.1 : 1}
|
|
385
404
|
value={rule.args?.[`${param.key}_${item.value}`] || ''}
|
|
386
|
-
onChange={(val) => handleRuleValueChange(
|
|
405
|
+
onChange={(val) => handleRuleValueChange(ruleKey, `${param.key}_${item.value}`, val)}
|
|
387
406
|
style={{ width: '100%' }}
|
|
388
407
|
/>
|
|
389
408
|
) : item.componentType === 'checkbox' ? (
|
|
390
409
|
<Checkbox
|
|
410
|
+
disabled={readOnly}
|
|
391
411
|
checked={rule.args?.[`${param.key}_${item.value}`] || false}
|
|
392
412
|
onChange={(e) =>
|
|
393
|
-
handleRuleValueChange(
|
|
413
|
+
handleRuleValueChange(ruleKey, `${param.key}_${item.value}`, e.target.checked)
|
|
394
414
|
}
|
|
395
415
|
>
|
|
396
416
|
{t(item.label)}
|
|
397
417
|
</Checkbox>
|
|
398
418
|
) : item.componentType === 'datePicker' ? (
|
|
399
419
|
<DatePicker
|
|
420
|
+
disabled={readOnly}
|
|
400
421
|
value={
|
|
401
422
|
rule.args?.[`${param.key}_${item.value}`]
|
|
402
423
|
? dayjs(rule.args[`${param.key}_${item.value}`])
|
|
403
424
|
: null
|
|
404
425
|
}
|
|
405
426
|
onChange={(date) =>
|
|
406
|
-
handleRuleValueChange(
|
|
427
|
+
handleRuleValueChange(ruleKey, `${param.key}_${item.value}`, date?.toISOString())
|
|
407
428
|
}
|
|
408
429
|
placeholder={t('Select date')}
|
|
409
430
|
style={{ width: '100%' }}
|
|
@@ -417,8 +438,9 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
|
417
438
|
</div>
|
|
418
439
|
) : param.componentType === 'singleSelect' ? (
|
|
419
440
|
<Select
|
|
441
|
+
disabled={readOnly}
|
|
420
442
|
value={currentValue}
|
|
421
|
-
onChange={(val) => handleRuleValueChange(
|
|
443
|
+
onChange={(val) => handleRuleValueChange(ruleKey, param.key, val)}
|
|
422
444
|
placeholder={t('Please select')}
|
|
423
445
|
style={{ width: '100%', height: '100%' }}
|
|
424
446
|
allowClear
|
|
@@ -426,9 +448,10 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
|
426
448
|
/>
|
|
427
449
|
) : param.componentType === 'multipleSelect' ? (
|
|
428
450
|
<Select
|
|
451
|
+
disabled={readOnly}
|
|
429
452
|
mode="multiple"
|
|
430
453
|
value={currentValue}
|
|
431
|
-
onChange={(val) => handleRuleValueChange(
|
|
454
|
+
onChange={(val) => handleRuleValueChange(ruleKey, param.key, val)}
|
|
432
455
|
placeholder={t('Please select')}
|
|
433
456
|
style={{ width: '100%' }}
|
|
434
457
|
allowClear
|
|
@@ -436,24 +459,27 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
|
436
459
|
/>
|
|
437
460
|
) : param.componentType === 'inputNumber' ? (
|
|
438
461
|
<InputNumber
|
|
462
|
+
disabled={readOnly}
|
|
439
463
|
precision={allowDecimalInputNumber ? undefined : 0}
|
|
440
464
|
step={allowDecimalInputNumber ? 0.1 : 1}
|
|
441
465
|
value={currentValue !== undefined ? currentValue : ''}
|
|
442
|
-
onChange={(val) => handleRuleValueChange(
|
|
466
|
+
onChange={(val) => handleRuleValueChange(ruleKey, param.key, val)}
|
|
443
467
|
placeholder={t('Enter value')}
|
|
444
468
|
style={{ width: '100%' }}
|
|
445
469
|
/>
|
|
446
470
|
) : param.componentType === 'datePicker' ? (
|
|
447
471
|
<DatePicker
|
|
472
|
+
disabled={readOnly}
|
|
448
473
|
value={currentValue ? dayjs(currentValue) : null}
|
|
449
|
-
onChange={(date) => handleRuleValueChange(
|
|
474
|
+
onChange={(date) => handleRuleValueChange(ruleKey, param.key, date?.toISOString())}
|
|
450
475
|
placeholder={t('Select date')}
|
|
451
476
|
style={{ width: '100%' }}
|
|
452
477
|
/>
|
|
453
478
|
) : (
|
|
454
479
|
<Input
|
|
480
|
+
disabled={readOnly}
|
|
455
481
|
value={currentValue !== undefined ? currentValue : ''}
|
|
456
|
-
onChange={(e) => handleRuleValueChange(
|
|
482
|
+
onChange={(e) => handleRuleValueChange(ruleKey, param.key, e.target.value)}
|
|
457
483
|
placeholder={t('Enter value')}
|
|
458
484
|
/>
|
|
459
485
|
)}
|
|
@@ -465,40 +491,61 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
|
465
491
|
};
|
|
466
492
|
|
|
467
493
|
const menuItems: MenuProps['items'] = useMemo(() => {
|
|
468
|
-
const addedRuleNames = new Set(rules.map((rule) => rule.name));
|
|
494
|
+
const addedRuleNames = new Set([...inheritedRules, ...rules].map((rule) => rule.name));
|
|
469
495
|
return validationOptions
|
|
470
496
|
.filter((option) => !addedRuleNames.has(option.key))
|
|
471
497
|
.map((option) => ({
|
|
472
498
|
key: option.key,
|
|
473
499
|
label: t(option.label),
|
|
474
500
|
}));
|
|
475
|
-
}, [rules, t, validationOptions]);
|
|
501
|
+
}, [inheritedRules, rules, t, validationOptions]);
|
|
476
502
|
|
|
477
503
|
const menu: MenuProps = {
|
|
478
504
|
items: menuItems,
|
|
479
505
|
onClick: ({ key }) => handleAddRule(key as string),
|
|
480
506
|
};
|
|
481
507
|
|
|
482
|
-
|
|
483
|
-
<div
|
|
484
|
-
{
|
|
508
|
+
const renderSectionTitle = (title: string, extraStyle?: React.CSSProperties) => (
|
|
509
|
+
<div
|
|
510
|
+
style={{
|
|
511
|
+
alignItems: 'center',
|
|
512
|
+
color: token.colorTextSecondary,
|
|
513
|
+
display: 'flex',
|
|
514
|
+
fontSize: token.fontSize,
|
|
515
|
+
fontWeight: 500,
|
|
516
|
+
lineHeight: token.lineHeight,
|
|
517
|
+
marginBottom: token.marginXS,
|
|
518
|
+
...extraStyle,
|
|
519
|
+
}}
|
|
520
|
+
>
|
|
521
|
+
{title}
|
|
522
|
+
</div>
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
const renderRuleList = (dataSource: ValidationRule[], readOnly: boolean, source: 'field' | 'ui') => {
|
|
526
|
+
if (!dataSource.length) {
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return (
|
|
531
|
+
<div style={{ marginBottom: token.marginSM }}>
|
|
485
532
|
<List
|
|
486
533
|
size="small"
|
|
487
534
|
style={{
|
|
488
|
-
backgroundColor: token.colorFillAlter,
|
|
489
|
-
border: `1px solid ${token.colorBorder}`,
|
|
535
|
+
backgroundColor: readOnly ? token.colorFillQuaternary : token.colorFillAlter,
|
|
536
|
+
border: `1px solid ${readOnly ? token.colorBorderSecondary : token.colorBorder}`,
|
|
490
537
|
borderRadius: token.borderRadius,
|
|
491
|
-
marginBottom: token.marginSM,
|
|
492
538
|
}}
|
|
493
|
-
dataSource={
|
|
494
|
-
renderItem={(rule) => {
|
|
539
|
+
dataSource={dataSource}
|
|
540
|
+
renderItem={(rule, index) => {
|
|
541
|
+
const ruleKey = getRuleKey(rule, index, source);
|
|
495
542
|
const option = validationOptions.find((opt) => opt.key === rule.name);
|
|
496
|
-
const ruleLabel = option ? t(option.label) : rule.name;
|
|
543
|
+
const ruleLabel = option ? t(option.label) : formatFallbackRuleLabel(rule.name);
|
|
497
544
|
const hasParams = option?.hasValue && option.params.length > 0;
|
|
498
|
-
const isExpanded = expandedKeys.includes(
|
|
545
|
+
const isExpanded = expandedKeys.includes(ruleKey);
|
|
499
546
|
|
|
500
547
|
return (
|
|
501
|
-
<List.Item style={{ display: 'block', padding: 0 }}>
|
|
548
|
+
<List.Item key={ruleKey} style={{ display: 'block', padding: 0 }}>
|
|
502
549
|
<div
|
|
503
550
|
style={{
|
|
504
551
|
display: 'flex',
|
|
@@ -506,10 +553,10 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
|
506
553
|
alignItems: 'center',
|
|
507
554
|
padding: `${token.paddingXS}px ${token.paddingSM}px`,
|
|
508
555
|
cursor: hasParams ? 'pointer' : 'default',
|
|
509
|
-
backgroundColor: token.colorBgContainer,
|
|
556
|
+
backgroundColor: readOnly ? token.colorFillQuaternary : token.colorBgContainer,
|
|
510
557
|
borderRadius: 6,
|
|
511
558
|
}}
|
|
512
|
-
onClick={() => hasParams && handleToggleExpand(
|
|
559
|
+
onClick={() => hasParams && handleToggleExpand(ruleKey)}
|
|
513
560
|
>
|
|
514
561
|
<div style={{ display: 'flex', alignItems: 'center', gap: token.marginXS }}>
|
|
515
562
|
<div
|
|
@@ -533,34 +580,49 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
|
|
|
533
580
|
)}
|
|
534
581
|
</span>
|
|
535
582
|
</div>
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
e
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
583
|
+
{!readOnly && (
|
|
584
|
+
<Button
|
|
585
|
+
type="text"
|
|
586
|
+
size="small"
|
|
587
|
+
icon={<DeleteOutlined />}
|
|
588
|
+
onClick={(e) => {
|
|
589
|
+
e.stopPropagation();
|
|
590
|
+
handleRemoveRule(ruleKey);
|
|
591
|
+
}}
|
|
592
|
+
style={{ color: token.colorTextSecondary }}
|
|
593
|
+
/>
|
|
594
|
+
)}
|
|
546
595
|
</div>
|
|
547
596
|
{hasParams && isExpanded && (
|
|
548
597
|
<div
|
|
549
598
|
style={{
|
|
550
599
|
padding: token.paddingSM,
|
|
551
|
-
backgroundColor: token.colorBgContainer,
|
|
552
|
-
borderTop: `1px solid ${token.colorBorder}`,
|
|
600
|
+
backgroundColor: readOnly ? token.colorFillQuaternary : token.colorBgContainer,
|
|
601
|
+
borderTop: `1px solid ${readOnly ? token.colorBorderSecondary : token.colorBorder}`,
|
|
553
602
|
borderRadius: '0 0 6px 6px',
|
|
554
603
|
}}
|
|
555
604
|
>
|
|
556
|
-
{renderValidationForm(rule)}
|
|
605
|
+
{renderValidationForm(rule, ruleKey, readOnly)}
|
|
557
606
|
</div>
|
|
558
607
|
)}
|
|
559
608
|
</List.Item>
|
|
560
609
|
);
|
|
561
610
|
}}
|
|
562
611
|
/>
|
|
612
|
+
</div>
|
|
613
|
+
);
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
return (
|
|
617
|
+
<div className={noCascadeCls} style={{ marginBottom: token.marginLG }}>
|
|
618
|
+
{inheritedRules.length > 0 && (
|
|
619
|
+
<>
|
|
620
|
+
{renderSectionTitle(t('Server-side field validation rules'))}
|
|
621
|
+
{renderRuleList(inheritedRules, true, 'field')}
|
|
622
|
+
{renderSectionTitle(t('Client-side validation rules'), { marginTop: token.margin })}
|
|
623
|
+
</>
|
|
563
624
|
)}
|
|
625
|
+
{renderRuleList(rules, false, 'ui')}
|
|
564
626
|
|
|
565
627
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
566
628
|
<div>
|