@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
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import type { TObject } from "@alepha/core";
|
|
2
|
+
import { parseQueryString } from "@alepha/postgres";
|
|
3
|
+
import { useEvents } from "@alepha/react";
|
|
4
|
+
import {
|
|
5
|
+
ActionIcon,
|
|
6
|
+
Badge,
|
|
7
|
+
Divider,
|
|
8
|
+
Flex,
|
|
9
|
+
Group,
|
|
10
|
+
Popover,
|
|
11
|
+
Stack,
|
|
12
|
+
Text,
|
|
13
|
+
TextInput,
|
|
14
|
+
type TextInputProps,
|
|
15
|
+
} from "@mantine/core";
|
|
16
|
+
import { IconFilter, IconInfoTriangle, IconX } from "@tabler/icons-react";
|
|
17
|
+
import { useRef, useState } from "react";
|
|
18
|
+
import { ui } from "../../constants/ui.ts";
|
|
19
|
+
import {
|
|
20
|
+
extractSchemaFields,
|
|
21
|
+
OPERATOR_INFO,
|
|
22
|
+
type SchemaField,
|
|
23
|
+
} from "../../utils/extractSchemaFields.ts";
|
|
24
|
+
import ActionButton from "../buttons/ActionButton.tsx";
|
|
25
|
+
|
|
26
|
+
export interface ControlQueryBuilderProps
|
|
27
|
+
extends Omit<TextInputProps, "value" | "onChange"> {
|
|
28
|
+
schema?: TObject;
|
|
29
|
+
value?: string;
|
|
30
|
+
onChange?: (value: string) => void;
|
|
31
|
+
placeholder?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Query builder with text input and help popover.
|
|
36
|
+
* Generates query strings for parseQueryString syntax.
|
|
37
|
+
*/
|
|
38
|
+
const ControlQueryBuilder = ({
|
|
39
|
+
schema,
|
|
40
|
+
value = "",
|
|
41
|
+
onChange,
|
|
42
|
+
placeholder = "Enter query or click for assistance...",
|
|
43
|
+
...textInputProps
|
|
44
|
+
}: ControlQueryBuilderProps) => {
|
|
45
|
+
const [helpOpened, setHelpOpened] = useState(false);
|
|
46
|
+
const [textValue, setTextValue] = useState(value);
|
|
47
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
48
|
+
const fields = schema ? extractSchemaFields(schema) : [];
|
|
49
|
+
const [error, setError] = useState<string | null>(null);
|
|
50
|
+
|
|
51
|
+
const isValid = (value: string) => {
|
|
52
|
+
try {
|
|
53
|
+
parseQueryString(value.trim());
|
|
54
|
+
} catch (e) {
|
|
55
|
+
setError((e as Error).message);
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
setError(null);
|
|
59
|
+
return true;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const handleTextChange = (newValue: string) => {
|
|
63
|
+
setTextValue(newValue);
|
|
64
|
+
if (isValid(newValue)) {
|
|
65
|
+
onChange?.(newValue);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const handleClear = () => {
|
|
70
|
+
setTextValue("");
|
|
71
|
+
onChange?.("");
|
|
72
|
+
isValid("");
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const handleInsert = (text: string) => {
|
|
76
|
+
const newValue = textValue ? `${textValue}${text} ` : `${text} `;
|
|
77
|
+
setTextValue(newValue);
|
|
78
|
+
if (isValid(newValue)) {
|
|
79
|
+
onChange?.(newValue);
|
|
80
|
+
}
|
|
81
|
+
// Refocus the input after inserting
|
|
82
|
+
setTimeout(() => {
|
|
83
|
+
inputRef.current?.focus();
|
|
84
|
+
// set cursor to end
|
|
85
|
+
const length = inputRef.current?.value.length || 0;
|
|
86
|
+
inputRef.current?.setSelectionRange(length, length);
|
|
87
|
+
}, 0);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
useEvents(
|
|
91
|
+
{
|
|
92
|
+
"form:change": (event) => {
|
|
93
|
+
if (event.id === inputRef.current?.form?.id) {
|
|
94
|
+
if (event.path === (textInputProps as any)["data-path"]) {
|
|
95
|
+
setTextValue(event.value ?? "");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
[],
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<Popover
|
|
105
|
+
width={800}
|
|
106
|
+
position="bottom-start"
|
|
107
|
+
shadow="md"
|
|
108
|
+
opened={helpOpened}
|
|
109
|
+
onChange={setHelpOpened}
|
|
110
|
+
closeOnClickOutside
|
|
111
|
+
closeOnEscape
|
|
112
|
+
transitionProps={{
|
|
113
|
+
transition: "fade-up",
|
|
114
|
+
duration: 200,
|
|
115
|
+
timingFunction: "ease",
|
|
116
|
+
}}
|
|
117
|
+
>
|
|
118
|
+
<Popover.Target>
|
|
119
|
+
<TextInput
|
|
120
|
+
ref={inputRef}
|
|
121
|
+
placeholder={placeholder}
|
|
122
|
+
value={textValue}
|
|
123
|
+
onChange={(e) => handleTextChange(e.currentTarget.value)}
|
|
124
|
+
onFocus={() => setHelpOpened(true)}
|
|
125
|
+
leftSection={
|
|
126
|
+
error ? <IconInfoTriangle size={16} /> : <IconFilter size={16} />
|
|
127
|
+
}
|
|
128
|
+
rightSection={
|
|
129
|
+
textValue && (
|
|
130
|
+
<ActionIcon
|
|
131
|
+
size="sm"
|
|
132
|
+
variant="subtle"
|
|
133
|
+
color="gray"
|
|
134
|
+
onClick={handleClear}
|
|
135
|
+
>
|
|
136
|
+
<IconX size={14} />
|
|
137
|
+
</ActionIcon>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
{...textInputProps}
|
|
141
|
+
/>
|
|
142
|
+
</Popover.Target>
|
|
143
|
+
<Popover.Dropdown
|
|
144
|
+
bg={"transparent"}
|
|
145
|
+
p={"xs"}
|
|
146
|
+
bd={`1px solid ${ui.colors.border}`}
|
|
147
|
+
style={{
|
|
148
|
+
backdropFilter: "blur(20px)",
|
|
149
|
+
}}
|
|
150
|
+
>
|
|
151
|
+
<QueryHelp fields={fields} onInsert={handleInsert} />
|
|
152
|
+
</Popover.Dropdown>
|
|
153
|
+
</Popover>
|
|
154
|
+
);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
158
|
+
// Query Help Component
|
|
159
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
interface QueryHelpProps {
|
|
162
|
+
fields: SchemaField[];
|
|
163
|
+
onInsert: (text: string) => void;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function QueryHelp({ fields, onInsert }: QueryHelpProps) {
|
|
167
|
+
return (
|
|
168
|
+
<Group
|
|
169
|
+
gap="md"
|
|
170
|
+
align="flex-start"
|
|
171
|
+
wrap="nowrap"
|
|
172
|
+
bg={ui.colors.surface}
|
|
173
|
+
p={"sm"}
|
|
174
|
+
bdrs={"sm"}
|
|
175
|
+
>
|
|
176
|
+
{/* Left Column: Operators */}
|
|
177
|
+
<Stack gap="md" style={{ flex: 1 }}>
|
|
178
|
+
{/* Available Operators */}
|
|
179
|
+
<Stack gap="xs">
|
|
180
|
+
<Text size="sm" fw={600}>
|
|
181
|
+
Operators
|
|
182
|
+
</Text>
|
|
183
|
+
<Stack gap={4}>
|
|
184
|
+
{Object.entries(OPERATOR_INFO).map(([key, info]) => (
|
|
185
|
+
<Group key={key} gap="xs" wrap="nowrap">
|
|
186
|
+
<ActionButton
|
|
187
|
+
px={"xs"}
|
|
188
|
+
size={"xs"}
|
|
189
|
+
h={24}
|
|
190
|
+
variant={"default"}
|
|
191
|
+
justify={"center"}
|
|
192
|
+
miw={48}
|
|
193
|
+
onClick={() => onInsert(info.symbol)}
|
|
194
|
+
>
|
|
195
|
+
{info.symbol}
|
|
196
|
+
</ActionButton>
|
|
197
|
+
<Text size="xs" c="dimmed" style={{ flex: 1 }}>
|
|
198
|
+
{info.label}
|
|
199
|
+
</Text>
|
|
200
|
+
</Group>
|
|
201
|
+
))}
|
|
202
|
+
</Stack>
|
|
203
|
+
</Stack>
|
|
204
|
+
|
|
205
|
+
<Divider />
|
|
206
|
+
|
|
207
|
+
{/* Logic Operators */}
|
|
208
|
+
<Stack gap="xs">
|
|
209
|
+
<Text size="sm" fw={600}>
|
|
210
|
+
Logic
|
|
211
|
+
</Text>
|
|
212
|
+
<Stack gap={4}>
|
|
213
|
+
<Group gap="xs" wrap="nowrap">
|
|
214
|
+
<ActionButton
|
|
215
|
+
px={"xs"}
|
|
216
|
+
size={"xs"}
|
|
217
|
+
h={24}
|
|
218
|
+
variant={"default"}
|
|
219
|
+
justify={"center"}
|
|
220
|
+
miw={48}
|
|
221
|
+
onClick={() => onInsert("&")}
|
|
222
|
+
>
|
|
223
|
+
&
|
|
224
|
+
</ActionButton>
|
|
225
|
+
<Text size="xs" c="dimmed">
|
|
226
|
+
AND
|
|
227
|
+
</Text>
|
|
228
|
+
</Group>
|
|
229
|
+
<Group gap="xs" wrap="nowrap">
|
|
230
|
+
<ActionButton
|
|
231
|
+
px={"xs"}
|
|
232
|
+
size={"xs"}
|
|
233
|
+
h={24}
|
|
234
|
+
variant={"default"}
|
|
235
|
+
justify={"center"}
|
|
236
|
+
miw={48}
|
|
237
|
+
onClick={() => onInsert("|")}
|
|
238
|
+
>
|
|
239
|
+
|
|
|
240
|
+
</ActionButton>
|
|
241
|
+
<Text size="xs" c="dimmed">
|
|
242
|
+
OR
|
|
243
|
+
</Text>
|
|
244
|
+
</Group>
|
|
245
|
+
</Stack>
|
|
246
|
+
</Stack>
|
|
247
|
+
</Stack>
|
|
248
|
+
|
|
249
|
+
{/* Divider */}
|
|
250
|
+
{fields.length > 0 && <Divider orientation="vertical" />}
|
|
251
|
+
|
|
252
|
+
{/* Right Column: Fields */}
|
|
253
|
+
{fields.length > 0 && (
|
|
254
|
+
<Flex direction={"column"} gap="xs" style={{ flex: 2 }}>
|
|
255
|
+
<Text size="sm" fw={600}>
|
|
256
|
+
Fields
|
|
257
|
+
</Text>
|
|
258
|
+
<Flex
|
|
259
|
+
direction={"column"}
|
|
260
|
+
gap={4}
|
|
261
|
+
style={{ maxHeight: 300, overflowY: "auto" }}
|
|
262
|
+
>
|
|
263
|
+
{fields.map((field) => (
|
|
264
|
+
<Flex key={field.path} gap="xs" wrap="nowrap" align="flex-start">
|
|
265
|
+
<ActionButton
|
|
266
|
+
px={"xs"}
|
|
267
|
+
size={"xs"}
|
|
268
|
+
h={24}
|
|
269
|
+
variant={"default"}
|
|
270
|
+
justify={"end"}
|
|
271
|
+
miw={120}
|
|
272
|
+
onClick={() => onInsert(field.path)}
|
|
273
|
+
>
|
|
274
|
+
{field.path}
|
|
275
|
+
</ActionButton>
|
|
276
|
+
<Flex
|
|
277
|
+
mt={3}
|
|
278
|
+
direction={"column"}
|
|
279
|
+
gap={2}
|
|
280
|
+
style={{ flex: 1, minWidth: 0 }}
|
|
281
|
+
>
|
|
282
|
+
<Text size="xs" c="dimmed" lineClamp={1}>
|
|
283
|
+
{field.description || field.type}
|
|
284
|
+
</Text>
|
|
285
|
+
{field.enum && (
|
|
286
|
+
<Group gap={0} wrap="wrap">
|
|
287
|
+
{field.enum.map((enumValue) => (
|
|
288
|
+
<ActionButton
|
|
289
|
+
px={"xs"}
|
|
290
|
+
size={"xs"}
|
|
291
|
+
h={24}
|
|
292
|
+
key={enumValue}
|
|
293
|
+
onClick={() => onInsert(enumValue)}
|
|
294
|
+
>
|
|
295
|
+
{enumValue}
|
|
296
|
+
</ActionButton>
|
|
297
|
+
))}
|
|
298
|
+
</Group>
|
|
299
|
+
)}
|
|
300
|
+
</Flex>
|
|
301
|
+
<Badge size="xs" variant="light" style={{ flexShrink: 0 }}>
|
|
302
|
+
{field.type}
|
|
303
|
+
</Badge>
|
|
304
|
+
</Flex>
|
|
305
|
+
))}
|
|
306
|
+
</Flex>
|
|
307
|
+
</Flex>
|
|
308
|
+
)}
|
|
309
|
+
</Group>
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export default ControlQueryBuilder;
|
|
@@ -97,7 +97,7 @@ const ControlSelect = (props: ControlSelectProps) => {
|
|
|
97
97
|
|
|
98
98
|
return (
|
|
99
99
|
<Input.Wrapper {...inputProps}>
|
|
100
|
-
<Flex
|
|
100
|
+
<Flex>
|
|
101
101
|
<SegmentedControl
|
|
102
102
|
disabled={inputProps.disabled}
|
|
103
103
|
defaultValue={String(props.input.props.defaultValue)}
|
|
@@ -19,6 +19,7 @@ export interface TypeFormProps<T extends TObject> {
|
|
|
19
19
|
lg?: number;
|
|
20
20
|
xl?: number;
|
|
21
21
|
};
|
|
22
|
+
schema?: TObject;
|
|
22
23
|
children?: (input: FormModel<T>["input"]) => ReactNode;
|
|
23
24
|
controlProps?: Partial<Omit<ControlProps, "input">>;
|
|
24
25
|
skipFormElement?: boolean;
|
|
@@ -63,11 +64,12 @@ const TypeForm = <T extends TObject>(props: TypeFormProps<T>) => {
|
|
|
63
64
|
submitButtonProps,
|
|
64
65
|
} = props;
|
|
65
66
|
|
|
66
|
-
|
|
67
|
+
const schema = props.schema || form.options.schema;
|
|
68
|
+
if (!schema?.properties) {
|
|
67
69
|
return null;
|
|
68
70
|
}
|
|
69
71
|
|
|
70
|
-
const fieldNames = Object.keys(
|
|
72
|
+
const fieldNames = Object.keys(schema.properties);
|
|
71
73
|
|
|
72
74
|
// Filter out unsupported field types (objects only, arrays are now supported)
|
|
73
75
|
const supportedFields = fieldNames.filter((fieldName) => {
|
|
@@ -140,11 +142,11 @@ const TypeForm = <T extends TObject>(props: TypeFormProps<T>) => {
|
|
|
140
142
|
<Flex direction={"column"} gap={"sm"}>
|
|
141
143
|
{renderFields()}
|
|
142
144
|
{!skipSubmitButton && (
|
|
143
|
-
<Flex>
|
|
145
|
+
<Flex gap={"sm"}>
|
|
144
146
|
<ActionButton form={form} {...submitButtonProps}>
|
|
145
147
|
{submitButtonProps?.children ?? "Submit"}
|
|
146
148
|
</ActionButton>
|
|
147
|
-
<
|
|
149
|
+
<ActionButton type={"reset"}>Reset</ActionButton>
|
|
148
150
|
</Flex>
|
|
149
151
|
)}
|
|
150
152
|
</Flex>
|
|
@@ -27,7 +27,14 @@ export interface AdminShellProps {
|
|
|
27
27
|
|
|
28
28
|
declare module "@alepha/core" {
|
|
29
29
|
interface State {
|
|
30
|
+
/**
|
|
31
|
+
* Whether the sidebar is opened or closed.
|
|
32
|
+
*/
|
|
30
33
|
"alepha.ui.sidebar.opened"?: boolean;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Whether the sidebar is collapsed (narrow) or expanded (wide).
|
|
37
|
+
*/
|
|
31
38
|
"alepha.ui.sidebar.collapsed"?: boolean;
|
|
32
39
|
}
|
|
33
40
|
}
|
|
@@ -56,18 +56,20 @@ export const Sidebar = (props: SidebarProps) => {
|
|
|
56
56
|
if (item.type === "section") {
|
|
57
57
|
if (props.collapsed) return;
|
|
58
58
|
return (
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
59
|
+
<Flex mt={"md"} mb={"xs"} align={"center"} gap={"xs"}>
|
|
60
|
+
<ThemeIcon c={"dimmed"} size={"xs"} variant={"transparent"}>
|
|
61
|
+
{item.icon}
|
|
62
|
+
</ThemeIcon>
|
|
63
|
+
<Text
|
|
64
|
+
key={key}
|
|
65
|
+
size={"xs"}
|
|
66
|
+
c={"dimmed"}
|
|
67
|
+
tt={"uppercase"}
|
|
68
|
+
fw={"bold"}
|
|
69
|
+
>
|
|
70
|
+
{item.label}
|
|
71
|
+
</Text>
|
|
72
|
+
</Flex>
|
|
71
73
|
);
|
|
72
74
|
}
|
|
73
75
|
}
|
|
@@ -191,7 +193,9 @@ export const SidebarItem = (props: SidebarItemProps) => {
|
|
|
191
193
|
if (level > maxLevel) return null;
|
|
192
194
|
|
|
193
195
|
const handleItemClick = (e: MouseEvent) => {
|
|
194
|
-
|
|
196
|
+
if (!props.item.target) {
|
|
197
|
+
e.preventDefault();
|
|
198
|
+
}
|
|
195
199
|
if (item.children && item.children.length > 0) {
|
|
196
200
|
setIsOpen(!isOpen);
|
|
197
201
|
} else {
|
|
@@ -206,6 +210,7 @@ export const SidebarItem = (props: SidebarItemProps) => {
|
|
|
206
210
|
w={"100%"}
|
|
207
211
|
justify="space-between"
|
|
208
212
|
href={props.item.href}
|
|
213
|
+
target={props.item.target}
|
|
209
214
|
variant={"subtle"}
|
|
210
215
|
size={
|
|
211
216
|
props.item.theme?.size ??
|
|
@@ -310,7 +315,9 @@ const SidebarCollapsedItem = (props: SidebarItemProps) => {
|
|
|
310
315
|
const [isOpen, setIsOpen] = useState<boolean>(isActive(item));
|
|
311
316
|
|
|
312
317
|
const handleItemClick = (e: MouseEvent) => {
|
|
313
|
-
|
|
318
|
+
if (!props.item.target) {
|
|
319
|
+
e.preventDefault();
|
|
320
|
+
}
|
|
314
321
|
if (item.children && item.children.length > 0) {
|
|
315
322
|
setIsOpen(!isOpen);
|
|
316
323
|
} else {
|
|
@@ -332,6 +339,7 @@ const SidebarCollapsedItem = (props: SidebarItemProps) => {
|
|
|
332
339
|
onClick={handleItemClick}
|
|
333
340
|
icon={item.icon ?? <IconSquareRounded />}
|
|
334
341
|
href={props.item.href}
|
|
342
|
+
target={props.item.target}
|
|
335
343
|
menu={
|
|
336
344
|
item.children
|
|
337
345
|
? {
|
|
@@ -384,6 +392,7 @@ export interface SidebarSearch extends SidebarAbstractItem {
|
|
|
384
392
|
export interface SidebarSection extends SidebarAbstractItem {
|
|
385
393
|
type: "section";
|
|
386
394
|
label: string;
|
|
395
|
+
icon?: ReactNode;
|
|
387
396
|
}
|
|
388
397
|
|
|
389
398
|
export interface SidebarMenuItem extends SidebarAbstractItem {
|
|
@@ -391,6 +400,7 @@ export interface SidebarMenuItem extends SidebarAbstractItem {
|
|
|
391
400
|
description?: string;
|
|
392
401
|
icon?: ReactNode;
|
|
393
402
|
href?: string;
|
|
403
|
+
target?: "_blank" | "_self" | "_parent" | "_top";
|
|
394
404
|
activeStartsWith?: boolean; // Use startWith matching for active state
|
|
395
405
|
onClick?: () => void;
|
|
396
406
|
children?: SidebarMenuItem[];
|