@react-fabric/form 1.0.0-beta.9 → 1.0.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.
Files changed (215) hide show
  1. package/{src/input/ArrayInput.module.css → css/_arrayInput.css} +8 -8
  2. package/css/_colorPicker.css +95 -0
  3. package/{src/internal/Field.module.css → css/_field.css} +94 -33
  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 +107 -78
  7. package/dist/cjs/builder/FormField.js +60 -39
  8. package/dist/cjs/builder/SchemaEditor.js +72 -0
  9. package/dist/cjs/builder/components/AddSchemaFields.js +64 -0
  10. package/dist/cjs/builder/components/SchemaArrayForm.js +146 -0
  11. package/dist/cjs/builder/components/SchemaField.js +423 -0
  12. package/dist/cjs/form/Controller.js +66 -19
  13. package/dist/cjs/form/Form.js +97 -39
  14. package/dist/cjs/form/useForm.js +32 -0
  15. package/dist/cjs/hooks/useFileUploader.js +74 -45
  16. package/dist/cjs/hooks/useFormBuilder.js +85 -31
  17. package/dist/cjs/i18n/{ar.json.js → ar/form.json.js} +5 -4
  18. package/dist/cjs/i18n/{en.json.js → en/form.json.js} +5 -5
  19. package/dist/cjs/i18n/index.js +4 -4
  20. package/dist/cjs/index.js +41 -2
  21. package/dist/cjs/input/ArrayInput.js +259 -135
  22. package/dist/cjs/input/AvatarInput.js +99 -63
  23. package/dist/cjs/input/Checkbox.js +24 -25
  24. package/dist/cjs/input/ColorInput.js +138 -81
  25. package/dist/cjs/input/ColorPicker.js +78 -95
  26. package/dist/cjs/input/DateInput.js +192 -0
  27. package/dist/cjs/input/Field.js +21 -20
  28. package/dist/cjs/input/File.js +85 -80
  29. package/dist/cjs/input/Hidden.js +27 -13
  30. package/dist/cjs/input/Input.js +49 -48
  31. package/dist/cjs/input/Number.js +46 -44
  32. package/dist/cjs/input/Password.js +51 -49
  33. package/dist/cjs/input/Radio.js +26 -16
  34. package/dist/cjs/input/RadioGroup.js +26 -11
  35. package/dist/cjs/input/{Range.js → RangeSlider.js} +61 -66
  36. package/dist/cjs/input/Search.js +58 -64
  37. package/dist/cjs/input/Slider.js +57 -66
  38. package/dist/cjs/input/Switch.js +29 -27
  39. package/dist/cjs/input/Textarea.js +65 -53
  40. package/dist/cjs/internal/ErrorIcon.js +9 -9
  41. package/dist/cjs/internal/FieldWrapper.js +23 -13
  42. package/dist/cjs/internal/InputWrapper.js +85 -87
  43. package/dist/cjs/ranger/index.js +7 -7
  44. package/dist/cjs/ranger/utils.js +9 -4
  45. package/dist/cjs/select/AutoComplete.js +93 -76
  46. package/dist/cjs/select/DualList.js +235 -154
  47. package/dist/cjs/select/List.js +122 -89
  48. package/dist/cjs/select/Options.js +58 -54
  49. package/dist/cjs/select/Select.js +119 -89
  50. package/dist/cjs/select/helpers.js +5 -5
  51. package/dist/cjs/select/useSelect.js +83 -58
  52. package/dist/cjs/types/schema.js +43 -0
  53. package/dist/cjs/utils/eventHandlers.js +16 -6
  54. package/dist/cjs/utils/getColor.js +6 -6
  55. package/dist/cjs/utils/getProps.js +49 -0
  56. package/dist/esm/builder/FileField.js +109 -80
  57. package/dist/esm/builder/FormField.js +60 -39
  58. package/dist/esm/builder/SchemaEditor.js +72 -0
  59. package/dist/esm/builder/components/AddSchemaFields.js +64 -0
  60. package/dist/esm/builder/components/SchemaArrayForm.js +146 -0
  61. package/dist/esm/builder/components/SchemaField.js +423 -0
  62. package/dist/esm/form/Controller.js +66 -19
  63. package/dist/esm/form/Form.js +97 -39
  64. package/dist/esm/form/useForm.js +32 -0
  65. package/dist/esm/hooks/useFileUploader.js +75 -46
  66. package/dist/esm/hooks/useFormBuilder.js +84 -30
  67. package/dist/esm/i18n/{ar.json.js → ar/form.json.js} +5 -4
  68. package/dist/esm/i18n/{en.json.js → en/form.json.js} +5 -5
  69. package/dist/esm/i18n/index.js +2 -2
  70. package/dist/esm/index.js +36 -1
  71. package/dist/esm/input/ArrayInput.js +260 -136
  72. package/dist/esm/input/AvatarInput.js +99 -63
  73. package/dist/esm/input/Checkbox.js +26 -27
  74. package/dist/esm/input/ColorInput.js +141 -84
  75. package/dist/esm/input/ColorPicker.js +80 -97
  76. package/dist/esm/input/DateInput.js +192 -0
  77. package/dist/esm/input/Field.js +21 -20
  78. package/dist/esm/input/File.js +89 -84
  79. package/dist/esm/input/Hidden.js +28 -14
  80. package/dist/esm/input/Input.js +51 -50
  81. package/dist/esm/input/Number.js +48 -46
  82. package/dist/esm/input/Password.js +54 -52
  83. package/dist/esm/input/Radio.js +27 -17
  84. package/dist/esm/input/RadioGroup.js +27 -12
  85. package/dist/esm/input/{Range.js → RangeSlider.js} +62 -67
  86. package/dist/esm/input/Search.js +61 -67
  87. package/dist/esm/input/Slider.js +58 -67
  88. package/dist/esm/input/Switch.js +31 -29
  89. package/dist/esm/input/Textarea.js +67 -55
  90. package/dist/esm/internal/ErrorIcon.js +11 -11
  91. package/dist/esm/internal/FieldWrapper.js +23 -13
  92. package/dist/esm/internal/InputWrapper.js +88 -90
  93. package/dist/esm/ranger/index.js +7 -7
  94. package/dist/esm/ranger/utils.js +9 -4
  95. package/dist/esm/select/AutoComplete.js +95 -78
  96. package/dist/esm/select/DualList.js +233 -152
  97. package/dist/esm/select/List.js +125 -92
  98. package/dist/esm/select/Options.js +58 -54
  99. package/dist/esm/select/Select.js +122 -92
  100. package/dist/esm/select/helpers.js +5 -5
  101. package/dist/esm/select/useSelect.js +84 -59
  102. package/{src/input/ColorPicker.module.css → dist/esm/types/schema.js} +18 -27
  103. package/dist/esm/utils/eventHandlers.js +16 -6
  104. package/dist/esm/utils/getColor.js +6 -6
  105. package/dist/esm/utils/getProps.js +49 -0
  106. package/dist/types/builder/FileField.d.ts +7 -4
  107. package/dist/types/builder/FormField.d.ts +4 -2
  108. package/dist/types/builder/SchemaEditor.d.ts +6 -0
  109. package/dist/types/builder/components/AddSchemaFields.d.ts +2 -0
  110. package/dist/types/builder/components/SchemaArrayForm.d.ts +2 -0
  111. package/dist/types/builder/components/SchemaField.d.ts +12 -0
  112. package/dist/types/form/Controller.d.ts +45 -3
  113. package/dist/types/form/Form.d.ts +38 -7
  114. package/dist/types/form/useForm.d.ts +6 -0
  115. package/dist/types/hooks/useFileUploader.d.ts +24 -3
  116. package/dist/types/hooks/useFormBuilder.d.ts +41 -3
  117. package/dist/types/index.d.ts +32 -2
  118. package/dist/types/input/ArrayInput.d.ts +68 -13
  119. package/dist/types/input/AvatarInput.d.ts +23 -6
  120. package/dist/types/input/Checkbox.d.ts +12 -10
  121. package/dist/types/input/ColorInput.d.ts +15 -5
  122. package/dist/types/input/ColorPicker.d.ts +10 -2
  123. package/dist/types/input/DateInput.d.ts +21 -0
  124. package/dist/types/input/Field.d.ts +9 -36
  125. package/dist/types/input/File.d.ts +16 -4
  126. package/dist/types/input/Hidden.d.ts +7 -6
  127. package/dist/types/input/Input.d.ts +11 -4
  128. package/dist/types/input/Number.d.ts +9 -4
  129. package/dist/types/input/Password.d.ts +9 -4
  130. package/dist/types/input/Radio.d.ts +9 -4
  131. package/dist/types/input/RadioGroup.d.ts +14 -6
  132. package/dist/types/input/{Range.d.ts → RangeSlider.d.ts} +11 -6
  133. package/dist/types/input/Search.d.ts +14 -4
  134. package/dist/types/input/Slider.d.ts +9 -6
  135. package/dist/types/input/Switch.d.ts +13 -8
  136. package/dist/types/input/Textarea.d.ts +15 -4
  137. package/dist/types/internal/ErrorIcon.d.ts +4 -1
  138. package/dist/types/internal/FieldWrapper.d.ts +10 -1
  139. package/dist/types/internal/InputWrapper.d.ts +19 -1
  140. package/dist/types/ranger/index.d.ts +7 -3
  141. package/dist/types/ranger/utils.d.ts +2 -2
  142. package/dist/types/select/AutoComplete.d.ts +7 -2
  143. package/dist/types/select/DualList.d.ts +11 -37
  144. package/dist/types/select/List.d.ts +7 -2
  145. package/dist/types/select/Options.d.ts +8 -7
  146. package/dist/types/select/Select.d.ts +7 -2
  147. package/dist/types/select/helpers.d.ts +2 -2
  148. package/dist/types/select/useSelect.d.ts +10 -28
  149. package/dist/types/types/index.d.ts +73 -39
  150. package/dist/types/types/schema.d.ts +78 -32
  151. package/dist/types/utils/eventHandlers.d.ts +4 -4
  152. package/dist/types/utils/getColor.d.ts +4 -4
  153. package/dist/types/utils/getProps.d.ts +26 -0
  154. package/dist/types/utils/index.d.ts +1 -0
  155. package/package.json +25 -25
  156. package/src/builder/FileField.tsx +73 -56
  157. package/src/builder/FormField.tsx +66 -58
  158. package/src/builder/SchemaEditor.tsx +78 -0
  159. package/src/builder/components/AddSchemaFields.tsx +64 -0
  160. package/src/builder/components/SchemaArrayForm.tsx +146 -0
  161. package/src/builder/components/SchemaField.tsx +405 -0
  162. package/src/form/Controller.tsx +61 -24
  163. package/src/form/Form.tsx +74 -50
  164. package/src/form/useForm.ts +53 -0
  165. package/src/hooks/useFileUploader.ts +115 -84
  166. package/src/hooks/useFormBuilder.tsx +80 -41
  167. package/src/i18n/{ar.json → ar/form.json} +5 -4
  168. package/src/i18n/{en.json → en/form.json} +5 -5
  169. package/src/i18n/index.ts +2 -2
  170. package/src/index.ts +34 -20
  171. package/src/input/ArrayInput.tsx +255 -173
  172. package/src/input/AvatarInput.tsx +82 -39
  173. package/src/input/Checkbox.tsx +30 -54
  174. package/src/input/ColorInput.tsx +146 -105
  175. package/src/input/ColorPicker.tsx +76 -102
  176. package/src/input/DateInput.tsx +225 -0
  177. package/src/input/Field.tsx +23 -65
  178. package/src/input/File.tsx +76 -98
  179. package/src/input/Hidden.tsx +36 -24
  180. package/src/input/Input.tsx +55 -53
  181. package/src/input/Number.tsx +49 -54
  182. package/src/input/Password.tsx +61 -66
  183. package/src/input/Radio.tsx +28 -37
  184. package/src/input/RadioGroup.tsx +30 -25
  185. package/src/input/{Range.tsx → RangeSlider.tsx} +81 -109
  186. package/src/input/Search.tsx +64 -77
  187. package/src/input/Slider.tsx +80 -109
  188. package/src/input/Switch.tsx +39 -53
  189. package/src/input/Textarea.tsx +72 -59
  190. package/src/internal/ErrorIcon.tsx +22 -21
  191. package/src/internal/FieldWrapper.tsx +33 -23
  192. package/src/internal/InputWrapper.tsx +132 -143
  193. package/src/ranger/index.tsx +37 -117
  194. package/src/ranger/utils.ts +12 -11
  195. package/src/select/AutoComplete.tsx +113 -123
  196. package/src/select/DualList.tsx +218 -204
  197. package/src/select/List.tsx +130 -97
  198. package/src/select/Options.tsx +59 -57
  199. package/src/select/Select.tsx +148 -122
  200. package/src/select/helpers.ts +13 -8
  201. package/src/select/useSelect.tsx +110 -127
  202. package/src/types/index.ts +76 -45
  203. package/src/types/schema.ts +97 -40
  204. package/src/utils/eventHandlers.ts +24 -26
  205. package/src/utils/getColor.ts +9 -8
  206. package/src/utils/getProps.ts +47 -0
  207. package/src/utils/index.ts +1 -0
  208. package/dist/cjs/input/ArrayInput.module.css +0 -1
  209. package/dist/cjs/input/ColorPicker.module.css +0 -1
  210. package/dist/cjs/input/File.module.css +0 -1
  211. package/dist/cjs/internal/Field.module.css +0 -1
  212. package/dist/esm/input/ArrayInput.module.css +0 -1
  213. package/dist/esm/input/ColorPicker.module.css +0 -1
  214. package/dist/esm/input/File.module.css +0 -1
  215. package/dist/esm/internal/Field.module.css +0 -1
@@ -21,6 +21,9 @@
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
+ /* eslint-disable @eslint-react/static-components */
25
+
26
+ import { isEmpty } from "@react-fabric/utilities";
24
27
  import { useMemo } from "react";
25
28
  import { useTranslation } from "react-i18next";
26
29
  import { Controller } from "../form/Controller";
@@ -28,23 +31,38 @@ import { type UploadHandler } from "../hooks/useFileUploader";
28
31
  import { ArrayInput } from "../input/ArrayInput";
29
32
  import { AvatarInput } from "../input/AvatarInput";
30
33
  import { ColorInput } from "../input/ColorInput";
34
+ import { DateInput } from "../input/DateInput";
31
35
  import { Input } from "../input/Input";
32
36
  import { Number } from "../input/Number";
33
- import { Range } from "../input/Range";
37
+ import { RangeSlider } from "../input/RangeSlider";
34
38
  import { Switch } from "../input/Switch";
35
39
  import { Textarea } from "../input/Textarea";
36
40
  import { Select } from "../select/Select";
37
- import { type SchemaType } from "../types/schema";
41
+ import { type BaseSelectProps } from "../types";
42
+ import { DATA_TYPES, type SchemaType } from "../types/schema";
38
43
  import { FileField } from "./FileField";
39
44
 
40
- export const FormField = ({
45
+ const getField = (datatype: DATA_TYPES, hasOptions: boolean) => {
46
+ if (datatype === DATA_TYPES.NUMBER) return Number;
47
+ if (datatype === DATA_TYPES.RANGE) return RangeSlider;
48
+ if (datatype === DATA_TYPES.BOOL) return Switch;
49
+ if (datatype === DATA_TYPES.TEXT) return Textarea;
50
+ if (datatype === DATA_TYPES.DATE) return DateInput;
51
+ if (datatype === DATA_TYPES.DATETIME) return DateInput;
52
+ if (datatype === DATA_TYPES.COLOR) return ColorInput;
53
+ if (hasOptions) return Select;
54
+
55
+ return Input;
56
+ };
57
+
58
+ export function FormField<T extends AnyObject = string>({
41
59
  id,
42
60
  label,
43
61
  required,
44
62
  inline,
45
63
  prefix,
46
64
  autoFocus,
47
- defaultValue,
65
+ optionLists,
48
66
  fileUrl,
49
67
  uploadHandler,
50
68
  ...rest
@@ -52,41 +70,28 @@ export const FormField = ({
52
70
  inline?: boolean;
53
71
  prefix?: string;
54
72
  autoFocus?: boolean;
73
+ optionLists?: KeyValue<BaseSelectProps<T>>;
55
74
  fileUrl?: (path: string) => string;
56
75
  uploadHandler?: UploadHandler;
57
- }) => {
76
+ }) {
58
77
  const { t } = useTranslation();
59
- const fieldName = useMemo(
60
- () => [prefix, id].filter(Boolean).join("."),
61
- [id, prefix],
62
- );
63
-
64
- const E = useMemo(() => {
65
- if (rest.datatype === "number" || rest.datatype === "decimal")
66
- return Number;
67
- if (rest.datatype === "range") return Range;
68
- if (rest.datatype === "boolean") return Switch;
69
- if (rest.datatype === "text") return Textarea;
70
- if (rest.datatype === "color") return ColorInput;
71
- if (rest.datatype === "string" && rest.options) return Select;
78
+ const fieldName = useMemo(() => [prefix, id].filter(Boolean).join("."), [id, prefix]);
72
79
 
73
- return Input;
74
- }, [rest.datatype]);
80
+ const hasOptions = useMemo(
81
+ () =>
82
+ rest.datatype === DATA_TYPES.STRING &&
83
+ ((rest.optionList === "--CUSTOM--" && !isEmpty(rest.options)) || !isEmpty(optionLists?.[rest.optionList ?? ""])),
84
+ [rest, optionLists],
85
+ );
75
86
 
76
87
  const props = useMemo(() => {
77
- if (rest.datatype === "number")
88
+ if (rest.datatype === DATA_TYPES.NUMBER)
78
89
  return {
79
90
  min: rest.min,
80
91
  max: rest.max,
81
92
  step: rest.step ?? 1,
82
93
  };
83
- if (rest.datatype === "decimal")
84
- return {
85
- min: rest.min,
86
- max: rest.max,
87
- step: rest.step ?? 0.1,
88
- };
89
- if (rest.datatype === "range")
94
+ if (rest.datatype === DATA_TYPES.RANGE)
90
95
  return {
91
96
  min: rest.min ?? 1,
92
97
  max: rest.max ?? 99,
@@ -94,54 +99,57 @@ export const FormField = ({
94
99
  showValue: true,
95
100
  showLabels: true,
96
101
  };
97
- if (rest.datatype === "text") return { rows: 3 };
98
- if (rest.datatype === "color") return { showPicker: true };
102
+ if (rest.datatype === DATA_TYPES.DATE)
103
+ return {
104
+ min: rest.min,
105
+ max: rest.max,
106
+ type: "date",
107
+ };
108
+ if (rest.datatype === DATA_TYPES.DATETIME)
109
+ return {
110
+ min: rest.min,
111
+ max: rest.max,
112
+ type: "datetime",
113
+ };
114
+ if (rest.datatype === DATA_TYPES.TEXT) return { rows: 3 };
115
+ if (rest.datatype === DATA_TYPES.COLOR) return { showPicker: true };
99
116
 
100
- if (rest.datatype === "string" && rest.options)
117
+ if (rest.datatype === DATA_TYPES.STRING && hasOptions)
101
118
  return {
102
- options: rest.options,
103
119
  multiple: rest.multiple,
120
+ allowCreate: rest.allowCustom,
104
121
  searchable: true,
122
+ ...(optionLists?.[rest.optionList ?? ""] ?? { options: rest.options }),
105
123
  };
106
124
 
107
125
  return {};
108
- }, [rest]);
126
+ }, [rest, hasOptions, optionLists]);
109
127
 
110
- if (rest.datatype === "avatar")
128
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
129
+ const E = useMemo<AnyObject>(() => {
130
+ return getField(rest.datatype, hasOptions);
131
+ }, [rest.datatype, hasOptions]);
132
+
133
+ if (rest.datatype === DATA_TYPES.AVATAR)
111
134
  return (
112
135
  uploadHandler && (
113
136
  <Controller name={fieldName}>
114
- <AvatarInput
115
- uploadHandler={uploadHandler}
116
- fileUrl={fileUrl}
117
- name={fieldName}
118
- />
137
+ <AvatarInput uploadHandler={uploadHandler} fileUrl={fileUrl} name={fieldName} />
119
138
  </Controller>
120
139
  )
121
140
  );
122
141
 
123
- if (rest.datatype === "file")
142
+ if (rest.datatype === DATA_TYPES.FILE)
124
143
  return (
125
144
  uploadHandler && (
126
145
  <Controller name={fieldName}>
127
- <FileField
128
- uploadHandler={uploadHandler}
129
- inline={inline}
130
- label={label}
131
- id={id}
132
- {...rest}
133
- />
146
+ <FileField uploadHandler={uploadHandler} inline={inline} label={label} id={id} {...rest} />
134
147
  </Controller>
135
148
  )
136
149
  );
137
150
 
138
- return rest.multiple && !("options" in rest) ? (
139
- <ArrayInput
140
- name={fieldName}
141
- label={t(label, label)}
142
- required={required}
143
- onAdd={() => ""}
144
- >
151
+ return rest.multiple && !hasOptions ? (
152
+ <ArrayInput name={fieldName} label={t(`fb.${id}`, label || id)} required={required} onAdd={() => String("")}>
145
153
  <Controller>
146
154
  <E {...props} />
147
155
  </Controller>
@@ -150,12 +158,12 @@ export const FormField = ({
150
158
  <Controller name={fieldName}>
151
159
  <E
152
160
  {...props}
153
- autoFocus={autoFocus}
154
- label={t(label, label)}
155
- placeholder={t(label, label)}
161
+ {...{ autoFocus }}
162
+ label={t(`fb.${id}`, label || id)}
163
+ placeholder={t(`fb.${id}`, label || id)}
156
164
  required={required}
157
165
  inline={inline}
158
166
  />
159
167
  </Controller>
160
168
  );
161
- };
169
+ }
@@ -0,0 +1,78 @@
1
+ /*
2
+ * React Fabric
3
+ * @version: 1.0.0
4
+ *
5
+ *
6
+ * The MIT License (MIT)
7
+ * Copyright (c) 2025 Adarsh Pastakia
8
+ *
9
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software
10
+ * and associated documentation files (the "Software"), to deal in the Software without restriction,
11
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
12
+ * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
13
+ * subject to the following conditions:
14
+ *
15
+ * The above copyright notice and this permission notice shall be included in all copies or substantial
16
+ * portions of the Software.
17
+ *
18
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
19
+ * TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
20
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ */
23
+
24
+ import { Footer } from "@react-fabric/core";
25
+ import { EMPTY_ARRAY } from "@react-fabric/utilities";
26
+ import { useCallback, useEffect, useRef } from "react";
27
+ import { Form, type FormRef } from "../form/Form";
28
+ import { DATA_TYPES, type FormSchema, type SchemaEditorProps } from "../types/schema";
29
+ import { SchemaArrayForm } from "./components/SchemaArrayForm";
30
+
31
+ /**
32
+ * SchemaEditor component allows users to edit a schema definition.
33
+ * It provides a form interface to modify schema properties dynamically.
34
+ */
35
+ export function SchemaEditor({
36
+ schemaDef,
37
+ dynamic,
38
+ acceptableTypes,
39
+ optionLists = EMPTY_ARRAY,
40
+ children,
41
+ onSubmit,
42
+ onChange,
43
+ }: SchemaEditorProps) {
44
+ const formRef = useRef<FormRef<{ schema: FormSchema }>>(null);
45
+
46
+ const handleChange = useCallback(
47
+ (model: { schema: FormSchema }) => {
48
+ onChange?.(model.schema);
49
+ },
50
+ [onChange],
51
+ );
52
+
53
+ const handleSubmit = useCallback(
54
+ (model: { schema: FormSchema }) => {
55
+ onSubmit?.(model.schema);
56
+ },
57
+ [onSubmit],
58
+ );
59
+
60
+ useEffect(() => {
61
+ formRef.current?.setValues({ schema: schemaDef });
62
+ }, [schemaDef]);
63
+
64
+ return (
65
+ <Form formRef={formRef} onChange={handleChange} onSubmit={handleSubmit}>
66
+ <SchemaArrayForm
67
+ dynamic={dynamic}
68
+ optionLists={optionLists}
69
+ acceptableTypes={acceptableTypes ?? Object.values(DATA_TYPES)}
70
+ />
71
+ {children && (
72
+ <Footer flex className="border-t bg-default px-6 py-2">
73
+ {children}
74
+ </Footer>
75
+ )}
76
+ </Form>
77
+ );
78
+ }
@@ -0,0 +1,64 @@
1
+ /*
2
+ * React Fabric
3
+ * @version: 1.0.0
4
+ *
5
+ *
6
+ * The MIT License (MIT)
7
+ * Copyright (c) 2025 Adarsh Pastakia
8
+ *
9
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software
10
+ * and associated documentation files (the "Software"), to deal in the Software without restriction,
11
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
12
+ * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
13
+ * subject to the following conditions:
14
+ *
15
+ * The above copyright notice and this permission notice shall be included in all copies or substantial
16
+ * portions of the Software.
17
+ *
18
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
19
+ * TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
20
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ */
23
+
24
+ import { yupResolver } from "@hookform/resolvers/yup";
25
+ import { Button, Content, Footer, Modal, type ModalProps } from "@react-fabric/core";
26
+ import { yup } from "@react-fabric/utilities";
27
+ import { useCallback } from "react";
28
+ import { Controller } from "../../form/Controller";
29
+ import { Form } from "../../form/Form";
30
+ import { ArrayInput } from "../../input/ArrayInput";
31
+ import { Input } from "../../input/Input";
32
+
33
+ const fieldSchema = yup.object({
34
+ fields: yup.array(yup.string().trim().required()).required().min(1),
35
+ });
36
+
37
+ type FieldSchemaType = yup.InferType<typeof fieldSchema>;
38
+
39
+ export function AddSchemaFields(props: ModalProps) {
40
+ const handleSubmit = useCallback(
41
+ (model: FieldSchemaType) => {
42
+ props.onClose?.(model.fields.map((f: string) => f.split(",").map((ff) => ff.trim())).flat(2));
43
+ },
44
+ [props],
45
+ );
46
+
47
+ return (
48
+ <Modal {...props}>
49
+ <Form resolver={yupResolver(fieldSchema)} defaultValues={{ fields: [" "] }} onSubmit={handleSubmit}>
50
+ <Content>
51
+ <ArrayInput minItems={1} name="fields" onAdd={() => String("")}>
52
+ <Controller>
53
+ <Input {...{ autoFocus: true }} />
54
+ </Controller>
55
+ </ArrayInput>
56
+ <p className="text-sm text-muted">Provide list of fields or comma-separated value</p>
57
+ </Content>
58
+ <Footer flex justify="end" className="px-2 py-1">
59
+ <Button type="submit">Add</Button>
60
+ </Footer>
61
+ </Form>
62
+ </Modal>
63
+ );
64
+ }
@@ -0,0 +1,146 @@
1
+ /*
2
+ * React Fabric
3
+ * @version: 1.0.0
4
+ *
5
+ *
6
+ * The MIT License (MIT)
7
+ * Copyright (c) 2025 Adarsh Pastakia
8
+ *
9
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software
10
+ * and associated documentation files (the "Software"), to deal in the Software without restriction,
11
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
12
+ * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
13
+ * subject to the following conditions:
14
+ *
15
+ * The above copyright notice and this permission notice shall be included in all copies or substantial
16
+ * portions of the Software.
17
+ *
18
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
19
+ * TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
20
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ */
23
+
24
+ import { RestrictToVerticalAxis } from "@dnd-kit/abstract/modifiers";
25
+ import { DragDropProvider, type DragEndEvent } from "@dnd-kit/react";
26
+ import { Button, Content, Header, useOverlayService } from "@react-fabric/core";
27
+ import { isArray } from "@react-fabric/utilities";
28
+ import { useCallback, useState } from "react";
29
+ import { useFieldArray, useFormContext } from "react-hook-form";
30
+ import type { FormSchema, SchemaDef } from "../../types/schema";
31
+ import { DATA_TYPES, type SchemaEditorProps } from "../../types/schema";
32
+ import { AddSchemaFields } from "./AddSchemaFields";
33
+ import { SchemaField } from "./SchemaField";
34
+
35
+ function Wrapper({
36
+ children,
37
+ fields,
38
+ onMove,
39
+ }: {
40
+ children: React.ReactNode;
41
+ fields: KeyValue[];
42
+ onMove: (active: number, end: number) => void;
43
+ }) {
44
+ // pass id list to dnd context
45
+ const idMap = fields.map((item) => item.__ID__ as string);
46
+ const handleDragEnd = (e: DragEndEvent) => {
47
+ if (e.operation.source?.id !== e.operation.target?.id) {
48
+ // find index of item and drop over
49
+ const active = idMap.indexOf(`${e.operation.source?.id}`);
50
+ const end = idMap.indexOf(`${e.operation.target?.id}`);
51
+ onMove(active, end);
52
+ }
53
+ };
54
+ return (
55
+ <DragDropProvider onDragEnd={handleDragEnd} modifiers={[RestrictToVerticalAxis]}>
56
+ {children}
57
+ </DragDropProvider>
58
+ );
59
+ }
60
+
61
+ // Need to disable prop-types lint due to uknown lint error
62
+ export function SchemaArrayForm({ dynamic, acceptableTypes, optionLists }: Partial<SchemaEditorProps>) {
63
+ const form = useFormContext<{ schema: FormSchema }>();
64
+ const [defaultOpen, setDefaultOpen] = useState(true);
65
+ const { fields, append, remove, move } = useFieldArray({
66
+ name: "schema",
67
+ control: form.control,
68
+ });
69
+
70
+ const handleTypeChange = useCallback(
71
+ (index: number, val: SchemaDef) => {
72
+ form.setValue(`schema.${index}`, val);
73
+ },
74
+ [form],
75
+ );
76
+
77
+ const handleRemove = useCallback(
78
+ (index: number) => {
79
+ remove(index);
80
+ },
81
+ [remove],
82
+ );
83
+
84
+ const [renderModal, openAddfields] = useOverlayService<unknown, string[]>(AddSchemaFields);
85
+ const handleAdd = useCallback(() => {
86
+ void openAddfields().then((fields) => {
87
+ if (isArray(fields)) {
88
+ const existing = form.getValues().schema.map((field) => field.id);
89
+ fields.forEach((field) => {
90
+ if (!existing.includes(field)) {
91
+ append({
92
+ id: field,
93
+ datatype: DATA_TYPES.STRING,
94
+ label: "",
95
+ });
96
+ }
97
+ });
98
+ }
99
+ });
100
+ // eslint-disable-next-line @eslint-react/exhaustive-deps
101
+ }, []);
102
+
103
+ return (
104
+ <Wrapper onMove={move} fields={fields}>
105
+ <Header flex justify="end" className="gap-1 px-2">
106
+ {dynamic && (
107
+ <Button size="sm" variant="link" icon="icon-[mdi--plus-circle-outline]" aria-label="add field" onClick={handleAdd} />
108
+ )}
109
+ <Button
110
+ size="sm"
111
+ variant="link"
112
+ icon="icon-[mdi--arrow-collapse-vertical]"
113
+ aria-label="collapse all"
114
+ onClick={() => setDefaultOpen(false)}
115
+ />
116
+ <Button
117
+ size="sm"
118
+ variant="link"
119
+ icon="icon-[mdi--arrow-expand-vertical]"
120
+ aria-label="expand all"
121
+ onClick={() => setDefaultOpen(true)}
122
+ />
123
+ </Header>
124
+ <Content className="divide-y">
125
+ {fields.map((field, index) => (
126
+ <SchemaField
127
+ // eslint-disable-next-line @eslint-react/no-array-index-key
128
+ key={index}
129
+ index={index}
130
+ defaultOpen={defaultOpen}
131
+ id={form.getValues().schema[index].id}
132
+ fieldId={field.id}
133
+ field={`schema.${index}`}
134
+ dynamic={dynamic}
135
+ optionLists={optionLists}
136
+ onRemove={() => handleRemove(index)}
137
+ getModel={() => form.getValues().schema[index]}
138
+ onTypeChange={(val: SchemaDef) => handleTypeChange(index, val)}
139
+ acceptableTypes={acceptableTypes ?? Object.values(DATA_TYPES)}
140
+ />
141
+ ))}
142
+ </Content>
143
+ {renderModal()}
144
+ </Wrapper>
145
+ );
146
+ }