rsuite 6.1.3 → 6.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/Timeline/styles/index.css +11 -0
- package/Timeline/styles/index.scss +13 -0
- package/Uploader/styles/index.css +3 -0
- package/Uploader/styles/index.scss +3 -0
- package/cjs/AutoComplete/AutoComplete.d.ts +2 -0
- package/cjs/AutoComplete/AutoComplete.js +3 -1
- package/cjs/CheckTree/utils.js +2 -1
- package/cjs/Form/Form.d.ts +37 -0
- package/cjs/Form/Form.js +16 -1
- package/cjs/Form/hooks/useFormValidate.d.ts +2 -0
- package/cjs/Form/hooks/useFormValidate.js +117 -1
- package/cjs/Form/index.d.ts +1 -0
- package/cjs/Form/resolvers.d.ts +59 -0
- package/cjs/Form/resolvers.js +4 -0
- package/cjs/Timeline/Timeline.d.ts +5 -0
- package/cjs/Timeline/Timeline.js +13 -6
- package/cjs/Tree/hooks/useFlattenTree.js +5 -8
- package/cjs/Uploader/Uploader.d.ts +2 -0
- package/cjs/Uploader/Uploader.js +48 -2
- package/cjs/internals/Picker/PickerIndicator.js +13 -11
- package/cjs/toaster/toaster.js +38 -3
- package/dist/rsuite-no-reset.css +14 -0
- package/dist/rsuite-no-reset.min.css +1 -1
- package/dist/rsuite.css +14 -0
- package/dist/rsuite.js +9 -9
- package/dist/rsuite.min.css +1 -1
- package/dist/rsuite.min.js +1 -1
- package/dist/rsuite.min.js.map +1 -1
- package/esm/AutoComplete/AutoComplete.d.ts +2 -0
- package/esm/AutoComplete/AutoComplete.js +3 -1
- package/esm/CheckTree/utils.js +2 -1
- package/esm/Form/Form.d.ts +37 -0
- package/esm/Form/Form.js +16 -1
- package/esm/Form/hooks/useFormValidate.d.ts +2 -0
- package/esm/Form/hooks/useFormValidate.js +117 -1
- package/esm/Form/index.d.ts +1 -0
- package/esm/Form/resolvers.d.ts +59 -0
- package/esm/Form/resolvers.js +2 -0
- package/esm/Timeline/Timeline.d.ts +5 -0
- package/esm/Timeline/Timeline.js +13 -6
- package/esm/Tree/hooks/useFlattenTree.js +5 -8
- package/esm/Uploader/Uploader.d.ts +2 -0
- package/esm/Uploader/Uploader.js +48 -2
- package/esm/internals/Picker/PickerIndicator.js +13 -11
- package/esm/toaster/toaster.js +38 -3
- package/package.json +1 -1
|
@@ -30,6 +30,8 @@ export interface AutoCompleteProps<T = string> extends FormControlPickerProps<T,
|
|
|
30
30
|
onOpen?: () => void;
|
|
31
31
|
/** Called on close */
|
|
32
32
|
onClose?: () => void;
|
|
33
|
+
/** Ref to the input element */
|
|
34
|
+
inputRef?: React.Ref<HTMLInputElement>;
|
|
33
35
|
}
|
|
34
36
|
/**
|
|
35
37
|
* Autocomplete function of input field.
|
|
@@ -51,6 +51,7 @@ const AutoComplete = forwardRef((props, ref) => {
|
|
|
51
51
|
onFocus,
|
|
52
52
|
onBlur,
|
|
53
53
|
onMenuFocus,
|
|
54
|
+
inputRef,
|
|
54
55
|
...rest
|
|
55
56
|
} = propsWithDefaults;
|
|
56
57
|
const datalist = transformData(data);
|
|
@@ -204,7 +205,8 @@ const AutoComplete = forwardRef((props, ref) => {
|
|
|
204
205
|
onBlur: handleInputBlur,
|
|
205
206
|
onFocus: handleInputFocus,
|
|
206
207
|
onChange: handleChange,
|
|
207
|
-
onKeyDown: handleKeyDownEvent
|
|
208
|
+
onKeyDown: handleKeyDownEvent,
|
|
209
|
+
inputRef: inputRef
|
|
208
210
|
})));
|
|
209
211
|
});
|
|
210
212
|
AutoComplete.displayName = 'AutoComplete';
|
package/esm/CheckTree/utils.js
CHANGED
|
@@ -213,7 +213,8 @@ export function getDisabledState(nodes, node, props) {
|
|
|
213
213
|
*/
|
|
214
214
|
export function getCheckTreeDefaultValue(value, uncheckableItemValues) {
|
|
215
215
|
if (Array.isArray(value) && Array.isArray(uncheckableItemValues)) {
|
|
216
|
-
|
|
216
|
+
const filtered = value.filter(v => !uncheckableItemValues.includes(v));
|
|
217
|
+
return filtered.length === value.length ? value : filtered;
|
|
217
218
|
}
|
|
218
219
|
return value;
|
|
219
220
|
}
|
package/esm/Form/Form.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { FormControlComponent } from '../FormControl';
|
|
|
3
3
|
import { FormInstance } from './hooks/useFormRef';
|
|
4
4
|
import { Schema } from 'schema-typed';
|
|
5
5
|
import type { WithAsProps, CheckTriggerType } from '../internals/types';
|
|
6
|
+
import type { Resolver } from './resolvers';
|
|
6
7
|
export interface FormProps<V = Record<string, any>, M = any, E = {
|
|
7
8
|
[P in keyof V]?: M;
|
|
8
9
|
}> extends WithAsProps, Omit<FormHTMLAttributes<HTMLFormElement>, 'onChange' | 'onSubmit' | 'onError' | 'onReset'> {
|
|
@@ -40,6 +41,42 @@ export interface FormProps<V = Record<string, any>, M = any, E = {
|
|
|
40
41
|
* @see https://github.com/rsuite/schema-typed
|
|
41
42
|
*/
|
|
42
43
|
model?: Schema;
|
|
44
|
+
/**
|
|
45
|
+
* A resolver function for integrating third-party validation libraries such as
|
|
46
|
+
* Yup, Zod, AJV, Joi, Valibot, etc.
|
|
47
|
+
*
|
|
48
|
+
* When provided, the `resolver` takes precedence over the `model` prop for
|
|
49
|
+
* form-level validation (`check` / `checkAsync`). Field-level inline `rule`
|
|
50
|
+
* props on `<Form.Control>` components are still respected.
|
|
51
|
+
*
|
|
52
|
+
* The resolver receives the current form values and must return (or resolve to)
|
|
53
|
+
* a `{ errors }` object where each key is a field name and each value is an
|
|
54
|
+
* error message or error object. An empty `errors` object means the form is valid.
|
|
55
|
+
*
|
|
56
|
+
* **Note:** If the resolver is asynchronous, form-level sync validation
|
|
57
|
+
* (`check()`) will return `false` and log a warning. Use `checkAsync()` or
|
|
58
|
+
* rely on the `onSubmit` callback (which always awaits the resolver).
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```tsx
|
|
62
|
+
* import * as yup from 'yup';
|
|
63
|
+
*
|
|
64
|
+
* const schema = yup.object({ name: yup.string().email().required() });
|
|
65
|
+
* const resolver = async (formValue) => {
|
|
66
|
+
* try {
|
|
67
|
+
* await schema.validate(formValue, { abortEarly: false });
|
|
68
|
+
* return { errors: {} };
|
|
69
|
+
* } catch (e) {
|
|
70
|
+
* const errors = {};
|
|
71
|
+
* e.inner.forEach(err => { if (err.path) errors[err.path] = err.message; });
|
|
72
|
+
* return { errors };
|
|
73
|
+
* }
|
|
74
|
+
* };
|
|
75
|
+
*
|
|
76
|
+
* <Form resolver={resolver} onSubmit={handleSubmit}>…</Form>
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
resolver?: Resolver<V>;
|
|
43
80
|
/**
|
|
44
81
|
* Make the form readonly
|
|
45
82
|
*/
|
package/esm/Form/Form.js
CHANGED
|
@@ -52,6 +52,7 @@ const Form = forwardRef((props, ref) => {
|
|
|
52
52
|
fluid,
|
|
53
53
|
layout,
|
|
54
54
|
model: formModel = defaultSchema,
|
|
55
|
+
resolver,
|
|
55
56
|
readOnly,
|
|
56
57
|
plaintext,
|
|
57
58
|
children,
|
|
@@ -82,7 +83,8 @@ const Form = forwardRef((props, ref) => {
|
|
|
82
83
|
getCombinedModel,
|
|
83
84
|
onCheck,
|
|
84
85
|
onError,
|
|
85
|
-
nestedField
|
|
86
|
+
nestedField,
|
|
87
|
+
resolver
|
|
86
88
|
};
|
|
87
89
|
const {
|
|
88
90
|
formError,
|
|
@@ -98,6 +100,19 @@ const Form = forwardRef((props, ref) => {
|
|
|
98
100
|
cleanErrorForField
|
|
99
101
|
} = useFormValidate(controlledFormError, formValidateProps);
|
|
100
102
|
const submit = useEventCallback(event => {
|
|
103
|
+
if (resolver) {
|
|
104
|
+
// When a resolver is provided, always use the async validation path so that
|
|
105
|
+
// both sync and async resolvers are handled correctly.
|
|
106
|
+
checkAsync().then(({
|
|
107
|
+
hasError
|
|
108
|
+
}) => {
|
|
109
|
+
if (!hasError) {
|
|
110
|
+
onSubmit?.(formValue, event);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
101
116
|
// Check the form before submitting
|
|
102
117
|
if (check()) {
|
|
103
118
|
onSubmit?.(formValue, event);
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import type { Resolver } from '../resolvers';
|
|
1
2
|
export interface FormErrorProps {
|
|
2
3
|
formValue: any;
|
|
3
4
|
getCombinedModel: () => any;
|
|
4
5
|
onCheck?: (formError: any) => void;
|
|
5
6
|
onError?: (formError: any) => void;
|
|
6
7
|
nestedField?: boolean;
|
|
8
|
+
resolver?: Resolver;
|
|
7
9
|
}
|
|
8
10
|
export default function useFormValidate(_formError: any, props: FormErrorProps): {
|
|
9
11
|
formError: any;
|
|
@@ -10,7 +10,8 @@ export default function useFormValidate(_formError, props) {
|
|
|
10
10
|
getCombinedModel,
|
|
11
11
|
onCheck,
|
|
12
12
|
onError,
|
|
13
|
-
nestedField
|
|
13
|
+
nestedField,
|
|
14
|
+
resolver
|
|
14
15
|
} = props;
|
|
15
16
|
const [realFormError, setFormError] = useControlled(_formError, {});
|
|
16
17
|
const checkOptions = {
|
|
@@ -19,12 +20,64 @@ export default function useFormValidate(_formError, props) {
|
|
|
19
20
|
const realFormErrorRef = useRef(realFormError);
|
|
20
21
|
realFormErrorRef.current = realFormError;
|
|
21
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Returns true when an error value is considered non-empty (i.e. the field has an error).
|
|
25
|
+
*/
|
|
26
|
+
const isValidError = error => error !== undefined && error !== null && error !== '';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Merges resolver errors into the current form error state, removing entries that
|
|
30
|
+
* are no longer invalid according to the latest resolver result.
|
|
31
|
+
*/
|
|
32
|
+
const mergeResolverErrors = (current, resolverErrors) => {
|
|
33
|
+
const next = {
|
|
34
|
+
...current
|
|
35
|
+
};
|
|
36
|
+
Object.keys({
|
|
37
|
+
...current,
|
|
38
|
+
...resolverErrors
|
|
39
|
+
}).forEach(key => {
|
|
40
|
+
if (isValidError(resolverErrors[key])) {
|
|
41
|
+
next[key] = resolverErrors[key];
|
|
42
|
+
} else {
|
|
43
|
+
delete next[key];
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return next;
|
|
47
|
+
};
|
|
48
|
+
|
|
22
49
|
/**
|
|
23
50
|
* Validate the form data and return a boolean.
|
|
24
51
|
* The error message after verification is returned in the callback.
|
|
52
|
+
*
|
|
53
|
+
* When a `resolver` is provided and the resolver returns a Promise (async resolver),
|
|
54
|
+
* this method cannot resolve the result synchronously. In that case it returns `false`
|
|
55
|
+
* immediately and you should use `checkAsync()` instead.
|
|
25
56
|
* @param callback
|
|
26
57
|
*/
|
|
27
58
|
const check = useEventCallback(callback => {
|
|
59
|
+
if (resolver) {
|
|
60
|
+
const result = resolver(formValue || {});
|
|
61
|
+
|
|
62
|
+
// Async resolver: cannot handle synchronously
|
|
63
|
+
if (result instanceof Promise) {
|
|
64
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
65
|
+
console.warn('[rsuite] The `resolver` provided to <Form> returns a Promise. ' + 'Use `checkAsync()` or rely on `onSubmit` for async validation.');
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
const {
|
|
70
|
+
errors
|
|
71
|
+
} = result;
|
|
72
|
+
const hasError = Object.keys(errors).length > 0;
|
|
73
|
+
setFormError(errors);
|
|
74
|
+
onCheck?.(errors);
|
|
75
|
+
callback?.(errors);
|
|
76
|
+
if (hasError) {
|
|
77
|
+
onError?.(errors);
|
|
78
|
+
}
|
|
79
|
+
return !hasError;
|
|
80
|
+
}
|
|
28
81
|
const formError = {};
|
|
29
82
|
let errorCount = 0;
|
|
30
83
|
const model = getCombinedModel();
|
|
@@ -59,6 +112,35 @@ export default function useFormValidate(_formError, props) {
|
|
|
59
112
|
return true;
|
|
60
113
|
});
|
|
61
114
|
const checkFieldForNextValue = useEventCallback((fieldName, nextValue, callback) => {
|
|
115
|
+
if (resolver) {
|
|
116
|
+
const result = resolver(nextValue);
|
|
117
|
+
if (result instanceof Promise) {
|
|
118
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
119
|
+
console.warn('[rsuite] The `resolver` provided to <Form> returns a Promise. ' + 'Use `checkAsync()` or `checkForFieldAsync()` for async validation.');
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
const {
|
|
124
|
+
errors
|
|
125
|
+
} = result;
|
|
126
|
+
const fieldError = errors[fieldName];
|
|
127
|
+
const hasFieldError = isValidError(fieldError);
|
|
128
|
+
// Merge resolver errors with existing errors, clearing fields that now pass
|
|
129
|
+
const nextFormError = mergeResolverErrors(realFormError, errors);
|
|
130
|
+
setFormError(nextFormError);
|
|
131
|
+
onCheck?.(nextFormError);
|
|
132
|
+
const callbackResult = {
|
|
133
|
+
hasError: hasFieldError,
|
|
134
|
+
errorMessage: fieldError
|
|
135
|
+
};
|
|
136
|
+
callback?.(hasFieldError ? callbackResult : {
|
|
137
|
+
hasError: false
|
|
138
|
+
});
|
|
139
|
+
if (Object.keys(nextFormError).length > 0) {
|
|
140
|
+
onError?.(nextFormError);
|
|
141
|
+
}
|
|
142
|
+
return !hasFieldError;
|
|
143
|
+
}
|
|
62
144
|
const model = getCombinedModel();
|
|
63
145
|
const resultOfCurrentField = model.checkForField(fieldName, nextValue, checkOptions);
|
|
64
146
|
let nextFormError = {
|
|
@@ -117,6 +199,22 @@ export default function useFormValidate(_formError, props) {
|
|
|
117
199
|
* Check form data asynchronously and return a Promise
|
|
118
200
|
*/
|
|
119
201
|
const checkAsync = useEventCallback(() => {
|
|
202
|
+
if (resolver) {
|
|
203
|
+
return Promise.resolve(resolver(formValue || {})).then(({
|
|
204
|
+
errors
|
|
205
|
+
}) => {
|
|
206
|
+
const hasError = Object.keys(errors).length > 0;
|
|
207
|
+
onCheck?.(errors);
|
|
208
|
+
setFormError(errors);
|
|
209
|
+
if (hasError) {
|
|
210
|
+
onError?.(errors);
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
hasError,
|
|
214
|
+
formError: errors
|
|
215
|
+
};
|
|
216
|
+
});
|
|
217
|
+
}
|
|
120
218
|
const promises = [];
|
|
121
219
|
const keys = [];
|
|
122
220
|
const model = getCombinedModel();
|
|
@@ -145,6 +243,24 @@ export default function useFormValidate(_formError, props) {
|
|
|
145
243
|
});
|
|
146
244
|
});
|
|
147
245
|
const checkFieldAsyncForNextValue = useEventCallback((fieldName, nextValue) => {
|
|
246
|
+
if (resolver) {
|
|
247
|
+
return Promise.resolve(resolver(nextValue)).then(({
|
|
248
|
+
errors
|
|
249
|
+
}) => {
|
|
250
|
+
const fieldError = errors[fieldName];
|
|
251
|
+
const hasFieldError = isValidError(fieldError);
|
|
252
|
+
const nextFormError = mergeResolverErrors(realFormError, errors);
|
|
253
|
+
onCheck?.(nextFormError);
|
|
254
|
+
setFormError(nextFormError);
|
|
255
|
+
if (Object.keys(nextFormError).length > 0) {
|
|
256
|
+
onError?.(nextFormError);
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
hasError: hasFieldError,
|
|
260
|
+
errorMessage: fieldError
|
|
261
|
+
};
|
|
262
|
+
});
|
|
263
|
+
}
|
|
148
264
|
const model = getCombinedModel();
|
|
149
265
|
return model.checkForFieldAsync(fieldName, nextValue, checkOptions).then(resultOfCurrentField => {
|
|
150
266
|
let nextFormError = {
|
package/esm/Form/index.d.ts
CHANGED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The result returned by a validation resolver.
|
|
3
|
+
*
|
|
4
|
+
* @template E - The type of the form error map. Defaults to a record of string keys to any.
|
|
5
|
+
*/
|
|
6
|
+
export interface ResolverResult<E = Record<string, any>> {
|
|
7
|
+
errors: E;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A resolver is a function that integrates third-party validation libraries
|
|
11
|
+
* (e.g. Yup, Zod, AJV, Joi, Valibot…) with the rsuite `Form` component.
|
|
12
|
+
*
|
|
13
|
+
* The resolver receives the current form values and an optional context object,
|
|
14
|
+
* and must return (or resolve to) a `ResolverResult` whose `errors` property is
|
|
15
|
+
* a plain object that maps field names to error messages / error objects.
|
|
16
|
+
*
|
|
17
|
+
* An **empty** `errors` object means the form is valid.
|
|
18
|
+
*
|
|
19
|
+
* @template V - The shape of the form values. Defaults to `Record<string, any>`.
|
|
20
|
+
* @template E - The shape of the error map. Defaults to `Record<string, any>`.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```tsx
|
|
24
|
+
* // Yup example
|
|
25
|
+
* import * as yup from 'yup';
|
|
26
|
+
* import type { Resolver } from 'rsuite';
|
|
27
|
+
*
|
|
28
|
+
* const schema = yup.object({ name: yup.string().email('Invalid email').required() });
|
|
29
|
+
*
|
|
30
|
+
* const resolver: Resolver = async (formValue) => {
|
|
31
|
+
* try {
|
|
32
|
+
* await schema.validate(formValue, { abortEarly: false });
|
|
33
|
+
* return { errors: {} };
|
|
34
|
+
* } catch (e: any) {
|
|
35
|
+
* const errors: Record<string, string> = {};
|
|
36
|
+
* e.inner.forEach((err: yup.ValidationError) => {
|
|
37
|
+
* if (err.path) errors[err.path] = err.message;
|
|
38
|
+
* });
|
|
39
|
+
* return { errors };
|
|
40
|
+
* }
|
|
41
|
+
* };
|
|
42
|
+
*
|
|
43
|
+
* // Zod example
|
|
44
|
+
* import { z } from 'zod';
|
|
45
|
+
*
|
|
46
|
+
* const schema = z.object({ name: z.string().email('Invalid email') });
|
|
47
|
+
*
|
|
48
|
+
* const resolver: Resolver = (formValue) => {
|
|
49
|
+
* const result = schema.safeParse(formValue);
|
|
50
|
+
* if (result.success) return { errors: {} };
|
|
51
|
+
* const errors: Record<string, string> = {};
|
|
52
|
+
* result.error.issues.forEach(err => {
|
|
53
|
+
* if (err.path.length) errors[err.path[0]] = err.message;
|
|
54
|
+
* });
|
|
55
|
+
* return { errors };
|
|
56
|
+
* };
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export type Resolver<V = Record<string, any>, E = Record<string, any>> = (formValue: V, context?: any) => ResolverResult<E> | Promise<ResolverResult<E>>;
|
|
@@ -7,6 +7,11 @@ export interface TimelineProps extends BoxProps {
|
|
|
7
7
|
align?: 'left' | 'right' | 'alternate';
|
|
8
8
|
/** Timeline endless **/
|
|
9
9
|
endless?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Reverse the order of Timeline items
|
|
12
|
+
* @version 6.2.0
|
|
13
|
+
**/
|
|
14
|
+
reverse?: boolean;
|
|
10
15
|
/**
|
|
11
16
|
* Whether an item is active (with highlighted dot).
|
|
12
17
|
*
|
package/esm/Timeline/Timeline.js
CHANGED
|
@@ -30,6 +30,7 @@ const Timeline = forwardRef((props, ref) => {
|
|
|
30
30
|
className,
|
|
31
31
|
align = 'left',
|
|
32
32
|
endless,
|
|
33
|
+
reverse,
|
|
33
34
|
isItemActive = ACTIVE_LAST,
|
|
34
35
|
...rest
|
|
35
36
|
} = propsWithDefaults;
|
|
@@ -41,17 +42,23 @@ const Timeline = forwardRef((props, ref) => {
|
|
|
41
42
|
const withTime = some(React.Children.toArray(children), item => item?.props?.time);
|
|
42
43
|
const classes = merge(className, withPrefix(`align-${align}`, {
|
|
43
44
|
endless,
|
|
44
|
-
'with-time': withTime
|
|
45
|
+
'with-time': withTime,
|
|
46
|
+
reverse
|
|
45
47
|
}));
|
|
48
|
+
const childrenArray = React.Children.toArray(children);
|
|
49
|
+
const orderedChildren = reverse ? [...childrenArray].reverse() : childrenArray;
|
|
46
50
|
return /*#__PURE__*/React.createElement(Box, _extends({
|
|
47
51
|
as: as,
|
|
48
52
|
ref: ref,
|
|
49
53
|
className: classes
|
|
50
|
-
}, rest),
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
}, rest), orderedChildren.map((child, domIndex) => {
|
|
55
|
+
const logicalIndex = reverse ? count - 1 - domIndex : domIndex;
|
|
56
|
+
return /*#__PURE__*/React.cloneElement(child, {
|
|
57
|
+
last: logicalIndex + 1 === count,
|
|
58
|
+
INTERNAL_active: isItemActive(logicalIndex, count),
|
|
59
|
+
align
|
|
60
|
+
});
|
|
61
|
+
}));
|
|
55
62
|
}, SubcomponentsAndStaticMethods);
|
|
56
63
|
Timeline.displayName = 'Timeline';
|
|
57
64
|
export default Timeline;
|
|
@@ -90,23 +90,20 @@ function useFlattenTree(data, options) {
|
|
|
90
90
|
forceUpdate();
|
|
91
91
|
}, [callback, forceUpdate, valueKey, labelKey, uncheckableItemValues, childrenKey]);
|
|
92
92
|
useEffect(() => {
|
|
93
|
-
// when data is changed, should clear the flattenedNodes, avoid duplicate keys
|
|
94
93
|
flattenedNodes.current = {};
|
|
95
94
|
seenValues.current.clear();
|
|
96
95
|
flattenTreeData(data);
|
|
96
|
+
if (multiple) {
|
|
97
|
+
updateTreeNodeCheckState(value);
|
|
98
|
+
forceUpdate();
|
|
99
|
+
}
|
|
97
100
|
}, [data]);
|
|
98
101
|
useEffect(() => {
|
|
99
102
|
if (multiple) {
|
|
100
103
|
updateTreeNodeCheckState(value);
|
|
101
104
|
forceUpdate();
|
|
102
105
|
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Add a dependency on data, because when loading data asynchronously through getChildren,
|
|
106
|
-
* data may change and the node status needs to be updated.
|
|
107
|
-
* @see https://github.com/rsuite/rsuite/issues/3973
|
|
108
|
-
*/
|
|
109
|
-
}, [value, data]);
|
|
106
|
+
}, [value]);
|
|
110
107
|
return flattenedNodes.current;
|
|
111
108
|
}
|
|
112
109
|
export default useFlattenTree;
|
|
@@ -99,6 +99,8 @@ export interface UploaderProps extends BaseBoxProps, Omit<UploadTriggerProps, 'o
|
|
|
99
99
|
onProgress?: (percent: number, file: FileType, event: ProgressEvent, xhr: XMLHttpRequest) => void;
|
|
100
100
|
/** In the file list, click the callback function to delete a file */
|
|
101
101
|
onRemove?: (file: FileType) => void;
|
|
102
|
+
/** Callback function called when all files in the current upload batch have finished (succeeded or failed) */
|
|
103
|
+
onCompletion?: (completedFiles: FileType[], failedFiles: FileType[]) => void;
|
|
102
104
|
/** Custom render file information */
|
|
103
105
|
renderFileInfo?: (file: FileType, fileElement: React.ReactNode) => React.ReactNode;
|
|
104
106
|
/** Custom render thumbnail */
|
package/esm/Uploader/Uploader.js
CHANGED
|
@@ -127,6 +127,7 @@ const Uploader = forwardRef((props, ref) => {
|
|
|
127
127
|
onError,
|
|
128
128
|
onProgress,
|
|
129
129
|
onReupload,
|
|
130
|
+
onCompletion,
|
|
130
131
|
...rest
|
|
131
132
|
} = propsWithDefaults;
|
|
132
133
|
const {
|
|
@@ -138,6 +139,11 @@ const Uploader = forwardRef((props, ref) => {
|
|
|
138
139
|
const rootRef = useRef(null);
|
|
139
140
|
const xhrs = useRef({});
|
|
140
141
|
const trigger = useRef(null);
|
|
142
|
+
const uploadingCount = useRef(0);
|
|
143
|
+
const uploadResults = useRef({
|
|
144
|
+
completed: [],
|
|
145
|
+
failed: []
|
|
146
|
+
});
|
|
141
147
|
const [fileList, dispatch] = useFileList(fileListProp || defaultFileList);
|
|
142
148
|
useEffect(() => {
|
|
143
149
|
if (typeof fileListProp !== 'undefined') {
|
|
@@ -177,7 +183,16 @@ const Uploader = forwardRef((props, ref) => {
|
|
|
177
183
|
};
|
|
178
184
|
updateFileStatus(nextFile);
|
|
179
185
|
onSuccess?.(response, nextFile, event, xhr);
|
|
180
|
-
|
|
186
|
+
uploadingCount.current--;
|
|
187
|
+
uploadResults.current.completed.push(nextFile);
|
|
188
|
+
if (uploadingCount.current === 0) {
|
|
189
|
+
onCompletion?.(uploadResults.current.completed, uploadResults.current.failed);
|
|
190
|
+
uploadResults.current = {
|
|
191
|
+
completed: [],
|
|
192
|
+
failed: []
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}, [onCompletion, onSuccess, updateFileStatus]);
|
|
181
196
|
|
|
182
197
|
/**
|
|
183
198
|
* Callback for file upload error.
|
|
@@ -193,7 +208,16 @@ const Uploader = forwardRef((props, ref) => {
|
|
|
193
208
|
};
|
|
194
209
|
updateFileStatus(nextFile);
|
|
195
210
|
onError?.(status, nextFile, event, xhr);
|
|
196
|
-
|
|
211
|
+
uploadingCount.current--;
|
|
212
|
+
uploadResults.current.failed.push(nextFile);
|
|
213
|
+
if (uploadingCount.current === 0) {
|
|
214
|
+
onCompletion?.(uploadResults.current.completed, uploadResults.current.failed);
|
|
215
|
+
uploadResults.current = {
|
|
216
|
+
completed: [],
|
|
217
|
+
failed: []
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}, [onCompletion, onError, updateFileStatus]);
|
|
197
221
|
|
|
198
222
|
/**
|
|
199
223
|
* Callback for file upload progress update.
|
|
@@ -247,9 +271,19 @@ const Uploader = forwardRef((props, ref) => {
|
|
|
247
271
|
fileList.current.forEach(file => {
|
|
248
272
|
const checkState = shouldUpload?.(file);
|
|
249
273
|
if (checkState instanceof Promise) {
|
|
274
|
+
uploadingCount.current++;
|
|
250
275
|
checkState.then(res => {
|
|
251
276
|
if (res) {
|
|
252
277
|
handleUploadFile(file);
|
|
278
|
+
} else {
|
|
279
|
+
uploadingCount.current--;
|
|
280
|
+
if (uploadingCount.current === 0) {
|
|
281
|
+
onCompletion?.(uploadResults.current.completed, uploadResults.current.failed);
|
|
282
|
+
uploadResults.current = {
|
|
283
|
+
completed: [],
|
|
284
|
+
failed: []
|
|
285
|
+
};
|
|
286
|
+
}
|
|
253
287
|
}
|
|
254
288
|
});
|
|
255
289
|
return;
|
|
@@ -257,6 +291,7 @@ const Uploader = forwardRef((props, ref) => {
|
|
|
257
291
|
return;
|
|
258
292
|
}
|
|
259
293
|
if (file.status === 'inited') {
|
|
294
|
+
uploadingCount.current++;
|
|
260
295
|
handleUploadFile(file);
|
|
261
296
|
}
|
|
262
297
|
});
|
|
@@ -303,6 +338,15 @@ const Uploader = forwardRef((props, ref) => {
|
|
|
303
338
|
const nextFileList = fileList.current.filter(f => f.fileKey !== fileKey);
|
|
304
339
|
if (xhrs.current?.[file.fileKey]?.readyState !== 4) {
|
|
305
340
|
xhrs.current[file.fileKey]?.abort();
|
|
341
|
+
uploadingCount.current--;
|
|
342
|
+
uploadResults.current.failed.push(file);
|
|
343
|
+
if (uploadingCount.current === 0) {
|
|
344
|
+
onCompletion?.(uploadResults.current.completed, uploadResults.current.failed);
|
|
345
|
+
uploadResults.current = {
|
|
346
|
+
completed: [],
|
|
347
|
+
failed: []
|
|
348
|
+
};
|
|
349
|
+
}
|
|
306
350
|
}
|
|
307
351
|
dispatch({
|
|
308
352
|
type: 'remove',
|
|
@@ -313,6 +357,7 @@ const Uploader = forwardRef((props, ref) => {
|
|
|
313
357
|
cleanInputValue();
|
|
314
358
|
});
|
|
315
359
|
const handleReupload = useEventCallback(file => {
|
|
360
|
+
uploadingCount.current++;
|
|
316
361
|
autoUpload && handleUploadFile(file);
|
|
317
362
|
onReupload?.(file);
|
|
318
363
|
});
|
|
@@ -320,6 +365,7 @@ const Uploader = forwardRef((props, ref) => {
|
|
|
320
365
|
// public API
|
|
321
366
|
const start = useCallback(file => {
|
|
322
367
|
if (file) {
|
|
368
|
+
uploadingCount.current++;
|
|
323
369
|
handleUploadFile(file);
|
|
324
370
|
return;
|
|
325
371
|
}
|
|
@@ -31,21 +31,23 @@ const PickerIndicator = ({
|
|
|
31
31
|
size: size === 'xs' ? 'xs' : 'sm'
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
|
-
|
|
35
|
-
return /*#__PURE__*/React.createElement(CloseButton, {
|
|
36
|
-
className: prefix('clean'),
|
|
37
|
-
tabIndex: -1,
|
|
38
|
-
locale: {
|
|
39
|
-
closeLabel: clear
|
|
40
|
-
},
|
|
41
|
-
onClick: onClose
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
return caretAs && /*#__PURE__*/React.createElement(Icon, {
|
|
34
|
+
const caret = caretAs && /*#__PURE__*/React.createElement(Icon, {
|
|
45
35
|
as: caretAs,
|
|
46
36
|
className: prefix('caret-icon'),
|
|
47
37
|
"data-testid": "caret"
|
|
48
38
|
});
|
|
39
|
+
const cleanButton = showCleanButton && !disabled && /*#__PURE__*/React.createElement(CloseButton, {
|
|
40
|
+
className: prefix('clean'),
|
|
41
|
+
tabIndex: -1,
|
|
42
|
+
locale: {
|
|
43
|
+
closeLabel: clear
|
|
44
|
+
},
|
|
45
|
+
onClick: onClose
|
|
46
|
+
});
|
|
47
|
+
if (caret && cleanButton) {
|
|
48
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null, caret, cleanButton);
|
|
49
|
+
}
|
|
50
|
+
return cleanButton || caret || null;
|
|
49
51
|
};
|
|
50
52
|
const props = Component === InputGroup.Addon ? {
|
|
51
53
|
disabled
|
package/esm/toaster/toaster.js
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import ToastContainer, { defaultToasterContainer } from "./ToastContainer.js";
|
|
3
3
|
import { RSUITE_TOASTER_ID } from "../internals/symbols.js";
|
|
4
|
+
import { guid } from "../internals/utils/index.js";
|
|
4
5
|
const containers = new Map();
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Track in-progress container creation promises keyed by `${containerId}_${placement}`.
|
|
9
|
+
* This prevents duplicate containers from being created when `push` is called multiple
|
|
10
|
+
* times synchronously (e.g. inside a loop) before the first container has mounted.
|
|
11
|
+
*/
|
|
12
|
+
const pendingContainerPromises = new Map();
|
|
13
|
+
|
|
6
14
|
/**
|
|
7
15
|
* Create a container instance.
|
|
8
16
|
* @param placement
|
|
@@ -10,7 +18,9 @@ const containers = new Map();
|
|
|
10
18
|
*/
|
|
11
19
|
async function createContainer(placement, props) {
|
|
12
20
|
const [container, containerId] = await ToastContainer.getInstance(props);
|
|
13
|
-
|
|
21
|
+
const key = `${containerId}_${placement}`;
|
|
22
|
+
containers.set(key, container);
|
|
23
|
+
pendingContainerPromises.delete(key);
|
|
14
24
|
return container;
|
|
15
25
|
}
|
|
16
26
|
|
|
@@ -30,12 +40,37 @@ toaster.push = (message, options = {}) => {
|
|
|
30
40
|
...restOptions
|
|
31
41
|
} = options;
|
|
32
42
|
const containerElement = typeof container === 'function' ? container() : container;
|
|
33
|
-
|
|
34
|
-
|
|
43
|
+
if (containerElement) {
|
|
44
|
+
// Pre-assign the container ID so subsequent synchronous calls can find it
|
|
45
|
+
// before the async container creation has completed.
|
|
46
|
+
if (!containerElement[RSUITE_TOASTER_ID]) {
|
|
47
|
+
containerElement[RSUITE_TOASTER_ID] = guid();
|
|
48
|
+
}
|
|
49
|
+
const containerElementId = containerElement[RSUITE_TOASTER_ID];
|
|
50
|
+
const key = `${containerElementId}_${placement}`;
|
|
35
51
|
const existedContainer = getContainer(containerElementId, placement);
|
|
36
52
|
if (existedContainer) {
|
|
37
53
|
return existedContainer.current?.push(message, restOptions);
|
|
38
54
|
}
|
|
55
|
+
|
|
56
|
+
// A container creation for this placement may already be in progress (e.g. when `push`
|
|
57
|
+
// is called multiple times synchronously in a loop). Reuse that promise instead of
|
|
58
|
+
// creating a second container.
|
|
59
|
+
const pendingPromise = pendingContainerPromises.get(key);
|
|
60
|
+
if (pendingPromise) {
|
|
61
|
+
return pendingPromise.then(ref => ref.current?.push(message, restOptions));
|
|
62
|
+
}
|
|
63
|
+
const newOptions = {
|
|
64
|
+
...options,
|
|
65
|
+
container: containerElement,
|
|
66
|
+
placement
|
|
67
|
+
};
|
|
68
|
+
const containerPromise = createContainer(placement, newOptions);
|
|
69
|
+
|
|
70
|
+
// Register the pending promise before any async work begins so that subsequent
|
|
71
|
+
// synchronous `push` calls for the same placement chain onto it.
|
|
72
|
+
pendingContainerPromises.set(key, containerPromise);
|
|
73
|
+
return containerPromise.then(ref => ref.current?.push(message, restOptions));
|
|
39
74
|
}
|
|
40
75
|
const newOptions = {
|
|
41
76
|
...options,
|