@alepha/ui 0.16.2 → 0.17.1
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/{AdminApiKeys-CoTOTfgU.js → AdminApiKeys-CF_qOO3u.js} +20 -20
- package/dist/admin/AdminApiKeys-CF_qOO3u.js.map +1 -0
- package/dist/admin/{AdminAudits-BmsxFbDa.js → AdminAudits-BQno3hZG.js} +7 -8
- package/dist/admin/AdminAudits-BQno3hZG.js.map +1 -0
- package/dist/admin/{AdminFiles-BBB8knca.js → AdminFiles-kvuUaASF.js} +3 -5
- package/dist/admin/{AdminFiles-BBB8knca.js.map → AdminFiles-kvuUaASF.js.map} +1 -1
- package/dist/admin/AdminJobDashboard-CrPxp0W1.js +485 -0
- package/dist/admin/AdminJobDashboard-CrPxp0W1.js.map +1 -0
- package/dist/admin/AdminJobExecutions-D-b4Zt7W.js +678 -0
- package/dist/admin/AdminJobExecutions-D-b4Zt7W.js.map +1 -0
- package/dist/admin/AdminJobRegistry-CNX5cpDx.js +301 -0
- package/dist/admin/AdminJobRegistry-CNX5cpDx.js.map +1 -0
- package/dist/admin/{AdminLayout-CsjvpeD1.js → AdminLayout-e-ZP5nWw.js} +1 -1
- package/dist/admin/{AdminLayout-CsjvpeD1.js.map → AdminLayout-e-ZP5nWw.js.map} +1 -1
- package/dist/admin/{AdminNotifications-LwR6RKrx.js → AdminNotifications-DeHJFf6W.js} +3 -5
- package/dist/admin/{AdminNotifications-LwR6RKrx.js.map → AdminNotifications-DeHJFf6W.js.map} +1 -1
- package/dist/admin/{AdminParameters-B_83Vie9.js → AdminParameters-iQE8o7a7.js} +43 -36
- package/dist/admin/AdminParameters-iQE8o7a7.js.map +1 -0
- package/dist/admin/{AdminSessions-CWnPosdd.js → AdminSessions-oKJCbd7w.js} +5 -7
- package/dist/admin/AdminSessions-oKJCbd7w.js.map +1 -0
- package/dist/admin/{AdminUserAudits-nHv636E_.js → AdminUserAudits-BNCEle_E.js} +6 -8
- package/dist/admin/AdminUserAudits-BNCEle_E.js.map +1 -0
- package/dist/admin/{AdminUserCreate-CjYD3Kjc.js → AdminUserCreate-CgqeFwCt.js} +6 -7
- package/dist/admin/AdminUserCreate-CgqeFwCt.js.map +1 -0
- package/dist/admin/{AdminUserDetails-Ccq-LsZ0.js → AdminUserDetails-DDe1A1GP.js} +30 -29
- package/dist/admin/AdminUserDetails-DDe1A1GP.js.map +1 -0
- package/dist/admin/{AdminUserLayout-7s41DiF_.js → AdminUserLayout-HAlobhWf.js} +18 -16
- package/dist/admin/AdminUserLayout-HAlobhWf.js.map +1 -0
- package/dist/admin/{AdminUserSessions-Ds3ODq_d.js → AdminUserSessions-Bq1LnVLf.js} +5 -7
- package/dist/admin/AdminUserSessions-Bq1LnVLf.js.map +1 -0
- package/dist/admin/{AdminUserSettings-CGh4gROo.js → AdminUserSettings-BRsBZoxV.js} +10 -10
- package/dist/admin/AdminUserSettings-BRsBZoxV.js.map +1 -0
- package/dist/admin/{AdminUsers-CvPiBzQK.js → AdminUsers-D71kIOSn.js} +6 -8
- package/dist/admin/AdminUsers-D71kIOSn.js.map +1 -0
- package/dist/admin/index.d.ts +7 -83
- package/dist/admin/index.d.ts.map +1 -1
- package/dist/admin/index.js +49 -70
- package/dist/admin/index.js.map +1 -1
- package/dist/auth/{Login-DS_OqA0G.js → Login-BS_FYTy0.js} +13 -8
- package/dist/auth/Login-BS_FYTy0.js.map +1 -0
- package/dist/auth/{Profile-Di7N7HZL.js → Profile-CjDsW378.js} +16 -10
- package/dist/auth/Profile-CjDsW378.js.map +1 -0
- package/dist/auth/{Register-BRR2_gux.js → Register-C5eqzAaD.js} +21 -12
- package/dist/auth/Register-C5eqzAaD.js.map +1 -0
- package/dist/auth/{ResetPassword-oQu72lod.js → ResetPassword-XifinVao.js} +14 -8
- package/dist/auth/ResetPassword-XifinVao.js.map +1 -0
- package/dist/auth/{VerifyEmail-DC6HPZjd.js → VerifyEmail-DTgbeJOO.js} +6 -4
- package/dist/auth/VerifyEmail-DTgbeJOO.js.map +1 -0
- package/dist/auth/index.d.ts +4 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +15 -14
- package/dist/auth/index.js.map +1 -1
- package/dist/core/index.d.ts +37 -26
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +444 -193
- package/dist/core/index.js.map +1 -1
- package/dist/demo/DemoDataTable-lnBKWBf8.js +362 -0
- package/dist/demo/DemoDataTable-lnBKWBf8.js.map +1 -0
- package/dist/demo/{DemoHome-DpRrPlBC.js → DemoHome-CUMZsYaH.js} +6 -7
- package/dist/demo/DemoHome-CUMZsYaH.js.map +1 -0
- package/dist/demo/{DemoJsonViewer-zeucGKHV.js → DemoJsonViewer-_uokbGaW.js} +17 -19
- package/dist/demo/DemoJsonViewer-_uokbGaW.js.map +1 -0
- package/dist/demo/{DemoLayout-PhgbAAiQ.js → DemoLayout-DHVoacE6.js} +2 -4
- package/dist/demo/{DemoLayout-PhgbAAiQ.js.map → DemoLayout-DHVoacE6.js.map} +1 -1
- package/dist/demo/{DemoLogin-DSzP0Lkv.js → DemoLogin-DjJ9314c.js} +22 -17
- package/dist/demo/DemoLogin-DjJ9314c.js.map +1 -0
- package/dist/demo/{DemoRegister-DavFBsCz.js → DemoRegister-DzkJ5M83.js} +34 -25
- package/dist/demo/DemoRegister-DzkJ5M83.js.map +1 -0
- package/dist/demo/{DemoResetPassword-BS2rIAQK.js → DemoResetPassword-DWh4_BpQ.js} +27 -21
- package/dist/demo/DemoResetPassword-DWh4_BpQ.js.map +1 -0
- package/dist/demo/{DemoSidebar-zNkUmHRl.js → DemoSidebar-C1csnGhX.js} +2 -2
- package/dist/demo/{DemoSidebar-zNkUmHRl.js.map → DemoSidebar-C1csnGhX.js.map} +1 -1
- package/dist/demo/{DemoTypeForm-B9q7oT0b.js → DemoTypeForm-CWz6fJrJ.js} +2 -2
- package/dist/demo/{DemoTypeForm-B9q7oT0b.js.map → DemoTypeForm-CWz6fJrJ.js.map} +1 -1
- package/dist/demo/{DemoVerifyEmail-Bi4SdWz0.js → DemoVerifyEmail-DbU_tCj8.js} +13 -11
- package/dist/demo/DemoVerifyEmail-DbU_tCj8.js.map +1 -0
- package/dist/demo/{IconGoogle-CTeZyrek.js → IconGoogle-Ch1m3Uzl.js} +1 -1
- package/dist/demo/{IconGoogle-CTeZyrek.js.map → IconGoogle-Ch1m3Uzl.js.map} +1 -1
- package/dist/demo/{Showcase-C9btr_SJ.js → Showcase-BzoXNlCn.js} +10 -10
- package/dist/demo/Showcase-BzoXNlCn.js.map +1 -0
- package/dist/demo/index.d.ts +1 -68
- package/dist/demo/index.d.ts.map +1 -1
- package/dist/demo/index.js +11 -15
- package/dist/demo/index.js.map +1 -1
- package/dist/json/index.js +2 -2
- package/dist/json/index.js.map +1 -1
- package/package.json +9 -5
- package/src/admin/AdminRouter.ts +36 -5
- package/src/admin/components/audits/AdminAudits.tsx +5 -5
- package/src/admin/components/jobs/AdminJobDashboard.tsx +455 -0
- package/src/admin/components/jobs/AdminJobExecutions.tsx +693 -0
- package/src/admin/components/jobs/AdminJobRegistry.tsx +325 -0
- package/src/admin/components/keys/AdminApiKeys.tsx +28 -31
- package/src/admin/components/parameters/AdminParameters.tsx +3 -3
- package/src/admin/components/parameters/ParameterDetails.tsx +34 -29
- package/src/admin/components/parameters/ParameterEmptyState.tsx +5 -5
- package/src/admin/components/parameters/ParameterHistory.tsx +11 -19
- package/src/admin/components/parameters/ParameterTree.tsx +16 -18
- package/src/admin/components/sessions/AdminSessions.tsx +3 -3
- package/src/admin/components/shared/AdminResourceHeader.tsx +20 -16
- package/src/admin/components/users/AdminUserAudits.tsx +5 -5
- package/src/admin/components/users/AdminUserCreate.tsx +3 -3
- package/src/admin/components/users/AdminUserDetails.tsx +51 -53
- package/src/admin/components/users/AdminUserLayout.tsx +7 -7
- package/src/admin/components/users/AdminUserSessions.tsx +3 -3
- package/src/admin/components/users/AdminUserSettings.tsx +9 -9
- package/src/admin/components/users/AdminUsers.tsx +5 -5
- package/src/admin/components/verifications/AdminVerifications.tsx +3 -3
- package/src/admin/index.ts +0 -24
- package/src/auth/components/Login.tsx +13 -13
- package/src/auth/components/Profile.tsx +17 -26
- package/src/auth/components/Register.tsx +21 -31
- package/src/auth/components/ResetPassword.tsx +13 -22
- package/src/auth/components/VerifyEmail.tsx +5 -5
- package/src/auth/components/buttons/UserButton.tsx +14 -4
- package/src/core/components/buttons/ActionButton.tsx +9 -2
- package/src/core/components/data/ErrorViewer.tsx +15 -15
- package/src/core/components/dialogs/AlertDialog.tsx +3 -3
- package/src/core/components/dialogs/ConfirmDialog.tsx +3 -3
- package/src/core/components/dialogs/PromptDialog.tsx +3 -3
- package/src/core/components/form/Control.tsx +9 -0
- package/src/core/components/form/ControlArray.tsx +6 -7
- package/src/core/components/form/ControlObject.tsx +3 -3
- package/src/core/components/form/ControlQueryBuilder.tsx +20 -22
- package/src/core/components/form/ControlSelect.tsx +4 -0
- package/src/core/components/form/TypeForm.tsx +7 -0
- package/src/core/components/layout/Breadcrumb.tsx +6 -6
- package/src/core/components/layout/Omnibar.tsx +2 -1
- package/src/core/components/layout/Sidebar.tsx +5 -1
- package/src/core/components/table/ColumnPicker.tsx +47 -31
- package/src/core/components/table/DataTable.tsx +277 -201
- package/src/core/components/table/DataTableFilters.tsx +8 -0
- package/src/core/components/table/DataTableToolbar.tsx +98 -5
- package/src/core/components/table/FilterPicker.tsx +28 -26
- package/src/core/components/table/types.ts +52 -37
- package/src/core/components/table/useTableSelection.ts +83 -0
- package/src/core/styles.css +1 -0
- package/src/core/utils/parseInput.ts +1 -0
- package/src/demo/components/DemoHome.tsx +5 -5
- package/src/demo/components/core/DemoDataTable.tsx +209 -5
- package/src/demo/components/json/DemoJsonViewer.tsx +1 -1
- package/src/demo/components/shared/MacWindow.tsx +7 -7
- package/src/demo/components/shared/Showcase.tsx +3 -3
- package/src/demo/index.ts +0 -11
- package/src/json/components/JsonViewer.tsx +3 -3
- package/dist/admin/AdminApiKeys-CoTOTfgU.js.map +0 -1
- package/dist/admin/AdminAudits-BmsxFbDa.js.map +0 -1
- package/dist/admin/AdminJobs-C604joTz.js +0 -698
- package/dist/admin/AdminJobs-C604joTz.js.map +0 -1
- package/dist/admin/AdminParameters-B_83Vie9.js.map +0 -1
- package/dist/admin/AdminSessions-CWnPosdd.js.map +0 -1
- package/dist/admin/AdminUserAudits-nHv636E_.js.map +0 -1
- package/dist/admin/AdminUserCreate-CjYD3Kjc.js.map +0 -1
- package/dist/admin/AdminUserDetails-Ccq-LsZ0.js.map +0 -1
- package/dist/admin/AdminUserLayout-7s41DiF_.js.map +0 -1
- package/dist/admin/AdminUserSessions-Ds3ODq_d.js.map +0 -1
- package/dist/admin/AdminUserSettings-CGh4gROo.js.map +0 -1
- package/dist/admin/AdminUsers-CvPiBzQK.js.map +0 -1
- package/dist/admin/rolldown-runtime-CjeV3_4I.js +0 -18
- package/dist/auth/Login-DS_OqA0G.js.map +0 -1
- package/dist/auth/Profile-Di7N7HZL.js.map +0 -1
- package/dist/auth/Register-BRR2_gux.js.map +0 -1
- package/dist/auth/ResetPassword-oQu72lod.js.map +0 -1
- package/dist/auth/VerifyEmail-DC6HPZjd.js.map +0 -1
- package/dist/demo/DemoDataTable-DCsJq8v5.js +0 -149
- package/dist/demo/DemoDataTable-DCsJq8v5.js.map +0 -1
- package/dist/demo/DemoHome-DpRrPlBC.js.map +0 -1
- package/dist/demo/DemoJsonViewer-zeucGKHV.js.map +0 -1
- package/dist/demo/DemoLogin-DSzP0Lkv.js.map +0 -1
- package/dist/demo/DemoRegister-DavFBsCz.js.map +0 -1
- package/dist/demo/DemoResetPassword-BS2rIAQK.js.map +0 -1
- package/dist/demo/DemoVerifyEmail-Bi4SdWz0.js.map +0 -1
- package/dist/demo/Showcase-C9btr_SJ.js.map +0 -1
- package/dist/demo/rolldown-runtime-CjeV3_4I.js +0 -18
- package/src/admin/components/jobs/AdminJobs.tsx +0 -772
|
@@ -1,16 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
Checkbox,
|
|
3
|
+
Drawer,
|
|
4
|
+
Flex,
|
|
5
|
+
Table,
|
|
6
|
+
Text,
|
|
7
|
+
UnstyledButton,
|
|
8
|
+
} from "@mantine/core";
|
|
3
9
|
import {
|
|
4
10
|
IconArrowDown,
|
|
5
11
|
IconArrowsSort,
|
|
6
12
|
IconArrowUp,
|
|
13
|
+
IconChevronDown,
|
|
14
|
+
IconChevronRight,
|
|
7
15
|
} from "@tabler/icons-react";
|
|
8
16
|
import { Alepha, type Static, type TObject, t } from "alepha";
|
|
9
17
|
import { DateTimeProvider } from "alepha/datetime";
|
|
10
18
|
import { useInject } from "alepha/react";
|
|
11
19
|
import { type FormModel, useForm } from "alepha/react/form";
|
|
12
|
-
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
20
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
13
21
|
import { ui } from "../../constants/ui.ts";
|
|
22
|
+
import ActionButton from "../buttons/ActionButton.tsx";
|
|
14
23
|
import DataTableFilters, {
|
|
15
24
|
type DataTableFiltersProps,
|
|
16
25
|
} from "./DataTableFilters.tsx";
|
|
@@ -23,8 +32,8 @@ import type {
|
|
|
23
32
|
FilterVisibility,
|
|
24
33
|
MaybePage,
|
|
25
34
|
} from "./types.ts";
|
|
26
|
-
|
|
27
|
-
|
|
35
|
+
import { DEFAULT_MAX_VISIBLE_COLUMNS } from "./types.ts";
|
|
36
|
+
import { useTableSelection } from "./useTableSelection.ts";
|
|
28
37
|
|
|
29
38
|
type SortDirection = "asc" | "desc" | null;
|
|
30
39
|
|
|
@@ -73,15 +82,21 @@ const toggleSort = (
|
|
|
73
82
|
return parts.length > 0 ? parts.join(",") : undefined;
|
|
74
83
|
};
|
|
75
84
|
|
|
85
|
+
const toAriaSort = (
|
|
86
|
+
dir: SortDirection,
|
|
87
|
+
): "ascending" | "descending" | "none" => {
|
|
88
|
+
if (dir === "asc") return "ascending";
|
|
89
|
+
if (dir === "desc") return "descending";
|
|
90
|
+
return "none";
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const FIT_STYLE = { width: 1, whiteSpace: "nowrap" } as const;
|
|
94
|
+
|
|
76
95
|
const DataTable = <T extends object, Filters extends TObject>(
|
|
77
96
|
props: DataTableProps<T, Filters>,
|
|
78
97
|
) => {
|
|
79
98
|
const [items, setItems] = useState<MaybePage<T>>(
|
|
80
|
-
typeof props.items === "function"
|
|
81
|
-
? {
|
|
82
|
-
content: [],
|
|
83
|
-
}
|
|
84
|
-
: props.items,
|
|
99
|
+
typeof props.items === "function" ? { content: [] } : props.items,
|
|
85
100
|
);
|
|
86
101
|
|
|
87
102
|
const defaultSize = props.infinityScroll ? 100 : props.defaultSize || 10;
|
|
@@ -89,54 +104,39 @@ const DataTable = <T extends object, Filters extends TObject>(
|
|
|
89
104
|
const [size, setSize] = useState(String(defaultSize));
|
|
90
105
|
const [currentPage, setCurrentPage] = useState(0);
|
|
91
106
|
const alepha = useInject(Alepha);
|
|
107
|
+
const sentinelRef = useRef<HTMLDivElement>(null);
|
|
92
108
|
|
|
93
109
|
// Column visibility state
|
|
94
110
|
const [columnVisibility, setColumnVisibility] = useState<ColumnVisibility>(
|
|
95
111
|
() => {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
[key]
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
);
|
|
112
|
+
const entries = Object.entries(props.columns);
|
|
113
|
+
let visibleCount = 0;
|
|
114
|
+
return entries.reduce((acc, [key, col]) => {
|
|
115
|
+
if (col.defaultHidden) {
|
|
116
|
+
acc[key] = false;
|
|
117
|
+
} else if (visibleCount < DEFAULT_MAX_VISIBLE_COLUMNS) {
|
|
118
|
+
acc[key] = true;
|
|
119
|
+
visibleCount++;
|
|
120
|
+
} else {
|
|
121
|
+
acc[key] = false;
|
|
122
|
+
}
|
|
123
|
+
return acc;
|
|
124
|
+
}, {} as ColumnVisibility);
|
|
109
125
|
},
|
|
110
126
|
);
|
|
111
127
|
|
|
112
|
-
// Filter visibility state
|
|
128
|
+
// Filter visibility state — default: none visible
|
|
113
129
|
const [filterVisibility, setFilterVisibility] = useState<FilterVisibility>(
|
|
114
130
|
() => {
|
|
115
|
-
if (props.
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
if (!props.filters?.properties) {
|
|
119
|
-
return {};
|
|
120
|
-
}
|
|
131
|
+
if (!props.filters?.properties) return {};
|
|
132
|
+
const defaults = new Set(props.defaultFilters ?? []);
|
|
121
133
|
return Object.keys(props.filters.properties).reduce(
|
|
122
|
-
(acc, key) => ({ ...acc, [key]:
|
|
134
|
+
(acc, key) => ({ ...acc, [key]: defaults.has(key) }),
|
|
123
135
|
{} as FilterVisibility,
|
|
124
136
|
);
|
|
125
137
|
},
|
|
126
138
|
);
|
|
127
139
|
|
|
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
140
|
// Compute visible columns
|
|
141
141
|
const visibleColumns = useMemo(() => {
|
|
142
142
|
return Object.entries(props.columns).filter(
|
|
@@ -144,7 +144,7 @@ const DataTable = <T extends object, Filters extends TObject>(
|
|
|
144
144
|
);
|
|
145
145
|
}, [props.columns, columnVisibility]);
|
|
146
146
|
|
|
147
|
-
// Current sort string
|
|
147
|
+
// Current sort string
|
|
148
148
|
const [sortString, setSortString] = useState<string | undefined>(undefined);
|
|
149
149
|
|
|
150
150
|
// Handle column header click for sorting
|
|
@@ -156,88 +156,52 @@ const DataTable = <T extends object, Filters extends TObject>(
|
|
|
156
156
|
form.input.page.set(0); // Reset to first page when sorting changes
|
|
157
157
|
};
|
|
158
158
|
|
|
159
|
-
//
|
|
160
|
-
const [selectedKeys, setSelectedKeys] = useState<Set<string>>(new Set());
|
|
161
|
-
|
|
162
|
-
// Default getItemKey uses JSON.stringify if not provided
|
|
159
|
+
// Item key resolver — prefers explicit prop, falls back to .id, then JSON
|
|
163
160
|
const getItemKey = useCallback(
|
|
164
161
|
(item: T): string => {
|
|
165
|
-
if (props.getItemKey)
|
|
166
|
-
|
|
167
|
-
}
|
|
162
|
+
if (props.getItemKey) return props.getItemKey(item);
|
|
163
|
+
if ("id" in item) return String((item as Record<string, unknown>).id);
|
|
168
164
|
return JSON.stringify(item);
|
|
169
165
|
},
|
|
170
166
|
[props.getItemKey],
|
|
171
167
|
);
|
|
172
168
|
|
|
173
|
-
//
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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],
|
|
169
|
+
// Checkbox selection (extracted hook)
|
|
170
|
+
const selection = useTableSelection(
|
|
171
|
+
items.content as T[],
|
|
172
|
+
getItemKey,
|
|
173
|
+
props.withCheckbox ?? false,
|
|
213
174
|
);
|
|
214
175
|
|
|
215
|
-
//
|
|
216
|
-
const
|
|
217
|
-
if (
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
});
|
|
176
|
+
// Panel — normalize shorthand vs object form
|
|
177
|
+
const panelConfig = useMemo(() => {
|
|
178
|
+
if (!props.panel) return null;
|
|
179
|
+
if (typeof props.panel === "function") {
|
|
180
|
+
return { render: props.panel, can: undefined };
|
|
235
181
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
182
|
+
return props.panel;
|
|
183
|
+
}, [props.panel]);
|
|
184
|
+
|
|
185
|
+
// Drawer — normalize shorthand vs object form
|
|
186
|
+
const [drawerItem, setDrawerItem] = useState<T | null>(null);
|
|
187
|
+
const drawerConfig = useMemo(() => {
|
|
188
|
+
if (!props.drawer) return null;
|
|
189
|
+
if (typeof props.drawer === "function") {
|
|
190
|
+
return { render: props.drawer, can: undefined, props: undefined };
|
|
191
|
+
}
|
|
192
|
+
return props.drawer;
|
|
193
|
+
}, [props.drawer]);
|
|
194
|
+
|
|
195
|
+
// Panel expand state
|
|
196
|
+
const [expandedKeys, setExpandedKeys] = useState<Set<string>>(new Set());
|
|
197
|
+
|
|
198
|
+
const toggleExpand = useCallback((key: string) => {
|
|
199
|
+
setExpandedKeys((prev) => {
|
|
200
|
+
const next = new Set(prev);
|
|
201
|
+
if (next.has(key)) next.delete(key);
|
|
202
|
+
else next.add(key);
|
|
203
|
+
return next;
|
|
204
|
+
});
|
|
241
205
|
}, []);
|
|
242
206
|
|
|
243
207
|
const form = useForm(
|
|
@@ -276,7 +240,7 @@ const DataTable = <T extends object, Filters extends TObject>(
|
|
|
276
240
|
},
|
|
277
241
|
onReset: async () => {
|
|
278
242
|
setPage(1);
|
|
279
|
-
setSize(
|
|
243
|
+
setSize(String(defaultSize));
|
|
280
244
|
await form.submit();
|
|
281
245
|
},
|
|
282
246
|
onChange: async (key, value) => {
|
|
@@ -302,10 +266,6 @@ const DataTable = <T extends object, Filters extends TObject>(
|
|
|
302
266
|
[items],
|
|
303
267
|
);
|
|
304
268
|
|
|
305
|
-
const submitDebounce = useDebouncedCallback(() => form.submit(), {
|
|
306
|
-
delay: 800,
|
|
307
|
-
});
|
|
308
|
-
|
|
309
269
|
const dt = useInject(DateTimeProvider);
|
|
310
270
|
|
|
311
271
|
useEffect(() => {
|
|
@@ -326,30 +286,27 @@ const DataTable = <T extends object, Filters extends TObject>(
|
|
|
326
286
|
}
|
|
327
287
|
}, [props.items]);
|
|
328
288
|
|
|
329
|
-
// Infinity scroll
|
|
289
|
+
// Infinity scroll via IntersectionObserver
|
|
330
290
|
useEffect(() => {
|
|
331
291
|
if (!props.infinityScroll || typeof props.items !== "function") return;
|
|
332
292
|
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const scrollTop = window.scrollY;
|
|
337
|
-
const windowHeight = window.innerHeight;
|
|
338
|
-
const docHeight = document.documentElement.scrollHeight;
|
|
293
|
+
const sentinel = sentinelRef.current;
|
|
294
|
+
if (!sentinel) return;
|
|
339
295
|
|
|
340
|
-
|
|
296
|
+
const observer = new IntersectionObserver(
|
|
297
|
+
(entries) => {
|
|
298
|
+
if (!entries[0].isIntersecting || form.submitting) return;
|
|
341
299
|
|
|
342
|
-
if (isNearBottom) {
|
|
343
300
|
const totalPages = items.page?.totalPages ?? 1;
|
|
344
|
-
|
|
345
301
|
if (currentPage + 1 < totalPages) {
|
|
346
302
|
form.input.page.set(currentPage + 1);
|
|
347
303
|
}
|
|
348
|
-
}
|
|
349
|
-
|
|
304
|
+
},
|
|
305
|
+
{ rootMargin: "300px" },
|
|
306
|
+
);
|
|
350
307
|
|
|
351
|
-
|
|
352
|
-
return () =>
|
|
308
|
+
observer.observe(sentinel);
|
|
309
|
+
return () => observer.disconnect();
|
|
353
310
|
}, [
|
|
354
311
|
props.infinityScroll,
|
|
355
312
|
form.submitting,
|
|
@@ -358,13 +315,19 @@ const DataTable = <T extends object, Filters extends TObject>(
|
|
|
358
315
|
form,
|
|
359
316
|
]);
|
|
360
317
|
|
|
318
|
+
// Total column count (for colSpan)
|
|
319
|
+
const totalColumns =
|
|
320
|
+
visibleColumns.length +
|
|
321
|
+
(panelConfig ? 1 : 0) +
|
|
322
|
+
(props.withCheckbox ? 1 : 0);
|
|
323
|
+
|
|
361
324
|
// Checkbox header column
|
|
362
325
|
const checkboxHeader = props.withCheckbox ? (
|
|
363
326
|
<Table.Th style={{ width: 40 }}>
|
|
364
327
|
<Checkbox
|
|
365
|
-
checked={allSelected}
|
|
366
|
-
indeterminate={someSelected}
|
|
367
|
-
onChange={
|
|
328
|
+
checked={selection.allSelected}
|
|
329
|
+
indeterminate={selection.someSelected}
|
|
330
|
+
onChange={selection.toggleAll}
|
|
368
331
|
aria-label="Select all"
|
|
369
332
|
/>
|
|
370
333
|
</Table.Th>
|
|
@@ -376,74 +339,145 @@ const DataTable = <T extends object, Filters extends TObject>(
|
|
|
376
339
|
? getSortDirection(sortString, sortField)
|
|
377
340
|
: null;
|
|
378
341
|
|
|
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
342
|
return (
|
|
393
343
|
<Table.Th
|
|
394
344
|
key={key}
|
|
345
|
+
onClick={
|
|
346
|
+
col.sortable ? () => handleSortClick(key, col.sortKey) : undefined
|
|
347
|
+
}
|
|
348
|
+
aria-sort={col.sortable ? toAriaSort(sortDir) : undefined}
|
|
395
349
|
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" } : {}),
|
|
350
|
+
...(col.fit ? FIT_STYLE : {}),
|
|
351
|
+
...(col.sortable ? { cursor: "pointer", userSelect: "none" } : {}),
|
|
404
352
|
}}
|
|
405
353
|
>
|
|
406
|
-
|
|
407
|
-
<
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
354
|
+
<Flex align="center" gap={4}>
|
|
355
|
+
<Text size="xs">{col.label}</Text>
|
|
356
|
+
{col.sortable && (
|
|
357
|
+
<Flex c="dimmed">
|
|
358
|
+
{sortDir === "asc" && <IconArrowUp size={ui.sizes.icon.sm} />}
|
|
359
|
+
{sortDir === "desc" && <IconArrowDown size={ui.sizes.icon.sm} />}
|
|
360
|
+
{sortDir === null && <IconArrowsSort size={ui.sizes.icon.sm} />}
|
|
361
|
+
</Flex>
|
|
362
|
+
)}
|
|
363
|
+
</Flex>
|
|
413
364
|
</Table.Th>
|
|
414
365
|
);
|
|
415
366
|
});
|
|
416
367
|
|
|
417
|
-
const rows = items.content.
|
|
368
|
+
const rows = items.content.flatMap((item, index) => {
|
|
418
369
|
const trProps = props.tableTrProps ? props.tableTrProps(item as T) : {};
|
|
419
370
|
const itemKey = getItemKey(item as T);
|
|
420
|
-
const isSelected =
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
371
|
+
const isSelected = selection.isSelected(item as T);
|
|
372
|
+
const showPanel =
|
|
373
|
+
panelConfig && (panelConfig.can ? panelConfig.can(item as T) : true);
|
|
374
|
+
const isExpanded = expandedKeys.has(itemKey);
|
|
375
|
+
const canOpenDrawer =
|
|
376
|
+
drawerConfig && (drawerConfig.can ? drawerConfig.can(item as T) : true);
|
|
377
|
+
|
|
378
|
+
const elements = [
|
|
379
|
+
<Table.Tr
|
|
380
|
+
key={itemKey}
|
|
381
|
+
{...trProps}
|
|
382
|
+
style={{
|
|
383
|
+
...(canOpenDrawer ? { cursor: "pointer" } : {}),
|
|
384
|
+
...(trProps.style ?? {}),
|
|
385
|
+
}}
|
|
386
|
+
onClick={(e) => {
|
|
387
|
+
if (canOpenDrawer) setDrawerItem(item as T);
|
|
388
|
+
trProps.onClick?.(e);
|
|
389
|
+
}}
|
|
390
|
+
>
|
|
391
|
+
{panelConfig && (
|
|
392
|
+
<Table.Td style={{ width: 36, textAlign: "center" }} py={2} px={0}>
|
|
393
|
+
{showPanel && (
|
|
394
|
+
<UnstyledButton
|
|
395
|
+
onClick={(e) => {
|
|
396
|
+
e.stopPropagation();
|
|
397
|
+
toggleExpand(itemKey);
|
|
398
|
+
}}
|
|
399
|
+
style={{ display: "inline-flex" }}
|
|
400
|
+
>
|
|
401
|
+
<Flex c="dimmed" align="center" justify="center">
|
|
402
|
+
{isExpanded ? (
|
|
403
|
+
<IconChevronDown size={ui.sizes.icon.sm} />
|
|
404
|
+
) : (
|
|
405
|
+
<IconChevronRight size={ui.sizes.icon.sm} />
|
|
406
|
+
)}
|
|
407
|
+
</Flex>
|
|
408
|
+
</UnstyledButton>
|
|
409
|
+
)}
|
|
410
|
+
</Table.Td>
|
|
411
|
+
)}
|
|
424
412
|
{props.withCheckbox && (
|
|
425
|
-
<Table.Td style={{ width: 40 }}>
|
|
413
|
+
<Table.Td style={{ width: 40 }} onClick={(e) => e.stopPropagation()}>
|
|
426
414
|
<Checkbox
|
|
427
415
|
checked={isSelected}
|
|
428
|
-
onChange={() =>
|
|
416
|
+
onChange={() => selection.toggleItem(item as T)}
|
|
429
417
|
aria-label="Select row"
|
|
430
418
|
/>
|
|
431
419
|
</Table.Td>
|
|
432
420
|
)}
|
|
433
|
-
{visibleColumns.map(([key, col]) =>
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
421
|
+
{visibleColumns.map(([key, col]) => {
|
|
422
|
+
const ctx = {
|
|
423
|
+
index,
|
|
424
|
+
form: form as unknown as FormModel<Filters>,
|
|
425
|
+
alepha,
|
|
426
|
+
} as DataTableColumnContext<Filters>;
|
|
427
|
+
|
|
428
|
+
if (col.actions) {
|
|
429
|
+
const rowActions = col
|
|
430
|
+
.actions(item as T, ctx)
|
|
431
|
+
.filter((a) => a.visible !== false);
|
|
432
|
+
return (
|
|
433
|
+
<Table.Td
|
|
434
|
+
py={2}
|
|
435
|
+
px={4}
|
|
436
|
+
key={key}
|
|
437
|
+
style={col.fit ? FIT_STYLE : undefined}
|
|
438
|
+
onClick={(e) => e.stopPropagation()}
|
|
439
|
+
>
|
|
440
|
+
<Flex gap={4}>
|
|
441
|
+
{rowActions.map(({ visible: _, ...actionProps }, i) => (
|
|
442
|
+
<ActionButton
|
|
443
|
+
key={i}
|
|
444
|
+
variant="subtle"
|
|
445
|
+
size="xs"
|
|
446
|
+
preventDefault
|
|
447
|
+
h={20}
|
|
448
|
+
{...actionProps}
|
|
449
|
+
/>
|
|
450
|
+
))}
|
|
451
|
+
</Flex>
|
|
452
|
+
</Table.Td>
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return (
|
|
457
|
+
<Table.Td
|
|
458
|
+
py={2}
|
|
459
|
+
px={4}
|
|
460
|
+
key={key}
|
|
461
|
+
style={col.fit ? FIT_STYLE : undefined}
|
|
462
|
+
>
|
|
463
|
+
{col.value?.(item as T, ctx)}
|
|
464
|
+
</Table.Td>
|
|
465
|
+
);
|
|
466
|
+
})}
|
|
467
|
+
</Table.Tr>,
|
|
468
|
+
];
|
|
469
|
+
|
|
470
|
+
if (panelConfig && showPanel && isExpanded) {
|
|
471
|
+
elements.push(
|
|
472
|
+
<Table.Tr key={`${itemKey}-panel`}>
|
|
473
|
+
<Table.Td colSpan={totalColumns} p={0}>
|
|
474
|
+
{panelConfig.render(item as T)}
|
|
443
475
|
</Table.Td>
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
476
|
+
</Table.Tr>,
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return elements;
|
|
447
481
|
});
|
|
448
482
|
|
|
449
483
|
const filterSchema = useMemo(() => {
|
|
@@ -452,26 +486,21 @@ const DataTable = <T extends object, Filters extends TObject>(
|
|
|
452
486
|
}, [props.filters, form.options.schema]);
|
|
453
487
|
|
|
454
488
|
return (
|
|
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
|
-
>
|
|
489
|
+
<Flex flex={1} p={0} bdrs="sm" direction="column">
|
|
463
490
|
<DataTableToolbar
|
|
464
491
|
columns={props.columns}
|
|
465
492
|
filters={props.filters}
|
|
466
493
|
columnVisibility={columnVisibility}
|
|
467
494
|
filterVisibility={filterVisibility}
|
|
468
|
-
onColumnVisibilityChange={
|
|
469
|
-
onFilterVisibilityChange={
|
|
495
|
+
onColumnVisibilityChange={setColumnVisibility}
|
|
496
|
+
onFilterVisibilityChange={setFilterVisibility}
|
|
470
497
|
actions={props.actions}
|
|
471
498
|
onRefresh={() => form.submit()}
|
|
472
|
-
|
|
499
|
+
items={items.content as T[]}
|
|
500
|
+
withExport={props.withExport}
|
|
501
|
+
selectedItems={selection.selectedItems}
|
|
473
502
|
checkboxActions={props.checkboxActions}
|
|
474
|
-
onClearSelection={
|
|
503
|
+
onClearSelection={selection.clear}
|
|
475
504
|
/>
|
|
476
505
|
|
|
477
506
|
{filterSchema && props.filters && (
|
|
@@ -486,17 +515,52 @@ const DataTable = <T extends object, Filters extends TObject>(
|
|
|
486
515
|
)}
|
|
487
516
|
|
|
488
517
|
<Flex className="overflow-auto">
|
|
489
|
-
<Table
|
|
490
|
-
|
|
518
|
+
<Table
|
|
519
|
+
aria-label="Data table"
|
|
520
|
+
withColumnBorders
|
|
521
|
+
withRowBorders
|
|
522
|
+
{...props.tableProps}
|
|
523
|
+
>
|
|
524
|
+
<Table.Thead
|
|
525
|
+
style={{
|
|
526
|
+
position: "sticky",
|
|
527
|
+
top: 0,
|
|
528
|
+
zIndex: 1,
|
|
529
|
+
backgroundColor: "var(--mantine-color-body)",
|
|
530
|
+
}}
|
|
531
|
+
>
|
|
491
532
|
<Table.Tr>
|
|
533
|
+
{panelConfig && <Table.Th style={{ width: 36 }} />}
|
|
492
534
|
{checkboxHeader}
|
|
493
535
|
{head}
|
|
494
536
|
</Table.Tr>
|
|
495
537
|
</Table.Thead>
|
|
496
|
-
<Table.Tbody
|
|
538
|
+
<Table.Tbody
|
|
539
|
+
style={{
|
|
540
|
+
opacity: form.submitting ? 0.5 : 1,
|
|
541
|
+
transition: "opacity 150ms ease",
|
|
542
|
+
}}
|
|
543
|
+
>
|
|
544
|
+
{rows}
|
|
545
|
+
{items.content.length === 0 && (
|
|
546
|
+
<Table.Tr>
|
|
547
|
+
<Table.Td
|
|
548
|
+
colSpan={totalColumns || 1}
|
|
549
|
+
py="xl"
|
|
550
|
+
style={{ textAlign: "center" }}
|
|
551
|
+
>
|
|
552
|
+
<Text c="dimmed" size="sm">
|
|
553
|
+
{form.submitting ? "Loading…" : "No results"}
|
|
554
|
+
</Text>
|
|
555
|
+
</Table.Td>
|
|
556
|
+
</Table.Tr>
|
|
557
|
+
)}
|
|
558
|
+
</Table.Tbody>
|
|
497
559
|
</Table>
|
|
498
560
|
</Flex>
|
|
499
561
|
|
|
562
|
+
{props.infinityScroll && <div ref={sentinelRef} />}
|
|
563
|
+
|
|
500
564
|
{!props.infinityScroll && (
|
|
501
565
|
<DataTablePagination
|
|
502
566
|
page={page}
|
|
@@ -510,6 +574,18 @@ const DataTable = <T extends object, Filters extends TObject>(
|
|
|
510
574
|
}}
|
|
511
575
|
/>
|
|
512
576
|
)}
|
|
577
|
+
|
|
578
|
+
{drawerConfig && (
|
|
579
|
+
<Drawer
|
|
580
|
+
opened={drawerItem !== null}
|
|
581
|
+
onClose={() => setDrawerItem(null)}
|
|
582
|
+
position="right"
|
|
583
|
+
size="xl"
|
|
584
|
+
{...drawerConfig.props}
|
|
585
|
+
>
|
|
586
|
+
{drawerItem && drawerConfig.render(drawerItem)}
|
|
587
|
+
</Drawer>
|
|
588
|
+
)}
|
|
513
589
|
</Flex>
|
|
514
590
|
);
|
|
515
591
|
};
|
|
@@ -51,11 +51,19 @@ const DataTableFilters = ({
|
|
|
51
51
|
style={{ borderBottom: "1px solid var(--alepha-border)" }}
|
|
52
52
|
>
|
|
53
53
|
<TypeForm
|
|
54
|
+
size={"xs"}
|
|
54
55
|
{...typeFormProps}
|
|
55
56
|
skipSubmitButton
|
|
56
57
|
fill
|
|
57
58
|
form={form}
|
|
58
59
|
schema={visibleSchema}
|
|
60
|
+
columns={{
|
|
61
|
+
base: 1,
|
|
62
|
+
sm: 2,
|
|
63
|
+
md: 3,
|
|
64
|
+
lg: 4,
|
|
65
|
+
xl: 6,
|
|
66
|
+
}}
|
|
59
67
|
/>
|
|
60
68
|
</Flex>
|
|
61
69
|
);
|