@alepha/ui 0.11.5 → 0.11.6
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/index.d.ts +127 -24
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +644 -57
- package/dist/index.js.map +1 -1
- package/package.json +21 -20
- package/src/components/buttons/ActionButton.tsx +29 -3
- package/src/components/buttons/ToggleSidebarButton.tsx +4 -1
- package/src/components/form/Control.tsx +28 -11
- package/src/components/form/ControlNumber.tsx +96 -0
- package/src/components/form/ControlQueryBuilder.tsx +248 -0
- 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 +238 -15
- package/src/constants/ui.ts +1 -0
- package/src/index.ts +2 -0
- package/src/utils/extractSchemaFields.ts +171 -0
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.6",
|
|
10
10
|
"type": "module",
|
|
11
11
|
"engines": {
|
|
12
12
|
"node": ">=22.0.0"
|
|
@@ -20,32 +20,33 @@
|
|
|
20
20
|
"src"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@alepha/core": "0.11.
|
|
24
|
-
"@alepha/
|
|
25
|
-
"@alepha/react
|
|
26
|
-
"@alepha/react-
|
|
27
|
-
"@alepha/react-
|
|
28
|
-
"@alepha/
|
|
29
|
-
"@
|
|
30
|
-
"@mantine/
|
|
31
|
-
"@mantine/
|
|
32
|
-
"@mantine/
|
|
33
|
-
"@mantine/
|
|
34
|
-
"@mantine/
|
|
35
|
-
"@mantine/
|
|
23
|
+
"@alepha/core": "0.11.6",
|
|
24
|
+
"@alepha/datetime": "0.11.6",
|
|
25
|
+
"@alepha/react": "0.11.6",
|
|
26
|
+
"@alepha/react-form": "0.11.6",
|
|
27
|
+
"@alepha/react-head": "0.11.6",
|
|
28
|
+
"@alepha/react-i18n": "0.11.6",
|
|
29
|
+
"@alepha/server": "0.11.6",
|
|
30
|
+
"@mantine/core": "^8.3.7",
|
|
31
|
+
"@mantine/dates": "^8.3.7",
|
|
32
|
+
"@mantine/hooks": "^8.3.7",
|
|
33
|
+
"@mantine/modals": "^8.3.7",
|
|
34
|
+
"@mantine/notifications": "^8.3.7",
|
|
35
|
+
"@mantine/nprogress": "^8.3.7",
|
|
36
|
+
"@mantine/spotlight": "^8.3.7",
|
|
36
37
|
"@tabler/icons-react": "^3.35.0",
|
|
37
38
|
"dayjs": "^1.11.19"
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
|
40
|
-
"@alepha/cli": "0.11.
|
|
41
|
-
"@alepha/vite": "0.11.
|
|
42
|
-
"@biomejs/biome": "^2.3.
|
|
41
|
+
"@alepha/cli": "0.11.6",
|
|
42
|
+
"@alepha/vite": "0.11.6",
|
|
43
|
+
"@biomejs/biome": "^2.3.4",
|
|
43
44
|
"react": "^19.2.0",
|
|
44
45
|
"react-dom": "^19.2.0",
|
|
45
|
-
"tsdown": "^0.16.
|
|
46
|
+
"tsdown": "^0.16.1",
|
|
46
47
|
"typescript": "^5.9.3",
|
|
47
|
-
"vite": "^7.
|
|
48
|
-
"vitest": "^4.0.
|
|
48
|
+
"vite": "^7.2.2",
|
|
49
|
+
"vitest": "^4.0.8"
|
|
49
50
|
},
|
|
50
51
|
"peerDependencies": {
|
|
51
52
|
"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
|
};
|
|
@@ -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
|
+
<Input.Wrapper {...inputProps}>
|
|
87
|
+
<ControlQueryBuilder
|
|
88
|
+
schema={props.query}
|
|
89
|
+
value={props.input.props.value}
|
|
90
|
+
onChange={(value) => {
|
|
91
|
+
props.input.set(value);
|
|
92
|
+
}}
|
|
93
|
+
/>
|
|
94
|
+
</Input.Wrapper>
|
|
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;
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import type { TObject } from "@alepha/core";
|
|
2
|
+
import {
|
|
3
|
+
ActionIcon,
|
|
4
|
+
Badge,
|
|
5
|
+
Code,
|
|
6
|
+
Divider,
|
|
7
|
+
Group,
|
|
8
|
+
Popover,
|
|
9
|
+
Stack,
|
|
10
|
+
Text,
|
|
11
|
+
TextInput,
|
|
12
|
+
type TextInputProps,
|
|
13
|
+
} from "@mantine/core";
|
|
14
|
+
import { IconFilter, IconX } from "@tabler/icons-react";
|
|
15
|
+
import { useRef, useState } from "react";
|
|
16
|
+
import {
|
|
17
|
+
extractSchemaFields,
|
|
18
|
+
OPERATOR_INFO,
|
|
19
|
+
type SchemaField,
|
|
20
|
+
} from "../../utils/extractSchemaFields.ts";
|
|
21
|
+
|
|
22
|
+
export interface ControlQueryBuilderProps
|
|
23
|
+
extends Omit<TextInputProps, "value" | "onChange"> {
|
|
24
|
+
schema?: TObject;
|
|
25
|
+
value?: string;
|
|
26
|
+
onChange?: (value: string) => void;
|
|
27
|
+
placeholder?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Query builder with text input and help popover.
|
|
32
|
+
* Generates query strings for parseQueryString syntax.
|
|
33
|
+
*/
|
|
34
|
+
const ControlQueryBuilder = ({
|
|
35
|
+
schema,
|
|
36
|
+
value = "",
|
|
37
|
+
onChange,
|
|
38
|
+
placeholder = "Enter query or click help for assistance...",
|
|
39
|
+
...textInputProps
|
|
40
|
+
}: ControlQueryBuilderProps) => {
|
|
41
|
+
const [helpOpened, setHelpOpened] = useState(false);
|
|
42
|
+
const [textValue, setTextValue] = useState(value);
|
|
43
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
44
|
+
const fields = schema ? extractSchemaFields(schema) : [];
|
|
45
|
+
|
|
46
|
+
const handleTextChange = (newValue: string) => {
|
|
47
|
+
setTextValue(newValue);
|
|
48
|
+
onChange?.(newValue);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const handleClear = () => {
|
|
52
|
+
setTextValue("");
|
|
53
|
+
onChange?.("");
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const handleInsert = (text: string) => {
|
|
57
|
+
const newValue = textValue ? `${textValue}${text} ` : `${text} `;
|
|
58
|
+
setTextValue(newValue);
|
|
59
|
+
onChange?.(newValue);
|
|
60
|
+
// Refocus the input after inserting
|
|
61
|
+
setTimeout(() => {
|
|
62
|
+
inputRef.current?.focus();
|
|
63
|
+
}, 0);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<Popover
|
|
68
|
+
width={800}
|
|
69
|
+
position="bottom-start"
|
|
70
|
+
shadow="md"
|
|
71
|
+
opened={helpOpened}
|
|
72
|
+
onChange={setHelpOpened}
|
|
73
|
+
closeOnClickOutside
|
|
74
|
+
closeOnEscape
|
|
75
|
+
withArrow
|
|
76
|
+
arrowSize={14}
|
|
77
|
+
transitionProps={{
|
|
78
|
+
transition: "fade-down",
|
|
79
|
+
duration: 200,
|
|
80
|
+
timingFunction: "ease",
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
<Popover.Target>
|
|
84
|
+
<TextInput
|
|
85
|
+
ref={inputRef}
|
|
86
|
+
placeholder={placeholder}
|
|
87
|
+
value={textValue}
|
|
88
|
+
onChange={(e) => handleTextChange(e.currentTarget.value)}
|
|
89
|
+
onFocus={() => setHelpOpened(true)}
|
|
90
|
+
leftSection={<IconFilter size={16} />}
|
|
91
|
+
rightSection={
|
|
92
|
+
textValue && (
|
|
93
|
+
<ActionIcon
|
|
94
|
+
size="sm"
|
|
95
|
+
variant="subtle"
|
|
96
|
+
color="gray"
|
|
97
|
+
onClick={handleClear}
|
|
98
|
+
>
|
|
99
|
+
<IconX size={14} />
|
|
100
|
+
</ActionIcon>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
{...textInputProps}
|
|
104
|
+
/>
|
|
105
|
+
</Popover.Target>
|
|
106
|
+
<Popover.Dropdown>
|
|
107
|
+
<QueryHelp fields={fields} onInsert={handleInsert} />
|
|
108
|
+
</Popover.Dropdown>
|
|
109
|
+
</Popover>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
114
|
+
// Query Help Component
|
|
115
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
interface QueryHelpProps {
|
|
118
|
+
fields: SchemaField[];
|
|
119
|
+
onInsert: (text: string) => void;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function QueryHelp({ fields, onInsert }: QueryHelpProps) {
|
|
123
|
+
return (
|
|
124
|
+
<Group gap="md" align="flex-start" wrap="nowrap">
|
|
125
|
+
{/* Left Column: Operators */}
|
|
126
|
+
<Stack gap="md" style={{ flex: 1 }}>
|
|
127
|
+
{/* Available Operators */}
|
|
128
|
+
<Stack gap="xs">
|
|
129
|
+
<Text size="sm" fw={600}>
|
|
130
|
+
Operators
|
|
131
|
+
</Text>
|
|
132
|
+
<Stack gap={4}>
|
|
133
|
+
{Object.entries(OPERATOR_INFO).map(([key, info]) => (
|
|
134
|
+
<Group key={key} gap="xs" wrap="nowrap">
|
|
135
|
+
<Code
|
|
136
|
+
style={{
|
|
137
|
+
minWidth: 35,
|
|
138
|
+
textAlign: "center",
|
|
139
|
+
cursor: "pointer",
|
|
140
|
+
}}
|
|
141
|
+
onClick={() => onInsert(info.symbol)}
|
|
142
|
+
>
|
|
143
|
+
{info.symbol}
|
|
144
|
+
</Code>
|
|
145
|
+
<Text size="xs" c="dimmed" style={{ flex: 1 }}>
|
|
146
|
+
{info.label}
|
|
147
|
+
</Text>
|
|
148
|
+
</Group>
|
|
149
|
+
))}
|
|
150
|
+
</Stack>
|
|
151
|
+
</Stack>
|
|
152
|
+
|
|
153
|
+
<Divider />
|
|
154
|
+
|
|
155
|
+
{/* Logic Operators */}
|
|
156
|
+
<Stack gap="xs">
|
|
157
|
+
<Text size="sm" fw={600}>
|
|
158
|
+
Logic
|
|
159
|
+
</Text>
|
|
160
|
+
<Stack gap={4}>
|
|
161
|
+
<Group gap="xs" wrap="nowrap">
|
|
162
|
+
<Code
|
|
163
|
+
style={{
|
|
164
|
+
minWidth: 35,
|
|
165
|
+
textAlign: "center",
|
|
166
|
+
cursor: "pointer",
|
|
167
|
+
}}
|
|
168
|
+
onClick={() => onInsert("&")}
|
|
169
|
+
>
|
|
170
|
+
&
|
|
171
|
+
</Code>
|
|
172
|
+
<Text size="xs" c="dimmed">
|
|
173
|
+
AND
|
|
174
|
+
</Text>
|
|
175
|
+
</Group>
|
|
176
|
+
<Group gap="xs" wrap="nowrap">
|
|
177
|
+
<Code
|
|
178
|
+
style={{
|
|
179
|
+
minWidth: 35,
|
|
180
|
+
textAlign: "center",
|
|
181
|
+
cursor: "pointer",
|
|
182
|
+
}}
|
|
183
|
+
onClick={() => onInsert("|")}
|
|
184
|
+
>
|
|
185
|
+
|
|
|
186
|
+
</Code>
|
|
187
|
+
<Text size="xs" c="dimmed">
|
|
188
|
+
OR
|
|
189
|
+
</Text>
|
|
190
|
+
</Group>
|
|
191
|
+
</Stack>
|
|
192
|
+
</Stack>
|
|
193
|
+
</Stack>
|
|
194
|
+
|
|
195
|
+
{/* Divider */}
|
|
196
|
+
{fields.length > 0 && <Divider orientation="vertical" />}
|
|
197
|
+
|
|
198
|
+
{/* Right Column: Fields */}
|
|
199
|
+
{fields.length > 0 && (
|
|
200
|
+
<Stack gap="xs" style={{ flex: 2 }}>
|
|
201
|
+
<Text size="sm" fw={600}>
|
|
202
|
+
Fields
|
|
203
|
+
</Text>
|
|
204
|
+
<Stack gap={4} style={{ maxHeight: 300, overflowY: "auto" }}>
|
|
205
|
+
{fields.map((field) => (
|
|
206
|
+
<Group key={field.path} gap="xs" wrap="nowrap" align="flex-start">
|
|
207
|
+
<Code
|
|
208
|
+
style={{ minWidth: 120, cursor: "pointer" }}
|
|
209
|
+
onClick={() => onInsert(field.path)}
|
|
210
|
+
>
|
|
211
|
+
{field.path}
|
|
212
|
+
</Code>
|
|
213
|
+
<Stack gap={2} style={{ flex: 1, minWidth: 0 }}>
|
|
214
|
+
<Text size="xs" c="dimmed" lineClamp={1}>
|
|
215
|
+
{field.description || field.type}
|
|
216
|
+
</Text>
|
|
217
|
+
{field.enum && (
|
|
218
|
+
<Group gap={4} wrap="wrap">
|
|
219
|
+
{field.enum.map((enumValue) => (
|
|
220
|
+
<Code
|
|
221
|
+
key={enumValue}
|
|
222
|
+
style={{
|
|
223
|
+
cursor: "pointer",
|
|
224
|
+
fontStyle: "italic",
|
|
225
|
+
fontSize: "0.75rem",
|
|
226
|
+
}}
|
|
227
|
+
c="blue"
|
|
228
|
+
onClick={() => onInsert(enumValue)}
|
|
229
|
+
>
|
|
230
|
+
{enumValue}
|
|
231
|
+
</Code>
|
|
232
|
+
))}
|
|
233
|
+
</Group>
|
|
234
|
+
)}
|
|
235
|
+
</Stack>
|
|
236
|
+
<Badge size="xs" variant="light" style={{ flexShrink: 0 }}>
|
|
237
|
+
{field.type}
|
|
238
|
+
</Badge>
|
|
239
|
+
</Group>
|
|
240
|
+
))}
|
|
241
|
+
</Stack>
|
|
242
|
+
</Stack>
|
|
243
|
+
)}
|
|
244
|
+
</Group>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export default ControlQueryBuilder;
|
|
@@ -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
|
}
|