@alepha/ui 0.13.6 → 0.13.8

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.
Files changed (157) hide show
  1. package/dist/admin/AdminAudits-CwvH8e8c.js +215 -0
  2. package/dist/admin/AdminAudits-CwvH8e8c.js.map +1 -0
  3. package/dist/admin/AdminAudits-Dv8Vk_6r.js +3 -0
  4. package/dist/admin/AdminFiles-5CPA3lQk.js +3 -0
  5. package/dist/admin/{AdminFiles-B_jfB_Py.js → AdminFiles-C_w1tb_x.js} +4 -3
  6. package/dist/admin/AdminFiles-C_w1tb_x.js.map +1 -0
  7. package/dist/admin/AdminLayout-BnSmtA4x.js +3 -0
  8. package/dist/admin/AdminLayout-XiSivwWH.js +39 -0
  9. package/dist/admin/AdminLayout-XiSivwWH.js.map +1 -0
  10. package/dist/admin/AdminNotifications-DLjmZWtf.js +3 -0
  11. package/dist/admin/{AdminNotifications-BFEjqpqx.js → AdminNotifications-DuYy74AN.js} +3 -3
  12. package/dist/admin/AdminNotifications-DuYy74AN.js.map +1 -0
  13. package/dist/admin/AdminParameters-DYg48Jwe.js +3 -0
  14. package/dist/admin/AdminParameters-YagqWTG3.js +575 -0
  15. package/dist/admin/AdminParameters-YagqWTG3.js.map +1 -0
  16. package/dist/admin/{AdminSessions-D7DESfWK.js → AdminSessions-BCjgJ-93.js} +4 -4
  17. package/dist/admin/AdminSessions-BCjgJ-93.js.map +1 -0
  18. package/dist/admin/AdminSessions-DEh2uN-4.js +3 -0
  19. package/dist/admin/AdminUserAudits-B_PUXCKC.js +177 -0
  20. package/dist/admin/AdminUserAudits-B_PUXCKC.js.map +1 -0
  21. package/dist/admin/AdminUserAudits-D7cTcElL.js +3 -0
  22. package/dist/admin/{AdminUserCreate-Bhxsn92l.js → AdminUserCreate-DzfRbGZ4.js} +4 -4
  23. package/dist/admin/AdminUserCreate-DzfRbGZ4.js.map +1 -0
  24. package/dist/admin/{AdminUserCreate-CYI_xW5T.js → AdminUserCreate-oUA1KDIl.js} +1 -1
  25. package/dist/admin/{AdminUserDetails-C2y1Ig4n.js → AdminUserDetails-DeTrJm-t.js} +5 -5
  26. package/dist/admin/AdminUserDetails-DeTrJm-t.js.map +1 -0
  27. package/dist/admin/{AdminUserDetails-Cmzx9HxH.js → AdminUserDetails-y1H5DW8Y.js} +1 -1
  28. package/dist/admin/{AdminUserLayout-sW6cjZL0.js → AdminUserLayout-CsfrrZkD.js} +4 -7
  29. package/dist/admin/AdminUserLayout-CsfrrZkD.js.map +1 -0
  30. package/dist/admin/{AdminUserLayout-DGSf612u.js → AdminUserLayout-Dejnz13m.js} +1 -1
  31. package/dist/admin/AdminUserSessions-Bbhcpz4k.js +3 -0
  32. package/dist/admin/{AdminUserSessions-CvN15wPe.js → AdminUserSessions-DO9H85O-.js} +4 -4
  33. package/dist/admin/AdminUserSessions-DO9H85O-.js.map +1 -0
  34. package/dist/admin/{AdminUserSettings-DvaaxgcV.js → AdminUserSettings-B3jA8g3p.js} +4 -4
  35. package/dist/admin/AdminUserSettings-B3jA8g3p.js.map +1 -0
  36. package/dist/admin/AdminUserSettings-CE0xpbQc.js +3 -0
  37. package/dist/admin/AdminUsers-CegGZDhW.js +3 -0
  38. package/dist/admin/{AdminUsers-BR3C-jrg.js → AdminUsers-ebbrJBT0.js} +13 -17
  39. package/dist/admin/AdminUsers-ebbrJBT0.js.map +1 -0
  40. package/dist/admin/index.d.ts +2700 -1178
  41. package/dist/admin/index.js +65 -62
  42. package/dist/admin/index.js.map +1 -1
  43. package/dist/auth/AuthLayout-BAZJHzDG.js +23 -0
  44. package/dist/auth/AuthLayout-BAZJHzDG.js.map +1 -0
  45. package/dist/auth/{Login-7HlBjDeV.js → Login-CeNZZjrr.js} +80 -44
  46. package/dist/auth/Login-CeNZZjrr.js.map +1 -0
  47. package/dist/auth/Login-hQcu1nlu.js +4 -0
  48. package/dist/auth/Register-B6HBNVHS.js +4 -0
  49. package/dist/auth/{Register-CuQr3kgi.js → Register-s4ENeyiE.js} +131 -91
  50. package/dist/auth/Register-s4ENeyiE.js.map +1 -0
  51. package/dist/auth/ResetPassword-Cjd-W-Nu.js +3 -0
  52. package/dist/auth/ResetPassword-GLIFkJT7.js +278 -0
  53. package/dist/auth/ResetPassword-GLIFkJT7.js.map +1 -0
  54. package/dist/auth/index.d.ts +605 -532
  55. package/dist/auth/index.js +26 -18
  56. package/dist/auth/index.js.map +1 -1
  57. package/dist/core/index.d.ts +425 -155
  58. package/dist/core/index.js +1751 -1369
  59. package/dist/core/index.js.map +1 -1
  60. package/package.json +23 -20
  61. package/src/admin/AdminRouter.ts +70 -16
  62. package/src/admin/components/AdminLayout.tsx +41 -61
  63. package/src/admin/components/audits/AdminAudits.tsx +240 -0
  64. package/src/admin/components/{AdminFiles.tsx → files/AdminFiles.tsx} +1 -1
  65. package/src/admin/components/{AdminJobs.tsx → jobs/AdminJobs.tsx} +1 -1
  66. package/src/admin/components/parameters/AdminParameters.tsx +137 -0
  67. package/src/admin/components/parameters/ParameterDetails.tsx +228 -0
  68. package/src/admin/components/parameters/ParameterHistory.tsx +146 -0
  69. package/src/admin/components/parameters/ParameterTree.tsx +146 -0
  70. package/src/admin/components/parameters/types.ts +35 -0
  71. package/src/admin/components/{AdminSessions.tsx → sessions/AdminSessions.tsx} +1 -1
  72. package/src/admin/components/users/AdminUserAudits.tsx +183 -0
  73. package/src/admin/components/{AdminUserCreate.tsx → users/AdminUserCreate.tsx} +1 -1
  74. package/src/admin/components/{AdminUserLayout.tsx → users/AdminUserLayout.tsx} +1 -4
  75. package/src/admin/components/{AdminUserSettings.tsx → users/AdminUserSettings.tsx} +1 -1
  76. package/src/admin/components/{AdminUsers.tsx → users/AdminUsers.tsx} +10 -12
  77. package/src/admin/index.ts +24 -16
  78. package/src/auth/AuthRouter.ts +23 -17
  79. package/src/auth/components/AuthLayout.tsx +6 -3
  80. package/src/auth/components/Login.tsx +109 -47
  81. package/src/auth/components/Register.tsx +158 -94
  82. package/src/auth/components/ResetPassword.tsx +51 -5
  83. package/src/auth/components/buttons/UserButton.tsx +2 -0
  84. package/src/core/atoms/alephaThemeAtom.ts +13 -0
  85. package/src/core/atoms/alephaThemeListAtom.ts +10 -0
  86. package/src/core/atoms/themes/default.ts +6 -0
  87. package/src/core/{themes → atoms/themes}/midnight.ts +3 -5
  88. package/src/core/components/buttons/ActionButton.tsx +33 -26
  89. package/src/core/components/buttons/DarkModeButton.tsx +0 -1
  90. package/src/core/components/buttons/ThemeButton.tsx +10 -7
  91. package/src/core/components/buttons/ToggleSidebarButton.tsx +19 -16
  92. package/src/core/components/data/ErrorViewer.tsx +171 -0
  93. package/src/core/components/data/JsonViewer.tsx +147 -138
  94. package/src/core/components/form/Control.tsx +95 -18
  95. package/src/core/components/form/ControlArray.tsx +377 -0
  96. package/src/core/components/form/ControlObject.tsx +127 -0
  97. package/src/core/components/form/TypeForm.tsx +99 -37
  98. package/src/core/components/layout/AdminShell.tsx +14 -1
  99. package/src/core/components/layout/AlephaMantineProvider.tsx +7 -3
  100. package/src/core/components/layout/Omnibar.tsx +1 -1
  101. package/src/core/components/layout/Sidebar.tsx +47 -14
  102. package/src/core/components/table/ColumnPicker.tsx +126 -0
  103. package/src/core/components/table/DataTable.tsx +354 -181
  104. package/src/core/components/table/DataTableFilters.tsx +64 -0
  105. package/src/core/components/table/DataTablePagination.tsx +59 -0
  106. package/src/core/components/table/DataTableToolbar.tsx +126 -0
  107. package/src/core/components/table/FilterPicker.tsx +138 -0
  108. package/src/core/components/table/types.ts +199 -0
  109. package/src/core/helpers/isComponentType.ts +9 -0
  110. package/src/core/helpers/renderIcon.tsx +13 -0
  111. package/src/core/hooks/useTheme.ts +24 -18
  112. package/src/core/index.ts +24 -3
  113. package/src/core/interfaces/AlephaTheme.ts +8 -0
  114. package/src/core/providers/ThemeProvider.ts +44 -62
  115. package/src/core/services/DialogService.tsx +24 -0
  116. package/src/core/utils/parseInput.ts +2 -2
  117. package/styles.css +1 -1
  118. package/dist/admin/AdminFiles-B-0UcHVV.js +0 -3
  119. package/dist/admin/AdminFiles-B_jfB_Py.js.map +0 -1
  120. package/dist/admin/AdminLayout-BMtiXAzS.js +0 -396
  121. package/dist/admin/AdminLayout-BMtiXAzS.js.map +0 -1
  122. package/dist/admin/AdminLayout-BNo3GoHR.js +0 -3
  123. package/dist/admin/AdminNotifications-BFEjqpqx.js.map +0 -1
  124. package/dist/admin/AdminNotifications-DJs2ZjNj.js +0 -3
  125. package/dist/admin/AdminSessions-D7DESfWK.js.map +0 -1
  126. package/dist/admin/AdminSessions-PS2M8iXi.js +0 -3
  127. package/dist/admin/AdminUserCreate-Bhxsn92l.js.map +0 -1
  128. package/dist/admin/AdminUserDetails-C2y1Ig4n.js.map +0 -1
  129. package/dist/admin/AdminUserLayout-sW6cjZL0.js.map +0 -1
  130. package/dist/admin/AdminUserSessions-CvN15wPe.js.map +0 -1
  131. package/dist/admin/AdminUserSessions-D-aOcZgV.js +0 -3
  132. package/dist/admin/AdminUserSettings-CEMhIYrI.js +0 -3
  133. package/dist/admin/AdminUserSettings-DvaaxgcV.js.map +0 -1
  134. package/dist/admin/AdminUsers-BR3C-jrg.js.map +0 -1
  135. package/dist/admin/AdminUsers-CMW9vN09.js +0 -3
  136. package/dist/auth/AuthLayout-CzwUKD9y.js +0 -19
  137. package/dist/auth/AuthLayout-CzwUKD9y.js.map +0 -1
  138. package/dist/auth/Login-7HlBjDeV.js.map +0 -1
  139. package/dist/auth/Login-C-e27DGb.js +0 -4
  140. package/dist/auth/Register-CuQr3kgi.js.map +0 -1
  141. package/dist/auth/Register-DbvXwgbG.js +0 -4
  142. package/dist/auth/ResetPassword-BzU-cdd4.js +0 -243
  143. package/dist/auth/ResetPassword-BzU-cdd4.js.map +0 -1
  144. package/dist/auth/ResetPassword-DSvrdpaA.js +0 -3
  145. package/src/admin/AdminSidebar.ts +0 -31
  146. package/src/admin/components/AdminParameters.tsx +0 -24
  147. package/src/core/themes/aurora.ts +0 -107
  148. package/src/core/themes/crystal.ts +0 -107
  149. package/src/core/themes/default.ts +0 -7
  150. package/src/core/themes/ember.ts +0 -107
  151. package/src/core/themes/index.ts +0 -7
  152. package/src/core/themes/remoraid.ts +0 -278
  153. package/src/core/themes/slate.ts +0 -81
  154. /package/src/admin/components/{AdminNotifications.tsx → notifications/AdminNotifications.tsx} +0 -0
  155. /package/src/admin/components/{AdminUserDetails.tsx → users/AdminUserDetails.tsx} +0 -0
  156. /package/src/admin/components/{AdminUserSessions.tsx → users/AdminUserSessions.tsx} +0 -0
  157. /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
- Alepha,
15
- type Async,
16
- type Page,
17
- type PageMetadata,
18
- type Static,
19
- type TObject,
20
- t,
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 ActionButton from "../buttons/ActionButton.tsx";
26
- import TypeForm, { type TypeFormProps } from "../form/TypeForm.tsx";
27
-
28
- export interface DataTableColumnContext<Filters extends TObject> {
29
- index: number;
30
- form: FormModel<Filters>;
31
- alepha: Alepha;
32
- }
33
-
34
- export interface DataTableColumn<T extends object, Filters extends TObject> {
35
- label: string;
36
- value: (item: T, ctx: DataTableColumnContext<Filters>) => ReactNode;
37
- fit?: boolean;
38
- }
39
-
40
- export type MaybePage<T> = Omit<Page<T>, "page"> & {
41
- page?: Partial<PageMetadata>;
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
- export interface DataTableSubmitContext<T extends object> {
45
- items: T[];
46
- }
47
-
48
- export interface DataTableProps<T extends object, Filters extends TObject> {
49
- /**
50
- * 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.
51
- */
52
- items:
53
- | MaybePage<T>
54
- | ((
55
- filters: Static<Filters> & {
56
- page: number;
57
- size: number;
58
- sort?: string;
59
- },
60
- ctx: DataTableSubmitContext<T>,
61
- ) => Async<MaybePage<T>>);
62
-
63
- /**
64
- * The columns to display in the table. Each column is defined by a key and a DataTableColumn object.
65
- */
66
- columns: {
67
- [key: string]: DataTableColumn<T, Filters>;
68
- };
69
-
70
- defaultSize?: number;
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, args) => {
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?.(key, value, form as any);
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
- const head = Object.entries(props.columns).map(([key, col]) => (
247
- <Table.Th
248
- key={key}
249
- style={{
250
- ...(col.fit
251
- ? {
252
- width: "1%",
253
- whiteSpace: "nowrap",
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
- ? props.tableTrProps(item as T)
267
- : ({} as TableTrProps);
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={JSON.stringify(item)} {...trProps}>
270
- {Object.entries(props.columns).map(([key, col]) => (
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(item as T, {
273
- index,
274
- form: form as unknown as FormModel<Filters>,
275
- alepha,
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 schema = t.omit(form.options.schema, ["page", "size", "sort"]);
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 direction={"column"} gap={"sm"} flex={1}>
287
- {props.filters ? (
288
- <Card withBorder p={"lg"} bg={ui.colors.elevated}>
289
- <TypeForm
290
- {...props.typeFormProps}
291
- form={form as unknown as FormModel<Filters>}
292
- schema={schema}
293
- />
294
- </Card>
295
- ) : null}
296
-
297
- <Flex className={"overflow-auto"}>
298
- <Table
299
- striped
300
- withRowBorders
301
- withColumnBorders
302
- withTableBorder
303
- stripedColor={""}
304
- {...props.tableProps}
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>{head}</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
- <Flex justify={"space-between"} align={"center"}>
315
- <Pagination
316
- withEdges
317
- total={items.page?.totalPages ?? 1}
318
- value={page}
319
- onChange={(value) => {
320
- form.input.page.set(value - 1);
321
- }}
322
- />
323
- <Flex>
324
- <Select
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;