@alepha/ui 0.13.6 → 0.13.8
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/dist/admin/AdminAudits-CwvH8e8c.js +215 -0
- package/dist/admin/AdminAudits-CwvH8e8c.js.map +1 -0
- package/dist/admin/AdminAudits-Dv8Vk_6r.js +3 -0
- package/dist/admin/AdminFiles-5CPA3lQk.js +3 -0
- package/dist/admin/{AdminFiles-B_jfB_Py.js → AdminFiles-C_w1tb_x.js} +4 -3
- package/dist/admin/AdminFiles-C_w1tb_x.js.map +1 -0
- package/dist/admin/AdminLayout-BnSmtA4x.js +3 -0
- package/dist/admin/AdminLayout-XiSivwWH.js +39 -0
- package/dist/admin/AdminLayout-XiSivwWH.js.map +1 -0
- package/dist/admin/AdminNotifications-DLjmZWtf.js +3 -0
- package/dist/admin/{AdminNotifications-BFEjqpqx.js → AdminNotifications-DuYy74AN.js} +3 -3
- package/dist/admin/AdminNotifications-DuYy74AN.js.map +1 -0
- package/dist/admin/AdminParameters-DYg48Jwe.js +3 -0
- package/dist/admin/AdminParameters-YagqWTG3.js +575 -0
- package/dist/admin/AdminParameters-YagqWTG3.js.map +1 -0
- package/dist/admin/{AdminSessions-D7DESfWK.js → AdminSessions-BCjgJ-93.js} +4 -4
- package/dist/admin/AdminSessions-BCjgJ-93.js.map +1 -0
- package/dist/admin/AdminSessions-DEh2uN-4.js +3 -0
- package/dist/admin/AdminUserAudits-B_PUXCKC.js +177 -0
- package/dist/admin/AdminUserAudits-B_PUXCKC.js.map +1 -0
- package/dist/admin/AdminUserAudits-D7cTcElL.js +3 -0
- package/dist/admin/{AdminUserCreate-Bhxsn92l.js → AdminUserCreate-DzfRbGZ4.js} +4 -4
- package/dist/admin/AdminUserCreate-DzfRbGZ4.js.map +1 -0
- package/dist/admin/{AdminUserCreate-CYI_xW5T.js → AdminUserCreate-oUA1KDIl.js} +1 -1
- package/dist/admin/{AdminUserDetails-C2y1Ig4n.js → AdminUserDetails-DeTrJm-t.js} +5 -5
- package/dist/admin/AdminUserDetails-DeTrJm-t.js.map +1 -0
- package/dist/admin/{AdminUserDetails-Cmzx9HxH.js → AdminUserDetails-y1H5DW8Y.js} +1 -1
- package/dist/admin/{AdminUserLayout-sW6cjZL0.js → AdminUserLayout-CsfrrZkD.js} +4 -7
- package/dist/admin/AdminUserLayout-CsfrrZkD.js.map +1 -0
- package/dist/admin/{AdminUserLayout-DGSf612u.js → AdminUserLayout-Dejnz13m.js} +1 -1
- package/dist/admin/AdminUserSessions-Bbhcpz4k.js +3 -0
- package/dist/admin/{AdminUserSessions-CvN15wPe.js → AdminUserSessions-DO9H85O-.js} +4 -4
- package/dist/admin/AdminUserSessions-DO9H85O-.js.map +1 -0
- package/dist/admin/{AdminUserSettings-DvaaxgcV.js → AdminUserSettings-B3jA8g3p.js} +4 -4
- package/dist/admin/AdminUserSettings-B3jA8g3p.js.map +1 -0
- package/dist/admin/AdminUserSettings-CE0xpbQc.js +3 -0
- package/dist/admin/AdminUsers-CegGZDhW.js +3 -0
- package/dist/admin/{AdminUsers-BR3C-jrg.js → AdminUsers-ebbrJBT0.js} +13 -17
- package/dist/admin/AdminUsers-ebbrJBT0.js.map +1 -0
- package/dist/admin/index.d.ts +2700 -1178
- package/dist/admin/index.js +65 -62
- package/dist/admin/index.js.map +1 -1
- package/dist/auth/AuthLayout-BAZJHzDG.js +23 -0
- package/dist/auth/AuthLayout-BAZJHzDG.js.map +1 -0
- package/dist/auth/{Login-7HlBjDeV.js → Login-CeNZZjrr.js} +80 -44
- package/dist/auth/Login-CeNZZjrr.js.map +1 -0
- package/dist/auth/Login-hQcu1nlu.js +4 -0
- package/dist/auth/Register-B6HBNVHS.js +4 -0
- package/dist/auth/{Register-CuQr3kgi.js → Register-s4ENeyiE.js} +131 -91
- package/dist/auth/Register-s4ENeyiE.js.map +1 -0
- package/dist/auth/ResetPassword-Cjd-W-Nu.js +3 -0
- package/dist/auth/ResetPassword-GLIFkJT7.js +278 -0
- package/dist/auth/ResetPassword-GLIFkJT7.js.map +1 -0
- package/dist/auth/index.d.ts +605 -532
- package/dist/auth/index.js +26 -18
- package/dist/auth/index.js.map +1 -1
- package/dist/core/index.d.ts +425 -155
- package/dist/core/index.js +1751 -1369
- package/dist/core/index.js.map +1 -1
- package/package.json +23 -20
- package/src/admin/AdminRouter.ts +70 -16
- package/src/admin/components/AdminLayout.tsx +41 -61
- package/src/admin/components/audits/AdminAudits.tsx +240 -0
- package/src/admin/components/{AdminFiles.tsx → files/AdminFiles.tsx} +1 -1
- package/src/admin/components/{AdminJobs.tsx → jobs/AdminJobs.tsx} +1 -1
- package/src/admin/components/parameters/AdminParameters.tsx +137 -0
- package/src/admin/components/parameters/ParameterDetails.tsx +228 -0
- package/src/admin/components/parameters/ParameterHistory.tsx +146 -0
- package/src/admin/components/parameters/ParameterTree.tsx +146 -0
- package/src/admin/components/parameters/types.ts +35 -0
- package/src/admin/components/{AdminSessions.tsx → sessions/AdminSessions.tsx} +1 -1
- package/src/admin/components/users/AdminUserAudits.tsx +183 -0
- package/src/admin/components/{AdminUserCreate.tsx → users/AdminUserCreate.tsx} +1 -1
- package/src/admin/components/{AdminUserLayout.tsx → users/AdminUserLayout.tsx} +1 -4
- package/src/admin/components/{AdminUserSettings.tsx → users/AdminUserSettings.tsx} +1 -1
- package/src/admin/components/{AdminUsers.tsx → users/AdminUsers.tsx} +10 -12
- package/src/admin/index.ts +24 -16
- package/src/auth/AuthRouter.ts +23 -17
- package/src/auth/components/AuthLayout.tsx +6 -3
- package/src/auth/components/Login.tsx +109 -47
- package/src/auth/components/Register.tsx +158 -94
- package/src/auth/components/ResetPassword.tsx +51 -5
- package/src/auth/components/buttons/UserButton.tsx +2 -0
- package/src/core/atoms/alephaThemeAtom.ts +13 -0
- package/src/core/atoms/alephaThemeListAtom.ts +10 -0
- package/src/core/atoms/themes/default.ts +6 -0
- package/src/core/{themes → atoms/themes}/midnight.ts +3 -5
- package/src/core/components/buttons/ActionButton.tsx +33 -26
- package/src/core/components/buttons/DarkModeButton.tsx +0 -1
- package/src/core/components/buttons/ThemeButton.tsx +10 -7
- package/src/core/components/buttons/ToggleSidebarButton.tsx +19 -16
- package/src/core/components/data/ErrorViewer.tsx +171 -0
- package/src/core/components/data/JsonViewer.tsx +147 -138
- package/src/core/components/form/Control.tsx +95 -18
- package/src/core/components/form/ControlArray.tsx +377 -0
- package/src/core/components/form/ControlObject.tsx +127 -0
- package/src/core/components/form/TypeForm.tsx +99 -37
- package/src/core/components/layout/AdminShell.tsx +14 -1
- package/src/core/components/layout/AlephaMantineProvider.tsx +7 -3
- package/src/core/components/layout/Omnibar.tsx +1 -1
- package/src/core/components/layout/Sidebar.tsx +47 -14
- package/src/core/components/table/ColumnPicker.tsx +126 -0
- package/src/core/components/table/DataTable.tsx +354 -181
- package/src/core/components/table/DataTableFilters.tsx +64 -0
- package/src/core/components/table/DataTablePagination.tsx +59 -0
- package/src/core/components/table/DataTableToolbar.tsx +126 -0
- package/src/core/components/table/FilterPicker.tsx +138 -0
- package/src/core/components/table/types.ts +199 -0
- package/src/core/helpers/isComponentType.ts +9 -0
- package/src/core/helpers/renderIcon.tsx +13 -0
- package/src/core/hooks/useTheme.ts +24 -18
- package/src/core/index.ts +24 -3
- package/src/core/interfaces/AlephaTheme.ts +8 -0
- package/src/core/providers/ThemeProvider.ts +44 -62
- package/src/core/services/DialogService.tsx +24 -0
- package/src/core/utils/parseInput.ts +2 -2
- package/styles.css +1 -1
- package/dist/admin/AdminFiles-B-0UcHVV.js +0 -3
- package/dist/admin/AdminFiles-B_jfB_Py.js.map +0 -1
- package/dist/admin/AdminLayout-BMtiXAzS.js +0 -396
- package/dist/admin/AdminLayout-BMtiXAzS.js.map +0 -1
- package/dist/admin/AdminLayout-BNo3GoHR.js +0 -3
- package/dist/admin/AdminNotifications-BFEjqpqx.js.map +0 -1
- package/dist/admin/AdminNotifications-DJs2ZjNj.js +0 -3
- package/dist/admin/AdminSessions-D7DESfWK.js.map +0 -1
- package/dist/admin/AdminSessions-PS2M8iXi.js +0 -3
- package/dist/admin/AdminUserCreate-Bhxsn92l.js.map +0 -1
- package/dist/admin/AdminUserDetails-C2y1Ig4n.js.map +0 -1
- package/dist/admin/AdminUserLayout-sW6cjZL0.js.map +0 -1
- package/dist/admin/AdminUserSessions-CvN15wPe.js.map +0 -1
- package/dist/admin/AdminUserSessions-D-aOcZgV.js +0 -3
- package/dist/admin/AdminUserSettings-CEMhIYrI.js +0 -3
- package/dist/admin/AdminUserSettings-DvaaxgcV.js.map +0 -1
- package/dist/admin/AdminUsers-BR3C-jrg.js.map +0 -1
- package/dist/admin/AdminUsers-CMW9vN09.js +0 -3
- package/dist/auth/AuthLayout-CzwUKD9y.js +0 -19
- package/dist/auth/AuthLayout-CzwUKD9y.js.map +0 -1
- package/dist/auth/Login-7HlBjDeV.js.map +0 -1
- package/dist/auth/Login-C-e27DGb.js +0 -4
- package/dist/auth/Register-CuQr3kgi.js.map +0 -1
- package/dist/auth/Register-DbvXwgbG.js +0 -4
- package/dist/auth/ResetPassword-BzU-cdd4.js +0 -243
- package/dist/auth/ResetPassword-BzU-cdd4.js.map +0 -1
- package/dist/auth/ResetPassword-DSvrdpaA.js +0 -3
- package/src/admin/AdminSidebar.ts +0 -31
- package/src/admin/components/AdminParameters.tsx +0 -24
- package/src/core/themes/aurora.ts +0 -107
- package/src/core/themes/crystal.ts +0 -107
- package/src/core/themes/default.ts +0 -7
- package/src/core/themes/ember.ts +0 -107
- package/src/core/themes/index.ts +0 -7
- package/src/core/themes/remoraid.ts +0 -278
- package/src/core/themes/slate.ts +0 -81
- /package/src/admin/components/{AdminNotifications.tsx → notifications/AdminNotifications.tsx} +0 -0
- /package/src/admin/components/{AdminUserDetails.tsx → users/AdminUserDetails.tsx} +0 -0
- /package/src/admin/components/{AdminUserSessions.tsx → users/AdminUserSessions.tsx} +0 -0
- /package/src/admin/components/{AdminVerifications.tsx → verifications/AdminVerifications.tsx} +0 -0
|
@@ -25,8 +25,10 @@ import {
|
|
|
25
25
|
type GenericControlProps,
|
|
26
26
|
parseInput,
|
|
27
27
|
} from "../../utils/parseInput.ts";
|
|
28
|
+
import ControlArray, { type ControlArrayProps } from "./ControlArray.tsx";
|
|
28
29
|
import ControlDate from "./ControlDate.tsx";
|
|
29
30
|
import ControlNumber, { type ControlNumberProps } from "./ControlNumber.tsx";
|
|
31
|
+
import ControlObject, { type ControlObjectProps } from "./ControlObject.tsx";
|
|
30
32
|
import ControlQueryBuilder from "./ControlQueryBuilder.tsx";
|
|
31
33
|
import ControlSelect, { type ControlSelectProps } from "./ControlSelect.tsx";
|
|
32
34
|
|
|
@@ -43,6 +45,8 @@ export interface ControlProps extends GenericControlProps {
|
|
|
43
45
|
datetime?: boolean | DateTimePickerProps;
|
|
44
46
|
time?: boolean | TimeInputProps;
|
|
45
47
|
query?: any; // Enable query builder mode with schema-aware autocomplete
|
|
48
|
+
object?: boolean | Partial<Omit<ControlObjectProps, "input">>; // Nested object editing
|
|
49
|
+
array?: boolean | Partial<Omit<ControlArrayProps, "input">>; // Array of items editing
|
|
46
50
|
custom?: ComponentType<CustomControlProps>;
|
|
47
51
|
}
|
|
48
52
|
|
|
@@ -64,17 +68,22 @@ export interface ControlProps extends GenericControlProps {
|
|
|
64
68
|
* - DateTimePicker (for date-time format)
|
|
65
69
|
* - TimeInput (for time format)
|
|
66
70
|
* - QueryBuilder (for building type-safe queries with autocomplete)
|
|
71
|
+
* - ControlObject (for nested object schemas)
|
|
72
|
+
* - ControlArray (for arrays of objects)
|
|
67
73
|
* - Custom component
|
|
68
74
|
*
|
|
69
75
|
* Automatically handles labels, descriptions, error messages, required state, and default icons.
|
|
70
76
|
*/
|
|
71
77
|
const Control = (_props: ControlProps) => {
|
|
72
78
|
const form = useFormState(_props.input, ["error"]);
|
|
73
|
-
|
|
79
|
+
|
|
80
|
+
// Early return if input is not properly initialized
|
|
74
81
|
if (!_props.input?.props) {
|
|
75
82
|
return null;
|
|
76
83
|
}
|
|
77
84
|
|
|
85
|
+
const { inputProps, id, icon, format, schema } = parseInput(_props, form);
|
|
86
|
+
|
|
78
87
|
const props = {
|
|
79
88
|
..._props,
|
|
80
89
|
...schema.$control,
|
|
@@ -114,6 +123,56 @@ const Control = (_props: ControlProps) => {
|
|
|
114
123
|
}
|
|
115
124
|
//endregion
|
|
116
125
|
|
|
126
|
+
//region <ControlObject/>
|
|
127
|
+
// Handle nested objects with properties
|
|
128
|
+
const isObject =
|
|
129
|
+
props.input.schema &&
|
|
130
|
+
"type" in props.input.schema &&
|
|
131
|
+
props.input.schema.type === "object" &&
|
|
132
|
+
"properties" in props.input.schema;
|
|
133
|
+
|
|
134
|
+
if (props.object || isObject) {
|
|
135
|
+
const controlObjectProps =
|
|
136
|
+
typeof props.object === "object" ? props.object : {};
|
|
137
|
+
return (
|
|
138
|
+
<ControlObject
|
|
139
|
+
input={props.input}
|
|
140
|
+
title={props.title}
|
|
141
|
+
description={props.description}
|
|
142
|
+
{...controlObjectProps}
|
|
143
|
+
/>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
//endregion
|
|
147
|
+
|
|
148
|
+
//region <ControlArray/>
|
|
149
|
+
// Handle arrays of objects (arrays of primitives are handled by ControlSelect)
|
|
150
|
+
const isArray =
|
|
151
|
+
props.input.schema &&
|
|
152
|
+
"type" in props.input.schema &&
|
|
153
|
+
props.input.schema.type === "array";
|
|
154
|
+
|
|
155
|
+
const isArrayOfObjects =
|
|
156
|
+
isArray &&
|
|
157
|
+
"items" in props.input.schema &&
|
|
158
|
+
props.input.schema.items &&
|
|
159
|
+
typeof props.input.schema.items === "object" &&
|
|
160
|
+
"properties" in props.input.schema.items;
|
|
161
|
+
|
|
162
|
+
if (props.array || isArrayOfObjects) {
|
|
163
|
+
const controlArrayProps =
|
|
164
|
+
typeof props.array === "object" ? props.array : {};
|
|
165
|
+
return (
|
|
166
|
+
<ControlArray
|
|
167
|
+
input={props.input}
|
|
168
|
+
title={props.title}
|
|
169
|
+
description={props.description}
|
|
170
|
+
{...controlArrayProps}
|
|
171
|
+
/>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
//endregion
|
|
175
|
+
|
|
117
176
|
//region <NumberInput/>
|
|
118
177
|
if (
|
|
119
178
|
props.number ||
|
|
@@ -170,16 +229,13 @@ const Control = (_props: ControlProps) => {
|
|
|
170
229
|
|
|
171
230
|
//region <ControlSelect/>
|
|
172
231
|
// Handle: single enum, array of enum, array of strings, or explicit select/multi/tags props
|
|
232
|
+
// Note: arrays of objects are handled by ControlArray above, this handles primitive arrays
|
|
173
233
|
const isEnum =
|
|
174
234
|
props.input.schema &&
|
|
175
235
|
"enum" in props.input.schema &&
|
|
176
236
|
props.input.schema.enum;
|
|
177
|
-
const isArray =
|
|
178
|
-
props.input.schema &&
|
|
179
|
-
"type" in props.input.schema &&
|
|
180
|
-
props.input.schema.type === "array";
|
|
181
237
|
|
|
182
|
-
if (isEnum || isArray || props.select) {
|
|
238
|
+
if (isEnum || (isArray && !isArrayOfObjects) || props.select) {
|
|
183
239
|
const opts = typeof props.select === "object" ? props.select : {};
|
|
184
240
|
return (
|
|
185
241
|
<ControlSelect
|
|
@@ -195,21 +251,42 @@ const Control = (_props: ControlProps) => {
|
|
|
195
251
|
|
|
196
252
|
//region <Switch/>
|
|
197
253
|
if (
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
props.switch
|
|
254
|
+
props.input.schema &&
|
|
255
|
+
"type" in props.input.schema &&
|
|
256
|
+
props.input.schema.type === "boolean"
|
|
202
257
|
) {
|
|
203
|
-
|
|
258
|
+
if (props.switch) {
|
|
259
|
+
const switchProps = typeof props.switch === "object" ? props.switch : {};
|
|
260
|
+
|
|
261
|
+
return (
|
|
262
|
+
<Switch
|
|
263
|
+
{...inputProps}
|
|
264
|
+
id={id}
|
|
265
|
+
color={"blue"}
|
|
266
|
+
defaultChecked={props.input.props.defaultValue}
|
|
267
|
+
{...props.input.props}
|
|
268
|
+
{...switchProps}
|
|
269
|
+
/>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// by default, render as <Select/> with Yes/No/Empty options
|
|
274
|
+
const selectProps: Partial<ControlSelectProps> = {
|
|
275
|
+
loader: async () => [
|
|
276
|
+
{ value: "true", label: "Yes" },
|
|
277
|
+
{ value: "false", label: "No" },
|
|
278
|
+
{ value: "", label: "" },
|
|
279
|
+
],
|
|
280
|
+
...props.input.props,
|
|
281
|
+
};
|
|
204
282
|
|
|
205
283
|
return (
|
|
206
|
-
<
|
|
207
|
-
{
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
{...
|
|
212
|
-
{...switchProps}
|
|
284
|
+
<ControlSelect
|
|
285
|
+
input={props.input}
|
|
286
|
+
title={props.title}
|
|
287
|
+
description={props.description}
|
|
288
|
+
icon={icon}
|
|
289
|
+
{...selectProps}
|
|
213
290
|
/>
|
|
214
291
|
);
|
|
215
292
|
}
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { useEvents } from "@alepha/react";
|
|
2
|
+
import type { BaseInputField } from "@alepha/react/form";
|
|
3
|
+
import {
|
|
4
|
+
ActionIcon,
|
|
5
|
+
Fieldset,
|
|
6
|
+
Flex,
|
|
7
|
+
Grid,
|
|
8
|
+
Stack,
|
|
9
|
+
Text,
|
|
10
|
+
UnstyledButton,
|
|
11
|
+
} from "@mantine/core";
|
|
12
|
+
import { IconGripVertical, IconPlus, IconTrash } from "@tabler/icons-react";
|
|
13
|
+
import type { TObject, TSchema } from "alepha";
|
|
14
|
+
import { useRef, useState } from "react";
|
|
15
|
+
import { ui } from "../../constants/ui.ts";
|
|
16
|
+
import {
|
|
17
|
+
type GenericControlProps,
|
|
18
|
+
parseInput,
|
|
19
|
+
} from "../../utils/parseInput.ts";
|
|
20
|
+
import Control, { type ControlProps } from "./Control.tsx";
|
|
21
|
+
|
|
22
|
+
export interface ControlArrayProps extends GenericControlProps {
|
|
23
|
+
/**
|
|
24
|
+
* Minimum number of items allowed.
|
|
25
|
+
* @default 0
|
|
26
|
+
*/
|
|
27
|
+
min?: number;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Maximum number of items allowed.
|
|
31
|
+
* @default Infinity
|
|
32
|
+
*/
|
|
33
|
+
max?: number;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Label for the add button.
|
|
37
|
+
* @default "Add item"
|
|
38
|
+
*/
|
|
39
|
+
addLabel?: string;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Number of columns for object item fields.
|
|
43
|
+
* @default 1
|
|
44
|
+
*/
|
|
45
|
+
columns?: number;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Variant for the container.
|
|
49
|
+
* - "fieldset": Uses Mantine Fieldset with legend
|
|
50
|
+
* - "plain": No container, just renders items
|
|
51
|
+
* @default "fieldset"
|
|
52
|
+
*/
|
|
53
|
+
variant?: "fieldset" | "plain";
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Per-field control props override for object items.
|
|
57
|
+
* Keys are field names from the item schema.
|
|
58
|
+
*/
|
|
59
|
+
controlProps?: Record<string, Partial<Omit<ControlProps, "input">>>;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Control props for primitive items.
|
|
63
|
+
*/
|
|
64
|
+
itemControlProps?: Partial<Omit<ControlProps, "input">>;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Show drag handle for reordering.
|
|
68
|
+
* @default false
|
|
69
|
+
*/
|
|
70
|
+
sortable?: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* ControlArray component for editing arrays of schema items.
|
|
75
|
+
*
|
|
76
|
+
* Features:
|
|
77
|
+
* - Dynamic add/remove of items
|
|
78
|
+
* - Supports arrays of objects with nested fields
|
|
79
|
+
* - Supports arrays of primitives
|
|
80
|
+
* - Grid layout for object items
|
|
81
|
+
* - Min/max constraints
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```tsx
|
|
85
|
+
* // For a schema like:
|
|
86
|
+
* // t.object({
|
|
87
|
+
* // contacts: t.array(t.object({
|
|
88
|
+
* // name: t.text(),
|
|
89
|
+
* // email: t.text({ format: "email" }),
|
|
90
|
+
* // }))
|
|
91
|
+
* // })
|
|
92
|
+
*
|
|
93
|
+
* <ControlArray
|
|
94
|
+
* input={form.input.contacts}
|
|
95
|
+
* columns={2}
|
|
96
|
+
* addLabel="Add contact"
|
|
97
|
+
* controlProps={{
|
|
98
|
+
* email: { text: { placeholder: "email@example.com" } }
|
|
99
|
+
* }}
|
|
100
|
+
* />
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
const ControlArray = (props: ControlArrayProps) => {
|
|
104
|
+
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
|
+
);
|
|
141
|
+
|
|
142
|
+
if (!props.input?.props) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const schema = props.input.schema;
|
|
147
|
+
if (!schema || !("items" in schema)) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const itemSchema = (schema as any).items as TSchema;
|
|
152
|
+
const isObjectItem = itemSchema && "properties" in itemSchema;
|
|
153
|
+
const { min = 0, max = Number.POSITIVE_INFINITY, columns = 1 } = props;
|
|
154
|
+
|
|
155
|
+
const updateFormValue = (newItems: Array<{ key: number; value: any }>) => {
|
|
156
|
+
props.input.set(newItems.map((item) => item.value));
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const handleAdd = () => {
|
|
160
|
+
if (items.length >= max) return;
|
|
161
|
+
|
|
162
|
+
// Create default value based on item schema
|
|
163
|
+
let newValue: any;
|
|
164
|
+
if (isObjectItem) {
|
|
165
|
+
newValue = {};
|
|
166
|
+
// Initialize with default values from schema if available
|
|
167
|
+
const objSchema = itemSchema as TObject;
|
|
168
|
+
for (const [key, propSchema] of Object.entries(objSchema.properties)) {
|
|
169
|
+
if ("default" in propSchema) {
|
|
170
|
+
newValue[key] = propSchema.default;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
newValue = "";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const newItems = [...items, { key: idCounter.current++, value: newValue }];
|
|
178
|
+
setItems(newItems);
|
|
179
|
+
updateFormValue(newItems);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const handleRemove = (index: number) => {
|
|
183
|
+
if (items.length <= min) return;
|
|
184
|
+
const newItems = items.filter((_, i) => i !== index);
|
|
185
|
+
setItems(newItems);
|
|
186
|
+
updateFormValue(newItems);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const handleItemChange = (index: number, value: any) => {
|
|
190
|
+
const newItems = [...items];
|
|
191
|
+
newItems[index] = { ...newItems[index], value };
|
|
192
|
+
setItems(newItems);
|
|
193
|
+
updateFormValue(newItems);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const handleFieldChange = (index: number, field: string, value: any) => {
|
|
197
|
+
const newItems = [...items];
|
|
198
|
+
newItems[index] = {
|
|
199
|
+
...newItems[index],
|
|
200
|
+
value: { ...newItems[index].value, [field]: value },
|
|
201
|
+
};
|
|
202
|
+
setItems(newItems);
|
|
203
|
+
updateFormValue(newItems);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const colSpan = 12 / columns;
|
|
207
|
+
const fieldNames = isObjectItem
|
|
208
|
+
? Object.keys((itemSchema as TObject).properties)
|
|
209
|
+
: [];
|
|
210
|
+
|
|
211
|
+
const renderItems = () => (
|
|
212
|
+
<Stack gap="sm">
|
|
213
|
+
{items.map((item, index) => (
|
|
214
|
+
<Flex
|
|
215
|
+
key={item.key}
|
|
216
|
+
gap="sm"
|
|
217
|
+
align="flex-start"
|
|
218
|
+
p="xs"
|
|
219
|
+
bg={ui.colors.surface}
|
|
220
|
+
style={{ borderRadius: "var(--mantine-radius-sm)" }}
|
|
221
|
+
>
|
|
222
|
+
{props.sortable && (
|
|
223
|
+
<ActionIcon
|
|
224
|
+
variant="subtle"
|
|
225
|
+
color="gray"
|
|
226
|
+
style={{ cursor: "grab" }}
|
|
227
|
+
>
|
|
228
|
+
<IconGripVertical size={16} />
|
|
229
|
+
</ActionIcon>
|
|
230
|
+
)}
|
|
231
|
+
|
|
232
|
+
{isObjectItem ? (
|
|
233
|
+
<Grid style={{ flex: 1 }} gutter="sm">
|
|
234
|
+
{fieldNames.map((fieldName) => {
|
|
235
|
+
const fieldSchema = (itemSchema as TObject).properties[
|
|
236
|
+
fieldName
|
|
237
|
+
];
|
|
238
|
+
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
|
+
};
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<Grid.Col key={fieldName} span={colSpan}>
|
|
259
|
+
<Control input={virtualInput} {...fieldControlProps} />
|
|
260
|
+
</Grid.Col>
|
|
261
|
+
);
|
|
262
|
+
})}
|
|
263
|
+
</Grid>
|
|
264
|
+
) : (
|
|
265
|
+
<Flex style={{ flex: 1 }}>
|
|
266
|
+
<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
|
+
}
|
|
281
|
+
{...props.itemControlProps}
|
|
282
|
+
/>
|
|
283
|
+
</Flex>
|
|
284
|
+
)}
|
|
285
|
+
|
|
286
|
+
<ActionIcon
|
|
287
|
+
variant="subtle"
|
|
288
|
+
color="red"
|
|
289
|
+
onClick={() => handleRemove(index)}
|
|
290
|
+
disabled={items.length <= min}
|
|
291
|
+
>
|
|
292
|
+
<IconTrash size={16} />
|
|
293
|
+
</ActionIcon>
|
|
294
|
+
</Flex>
|
|
295
|
+
))}
|
|
296
|
+
|
|
297
|
+
<UnstyledButton
|
|
298
|
+
onClick={handleAdd}
|
|
299
|
+
disabled={items.length >= max}
|
|
300
|
+
style={{
|
|
301
|
+
display: "flex",
|
|
302
|
+
alignItems: "center",
|
|
303
|
+
justifyContent: "center",
|
|
304
|
+
gap: 6,
|
|
305
|
+
padding: "8px 12px",
|
|
306
|
+
borderRadius: "var(--mantine-radius-sm)",
|
|
307
|
+
border: "1px dashed var(--mantine-color-dimmed)",
|
|
308
|
+
color: "var(--mantine-color-dimmed)",
|
|
309
|
+
fontSize: "var(--mantine-font-size-sm)",
|
|
310
|
+
cursor: items.length >= max ? "not-allowed" : "pointer",
|
|
311
|
+
opacity: items.length >= max ? 0.5 : 1,
|
|
312
|
+
transition: "all 150ms ease",
|
|
313
|
+
}}
|
|
314
|
+
onMouseEnter={(e) => {
|
|
315
|
+
if (items.length < max) {
|
|
316
|
+
e.currentTarget.style.borderColor =
|
|
317
|
+
"var(--mantine-color-blue-filled)";
|
|
318
|
+
e.currentTarget.style.color = "var(--mantine-color-blue-filled)";
|
|
319
|
+
e.currentTarget.style.background =
|
|
320
|
+
"var(--mantine-color-blue-light)";
|
|
321
|
+
}
|
|
322
|
+
}}
|
|
323
|
+
onMouseLeave={(e) => {
|
|
324
|
+
e.currentTarget.style.borderColor = "var(--mantine-color-dimmed)";
|
|
325
|
+
e.currentTarget.style.color = "var(--mantine-color-dimmed)";
|
|
326
|
+
e.currentTarget.style.background = "transparent";
|
|
327
|
+
}}
|
|
328
|
+
>
|
|
329
|
+
<IconPlus size={14} />
|
|
330
|
+
{props.addLabel ?? "Add"}
|
|
331
|
+
</UnstyledButton>
|
|
332
|
+
</Stack>
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
if (props.variant === "plain") {
|
|
336
|
+
return (
|
|
337
|
+
<Stack gap="xs">
|
|
338
|
+
{inputProps.label && (
|
|
339
|
+
<Text size="sm" fw={500}>
|
|
340
|
+
{inputProps.label}
|
|
341
|
+
</Text>
|
|
342
|
+
)}
|
|
343
|
+
{inputProps.description && (
|
|
344
|
+
<Text size="sm" c="dimmed">
|
|
345
|
+
{inputProps.description}
|
|
346
|
+
</Text>
|
|
347
|
+
)}
|
|
348
|
+
{renderItems()}
|
|
349
|
+
{inputProps.error && (
|
|
350
|
+
<Text size="sm" c="red">
|
|
351
|
+
{inputProps.error}
|
|
352
|
+
</Text>
|
|
353
|
+
)}
|
|
354
|
+
</Stack>
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return (
|
|
359
|
+
<Fieldset legend={inputProps.label}>
|
|
360
|
+
<Stack gap="xs">
|
|
361
|
+
{inputProps.description && (
|
|
362
|
+
<Text size="sm" c="dimmed">
|
|
363
|
+
{inputProps.description}
|
|
364
|
+
</Text>
|
|
365
|
+
)}
|
|
366
|
+
{renderItems()}
|
|
367
|
+
{inputProps.error && (
|
|
368
|
+
<Text size="sm" c="red">
|
|
369
|
+
{inputProps.error}
|
|
370
|
+
</Text>
|
|
371
|
+
)}
|
|
372
|
+
</Stack>
|
|
373
|
+
</Fieldset>
|
|
374
|
+
);
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
export default ControlArray;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { BaseInputField, ObjectInputField } from "@alepha/react/form";
|
|
2
|
+
import { Fieldset, Grid, Stack, Text } from "@mantine/core";
|
|
3
|
+
import type { TObject } from "alepha";
|
|
4
|
+
import {
|
|
5
|
+
type GenericControlProps,
|
|
6
|
+
parseInput,
|
|
7
|
+
} from "../../utils/parseInput.ts";
|
|
8
|
+
import Control, { type ControlProps } from "./Control.tsx";
|
|
9
|
+
|
|
10
|
+
export interface ControlObjectProps extends GenericControlProps {
|
|
11
|
+
/**
|
|
12
|
+
* Number of columns for the grid layout.
|
|
13
|
+
* @default 1
|
|
14
|
+
*/
|
|
15
|
+
columns?: number;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Variant for the container.
|
|
19
|
+
* - "fieldset": Uses Mantine Fieldset with legend
|
|
20
|
+
* - "plain": No container, just renders fields
|
|
21
|
+
* @default "fieldset"
|
|
22
|
+
*/
|
|
23
|
+
variant?: "fieldset" | "plain";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Per-field control props override.
|
|
27
|
+
* Keys are field names from the schema.
|
|
28
|
+
*/
|
|
29
|
+
controlProps?: Record<string, Partial<Omit<ControlProps, "input">>>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* ControlObject component for editing nested object schemas.
|
|
34
|
+
*
|
|
35
|
+
* Features:
|
|
36
|
+
* - Renders all properties of an object schema
|
|
37
|
+
* - Supports grid layout with configurable columns
|
|
38
|
+
* - Per-field customization via controlProps
|
|
39
|
+
* - Recursive support for deeply nested objects
|
|
40
|
+
*
|
|
41
|
+
* The form system provides nested InputFields under the `.items` property.
|
|
42
|
+
* For example: form.input.address.items.street
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```tsx
|
|
46
|
+
* // For a schema like:
|
|
47
|
+
* // t.object({
|
|
48
|
+
* // address: t.object({
|
|
49
|
+
* // street: t.text(),
|
|
50
|
+
* // city: t.text(),
|
|
51
|
+
* // zip: t.text(),
|
|
52
|
+
* // })
|
|
53
|
+
* // })
|
|
54
|
+
*
|
|
55
|
+
* <ControlObject
|
|
56
|
+
* input={form.input.address}
|
|
57
|
+
* columns={2}
|
|
58
|
+
* controlProps={{
|
|
59
|
+
* zip: { text: { maxLength: 10 } }
|
|
60
|
+
* }}
|
|
61
|
+
* />
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
const ControlObject = (props: ControlObjectProps) => {
|
|
65
|
+
const { inputProps } = parseInput(props, {});
|
|
66
|
+
|
|
67
|
+
if (!props.input?.props) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const schema = props.input.schema as TObject;
|
|
72
|
+
if (!schema?.properties) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const fieldNames = Object.keys(schema.properties);
|
|
77
|
+
const columns = props.columns ?? 1;
|
|
78
|
+
const colSpan = 12 / columns;
|
|
79
|
+
|
|
80
|
+
// The form system provides nested InputFields under .items
|
|
81
|
+
const objectInput = props.input as ObjectInputField<TObject>;
|
|
82
|
+
const nestedItems = objectInput.items as Record<string, BaseInputField>;
|
|
83
|
+
|
|
84
|
+
const renderFields = () => (
|
|
85
|
+
<Grid>
|
|
86
|
+
{fieldNames.map((fieldName) => {
|
|
87
|
+
const fieldControlProps = props.controlProps?.[fieldName] ?? {};
|
|
88
|
+
|
|
89
|
+
// Access nested InputField from .items
|
|
90
|
+
const field = nestedItems?.[fieldName];
|
|
91
|
+
if (!field) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<Grid.Col key={fieldName} span={colSpan}>
|
|
97
|
+
<Control input={field} {...fieldControlProps} />
|
|
98
|
+
</Grid.Col>
|
|
99
|
+
);
|
|
100
|
+
})}
|
|
101
|
+
</Grid>
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
if (props.variant === "plain") {
|
|
105
|
+
return renderFields();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<Fieldset legend={inputProps.label}>
|
|
110
|
+
<Stack gap="xs">
|
|
111
|
+
{inputProps.description && (
|
|
112
|
+
<Text size="sm" c="dimmed">
|
|
113
|
+
{inputProps.description}
|
|
114
|
+
</Text>
|
|
115
|
+
)}
|
|
116
|
+
{renderFields()}
|
|
117
|
+
{inputProps.error && (
|
|
118
|
+
<Text size="sm" c="red">
|
|
119
|
+
{inputProps.error}
|
|
120
|
+
</Text>
|
|
121
|
+
)}
|
|
122
|
+
</Stack>
|
|
123
|
+
</Fieldset>
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export default ControlObject;
|