@react-fabric/form 1.0.0-beta.9 → 1.0.2
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/{src/input/ArrayInput.module.css → css/_arrayInput.css} +8 -8
- package/css/_colorPicker.css +95 -0
- package/{src/internal/Field.module.css → css/_field.css} +94 -33
- package/{src/input/File.module.css → css/_file.css} +21 -6
- package/css/styles.css +27 -0
- package/dist/cjs/builder/FileField.js +107 -78
- package/dist/cjs/builder/FormField.js +60 -39
- package/dist/cjs/builder/SchemaEditor.js +72 -0
- package/dist/cjs/builder/components/AddSchemaFields.js +64 -0
- package/dist/cjs/builder/components/SchemaArrayForm.js +146 -0
- package/dist/cjs/builder/components/SchemaField.js +423 -0
- package/dist/cjs/form/Controller.js +66 -19
- package/dist/cjs/form/Form.js +97 -39
- package/dist/cjs/form/useForm.js +32 -0
- package/dist/cjs/hooks/useFileUploader.js +74 -45
- package/dist/cjs/hooks/useFormBuilder.js +85 -31
- package/dist/cjs/i18n/{ar.json.js → ar/form.json.js} +5 -4
- package/dist/cjs/i18n/{en.json.js → en/form.json.js} +5 -5
- package/dist/cjs/i18n/index.js +4 -4
- package/dist/cjs/index.js +41 -2
- package/dist/cjs/input/ArrayInput.js +259 -135
- package/dist/cjs/input/AvatarInput.js +99 -63
- package/dist/cjs/input/Checkbox.js +24 -25
- package/dist/cjs/input/ColorInput.js +138 -81
- package/dist/cjs/input/ColorPicker.js +78 -95
- package/dist/cjs/input/DateInput.js +192 -0
- package/dist/cjs/input/Field.js +21 -20
- package/dist/cjs/input/File.js +85 -80
- package/dist/cjs/input/Hidden.js +27 -13
- package/dist/cjs/input/Input.js +49 -48
- package/dist/cjs/input/Number.js +46 -44
- package/dist/cjs/input/Password.js +51 -49
- package/dist/cjs/input/Radio.js +26 -16
- package/dist/cjs/input/RadioGroup.js +26 -11
- package/dist/cjs/input/{Range.js → RangeSlider.js} +61 -66
- package/dist/cjs/input/Search.js +58 -64
- package/dist/cjs/input/Slider.js +57 -66
- package/dist/cjs/input/Switch.js +29 -27
- package/dist/cjs/input/Textarea.js +65 -53
- package/dist/cjs/internal/ErrorIcon.js +9 -9
- package/dist/cjs/internal/FieldWrapper.js +23 -13
- package/dist/cjs/internal/InputWrapper.js +85 -87
- package/dist/cjs/ranger/index.js +7 -7
- package/dist/cjs/ranger/utils.js +9 -4
- package/dist/cjs/select/AutoComplete.js +93 -76
- package/dist/cjs/select/DualList.js +235 -154
- package/dist/cjs/select/List.js +122 -89
- package/dist/cjs/select/Options.js +58 -54
- package/dist/cjs/select/Select.js +119 -89
- package/dist/cjs/select/helpers.js +5 -5
- package/dist/cjs/select/useSelect.js +83 -58
- package/dist/cjs/types/schema.js +43 -0
- package/dist/cjs/utils/eventHandlers.js +16 -6
- package/dist/cjs/utils/getColor.js +6 -6
- package/dist/cjs/utils/getProps.js +49 -0
- package/dist/esm/builder/FileField.js +109 -80
- package/dist/esm/builder/FormField.js +60 -39
- package/dist/esm/builder/SchemaEditor.js +72 -0
- package/dist/esm/builder/components/AddSchemaFields.js +64 -0
- package/dist/esm/builder/components/SchemaArrayForm.js +146 -0
- package/dist/esm/builder/components/SchemaField.js +423 -0
- package/dist/esm/form/Controller.js +66 -19
- package/dist/esm/form/Form.js +97 -39
- package/dist/esm/form/useForm.js +32 -0
- package/dist/esm/hooks/useFileUploader.js +75 -46
- package/dist/esm/hooks/useFormBuilder.js +84 -30
- package/dist/esm/i18n/{ar.json.js → ar/form.json.js} +5 -4
- package/dist/esm/i18n/{en.json.js → en/form.json.js} +5 -5
- package/dist/esm/i18n/index.js +2 -2
- package/dist/esm/index.js +36 -1
- package/dist/esm/input/ArrayInput.js +260 -136
- package/dist/esm/input/AvatarInput.js +99 -63
- package/dist/esm/input/Checkbox.js +26 -27
- package/dist/esm/input/ColorInput.js +141 -84
- package/dist/esm/input/ColorPicker.js +80 -97
- package/dist/esm/input/DateInput.js +192 -0
- package/dist/esm/input/Field.js +21 -20
- package/dist/esm/input/File.js +89 -84
- package/dist/esm/input/Hidden.js +28 -14
- package/dist/esm/input/Input.js +51 -50
- package/dist/esm/input/Number.js +48 -46
- package/dist/esm/input/Password.js +54 -52
- package/dist/esm/input/Radio.js +27 -17
- package/dist/esm/input/RadioGroup.js +27 -12
- package/dist/esm/input/{Range.js → RangeSlider.js} +62 -67
- package/dist/esm/input/Search.js +61 -67
- package/dist/esm/input/Slider.js +58 -67
- package/dist/esm/input/Switch.js +31 -29
- package/dist/esm/input/Textarea.js +67 -55
- package/dist/esm/internal/ErrorIcon.js +11 -11
- package/dist/esm/internal/FieldWrapper.js +23 -13
- package/dist/esm/internal/InputWrapper.js +88 -90
- package/dist/esm/ranger/index.js +7 -7
- package/dist/esm/ranger/utils.js +9 -4
- package/dist/esm/select/AutoComplete.js +95 -78
- package/dist/esm/select/DualList.js +233 -152
- package/dist/esm/select/List.js +125 -92
- package/dist/esm/select/Options.js +58 -54
- package/dist/esm/select/Select.js +122 -92
- package/dist/esm/select/helpers.js +5 -5
- package/dist/esm/select/useSelect.js +84 -59
- package/{src/input/ColorPicker.module.css → dist/esm/types/schema.js} +18 -27
- package/dist/esm/utils/eventHandlers.js +16 -6
- package/dist/esm/utils/getColor.js +6 -6
- package/dist/esm/utils/getProps.js +49 -0
- package/dist/types/builder/FileField.d.ts +7 -4
- package/dist/types/builder/FormField.d.ts +4 -2
- package/dist/types/builder/SchemaEditor.d.ts +6 -0
- package/dist/types/builder/components/AddSchemaFields.d.ts +2 -0
- package/dist/types/builder/components/SchemaArrayForm.d.ts +2 -0
- package/dist/types/builder/components/SchemaField.d.ts +12 -0
- package/dist/types/form/Controller.d.ts +45 -3
- package/dist/types/form/Form.d.ts +38 -7
- package/dist/types/form/useForm.d.ts +6 -0
- package/dist/types/hooks/useFileUploader.d.ts +24 -3
- package/dist/types/hooks/useFormBuilder.d.ts +41 -3
- package/dist/types/index.d.ts +32 -2
- package/dist/types/input/ArrayInput.d.ts +68 -13
- package/dist/types/input/AvatarInput.d.ts +23 -6
- package/dist/types/input/Checkbox.d.ts +12 -10
- package/dist/types/input/ColorInput.d.ts +15 -5
- package/dist/types/input/ColorPicker.d.ts +10 -2
- package/dist/types/input/DateInput.d.ts +21 -0
- package/dist/types/input/Field.d.ts +9 -36
- package/dist/types/input/File.d.ts +16 -4
- package/dist/types/input/Hidden.d.ts +7 -6
- package/dist/types/input/Input.d.ts +11 -4
- package/dist/types/input/Number.d.ts +9 -4
- package/dist/types/input/Password.d.ts +9 -4
- package/dist/types/input/Radio.d.ts +9 -4
- package/dist/types/input/RadioGroup.d.ts +14 -6
- package/dist/types/input/{Range.d.ts → RangeSlider.d.ts} +11 -6
- package/dist/types/input/Search.d.ts +14 -4
- package/dist/types/input/Slider.d.ts +9 -6
- package/dist/types/input/Switch.d.ts +13 -8
- package/dist/types/input/Textarea.d.ts +15 -4
- package/dist/types/internal/ErrorIcon.d.ts +4 -1
- package/dist/types/internal/FieldWrapper.d.ts +10 -1
- package/dist/types/internal/InputWrapper.d.ts +19 -1
- package/dist/types/ranger/index.d.ts +7 -3
- package/dist/types/ranger/utils.d.ts +2 -2
- package/dist/types/select/AutoComplete.d.ts +7 -2
- package/dist/types/select/DualList.d.ts +11 -37
- package/dist/types/select/List.d.ts +7 -2
- package/dist/types/select/Options.d.ts +8 -7
- package/dist/types/select/Select.d.ts +7 -2
- package/dist/types/select/helpers.d.ts +2 -2
- package/dist/types/select/useSelect.d.ts +10 -28
- package/dist/types/types/index.d.ts +73 -39
- package/dist/types/types/schema.d.ts +78 -32
- package/dist/types/utils/eventHandlers.d.ts +4 -4
- package/dist/types/utils/getColor.d.ts +4 -4
- package/dist/types/utils/getProps.d.ts +26 -0
- package/dist/types/utils/index.d.ts +1 -0
- package/package.json +25 -25
- package/src/builder/FileField.tsx +73 -56
- package/src/builder/FormField.tsx +66 -58
- package/src/builder/SchemaEditor.tsx +78 -0
- package/src/builder/components/AddSchemaFields.tsx +64 -0
- package/src/builder/components/SchemaArrayForm.tsx +146 -0
- package/src/builder/components/SchemaField.tsx +405 -0
- package/src/form/Controller.tsx +61 -24
- package/src/form/Form.tsx +74 -50
- package/src/form/useForm.ts +53 -0
- package/src/hooks/useFileUploader.ts +115 -84
- package/src/hooks/useFormBuilder.tsx +80 -41
- package/src/i18n/{ar.json → ar/form.json} +5 -4
- package/src/i18n/{en.json → en/form.json} +5 -5
- package/src/i18n/index.ts +2 -2
- package/src/index.ts +34 -20
- package/src/input/ArrayInput.tsx +255 -173
- package/src/input/AvatarInput.tsx +82 -39
- package/src/input/Checkbox.tsx +30 -54
- package/src/input/ColorInput.tsx +146 -105
- package/src/input/ColorPicker.tsx +76 -102
- package/src/input/DateInput.tsx +225 -0
- package/src/input/Field.tsx +23 -65
- package/src/input/File.tsx +76 -98
- package/src/input/Hidden.tsx +36 -24
- package/src/input/Input.tsx +55 -53
- package/src/input/Number.tsx +49 -54
- package/src/input/Password.tsx +61 -66
- package/src/input/Radio.tsx +28 -37
- package/src/input/RadioGroup.tsx +30 -25
- package/src/input/{Range.tsx → RangeSlider.tsx} +81 -109
- package/src/input/Search.tsx +64 -77
- package/src/input/Slider.tsx +80 -109
- package/src/input/Switch.tsx +39 -53
- package/src/input/Textarea.tsx +72 -59
- package/src/internal/ErrorIcon.tsx +22 -21
- package/src/internal/FieldWrapper.tsx +33 -23
- package/src/internal/InputWrapper.tsx +132 -143
- package/src/ranger/index.tsx +37 -117
- package/src/ranger/utils.ts +12 -11
- package/src/select/AutoComplete.tsx +113 -123
- package/src/select/DualList.tsx +218 -204
- package/src/select/List.tsx +130 -97
- package/src/select/Options.tsx +59 -57
- package/src/select/Select.tsx +148 -122
- package/src/select/helpers.ts +13 -8
- package/src/select/useSelect.tsx +110 -127
- package/src/types/index.ts +76 -45
- package/src/types/schema.ts +97 -40
- package/src/utils/eventHandlers.ts +24 -26
- package/src/utils/getColor.ts +9 -8
- package/src/utils/getProps.ts +47 -0
- package/src/utils/index.ts +1 -0
- package/dist/cjs/input/ArrayInput.module.css +0 -1
- package/dist/cjs/input/ColorPicker.module.css +0 -1
- package/dist/cjs/input/File.module.css +0 -1
- package/dist/cjs/internal/Field.module.css +0 -1
- package/dist/esm/input/ArrayInput.module.css +0 -1
- package/dist/esm/input/ColorPicker.module.css +0 -1
- package/dist/esm/input/File.module.css +0 -1
- package/dist/esm/internal/Field.module.css +0 -1
package/src/input/ArrayInput.tsx
CHANGED
|
@@ -21,29 +21,32 @@
|
|
|
21
21
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import
|
|
24
|
+
import { RestrictToVerticalAxis } from "@dnd-kit/abstract/modifiers";
|
|
25
|
+
import { DragDropProvider, type DragEndEvent } from "@dnd-kit/react";
|
|
26
|
+
import { useSortable } from "@dnd-kit/react/sortable";
|
|
27
|
+
import { Button, cloneChildren, Icon, Tooltip } from "@react-fabric/core";
|
|
28
|
+
import { cn, isFalse, isObject, isString } from "@react-fabric/utilities";
|
|
29
29
|
import {
|
|
30
|
-
Children,
|
|
31
30
|
cloneElement,
|
|
31
|
+
isValidElement,
|
|
32
32
|
useCallback,
|
|
33
|
+
useEffect,
|
|
33
34
|
useImperativeHandle,
|
|
34
35
|
useMemo,
|
|
36
|
+
useState,
|
|
35
37
|
type PropsWithChildren,
|
|
36
|
-
type
|
|
37
|
-
type RefObject,
|
|
38
|
+
type Ref,
|
|
38
39
|
} from "react";
|
|
39
40
|
import { useFieldArray, useFormContext } from "react-hook-form";
|
|
40
41
|
import { useTranslation } from "react-i18next";
|
|
41
42
|
import { FieldWrapper } from "../internal/FieldWrapper";
|
|
42
|
-
import classes from "./ArrayInput.module.css";
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
interface ErrorObject {
|
|
45
|
+
key: string;
|
|
46
|
+
values: Record<string, string>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ArrayInputProps<T extends AnyObject = string> {
|
|
47
50
|
/**
|
|
48
51
|
* input label
|
|
49
52
|
*/
|
|
@@ -51,7 +54,7 @@ export interface ArrayInputProps {
|
|
|
51
54
|
/**
|
|
52
55
|
* append to label end
|
|
53
56
|
*/
|
|
54
|
-
appendLabel?:
|
|
57
|
+
appendLabel?: React.ReactElement | string | number | boolean;
|
|
55
58
|
/**
|
|
56
59
|
* info label at bottom
|
|
57
60
|
*/
|
|
@@ -77,6 +80,10 @@ export interface ArrayInputProps {
|
|
|
77
80
|
* minimum required length
|
|
78
81
|
*/
|
|
79
82
|
minItems?: number;
|
|
83
|
+
/**
|
|
84
|
+
* maximum length
|
|
85
|
+
*/
|
|
86
|
+
maxItems?: number;
|
|
80
87
|
/**
|
|
81
88
|
* field name to focus on add
|
|
82
89
|
*/
|
|
@@ -87,23 +94,32 @@ export interface ArrayInputProps {
|
|
|
87
94
|
enableSorting?: boolean;
|
|
88
95
|
children:
|
|
89
96
|
| React.ReactNode
|
|
90
|
-
| ((props: { index: number; name: string }) => ReactNode);
|
|
97
|
+
| ((props: { index: number; name: string; item: T; onChange: (val: false | undefined | T) => void }) => React.ReactNode);
|
|
91
98
|
/**
|
|
92
99
|
* add item button label
|
|
93
100
|
*/
|
|
94
101
|
addLabel?: string;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
fixedList?: boolean;
|
|
99
|
-
arrayRef?: RefObject<{
|
|
100
|
-
addItem: (item: AnyObject) => void;
|
|
102
|
+
buttonPosition?: "top" | "bottom" | "both";
|
|
103
|
+
arrayRef?: Ref<{
|
|
104
|
+
addItem: (item: false | undefined | T | Promise<false | undefined | T>) => void;
|
|
101
105
|
removeItem: (idx: number) => void;
|
|
102
106
|
}>;
|
|
103
107
|
/**
|
|
104
|
-
*
|
|
108
|
+
* add new item
|
|
105
109
|
*/
|
|
106
|
-
onAdd?: () =>
|
|
110
|
+
onAdd?: () => false | undefined | T | Promise<false | undefined | T>;
|
|
111
|
+
/**
|
|
112
|
+
* on item remove
|
|
113
|
+
*/
|
|
114
|
+
onRemove?: (item: T, idx: number) => void;
|
|
115
|
+
/**
|
|
116
|
+
* before item remove
|
|
117
|
+
*/
|
|
118
|
+
onBeforeRemove?: (item: T, idx: number) => Promise<boolean> | boolean;
|
|
119
|
+
/**
|
|
120
|
+
* can remove item
|
|
121
|
+
*/
|
|
122
|
+
canRemove?: false | "newonly" | ((props: { item: T; index: number; lastItem: boolean }) => boolean);
|
|
107
123
|
|
|
108
124
|
/**
|
|
109
125
|
* field width
|
|
@@ -119,213 +135,279 @@ export interface ArrayInputProps {
|
|
|
119
135
|
labelWidth?: string;
|
|
120
136
|
}
|
|
121
137
|
|
|
122
|
-
|
|
138
|
+
function SortableItem({
|
|
139
|
+
id,
|
|
140
|
+
index,
|
|
141
|
+
children,
|
|
142
|
+
className,
|
|
143
|
+
enableSorting,
|
|
144
|
+
}: PropsWithChildren<{ id: string; index: number; className: string; enableSorting?: boolean }>) {
|
|
145
|
+
const { ref, handleRef, isDragging } = useSortable({ id, index, disabled: !enableSorting });
|
|
146
|
+
if (enableSorting) {
|
|
147
|
+
return (
|
|
148
|
+
<div className={className} ref={ref}>
|
|
149
|
+
<Icon ref={handleRef} className={isDragging ? "cursor-grabbing" : "cursor-grab"} icon="icon-[mdi--drag-vertical]" />
|
|
150
|
+
{children}
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return <div className={className}>{children}</div>;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function Wrapper({
|
|
159
|
+
children,
|
|
160
|
+
fields,
|
|
161
|
+
enableSorting,
|
|
162
|
+
onMove,
|
|
163
|
+
}: {
|
|
164
|
+
children: React.ReactNode;
|
|
165
|
+
enableSorting?: boolean;
|
|
166
|
+
fields: KeyValue[];
|
|
167
|
+
onMove: (active: number, end: number) => void;
|
|
168
|
+
}) {
|
|
169
|
+
if (enableSorting) {
|
|
170
|
+
// pass id list to dnd context
|
|
171
|
+
const idMap = fields.map((item) => item.__ID__ as string);
|
|
172
|
+
const handleDragEnd = (e: DragEndEvent) => {
|
|
173
|
+
if (e.operation.source?.id !== e.operation.target?.id) {
|
|
174
|
+
// find index of item and drop over
|
|
175
|
+
const active = idMap.indexOf(`${e.operation.source?.id}`);
|
|
176
|
+
const end = idMap.indexOf(`${e.operation.target?.id}`);
|
|
177
|
+
onMove(active, end);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
return (
|
|
181
|
+
<DragDropProvider onDragEnd={handleDragEnd} modifiers={[RestrictToVerticalAxis]}>
|
|
182
|
+
{children}
|
|
183
|
+
</DragDropProvider>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return children;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* ArrayInput component for managing an array of items in a form.
|
|
192
|
+
* It allows adding, removing, and sorting items within the array.
|
|
193
|
+
* It integrates with react-hook-form for form state management and validation.
|
|
194
|
+
* It supports fixed-length lists, custom add/remove handlers, and can display validation errors.
|
|
195
|
+
* It also provides options for inline display and custom button positioning.
|
|
196
|
+
*
|
|
197
|
+
* It uses the `useFieldArray` hook from `react-hook-form` to manage the array state and provides a simple interface for adding and removing items.
|
|
198
|
+
*
|
|
199
|
+
* It also supports drag-and-drop sorting using `@dnd-kit/core` and `@dnd-kit/sortable` for a better user experience when reordering items.
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* ```jsx
|
|
203
|
+
* <ArrayInput
|
|
204
|
+
* name="items"
|
|
205
|
+
* label="Items"
|
|
206
|
+
* addLabel="Add Item"
|
|
207
|
+
* onAdd={() => ({ name: "", value: "" })}
|
|
208
|
+
* onRemove={(item, index) => console.log("Removed item:", item, "at index:", index)}
|
|
209
|
+
* onBeforeRemove={(item, index) => confirm(`Are you sure you want to remove item at index ${index}?`)}
|
|
210
|
+
* canRemove={({ item, index, lastItem }) => !lastItem} // Prevent removing the last item
|
|
211
|
+
* >
|
|
212
|
+
* {({ index, name }) => (
|
|
213
|
+
* <Input
|
|
214
|
+
* name={name}
|
|
215
|
+
* label={`Item ${index + 1}`}
|
|
216
|
+
* placeholder="Enter item value"
|
|
217
|
+
* required={index === 0} // Make the first item required
|
|
218
|
+
* />
|
|
219
|
+
* )}
|
|
220
|
+
* </ArrayInput>
|
|
221
|
+
* ```
|
|
222
|
+
*
|
|
223
|
+
* @see {@link https://react-hook-form.com/api/usefieldarray} for more details on `useFieldArray`.
|
|
224
|
+
* @see {@link https://react-dnd.github.io/react-dnd/about} for more details on DnD context and sorting.
|
|
225
|
+
*/
|
|
226
|
+
export function ArrayInput<T extends AnyObject = string>({
|
|
123
227
|
name,
|
|
124
228
|
children,
|
|
125
229
|
addLabel,
|
|
126
|
-
fixedList,
|
|
127
230
|
focusName = "",
|
|
231
|
+
buttonPosition = "bottom",
|
|
128
232
|
onAdd,
|
|
233
|
+
onRemove,
|
|
234
|
+
onBeforeRemove,
|
|
235
|
+
canRemove,
|
|
129
236
|
arrayRef,
|
|
130
237
|
enableSorting,
|
|
131
238
|
disabled = false,
|
|
132
239
|
readOnly = false,
|
|
133
240
|
minItems = 0,
|
|
241
|
+
maxItems = Number.MAX_SAFE_INTEGER,
|
|
134
242
|
...rest
|
|
135
|
-
}: ArrayInputProps)
|
|
243
|
+
}: ArrayInputProps<T>) {
|
|
136
244
|
const { t } = useTranslation("form");
|
|
137
245
|
const form = useFormContext();
|
|
138
246
|
|
|
139
|
-
if (!form?.control)
|
|
140
|
-
throw new Error("ArrayInput must be contained within a Form element");
|
|
247
|
+
if (!form?.control) throw new Error("ArrayInput must be contained within a Form element");
|
|
141
248
|
|
|
142
249
|
const { fields, append, remove, move } = useFieldArray({
|
|
143
250
|
name,
|
|
251
|
+
keyName: "__ID__",
|
|
144
252
|
control: form.control,
|
|
145
253
|
});
|
|
146
254
|
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
255
|
+
const [initialList, setInitialList] = useState<string[]>(() => []);
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
// eslint-disable-next-line @eslint-react/set-state-in-effect
|
|
258
|
+
setInitialList(fields.map((f) => f.__ID__));
|
|
259
|
+
}, [fields]);
|
|
260
|
+
|
|
261
|
+
const error = useMemo(
|
|
262
|
+
() => form.formState.errors[name]?.message ?? form.formState.errors[name]?.root?.message,
|
|
263
|
+
[form.formState.errors, name],
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const removeCheck = useCallback(
|
|
267
|
+
(item: T, index: number) => {
|
|
268
|
+
if (canRemove === false) return false;
|
|
269
|
+
if (canRemove === "newonly") {
|
|
270
|
+
return !initialList.includes((item as KeyValue).__ID__ as string);
|
|
271
|
+
}
|
|
272
|
+
if (typeof canRemove === "function") {
|
|
273
|
+
return canRemove({
|
|
274
|
+
item,
|
|
275
|
+
index,
|
|
276
|
+
lastItem: index + 1 === fields.length,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
return true;
|
|
280
|
+
},
|
|
281
|
+
[canRemove, initialList, fields],
|
|
152
282
|
);
|
|
153
283
|
|
|
154
284
|
const handleAdd = useCallback(
|
|
155
|
-
(item
|
|
156
|
-
|
|
285
|
+
async (item?: Promise<false | undefined | T>) => {
|
|
286
|
+
if (!item) return;
|
|
287
|
+
append(await item);
|
|
157
288
|
setTimeout(() => {
|
|
158
|
-
form.setFocus(
|
|
159
|
-
`${name}.${fields.length}${focusName ? "." + focusName : ""}`,
|
|
160
|
-
);
|
|
289
|
+
form.setFocus(`${name}.${fields.length}${focusName ? "." + focusName : ""}`);
|
|
161
290
|
}, 100);
|
|
162
291
|
},
|
|
163
|
-
[name, fields, focusName],
|
|
292
|
+
[append, form, name, fields.length, focusName],
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
const handleRemove = useCallback(
|
|
296
|
+
(item: T, index: number) => {
|
|
297
|
+
const ret = onBeforeRemove?.(item, index);
|
|
298
|
+
void Promise.resolve(ret).then((b) => {
|
|
299
|
+
if (!isFalse(b)) {
|
|
300
|
+
remove(index);
|
|
301
|
+
onRemove?.(item, index);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
},
|
|
305
|
+
[remove, onRemove, onBeforeRemove],
|
|
164
306
|
);
|
|
165
307
|
|
|
166
308
|
useImperativeHandle(arrayRef, () => ({
|
|
167
|
-
addItem: handleAdd,
|
|
309
|
+
addItem: (t) => handleAdd(Promise.resolve(t)),
|
|
168
310
|
removeItem: remove,
|
|
169
311
|
}));
|
|
170
312
|
|
|
171
|
-
const
|
|
172
|
-
(
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
313
|
+
const addButton = useMemo(
|
|
314
|
+
() => (
|
|
315
|
+
<div className="flex justify-end">
|
|
316
|
+
<Tooltip
|
|
317
|
+
color="danger"
|
|
318
|
+
content={
|
|
319
|
+
isObject<ErrorObject>(error)
|
|
320
|
+
? t(
|
|
321
|
+
error.key,
|
|
322
|
+
isObject(error.values)
|
|
323
|
+
? {
|
|
324
|
+
...error.values,
|
|
325
|
+
label: t(error.values.label ?? "form:badkey", error.values.path),
|
|
326
|
+
}
|
|
327
|
+
: {},
|
|
328
|
+
)
|
|
329
|
+
: isString(error)
|
|
330
|
+
? error
|
|
331
|
+
: ""
|
|
182
332
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
333
|
+
>
|
|
334
|
+
<Button
|
|
335
|
+
size="sm"
|
|
336
|
+
aria-label="Add item"
|
|
337
|
+
icon="icon-[mdi--plus-circle-outline]"
|
|
338
|
+
onClick={() => handleAdd(Promise.resolve(onAdd?.()))}
|
|
339
|
+
data-invalid={!!error}
|
|
340
|
+
disabled={!!disabled || readOnly || fields.length > maxItems}
|
|
341
|
+
className={cn("fabric-addButton", canRemove !== false && "me-10")}
|
|
188
342
|
>
|
|
189
|
-
|
|
190
|
-
</
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
},
|
|
196
|
-
[enableSorting, fields],
|
|
197
|
-
);
|
|
198
|
-
// @ts-expect-error ignore
|
|
199
|
-
Wrapper.displayName = "SortableWrapper";
|
|
200
|
-
|
|
201
|
-
const SortableItem = useCallback(
|
|
202
|
-
({
|
|
203
|
-
id,
|
|
204
|
-
children,
|
|
205
|
-
className,
|
|
206
|
-
}: PropsWithChildren<{ id: AnyObject; className: string }>) => {
|
|
207
|
-
if (enableSorting) {
|
|
208
|
-
const {
|
|
209
|
-
attributes,
|
|
210
|
-
listeners,
|
|
211
|
-
setNodeRef,
|
|
212
|
-
transform,
|
|
213
|
-
transition,
|
|
214
|
-
isDragging,
|
|
215
|
-
} = useSortable({ id });
|
|
216
|
-
return (
|
|
217
|
-
<div
|
|
218
|
-
className={className}
|
|
219
|
-
ref={setNodeRef}
|
|
220
|
-
style={{
|
|
221
|
-
transform: CSS.Transform.toString(transform),
|
|
222
|
-
transition,
|
|
223
|
-
}}
|
|
224
|
-
>
|
|
225
|
-
<Icon
|
|
226
|
-
className={isDragging ? "cursor-grabbing" : "cursor-grab"}
|
|
227
|
-
icon={CoreIcons.dragHandle}
|
|
228
|
-
{...attributes}
|
|
229
|
-
{...listeners}
|
|
230
|
-
/>
|
|
231
|
-
{children}
|
|
232
|
-
</div>
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return <div className={className}>{children}</div>;
|
|
237
|
-
},
|
|
238
|
-
[enableSorting],
|
|
343
|
+
{addLabel ?? t("form:addArray")}
|
|
344
|
+
</Button>
|
|
345
|
+
</Tooltip>
|
|
346
|
+
</div>
|
|
347
|
+
),
|
|
348
|
+
[error, t, disabled, readOnly, fields.length, maxItems, canRemove, addLabel, handleAdd, onAdd],
|
|
239
349
|
);
|
|
240
|
-
// @ts-expect-error ignore
|
|
241
|
-
SortableItem.displayName = "SortableItem";
|
|
242
350
|
|
|
243
351
|
return (
|
|
244
|
-
<div className={
|
|
245
|
-
<FieldWrapper {...rest}>
|
|
246
|
-
<Wrapper>
|
|
352
|
+
<div className={"fabric-arrayInput"}>
|
|
353
|
+
<FieldWrapper {...rest} appendLabel={<div className="-me-2">{onAdd && buttonPosition !== "bottom" && addButton}</div>}>
|
|
354
|
+
<Wrapper onMove={move} enableSorting={enableSorting} fields={fields}>
|
|
247
355
|
{fields.map((item, index) => (
|
|
248
356
|
<SortableItem
|
|
249
|
-
key={item.
|
|
250
|
-
id={item.
|
|
357
|
+
key={item.__ID__}
|
|
358
|
+
id={item.__ID__}
|
|
359
|
+
index={index}
|
|
360
|
+
enableSorting={enableSorting}
|
|
251
361
|
className="flex items-center flex-nowrap gap-2 mb-1"
|
|
252
362
|
>
|
|
253
363
|
{typeof children === "function"
|
|
254
364
|
? children({
|
|
255
365
|
index,
|
|
366
|
+
item: item as T,
|
|
256
367
|
name: `${name}.${index}`,
|
|
368
|
+
onChange: (val) => {
|
|
369
|
+
void Promise.resolve(val).then((result) => {
|
|
370
|
+
if (result) form.setValue(`${name}.${index}`, result);
|
|
371
|
+
});
|
|
372
|
+
},
|
|
257
373
|
})
|
|
258
|
-
:
|
|
259
|
-
|
|
260
|
-
child
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
{!fixedList && (
|
|
374
|
+
: cloneChildren(children)?.map((child) => {
|
|
375
|
+
if (isValidElement(child)) {
|
|
376
|
+
const childEl = child as React.ReactElement<{ name: string }>;
|
|
377
|
+
return cloneElement(
|
|
378
|
+
childEl,
|
|
379
|
+
Object.assign(
|
|
380
|
+
{
|
|
381
|
+
disabled,
|
|
382
|
+
readOnly,
|
|
383
|
+
"data-inner": true,
|
|
384
|
+
name: `${name}.${index}${childEl.props.name ? "." + childEl.props.name : ""}`,
|
|
385
|
+
},
|
|
386
|
+
index > 0 && {
|
|
387
|
+
label: undefined,
|
|
388
|
+
required: undefined,
|
|
389
|
+
appendLabel: undefined,
|
|
390
|
+
},
|
|
391
|
+
),
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
})}
|
|
395
|
+
{canRemove !== false && (
|
|
281
396
|
<Button
|
|
282
397
|
aria-label="Remove item"
|
|
283
|
-
icon=
|
|
398
|
+
icon="icon-[mdi--minus-circle-outline]"
|
|
284
399
|
color="danger"
|
|
285
400
|
variant="link"
|
|
286
401
|
className="self-end"
|
|
287
|
-
disabled={disabled || readOnly || fields.length <= minItems}
|
|
288
|
-
onClick={() =>
|
|
402
|
+
disabled={disabled || readOnly || fields.length <= minItems || !removeCheck(item as T, index)}
|
|
403
|
+
onClick={() => handleRemove(item as T, index)}
|
|
289
404
|
/>
|
|
290
405
|
)}
|
|
291
406
|
</SortableItem>
|
|
292
407
|
))}
|
|
293
408
|
</Wrapper>
|
|
294
409
|
</FieldWrapper>
|
|
295
|
-
{
|
|
296
|
-
<div className="flex justify-end">
|
|
297
|
-
<Tooltip
|
|
298
|
-
color="danger"
|
|
299
|
-
content={
|
|
300
|
-
isString(error)
|
|
301
|
-
? `${error}`
|
|
302
|
-
: (t(
|
|
303
|
-
error?.key,
|
|
304
|
-
error?.values
|
|
305
|
-
? {
|
|
306
|
-
...error?.values,
|
|
307
|
-
label: t(
|
|
308
|
-
error.values?.label ?? "form:badkey",
|
|
309
|
-
error.values?.path,
|
|
310
|
-
),
|
|
311
|
-
}
|
|
312
|
-
: {},
|
|
313
|
-
) as string)
|
|
314
|
-
}
|
|
315
|
-
>
|
|
316
|
-
<Button
|
|
317
|
-
aria-label="Add item"
|
|
318
|
-
icon={CoreIcons.insert}
|
|
319
|
-
onClick={() => handleAdd(onAdd())}
|
|
320
|
-
data-invalid={!!error}
|
|
321
|
-
disabled={!!disabled || readOnly}
|
|
322
|
-
className={classNames(classes.addButton, "me-10")}
|
|
323
|
-
>
|
|
324
|
-
{addLabel ?? t("addArray")}
|
|
325
|
-
</Button>
|
|
326
|
-
</Tooltip>
|
|
327
|
-
</div>
|
|
328
|
-
)}
|
|
410
|
+
{onAdd && buttonPosition !== "top" && addButton}
|
|
329
411
|
</div>
|
|
330
412
|
);
|
|
331
|
-
}
|
|
413
|
+
}
|