@alepha/ui 0.16.0 → 0.16.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.
Files changed (152) hide show
  1. package/dist/admin/{AdminApiKeys-GMORg-1l.js → AdminApiKeys-CoTOTfgU.js} +4 -3
  2. package/dist/admin/{AdminApiKeys-GMORg-1l.js.map → AdminApiKeys-CoTOTfgU.js.map} +1 -1
  3. package/dist/admin/{AdminAudits-pkWrjq1Z.js → AdminAudits-BmsxFbDa.js} +4 -3
  4. package/dist/admin/{AdminAudits-pkWrjq1Z.js.map → AdminAudits-BmsxFbDa.js.map} +1 -1
  5. package/dist/admin/{AdminFiles-WeQbsCsl.js → AdminFiles-BBB8knca.js} +4 -3
  6. package/dist/admin/{AdminFiles-WeQbsCsl.js.map → AdminFiles-BBB8knca.js.map} +1 -1
  7. package/dist/admin/{AdminJobs-B-q9iGO3.js → AdminJobs-C604joTz.js} +4 -3
  8. package/dist/admin/{AdminJobs-B-q9iGO3.js.map → AdminJobs-C604joTz.js.map} +1 -1
  9. package/dist/admin/{AdminLayout-D8yZ-8lG.js → AdminLayout-CsjvpeD1.js} +6 -10
  10. package/dist/admin/AdminLayout-CsjvpeD1.js.map +1 -0
  11. package/dist/admin/{AdminNotifications-Ds5Un0NJ.js → AdminNotifications-LwR6RKrx.js} +4 -3
  12. package/dist/admin/{AdminNotifications-Ds5Un0NJ.js.map → AdminNotifications-LwR6RKrx.js.map} +1 -1
  13. package/dist/admin/AdminParameters-B_83Vie9.js +767 -0
  14. package/dist/admin/AdminParameters-B_83Vie9.js.map +1 -0
  15. package/dist/admin/{AdminSessions-DzIOxM3b.js → AdminSessions-CWnPosdd.js} +4 -3
  16. package/dist/admin/{AdminSessions-DzIOxM3b.js.map → AdminSessions-CWnPosdd.js.map} +1 -1
  17. package/dist/admin/{AdminUserAudits-CiUPN2BC.js → AdminUserAudits-nHv636E_.js} +4 -3
  18. package/dist/admin/{AdminUserAudits-CiUPN2BC.js.map → AdminUserAudits-nHv636E_.js.map} +1 -1
  19. package/dist/admin/{AdminUserCreate-BwQKr4xE.js → AdminUserCreate-CjYD3Kjc.js} +4 -3
  20. package/dist/admin/{AdminUserCreate-BwQKr4xE.js.map → AdminUserCreate-CjYD3Kjc.js.map} +1 -1
  21. package/dist/admin/{AdminUserDetails-uqtC5aJ1.js → AdminUserDetails-Ccq-LsZ0.js} +4 -3
  22. package/dist/admin/{AdminUserDetails-uqtC5aJ1.js.map → AdminUserDetails-Ccq-LsZ0.js.map} +1 -1
  23. package/dist/admin/{AdminUserLayout-CiPay35T.js → AdminUserLayout-7s41DiF_.js} +6 -7
  24. package/dist/admin/AdminUserLayout-7s41DiF_.js.map +1 -0
  25. package/dist/admin/{AdminUserSessions-DAE8Nf1F.js → AdminUserSessions-Ds3ODq_d.js} +4 -3
  26. package/dist/admin/{AdminUserSessions-DAE8Nf1F.js.map → AdminUserSessions-Ds3ODq_d.js.map} +1 -1
  27. package/dist/admin/{AdminUserSettings-EbahaV2a.js → AdminUserSettings-CGh4gROo.js} +4 -3
  28. package/dist/admin/{AdminUserSettings-EbahaV2a.js.map → AdminUserSettings-CGh4gROo.js.map} +1 -1
  29. package/dist/admin/{AdminUsers-Dcjh0KNW.js → AdminUsers-CvPiBzQK.js} +4 -3
  30. package/dist/admin/{AdminUsers-Dcjh0KNW.js.map → AdminUsers-CvPiBzQK.js.map} +1 -1
  31. package/dist/admin/index.d.ts +22 -10
  32. package/dist/admin/index.d.ts.map +1 -1
  33. package/dist/admin/index.js +47 -48
  34. package/dist/admin/index.js.map +1 -1
  35. package/dist/admin/rolldown-runtime-CjeV3_4I.js +18 -0
  36. package/dist/auth/{AuthLayout-mFOWbiSP.js → AuthLayout-CdJcrPs4.js} +2 -4
  37. package/dist/auth/AuthLayout-CdJcrPs4.js.map +1 -0
  38. package/dist/{demo/IconGoogle-CbBF8Hqq.js → auth/IconGoogle-Bm18QD2q.js} +2 -4
  39. package/dist/auth/{IconGoogle-DpSlPZ1u.js.map → IconGoogle-Bm18QD2q.js.map} +1 -1
  40. package/dist/auth/{Login-BBqTosqZ.js → Login-DS_OqA0G.js} +7 -6
  41. package/dist/auth/Login-DS_OqA0G.js.map +1 -0
  42. package/dist/auth/{Profile-Bxj8Nwom.js → Profile-Di7N7HZL.js} +2 -3
  43. package/dist/auth/{Profile-Bxj8Nwom.js.map → Profile-Di7N7HZL.js.map} +1 -1
  44. package/dist/auth/{Register-Ce675Crg.js → Register-BRR2_gux.js} +7 -6
  45. package/dist/auth/Register-BRR2_gux.js.map +1 -0
  46. package/dist/auth/{ResetPassword-DWdt7c40.js → ResetPassword-oQu72lod.js} +4 -3
  47. package/dist/auth/{ResetPassword-DWdt7c40.js.map → ResetPassword-oQu72lod.js.map} +1 -1
  48. package/dist/auth/{VerifyEmail-CI4JwByV.js → VerifyEmail-DC6HPZjd.js} +4 -3
  49. package/dist/auth/{VerifyEmail-CI4JwByV.js.map → VerifyEmail-DC6HPZjd.js.map} +1 -1
  50. package/dist/auth/index.d.ts +14 -14
  51. package/dist/auth/index.d.ts.map +1 -1
  52. package/dist/auth/index.js +13 -17
  53. package/dist/auth/index.js.map +1 -1
  54. package/dist/auth/rolldown-runtime-CjeV3_4I.js +18 -0
  55. package/dist/core/index.d.ts +147 -68
  56. package/dist/core/index.d.ts.map +1 -1
  57. package/dist/core/index.js +349 -287
  58. package/dist/core/index.js.map +1 -1
  59. package/dist/demo/{DemoDataTable-CguplbR7.js → DemoDataTable-DCsJq8v5.js} +4 -5
  60. package/dist/demo/DemoDataTable-DCsJq8v5.js.map +1 -0
  61. package/dist/demo/{DemoHome-Cce2bWmg.js → DemoHome-DpRrPlBC.js} +4 -3
  62. package/dist/demo/{DemoHome-Cce2bWmg.js.map → DemoHome-DpRrPlBC.js.map} +1 -1
  63. package/dist/demo/{DemoJsonViewer-Dgdk3Txb.js → DemoJsonViewer-zeucGKHV.js} +6 -5
  64. package/dist/demo/DemoJsonViewer-zeucGKHV.js.map +1 -0
  65. package/dist/demo/{DemoLayout-B20TEuhV.js → DemoLayout-PhgbAAiQ.js} +6 -5
  66. package/dist/demo/DemoLayout-PhgbAAiQ.js.map +1 -0
  67. package/dist/demo/{DemoLogin-CvCG2WVh.js → DemoLogin-DSzP0Lkv.js} +8 -10
  68. package/dist/demo/DemoLogin-DSzP0Lkv.js.map +1 -0
  69. package/dist/demo/{DemoRegister-CmeHbOAs.js → DemoRegister-DavFBsCz.js} +8 -10
  70. package/dist/demo/DemoRegister-DavFBsCz.js.map +1 -0
  71. package/dist/demo/{DemoResetPassword-CKO5iA_6.js → DemoResetPassword-BS2rIAQK.js} +5 -7
  72. package/dist/demo/DemoResetPassword-BS2rIAQK.js.map +1 -0
  73. package/dist/demo/{DemoSidebar-MVmQKfMt.js → DemoSidebar-zNkUmHRl.js} +4 -5
  74. package/dist/demo/DemoSidebar-zNkUmHRl.js.map +1 -0
  75. package/dist/demo/{DemoTypeForm-w-qtfRlC.js → DemoTypeForm-B9q7oT0b.js} +4 -5
  76. package/dist/demo/DemoTypeForm-B9q7oT0b.js.map +1 -0
  77. package/dist/demo/{DemoVerifyEmail-C8FFJT5A.js → DemoVerifyEmail-Bi4SdWz0.js} +5 -7
  78. package/dist/demo/DemoVerifyEmail-Bi4SdWz0.js.map +1 -0
  79. package/dist/{auth/IconGoogle-DpSlPZ1u.js → demo/IconGoogle-CTeZyrek.js} +2 -4
  80. package/dist/demo/{IconGoogle-CbBF8Hqq.js.map → IconGoogle-CTeZyrek.js.map} +1 -1
  81. package/dist/demo/{Showcase-CQrMWars.js → Showcase-C9btr_SJ.js} +3 -5
  82. package/dist/demo/Showcase-C9btr_SJ.js.map +1 -0
  83. package/dist/demo/index.d.ts +2 -2
  84. package/dist/demo/index.d.ts.map +1 -1
  85. package/dist/demo/index.js +15 -15
  86. package/dist/demo/rolldown-runtime-CjeV3_4I.js +18 -0
  87. package/package.json +5 -3
  88. package/src/admin/AdminRouter.ts +15 -24
  89. package/src/admin/components/AdminLayout.tsx +6 -10
  90. package/src/admin/components/parameters/AdminParameters.tsx +154 -76
  91. package/src/admin/components/parameters/ParameterDetails.tsx +153 -93
  92. package/src/admin/components/parameters/ParameterEmptyState.tsx +27 -0
  93. package/src/admin/components/parameters/ParameterHistory.tsx +15 -20
  94. package/src/admin/components/parameters/ParameterTree.tsx +280 -104
  95. package/src/admin/components/parameters/types.ts +3 -3
  96. package/src/admin/primitives/$uiAdmin.ts +2 -2
  97. package/src/auth/AuthRouter.ts +1 -4
  98. package/src/auth/components/AuthLayout.tsx +0 -1
  99. package/src/core/components/buttons/ActionButton.tsx +4 -15
  100. package/src/core/components/buttons/DarkModeButton.tsx +8 -4
  101. package/src/core/components/buttons/ToggleSidebarButton.tsx +3 -5
  102. package/src/core/components/form/Control.tsx +10 -32
  103. package/src/core/components/form/ControlArray.tsx +200 -89
  104. package/src/core/components/form/TypeForm.browser.spec.tsx +727 -0
  105. package/src/core/components/layout/AlephaMantineProvider.tsx +1 -0
  106. package/src/core/components/layout/Breadcrumb.tsx +91 -0
  107. package/src/core/components/layout/{AdminShell.tsx → DashboardShell.tsx} +77 -32
  108. package/src/core/components/layout/Sidebar.tsx +58 -18
  109. package/src/core/constants/ui.ts +1 -1
  110. package/src/core/helpers/renderIcon.tsx +5 -2
  111. package/src/core/index.ts +9 -5
  112. package/src/core/styles.css +7 -6
  113. package/src/core/utils/string.ts +28 -4
  114. package/src/demo/components/DemoLayout.tsx +6 -2
  115. package/dist/admin/AdminApiKeys-DsmGnHNh.js +0 -3
  116. package/dist/admin/AdminAudits-8SM96viT.js +0 -3
  117. package/dist/admin/AdminFiles-B56ocq4H.js +0 -3
  118. package/dist/admin/AdminJobs-CED1syCn.js +0 -3
  119. package/dist/admin/AdminLayout-D8yZ-8lG.js.map +0 -1
  120. package/dist/admin/AdminNotifications-B0B1rdc4.js +0 -3
  121. package/dist/admin/AdminParameters-BU3lATdJ.js +0 -3
  122. package/dist/admin/AdminParameters-CfDUpc78.js +0 -575
  123. package/dist/admin/AdminParameters-CfDUpc78.js.map +0 -1
  124. package/dist/admin/AdminSessions-BDGK2MS6.js +0 -3
  125. package/dist/admin/AdminUserAudits-Cj79gENT.js +0 -3
  126. package/dist/admin/AdminUserCreate-Cq-mUmBs.js +0 -3
  127. package/dist/admin/AdminUserDetails-DRjVAPFd.js +0 -3
  128. package/dist/admin/AdminUserLayout-CGzmHHby.js +0 -3
  129. package/dist/admin/AdminUserLayout-CiPay35T.js.map +0 -1
  130. package/dist/admin/AdminUserSessions-DcdzuNZ9.js +0 -3
  131. package/dist/admin/AdminUserSettings-D7V6-ceX.js +0 -3
  132. package/dist/admin/AdminUsers-D9nyzGqQ.js +0 -3
  133. package/dist/auth/AuthLayout-mFOWbiSP.js.map +0 -1
  134. package/dist/auth/Login-BBqTosqZ.js.map +0 -1
  135. package/dist/auth/Login-CoU63mMR.js +0 -4
  136. package/dist/auth/Register-BV_oa_AK.js +0 -4
  137. package/dist/auth/Register-Ce675Crg.js.map +0 -1
  138. package/dist/auth/ResetPassword-D5wC8GAA.js +0 -3
  139. package/dist/auth/VerifyEmail-DAfqVm5s.js +0 -3
  140. package/dist/demo/DemoDataTable-CguplbR7.js.map +0 -1
  141. package/dist/demo/DemoHome-DC9qkMNe.js +0 -3
  142. package/dist/demo/DemoJsonViewer-DIssGVlJ.js +0 -4
  143. package/dist/demo/DemoJsonViewer-Dgdk3Txb.js.map +0 -1
  144. package/dist/demo/DemoLayout-B20TEuhV.js.map +0 -1
  145. package/dist/demo/DemoLayout-DSRyf4qJ.js +0 -3
  146. package/dist/demo/DemoLogin-CvCG2WVh.js.map +0 -1
  147. package/dist/demo/DemoRegister-CmeHbOAs.js.map +0 -1
  148. package/dist/demo/DemoResetPassword-CKO5iA_6.js.map +0 -1
  149. package/dist/demo/DemoSidebar-MVmQKfMt.js.map +0 -1
  150. package/dist/demo/DemoTypeForm-w-qtfRlC.js.map +0 -1
  151. package/dist/demo/DemoVerifyEmail-C8FFJT5A.js.map +0 -1
  152. package/dist/demo/Showcase-CQrMWars.js.map +0 -1
@@ -264,40 +264,18 @@ const Control = (_props: ControlProps) => {
264
264
  "type" in props.input.schema &&
265
265
  props.input.schema.type === "boolean"
266
266
  ) {
267
- if (props.switch) {
268
- const switchProps = typeof props.switch === "object" ? props.switch : {};
269
-
270
- return (
271
- <Switch
272
- {...inputProps}
273
- id={id}
274
- color={"blue"}
275
- defaultChecked={props.input.props.defaultValue}
276
- onChange={(event) => {
277
- props.input.set(event.currentTarget.checked);
278
- }}
279
- {...switchProps}
280
- />
281
- );
282
- }
283
-
284
- // by default, render as <Select/> with Yes/No/Empty options
285
- const selectProps: Partial<ControlSelectProps> = {
286
- loader: async () => [
287
- { value: "true", label: "Yes" },
288
- { value: "false", label: "No" },
289
- { value: "", label: "" },
290
- ],
291
- ...props.input.props,
292
- };
267
+ const switchProps = typeof props.switch === "object" ? props.switch : {};
293
268
 
294
269
  return (
295
- <ControlSelect
296
- input={props.input}
297
- title={props.title}
298
- description={props.description}
299
- icon={icon}
300
- {...selectProps}
270
+ <Switch
271
+ {...inputProps}
272
+ id={id}
273
+ color={"blue"}
274
+ defaultChecked={props.input.props.defaultValue}
275
+ onChange={(event) => {
276
+ props.input.set(event.currentTarget.checked);
277
+ }}
278
+ {...switchProps}
301
279
  />
302
280
  );
303
281
  }
@@ -9,9 +9,9 @@ import {
9
9
  } from "@mantine/core";
10
10
  import { IconGripVertical, IconPlus, IconTrash } from "@tabler/icons-react";
11
11
  import type { TObject, TSchema } from "alepha";
12
- import { useEvents } from "alepha/react";
12
+ import { useAlepha } from "alepha/react";
13
13
  import type { BaseInputField } from "alepha/react/form";
14
- import { useRef, useState } from "react";
14
+ import { useCallback, useEffect, useRef, useState } from "react";
15
15
  import { ui } from "../../constants/ui.ts";
16
16
  import {
17
17
  type GenericControlProps,
@@ -19,6 +19,14 @@ import {
19
19
  } from "../../utils/parseInput.ts";
20
20
  import Control, { type ControlProps } from "./Control.tsx";
21
21
 
22
+ /**
23
+ * Represents an array item with a stable key for React reconciliation.
24
+ */
25
+ interface ArrayItem {
26
+ key: number;
27
+ value: any;
28
+ }
29
+
22
30
  export interface ControlArrayProps extends GenericControlProps {
23
31
  /**
24
32
  * Minimum number of items allowed.
@@ -70,6 +78,169 @@ export interface ControlArrayProps extends GenericControlProps {
70
78
  sortable?: boolean;
71
79
  }
72
80
 
81
+ /**
82
+ * Custom hook to sync array items with form state.
83
+ * Uses form events as the source of truth, eliminating dual-state issues.
84
+ */
85
+ const useArrayItems = (
86
+ input: BaseInputField | undefined,
87
+ ): {
88
+ items: ArrayItem[];
89
+ setItems: (items: ArrayItem[]) => void;
90
+ nextKey: () => number;
91
+ } => {
92
+ const alepha = useAlepha();
93
+ const keyCounter = useRef(0);
94
+
95
+ // Initialize from defaultValue
96
+ const [items, setItemsState] = useState<ArrayItem[]>(() => {
97
+ const defaultValue = input?.props?.defaultValue;
98
+ if (Array.isArray(defaultValue)) {
99
+ return defaultValue.map((value) => ({
100
+ key: keyCounter.current++,
101
+ value,
102
+ }));
103
+ }
104
+ return [];
105
+ });
106
+
107
+ // Sync form value to local state
108
+ const syncFromFormValue = useCallback((formValue: any[] | undefined) => {
109
+ if (!Array.isArray(formValue)) {
110
+ setItemsState([]);
111
+ return;
112
+ }
113
+
114
+ // Preserve keys for existing items where possible
115
+ setItemsState((prevItems) => {
116
+ // If lengths match and values are same references, keep existing keys
117
+ if (prevItems.length === formValue.length) {
118
+ const allSame = prevItems.every(
119
+ (item, i) => item.value === formValue[i],
120
+ );
121
+ if (allSame) return prevItems;
122
+ }
123
+
124
+ // Otherwise, create new items with fresh keys
125
+ keyCounter.current = 0;
126
+ return formValue.map((value) => ({
127
+ key: keyCounter.current++,
128
+ value,
129
+ }));
130
+ });
131
+ }, []);
132
+
133
+ // Listen for form changes and reset events
134
+ useEffect(() => {
135
+ if (!input?.form) return;
136
+
137
+ const formId = input.form.id;
138
+ const fieldPath = input.path;
139
+
140
+ const listeners = [
141
+ // Handle form reset
142
+ alepha.events.on("form:reset", (event) => {
143
+ if (event.id === formId) {
144
+ const defaultValue = input.props?.defaultValue;
145
+ keyCounter.current = 0;
146
+ if (Array.isArray(defaultValue)) {
147
+ setItemsState(
148
+ defaultValue.map((value) => ({
149
+ key: keyCounter.current++,
150
+ value,
151
+ })),
152
+ );
153
+ } else {
154
+ setItemsState([]);
155
+ }
156
+ }
157
+ }),
158
+
159
+ // Handle external value changes (e.g., programmatic updates)
160
+ alepha.events.on("form:change", (event) => {
161
+ if (event.id === formId && event.path === fieldPath) {
162
+ // Value was changed externally, sync our state
163
+ syncFromFormValue(event.value);
164
+ }
165
+ }),
166
+ ];
167
+
168
+ return () => {
169
+ for (const unsub of listeners) {
170
+ unsub();
171
+ }
172
+ };
173
+ }, [alepha, input, syncFromFormValue]);
174
+
175
+ // Update form when items change
176
+ const setItems = useCallback(
177
+ (newItems: ArrayItem[]) => {
178
+ setItemsState(newItems);
179
+ // Update form value - this will trigger form:change but we'll detect it's from us
180
+ input?.set(newItems.map((item) => item.value));
181
+ },
182
+ [input],
183
+ );
184
+
185
+ const nextKey = useCallback(() => keyCounter.current++, []);
186
+
187
+ return { items, setItems, nextKey };
188
+ };
189
+
190
+ /**
191
+ * Creates a proper InputField for an array item that integrates with the form system.
192
+ * Uses array index for test IDs to ensure predictable, testable element identifiers.
193
+ */
194
+ const createArrayItemInput = (
195
+ parentInput: BaseInputField,
196
+ itemSchema: TSchema,
197
+ index: number,
198
+ _itemKey: number,
199
+ value: any,
200
+ onValueChange: (value: any) => void,
201
+ ): BaseInputField => {
202
+ return {
203
+ schema: itemSchema,
204
+ path: `${parentInput.path}/${index}`,
205
+ required: false,
206
+ form: parentInput.form,
207
+ props: {
208
+ id: `${parentInput.props.id}-${index}`,
209
+ name: `${parentInput.props.name}[${index}]`,
210
+ defaultValue: value,
211
+ },
212
+ set: onValueChange,
213
+ };
214
+ };
215
+
216
+ /**
217
+ * Creates a proper InputField for a nested object field within an array item.
218
+ * Uses array index for test IDs to ensure predictable, testable element identifiers.
219
+ */
220
+ const createArrayItemFieldInput = (
221
+ parentInput: BaseInputField,
222
+ itemSchema: TObject,
223
+ fieldName: string,
224
+ index: number,
225
+ _itemKey: number,
226
+ itemValue: any,
227
+ onFieldChange: (field: string, value: any) => void,
228
+ ): BaseInputField => {
229
+ const fieldSchema = itemSchema.properties[fieldName];
230
+ return {
231
+ schema: fieldSchema,
232
+ path: `${parentInput.path}/${index}/${fieldName}`,
233
+ required: itemSchema.required?.includes(fieldName) ?? false,
234
+ form: parentInput.form,
235
+ props: {
236
+ id: `${parentInput.props.id}-${index}-${fieldName}`,
237
+ name: `${parentInput.props.name}[${index}].${fieldName}`,
238
+ defaultValue: itemValue?.[fieldName],
239
+ },
240
+ set: (value: any) => onFieldChange(fieldName, value),
241
+ };
242
+ };
243
+
73
244
  /**
74
245
  * ControlArray component for editing arrays of schema items.
75
246
  *
@@ -79,6 +250,7 @@ export interface ControlArrayProps extends GenericControlProps {
79
250
  * - Supports arrays of primitives
80
251
  * - Grid layout for object items
81
252
  * - Min/max constraints
253
+ * - Syncs with form state (handles external updates and resets)
82
254
  *
83
255
  * @example
84
256
  * ```tsx
@@ -102,42 +274,7 @@ export interface ControlArrayProps extends GenericControlProps {
102
274
  */
103
275
  const ControlArray = (props: ControlArrayProps) => {
104
276
  const { inputProps } = parseInput(props, {});
105
- const idCounter = useRef(0);
106
-
107
- // Initialize items with unique keys for React
108
- const [items, setItems] = useState<Array<{ key: number; value: any }>>(() => {
109
- const defaultValue = props.input?.props?.defaultValue;
110
- if (Array.isArray(defaultValue)) {
111
- return defaultValue.map((value) => ({
112
- key: idCounter.current++,
113
- value,
114
- }));
115
- }
116
- return [];
117
- });
118
-
119
- // Listen for form reset events
120
- useEvents(
121
- {
122
- "form:reset": (event) => {
123
- if (event.id === props.input?.form?.id) {
124
- const defaultValue = props.input?.props?.defaultValue;
125
- if (Array.isArray(defaultValue)) {
126
- idCounter.current = 0;
127
- setItems(
128
- defaultValue.map((value) => ({
129
- key: idCounter.current++,
130
- value,
131
- })),
132
- );
133
- } else {
134
- setItems([]);
135
- }
136
- }
137
- },
138
- },
139
- [props.input],
140
- );
277
+ const { items, setItems, nextKey } = useArrayItems(props.input);
141
278
 
142
279
  if (!props.input?.props) {
143
280
  return null;
@@ -148,14 +285,10 @@ const ControlArray = (props: ControlArrayProps) => {
148
285
  return null;
149
286
  }
150
287
 
151
- const itemSchema = (schema as any).items as TSchema;
288
+ const itemSchema = (schema as { items: TSchema }).items;
152
289
  const isObjectItem = itemSchema && "properties" in itemSchema;
153
290
  const { min = 0, max = Number.POSITIVE_INFINITY, columns = 1 } = props;
154
291
 
155
- const updateFormValue = (newItems: Array<{ key: number; value: any }>) => {
156
- props.input.set(newItems.map((item) => item.value));
157
- };
158
-
159
292
  const handleAdd = () => {
160
293
  if (items.length >= max) return;
161
294
 
@@ -174,23 +307,18 @@ const ControlArray = (props: ControlArrayProps) => {
174
307
  newValue = "";
175
308
  }
176
309
 
177
- const newItems = [...items, { key: idCounter.current++, value: newValue }];
178
- setItems(newItems);
179
- updateFormValue(newItems);
310
+ setItems([...items, { key: nextKey(), value: newValue }]);
180
311
  };
181
312
 
182
313
  const handleRemove = (index: number) => {
183
314
  if (items.length <= min) return;
184
- const newItems = items.filter((_, i) => i !== index);
185
- setItems(newItems);
186
- updateFormValue(newItems);
315
+ setItems(items.filter((_, i) => i !== index));
187
316
  };
188
317
 
189
318
  const handleItemChange = (index: number, value: any) => {
190
319
  const newItems = [...items];
191
320
  newItems[index] = { ...newItems[index], value };
192
321
  setItems(newItems);
193
- updateFormValue(newItems);
194
322
  };
195
323
 
196
324
  const handleFieldChange = (index: number, field: string, value: any) => {
@@ -200,12 +328,12 @@ const ControlArray = (props: ControlArrayProps) => {
200
328
  value: { ...newItems[index].value, [field]: value },
201
329
  };
202
330
  setItems(newItems);
203
- updateFormValue(newItems);
204
331
  };
205
332
 
206
333
  const colSpan = 12 / columns;
207
- const fieldNames = isObjectItem
208
- ? Object.keys((itemSchema as TObject).properties)
334
+ const objectItemSchema = isObjectItem ? (itemSchema as TObject) : null;
335
+ const fieldNames = objectItemSchema
336
+ ? Object.keys(objectItemSchema.properties)
209
337
  : [];
210
338
 
211
339
  const renderItems = () => (
@@ -229,34 +357,23 @@ const ControlArray = (props: ControlArrayProps) => {
229
357
  </ActionIcon>
230
358
  )}
231
359
 
232
- {isObjectItem ? (
360
+ {objectItemSchema ? (
233
361
  <Grid style={{ flex: 1 }} gutter="sm">
234
362
  {fieldNames.map((fieldName) => {
235
- const fieldSchema = (itemSchema as TObject).properties[
236
- fieldName
237
- ];
238
363
  const fieldControlProps = props.controlProps?.[fieldName] ?? {};
239
-
240
- // Create a virtual InputField for the nested property
241
- const virtualInput: BaseInputField = {
242
- schema: fieldSchema,
243
- props: {
244
- id: `${props.input.props.id}-${item.key}-${fieldName}`,
245
- name: `${props.input.props.name}[${index}].${fieldName}`,
246
- defaultValue: item.value?.[fieldName],
247
- },
248
- path: `${props.input.path}/${index}/${fieldName}`,
249
- required:
250
- (itemSchema as TObject).required?.includes(fieldName) ??
251
- false,
252
- form: props.input.form,
253
- set: (value: any) =>
254
- handleFieldChange(index, fieldName, value),
255
- };
364
+ const fieldInput = createArrayItemFieldInput(
365
+ props.input,
366
+ objectItemSchema,
367
+ fieldName,
368
+ index,
369
+ item.key,
370
+ item.value,
371
+ (field, value) => handleFieldChange(index, field, value),
372
+ );
256
373
 
257
374
  return (
258
375
  <Grid.Col key={fieldName} span={colSpan}>
259
- <Control input={virtualInput} {...fieldControlProps} />
376
+ <Control input={fieldInput} {...fieldControlProps} />
260
377
  </Grid.Col>
261
378
  );
262
379
  })}
@@ -264,20 +381,14 @@ const ControlArray = (props: ControlArrayProps) => {
264
381
  ) : (
265
382
  <Flex style={{ flex: 1 }}>
266
383
  <Control
267
- input={
268
- {
269
- schema: itemSchema,
270
- props: {
271
- id: `${props.input.props.id}-${item.key}`,
272
- name: `${props.input.props.name}[${index}]`,
273
- defaultValue: item.value,
274
- },
275
- path: `${props.input.path}/${index}`,
276
- required: false,
277
- form: props.input.form,
278
- set: (value: any) => handleItemChange(index, value),
279
- } as BaseInputField
280
- }
384
+ input={createArrayItemInput(
385
+ props.input,
386
+ itemSchema,
387
+ index,
388
+ item.key,
389
+ item.value,
390
+ (value) => handleItemChange(index, value),
391
+ )}
281
392
  {...props.itemControlProps}
282
393
  />
283
394
  </Flex>