@alepha/ui 0.11.6 → 0.11.9
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 +54 -27
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +477 -132
- package/dist/index.js.map +1 -1
- package/package.json +13 -12
- package/src/components/data/JsonViewer.tsx +352 -0
- package/src/components/form/Control.tsx +9 -9
- package/src/components/form/ControlQueryBuilder.tsx +117 -52
- package/src/components/form/ControlSelect.tsx +1 -1
- package/src/components/table/DataTable.tsx +86 -32
- package/src/index.ts +1 -0
- package/src/services/DialogService.tsx +13 -2
- package/src/utils/extractSchemaFields.ts +2 -1
- package/dist/AlephaMantineProvider-Ba88lMeq.js +0 -3
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { TObject } from "@alepha/core";
|
|
2
|
+
import { parseQueryString } from "@alepha/postgres";
|
|
3
|
+
import { useEvents } from "@alepha/react";
|
|
2
4
|
import {
|
|
3
5
|
ActionIcon,
|
|
4
6
|
Badge,
|
|
5
|
-
Code,
|
|
6
7
|
Divider,
|
|
8
|
+
Flex,
|
|
7
9
|
Group,
|
|
8
10
|
Popover,
|
|
9
11
|
Stack,
|
|
@@ -11,13 +13,15 @@ import {
|
|
|
11
13
|
TextInput,
|
|
12
14
|
type TextInputProps,
|
|
13
15
|
} from "@mantine/core";
|
|
14
|
-
import { IconFilter, IconX } from "@tabler/icons-react";
|
|
16
|
+
import { IconFilter, IconInfoTriangle, IconX } from "@tabler/icons-react";
|
|
15
17
|
import { useRef, useState } from "react";
|
|
18
|
+
import { ui } from "../../constants/ui.ts";
|
|
16
19
|
import {
|
|
17
20
|
extractSchemaFields,
|
|
18
21
|
OPERATOR_INFO,
|
|
19
22
|
type SchemaField,
|
|
20
23
|
} from "../../utils/extractSchemaFields.ts";
|
|
24
|
+
import ActionButton from "../buttons/ActionButton.tsx";
|
|
21
25
|
|
|
22
26
|
export interface ControlQueryBuilderProps
|
|
23
27
|
extends Omit<TextInputProps, "value" | "onChange"> {
|
|
@@ -35,34 +39,67 @@ const ControlQueryBuilder = ({
|
|
|
35
39
|
schema,
|
|
36
40
|
value = "",
|
|
37
41
|
onChange,
|
|
38
|
-
placeholder = "Enter query or click
|
|
42
|
+
placeholder = "Enter query or click for assistance...",
|
|
39
43
|
...textInputProps
|
|
40
44
|
}: ControlQueryBuilderProps) => {
|
|
41
45
|
const [helpOpened, setHelpOpened] = useState(false);
|
|
42
46
|
const [textValue, setTextValue] = useState(value);
|
|
43
47
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
44
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
|
+
};
|
|
45
61
|
|
|
46
62
|
const handleTextChange = (newValue: string) => {
|
|
47
63
|
setTextValue(newValue);
|
|
48
|
-
|
|
64
|
+
if (isValid(newValue)) {
|
|
65
|
+
onChange?.(newValue);
|
|
66
|
+
}
|
|
49
67
|
};
|
|
50
68
|
|
|
51
69
|
const handleClear = () => {
|
|
52
70
|
setTextValue("");
|
|
53
71
|
onChange?.("");
|
|
72
|
+
isValid("");
|
|
54
73
|
};
|
|
55
74
|
|
|
56
75
|
const handleInsert = (text: string) => {
|
|
57
76
|
const newValue = textValue ? `${textValue}${text} ` : `${text} `;
|
|
58
77
|
setTextValue(newValue);
|
|
59
|
-
|
|
78
|
+
if (isValid(newValue)) {
|
|
79
|
+
onChange?.(newValue);
|
|
80
|
+
}
|
|
60
81
|
// Refocus the input after inserting
|
|
61
82
|
setTimeout(() => {
|
|
62
83
|
inputRef.current?.focus();
|
|
84
|
+
// set cursor to end
|
|
85
|
+
const length = inputRef.current?.value.length || 0;
|
|
86
|
+
inputRef.current?.setSelectionRange(length, length);
|
|
63
87
|
}, 0);
|
|
64
88
|
};
|
|
65
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
|
+
|
|
66
103
|
return (
|
|
67
104
|
<Popover
|
|
68
105
|
width={800}
|
|
@@ -72,10 +109,8 @@ const ControlQueryBuilder = ({
|
|
|
72
109
|
onChange={setHelpOpened}
|
|
73
110
|
closeOnClickOutside
|
|
74
111
|
closeOnEscape
|
|
75
|
-
withArrow
|
|
76
|
-
arrowSize={14}
|
|
77
112
|
transitionProps={{
|
|
78
|
-
transition: "fade-
|
|
113
|
+
transition: "fade-up",
|
|
79
114
|
duration: 200,
|
|
80
115
|
timingFunction: "ease",
|
|
81
116
|
}}
|
|
@@ -87,7 +122,9 @@ const ControlQueryBuilder = ({
|
|
|
87
122
|
value={textValue}
|
|
88
123
|
onChange={(e) => handleTextChange(e.currentTarget.value)}
|
|
89
124
|
onFocus={() => setHelpOpened(true)}
|
|
90
|
-
leftSection={
|
|
125
|
+
leftSection={
|
|
126
|
+
error ? <IconInfoTriangle size={16} /> : <IconFilter size={16} />
|
|
127
|
+
}
|
|
91
128
|
rightSection={
|
|
92
129
|
textValue && (
|
|
93
130
|
<ActionIcon
|
|
@@ -103,7 +140,14 @@ const ControlQueryBuilder = ({
|
|
|
103
140
|
{...textInputProps}
|
|
104
141
|
/>
|
|
105
142
|
</Popover.Target>
|
|
106
|
-
<Popover.Dropdown
|
|
143
|
+
<Popover.Dropdown
|
|
144
|
+
bg={"transparent"}
|
|
145
|
+
p={"xs"}
|
|
146
|
+
bd={`1px solid ${ui.colors.border}`}
|
|
147
|
+
style={{
|
|
148
|
+
backdropFilter: "blur(20px)",
|
|
149
|
+
}}
|
|
150
|
+
>
|
|
107
151
|
<QueryHelp fields={fields} onInsert={handleInsert} />
|
|
108
152
|
</Popover.Dropdown>
|
|
109
153
|
</Popover>
|
|
@@ -121,7 +165,14 @@ interface QueryHelpProps {
|
|
|
121
165
|
|
|
122
166
|
function QueryHelp({ fields, onInsert }: QueryHelpProps) {
|
|
123
167
|
return (
|
|
124
|
-
<Group
|
|
168
|
+
<Group
|
|
169
|
+
gap="md"
|
|
170
|
+
align="flex-start"
|
|
171
|
+
wrap="nowrap"
|
|
172
|
+
bg={ui.colors.surface}
|
|
173
|
+
p={"sm"}
|
|
174
|
+
bdrs={"sm"}
|
|
175
|
+
>
|
|
125
176
|
{/* Left Column: Operators */}
|
|
126
177
|
<Stack gap="md" style={{ flex: 1 }}>
|
|
127
178
|
{/* Available Operators */}
|
|
@@ -132,16 +183,17 @@ function QueryHelp({ fields, onInsert }: QueryHelpProps) {
|
|
|
132
183
|
<Stack gap={4}>
|
|
133
184
|
{Object.entries(OPERATOR_INFO).map(([key, info]) => (
|
|
134
185
|
<Group key={key} gap="xs" wrap="nowrap">
|
|
135
|
-
<
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
186
|
+
<ActionButton
|
|
187
|
+
px={"xs"}
|
|
188
|
+
size={"xs"}
|
|
189
|
+
h={24}
|
|
190
|
+
variant={"default"}
|
|
191
|
+
justify={"center"}
|
|
192
|
+
miw={48}
|
|
141
193
|
onClick={() => onInsert(info.symbol)}
|
|
142
194
|
>
|
|
143
195
|
{info.symbol}
|
|
144
|
-
</
|
|
196
|
+
</ActionButton>
|
|
145
197
|
<Text size="xs" c="dimmed" style={{ flex: 1 }}>
|
|
146
198
|
{info.label}
|
|
147
199
|
</Text>
|
|
@@ -159,31 +211,33 @@ function QueryHelp({ fields, onInsert }: QueryHelpProps) {
|
|
|
159
211
|
</Text>
|
|
160
212
|
<Stack gap={4}>
|
|
161
213
|
<Group gap="xs" wrap="nowrap">
|
|
162
|
-
<
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
214
|
+
<ActionButton
|
|
215
|
+
px={"xs"}
|
|
216
|
+
size={"xs"}
|
|
217
|
+
h={24}
|
|
218
|
+
variant={"default"}
|
|
219
|
+
justify={"center"}
|
|
220
|
+
miw={48}
|
|
168
221
|
onClick={() => onInsert("&")}
|
|
169
222
|
>
|
|
170
223
|
&
|
|
171
|
-
</
|
|
224
|
+
</ActionButton>
|
|
172
225
|
<Text size="xs" c="dimmed">
|
|
173
226
|
AND
|
|
174
227
|
</Text>
|
|
175
228
|
</Group>
|
|
176
229
|
<Group gap="xs" wrap="nowrap">
|
|
177
|
-
<
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
230
|
+
<ActionButton
|
|
231
|
+
px={"xs"}
|
|
232
|
+
size={"xs"}
|
|
233
|
+
h={24}
|
|
234
|
+
variant={"default"}
|
|
235
|
+
justify={"center"}
|
|
236
|
+
miw={48}
|
|
183
237
|
onClick={() => onInsert("|")}
|
|
184
238
|
>
|
|
185
239
|
|
|
|
186
|
-
</
|
|
240
|
+
</ActionButton>
|
|
187
241
|
<Text size="xs" c="dimmed">
|
|
188
242
|
OR
|
|
189
243
|
</Text>
|
|
@@ -197,49 +251,60 @@ function QueryHelp({ fields, onInsert }: QueryHelpProps) {
|
|
|
197
251
|
|
|
198
252
|
{/* Right Column: Fields */}
|
|
199
253
|
{fields.length > 0 && (
|
|
200
|
-
<
|
|
254
|
+
<Flex direction={"column"} gap="xs" style={{ flex: 2 }}>
|
|
201
255
|
<Text size="sm" fw={600}>
|
|
202
256
|
Fields
|
|
203
257
|
</Text>
|
|
204
|
-
<
|
|
258
|
+
<Flex
|
|
259
|
+
direction={"column"}
|
|
260
|
+
gap={4}
|
|
261
|
+
style={{ maxHeight: 300, overflowY: "auto" }}
|
|
262
|
+
>
|
|
205
263
|
{fields.map((field) => (
|
|
206
|
-
<
|
|
207
|
-
<
|
|
208
|
-
|
|
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}
|
|
209
272
|
onClick={() => onInsert(field.path)}
|
|
210
273
|
>
|
|
211
274
|
{field.path}
|
|
212
|
-
</
|
|
213
|
-
<
|
|
275
|
+
</ActionButton>
|
|
276
|
+
<Flex
|
|
277
|
+
mt={3}
|
|
278
|
+
direction={"column"}
|
|
279
|
+
gap={2}
|
|
280
|
+
style={{ flex: 1, minWidth: 0 }}
|
|
281
|
+
>
|
|
214
282
|
<Text size="xs" c="dimmed" lineClamp={1}>
|
|
215
283
|
{field.description || field.type}
|
|
216
284
|
</Text>
|
|
217
285
|
{field.enum && (
|
|
218
|
-
<Group gap={
|
|
286
|
+
<Group gap={0} wrap="wrap">
|
|
219
287
|
{field.enum.map((enumValue) => (
|
|
220
|
-
<
|
|
288
|
+
<ActionButton
|
|
289
|
+
px={"xs"}
|
|
290
|
+
size={"xs"}
|
|
291
|
+
h={24}
|
|
221
292
|
key={enumValue}
|
|
222
|
-
style={{
|
|
223
|
-
cursor: "pointer",
|
|
224
|
-
fontStyle: "italic",
|
|
225
|
-
fontSize: "0.75rem",
|
|
226
|
-
}}
|
|
227
|
-
c="blue"
|
|
228
293
|
onClick={() => onInsert(enumValue)}
|
|
229
294
|
>
|
|
230
295
|
{enumValue}
|
|
231
|
-
</
|
|
296
|
+
</ActionButton>
|
|
232
297
|
))}
|
|
233
298
|
</Group>
|
|
234
299
|
)}
|
|
235
|
-
</
|
|
300
|
+
</Flex>
|
|
236
301
|
<Badge size="xs" variant="light" style={{ flexShrink: 0 }}>
|
|
237
302
|
{field.type}
|
|
238
303
|
</Badge>
|
|
239
|
-
</
|
|
304
|
+
</Flex>
|
|
240
305
|
))}
|
|
241
|
-
</
|
|
242
|
-
</
|
|
306
|
+
</Flex>
|
|
307
|
+
</Flex>
|
|
243
308
|
)}
|
|
244
309
|
</Group>
|
|
245
310
|
);
|
|
@@ -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)}
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
|
+
Alepha,
|
|
2
3
|
type Async,
|
|
3
4
|
type Page,
|
|
4
5
|
type PageMetadata,
|
|
6
|
+
type Static,
|
|
5
7
|
type TObject,
|
|
6
8
|
t,
|
|
7
9
|
} from "@alepha/core";
|
|
8
10
|
import { DateTimeProvider, type DurationLike } from "@alepha/datetime";
|
|
9
11
|
import { useInject } from "@alepha/react";
|
|
10
|
-
import { useForm } from "@alepha/react-form";
|
|
12
|
+
import { type FormModel, useForm } from "@alepha/react-form";
|
|
11
13
|
import {
|
|
12
14
|
Flex,
|
|
13
15
|
Pagination,
|
|
14
|
-
Paper,
|
|
15
16
|
Select,
|
|
16
17
|
Table,
|
|
17
18
|
type TableProps,
|
|
@@ -20,40 +21,60 @@ import {
|
|
|
20
21
|
import { useDebouncedCallback } from "@mantine/hooks";
|
|
21
22
|
import { type ReactNode, useEffect, useState } from "react";
|
|
22
23
|
import ActionButton from "../buttons/ActionButton.tsx";
|
|
23
|
-
import TypeForm from "../form/TypeForm.tsx";
|
|
24
|
+
import TypeForm, { type TypeFormProps } from "../form/TypeForm.tsx";
|
|
24
25
|
|
|
25
|
-
export interface
|
|
26
|
+
export interface DataTableColumnContext<Filters extends TObject> {
|
|
27
|
+
index: number;
|
|
28
|
+
form: FormModel<Filters>;
|
|
29
|
+
alepha: Alepha;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface DataTableColumn<T extends object, Filters extends TObject> {
|
|
26
33
|
label: string;
|
|
27
|
-
value: (item: T,
|
|
34
|
+
value: (item: T, ctx: DataTableColumnContext<Filters>) => ReactNode;
|
|
35
|
+
fit?: boolean;
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
export type MaybePage<T> = Omit<Page<T>, "page"> & {
|
|
31
39
|
page?: Partial<PageMetadata>;
|
|
32
40
|
};
|
|
33
41
|
|
|
34
|
-
export interface
|
|
42
|
+
export interface DataTableSubmitContext<T extends object> {
|
|
43
|
+
items: T[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface DataTableProps<T extends object, Filters extends TObject> {
|
|
35
47
|
/**
|
|
36
48
|
* The items to display in the table. Can be a static page of items or a function that returns a promise resolving to a page of items.
|
|
37
49
|
*/
|
|
38
50
|
items:
|
|
39
51
|
| MaybePage<T>
|
|
40
52
|
| ((
|
|
41
|
-
filters:
|
|
53
|
+
filters: Static<Filters> & {
|
|
42
54
|
page: number;
|
|
43
55
|
size: number;
|
|
44
56
|
sort?: string;
|
|
45
57
|
},
|
|
58
|
+
ctx: DataTableSubmitContext<T>,
|
|
46
59
|
) => Async<MaybePage<T>>);
|
|
47
60
|
|
|
48
61
|
/**
|
|
49
62
|
* The columns to display in the table. Each column is defined by a key and a DataTableColumn object.
|
|
50
63
|
*/
|
|
51
64
|
columns: {
|
|
52
|
-
[key: string]: DataTableColumn<T>;
|
|
65
|
+
[key: string]: DataTableColumn<T, Filters>;
|
|
53
66
|
};
|
|
54
67
|
|
|
55
68
|
defaultSize?: number;
|
|
56
69
|
|
|
70
|
+
typeFormProps?: Partial<Omit<TypeFormProps<Filters>, "form">>;
|
|
71
|
+
|
|
72
|
+
onFilterChange?: (
|
|
73
|
+
key: string,
|
|
74
|
+
value: unknown,
|
|
75
|
+
form: FormModel<Filters>,
|
|
76
|
+
) => void;
|
|
77
|
+
|
|
57
78
|
/**
|
|
58
79
|
* Optional filters to apply to the data.
|
|
59
80
|
*/
|
|
@@ -89,7 +110,9 @@ export interface DataTableProps<T extends object> {
|
|
|
89
110
|
tableTrProps?: (item: T) => TableTrProps;
|
|
90
111
|
}
|
|
91
112
|
|
|
92
|
-
const DataTable = <T extends object
|
|
113
|
+
const DataTable = <T extends object, Filters extends TObject>(
|
|
114
|
+
props: DataTableProps<T, Filters>,
|
|
115
|
+
) => {
|
|
93
116
|
const [items, setItems] = useState<MaybePage<T>>(
|
|
94
117
|
typeof props.items === "function"
|
|
95
118
|
? {
|
|
@@ -98,11 +121,11 @@ const DataTable = <T extends object>(props: DataTableProps<T>) => {
|
|
|
98
121
|
: props.items,
|
|
99
122
|
);
|
|
100
123
|
|
|
101
|
-
const defaultSize = props.infinityScroll ?
|
|
124
|
+
const defaultSize = props.infinityScroll ? 100 : props.defaultSize || 10;
|
|
102
125
|
const [page, setPage] = useState(1);
|
|
103
126
|
const [size, setSize] = useState(String(defaultSize));
|
|
104
|
-
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
|
105
127
|
const [currentPage, setCurrentPage] = useState(0);
|
|
128
|
+
const alepha = useInject(Alepha);
|
|
106
129
|
|
|
107
130
|
const form = useForm(
|
|
108
131
|
{
|
|
@@ -115,11 +138,14 @@ const DataTable = <T extends object>(props: DataTableProps<T>) => {
|
|
|
115
138
|
handler: async (values, args) => {
|
|
116
139
|
if (typeof props.items === "function") {
|
|
117
140
|
const response = await props.items(
|
|
118
|
-
values as
|
|
141
|
+
values as Static<Filters> & {
|
|
119
142
|
page: number;
|
|
120
143
|
size: number;
|
|
121
144
|
sort?: string;
|
|
122
145
|
},
|
|
146
|
+
{
|
|
147
|
+
items: items.content,
|
|
148
|
+
},
|
|
123
149
|
);
|
|
124
150
|
|
|
125
151
|
if (props.infinityScroll && values.page > 0) {
|
|
@@ -131,8 +157,8 @@ const DataTable = <T extends object>(props: DataTableProps<T>) => {
|
|
|
131
157
|
} else {
|
|
132
158
|
setItems(response);
|
|
133
159
|
}
|
|
160
|
+
|
|
134
161
|
setCurrentPage(values.page);
|
|
135
|
-
setIsLoadingMore(false);
|
|
136
162
|
}
|
|
137
163
|
},
|
|
138
164
|
onReset: async () => {
|
|
@@ -153,21 +179,20 @@ const DataTable = <T extends object>(props: DataTableProps<T>) => {
|
|
|
153
179
|
return;
|
|
154
180
|
}
|
|
155
181
|
|
|
156
|
-
|
|
182
|
+
props.onFilterChange?.(key, value, form as any);
|
|
157
183
|
},
|
|
158
184
|
},
|
|
159
|
-
[],
|
|
185
|
+
[items],
|
|
160
186
|
);
|
|
161
187
|
|
|
162
188
|
const submitDebounce = useDebouncedCallback(() => form.submit(), {
|
|
163
|
-
delay:
|
|
189
|
+
delay: 800,
|
|
164
190
|
});
|
|
165
191
|
|
|
166
192
|
const dt = useInject(DateTimeProvider);
|
|
167
193
|
|
|
168
194
|
useEffect(() => {
|
|
169
195
|
if (props.submitOnInit) {
|
|
170
|
-
console.log("submitting");
|
|
171
196
|
form.submit();
|
|
172
197
|
}
|
|
173
198
|
if (props.submitEvery) {
|
|
@@ -189,19 +214,18 @@ const DataTable = <T extends object>(props: DataTableProps<T>) => {
|
|
|
189
214
|
if (!props.infinityScroll || typeof props.items !== "function") return;
|
|
190
215
|
|
|
191
216
|
const handleScroll = () => {
|
|
192
|
-
if (
|
|
217
|
+
if (form.submitting) return;
|
|
193
218
|
|
|
194
219
|
const scrollTop = window.scrollY;
|
|
195
220
|
const windowHeight = window.innerHeight;
|
|
196
221
|
const docHeight = document.documentElement.scrollHeight;
|
|
197
222
|
|
|
198
|
-
const isNearBottom = scrollTop + windowHeight >= docHeight -
|
|
223
|
+
const isNearBottom = scrollTop + windowHeight >= docHeight - 300;
|
|
199
224
|
|
|
200
225
|
if (isNearBottom) {
|
|
201
226
|
const totalPages = items.page?.totalPages ?? 1;
|
|
202
227
|
|
|
203
228
|
if (currentPage + 1 < totalPages) {
|
|
204
|
-
setIsLoadingMore(true);
|
|
205
229
|
form.input.page.set(currentPage + 1);
|
|
206
230
|
}
|
|
207
231
|
}
|
|
@@ -211,14 +235,24 @@ const DataTable = <T extends object>(props: DataTableProps<T>) => {
|
|
|
211
235
|
return () => window.removeEventListener("scroll", handleScroll);
|
|
212
236
|
}, [
|
|
213
237
|
props.infinityScroll,
|
|
214
|
-
|
|
238
|
+
form.submitting,
|
|
215
239
|
items.page?.totalPages,
|
|
216
240
|
currentPage,
|
|
217
241
|
form,
|
|
218
242
|
]);
|
|
219
243
|
|
|
220
244
|
const head = Object.entries(props.columns).map(([key, col]) => (
|
|
221
|
-
<Table.Th
|
|
245
|
+
<Table.Th
|
|
246
|
+
key={key}
|
|
247
|
+
style={{
|
|
248
|
+
...(col.fit
|
|
249
|
+
? {
|
|
250
|
+
width: "1%",
|
|
251
|
+
whiteSpace: "nowrap",
|
|
252
|
+
}
|
|
253
|
+
: {}),
|
|
254
|
+
}}
|
|
255
|
+
>
|
|
222
256
|
<ActionButton justify={"space-between"} radius={0} fullWidth size={"xs"}>
|
|
223
257
|
{col.label}
|
|
224
258
|
</ActionButton>
|
|
@@ -232,7 +266,13 @@ const DataTable = <T extends object>(props: DataTableProps<T>) => {
|
|
|
232
266
|
return (
|
|
233
267
|
<Table.Tr key={JSON.stringify(item)} {...trProps}>
|
|
234
268
|
{Object.entries(props.columns).map(([key, col]) => (
|
|
235
|
-
<Table.Td key={key}>
|
|
269
|
+
<Table.Td key={key}>
|
|
270
|
+
{col.value(item as T, {
|
|
271
|
+
index,
|
|
272
|
+
form: form as unknown as FormModel<Filters>,
|
|
273
|
+
alepha,
|
|
274
|
+
})}
|
|
275
|
+
</Table.Td>
|
|
236
276
|
))}
|
|
237
277
|
</Table.Tr>
|
|
238
278
|
);
|
|
@@ -242,15 +282,29 @@ const DataTable = <T extends object>(props: DataTableProps<T>) => {
|
|
|
242
282
|
|
|
243
283
|
return (
|
|
244
284
|
<Flex direction={"column"} gap={"sm"} flex={1}>
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
285
|
+
{props.filters ? (
|
|
286
|
+
<TypeForm
|
|
287
|
+
{...props.typeFormProps}
|
|
288
|
+
form={form as unknown as FormModel<Filters>}
|
|
289
|
+
schema={schema}
|
|
290
|
+
/>
|
|
291
|
+
) : null}
|
|
292
|
+
|
|
293
|
+
<Flex flex={1} className={"overflow-auto"}>
|
|
294
|
+
<Table
|
|
295
|
+
striped
|
|
296
|
+
withRowBorders
|
|
297
|
+
withColumnBorders
|
|
298
|
+
withTableBorder
|
|
299
|
+
stripedColor={""}
|
|
300
|
+
{...props.tableProps}
|
|
301
|
+
>
|
|
302
|
+
<Table.Thead>
|
|
303
|
+
<Table.Tr>{head}</Table.Tr>
|
|
304
|
+
</Table.Thead>
|
|
305
|
+
<Table.Tbody>{rows}</Table.Tbody>
|
|
306
|
+
</Table>
|
|
307
|
+
</Flex>
|
|
254
308
|
|
|
255
309
|
{!props.infinityScroll && (
|
|
256
310
|
<Flex justify={"space-between"} align={"center"}>
|
package/src/index.ts
CHANGED
|
@@ -23,6 +23,7 @@ export type {
|
|
|
23
23
|
export { default as ActionButton } from "./components/buttons/ActionButton.tsx";
|
|
24
24
|
export { default as DarkModeButton } from "./components/buttons/DarkModeButton.tsx";
|
|
25
25
|
export { default as OmnibarButton } from "./components/buttons/OmnibarButton.tsx";
|
|
26
|
+
export { default as JsonViewer } from "./components/data/JsonViewer.tsx";
|
|
26
27
|
export { default as AlertDialog } from "./components/dialogs/AlertDialog.tsx";
|
|
27
28
|
export { default as ConfirmDialog } from "./components/dialogs/ConfirmDialog.tsx";
|
|
28
29
|
export { default as PromptDialog } from "./components/dialogs/PromptDialog.tsx";
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { Flex, type ModalProps } from "@mantine/core";
|
|
2
2
|
import { modals } from "@mantine/modals";
|
|
3
3
|
import type { ReactNode } from "react";
|
|
4
|
+
import JsonViewer from "../components/data/JsonViewer.tsx";
|
|
4
5
|
import AlertDialog from "../components/dialogs/AlertDialog";
|
|
5
6
|
import ConfirmDialog from "../components/dialogs/ConfirmDialog";
|
|
6
7
|
import PromptDialog from "../components/dialogs/PromptDialog";
|
|
8
|
+
import { ui } from "../constants/ui.ts";
|
|
7
9
|
|
|
8
10
|
// Base interfaces
|
|
9
11
|
export interface BaseDialogOptions extends Partial<ModalProps> {
|
|
@@ -161,7 +163,16 @@ export class DialogService {
|
|
|
161
163
|
* Show a JSON editor/viewer dialog
|
|
162
164
|
*/
|
|
163
165
|
public json(data?: any, options?: BaseDialogOptions): void {
|
|
164
|
-
|
|
166
|
+
this.open({
|
|
167
|
+
size: "lg",
|
|
168
|
+
title: options?.title || "Json Viewer",
|
|
169
|
+
...options,
|
|
170
|
+
content: (
|
|
171
|
+
<Flex bdrs={"md"} w={"100%"} flex={1} p={"sm"} bg={ui.colors.surface}>
|
|
172
|
+
<JsonViewer size={"xs"} data={data} />
|
|
173
|
+
</Flex>
|
|
174
|
+
),
|
|
175
|
+
});
|
|
165
176
|
}
|
|
166
177
|
|
|
167
178
|
/**
|
|
@@ -51,7 +51,7 @@ export function extractSchemaFields(
|
|
|
51
51
|
// Determine the display type - use format for datetime-related fields
|
|
52
52
|
const format = "format" in fieldSchema ? fieldSchema.format : undefined;
|
|
53
53
|
const baseType =
|
|
54
|
-
"type" in fieldSchema ? (fieldSchema.type as string) : "
|
|
54
|
+
"type" in fieldSchema ? (fieldSchema.type as string) : "object";
|
|
55
55
|
|
|
56
56
|
let displayType = baseType;
|
|
57
57
|
if (format === "date-time") {
|
|
@@ -76,6 +76,7 @@ export function extractSchemaFields(
|
|
|
76
76
|
// Handle enum
|
|
77
77
|
if ("enum" in fieldSchema && fieldSchema.enum) {
|
|
78
78
|
field.enum = fieldSchema.enum;
|
|
79
|
+
field.type = "enum";
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
// Handle nested objects
|