@react-fabric/form 1.0.0-beta.6 → 1.0.0-beta.61

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.
Files changed (187) hide show
  1. package/{src/input/ArrayInput.module.css → css/_arrayInput.css} +8 -8
  2. package/css/_colorPicker.css +65 -0
  3. package/{src/internal/Field.module.css → css/_field.css} +66 -30
  4. package/{src/input/File.module.css → css/_file.css} +21 -6
  5. package/css/styles.css +27 -0
  6. package/dist/cjs/builder/FileField.js +40 -28
  7. package/dist/cjs/builder/FormField.js +50 -34
  8. package/dist/cjs/builder/SchemaEditor.js +72 -0
  9. package/dist/cjs/builder/components/AddSchemaFields.js +63 -0
  10. package/dist/cjs/builder/components/SchemaArrayForm.js +145 -0
  11. package/dist/cjs/builder/components/SchemaField.js +414 -0
  12. package/dist/cjs/form/Controller.js +46 -1
  13. package/dist/cjs/form/Form.js +71 -13
  14. package/dist/cjs/hooks/useFileUploader.js +16 -0
  15. package/dist/cjs/hooks/useFormBuilder.js +67 -18
  16. package/dist/cjs/i18n/ar.json.js +5 -4
  17. package/dist/cjs/i18n/en.json.js +5 -5
  18. package/dist/cjs/index.js +12 -2
  19. package/dist/cjs/input/ArrayInput.js +128 -38
  20. package/dist/cjs/input/AvatarInput.js +43 -23
  21. package/dist/cjs/input/Checkbox.js +18 -8
  22. package/dist/cjs/input/ColorInput.js +114 -59
  23. package/dist/cjs/input/ColorPicker.js +60 -76
  24. package/dist/cjs/input/DateInput.js +197 -0
  25. package/dist/cjs/input/Field.js +10 -4
  26. package/dist/cjs/input/File.js +15 -8
  27. package/dist/cjs/input/Hidden.js +13 -2
  28. package/dist/cjs/input/Input.js +24 -10
  29. package/dist/cjs/input/Number.js +22 -10
  30. package/dist/cjs/input/Password.js +23 -13
  31. package/dist/cjs/input/Radio.js +14 -5
  32. package/dist/cjs/input/RadioGroup.js +10 -1
  33. package/dist/cjs/input/{Range.js → RangeSlider.js} +16 -8
  34. package/dist/cjs/input/Search.js +32 -22
  35. package/dist/cjs/input/Slider.js +12 -6
  36. package/dist/cjs/input/Switch.js +20 -9
  37. package/dist/cjs/input/Textarea.js +29 -8
  38. package/dist/cjs/internal/ErrorIcon.js +2 -2
  39. package/dist/cjs/internal/FieldWrapper.js +11 -4
  40. package/dist/cjs/internal/InputWrapper.js +29 -22
  41. package/dist/cjs/select/AutoComplete.js +22 -13
  42. package/dist/cjs/select/DualList.js +34 -12
  43. package/dist/cjs/select/List.js +49 -21
  44. package/dist/cjs/select/Options.js +49 -42
  45. package/dist/cjs/select/Select.js +21 -10
  46. package/dist/cjs/select/useSelect.js +22 -11
  47. package/dist/cjs/types/schema.js +43 -0
  48. package/dist/cjs/utils/eventHandlers.js +15 -5
  49. package/dist/esm/builder/FileField.js +42 -30
  50. package/dist/esm/builder/FormField.js +50 -34
  51. package/dist/esm/builder/SchemaEditor.js +72 -0
  52. package/dist/esm/builder/components/AddSchemaFields.js +63 -0
  53. package/dist/esm/builder/components/SchemaArrayForm.js +145 -0
  54. package/dist/esm/builder/components/SchemaField.js +414 -0
  55. package/dist/esm/form/Controller.js +46 -1
  56. package/dist/esm/form/Form.js +73 -16
  57. package/dist/esm/hooks/useFileUploader.js +16 -0
  58. package/dist/esm/hooks/useFormBuilder.js +65 -16
  59. package/dist/esm/i18n/ar.json.js +5 -4
  60. package/dist/esm/i18n/en.json.js +5 -5
  61. package/dist/esm/index.js +6 -2
  62. package/dist/esm/input/ArrayInput.js +128 -38
  63. package/dist/esm/input/AvatarInput.js +43 -23
  64. package/dist/esm/input/Checkbox.js +19 -9
  65. package/dist/esm/input/ColorInput.js +116 -61
  66. package/dist/esm/input/ColorPicker.js +60 -76
  67. package/dist/esm/input/DateInput.js +197 -0
  68. package/dist/esm/input/Field.js +11 -5
  69. package/dist/esm/input/File.js +18 -11
  70. package/dist/esm/input/Hidden.js +13 -2
  71. package/dist/esm/input/Input.js +26 -12
  72. package/dist/esm/input/Number.js +24 -12
  73. package/dist/esm/input/Password.js +26 -16
  74. package/dist/esm/input/Radio.js +15 -6
  75. package/dist/esm/input/RadioGroup.js +11 -2
  76. package/dist/esm/input/{Range.js → RangeSlider.js} +16 -8
  77. package/dist/esm/input/Search.js +35 -25
  78. package/dist/esm/input/Slider.js +12 -6
  79. package/dist/esm/input/Switch.js +20 -9
  80. package/dist/esm/input/Textarea.js +30 -9
  81. package/dist/esm/internal/ErrorIcon.js +3 -3
  82. package/dist/esm/internal/FieldWrapper.js +11 -4
  83. package/dist/esm/internal/InputWrapper.js +30 -23
  84. package/dist/esm/select/AutoComplete.js +23 -14
  85. package/dist/esm/select/DualList.js +35 -13
  86. package/dist/esm/select/List.js +50 -22
  87. package/dist/esm/select/Options.js +49 -42
  88. package/dist/esm/select/Select.js +22 -11
  89. package/dist/esm/select/useSelect.js +23 -12
  90. package/{src/input/ColorPicker.module.css → dist/esm/types/schema.js} +18 -27
  91. package/dist/esm/utils/eventHandlers.js +15 -5
  92. package/dist/types/builder/FileField.d.ts +4 -2
  93. package/dist/types/builder/FormField.d.ts +3 -1
  94. package/dist/types/builder/SchemaEditor.d.ts +9 -0
  95. package/dist/types/builder/components/AddSchemaFields.d.ts +2 -0
  96. package/dist/types/builder/components/SchemaArrayForm.d.ts +2 -0
  97. package/dist/types/builder/components/SchemaField.d.ts +10 -0
  98. package/dist/types/form/Controller.d.ts +47 -2
  99. package/dist/types/form/Form.d.ts +49 -6
  100. package/dist/types/hooks/useFileUploader.d.ts +16 -0
  101. package/dist/types/hooks/useFormBuilder.d.ts +38 -1
  102. package/dist/types/index.d.ts +6 -3
  103. package/dist/types/input/ArrayInput.d.ts +69 -12
  104. package/dist/types/input/AvatarInput.d.ts +23 -3
  105. package/dist/types/input/Checkbox.d.ts +13 -4
  106. package/dist/types/input/ColorInput.d.ts +15 -2
  107. package/dist/types/input/ColorPicker.d.ts +11 -1
  108. package/dist/types/input/DateInput.d.ts +28 -0
  109. package/dist/types/input/Field.d.ts +5 -1
  110. package/dist/types/input/File.d.ts +9 -0
  111. package/dist/types/input/Hidden.d.ts +13 -1
  112. package/dist/types/input/Input.d.ts +9 -1
  113. package/dist/types/input/Number.d.ts +9 -1
  114. package/dist/types/input/Password.d.ts +7 -1
  115. package/dist/types/input/Radio.d.ts +9 -1
  116. package/dist/types/input/RadioGroup.d.ts +8 -1
  117. package/dist/types/input/RangeSlider.d.ts +67 -0
  118. package/dist/types/input/Search.d.ts +14 -1
  119. package/dist/types/input/Slider.d.ts +8 -2
  120. package/dist/types/input/Switch.d.ts +10 -2
  121. package/dist/types/input/Textarea.d.ts +15 -1
  122. package/dist/types/internal/FieldWrapper.d.ts +1 -1
  123. package/dist/types/internal/InputWrapper.d.ts +1 -1
  124. package/dist/types/ranger/index.d.ts +2 -2
  125. package/dist/types/select/AutoComplete.d.ts +8 -1
  126. package/dist/types/select/DualList.d.ts +21 -7
  127. package/dist/types/select/List.d.ts +9 -1
  128. package/dist/types/select/Options.d.ts +4 -3
  129. package/dist/types/select/Select.d.ts +9 -1
  130. package/dist/types/select/useSelect.d.ts +10 -6
  131. package/dist/types/types/index.d.ts +38 -10
  132. package/dist/types/types/schema.d.ts +78 -33
  133. package/dist/types/utils/eventHandlers.d.ts +2 -2
  134. package/package.json +22 -20
  135. package/src/builder/FileField.tsx +56 -30
  136. package/src/builder/FormField.tsx +52 -32
  137. package/src/builder/SchemaEditor.tsx +79 -0
  138. package/src/builder/components/AddSchemaFields.tsx +75 -0
  139. package/src/builder/components/SchemaArrayForm.tsx +155 -0
  140. package/src/builder/components/SchemaField.tsx +435 -0
  141. package/src/form/Controller.tsx +48 -3
  142. package/src/form/Form.tsx +84 -13
  143. package/src/hooks/useFileUploader.ts +16 -0
  144. package/src/hooks/useFormBuilder.tsx +70 -19
  145. package/src/i18n/ar.json +5 -4
  146. package/src/i18n/en.json +5 -5
  147. package/src/index.ts +12 -3
  148. package/src/input/ArrayInput.tsx +195 -66
  149. package/src/input/AvatarInput.tsx +66 -18
  150. package/src/input/Checkbox.tsx +22 -9
  151. package/src/input/ColorInput.tsx +128 -72
  152. package/src/input/ColorPicker.tsx +54 -72
  153. package/src/input/DateInput.tsx +260 -0
  154. package/src/input/Field.tsx +15 -11
  155. package/src/input/File.tsx +18 -16
  156. package/src/input/Hidden.tsx +17 -2
  157. package/src/input/Input.tsx +27 -13
  158. package/src/input/Number.tsx +22 -13
  159. package/src/input/Password.tsx +28 -18
  160. package/src/input/Radio.tsx +15 -6
  161. package/src/input/RadioGroup.tsx +12 -3
  162. package/src/input/{Range.tsx → RangeSlider.tsx} +17 -9
  163. package/src/input/Search.tsx +36 -25
  164. package/src/input/Slider.tsx +20 -11
  165. package/src/input/Switch.tsx +17 -6
  166. package/src/input/Textarea.tsx +34 -9
  167. package/src/internal/ErrorIcon.tsx +3 -3
  168. package/src/internal/FieldWrapper.tsx +8 -4
  169. package/src/internal/InputWrapper.tsx +56 -30
  170. package/src/select/AutoComplete.tsx +16 -6
  171. package/src/select/DualList.tsx +40 -22
  172. package/src/select/List.tsx +52 -8
  173. package/src/select/Options.tsx +50 -46
  174. package/src/select/Select.tsx +29 -10
  175. package/src/select/useSelect.tsx +26 -9
  176. package/src/types/index.ts +42 -10
  177. package/src/types/schema.ts +96 -41
  178. package/src/utils/eventHandlers.ts +14 -4
  179. package/dist/cjs/input/ArrayInput.module.css +0 -1
  180. package/dist/cjs/input/ColorPicker.module.css +0 -1
  181. package/dist/cjs/input/File.module.css +0 -1
  182. package/dist/cjs/internal/Field.module.css +0 -1
  183. package/dist/esm/input/ArrayInput.module.css +0 -1
  184. package/dist/esm/input/ColorPicker.module.css +0 -1
  185. package/dist/esm/input/File.module.css +0 -1
  186. package/dist/esm/internal/Field.module.css +0 -1
  187. package/dist/types/input/Range.d.ts +0 -59
@@ -21,38 +21,48 @@
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 { yupResolver } from "@hookform/resolvers/yup";
25
+ import { EmptyContent } from "@react-fabric/core";
24
26
  import { isEmpty, isNil, yup } from "@react-fabric/utilities";
25
27
  import { Fragment, useMemo } from "react";
26
28
  import { FormField } from "../builder/FormField";
27
- import { type FormSchema } from "../types/schema";
29
+ import { type BaseSelectProps } from "../types";
30
+ import { DATA_TYPES, type FormSchema, type SchemaDef } from "../types/schema";
28
31
  import { type UploadHandler } from "./useFileUploader";
29
32
 
30
- const makeYupField = (field: FormSchema) => {
31
- let yupField: AnyObject = yup.string();
32
- if (field.datatype === "schema") {
33
+ const makeYupField = (field: SchemaDef) => {
34
+ let yupField: AnyObject = yup.string().trim();
35
+ if (field.datatype === DATA_TYPES.SCHEMA) {
33
36
  const child: yup.ObjectShape = Object.fromEntries(
34
37
  field.schema.map((field) => [field.id, makeYupField(field)]),
35
38
  );
36
39
  yupField = yup.object(child);
37
40
  }
38
- if (field.datatype === "boolean") yupField = yup.boolean();
41
+ if (field.datatype === DATA_TYPES.BOOL) {
42
+ yupField = yup.boolean();
43
+ if (field.required)
44
+ yupField = yupField.test({
45
+ test: (value: boolean) => !!value,
46
+ });
47
+ }
39
48
  if (
40
- field.datatype === "decimal" ||
41
- field.datatype === "number" ||
42
- field.datatype === "range"
49
+ field.datatype === DATA_TYPES.NUMBER ||
50
+ field.datatype === DATA_TYPES.RANGE
43
51
  ) {
44
52
  yupField = yup.number();
45
53
  !isNil(field.min) && (yupField = yupField.min(field.min));
46
54
  !isNil(field.max) && (yupField = yupField.max(field.max));
47
55
 
48
- if (field.datatype === "range") yupField = yup.array(yupField);
56
+ if (field.datatype === DATA_TYPES.RANGE) yupField = yup.array(yupField);
49
57
  }
50
- if (field.datatype === "string") {
51
- field.options && (yupField = yupField.oneOf(field.options));
58
+ if (field.datatype === DATA_TYPES.STRING) {
59
+ field.options &&
60
+ !field.allowCustom &&
61
+ (yupField = yupField.oneOf(field.options));
52
62
  field.regex && (yupField = yupField.matches(new RegExp(field.regex)));
53
63
  }
54
64
 
55
- if (field.datatype === "file") {
65
+ if (field.datatype === DATA_TYPES.FILE) {
56
66
  yupField = yup
57
67
  .object({
58
68
  filename: yup.string().required(),
@@ -83,34 +93,71 @@ const makeYupField = (field: FormSchema) => {
83
93
  return yupField.label(field.label);
84
94
  };
85
95
 
86
- export const useFormBuilder = (
87
- schema: FormSchema[],
96
+ /**
97
+ * useFormBuilder hook to create a form definition based on a schema.
98
+ * It generates a Yup validation schema and a form definition component.
99
+ *
100
+ * @param {FormSchema} schema - The schema defining the form fields.
101
+ * @param {Object} options - Optional parameters for the form builder.
102
+ * @returns An object containing the form definition and the Yup validation schema.
103
+ *
104
+ * @example
105
+ * ```jsx
106
+ * const { formDef, schemaDef } = useFormBuilder(schema, {
107
+ * prefix: "form",
108
+ * inline: true,
109
+ * fileUrl: (path) => `/files/${path}`,
110
+ * uploadHandler: async (file) => {
111
+ * // handle file upload
112
+ * },
113
+ * optionLists: {
114
+ * // define option lists for select fields
115
+ * }
116
+ * * });
117
+ * <FormBuilder
118
+ * schema={schemaDef}
119
+ * onSubmit={(data) => console.log(data)}
120
+ * >
121
+ * {formDef}
122
+ * </FormBuilder>
123
+ * ```
124
+ */
125
+ export const useFormBuilder = <T extends AnyObject = string>(
126
+ schema: FormSchema,
88
127
  options: {
89
128
  prefix?: string;
90
129
  inline?: boolean;
91
130
  fileUrl?: (path: string) => string;
92
131
  uploadHandler?: UploadHandler;
132
+ optionLists?: KeyValue<BaseSelectProps<T>>;
93
133
  } = {},
94
134
  ) => {
95
135
  const filteredSchema = useMemo(
96
136
  () => schema.filter?.((schm) => !!schm.id) ?? [],
97
137
  [schema],
98
138
  );
99
- const schemaDef = yup.object(
100
- Object.fromEntries(
101
- filteredSchema.map((field) => [field.id, makeYupField(field)]),
102
- ),
139
+ const schemaDef = useMemo(
140
+ () =>
141
+ yup.object(
142
+ Object.fromEntries(
143
+ filteredSchema.map((field) => [field.id, makeYupField(field)]),
144
+ ),
145
+ ),
146
+ [filteredSchema],
103
147
  );
104
148
 
149
+ const resolver = useMemo(() => yupResolver(schemaDef), [schemaDef]);
150
+
105
151
  const formDef = (
106
152
  <Fragment>
107
153
  {filteredSchema.map((field, idx) => (
108
154
  <div className="mb-4" key={field.id}>
109
- {field.datatype === "schema" ? null : (
155
+ {field.datatype === DATA_TYPES.SCHEMA ? null : (
110
156
  <FormField
111
157
  {...field}
112
158
  autoFocus={idx === 0}
113
159
  fileUrl={options.fileUrl}
160
+ optionLists={options.optionLists}
114
161
  uploadHandler={options.uploadHandler}
115
162
  inline={options.inline}
116
163
  prefix={options.prefix}
@@ -118,11 +165,15 @@ export const useFormBuilder = (
118
165
  )}
119
166
  </div>
120
167
  ))}
168
+ {filteredSchema.length === 0 && (
169
+ <EmptyContent message="No schema defined" />
170
+ )}
121
171
  </Fragment>
122
172
  );
123
173
 
124
174
  return {
125
175
  formDef,
176
+ resolver,
126
177
  schemaDef,
127
178
  };
128
179
  };
package/src/i18n/ar.json CHANGED
@@ -1,17 +1,18 @@
1
1
  {
2
2
  "addArray": "اضافة عنصر",
3
3
  "password": {
4
- "show": "عرض كلمة المرور",
5
- "hide": "اخفاء كلمة المرور"
4
+ "hide": "اخفاء كلمة المرور",
5
+ "show": "عرض كلمة المرور"
6
6
  },
7
7
  "select": {
8
+ "createOption": "إنشاء خيار جديد ل ",
8
9
  "emptyList": "لا توجد خيارات متاحة",
9
- "notFound": "لم يتم العثور على خيارات ل ",
10
- "createOption": "إنشاء خيار جديد ل "
10
+ "notFound": "لم يتم العثور على خيارات ل "
11
11
  },
12
12
  "yup": {
13
13
  "arrayMax": "{{label}} حدد ما يصل {{max}} غرض",
14
14
  "arrayMin": "{{label}} اختر على الأقل {{min}} غرض",
15
+ "empty": "{{label}} يجب ألا يكون فارغًا",
15
16
  "invalid": "{{label}} نوع بيانات غير صالح",
16
17
  "invalidType": "{{label}} غير صالح",
17
18
  "numberMax": "{{label}} الحد الأقصى {{max}}",
package/src/i18n/en.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "addArray": "Add Item",
3
3
  "password": {
4
- "show": "Show password",
5
- "hide": "Hide password"
4
+ "hide": "Hide password",
5
+ "show": "Show password"
6
6
  },
7
7
  "select": {
8
+ "createOption": "Create new option for <0>{{query}}</0>",
8
9
  "emptyList": "No options available",
9
- "notFound": "No options found for <0>{{query}}</0>",
10
- "createOption": "Create new option for <0>{{query}}</0>"
10
+ "notFound": "No options found for <0>{{query}}</0>"
11
11
  },
12
12
  "yup": {
13
13
  "arrayMax": "{{label}} must contain upto {{max}} item",
14
14
  "arrayMin": "{{label}} must contain atleast {{min}} item",
15
- "invalid": "{{label}} invalid",
16
15
  "empty": "{{label}} must not be empty",
16
+ "invalid": "{{label}} invalid",
17
17
  "invalidType": "{{label}} invalid datatype",
18
18
  "numberMax": "{{label}} maximum {{max}}",
19
19
  "numberMin": "{{label}} minimum {{min}}",
package/src/index.ts CHANGED
@@ -24,17 +24,26 @@
24
24
  import "./i18n";
25
25
 
26
26
  export { Controller } from "./form/Controller";
27
- export { Form, type FormRef } from "./form/Form";
27
+ export { Form, useForm, type FormRef } from "./form/Form";
28
+
29
+ export { SchemaEditor } from "./builder/SchemaEditor";
28
30
 
29
31
  export { useFileUploader } from "./hooks/useFileUploader";
30
32
  export { useFormBuilder } from "./hooks/useFormBuilder";
31
33
 
32
- export { type FormSchema } from "./types/schema";
34
+ export {
35
+ DATA_TYPES,
36
+ SCHEMA_CUSTOM_LIST,
37
+ type FormSchema,
38
+ type SchemaDef,
39
+ } from "./types/schema";
33
40
 
34
41
  export { ArrayInput } from "./input/ArrayInput";
42
+ export { AvatarInput } from "./input/AvatarInput";
35
43
  export { Checkbox } from "./input/Checkbox";
36
44
  export { ColorInput } from "./input/ColorInput";
37
45
  export { ColorPicker } from "./input/ColorPicker";
46
+ export { DateInput } from "./input/DateInput";
38
47
  export { Field } from "./input/Field";
39
48
  export { FileInput } from "./input/File";
40
49
  export { HiddenInput } from "./input/Hidden";
@@ -43,7 +52,7 @@ export { Number } from "./input/Number";
43
52
  export { Password } from "./input/Password";
44
53
  export { Radio } from "./input/Radio";
45
54
  export { RadioGroup } from "./input/RadioGroup";
46
- export { Range } from "./input/Range";
55
+ export { RangeSlider } from "./input/RangeSlider";
47
56
  export { Search } from "./input/Search";
48
57
  export { Slider } from "./input/Slider";
49
58
  export { Switch } from "./input/Switch";
@@ -23,27 +23,27 @@
23
23
 
24
24
  import { DndContext, type DragEndEvent } from "@dnd-kit/core";
25
25
  import { SortableContext, useSortable } from "@dnd-kit/sortable";
26
- import { Button, CoreIcons, Icon, Tooltip } from "@react-fabric/core";
27
- import { isString } from "@react-fabric/utilities";
26
+ import { Button, cloneChildren, Icon, Tooltip } from "@react-fabric/core";
27
+ import { isFalse, isString } from "@react-fabric/utilities";
28
28
  import classNames from "classnames";
29
29
  import {
30
- Children,
31
30
  cloneElement,
32
31
  useCallback,
32
+ useEffect,
33
33
  useImperativeHandle,
34
34
  useMemo,
35
+ useState,
35
36
  type PropsWithChildren,
36
- type ReactNode,
37
- type RefObject,
37
+ type Ref,
38
38
  } from "react";
39
39
  import { useFieldArray, useFormContext } from "react-hook-form";
40
40
  import { useTranslation } from "react-i18next";
41
41
  import { FieldWrapper } from "../internal/FieldWrapper";
42
- import classes from "./ArrayInput.module.css";
43
42
 
44
43
  import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
45
44
  import { CSS } from "@dnd-kit/utilities";
46
- export interface ArrayInputProps {
45
+
46
+ export interface ArrayInputProps<T extends AnyObject = string> {
47
47
  /**
48
48
  * input label
49
49
  */
@@ -77,6 +77,10 @@ export interface ArrayInputProps {
77
77
  * minimum required length
78
78
  */
79
79
  minItems?: number;
80
+ /**
81
+ * maximum length
82
+ */
83
+ maxItems?: number;
80
84
  /**
81
85
  * field name to focus on add
82
86
  */
@@ -87,23 +91,35 @@ export interface ArrayInputProps {
87
91
  enableSorting?: boolean;
88
92
  children:
89
93
  | React.ReactNode
90
- | ((props: { index: number; name: string }) => ReactNode);
94
+ | ((props: { index: number; name: string; item: T }) => React.ReactNode);
91
95
  /**
92
96
  * add item button label
93
97
  */
94
98
  addLabel?: string;
95
- /**
96
- * fixed length list
97
- */
98
- fixedList?: boolean;
99
- arrayRef?: RefObject<{
100
- addItem: (item: AnyObject) => void;
99
+ buttonPosition?: "top" | "bottom" | "both";
100
+ arrayRef?: Ref<{
101
+ addItem: (item: T) => void;
101
102
  removeItem: (idx: number) => void;
102
103
  }>;
103
104
  /**
104
- * Add item
105
+ * add new item
106
+ */
107
+ onAdd?: () => T;
108
+ /**
109
+ * on item remove
110
+ */
111
+ onRemove?: (item: T, idx: number) => void;
112
+ /**
113
+ * before item remove
105
114
  */
106
- onAdd?: () => AnyObject;
115
+ onBeforeRemove?: (item: T, idx: number) => Promise<boolean> | boolean;
116
+ /**
117
+ * can remove item
118
+ */
119
+ canRemove?:
120
+ | false
121
+ | "newonly"
122
+ | ((props: { item: T; index: number; lastItem: boolean }) => boolean);
107
123
 
108
124
  /**
109
125
  * field width
@@ -119,20 +135,63 @@ export interface ArrayInputProps {
119
135
  labelWidth?: string;
120
136
  }
121
137
 
122
- export const ArrayInput = ({
138
+ /**
139
+ * ArrayInput component for managing an array of items in a form.
140
+ * It allows adding, removing, and sorting items within the array.
141
+ * It integrates with react-hook-form for form state management and validation.
142
+ * It supports fixed-length lists, custom add/remove handlers, and can display validation errors.
143
+ * It also provides options for inline display and custom button positioning.
144
+ *
145
+ * It uses the `useFieldArray` hook from `react-hook-form` to manage the array state and provides a simple interface for adding and removing items.
146
+ *
147
+ * It also supports drag-and-drop sorting using `@dnd-kit/core` and `@dnd-kit/sortable` for a better user experience when reordering items.
148
+ *
149
+ * @param {ArrayInputProps<T>} props - The properties for the ArrayInput component.
150
+ * @returns {JSX.Element} The rendered ArrayInput component.
151
+ *
152
+ * @example
153
+ * ```jsx
154
+ * <ArrayInput
155
+ * name="items"
156
+ * label="Items"
157
+ * addLabel="Add Item"
158
+ * onAdd={() => ({ name: "", value: "" })}
159
+ * onRemove={(item, index) => console.log("Removed item:", item, "at index:", index)}
160
+ * onBeforeRemove={(item, index) => confirm(`Are you sure you want to remove item at index ${index}?`)}
161
+ * canRemove={({ item, index, lastItem }) => !lastItem} // Prevent removing the last item
162
+ * >
163
+ * {({ index, name }) => (
164
+ * <Input
165
+ * name={name}
166
+ * label={`Item ${index + 1}`}
167
+ * placeholder="Enter item value"
168
+ * required={index === 0} // Make the first item required
169
+ * />
170
+ * )}
171
+ * </ArrayInput>
172
+ * ```
173
+ *
174
+ * @see {@link https://react-hook-form.com/api/usefieldarray} for more details on `useFieldArray`.
175
+ * @see {@link https://react-dnd.github.io/react-dnd/about} for more details on DnD context and sorting.
176
+ */
177
+ export const ArrayInput = <T extends AnyObject = string>({
123
178
  name,
124
179
  children,
125
180
  addLabel,
126
- fixedList,
127
181
  focusName = "",
182
+ buttonPosition = "bottom",
128
183
  onAdd,
184
+ onRemove,
185
+ onBeforeRemove,
186
+ canRemove,
129
187
  arrayRef,
130
188
  enableSorting,
131
189
  disabled = false,
132
190
  readOnly = false,
133
191
  minItems = 0,
192
+ maxItems = Number.MAX_SAFE_INTEGER,
134
193
  ...rest
135
- }: ArrayInputProps) => {
194
+ }: ArrayInputProps<T>) => {
136
195
  const { t } = useTranslation("form");
137
196
  const form = useFormContext();
138
197
 
@@ -141,9 +200,15 @@ export const ArrayInput = ({
141
200
 
142
201
  const { fields, append, remove, move } = useFieldArray({
143
202
  name,
203
+ keyName: "__ID__",
144
204
  control: form.control,
145
205
  });
146
206
 
207
+ const [initialList, setInitialList] = useState<string[]>([]);
208
+ useEffect(() => {
209
+ setInitialList(fields.map((f) => f.__ID__));
210
+ }, []);
211
+
147
212
  const error = useMemo<AnyObject>(
148
213
  () =>
149
214
  form.formState.errors[name]?.message ??
@@ -151,8 +216,27 @@ export const ArrayInput = ({
151
216
  [form.formState.errors],
152
217
  );
153
218
 
219
+ const removeCheck = useCallback(
220
+ (item: T, index: number) => {
221
+ if (canRemove === false) return false;
222
+ if (canRemove === "newonly") {
223
+ return !initialList.includes((item as any).__ID__);
224
+ }
225
+ if (typeof canRemove === "function") {
226
+ return canRemove({
227
+ item,
228
+ index,
229
+ lastItem: index + 1 === fields.length,
230
+ });
231
+ }
232
+ return true;
233
+ },
234
+ [canRemove, initialList, fields],
235
+ );
236
+
154
237
  const handleAdd = useCallback(
155
- (item: AnyObject) => {
238
+ (item?: T) => {
239
+ if (!item) return;
156
240
  append(item);
157
241
  setTimeout(() => {
158
242
  form.setFocus(
@@ -163,16 +247,30 @@ export const ArrayInput = ({
163
247
  [name, fields, focusName],
164
248
  );
165
249
 
250
+ const handleRemove = useCallback(
251
+ (item: T, index: number) => {
252
+ const ret = onBeforeRemove?.(item, index);
253
+ void Promise.resolve(ret).then((b) => {
254
+ if (!isFalse(b)) {
255
+ remove(index);
256
+ onRemove?.(item, index);
257
+ }
258
+ });
259
+ },
260
+ [remove, onRemove, onBeforeRemove],
261
+ );
262
+
166
263
  useImperativeHandle(arrayRef, () => ({
167
264
  addItem: handleAdd,
168
265
  removeItem: remove,
169
266
  }));
170
267
 
171
268
  const Wrapper = useCallback(
172
- ({ children }: PropsWithChildren) => {
269
+ // eslint-disable-next-line @typescript-eslint/promise-function-async
270
+ ({ children }: { children: React.ReactNode }) => {
173
271
  if (enableSorting) {
174
272
  // pass id list to dnd context
175
- const idMap = fields.map((item) => item.id);
273
+ const idMap = fields.map((item) => item.__ID__);
176
274
  const handleDragEnd = (e: DragEndEvent) => {
177
275
  if (e.over && e.active.id !== e.over.id) {
178
276
  // find index of item and drop over
@@ -224,7 +322,7 @@ export const ArrayInput = ({
224
322
  >
225
323
  <Icon
226
324
  className={isDragging ? "cursor-grabbing" : "cursor-grab"}
227
- icon={CoreIcons.dragHandle}
325
+ icon="icon-[mdi--drag-vertical]"
228
326
  {...attributes}
229
327
  {...listeners}
230
328
  />
@@ -240,22 +338,81 @@ export const ArrayInput = ({
240
338
  // @ts-expect-error ignore
241
339
  SortableItem.displayName = "SortableItem";
242
340
 
341
+ const addButton = useMemo(
342
+ () => (
343
+ <div className="flex justify-end">
344
+ <Tooltip
345
+ color="danger-200"
346
+ content={
347
+ isString(error)
348
+ ? `${error}`
349
+ : (t(
350
+ error?.key,
351
+ error?.values
352
+ ? {
353
+ ...error?.values,
354
+ label: t(
355
+ error.values?.label ?? "form:badkey",
356
+ error.values?.path,
357
+ ),
358
+ }
359
+ : {},
360
+ ) as string)
361
+ }
362
+ >
363
+ <Button
364
+ size="sm"
365
+ aria-label="Add item"
366
+ icon="icon-[mdi--plus-circle-outline]"
367
+ onClick={() => handleAdd(onAdd?.())}
368
+ data-invalid={!!error}
369
+ disabled={!!disabled || readOnly || fields.length > maxItems}
370
+ className={classNames(
371
+ "fabric-addButton",
372
+ canRemove !== false && "me-10",
373
+ )}
374
+ >
375
+ {addLabel ?? t("form:addArray")}
376
+ </Button>
377
+ </Tooltip>
378
+ </div>
379
+ ),
380
+ [
381
+ error,
382
+ fields,
383
+ handleAdd,
384
+ canRemove,
385
+ disabled,
386
+ readOnly,
387
+ addLabel,
388
+ maxItems,
389
+ ],
390
+ );
391
+
243
392
  return (
244
- <div className={classes.arrayInput}>
245
- <FieldWrapper {...rest}>
393
+ <div className={"fabric-arrayInput"}>
394
+ <FieldWrapper
395
+ {...rest}
396
+ appendLabel={
397
+ <div className="-me-2">
398
+ {onAdd && buttonPosition !== "bottom" && addButton}
399
+ </div>
400
+ }
401
+ >
246
402
  <Wrapper>
247
403
  {fields.map((item, index) => (
248
404
  <SortableItem
249
- key={item.id}
250
- id={item.id}
405
+ key={item.__ID__}
406
+ id={item.__ID__}
251
407
  className="flex items-center flex-nowrap gap-2 mb-1"
252
408
  >
253
409
  {typeof children === "function"
254
410
  ? children({
255
411
  index,
412
+ item: item as any,
256
413
  name: `${name}.${index}`,
257
414
  })
258
- : Children.map(children, (child: AnyObject) =>
415
+ : cloneChildren(children, (child: AnyObject) =>
259
416
  cloneElement(
260
417
  child,
261
418
  Object.assign(
@@ -277,55 +434,27 @@ export const ArrayInput = ({
277
434
  ),
278
435
  ),
279
436
  )}
280
- {!fixedList && (
437
+ {canRemove !== false && (
281
438
  <Button
282
439
  aria-label="Remove item"
283
- icon={CoreIcons.remove}
440
+ icon="icon-[mdi--minus-circle-outline]"
284
441
  color="danger"
285
442
  variant="link"
286
443
  className="self-end"
287
- disabled={disabled || readOnly || fields.length <= minItems}
288
- onClick={() => remove(index)}
444
+ disabled={
445
+ disabled ||
446
+ readOnly ||
447
+ fields.length <= minItems ||
448
+ !removeCheck(item as any, index)
449
+ }
450
+ onClick={() => handleRemove(item as any, index)}
289
451
  />
290
452
  )}
291
453
  </SortableItem>
292
454
  ))}
293
455
  </Wrapper>
294
456
  </FieldWrapper>
295
- {!fixedList && onAdd && (
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
- )}
457
+ {onAdd && buttonPosition !== "top" && addButton}
329
458
  </div>
330
459
  );
331
460
  };