@alepha/ui 0.11.5 → 0.11.7
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/LICENSE +1 -1
- package/dist/AlephaMantineProvider-B4TwQ4tY.js +3 -0
- package/dist/{AlephaMantineProvider-Be0DAazb.js → AlephaMantineProvider-CzMrw7V3.js} +4 -4
- package/dist/{AlephaMantineProvider-Be0DAazb.js.map → AlephaMantineProvider-CzMrw7V3.js.map} +1 -1
- package/dist/index.d.ts +142 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1026 -94
- package/dist/index.js.map +1 -1
- package/package.json +22 -20
- package/src/components/buttons/ActionButton.tsx +29 -3
- package/src/components/buttons/ToggleSidebarButton.tsx +4 -1
- package/src/components/data/JsonViewer.tsx +352 -0
- package/src/components/form/Control.tsx +28 -11
- package/src/components/form/ControlNumber.tsx +96 -0
- package/src/components/form/ControlQueryBuilder.tsx +313 -0
- package/src/components/form/ControlSelect.tsx +1 -1
- package/src/components/form/TypeForm.tsx +6 -4
- package/src/components/layout/AdminShell.tsx +7 -0
- package/src/components/layout/Sidebar.tsx +24 -14
- package/src/components/table/DataTable.tsx +297 -20
- package/src/constants/ui.ts +1 -0
- package/src/index.ts +3 -0
- package/src/services/DialogService.tsx +13 -2
- package/src/utils/extractSchemaFields.ts +172 -0
- package/dist/AlephaMantineProvider-Ba88lMeq.js +0 -3
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"mantine"
|
|
7
7
|
],
|
|
8
8
|
"author": "Feunard",
|
|
9
|
-
"version": "0.11.
|
|
9
|
+
"version": "0.11.7",
|
|
10
10
|
"type": "module",
|
|
11
11
|
"engines": {
|
|
12
12
|
"node": ">=22.0.0"
|
|
@@ -20,32 +20,34 @@
|
|
|
20
20
|
"src"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@alepha/core": "0.11.
|
|
24
|
-
"@alepha/
|
|
25
|
-
"@alepha/
|
|
26
|
-
"@alepha/react
|
|
27
|
-
"@alepha/react-
|
|
28
|
-
"@alepha/
|
|
29
|
-
"@
|
|
30
|
-
"@
|
|
31
|
-
"@mantine/
|
|
32
|
-
"@mantine/
|
|
33
|
-
"@mantine/
|
|
34
|
-
"@mantine/
|
|
35
|
-
"@mantine/
|
|
23
|
+
"@alepha/core": "0.11.7",
|
|
24
|
+
"@alepha/datetime": "0.11.7",
|
|
25
|
+
"@alepha/postgres": "0.11.7",
|
|
26
|
+
"@alepha/react": "0.11.7",
|
|
27
|
+
"@alepha/react-form": "0.11.7",
|
|
28
|
+
"@alepha/react-head": "0.11.7",
|
|
29
|
+
"@alepha/react-i18n": "0.11.7",
|
|
30
|
+
"@alepha/server": "0.11.7",
|
|
31
|
+
"@mantine/core": "^8.3.7",
|
|
32
|
+
"@mantine/dates": "^8.3.7",
|
|
33
|
+
"@mantine/hooks": "^8.3.7",
|
|
34
|
+
"@mantine/modals": "^8.3.7",
|
|
35
|
+
"@mantine/notifications": "^8.3.7",
|
|
36
|
+
"@mantine/nprogress": "^8.3.7",
|
|
37
|
+
"@mantine/spotlight": "^8.3.7",
|
|
36
38
|
"@tabler/icons-react": "^3.35.0",
|
|
37
39
|
"dayjs": "^1.11.19"
|
|
38
40
|
},
|
|
39
41
|
"devDependencies": {
|
|
40
|
-
"@alepha/cli": "0.11.
|
|
41
|
-
"@alepha/vite": "0.11.
|
|
42
|
-
"@biomejs/biome": "^2.3.
|
|
42
|
+
"@alepha/cli": "0.11.7",
|
|
43
|
+
"@alepha/vite": "0.11.7",
|
|
44
|
+
"@biomejs/biome": "^2.3.5",
|
|
43
45
|
"react": "^19.2.0",
|
|
44
46
|
"react-dom": "^19.2.0",
|
|
45
|
-
"tsdown": "^0.16.
|
|
47
|
+
"tsdown": "^0.16.4",
|
|
46
48
|
"typescript": "^5.9.3",
|
|
47
|
-
"vite": "^7.
|
|
48
|
-
"vitest": "^4.0.
|
|
49
|
+
"vite": "^7.2.2",
|
|
50
|
+
"vitest": "^4.0.8"
|
|
49
51
|
},
|
|
50
52
|
"peerDependencies": {
|
|
51
53
|
"react": "*",
|
|
@@ -277,7 +277,7 @@ const ActionButton = (_props: ActionProps) => {
|
|
|
277
277
|
|
|
278
278
|
const renderAction = () => {
|
|
279
279
|
if ("href" in restProps && restProps.href) {
|
|
280
|
-
if (restProps.href.startsWith("http")) {
|
|
280
|
+
if (restProps.href.startsWith("http") || restProps.target) {
|
|
281
281
|
return (
|
|
282
282
|
<ActionHrefButton {...restProps} href={restProps.href}>
|
|
283
283
|
{restProps.children}
|
|
@@ -311,6 +311,13 @@ const ActionButton = (_props: ActionProps) => {
|
|
|
311
311
|
}
|
|
312
312
|
|
|
313
313
|
if ("form" in restProps && restProps.form) {
|
|
314
|
+
if (restProps.type === "reset") {
|
|
315
|
+
return (
|
|
316
|
+
<ActionResetButton {...restProps} form={restProps.form}>
|
|
317
|
+
{restProps.children}
|
|
318
|
+
</ActionResetButton>
|
|
319
|
+
);
|
|
320
|
+
}
|
|
314
321
|
return (
|
|
315
322
|
<ActionSubmitButton {...restProps} form={restProps.form}>
|
|
316
323
|
{restProps.children}
|
|
@@ -345,10 +352,18 @@ const ActionButton = (_props: ActionProps) => {
|
|
|
345
352
|
|
|
346
353
|
// Wrap with Tooltip if provided
|
|
347
354
|
if (tooltip) {
|
|
355
|
+
// openDelay: 1000 -> like HTML title attribute
|
|
356
|
+
const defaultTooltipProps: Partial<TooltipProps> = {
|
|
357
|
+
openDelay: 1000,
|
|
358
|
+
};
|
|
348
359
|
const tooltipProps: TooltipProps =
|
|
349
360
|
typeof tooltip === "string"
|
|
350
|
-
? {
|
|
351
|
-
|
|
361
|
+
? {
|
|
362
|
+
...defaultTooltipProps,
|
|
363
|
+
label: tooltip,
|
|
364
|
+
children: actionElement,
|
|
365
|
+
}
|
|
366
|
+
: { ...defaultTooltipProps, ...tooltip, children: actionElement };
|
|
352
367
|
|
|
353
368
|
return <Tooltip {...tooltipProps} />;
|
|
354
369
|
}
|
|
@@ -366,6 +381,7 @@ export default ActionButton;
|
|
|
366
381
|
|
|
367
382
|
export interface ActionSubmitButtonProps extends ButtonProps {
|
|
368
383
|
form: FormModel<any>;
|
|
384
|
+
type?: "submit" | "reset";
|
|
369
385
|
}
|
|
370
386
|
|
|
371
387
|
/**
|
|
@@ -386,6 +402,16 @@ const ActionSubmitButton = (props: ActionSubmitButtonProps) => {
|
|
|
386
402
|
);
|
|
387
403
|
};
|
|
388
404
|
|
|
405
|
+
const ActionResetButton = (props: ActionSubmitButtonProps) => {
|
|
406
|
+
const { form, ...buttonProps } = props;
|
|
407
|
+
const state = useFormState(form);
|
|
408
|
+
return (
|
|
409
|
+
<Button {...buttonProps} disabled={state.loading} type={"reset"}>
|
|
410
|
+
{props.children}
|
|
411
|
+
</Button>
|
|
412
|
+
);
|
|
413
|
+
};
|
|
414
|
+
|
|
389
415
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
390
416
|
|
|
391
417
|
// Action with useAction Hook
|
|
@@ -20,7 +20,10 @@ const ToggleSidebarButton = () => {
|
|
|
20
20
|
variant={"subtle"}
|
|
21
21
|
size={"md"}
|
|
22
22
|
onClick={() => setCollapsed(!collapsed)}
|
|
23
|
-
tooltip={
|
|
23
|
+
tooltip={{
|
|
24
|
+
position: "right",
|
|
25
|
+
label: collapsed ? "Show sidebar" : "Hide sidebar",
|
|
26
|
+
}}
|
|
24
27
|
/>
|
|
25
28
|
);
|
|
26
29
|
};
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ActionIcon,
|
|
3
|
+
Box,
|
|
4
|
+
Collapse,
|
|
5
|
+
CopyButton,
|
|
6
|
+
type MantineSize,
|
|
7
|
+
Text,
|
|
8
|
+
Tooltip,
|
|
9
|
+
} from "@mantine/core";
|
|
10
|
+
import {
|
|
11
|
+
IconCheck,
|
|
12
|
+
IconChevronDown,
|
|
13
|
+
IconChevronRight,
|
|
14
|
+
IconCopy,
|
|
15
|
+
} from "@tabler/icons-react";
|
|
16
|
+
import { type ReactNode, useState } from "react";
|
|
17
|
+
|
|
18
|
+
interface JsonViewerProps {
|
|
19
|
+
data: any;
|
|
20
|
+
defaultExpanded?: boolean;
|
|
21
|
+
maxDepth?: number;
|
|
22
|
+
copyable?: boolean;
|
|
23
|
+
size?: MantineSize;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface JsonNodeProps {
|
|
27
|
+
name?: string;
|
|
28
|
+
value: any;
|
|
29
|
+
depth: number;
|
|
30
|
+
maxDepth: number;
|
|
31
|
+
isLast?: boolean;
|
|
32
|
+
isArrayItem?: boolean;
|
|
33
|
+
size?: MantineSize;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const getSizeConfig = (size: MantineSize = "sm") => {
|
|
37
|
+
const configs = {
|
|
38
|
+
xs: { text: "xs", icon: 12, indent: 16, gap: 2 },
|
|
39
|
+
sm: { text: "sm", icon: 14, indent: 20, gap: 4 },
|
|
40
|
+
md: { text: "md", icon: 16, indent: 24, gap: 6 },
|
|
41
|
+
lg: { text: "lg", icon: 18, indent: 28, gap: 8 },
|
|
42
|
+
xl: { text: "xl", icon: 20, indent: 32, gap: 10 },
|
|
43
|
+
};
|
|
44
|
+
return configs[size] || configs.sm;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const JsonNode = ({
|
|
48
|
+
name,
|
|
49
|
+
value,
|
|
50
|
+
depth,
|
|
51
|
+
maxDepth,
|
|
52
|
+
isLast = false,
|
|
53
|
+
isArrayItem = false,
|
|
54
|
+
size = "sm",
|
|
55
|
+
}: JsonNodeProps) => {
|
|
56
|
+
const [expanded, setExpanded] = useState(depth < 2);
|
|
57
|
+
const sizeConfig = getSizeConfig(size);
|
|
58
|
+
|
|
59
|
+
const getValueType = (val: any): string => {
|
|
60
|
+
if (val === null) return "null";
|
|
61
|
+
if (val === undefined) return "undefined";
|
|
62
|
+
if (Array.isArray(val)) return "array";
|
|
63
|
+
return typeof val;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const valueType = getValueType(value);
|
|
67
|
+
|
|
68
|
+
const renderPrimitive = (val: any): ReactNode => {
|
|
69
|
+
const type = getValueType(val);
|
|
70
|
+
|
|
71
|
+
switch (type) {
|
|
72
|
+
case "string":
|
|
73
|
+
return (
|
|
74
|
+
<Text
|
|
75
|
+
component="span"
|
|
76
|
+
c="teal"
|
|
77
|
+
ff="monospace"
|
|
78
|
+
size={sizeConfig.text}
|
|
79
|
+
style={{ whiteSpace: "nowrap" }}
|
|
80
|
+
>
|
|
81
|
+
"{val}"
|
|
82
|
+
</Text>
|
|
83
|
+
);
|
|
84
|
+
case "number":
|
|
85
|
+
return (
|
|
86
|
+
<Text
|
|
87
|
+
component="span"
|
|
88
|
+
c="blue"
|
|
89
|
+
ff="monospace"
|
|
90
|
+
size={sizeConfig.text}
|
|
91
|
+
style={{ whiteSpace: "nowrap" }}
|
|
92
|
+
>
|
|
93
|
+
{val}
|
|
94
|
+
</Text>
|
|
95
|
+
);
|
|
96
|
+
case "boolean":
|
|
97
|
+
return (
|
|
98
|
+
<Text
|
|
99
|
+
component="span"
|
|
100
|
+
c="violet"
|
|
101
|
+
ff="monospace"
|
|
102
|
+
size={sizeConfig.text}
|
|
103
|
+
style={{ whiteSpace: "nowrap" }}
|
|
104
|
+
>
|
|
105
|
+
{String(val)}
|
|
106
|
+
</Text>
|
|
107
|
+
);
|
|
108
|
+
case "null":
|
|
109
|
+
return (
|
|
110
|
+
<Text
|
|
111
|
+
component="span"
|
|
112
|
+
c="dimmed"
|
|
113
|
+
ff="monospace"
|
|
114
|
+
size={sizeConfig.text}
|
|
115
|
+
style={{ whiteSpace: "nowrap" }}
|
|
116
|
+
>
|
|
117
|
+
null
|
|
118
|
+
</Text>
|
|
119
|
+
);
|
|
120
|
+
case "undefined":
|
|
121
|
+
return (
|
|
122
|
+
<Text
|
|
123
|
+
component="span"
|
|
124
|
+
c="dimmed"
|
|
125
|
+
ff="monospace"
|
|
126
|
+
size={sizeConfig.text}
|
|
127
|
+
style={{ whiteSpace: "nowrap" }}
|
|
128
|
+
>
|
|
129
|
+
undefined
|
|
130
|
+
</Text>
|
|
131
|
+
);
|
|
132
|
+
default:
|
|
133
|
+
return (
|
|
134
|
+
<Text
|
|
135
|
+
component="span"
|
|
136
|
+
ff="monospace"
|
|
137
|
+
size={sizeConfig.text}
|
|
138
|
+
style={{ whiteSpace: "nowrap" }}
|
|
139
|
+
>
|
|
140
|
+
{String(val)}
|
|
141
|
+
</Text>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const renderKey = () => {
|
|
147
|
+
if (!name) return null;
|
|
148
|
+
return (
|
|
149
|
+
<Text
|
|
150
|
+
component="span"
|
|
151
|
+
c="cyan"
|
|
152
|
+
ff="monospace"
|
|
153
|
+
fw={500}
|
|
154
|
+
size={sizeConfig.text}
|
|
155
|
+
>
|
|
156
|
+
{isArrayItem ? `[${name}]` : `"${name}"`}:
|
|
157
|
+
</Text>
|
|
158
|
+
);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
if (valueType === "object" || valueType === "array") {
|
|
162
|
+
const isObject = valueType === "object";
|
|
163
|
+
const entries = isObject
|
|
164
|
+
? Object.entries(value)
|
|
165
|
+
: value.map((v: any, i: number) => [i, v]);
|
|
166
|
+
const isEmpty = entries.length === 0;
|
|
167
|
+
const canExpand = depth < maxDepth && !isEmpty;
|
|
168
|
+
|
|
169
|
+
const preview = isObject ? "{...}" : "[...]";
|
|
170
|
+
const brackets = isObject ? ["{", "}"] : ["[", "]"];
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<Box>
|
|
174
|
+
<Box
|
|
175
|
+
style={{
|
|
176
|
+
display: "flex",
|
|
177
|
+
alignItems: "center",
|
|
178
|
+
gap: sizeConfig.gap,
|
|
179
|
+
minWidth: "max-content",
|
|
180
|
+
}}
|
|
181
|
+
>
|
|
182
|
+
{canExpand && (
|
|
183
|
+
<ActionIcon
|
|
184
|
+
size="xs"
|
|
185
|
+
variant="transparent"
|
|
186
|
+
c="dimmed"
|
|
187
|
+
onClick={() => setExpanded(!expanded)}
|
|
188
|
+
style={{ cursor: "pointer", flexShrink: 0 }}
|
|
189
|
+
>
|
|
190
|
+
{expanded ? (
|
|
191
|
+
<IconChevronDown size={sizeConfig.icon} />
|
|
192
|
+
) : (
|
|
193
|
+
<IconChevronRight size={sizeConfig.icon} />
|
|
194
|
+
)}
|
|
195
|
+
</ActionIcon>
|
|
196
|
+
)}
|
|
197
|
+
{!canExpand && (
|
|
198
|
+
<Box w={sizeConfig.icon + 6} style={{ flexShrink: 0 }} />
|
|
199
|
+
)}
|
|
200
|
+
<Box style={{ flexShrink: 0 }}>{renderKey()}</Box>{" "}
|
|
201
|
+
<Text
|
|
202
|
+
component="span"
|
|
203
|
+
c="dimmed"
|
|
204
|
+
ff="monospace"
|
|
205
|
+
size={sizeConfig.text}
|
|
206
|
+
style={{ flexShrink: 0 }}
|
|
207
|
+
>
|
|
208
|
+
{brackets[0]}
|
|
209
|
+
</Text>
|
|
210
|
+
{!expanded && !isEmpty && (
|
|
211
|
+
<Text
|
|
212
|
+
component="span"
|
|
213
|
+
c="dimmed"
|
|
214
|
+
ff="monospace"
|
|
215
|
+
fs="italic"
|
|
216
|
+
size={sizeConfig.text}
|
|
217
|
+
style={{ flexShrink: 0 }}
|
|
218
|
+
>
|
|
219
|
+
{preview}
|
|
220
|
+
</Text>
|
|
221
|
+
)}
|
|
222
|
+
{(isEmpty || !expanded) && (
|
|
223
|
+
<Text
|
|
224
|
+
component="span"
|
|
225
|
+
c="dimmed"
|
|
226
|
+
ff="monospace"
|
|
227
|
+
size={sizeConfig.text}
|
|
228
|
+
style={{ flexShrink: 0 }}
|
|
229
|
+
>
|
|
230
|
+
{brackets[1]}
|
|
231
|
+
</Text>
|
|
232
|
+
)}
|
|
233
|
+
{!isEmpty && !expanded && (
|
|
234
|
+
<Text
|
|
235
|
+
component="span"
|
|
236
|
+
c="dimmed"
|
|
237
|
+
size={sizeConfig.text}
|
|
238
|
+
style={{ flexShrink: 0 }}
|
|
239
|
+
>
|
|
240
|
+
{entries.length} {entries.length === 1 ? "item" : "items"}
|
|
241
|
+
</Text>
|
|
242
|
+
)}
|
|
243
|
+
</Box>
|
|
244
|
+
|
|
245
|
+
<Collapse in={expanded && canExpand}>
|
|
246
|
+
<Box
|
|
247
|
+
pl={sizeConfig.indent}
|
|
248
|
+
style={{
|
|
249
|
+
borderLeft: "1px solid var(--mantine-color-default-border)",
|
|
250
|
+
marginLeft: Math.floor((sizeConfig.icon + 6) / 2),
|
|
251
|
+
}}
|
|
252
|
+
>
|
|
253
|
+
{entries.map(
|
|
254
|
+
([key, val]: [string | number, any], index: number) => (
|
|
255
|
+
<JsonNode
|
|
256
|
+
key={String(key)}
|
|
257
|
+
name={String(key)}
|
|
258
|
+
value={val}
|
|
259
|
+
depth={depth + 1}
|
|
260
|
+
maxDepth={maxDepth}
|
|
261
|
+
isLast={index === entries.length - 1}
|
|
262
|
+
isArrayItem={!isObject}
|
|
263
|
+
size={size}
|
|
264
|
+
/>
|
|
265
|
+
),
|
|
266
|
+
)}
|
|
267
|
+
</Box>
|
|
268
|
+
<Box style={{ display: "flex", minWidth: "max-content" }}>
|
|
269
|
+
<Box w={sizeConfig.icon + 6} style={{ flexShrink: 0 }} />
|
|
270
|
+
<Text
|
|
271
|
+
c="dimmed"
|
|
272
|
+
ff="monospace"
|
|
273
|
+
size={sizeConfig.text}
|
|
274
|
+
style={{ flexShrink: 0 }}
|
|
275
|
+
>
|
|
276
|
+
{brackets[1]}
|
|
277
|
+
</Text>
|
|
278
|
+
</Box>
|
|
279
|
+
</Collapse>
|
|
280
|
+
</Box>
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
<Box
|
|
286
|
+
style={{
|
|
287
|
+
display: "flex",
|
|
288
|
+
alignItems: "center",
|
|
289
|
+
gap: sizeConfig.gap,
|
|
290
|
+
minWidth: "max-content",
|
|
291
|
+
}}
|
|
292
|
+
>
|
|
293
|
+
<Box w={sizeConfig.icon + 6} style={{ flexShrink: 0 }} />
|
|
294
|
+
<Box style={{ flexShrink: 0 }}>{renderKey()}</Box>
|
|
295
|
+
<Box style={{ flexShrink: 0 }}>{renderPrimitive(value)}</Box>
|
|
296
|
+
{!isLast && (
|
|
297
|
+
<Text
|
|
298
|
+
component="span"
|
|
299
|
+
c="dimmed"
|
|
300
|
+
ff="monospace"
|
|
301
|
+
size={sizeConfig.text}
|
|
302
|
+
style={{ flexShrink: 0 }}
|
|
303
|
+
>
|
|
304
|
+
,
|
|
305
|
+
</Text>
|
|
306
|
+
)}
|
|
307
|
+
</Box>
|
|
308
|
+
);
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
export const JsonViewer = ({
|
|
312
|
+
data,
|
|
313
|
+
defaultExpanded = true,
|
|
314
|
+
maxDepth = 10,
|
|
315
|
+
copyable = true,
|
|
316
|
+
size = "sm",
|
|
317
|
+
}: JsonViewerProps) => {
|
|
318
|
+
const sizeConfig = getSizeConfig(size);
|
|
319
|
+
const copyIconSize = sizeConfig.icon + 2;
|
|
320
|
+
|
|
321
|
+
return (
|
|
322
|
+
<Box pos="relative" w={"100%"}>
|
|
323
|
+
{copyable && (
|
|
324
|
+
<Box pos="absolute" top={0} right={0} style={{ zIndex: 1 }}>
|
|
325
|
+
<CopyButton value={JSON.stringify(data, null, 2)}>
|
|
326
|
+
{({ copied, copy }) => (
|
|
327
|
+
<Tooltip label={copied ? "Copied" : "Copy JSON"}>
|
|
328
|
+
<ActionIcon
|
|
329
|
+
color={copied ? "teal" : "gray"}
|
|
330
|
+
variant="subtle"
|
|
331
|
+
onClick={copy}
|
|
332
|
+
size={size}
|
|
333
|
+
>
|
|
334
|
+
{copied ? (
|
|
335
|
+
<IconCheck size={copyIconSize} />
|
|
336
|
+
) : (
|
|
337
|
+
<IconCopy size={copyIconSize} />
|
|
338
|
+
)}
|
|
339
|
+
</ActionIcon>
|
|
340
|
+
</Tooltip>
|
|
341
|
+
)}
|
|
342
|
+
</CopyButton>
|
|
343
|
+
</Box>
|
|
344
|
+
)}
|
|
345
|
+
<Box pt={copyable ? 30 : 0} style={{ overflowX: "auto" }}>
|
|
346
|
+
<JsonNode value={data} depth={0} maxDepth={maxDepth} size={size} />
|
|
347
|
+
</Box>
|
|
348
|
+
</Box>
|
|
349
|
+
);
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
export default JsonViewer;
|
|
@@ -6,8 +6,6 @@ import {
|
|
|
6
6
|
type FileInputProps,
|
|
7
7
|
Flex,
|
|
8
8
|
Input,
|
|
9
|
-
NumberInput,
|
|
10
|
-
type NumberInputProps,
|
|
11
9
|
PasswordInput,
|
|
12
10
|
type PasswordInputProps,
|
|
13
11
|
Switch,
|
|
@@ -28,6 +26,8 @@ import {
|
|
|
28
26
|
parseInput,
|
|
29
27
|
} from "../../utils/parseInput.ts";
|
|
30
28
|
import ControlDate from "./ControlDate.tsx";
|
|
29
|
+
import ControlNumber, { type ControlNumberProps } from "./ControlNumber.tsx";
|
|
30
|
+
import ControlQueryBuilder from "./ControlQueryBuilder.tsx";
|
|
31
31
|
import ControlSelect, { type ControlSelectProps } from "./ControlSelect.tsx";
|
|
32
32
|
|
|
33
33
|
export interface ControlProps extends GenericControlProps {
|
|
@@ -36,12 +36,13 @@ export interface ControlProps extends GenericControlProps {
|
|
|
36
36
|
select?: boolean | Partial<ControlSelectProps>;
|
|
37
37
|
password?: boolean | PasswordInputProps;
|
|
38
38
|
switch?: boolean | SwitchProps;
|
|
39
|
-
number?: boolean |
|
|
39
|
+
number?: boolean | Partial<ControlNumberProps>;
|
|
40
40
|
file?: boolean | FileInputProps;
|
|
41
41
|
color?: boolean | ColorInputProps;
|
|
42
42
|
date?: boolean | DateInputProps;
|
|
43
43
|
datetime?: boolean | DateTimePickerProps;
|
|
44
44
|
time?: boolean | TimeInputProps;
|
|
45
|
+
query?: any; // Enable query builder mode with schema-aware autocomplete
|
|
45
46
|
custom?: ComponentType<CustomControlProps>;
|
|
46
47
|
}
|
|
47
48
|
|
|
@@ -62,6 +63,7 @@ export interface ControlProps extends GenericControlProps {
|
|
|
62
63
|
* - DateInput (for date format)
|
|
63
64
|
* - DateTimePicker (for date-time format)
|
|
64
65
|
* - TimeInput (for time format)
|
|
66
|
+
* - QueryBuilder (for building type-safe queries with autocomplete)
|
|
65
67
|
* - Custom component
|
|
66
68
|
*
|
|
67
69
|
* Automatically handles labels, descriptions, error messages, required state, and default icons.
|
|
@@ -78,6 +80,22 @@ const Control = (_props: ControlProps) => {
|
|
|
78
80
|
...schema.$control,
|
|
79
81
|
};
|
|
80
82
|
|
|
83
|
+
//region <QueryBuilder/>
|
|
84
|
+
if (props.query) {
|
|
85
|
+
return (
|
|
86
|
+
<ControlQueryBuilder
|
|
87
|
+
{...props.input.props}
|
|
88
|
+
{...inputProps}
|
|
89
|
+
schema={props.query}
|
|
90
|
+
value={props.input.props.value}
|
|
91
|
+
onChange={(value) => {
|
|
92
|
+
props.input.set(value);
|
|
93
|
+
}}
|
|
94
|
+
/>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
//endregion
|
|
98
|
+
|
|
81
99
|
//region <Custom/>
|
|
82
100
|
if (props.custom) {
|
|
83
101
|
const Custom = props.custom;
|
|
@@ -104,16 +122,15 @@ const Control = (_props: ControlProps) => {
|
|
|
104
122
|
(props.input.schema.type === "number" ||
|
|
105
123
|
props.input.schema.type === "integer"))
|
|
106
124
|
) {
|
|
107
|
-
const
|
|
125
|
+
const controlNumberProps =
|
|
108
126
|
typeof props.number === "object" ? props.number : {};
|
|
109
|
-
const { type, ...inputPropsWithoutType } = props.input.props;
|
|
110
127
|
return (
|
|
111
|
-
<
|
|
112
|
-
{
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
{
|
|
116
|
-
{...
|
|
128
|
+
<ControlNumber
|
|
129
|
+
input={props.input}
|
|
130
|
+
title={props.title}
|
|
131
|
+
description={props.description}
|
|
132
|
+
icon={icon}
|
|
133
|
+
{...controlNumberProps}
|
|
117
134
|
/>
|
|
118
135
|
);
|
|
119
136
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { useEvents } from "@alepha/react";
|
|
2
|
+
import { useFormState } from "@alepha/react-form";
|
|
3
|
+
import {
|
|
4
|
+
Input,
|
|
5
|
+
NumberInput,
|
|
6
|
+
type NumberInputProps,
|
|
7
|
+
Slider,
|
|
8
|
+
type SliderProps,
|
|
9
|
+
} from "@mantine/core";
|
|
10
|
+
import { useRef, useState } from "react";
|
|
11
|
+
import {
|
|
12
|
+
type GenericControlProps,
|
|
13
|
+
parseInput,
|
|
14
|
+
} from "../../utils/parseInput.ts";
|
|
15
|
+
|
|
16
|
+
export interface ControlNumberProps extends GenericControlProps {
|
|
17
|
+
numberInputProps?: Partial<NumberInputProps>;
|
|
18
|
+
sliderProps?: Partial<SliderProps>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
*
|
|
23
|
+
*/
|
|
24
|
+
const ControlNumber = (props: ControlNumberProps) => {
|
|
25
|
+
const form = useFormState(props.input);
|
|
26
|
+
const { inputProps, id, icon } = parseInput(props, form);
|
|
27
|
+
const ref = useRef<HTMLInputElement | null>(null);
|
|
28
|
+
|
|
29
|
+
// HTML Reset doesn't trigger on <NumberInput /> so we handle it manually
|
|
30
|
+
|
|
31
|
+
const [value, setValue] = useState<number | undefined>(
|
|
32
|
+
props.input.props.defaultValue,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
useEvents(
|
|
36
|
+
{
|
|
37
|
+
"form:reset": (event) => {
|
|
38
|
+
if (event.id === props.input?.form.id && ref.current) {
|
|
39
|
+
setValue(props.input.props.defaultValue);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
[props.input],
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
if (!props.input?.props) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const { type, ...inputPropsWithoutType } = props.input.props;
|
|
51
|
+
|
|
52
|
+
if (props.sliderProps) {
|
|
53
|
+
return (
|
|
54
|
+
<Input.Wrapper {...inputProps}>
|
|
55
|
+
<div
|
|
56
|
+
style={{
|
|
57
|
+
height: 32,
|
|
58
|
+
padding: 8,
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
<Slider
|
|
62
|
+
{...inputProps}
|
|
63
|
+
ref={ref}
|
|
64
|
+
id={id}
|
|
65
|
+
{...inputPropsWithoutType}
|
|
66
|
+
{...props.sliderProps}
|
|
67
|
+
value={value}
|
|
68
|
+
onChange={(val) => {
|
|
69
|
+
setValue(val);
|
|
70
|
+
props.input.set(val);
|
|
71
|
+
}}
|
|
72
|
+
/>
|
|
73
|
+
</div>
|
|
74
|
+
</Input.Wrapper>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<NumberInput
|
|
80
|
+
{...inputProps}
|
|
81
|
+
ref={ref}
|
|
82
|
+
id={id}
|
|
83
|
+
leftSection={icon}
|
|
84
|
+
{...inputPropsWithoutType}
|
|
85
|
+
{...props.numberInputProps}
|
|
86
|
+
value={value ?? ""}
|
|
87
|
+
onChange={(val) => {
|
|
88
|
+
const newValue = val !== null ? Number(val) : undefined;
|
|
89
|
+
setValue(newValue);
|
|
90
|
+
props.input.set(newValue);
|
|
91
|
+
}}
|
|
92
|
+
/>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export default ControlNumber;
|