@eggspot/ui 0.0.0
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/eslint.config.js +4 -0
- package/package.json +66 -0
- package/postcss.config.mjs +1 -0
- package/src/components/Button.machine.tsx +50 -0
- package/src/components/Button.tsx +249 -0
- package/src/components/Button.variants.tsx +186 -0
- package/src/components/ButtonGroup.tsx +56 -0
- package/src/components/Calendar.tsx +275 -0
- package/src/components/Calendar.utils.tsx +22 -0
- package/src/components/Checkbox.tsx +199 -0
- package/src/components/ConfirmDialog.tsx +183 -0
- package/src/components/DashboardLayout/DashboardLayout.tsx +348 -0
- package/src/components/DashboardLayout/SidebarNav.tsx +509 -0
- package/src/components/DashboardLayout/index.ts +33 -0
- package/src/components/DataTable/DataTable.tsx +557 -0
- package/src/components/DataTable/DataTableColumnHeader.tsx +122 -0
- package/src/components/DataTable/DataTableDisplaySettings.tsx +265 -0
- package/src/components/DataTable/DataTableFloatingBar.tsx +44 -0
- package/src/components/DataTable/DataTablePagination.tsx +168 -0
- package/src/components/DataTable/DataTableStates.tsx +69 -0
- package/src/components/DataTable/DataTableToolbarContainer.tsx +47 -0
- package/src/components/DataTable/hooks/use-data-table-settings.ts +101 -0
- package/src/components/DataTable/index.ts +7 -0
- package/src/components/DataTable/types/data-table.ts +97 -0
- package/src/components/DatePicker.tsx +213 -0
- package/src/components/DatePicker.utils.tsx +38 -0
- package/src/components/Datefield.tsx +109 -0
- package/src/components/Datefield.utils.ts +10 -0
- package/src/components/Dialog.tsx +167 -0
- package/src/components/Field.tsx +49 -0
- package/src/components/Filter/Filter.store.tsx +122 -0
- package/src/components/Filter/Filter.tsx +11 -0
- package/src/components/Filter/Filter.types.ts +107 -0
- package/src/components/Filter/FilterBar.tsx +38 -0
- package/src/components/Filter/FilterBuilder.tsx +158 -0
- package/src/components/Filter/FilterField/DateModeRowValue.tsx +250 -0
- package/src/components/Filter/FilterField/FilterAsyncSelect.tsx +191 -0
- package/src/components/Filter/FilterField/FilterDateMode.tsx +241 -0
- package/src/components/Filter/FilterField/FilterDateRange.tsx +169 -0
- package/src/components/Filter/FilterField/FilterSelect.tsx +208 -0
- package/src/components/Filter/FilterField/FilterSingleDate.tsx +277 -0
- package/src/components/Filter/FilterField/OptionItem.tsx +112 -0
- package/src/components/Filter/FilterField/index.ts +6 -0
- package/src/components/Filter/FilterRow.tsx +527 -0
- package/src/components/Filter/index.ts +17 -0
- package/src/components/Form.tsx +195 -0
- package/src/components/Heading.tsx +41 -0
- package/src/components/Input.tsx +221 -0
- package/src/components/InputOTP.tsx +78 -0
- package/src/components/Label.tsx +65 -0
- package/src/components/Layout.tsx +129 -0
- package/src/components/ListBox.tsx +97 -0
- package/src/components/Menu.tsx +152 -0
- package/src/components/NativeSelect.tsx +77 -0
- package/src/components/NumberInput.tsx +114 -0
- package/src/components/Popover.tsx +44 -0
- package/src/components/Provider.tsx +22 -0
- package/src/components/RadioGroup.tsx +191 -0
- package/src/components/Resizable.tsx +71 -0
- package/src/components/ScrollArea.tsx +57 -0
- package/src/components/Select.tsx +626 -0
- package/src/components/Select.utils.tsx +64 -0
- package/src/components/Separator.tsx +25 -0
- package/src/components/Sheet.tsx +147 -0
- package/src/components/Sonner.tsx +96 -0
- package/src/components/Spinner.tsx +30 -0
- package/src/components/Switch.tsx +51 -0
- package/src/components/Text.tsx +35 -0
- package/src/components/Tooltip.tsx +58 -0
- package/src/consts/config.ts +2 -0
- package/src/hooks/.gitkeep +0 -0
- package/src/lib/utils.ts +10 -0
- package/tsconfig.json +11 -0
- package/tsconfig.lint.json +8 -0
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
import { memo, RefObject, useEffect, useMemo, useRef } from "react"
|
|
2
|
+
import { Checkbox } from "@eggspot/ui/components/Checkbox"
|
|
3
|
+
import { DataTableProps } from "@eggspot/ui/components/DataTable/types/data-table"
|
|
4
|
+
import { cn } from "@eggspot/ui/lib/utils"
|
|
5
|
+
import {
|
|
6
|
+
Cell,
|
|
7
|
+
ColumnDef,
|
|
8
|
+
ColumnPinningState,
|
|
9
|
+
flexRender,
|
|
10
|
+
getCoreRowModel,
|
|
11
|
+
getFilteredRowModel,
|
|
12
|
+
getPaginationRowModel,
|
|
13
|
+
getSortedRowModel,
|
|
14
|
+
Header,
|
|
15
|
+
Table,
|
|
16
|
+
useReactTable,
|
|
17
|
+
} from "@tanstack/react-table"
|
|
18
|
+
import { useVirtualizer } from "@tanstack/react-virtual"
|
|
19
|
+
|
|
20
|
+
import { FilterValue } from "../Filter"
|
|
21
|
+
import { DataTableDisplaySettings } from "./DataTableDisplaySettings"
|
|
22
|
+
import { DataTableFloatingBar } from "./DataTableFloatingBar"
|
|
23
|
+
import { DataTablePagination } from "./DataTablePagination"
|
|
24
|
+
import {
|
|
25
|
+
DataTableEmpty,
|
|
26
|
+
DataTableError,
|
|
27
|
+
DataTableLoading,
|
|
28
|
+
} from "./DataTableStates"
|
|
29
|
+
import { DataTableToolbarContainer } from "./DataTableToolbarContainer"
|
|
30
|
+
import { useDataTableSettings } from "./hooks/use-data-table-settings"
|
|
31
|
+
|
|
32
|
+
function HeaderCell({
|
|
33
|
+
header,
|
|
34
|
+
isPinned,
|
|
35
|
+
}: {
|
|
36
|
+
header: Header<any, any>
|
|
37
|
+
isPinned?: "left" | "right"
|
|
38
|
+
}) {
|
|
39
|
+
const isResizing = header.column.getIsResizing()
|
|
40
|
+
return (
|
|
41
|
+
<th
|
|
42
|
+
key={header.id}
|
|
43
|
+
className={cn(
|
|
44
|
+
"text-gray-11 bg-gray-4 relative h-11 px-4 text-left align-middle font-semibold",
|
|
45
|
+
isPinned === "left" &&
|
|
46
|
+
"sticky left-0 z-20 shadow-[-6px_0_2px_-6px_var(--gray-6)_inset]",
|
|
47
|
+
isPinned === "right" &&
|
|
48
|
+
"sticky right-0 z-20 shadow-[6px_0_2px_-6px_var(--gray-6)_inset]",
|
|
49
|
+
header.column.getCanResize() && "group"
|
|
50
|
+
)}
|
|
51
|
+
style={{
|
|
52
|
+
width: header.getSize(),
|
|
53
|
+
minWidth: header.column.columnDef.minSize,
|
|
54
|
+
maxWidth: header.column.columnDef.maxSize,
|
|
55
|
+
...(isPinned === "left" && {
|
|
56
|
+
left: header.column.getStart("left"),
|
|
57
|
+
}),
|
|
58
|
+
...(isPinned === "right" && {
|
|
59
|
+
right: header.column.getAfter("right"),
|
|
60
|
+
}),
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
{header.isPlaceholder
|
|
64
|
+
? null
|
|
65
|
+
: flexRender(header.column.columnDef.header, header.getContext())}
|
|
66
|
+
{header.column.getCanResize() && (
|
|
67
|
+
<div
|
|
68
|
+
data-slot="data-table-resize-handle"
|
|
69
|
+
onMouseDown={header.getResizeHandler()}
|
|
70
|
+
onTouchStart={header.getResizeHandler()}
|
|
71
|
+
className={cn(
|
|
72
|
+
"group/resize-handle absolute top-0 right-0 z-10 flex h-full w-2 cursor-col-resize touch-none items-center justify-center select-none"
|
|
73
|
+
)}
|
|
74
|
+
>
|
|
75
|
+
<div
|
|
76
|
+
className={cn(
|
|
77
|
+
"bg-gray-7 group-hover/resize-handle:bg-gray-11 h-6 w-px transition-colors duration-150",
|
|
78
|
+
isResizing && "bg-accent-9!"
|
|
79
|
+
)}
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
83
|
+
</th>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function TableCell({
|
|
88
|
+
cell,
|
|
89
|
+
isPinned,
|
|
90
|
+
}: {
|
|
91
|
+
cell: Cell<any, any>
|
|
92
|
+
isPinned?: "left" | "right"
|
|
93
|
+
}) {
|
|
94
|
+
return (
|
|
95
|
+
<td
|
|
96
|
+
key={cell.id}
|
|
97
|
+
title={cell.getValue()}
|
|
98
|
+
className={cn(
|
|
99
|
+
"group-hover:bg-gray-3 truncate px-4 py-1 align-middle text-sm",
|
|
100
|
+
"group-data-[state=selected]:bg-gray-4",
|
|
101
|
+
isPinned === "left" &&
|
|
102
|
+
"bg-gray-2 group-data-[state=selected]:bg-gray-5 sticky left-0 z-10 shadow-[-6px_0_2px_-6px_var(--gray-6)_inset]",
|
|
103
|
+
isPinned === "right" &&
|
|
104
|
+
"bg-gray-2 group-data-[state=selected]:bg-gray-5 sticky right-0 z-10 shadow-[6px_0_2px_-6px_var(--gray-6)_inset]"
|
|
105
|
+
)}
|
|
106
|
+
style={{
|
|
107
|
+
width: cell.column.getSize(),
|
|
108
|
+
minWidth: cell.column.columnDef.minSize,
|
|
109
|
+
maxWidth: cell.column.columnDef.maxSize,
|
|
110
|
+
...(isPinned === "left" && {
|
|
111
|
+
left: cell.column.getStart("left"),
|
|
112
|
+
}),
|
|
113
|
+
...(isPinned === "right" && {
|
|
114
|
+
right: cell.column.getAfter("right"),
|
|
115
|
+
}),
|
|
116
|
+
}}
|
|
117
|
+
>
|
|
118
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
119
|
+
</td>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function TableBody({
|
|
124
|
+
table,
|
|
125
|
+
tableContainerRef,
|
|
126
|
+
estimatedRowHeight,
|
|
127
|
+
}: {
|
|
128
|
+
table: Table<any>
|
|
129
|
+
tableContainerRef: RefObject<HTMLDivElement | null>
|
|
130
|
+
estimatedRowHeight: number
|
|
131
|
+
}) {
|
|
132
|
+
const { rows } = table.getRowModel()
|
|
133
|
+
const rowVirtualizer = useVirtualizer({
|
|
134
|
+
count: rows.length,
|
|
135
|
+
getScrollElement: () => tableContainerRef.current,
|
|
136
|
+
estimateSize: () => estimatedRowHeight,
|
|
137
|
+
overscan: 10,
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
const virtualRows = rowVirtualizer.getVirtualItems()
|
|
141
|
+
const totalSize = rowVirtualizer.getTotalSize()
|
|
142
|
+
|
|
143
|
+
const paddingTop = virtualRows.length > 0 ? virtualRows[0]?.start || 0 : 0
|
|
144
|
+
const paddingBottom =
|
|
145
|
+
virtualRows.length > 0
|
|
146
|
+
? totalSize - (virtualRows[virtualRows.length - 1]?.end || 0)
|
|
147
|
+
: 0
|
|
148
|
+
|
|
149
|
+
const leftPinnedColumns = table.getLeftVisibleLeafColumns()
|
|
150
|
+
const rightPinnedColumns = table.getRightVisibleLeafColumns()
|
|
151
|
+
const centerColumns = table.getCenterVisibleLeafColumns()
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<tbody>
|
|
155
|
+
{paddingTop > 0 && (
|
|
156
|
+
<tr>
|
|
157
|
+
<td style={{ height: `${paddingTop}px` }} />
|
|
158
|
+
</tr>
|
|
159
|
+
)}
|
|
160
|
+
{virtualRows.map((virtualRow) => {
|
|
161
|
+
const row = rows[virtualRow.index]
|
|
162
|
+
|
|
163
|
+
if (!row) return null
|
|
164
|
+
|
|
165
|
+
const isSelected = row.getIsSelected()
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<tr
|
|
169
|
+
key={row.id}
|
|
170
|
+
data-state={isSelected ? "selected" : undefined}
|
|
171
|
+
className={cn(
|
|
172
|
+
"group border-gray-3 h-11 border-b transition-colors"
|
|
173
|
+
)}
|
|
174
|
+
>
|
|
175
|
+
{leftPinnedColumns.map((column) => {
|
|
176
|
+
const cell = row
|
|
177
|
+
.getVisibleCells()
|
|
178
|
+
.find((c) => c.column.id === column.id)
|
|
179
|
+
return cell ? (
|
|
180
|
+
<TableCell key={cell.id} cell={cell} isPinned="left" />
|
|
181
|
+
) : null
|
|
182
|
+
})}
|
|
183
|
+
{centerColumns.map((column) => {
|
|
184
|
+
const cell = row
|
|
185
|
+
.getVisibleCells()
|
|
186
|
+
.find((c) => c.column.id === column.id)
|
|
187
|
+
return cell ? <TableCell key={cell.id} cell={cell} /> : null
|
|
188
|
+
})}
|
|
189
|
+
{rightPinnedColumns.map((column) => {
|
|
190
|
+
const cell = row
|
|
191
|
+
.getVisibleCells()
|
|
192
|
+
.find((c) => c.column.id === column.id)
|
|
193
|
+
return cell ? (
|
|
194
|
+
<TableCell key={cell.id} cell={cell} isPinned="right" />
|
|
195
|
+
) : null
|
|
196
|
+
})}
|
|
197
|
+
</tr>
|
|
198
|
+
)
|
|
199
|
+
})}
|
|
200
|
+
{paddingBottom > 0 && (
|
|
201
|
+
<tr>
|
|
202
|
+
<td style={{ height: `${paddingBottom}px` }} />
|
|
203
|
+
</tr>
|
|
204
|
+
)}
|
|
205
|
+
</tbody>
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const MemoizedTableBody = memo(
|
|
210
|
+
TableBody,
|
|
211
|
+
(prev, next) => prev.table.options.data === next.table.options.data
|
|
212
|
+
) as typeof TableBody
|
|
213
|
+
|
|
214
|
+
export function DataTable<TData extends { id: string | number }>({
|
|
215
|
+
// Core data
|
|
216
|
+
columns,
|
|
217
|
+
data,
|
|
218
|
+
dataSource = "server",
|
|
219
|
+
|
|
220
|
+
// Loading/error states
|
|
221
|
+
isLoading = false,
|
|
222
|
+
isFetching,
|
|
223
|
+
isError = false,
|
|
224
|
+
errorMessage,
|
|
225
|
+
|
|
226
|
+
// Components
|
|
227
|
+
filterBar,
|
|
228
|
+
actionBar,
|
|
229
|
+
|
|
230
|
+
// State for server-side operations
|
|
231
|
+
initialState,
|
|
232
|
+
onStateChange,
|
|
233
|
+
rowCount,
|
|
234
|
+
|
|
235
|
+
// Selection
|
|
236
|
+
enableRowSelection = true,
|
|
237
|
+
|
|
238
|
+
// Display settings
|
|
239
|
+
storageKey = "default-table",
|
|
240
|
+
defaultColumnVisibility = {},
|
|
241
|
+
defaultColumnSizing = {},
|
|
242
|
+
defaultColumnPinning = { left: [], right: [] },
|
|
243
|
+
defaultColumnOrder,
|
|
244
|
+
|
|
245
|
+
// Virtualization
|
|
246
|
+
estimatedRowHeight = 44,
|
|
247
|
+
|
|
248
|
+
// Custom empty state
|
|
249
|
+
emptyStateTitle,
|
|
250
|
+
emptyStateDescription,
|
|
251
|
+
}: DataTableProps<TData>) {
|
|
252
|
+
const rendered = useRef(false)
|
|
253
|
+
const tableContainerRef = useRef<HTMLDivElement>(null)
|
|
254
|
+
|
|
255
|
+
// Calculate default column order from columns if not provided
|
|
256
|
+
const calculatedDefaultOrder = useMemo(() => {
|
|
257
|
+
if (defaultColumnOrder) return defaultColumnOrder
|
|
258
|
+
return [
|
|
259
|
+
"select",
|
|
260
|
+
...(columns
|
|
261
|
+
.map(
|
|
262
|
+
(col) =>
|
|
263
|
+
(col as { accessorKey?: string }).accessorKey ||
|
|
264
|
+
(col as { id?: string }).id
|
|
265
|
+
)
|
|
266
|
+
.filter(Boolean) as string[]),
|
|
267
|
+
]
|
|
268
|
+
}, [columns, defaultColumnOrder])
|
|
269
|
+
|
|
270
|
+
const {
|
|
271
|
+
settings,
|
|
272
|
+
updateColumnVisibility,
|
|
273
|
+
updateColumnSizing,
|
|
274
|
+
updateColumnPinning,
|
|
275
|
+
updateColumnOrder,
|
|
276
|
+
resetToDefaults,
|
|
277
|
+
} = useDataTableSettings({
|
|
278
|
+
storageKey,
|
|
279
|
+
defaultColumnVisibility,
|
|
280
|
+
defaultColumnSizing,
|
|
281
|
+
defaultColumnPinning,
|
|
282
|
+
defaultColumnOrder: calculatedDefaultOrder,
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
// Selection column
|
|
286
|
+
const selectionColumn: ColumnDef<TData> = {
|
|
287
|
+
id: "select",
|
|
288
|
+
size: 48,
|
|
289
|
+
minSize: 48,
|
|
290
|
+
maxSize: 48,
|
|
291
|
+
enableResizing: false,
|
|
292
|
+
enableSorting: false,
|
|
293
|
+
enableHiding: false,
|
|
294
|
+
header: ({ table }) => {
|
|
295
|
+
return (
|
|
296
|
+
<div className="flex items-center justify-center">
|
|
297
|
+
<Checkbox
|
|
298
|
+
isSelected={table.getIsAllPageRowsSelected()}
|
|
299
|
+
isIndeterminate={table.getIsSomePageRowsSelected()}
|
|
300
|
+
onChange={(isSelected) => {
|
|
301
|
+
table.toggleAllPageRowsSelected(isSelected)
|
|
302
|
+
}}
|
|
303
|
+
aria-label="Select all"
|
|
304
|
+
className="translate-y-[2px]"
|
|
305
|
+
/>
|
|
306
|
+
</div>
|
|
307
|
+
)
|
|
308
|
+
},
|
|
309
|
+
cell: ({ row }) => (
|
|
310
|
+
<div className="flex items-center justify-center">
|
|
311
|
+
<Checkbox
|
|
312
|
+
isSelected={row.getIsSelected()}
|
|
313
|
+
isDisabled={!row.getCanSelect()}
|
|
314
|
+
isIndeterminate={row.getIsSomeSelected()}
|
|
315
|
+
onChange={row.getToggleSelectedHandler()}
|
|
316
|
+
aria-label="Select row"
|
|
317
|
+
className="translate-y-[2px]"
|
|
318
|
+
/>
|
|
319
|
+
</div>
|
|
320
|
+
),
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const allColumns = useMemo(() => {
|
|
324
|
+
if (enableRowSelection) {
|
|
325
|
+
return [selectionColumn, ...columns]
|
|
326
|
+
}
|
|
327
|
+
return columns
|
|
328
|
+
}, [columns, enableRowSelection])
|
|
329
|
+
|
|
330
|
+
// Always pin selection column to the left
|
|
331
|
+
const columnPinning = useMemo((): ColumnPinningState => {
|
|
332
|
+
const left = settings.columnPinning.left || []
|
|
333
|
+
const right = settings.columnPinning.right || []
|
|
334
|
+
|
|
335
|
+
// Ensure "select" is always first in left pinned columns when row selection is enabled
|
|
336
|
+
if (enableRowSelection && !left.includes("select")) {
|
|
337
|
+
return { left: ["select", ...left], right }
|
|
338
|
+
}
|
|
339
|
+
return { left, right }
|
|
340
|
+
}, [settings.columnPinning, enableRowSelection])
|
|
341
|
+
|
|
342
|
+
const table = useReactTable({
|
|
343
|
+
data,
|
|
344
|
+
columns: allColumns,
|
|
345
|
+
initialState: {
|
|
346
|
+
pagination: {
|
|
347
|
+
pageSize: 20,
|
|
348
|
+
},
|
|
349
|
+
...initialState,
|
|
350
|
+
},
|
|
351
|
+
rowCount: rowCount,
|
|
352
|
+
state: {
|
|
353
|
+
columnVisibility: settings.columnVisibility,
|
|
354
|
+
columnSizing: settings.columnSizing,
|
|
355
|
+
columnPinning,
|
|
356
|
+
columnOrder:
|
|
357
|
+
settings.columnOrder.length > 0
|
|
358
|
+
? settings.columnOrder
|
|
359
|
+
: calculatedDefaultOrder,
|
|
360
|
+
},
|
|
361
|
+
enableColumnResizing: true,
|
|
362
|
+
columnResizeMode: "onChange",
|
|
363
|
+
getCoreRowModel: getCoreRowModel(),
|
|
364
|
+
getSortedRowModel: getSortedRowModel(),
|
|
365
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
366
|
+
getFilteredRowModel: getFilteredRowModel(),
|
|
367
|
+
globalFilterFn: "auto",
|
|
368
|
+
onColumnVisibilityChange: (updater) => {
|
|
369
|
+
const newVisibility =
|
|
370
|
+
typeof updater === "function"
|
|
371
|
+
? updater(settings.columnVisibility)
|
|
372
|
+
: updater
|
|
373
|
+
updateColumnVisibility(newVisibility)
|
|
374
|
+
},
|
|
375
|
+
onColumnSizingChange: (updater) => {
|
|
376
|
+
const newSizing =
|
|
377
|
+
typeof updater === "function" ? updater(settings.columnSizing) : updater
|
|
378
|
+
updateColumnSizing(newSizing)
|
|
379
|
+
},
|
|
380
|
+
onColumnPinningChange: (updater) => {
|
|
381
|
+
const newPinning =
|
|
382
|
+
typeof updater === "function"
|
|
383
|
+
? updater(settings.columnPinning)
|
|
384
|
+
: updater
|
|
385
|
+
updateColumnPinning(newPinning)
|
|
386
|
+
},
|
|
387
|
+
onColumnOrderChange: (updater) => {
|
|
388
|
+
const newOrder =
|
|
389
|
+
typeof updater === "function" ? updater(settings.columnOrder) : updater
|
|
390
|
+
updateColumnOrder(newOrder)
|
|
391
|
+
},
|
|
392
|
+
getRowId: (row) => String(row.id),
|
|
393
|
+
manualPagination: dataSource === "server",
|
|
394
|
+
manualFiltering: dataSource === "server",
|
|
395
|
+
manualSorting: dataSource === "server",
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
const { rows } = table.getRowModel()
|
|
399
|
+
|
|
400
|
+
// Get pinned and center columns
|
|
401
|
+
const leftPinnedColumns = table.getLeftVisibleLeafColumns()
|
|
402
|
+
const rightPinnedColumns = table.getRightVisibleLeafColumns()
|
|
403
|
+
const centerColumns = table.getCenterVisibleLeafColumns()
|
|
404
|
+
|
|
405
|
+
const state = table.getState()
|
|
406
|
+
|
|
407
|
+
// column filters props (client side)
|
|
408
|
+
const columnFilters: FilterValue = useMemo(
|
|
409
|
+
() =>
|
|
410
|
+
state.columnFilters.reduce((acc, filter) => {
|
|
411
|
+
try {
|
|
412
|
+
acc[filter.id] = filter.value as any
|
|
413
|
+
return acc
|
|
414
|
+
} catch (error) {
|
|
415
|
+
return acc
|
|
416
|
+
}
|
|
417
|
+
}, {} as FilterValue),
|
|
418
|
+
[state.columnFilters]
|
|
419
|
+
)
|
|
420
|
+
const setColumnFilters = (filters: FilterValue) => {
|
|
421
|
+
table.setColumnFilters(
|
|
422
|
+
Object.entries(filters).map(([id, value]) => ({ id, value }))
|
|
423
|
+
)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Notify parent of filters change
|
|
427
|
+
useEffect(() => {
|
|
428
|
+
onStateChange?.(state)
|
|
429
|
+
|
|
430
|
+
setTimeout(() => {
|
|
431
|
+
rendered.current = true
|
|
432
|
+
}, 200)
|
|
433
|
+
|
|
434
|
+
if (rendered.current) {
|
|
435
|
+
table.setPageIndex(0)
|
|
436
|
+
}
|
|
437
|
+
}, [
|
|
438
|
+
state.pagination.pageSize,
|
|
439
|
+
state.sorting,
|
|
440
|
+
state.globalFilter,
|
|
441
|
+
state.columnFilters,
|
|
442
|
+
])
|
|
443
|
+
|
|
444
|
+
// Notify parent of page index change
|
|
445
|
+
useEffect(() => {
|
|
446
|
+
onStateChange?.(state)
|
|
447
|
+
}, [state.pagination.pageIndex])
|
|
448
|
+
|
|
449
|
+
const clearSelection = () => {
|
|
450
|
+
table.resetRowSelection()
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return (
|
|
454
|
+
<div className="flex h-full flex-col overflow-hidden">
|
|
455
|
+
{/* Toolbar */}
|
|
456
|
+
<DataTableToolbarContainer table={table}>
|
|
457
|
+
<div>
|
|
458
|
+
{filterBar?.({ value: columnFilters, onChange: setColumnFilters })}
|
|
459
|
+
</div>
|
|
460
|
+
<DataTableDisplaySettings
|
|
461
|
+
table={table}
|
|
462
|
+
onResetToDefaults={resetToDefaults}
|
|
463
|
+
/>
|
|
464
|
+
</DataTableToolbarContainer>
|
|
465
|
+
|
|
466
|
+
{/* Table Container */}
|
|
467
|
+
<div ref={tableContainerRef} className="relative flex-1 overflow-auto">
|
|
468
|
+
<table
|
|
469
|
+
data-fetching={isFetching ? "true" : undefined}
|
|
470
|
+
className="w-full table-fixed border-collapse data-[fetching=true]:opacity-60"
|
|
471
|
+
>
|
|
472
|
+
<thead className="sticky top-0 z-30">
|
|
473
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
|
474
|
+
<tr key={headerGroup.id}>
|
|
475
|
+
{leftPinnedColumns.map((column) => {
|
|
476
|
+
const header = headerGroup.headers.find(
|
|
477
|
+
(h) => h.column.id === column.id
|
|
478
|
+
)
|
|
479
|
+
return header ? (
|
|
480
|
+
<HeaderCell
|
|
481
|
+
key={header.id}
|
|
482
|
+
header={header}
|
|
483
|
+
isPinned="left"
|
|
484
|
+
/>
|
|
485
|
+
) : null
|
|
486
|
+
})}
|
|
487
|
+
{centerColumns.map((column) => {
|
|
488
|
+
const header = headerGroup.headers.find(
|
|
489
|
+
(h) => h.column.id === column.id
|
|
490
|
+
)
|
|
491
|
+
return header ? (
|
|
492
|
+
<HeaderCell key={header.id} header={header} />
|
|
493
|
+
) : null
|
|
494
|
+
})}
|
|
495
|
+
{rightPinnedColumns.map((column) => {
|
|
496
|
+
const header = headerGroup.headers.find(
|
|
497
|
+
(h) => h.column.id === column.id
|
|
498
|
+
)
|
|
499
|
+
return header ? (
|
|
500
|
+
<HeaderCell
|
|
501
|
+
key={header.id}
|
|
502
|
+
header={header}
|
|
503
|
+
isPinned="right"
|
|
504
|
+
/>
|
|
505
|
+
) : null
|
|
506
|
+
})}
|
|
507
|
+
</tr>
|
|
508
|
+
))}
|
|
509
|
+
</thead>
|
|
510
|
+
{isLoading ? (
|
|
511
|
+
<DataTableLoading />
|
|
512
|
+
) : isError ? (
|
|
513
|
+
<DataTableError message={errorMessage} />
|
|
514
|
+
) : rows.length === 0 ? (
|
|
515
|
+
<DataTableEmpty
|
|
516
|
+
title={emptyStateTitle}
|
|
517
|
+
description={emptyStateDescription}
|
|
518
|
+
/>
|
|
519
|
+
) : (
|
|
520
|
+
<>
|
|
521
|
+
{table.getState().columnSizingInfo.isResizingColumn ? (
|
|
522
|
+
<MemoizedTableBody
|
|
523
|
+
table={table}
|
|
524
|
+
tableContainerRef={tableContainerRef}
|
|
525
|
+
estimatedRowHeight={estimatedRowHeight}
|
|
526
|
+
/>
|
|
527
|
+
) : (
|
|
528
|
+
<TableBody
|
|
529
|
+
table={table}
|
|
530
|
+
tableContainerRef={tableContainerRef}
|
|
531
|
+
estimatedRowHeight={estimatedRowHeight}
|
|
532
|
+
/>
|
|
533
|
+
)}
|
|
534
|
+
</>
|
|
535
|
+
)}
|
|
536
|
+
</table>
|
|
537
|
+
</div>
|
|
538
|
+
|
|
539
|
+
<DataTablePagination
|
|
540
|
+
pageIndex={table.getState().pagination.pageIndex}
|
|
541
|
+
pageSize={table.getState().pagination.pageSize}
|
|
542
|
+
totalRows={table.getRowCount()}
|
|
543
|
+
onPageChange={table.setPageIndex}
|
|
544
|
+
onPageSizeChange={table.setPageSize}
|
|
545
|
+
/>
|
|
546
|
+
|
|
547
|
+
{actionBar && enableRowSelection && (
|
|
548
|
+
<DataTableFloatingBar
|
|
549
|
+
selectedCount={Object.keys(table.getState().rowSelection).length}
|
|
550
|
+
onClearSelection={clearSelection}
|
|
551
|
+
>
|
|
552
|
+
{actionBar?.({ table, clearSelection })}
|
|
553
|
+
</DataTableFloatingBar>
|
|
554
|
+
)}
|
|
555
|
+
</div>
|
|
556
|
+
)
|
|
557
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { Button } from "@eggspot/ui/components/Button"
|
|
2
|
+
import { cn } from "@eggspot/ui/lib/utils"
|
|
3
|
+
import { Column } from "@tanstack/react-table"
|
|
4
|
+
import {
|
|
5
|
+
ArrowDown,
|
|
6
|
+
ArrowUp,
|
|
7
|
+
ArrowUpDown,
|
|
8
|
+
EyeOff,
|
|
9
|
+
Pin,
|
|
10
|
+
PinOff,
|
|
11
|
+
} from "lucide-react"
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
Menu,
|
|
15
|
+
MenuItem,
|
|
16
|
+
MenuPopover,
|
|
17
|
+
MenuSeparator,
|
|
18
|
+
MenuTrigger,
|
|
19
|
+
} from "../Menu"
|
|
20
|
+
|
|
21
|
+
interface DataTableColumnHeaderProps<TData, TValue>
|
|
22
|
+
extends React.HTMLAttributes<HTMLDivElement> {
|
|
23
|
+
column: Column<TData, TValue>
|
|
24
|
+
title: string
|
|
25
|
+
enableSorting?: boolean
|
|
26
|
+
enablePinning?: boolean
|
|
27
|
+
enableHiding?: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function DataTableColumnHeader<TData, TValue>({
|
|
31
|
+
column,
|
|
32
|
+
title,
|
|
33
|
+
className,
|
|
34
|
+
enableSorting = true,
|
|
35
|
+
enablePinning = true,
|
|
36
|
+
enableHiding = true,
|
|
37
|
+
}: DataTableColumnHeaderProps<TData, TValue>) {
|
|
38
|
+
const isSorted = column.getIsSorted()
|
|
39
|
+
const isPinned = column.getIsPinned()
|
|
40
|
+
|
|
41
|
+
if (!enableSorting && !enablePinning && !enableHiding) {
|
|
42
|
+
return <div className={cn("flex items-center", className)}>{title}</div>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className={cn("flex items-center gap-1", className)}>
|
|
47
|
+
<MenuTrigger>
|
|
48
|
+
<Button
|
|
49
|
+
variant="ghost"
|
|
50
|
+
size="sm"
|
|
51
|
+
className={cn(
|
|
52
|
+
"text-gray-11 hover:bg-gray-5! hover:text-gray-12 aria-expanded:bg-gray-5! aria-expanded:text-gray-12 -ml-2 h-8 transition-colors",
|
|
53
|
+
isSorted && "text-gray-12 font-semibold"
|
|
54
|
+
)}
|
|
55
|
+
rightIcon={
|
|
56
|
+
isSorted === "desc" ? (
|
|
57
|
+
<ArrowDown />
|
|
58
|
+
) : isSorted === "asc" ? (
|
|
59
|
+
<ArrowUp />
|
|
60
|
+
) : (
|
|
61
|
+
<ArrowUpDown />
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
>
|
|
65
|
+
{title}
|
|
66
|
+
</Button>
|
|
67
|
+
<MenuPopover>
|
|
68
|
+
<Menu>
|
|
69
|
+
{enableSorting && (
|
|
70
|
+
<>
|
|
71
|
+
<MenuItem onClick={() => column.toggleSorting(false)}>
|
|
72
|
+
<ArrowUp className="text-gray-11 mr-2 h-3.5 w-3.5" />
|
|
73
|
+
Sort ascending
|
|
74
|
+
</MenuItem>
|
|
75
|
+
<MenuItem onClick={() => column.toggleSorting(true)}>
|
|
76
|
+
<ArrowDown className="text-gray-11 mr-2 h-3.5 w-3.5" />
|
|
77
|
+
Sort descending
|
|
78
|
+
</MenuItem>
|
|
79
|
+
{isSorted && (
|
|
80
|
+
<MenuItem onClick={() => column.clearSorting()}>
|
|
81
|
+
<ArrowUpDown className="text-gray-11 mr-2 h-3.5 w-3.5" />
|
|
82
|
+
Clear sorting
|
|
83
|
+
</MenuItem>
|
|
84
|
+
)}
|
|
85
|
+
{(enablePinning || enableHiding) && <MenuSeparator />}
|
|
86
|
+
</>
|
|
87
|
+
)}
|
|
88
|
+
{enablePinning && (
|
|
89
|
+
<>
|
|
90
|
+
{isPinned !== "left" && (
|
|
91
|
+
<MenuItem onClick={() => column.pin("left")}>
|
|
92
|
+
<Pin className="text-gray-11 mr-2 h-3.5 w-3.5 rotate-45" />
|
|
93
|
+
Pin to left
|
|
94
|
+
</MenuItem>
|
|
95
|
+
)}
|
|
96
|
+
{isPinned !== "right" && (
|
|
97
|
+
<MenuItem onClick={() => column.pin("right")}>
|
|
98
|
+
<Pin className="text-gray-11 mr-2 h-3.5 w-3.5 -rotate-45" />
|
|
99
|
+
Pin to right
|
|
100
|
+
</MenuItem>
|
|
101
|
+
)}
|
|
102
|
+
{isPinned && (
|
|
103
|
+
<MenuItem onClick={() => column.pin(false)}>
|
|
104
|
+
<PinOff className="text-gray-11 mr-2 h-3.5 w-3.5" />
|
|
105
|
+
Unpin column
|
|
106
|
+
</MenuItem>
|
|
107
|
+
)}
|
|
108
|
+
{enableHiding && <MenuSeparator />}
|
|
109
|
+
</>
|
|
110
|
+
)}
|
|
111
|
+
{enableHiding && (
|
|
112
|
+
<MenuItem onClick={() => column.toggleVisibility(false)}>
|
|
113
|
+
<EyeOff className="text-gray-11 mr-2 h-3.5 w-3.5" />
|
|
114
|
+
Hide column
|
|
115
|
+
</MenuItem>
|
|
116
|
+
)}
|
|
117
|
+
</Menu>
|
|
118
|
+
</MenuPopover>
|
|
119
|
+
</MenuTrigger>
|
|
120
|
+
</div>
|
|
121
|
+
)
|
|
122
|
+
}
|