@rainbow-oh/yee-c 0.8.20 → 0.9.1
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/dist/cjs/Form/Field.js +6 -6
- package/dist/cjs/Form/demo/async-validate.js +2 -2
- package/dist/cjs/Form/demo/field-status.d.ts +3 -0
- package/dist/cjs/Form/demo/field-status.js +116 -0
- package/dist/cjs/Form/demo/validate.js +2 -2
- package/dist/cjs/Form/form-store.d.ts +6 -1
- package/dist/cjs/Form/form-store.js +93 -8
- package/dist/cjs/Form/index.d.ts +1 -1
- package/dist/cjs/Form/interface.d.ts +26 -3
- package/dist/cjs/Form/style/index.less +26 -0
- package/dist/cjs/Form/utils/path.js +1 -1
- package/dist/cjs/TreeSelect/tree-select.js +14 -4
- package/dist/cjs/style/themes.css +1 -1
- package/dist/cjs/style/variables.css +1 -1
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/Form/Field.js +6 -6
- package/dist/esm/Form/demo/async-validate.js +2 -2
- package/dist/esm/Form/demo/field-status.d.ts +3 -0
- package/dist/esm/Form/demo/field-status.js +104 -0
- package/dist/esm/Form/demo/validate.js +2 -2
- package/dist/esm/Form/form-store.d.ts +6 -1
- package/dist/esm/Form/form-store.js +118 -13
- package/dist/esm/Form/hooks/useForm.js +0 -175
- package/dist/esm/Form/index.d.ts +1 -1
- package/dist/esm/Form/interface.d.ts +26 -3
- package/dist/esm/Form/style/index.less +26 -0
- package/dist/esm/Form/utils/path.js +2 -1
- package/dist/esm/TreeSelect/tree-select.js +20 -6
- package/dist/esm/style/themes.css +1 -1
- package/dist/esm/style/variables.css +1 -1
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/umd/yee-c.min.css +1 -1
- package/dist/umd/yee-c.min.js +1 -1
- package/package.json +1 -1
- package/src/Form/Field.tsx +7 -8
- package/src/Form/demo/async-validate.tsx +3 -2
- package/src/Form/demo/field-status.tsx +108 -0
- package/src/Form/demo/validate.tsx +3 -2
- package/src/Form/form-store.test.ts +156 -0
- package/src/Form/form-store.ts +117 -8
- package/src/Form/hooks/useForm.ts +0 -175
- package/src/Form/index.en-US.md +93 -88
- package/src/Form/index.ts +3 -0
- package/src/Form/index.zh-CN.md +56 -51
- package/src/Form/interface.ts +28 -3
- package/src/Form/style/index.less +26 -0
- package/src/Form/utils/path.ts +2 -1
- package/src/TreeSelect/tree-select.tsx +21 -4
- package/src/version.ts +1 -1
package/package.json
CHANGED
package/src/Form/Field.tsx
CHANGED
|
@@ -44,7 +44,7 @@ const Field: React.FC<FieldProps> = (props) => {
|
|
|
44
44
|
} = props;
|
|
45
45
|
const {
|
|
46
46
|
getCallbacks,
|
|
47
|
-
|
|
47
|
+
getFieldStatus,
|
|
48
48
|
getFieldValue,
|
|
49
49
|
getFieldsValue,
|
|
50
50
|
setFieldsValue,
|
|
@@ -88,7 +88,7 @@ const Field: React.FC<FieldProps> = (props) => {
|
|
|
88
88
|
);
|
|
89
89
|
};
|
|
90
90
|
|
|
91
|
-
const
|
|
91
|
+
const fieldStatus = getFieldStatus?.(normalizedName);
|
|
92
92
|
|
|
93
93
|
// Normalize tooltip into { icon, ...tooltipProps }, or null when not provided.
|
|
94
94
|
// Everything except `icon` is forwarded to <Tooltip>, so any Tooltip prop
|
|
@@ -152,8 +152,7 @@ const Field: React.FC<FieldProps> = (props) => {
|
|
|
152
152
|
`${prefixCls}-item`,
|
|
153
153
|
`${prefixCls}-item-${layout}`,
|
|
154
154
|
{
|
|
155
|
-
[`${prefixCls}-item-${
|
|
156
|
-
validate && validate.status,
|
|
155
|
+
[`${prefixCls}-item-${fieldStatus?.status}`]: fieldStatus?.status,
|
|
157
156
|
},
|
|
158
157
|
className,
|
|
159
158
|
)}
|
|
@@ -200,18 +199,18 @@ const Field: React.FC<FieldProps> = (props) => {
|
|
|
200
199
|
</div>
|
|
201
200
|
</div>
|
|
202
201
|
<AnimatePresence>
|
|
203
|
-
{
|
|
202
|
+
{fieldStatus?.message && (
|
|
204
203
|
<motion.small
|
|
205
204
|
initial={{ opacity: 0, y: -10 }}
|
|
206
205
|
animate={{ opacity: 1, y: 0 }}
|
|
207
206
|
exit={{ opacity: 0, y: -10 }}
|
|
208
207
|
transition={{ duration: 0.16 }}
|
|
209
208
|
className={clsx(`${prefixCls}-item-additional`, {
|
|
210
|
-
[`${prefixCls}-item-additional-${
|
|
211
|
-
|
|
209
|
+
[`${prefixCls}-item-additional-${fieldStatus.status}`]:
|
|
210
|
+
fieldStatus.status,
|
|
212
211
|
})}
|
|
213
212
|
>
|
|
214
|
-
{
|
|
213
|
+
{fieldStatus.message}
|
|
215
214
|
</motion.small>
|
|
216
215
|
)}
|
|
217
216
|
</AnimatePresence>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Button, Form, Grid, Input, Space } from '@rainbow-oh/yee-c';
|
|
2
|
+
import type { ValidateMessage } from '@rainbow-oh/yee-c';
|
|
2
3
|
import React from 'react';
|
|
3
4
|
|
|
4
5
|
// Simulate an async uniqueness check against a backend API.
|
|
@@ -16,8 +17,8 @@ export default () => {
|
|
|
16
17
|
console.log('Received values:', values);
|
|
17
18
|
};
|
|
18
19
|
|
|
19
|
-
const onFinishFailed = (
|
|
20
|
-
console.log('Failed:',
|
|
20
|
+
const onFinishFailed = (errors: ValidateMessage[]) => {
|
|
21
|
+
console.log('Failed:', errors);
|
|
21
22
|
};
|
|
22
23
|
|
|
23
24
|
return (
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Button,
|
|
3
|
+
Form,
|
|
4
|
+
Input,
|
|
5
|
+
Space,
|
|
6
|
+
type FieldStatusData,
|
|
7
|
+
} from '@rainbow-oh/yee-c';
|
|
8
|
+
import React, { useState } from 'react';
|
|
9
|
+
|
|
10
|
+
export default () => {
|
|
11
|
+
const [form] = Form.useForm();
|
|
12
|
+
const [currentStatus, setCurrentStatus] = useState<FieldStatusData | null>(
|
|
13
|
+
null,
|
|
14
|
+
);
|
|
15
|
+
const [submitResult, setSubmitResult] = useState('');
|
|
16
|
+
|
|
17
|
+
const applyStatus = (status: FieldStatusData | null) => {
|
|
18
|
+
form.setFieldStatus('username', status);
|
|
19
|
+
setCurrentStatus(form.getFieldStatus('username'));
|
|
20
|
+
setSubmitResult('');
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Form
|
|
25
|
+
form={form}
|
|
26
|
+
initialValues={{ username: 'demo-user' }}
|
|
27
|
+
onFinish={() => setSubmitResult('Submit succeeded')}
|
|
28
|
+
onFinishFailed={() =>
|
|
29
|
+
setSubmitResult('Submit blocked by the blocking error')
|
|
30
|
+
}
|
|
31
|
+
>
|
|
32
|
+
<Form.Field
|
|
33
|
+
name="username"
|
|
34
|
+
label="Username"
|
|
35
|
+
rules={[{ required: true, message: 'Please input your username!' }]}
|
|
36
|
+
>
|
|
37
|
+
<Input placeholder="Please input your username" />
|
|
38
|
+
</Form.Field>
|
|
39
|
+
|
|
40
|
+
<Space style={{ marginTop: 28, flexWrap: 'wrap' }}>
|
|
41
|
+
<Button
|
|
42
|
+
onClick={() =>
|
|
43
|
+
applyStatus({
|
|
44
|
+
status: 'error',
|
|
45
|
+
message: 'This username is unavailable',
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
>
|
|
49
|
+
Error
|
|
50
|
+
</Button>
|
|
51
|
+
<Button
|
|
52
|
+
onClick={() =>
|
|
53
|
+
applyStatus({
|
|
54
|
+
status: 'error',
|
|
55
|
+
message: 'This error prevents submission',
|
|
56
|
+
blocking: true,
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
>
|
|
60
|
+
Blocking error
|
|
61
|
+
</Button>
|
|
62
|
+
<Button
|
|
63
|
+
onClick={() =>
|
|
64
|
+
applyStatus({
|
|
65
|
+
status: 'warning',
|
|
66
|
+
message: 'This username is commonly used',
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
>
|
|
70
|
+
Warning
|
|
71
|
+
</Button>
|
|
72
|
+
<Button
|
|
73
|
+
onClick={() =>
|
|
74
|
+
applyStatus({
|
|
75
|
+
status: 'success',
|
|
76
|
+
message: 'This username is available',
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
>
|
|
80
|
+
Success
|
|
81
|
+
</Button>
|
|
82
|
+
<Button
|
|
83
|
+
onClick={() =>
|
|
84
|
+
applyStatus({
|
|
85
|
+
status: 'info',
|
|
86
|
+
message: 'The username can be changed later',
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
>
|
|
90
|
+
Info
|
|
91
|
+
</Button>
|
|
92
|
+
<Button onClick={() => applyStatus(null)}>Clear</Button>
|
|
93
|
+
<Button type="primary" htmlType="submit">
|
|
94
|
+
Submit
|
|
95
|
+
</Button>
|
|
96
|
+
</Space>
|
|
97
|
+
|
|
98
|
+
<pre style={{ marginTop: 16 }}>
|
|
99
|
+
Current status: {JSON.stringify(currentStatus, null, 2)}
|
|
100
|
+
</pre>
|
|
101
|
+
{submitResult ? <div>{submitResult}</div> : null}
|
|
102
|
+
<div style={{ marginTop: 8, color: '#999' }}>
|
|
103
|
+
Tip: set Success, then clear the input — the rule error takes display
|
|
104
|
+
precedence over the manually set status.
|
|
105
|
+
</div>
|
|
106
|
+
</Form>
|
|
107
|
+
);
|
|
108
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Button, Form, Grid, Input, Space } from '@rainbow-oh/yee-c';
|
|
2
|
+
import type { ValidateMessage } from '@rainbow-oh/yee-c';
|
|
2
3
|
import React from 'react';
|
|
3
4
|
|
|
4
5
|
export default () => {
|
|
@@ -8,8 +9,8 @@ export default () => {
|
|
|
8
9
|
console.log('Received values:', values);
|
|
9
10
|
};
|
|
10
11
|
|
|
11
|
-
const onFinishFailed = (
|
|
12
|
-
console.log('Failed:',
|
|
12
|
+
const onFinishFailed = (errors: ValidateMessage[]) => {
|
|
13
|
+
console.log('Failed:', errors);
|
|
13
14
|
};
|
|
14
15
|
|
|
15
16
|
return (
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type { ReactElement } from 'react';
|
|
3
|
+
import { FormStore } from './form-store';
|
|
4
|
+
import type { FieldEntity, Rule } from './interface';
|
|
5
|
+
|
|
6
|
+
const createEntity = (
|
|
7
|
+
name: string,
|
|
8
|
+
onStoreChange = vi.fn(),
|
|
9
|
+
rules?: Rule[],
|
|
10
|
+
): FieldEntity => ({
|
|
11
|
+
props: {
|
|
12
|
+
name,
|
|
13
|
+
rules,
|
|
14
|
+
children: {} as unknown as ReactElement,
|
|
15
|
+
},
|
|
16
|
+
onStoreChange,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('FormStore.clearFields', () => {
|
|
20
|
+
it('keeps values of unrelated fields on partial clear', () => {
|
|
21
|
+
const store = new FormStore();
|
|
22
|
+
store.setFieldsValue({ a: 1, b: 2 }, 'fill');
|
|
23
|
+
|
|
24
|
+
store.clearFields(['a']);
|
|
25
|
+
|
|
26
|
+
expect(store.getFieldsValue()).toEqual({ a: undefined, b: 2 });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('clears a nested path leaf and keeps its siblings', () => {
|
|
30
|
+
const store = new FormStore();
|
|
31
|
+
store.setFieldsValue({ 'user.name': 'tom', 'user.age': 18 }, 'fill');
|
|
32
|
+
|
|
33
|
+
store.clearFields(['user.name']);
|
|
34
|
+
|
|
35
|
+
expect(store.getFieldValue('user.name')).toBeUndefined();
|
|
36
|
+
expect(store.getFieldValue('user.age')).toBe(18);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('sets every top-level key to undefined when name is omitted', () => {
|
|
40
|
+
const store = new FormStore();
|
|
41
|
+
store.setFieldsValue({ a: 1, 'user.name': 'tom' }, 'fill');
|
|
42
|
+
|
|
43
|
+
store.clearFields();
|
|
44
|
+
|
|
45
|
+
const values = store.getFieldsValue();
|
|
46
|
+
expect(values.a).toBeUndefined();
|
|
47
|
+
expect(store.getFieldValue('user.name')).toBeUndefined();
|
|
48
|
+
expect(Object.keys(values)).toEqual(['a', 'user']);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('clears validation messages only for cleared fields', async () => {
|
|
52
|
+
const store = new FormStore();
|
|
53
|
+
store.registerFieldEntities(
|
|
54
|
+
createEntity('a', vi.fn(), [{ required: true, message: 'a required' }]),
|
|
55
|
+
);
|
|
56
|
+
store.registerFieldEntities(
|
|
57
|
+
createEntity('b', vi.fn(), [{ required: true, message: 'b required' }]),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const errors = await store.validateFields();
|
|
61
|
+
expect(errors).toHaveLength(2);
|
|
62
|
+
expect(store.getFieldValidate('a')?.message).toBe('a required');
|
|
63
|
+
|
|
64
|
+
store.clearFields(['a']);
|
|
65
|
+
|
|
66
|
+
expect(store.getFieldValidate('a')).toBeNull();
|
|
67
|
+
expect(store.getFieldValidate('b')?.message).toBe('b required');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('clears manual field statuses only for cleared fields', () => {
|
|
71
|
+
const store = new FormStore();
|
|
72
|
+
store.setFieldStatus('a', { status: 'warning', message: 'check a' });
|
|
73
|
+
store.setFieldStatus('b', { status: 'warning', message: 'check b' });
|
|
74
|
+
|
|
75
|
+
store.clearFields(['a']);
|
|
76
|
+
|
|
77
|
+
expect(store.getFieldStatus('a')).toBeNull();
|
|
78
|
+
expect(store.getFieldStatus('b')?.message).toBe('check b');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('refreshes only fields related to the cleared names', () => {
|
|
82
|
+
const store = new FormStore();
|
|
83
|
+
const onChangeA = vi.fn();
|
|
84
|
+
const onChangeB = vi.fn();
|
|
85
|
+
const onChangeNested = vi.fn();
|
|
86
|
+
store.registerFieldEntities(createEntity('a', onChangeA));
|
|
87
|
+
store.registerFieldEntities(createEntity('b', onChangeB));
|
|
88
|
+
store.registerFieldEntities(createEntity('a.x', onChangeNested));
|
|
89
|
+
store.setFieldsValue({ a: 1, b: 2 }, 'fill');
|
|
90
|
+
onChangeA.mockClear();
|
|
91
|
+
onChangeB.mockClear();
|
|
92
|
+
onChangeNested.mockClear();
|
|
93
|
+
|
|
94
|
+
store.clearFields(['a']);
|
|
95
|
+
|
|
96
|
+
expect(onChangeA).toHaveBeenCalled();
|
|
97
|
+
expect(onChangeNested).toHaveBeenCalled();
|
|
98
|
+
expect(onChangeB).not.toHaveBeenCalled();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('notifies only watchers related to the cleared names', () => {
|
|
102
|
+
const store = new FormStore();
|
|
103
|
+
const watchA = vi.fn();
|
|
104
|
+
const watchB = vi.fn();
|
|
105
|
+
const watchAll = vi.fn();
|
|
106
|
+
store.subscribe('a', watchA);
|
|
107
|
+
store.subscribe('b', watchB);
|
|
108
|
+
store.subscribe(undefined, watchAll);
|
|
109
|
+
|
|
110
|
+
store.setFieldsValue({ a: 1, b: 2 }, 'fill');
|
|
111
|
+
watchA.mockClear();
|
|
112
|
+
watchB.mockClear();
|
|
113
|
+
watchAll.mockClear();
|
|
114
|
+
|
|
115
|
+
store.clearFields(['a']);
|
|
116
|
+
|
|
117
|
+
expect(watchA).toHaveBeenCalled();
|
|
118
|
+
expect(watchAll).toHaveBeenCalled();
|
|
119
|
+
expect(watchB).not.toHaveBeenCalled();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('fires the onClear callback', () => {
|
|
123
|
+
const store = new FormStore();
|
|
124
|
+
const onClear = vi.fn();
|
|
125
|
+
store.setCallbacks({ onClear });
|
|
126
|
+
|
|
127
|
+
store.clearFields(['a']);
|
|
128
|
+
|
|
129
|
+
expect(onClear).toHaveBeenCalled();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('FormStore.initialize initialValues', () => {
|
|
134
|
+
it('resolves nested objects by field path', () => {
|
|
135
|
+
const store = new FormStore();
|
|
136
|
+
store.initialize({ initialValues: { user: { name: 'tom' }, age: 18 } });
|
|
137
|
+
|
|
138
|
+
expect(store.getFieldValue('user.name')).toBe('tom');
|
|
139
|
+
expect(store.getFieldValue('age')).toBe(18);
|
|
140
|
+
|
|
141
|
+
store.setFieldsValue({ 'user.name': 'jerry' }, 'fill');
|
|
142
|
+
store.resetFields(['user.name']);
|
|
143
|
+
expect(store.getFieldValue('user.name')).toBe('tom');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('FormStore.setFieldsValue with clear trigger', () => {
|
|
148
|
+
it('merges over the store instead of replacing it', () => {
|
|
149
|
+
const store = new FormStore();
|
|
150
|
+
store.setFieldsValue({ a: 1, b: 2 }, 'fill');
|
|
151
|
+
|
|
152
|
+
store.setFieldsValue({ a: undefined }, 'clear');
|
|
153
|
+
|
|
154
|
+
expect(store.getFieldsValue()).toEqual({ a: undefined, b: 2 });
|
|
155
|
+
});
|
|
156
|
+
});
|
package/src/Form/form-store.ts
CHANGED
|
@@ -9,6 +9,7 @@ import validate from './utils/validate';
|
|
|
9
9
|
import type {
|
|
10
10
|
Callbacks,
|
|
11
11
|
FieldEntity,
|
|
12
|
+
FieldStatusData,
|
|
12
13
|
FormInstance,
|
|
13
14
|
FormListField,
|
|
14
15
|
FormListOperation,
|
|
@@ -43,6 +44,7 @@ export class FormStore {
|
|
|
43
44
|
private fieldEntities: FieldEntity[] = [];
|
|
44
45
|
private fieldMap: Map<Name, FieldEntity> = new Map();
|
|
45
46
|
private validateMessages: Map<Name, ValidateMessage[]> = new Map();
|
|
47
|
+
private fieldStatuses: Map<Name, FieldStatusData> = new Map();
|
|
46
48
|
private initialized = false;
|
|
47
49
|
private initialValues: Store = {};
|
|
48
50
|
// List key manager
|
|
@@ -114,9 +116,81 @@ export class FormStore {
|
|
|
114
116
|
return null;
|
|
115
117
|
};
|
|
116
118
|
|
|
119
|
+
getFieldStatus = (name: NamePath): FieldStatusData | null => {
|
|
120
|
+
const normalizedName = stringifyPath(getNamePath(name));
|
|
121
|
+
const manualStatus = this.fieldStatuses.get(normalizedName);
|
|
122
|
+
const validateMessage = this.validateMessages.get(normalizedName)?.[0];
|
|
123
|
+
|
|
124
|
+
// Rule errors reflect the current value and auto-clear once validation
|
|
125
|
+
// passes, so they always take display precedence — a stale manually set
|
|
126
|
+
// status must never hide why submission failed.
|
|
127
|
+
if (validateMessage?.status === 'error') {
|
|
128
|
+
return {
|
|
129
|
+
status: 'error',
|
|
130
|
+
message: validateMessage.message,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (manualStatus) {
|
|
135
|
+
return { ...manualStatus };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (validateMessage) {
|
|
139
|
+
return {
|
|
140
|
+
status: validateMessage.status,
|
|
141
|
+
message: validateMessage.message,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return null;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
setFieldStatus = (name: NamePath, status: FieldStatusData | null) => {
|
|
149
|
+
const normalizedName = stringifyPath(getNamePath(name));
|
|
150
|
+
if (!normalizedName) return;
|
|
151
|
+
|
|
152
|
+
if (status) {
|
|
153
|
+
this.fieldStatuses.set(normalizedName, { ...status });
|
|
154
|
+
} else {
|
|
155
|
+
this.fieldStatuses.delete(normalizedName);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
this.fieldMap.get(normalizedName)?.onStoreChange();
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Delete entries whose key equals or nests under one of `names`
|
|
162
|
+
// (no names = delete all). Keeps rule messages and manual statuses in
|
|
163
|
+
// sync with the fields that reset/clear actually touched.
|
|
164
|
+
private deleteRelatedEntries = <T>(
|
|
165
|
+
entries: Map<Name, T>,
|
|
166
|
+
names?: NamePath[],
|
|
167
|
+
): void => {
|
|
168
|
+
if (!names) {
|
|
169
|
+
entries.clear();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const normalizedNames = names.map((name) =>
|
|
174
|
+
stringifyPath(getNamePath(name)),
|
|
175
|
+
);
|
|
176
|
+
Array.from(entries.keys()).forEach((fieldName) => {
|
|
177
|
+
if (normalizedNames.some((name) => isRelatedNamePath(fieldName, name))) {
|
|
178
|
+
entries.delete(fieldName);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
private clearFieldStatuses = (names?: NamePath[]) => {
|
|
184
|
+
this.deleteRelatedEntries(this.fieldStatuses, names);
|
|
185
|
+
};
|
|
186
|
+
|
|
117
187
|
setFieldsValue = (newStore: Store, trigger: TRIGGER = 'onChange') => {
|
|
118
188
|
if (trigger === 'reset' || trigger === 'clear') {
|
|
119
|
-
|
|
189
|
+
// 'reset' replaces the whole store; 'clear' merges the given keys
|
|
190
|
+
// (typically undefined) over the store so a partial clear never
|
|
191
|
+
// wipes values of unrelated fields.
|
|
192
|
+
this.store =
|
|
193
|
+
trigger === 'reset' ? newStore : { ...this.store, ...newStore };
|
|
120
194
|
if (trigger === 'reset') {
|
|
121
195
|
this.callbacks?.onReset?.();
|
|
122
196
|
} else {
|
|
@@ -196,7 +270,10 @@ export class FormStore {
|
|
|
196
270
|
});
|
|
197
271
|
}
|
|
198
272
|
this.callbacks?.onReset?.();
|
|
199
|
-
|
|
273
|
+
// A named reset only clears rule messages of the fields it touched;
|
|
274
|
+
// unrelated fields keep their (still valid) validation state.
|
|
275
|
+
this.deleteRelatedEntries(this.validateMessages, name);
|
|
276
|
+
this.clearFieldStatuses(name);
|
|
200
277
|
this.groupEntities.forEach((entity) => entity.reset());
|
|
201
278
|
this.refreshField();
|
|
202
279
|
};
|
|
@@ -204,11 +281,29 @@ export class FormStore {
|
|
|
204
281
|
clearFields = (name?: Name[]) => {
|
|
205
282
|
const names = name || Object.keys(this.store);
|
|
206
283
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
284
|
+
// Merge undefined over the current store (nested paths included) —
|
|
285
|
+
// unrelated fields keep their values.
|
|
286
|
+
let clearStore = { ...this.store };
|
|
287
|
+
names.forEach((n) => {
|
|
288
|
+
const parsedPath = parsePath(n);
|
|
289
|
+
if (parsedPath.length > 1) {
|
|
290
|
+
clearStore = setValueByPath(clearStore, parsedPath, undefined);
|
|
291
|
+
} else {
|
|
292
|
+
clearStore[n] = undefined;
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
this.deleteRelatedEntries(this.validateMessages, name);
|
|
296
|
+
this.clearFieldStatuses(name);
|
|
297
|
+
this.store = clearStore;
|
|
298
|
+
this.callbacks?.onClear?.();
|
|
299
|
+
|
|
300
|
+
// clearStore carries the whole store (nested paths must materialize
|
|
301
|
+
// it), so derive the refresh scope from `names` — only cleared
|
|
302
|
+
// fields re-render, not the entire form.
|
|
303
|
+
this.getRelatedFieldNames(names).forEach((fieldName) => {
|
|
304
|
+
this.fieldMap.get(fieldName)?.onStoreChange();
|
|
305
|
+
});
|
|
306
|
+
this.notifyWatchers(names);
|
|
212
307
|
};
|
|
213
308
|
|
|
214
309
|
validateField = async (
|
|
@@ -567,7 +662,19 @@ export class FormStore {
|
|
|
567
662
|
const { onFinish, onFinishFailed } = this.callbacks;
|
|
568
663
|
const fieldErrors = await this.validateFields();
|
|
569
664
|
const groupErrors = await this.validateGroups(true);
|
|
570
|
-
const
|
|
665
|
+
const statusErrors = Array.from(this.fieldStatuses.entries())
|
|
666
|
+
.filter(
|
|
667
|
+
([, status]) => status.status === 'error' && status.blocking === true,
|
|
668
|
+
)
|
|
669
|
+
.map(
|
|
670
|
+
([name, status]): ValidateMessage => ({
|
|
671
|
+
name,
|
|
672
|
+
message: status.message ?? '',
|
|
673
|
+
value: this.getFieldValue(name),
|
|
674
|
+
status: 'error',
|
|
675
|
+
}),
|
|
676
|
+
);
|
|
677
|
+
const allErrors = [...fieldErrors, ...groupErrors, ...statusErrors];
|
|
571
678
|
if (allErrors.length === 0) {
|
|
572
679
|
onFinish?.(this.getFieldsValue());
|
|
573
680
|
} else {
|
|
@@ -580,6 +687,8 @@ export class FormStore {
|
|
|
580
687
|
getForm(): FormInstance {
|
|
581
688
|
return {
|
|
582
689
|
getFieldValidate: this.getFieldValidate,
|
|
690
|
+
getFieldStatus: this.getFieldStatus,
|
|
691
|
+
setFieldStatus: this.setFieldStatus,
|
|
583
692
|
getFieldsValue: this.getFieldsValue,
|
|
584
693
|
getFieldValue: this.getFieldValue,
|
|
585
694
|
setFieldsValue: this.setFieldsValue,
|