@alepha/ui 0.13.5 → 0.13.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/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 +2044 -1044
- 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 +471 -426
- package/dist/auth/index.js +26 -18
- package/dist/auth/index.js.map +1 -1
- package/dist/core/index.d.ts +400 -130
- package/dist/core/index.js +1751 -1369
- package/dist/core/index.js.map +1 -1
- package/package.json +15 -11
- 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
|
@@ -1,116 +1,77 @@
|
|
|
1
1
|
import { useInject } from "@alepha/react";
|
|
2
2
|
import { type FormModel, useForm } from "@alepha/react/form";
|
|
3
|
-
import {
|
|
4
|
-
Card,
|
|
5
|
-
Flex,
|
|
6
|
-
Pagination,
|
|
7
|
-
Select,
|
|
8
|
-
Table,
|
|
9
|
-
type TableProps,
|
|
10
|
-
type TableTrProps,
|
|
11
|
-
} from "@mantine/core";
|
|
3
|
+
import { Checkbox, Flex, Table, Text, UnstyledButton } from "@mantine/core";
|
|
12
4
|
import { useDebouncedCallback } from "@mantine/hooks";
|
|
13
5
|
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
} from "alepha";
|
|
22
|
-
import { DateTimeProvider, type DurationLike } from "alepha/datetime";
|
|
23
|
-
import { type ReactNode, useEffect, useState } from "react";
|
|
6
|
+
IconArrowDown,
|
|
7
|
+
IconArrowsSort,
|
|
8
|
+
IconArrowUp,
|
|
9
|
+
} from "@tabler/icons-react";
|
|
10
|
+
import { Alepha, type Static, type TObject, t } from "alepha";
|
|
11
|
+
import { DateTimeProvider } from "alepha/datetime";
|
|
12
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
24
13
|
import { ui } from "../../constants/ui.ts";
|
|
25
|
-
import
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
14
|
+
import DataTableFilters, {
|
|
15
|
+
type DataTableFiltersProps,
|
|
16
|
+
} from "./DataTableFilters.tsx";
|
|
17
|
+
import DataTablePagination from "./DataTablePagination.tsx";
|
|
18
|
+
import DataTableToolbar from "./DataTableToolbar.tsx";
|
|
19
|
+
import type {
|
|
20
|
+
ColumnVisibility,
|
|
21
|
+
DataTableColumnContext,
|
|
22
|
+
DataTableProps,
|
|
23
|
+
FilterVisibility,
|
|
24
|
+
MaybePage,
|
|
25
|
+
} from "./types.ts";
|
|
26
|
+
|
|
27
|
+
const DEFAULT_VISIBLE_COLUMN_COUNT = 10;
|
|
28
|
+
|
|
29
|
+
type SortDirection = "asc" | "desc" | null;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Parse the sort string to get direction for a specific field.
|
|
33
|
+
* Alepha convention: 'field' = ASC, '-field' = DESC
|
|
34
|
+
*/
|
|
35
|
+
const getSortDirection = (
|
|
36
|
+
sortString: string | undefined,
|
|
37
|
+
field: string,
|
|
38
|
+
): SortDirection => {
|
|
39
|
+
if (!sortString) return null;
|
|
40
|
+
const parts = sortString.split(",").map((s) => s.trim());
|
|
41
|
+
for (const part of parts) {
|
|
42
|
+
if (part === field) return "asc";
|
|
43
|
+
if (part === `-${field}`) return "desc";
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
42
46
|
};
|
|
43
47
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
typeFormProps?: Partial<Omit<TypeFormProps<Filters>, "form">>;
|
|
73
|
-
|
|
74
|
-
onFilterChange?: (
|
|
75
|
-
key: string,
|
|
76
|
-
value: unknown,
|
|
77
|
-
form: FormModel<Filters>,
|
|
78
|
-
) => void;
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Optional filters to apply to the data.
|
|
82
|
-
*/
|
|
83
|
-
filters?: TObject;
|
|
84
|
-
|
|
85
|
-
panel?: (item: T) => ReactNode;
|
|
86
|
-
canPanel?: (item: T) => boolean;
|
|
87
|
-
|
|
88
|
-
submitOnInit?: boolean;
|
|
89
|
-
submitEvery?: DurationLike;
|
|
90
|
-
|
|
91
|
-
withLineNumbers?: boolean;
|
|
92
|
-
withCheckbox?: boolean;
|
|
93
|
-
checkboxActions?: any[];
|
|
94
|
-
|
|
95
|
-
actions?: any[];
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Enable infinity scroll mode. When true, pagination controls are hidden and new items are loaded automatically when scrolling to the bottom.
|
|
99
|
-
*/
|
|
100
|
-
infinityScroll?: boolean;
|
|
101
|
-
|
|
102
|
-
// -------------------------------------------------------------------------------------------------------------------
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Props to pass to the Mantine Table component.
|
|
106
|
-
*/
|
|
107
|
-
tableProps?: TableProps;
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Function to generate props for each table row based on the item.
|
|
111
|
-
*/
|
|
112
|
-
tableTrProps?: (item: T) => TableTrProps;
|
|
113
|
-
}
|
|
48
|
+
/**
|
|
49
|
+
* Toggle sort for a field in the sort string.
|
|
50
|
+
* Cycles: null -> asc -> desc -> null
|
|
51
|
+
*/
|
|
52
|
+
const toggleSort = (
|
|
53
|
+
sortString: string | undefined,
|
|
54
|
+
field: string,
|
|
55
|
+
): string | undefined => {
|
|
56
|
+
const current = getSortDirection(sortString, field);
|
|
57
|
+
|
|
58
|
+
// Remove existing sort for this field
|
|
59
|
+
const parts = (sortString || "")
|
|
60
|
+
.split(",")
|
|
61
|
+
.map((s) => s.trim())
|
|
62
|
+
.filter((s) => s && s !== field && s !== `-${field}`);
|
|
63
|
+
|
|
64
|
+
if (current === null) {
|
|
65
|
+
// No sort -> ASC
|
|
66
|
+
parts.unshift(field);
|
|
67
|
+
} else if (current === "asc") {
|
|
68
|
+
// ASC -> DESC
|
|
69
|
+
parts.unshift(`-${field}`);
|
|
70
|
+
}
|
|
71
|
+
// DESC -> remove (already filtered out above)
|
|
72
|
+
|
|
73
|
+
return parts.length > 0 ? parts.join(",") : undefined;
|
|
74
|
+
};
|
|
114
75
|
|
|
115
76
|
const DataTable = <T extends object, Filters extends TObject>(
|
|
116
77
|
props: DataTableProps<T, Filters>,
|
|
@@ -129,6 +90,156 @@ const DataTable = <T extends object, Filters extends TObject>(
|
|
|
129
90
|
const [currentPage, setCurrentPage] = useState(0);
|
|
130
91
|
const alepha = useInject(Alepha);
|
|
131
92
|
|
|
93
|
+
// Column visibility state
|
|
94
|
+
const [columnVisibility, setColumnVisibility] = useState<ColumnVisibility>(
|
|
95
|
+
() => {
|
|
96
|
+
if (props.defaultColumnVisibility) {
|
|
97
|
+
return props.defaultColumnVisibility;
|
|
98
|
+
}
|
|
99
|
+
const columnKeys = Object.keys(props.columns);
|
|
100
|
+
const maxVisible =
|
|
101
|
+
props.defaultVisibleColumnCount ?? DEFAULT_VISIBLE_COLUMN_COUNT;
|
|
102
|
+
return columnKeys.reduce(
|
|
103
|
+
(acc, key, index) => ({
|
|
104
|
+
...acc,
|
|
105
|
+
[key]: index < maxVisible,
|
|
106
|
+
}),
|
|
107
|
+
{} as ColumnVisibility,
|
|
108
|
+
);
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Filter visibility state
|
|
113
|
+
const [filterVisibility, setFilterVisibility] = useState<FilterVisibility>(
|
|
114
|
+
() => {
|
|
115
|
+
if (props.defaultFilterVisibility) {
|
|
116
|
+
return props.defaultFilterVisibility;
|
|
117
|
+
}
|
|
118
|
+
if (!props.filters?.properties) {
|
|
119
|
+
return {};
|
|
120
|
+
}
|
|
121
|
+
return Object.keys(props.filters.properties).reduce(
|
|
122
|
+
(acc, key) => ({ ...acc, [key]: true }),
|
|
123
|
+
{} as FilterVisibility,
|
|
124
|
+
);
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// Handle column visibility changes
|
|
129
|
+
const handleColumnVisibilityChange = (visibility: ColumnVisibility) => {
|
|
130
|
+
setColumnVisibility(visibility);
|
|
131
|
+
props.onColumnVisibilityChange?.(visibility);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Handle filter visibility changes
|
|
135
|
+
const handleFilterVisibilityChange = (visibility: FilterVisibility) => {
|
|
136
|
+
setFilterVisibility(visibility);
|
|
137
|
+
props.onFilterVisibilityChange?.(visibility);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Compute visible columns
|
|
141
|
+
const visibleColumns = useMemo(() => {
|
|
142
|
+
return Object.entries(props.columns).filter(
|
|
143
|
+
([key]) => columnVisibility[key] !== false,
|
|
144
|
+
);
|
|
145
|
+
}, [props.columns, columnVisibility]);
|
|
146
|
+
|
|
147
|
+
// Current sort string from form
|
|
148
|
+
const [sortString, setSortString] = useState<string | undefined>(undefined);
|
|
149
|
+
|
|
150
|
+
// Handle column header click for sorting
|
|
151
|
+
const handleSortClick = (columnKey: string, sortKey?: string) => {
|
|
152
|
+
const field = sortKey || columnKey;
|
|
153
|
+
const newSort = toggleSort(sortString, field);
|
|
154
|
+
setSortString(newSort);
|
|
155
|
+
form.input.sort.set(newSort);
|
|
156
|
+
form.input.page.set(0); // Reset to first page when sorting changes
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// Checkbox selection state
|
|
160
|
+
const [selectedKeys, setSelectedKeys] = useState<Set<string>>(new Set());
|
|
161
|
+
|
|
162
|
+
// Default getItemKey uses JSON.stringify if not provided
|
|
163
|
+
const getItemKey = useCallback(
|
|
164
|
+
(item: T): string => {
|
|
165
|
+
if (props.getItemKey) {
|
|
166
|
+
return props.getItemKey(item);
|
|
167
|
+
}
|
|
168
|
+
return JSON.stringify(item);
|
|
169
|
+
},
|
|
170
|
+
[props.getItemKey],
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// Get selected items from current content
|
|
174
|
+
const selectedItems = useMemo(() => {
|
|
175
|
+
if (!props.withCheckbox) return [];
|
|
176
|
+
return items.content.filter((item) =>
|
|
177
|
+
selectedKeys.has(getItemKey(item as T)),
|
|
178
|
+
) as T[];
|
|
179
|
+
}, [items.content, selectedKeys, getItemKey, props.withCheckbox]);
|
|
180
|
+
|
|
181
|
+
// Check if all current page items are selected
|
|
182
|
+
const allSelected = useMemo(() => {
|
|
183
|
+
if (items.content.length === 0) return false;
|
|
184
|
+
return items.content.every((item) =>
|
|
185
|
+
selectedKeys.has(getItemKey(item as T)),
|
|
186
|
+
);
|
|
187
|
+
}, [items.content, selectedKeys, getItemKey]);
|
|
188
|
+
|
|
189
|
+
// Check if some (but not all) items are selected
|
|
190
|
+
const someSelected = useMemo(() => {
|
|
191
|
+
if (items.content.length === 0) return false;
|
|
192
|
+
const selectedCount = items.content.filter((item) =>
|
|
193
|
+
selectedKeys.has(getItemKey(item as T)),
|
|
194
|
+
).length;
|
|
195
|
+
return selectedCount > 0 && selectedCount < items.content.length;
|
|
196
|
+
}, [items.content, selectedKeys, getItemKey]);
|
|
197
|
+
|
|
198
|
+
// Toggle selection of a single item
|
|
199
|
+
const toggleItemSelection = useCallback(
|
|
200
|
+
(item: T) => {
|
|
201
|
+
const key = getItemKey(item);
|
|
202
|
+
setSelectedKeys((prev) => {
|
|
203
|
+
const next = new Set(prev);
|
|
204
|
+
if (next.has(key)) {
|
|
205
|
+
next.delete(key);
|
|
206
|
+
} else {
|
|
207
|
+
next.add(key);
|
|
208
|
+
}
|
|
209
|
+
return next;
|
|
210
|
+
});
|
|
211
|
+
},
|
|
212
|
+
[getItemKey],
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
// Toggle selection of all items on current page
|
|
216
|
+
const toggleAllSelection = useCallback(() => {
|
|
217
|
+
if (allSelected) {
|
|
218
|
+
// Deselect all current page items
|
|
219
|
+
setSelectedKeys((prev) => {
|
|
220
|
+
const next = new Set(prev);
|
|
221
|
+
for (const item of items.content) {
|
|
222
|
+
next.delete(getItemKey(item as T));
|
|
223
|
+
}
|
|
224
|
+
return next;
|
|
225
|
+
});
|
|
226
|
+
} else {
|
|
227
|
+
// Select all current page items
|
|
228
|
+
setSelectedKeys((prev) => {
|
|
229
|
+
const next = new Set(prev);
|
|
230
|
+
for (const item of items.content) {
|
|
231
|
+
next.add(getItemKey(item as T));
|
|
232
|
+
}
|
|
233
|
+
return next;
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}, [allSelected, items.content, getItemKey]);
|
|
237
|
+
|
|
238
|
+
// Clear all selections
|
|
239
|
+
const clearSelection = useCallback(() => {
|
|
240
|
+
setSelectedKeys(new Set());
|
|
241
|
+
}, []);
|
|
242
|
+
|
|
132
243
|
const form = useForm(
|
|
133
244
|
{
|
|
134
245
|
schema: t.object({
|
|
@@ -137,7 +248,7 @@ const DataTable = <T extends object, Filters extends TObject>(
|
|
|
137
248
|
size: t.number({ default: defaultSize }),
|
|
138
249
|
sort: t.optional(t.string()),
|
|
139
250
|
}),
|
|
140
|
-
handler: async (values
|
|
251
|
+
handler: async (values) => {
|
|
141
252
|
if (typeof props.items === "function") {
|
|
142
253
|
const response = await props.items(
|
|
143
254
|
values as Static<Filters> & {
|
|
@@ -181,7 +292,11 @@ const DataTable = <T extends object, Filters extends TObject>(
|
|
|
181
292
|
return;
|
|
182
293
|
}
|
|
183
294
|
|
|
184
|
-
props.onFilterChange?.(
|
|
295
|
+
props.onFilterChange?.(
|
|
296
|
+
key,
|
|
297
|
+
value,
|
|
298
|
+
form as unknown as FormModel<Filters>,
|
|
299
|
+
);
|
|
185
300
|
},
|
|
186
301
|
},
|
|
187
302
|
[items],
|
|
@@ -243,99 +358,157 @@ const DataTable = <T extends object, Filters extends TObject>(
|
|
|
243
358
|
form,
|
|
244
359
|
]);
|
|
245
360
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
: {}),
|
|
256
|
-
}}
|
|
257
|
-
>
|
|
258
|
-
<ActionButton justify={"space-between"} radius={0} fullWidth size={"xs"}>
|
|
259
|
-
{col.label}
|
|
260
|
-
</ActionButton>
|
|
361
|
+
// Checkbox header column
|
|
362
|
+
const checkboxHeader = props.withCheckbox ? (
|
|
363
|
+
<Table.Th style={{ width: 40 }}>
|
|
364
|
+
<Checkbox
|
|
365
|
+
checked={allSelected}
|
|
366
|
+
indeterminate={someSelected}
|
|
367
|
+
onChange={toggleAllSelection}
|
|
368
|
+
aria-label="Select all"
|
|
369
|
+
/>
|
|
261
370
|
</Table.Th>
|
|
262
|
-
)
|
|
371
|
+
) : null;
|
|
372
|
+
|
|
373
|
+
const head = visibleColumns.map(([key, col]) => {
|
|
374
|
+
const sortField = col.sortKey || key;
|
|
375
|
+
const sortDir = col.sortable
|
|
376
|
+
? getSortDirection(sortString, sortField)
|
|
377
|
+
: null;
|
|
378
|
+
|
|
379
|
+
const headerContent = (
|
|
380
|
+
<Flex align="center" gap={4}>
|
|
381
|
+
<Text size="xs">{col.label}</Text>
|
|
382
|
+
{col.sortable && (
|
|
383
|
+
<Flex c="dimmed">
|
|
384
|
+
{sortDir === "asc" && <IconArrowUp size={ui.sizes.icon.sm} />}
|
|
385
|
+
{sortDir === "desc" && <IconArrowDown size={ui.sizes.icon.sm} />}
|
|
386
|
+
{sortDir === null && <IconArrowsSort size={ui.sizes.icon.sm} />}
|
|
387
|
+
</Flex>
|
|
388
|
+
)}
|
|
389
|
+
</Flex>
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
return (
|
|
393
|
+
<Table.Th
|
|
394
|
+
key={key}
|
|
395
|
+
style={{
|
|
396
|
+
...(col.fit
|
|
397
|
+
? {
|
|
398
|
+
// TODO: not working well (bad formatting in some cases)
|
|
399
|
+
// width: "1%",
|
|
400
|
+
// whiteSpace: "nowrap",
|
|
401
|
+
}
|
|
402
|
+
: {}),
|
|
403
|
+
...(col.sortable ? { cursor: "pointer" } : {}),
|
|
404
|
+
}}
|
|
405
|
+
>
|
|
406
|
+
{col.sortable ? (
|
|
407
|
+
<UnstyledButton onClick={() => handleSortClick(key, col.sortKey)}>
|
|
408
|
+
{headerContent}
|
|
409
|
+
</UnstyledButton>
|
|
410
|
+
) : (
|
|
411
|
+
headerContent
|
|
412
|
+
)}
|
|
413
|
+
</Table.Th>
|
|
414
|
+
);
|
|
415
|
+
});
|
|
263
416
|
|
|
264
417
|
const rows = items.content.map((item, index) => {
|
|
265
|
-
const trProps = props.tableTrProps
|
|
266
|
-
|
|
267
|
-
|
|
418
|
+
const trProps = props.tableTrProps ? props.tableTrProps(item as T) : {};
|
|
419
|
+
const itemKey = getItemKey(item as T);
|
|
420
|
+
const isSelected = selectedKeys.has(itemKey);
|
|
421
|
+
|
|
268
422
|
return (
|
|
269
|
-
<Table.Tr key={
|
|
270
|
-
{
|
|
423
|
+
<Table.Tr key={itemKey} {...trProps}>
|
|
424
|
+
{props.withCheckbox && (
|
|
425
|
+
<Table.Td style={{ width: 40 }}>
|
|
426
|
+
<Checkbox
|
|
427
|
+
checked={isSelected}
|
|
428
|
+
onChange={() => toggleItemSelection(item as T)}
|
|
429
|
+
aria-label="Select row"
|
|
430
|
+
/>
|
|
431
|
+
</Table.Td>
|
|
432
|
+
)}
|
|
433
|
+
{visibleColumns.map(([key, col]) => (
|
|
271
434
|
<Table.Td key={key}>
|
|
272
|
-
{col.value(
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
435
|
+
{col.value(
|
|
436
|
+
item as T,
|
|
437
|
+
{
|
|
438
|
+
index,
|
|
439
|
+
form: form as unknown as FormModel<Filters>,
|
|
440
|
+
alepha,
|
|
441
|
+
} as DataTableColumnContext<Filters>,
|
|
442
|
+
)}
|
|
277
443
|
</Table.Td>
|
|
278
444
|
))}
|
|
279
445
|
</Table.Tr>
|
|
280
446
|
);
|
|
281
447
|
});
|
|
282
448
|
|
|
283
|
-
const
|
|
449
|
+
const filterSchema = useMemo(() => {
|
|
450
|
+
if (!props.filters) return null;
|
|
451
|
+
return t.omit(form.options.schema, ["page", "size", "sort"]);
|
|
452
|
+
}, [props.filters, form.options.schema]);
|
|
284
453
|
|
|
285
454
|
return (
|
|
286
|
-
<Flex
|
|
287
|
-
{
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
455
|
+
<Flex
|
|
456
|
+
flex={1}
|
|
457
|
+
p={0}
|
|
458
|
+
bg="var(--alepha-elevated)"
|
|
459
|
+
bdrs="sm"
|
|
460
|
+
bd="1px solid var(--alepha-border)"
|
|
461
|
+
direction="column"
|
|
462
|
+
>
|
|
463
|
+
<DataTableToolbar
|
|
464
|
+
columns={props.columns}
|
|
465
|
+
filters={props.filters}
|
|
466
|
+
columnVisibility={columnVisibility}
|
|
467
|
+
filterVisibility={filterVisibility}
|
|
468
|
+
onColumnVisibilityChange={handleColumnVisibilityChange}
|
|
469
|
+
onFilterVisibilityChange={handleFilterVisibilityChange}
|
|
470
|
+
actions={props.actions}
|
|
471
|
+
onRefresh={() => form.submit()}
|
|
472
|
+
selectedItems={selectedItems}
|
|
473
|
+
checkboxActions={props.checkboxActions}
|
|
474
|
+
onClearSelection={clearSelection}
|
|
475
|
+
/>
|
|
476
|
+
|
|
477
|
+
{filterSchema && props.filters && (
|
|
478
|
+
<DataTableFilters
|
|
479
|
+
schema={filterSchema}
|
|
480
|
+
form={form as unknown as FormModel<TObject>}
|
|
481
|
+
typeFormProps={
|
|
482
|
+
props.typeFormProps as DataTableFiltersProps["typeFormProps"]
|
|
483
|
+
}
|
|
484
|
+
filterVisibility={filterVisibility}
|
|
485
|
+
/>
|
|
486
|
+
)}
|
|
487
|
+
|
|
488
|
+
<Flex className="overflow-auto">
|
|
489
|
+
<Table withColumnBorders withRowBorders {...props.tableProps}>
|
|
306
490
|
<Table.Thead>
|
|
307
|
-
<Table.Tr>
|
|
491
|
+
<Table.Tr>
|
|
492
|
+
{checkboxHeader}
|
|
493
|
+
{head}
|
|
494
|
+
</Table.Tr>
|
|
308
495
|
</Table.Thead>
|
|
309
496
|
<Table.Tbody>{rows}</Table.Tbody>
|
|
310
497
|
</Table>
|
|
311
498
|
</Flex>
|
|
312
499
|
|
|
313
500
|
{!props.infinityScroll && (
|
|
314
|
-
<
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
value={size}
|
|
326
|
-
onChange={(value) => {
|
|
327
|
-
form.input.size.set(Number(value));
|
|
328
|
-
}}
|
|
329
|
-
data={[
|
|
330
|
-
{ value: "5", label: "5" },
|
|
331
|
-
{ value: "10", label: "10" },
|
|
332
|
-
{ value: "25", label: "25" },
|
|
333
|
-
{ value: "50", label: "50" },
|
|
334
|
-
{ value: "100", label: "100" },
|
|
335
|
-
]}
|
|
336
|
-
/>
|
|
337
|
-
</Flex>
|
|
338
|
-
</Flex>
|
|
501
|
+
<DataTablePagination
|
|
502
|
+
page={page}
|
|
503
|
+
size={size}
|
|
504
|
+
totalPages={items.page?.totalPages ?? 1}
|
|
505
|
+
onPageChange={(value) => {
|
|
506
|
+
form.input.page.set(value - 1);
|
|
507
|
+
}}
|
|
508
|
+
onSizeChange={(value) => {
|
|
509
|
+
form.input.size.set(value);
|
|
510
|
+
}}
|
|
511
|
+
/>
|
|
339
512
|
)}
|
|
340
513
|
</Flex>
|
|
341
514
|
);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { FormModel } from "@alepha/react/form";
|
|
2
|
+
import { Flex } from "@mantine/core";
|
|
3
|
+
import { type TObject, t } from "alepha";
|
|
4
|
+
import { useMemo } from "react";
|
|
5
|
+
import { ui } from "../../constants/ui.ts";
|
|
6
|
+
import TypeForm, { type TypeFormProps } from "../form/TypeForm.tsx";
|
|
7
|
+
import type { FilterVisibility } from "./types.ts";
|
|
8
|
+
|
|
9
|
+
export interface DataTableFiltersProps {
|
|
10
|
+
schema: TObject;
|
|
11
|
+
form: FormModel<TObject>;
|
|
12
|
+
typeFormProps?: Partial<Omit<TypeFormProps<TObject>, "form">>;
|
|
13
|
+
filterVisibility: FilterVisibility;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const DataTableFilters = ({
|
|
17
|
+
schema,
|
|
18
|
+
form,
|
|
19
|
+
typeFormProps,
|
|
20
|
+
filterVisibility,
|
|
21
|
+
}: DataTableFiltersProps) => {
|
|
22
|
+
const visibleSchema = useMemo(() => {
|
|
23
|
+
const visibleKeys = Object.keys(schema.properties).filter(
|
|
24
|
+
(key) => filterVisibility[key] !== false,
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
if (visibleKeys.length === 0) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const visibleProps = visibleKeys.reduce(
|
|
32
|
+
(acc, key) => {
|
|
33
|
+
acc[key] = schema.properties[key];
|
|
34
|
+
return acc;
|
|
35
|
+
},
|
|
36
|
+
{} as Record<string, unknown>,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
return t.object(visibleProps as TObject["properties"]);
|
|
40
|
+
}, [schema, filterVisibility]);
|
|
41
|
+
|
|
42
|
+
if (!visibleSchema) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<Flex
|
|
48
|
+
w="100%"
|
|
49
|
+
p="xs"
|
|
50
|
+
bg={ui.colors.surface}
|
|
51
|
+
style={{ borderBottom: "1px solid var(--alepha-border)" }}
|
|
52
|
+
>
|
|
53
|
+
<TypeForm
|
|
54
|
+
{...typeFormProps}
|
|
55
|
+
skipSubmitButton
|
|
56
|
+
fill
|
|
57
|
+
form={form}
|
|
58
|
+
schema={visibleSchema}
|
|
59
|
+
/>
|
|
60
|
+
</Flex>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export default DataTableFilters;
|