datool 0.0.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/README.md +218 -0
- package/client-dist/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
- package/client-dist/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
- package/client-dist/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
- package/client-dist/assets/index-BeRNeRUq.css +1 -0
- package/client-dist/assets/index-uoZ4c_I8.js +164 -0
- package/client-dist/index.html +13 -0
- package/index.html +12 -0
- package/package.json +55 -0
- package/src/client/App.tsx +885 -0
- package/src/client/components/connection-status.tsx +43 -0
- package/src/client/components/data-table-cell.tsx +235 -0
- package/src/client/components/data-table-col-icon.tsx +73 -0
- package/src/client/components/data-table-header-col.tsx +225 -0
- package/src/client/components/data-table-search-input.tsx +729 -0
- package/src/client/components/data-table.tsx +2014 -0
- package/src/client/components/stream-controls.tsx +157 -0
- package/src/client/components/theme-provider.tsx +230 -0
- package/src/client/components/ui/button.tsx +68 -0
- package/src/client/components/ui/combobox.tsx +308 -0
- package/src/client/components/ui/context-menu.tsx +261 -0
- package/src/client/components/ui/dropdown-menu.tsx +267 -0
- package/src/client/components/ui/input-group.tsx +153 -0
- package/src/client/components/ui/input.tsx +19 -0
- package/src/client/components/ui/textarea.tsx +18 -0
- package/src/client/components/viewer-settings.tsx +185 -0
- package/src/client/index.css +192 -0
- package/src/client/lib/data-table-search.ts +750 -0
- package/src/client/lib/datool-icons.ts +37 -0
- package/src/client/lib/datool-url-state.ts +159 -0
- package/src/client/lib/filterable-table.ts +146 -0
- package/src/client/lib/table-search-persistence.ts +94 -0
- package/src/client/lib/utils.ts +6 -0
- package/src/client/main.tsx +14 -0
- package/src/index.ts +19 -0
- package/src/node/cli.ts +54 -0
- package/src/node/config.ts +231 -0
- package/src/node/lines.ts +82 -0
- package/src/node/runtime.ts +102 -0
- package/src/node/server.ts +403 -0
- package/src/node/sources/command.ts +82 -0
- package/src/node/sources/file.ts +116 -0
- package/src/node/sources/ssh.ts +59 -0
- package/src/shared/columns.ts +41 -0
- package/src/shared/types.ts +188 -0
|
@@ -0,0 +1,2014 @@
|
|
|
1
|
+
/* eslint-disable react-hooks/incompatible-library, react-refresh/only-export-components */
|
|
2
|
+
import {
|
|
3
|
+
functionalUpdate,
|
|
4
|
+
getCoreRowModel,
|
|
5
|
+
getFilteredRowModel,
|
|
6
|
+
getSortedRowModel,
|
|
7
|
+
useReactTable,
|
|
8
|
+
type ColumnDef,
|
|
9
|
+
type ColumnFiltersState,
|
|
10
|
+
type FilterFn,
|
|
11
|
+
type OnChangeFn,
|
|
12
|
+
type Row,
|
|
13
|
+
type RowData,
|
|
14
|
+
type RowSelectionState,
|
|
15
|
+
type SortingState,
|
|
16
|
+
type ColumnSizingState,
|
|
17
|
+
type VisibilityState,
|
|
18
|
+
} from "@tanstack/react-table"
|
|
19
|
+
import { useVirtualizer } from "@tanstack/react-virtual"
|
|
20
|
+
import {
|
|
21
|
+
Check,
|
|
22
|
+
CircleAlert,
|
|
23
|
+
EyeOff,
|
|
24
|
+
LayoutGrid,
|
|
25
|
+
LoaderCircle,
|
|
26
|
+
Search,
|
|
27
|
+
SlidersHorizontal,
|
|
28
|
+
Sparkles,
|
|
29
|
+
X,
|
|
30
|
+
} from "lucide-react"
|
|
31
|
+
import * as React from "react"
|
|
32
|
+
import { useDeferredValue } from "react"
|
|
33
|
+
|
|
34
|
+
import {
|
|
35
|
+
DataTableBodyCell,
|
|
36
|
+
DataTableCheckbox,
|
|
37
|
+
fallbackCellValue,
|
|
38
|
+
} from "./data-table-cell"
|
|
39
|
+
import {
|
|
40
|
+
DataTableHeaderCol,
|
|
41
|
+
type DataTableAlign,
|
|
42
|
+
type DataTableColumnMeta,
|
|
43
|
+
} from "./data-table-header-col"
|
|
44
|
+
import {
|
|
45
|
+
DataTableColIcon,
|
|
46
|
+
inferDataTableColumnKind,
|
|
47
|
+
type DataTableColumnKind,
|
|
48
|
+
} from "./data-table-col-icon"
|
|
49
|
+
import { Button } from "@/components/ui/button"
|
|
50
|
+
import {
|
|
51
|
+
ContextMenu,
|
|
52
|
+
ContextMenuContent,
|
|
53
|
+
ContextMenuItem,
|
|
54
|
+
ContextMenuLabel,
|
|
55
|
+
ContextMenuSeparator,
|
|
56
|
+
ContextMenuSub,
|
|
57
|
+
ContextMenuSubContent,
|
|
58
|
+
ContextMenuSubTrigger,
|
|
59
|
+
ContextMenuShortcut,
|
|
60
|
+
ContextMenuTrigger,
|
|
61
|
+
} from "@/components/ui/context-menu"
|
|
62
|
+
import {
|
|
63
|
+
getColumnHighlightTerms,
|
|
64
|
+
parseSearchQuery,
|
|
65
|
+
type DataTableSearchField,
|
|
66
|
+
} from "@/lib/data-table-search"
|
|
67
|
+
import {
|
|
68
|
+
buildTableSearchFields,
|
|
69
|
+
withColumnSearchFilters,
|
|
70
|
+
} from "@/lib/filterable-table"
|
|
71
|
+
import {
|
|
72
|
+
readPersistedSearch,
|
|
73
|
+
type SearchStatePersistence,
|
|
74
|
+
writePersistedSearch,
|
|
75
|
+
} from "@/lib/table-search-persistence"
|
|
76
|
+
import { cn } from "@/lib/utils"
|
|
77
|
+
|
|
78
|
+
type DataTableRow = Record<string, unknown>
|
|
79
|
+
|
|
80
|
+
export type DataTableRowActionScope = "row" | "selection"
|
|
81
|
+
|
|
82
|
+
export type DataTableRowActionButtonVariant =
|
|
83
|
+
| "default"
|
|
84
|
+
| "outline"
|
|
85
|
+
| "secondary"
|
|
86
|
+
| "ghost"
|
|
87
|
+
| "destructive"
|
|
88
|
+
| "link"
|
|
89
|
+
|
|
90
|
+
export type DataTableRowActionButtonSize =
|
|
91
|
+
| "default"
|
|
92
|
+
| "xs"
|
|
93
|
+
| "sm"
|
|
94
|
+
| "lg"
|
|
95
|
+
| "xl"
|
|
96
|
+
| "icon"
|
|
97
|
+
| "icon-xs"
|
|
98
|
+
| "icon-sm"
|
|
99
|
+
| "icon-lg"
|
|
100
|
+
| "icon-xl"
|
|
101
|
+
|
|
102
|
+
export type DataTableRowActionButtonConfig =
|
|
103
|
+
| false
|
|
104
|
+
| DataTableRowActionButtonVariant
|
|
105
|
+
| {
|
|
106
|
+
className?: string
|
|
107
|
+
label?: string
|
|
108
|
+
size?: DataTableRowActionButtonSize
|
|
109
|
+
variant?: DataTableRowActionButtonVariant
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export type DataTableRowActionContext<TData extends DataTableRow> = {
|
|
113
|
+
actionRowIds: string[]
|
|
114
|
+
actionRows: TData[]
|
|
115
|
+
anchorRow: TData
|
|
116
|
+
anchorRowId: string
|
|
117
|
+
selectedRowIds: string[]
|
|
118
|
+
selectedRows: TData[]
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
type DataTableRowActionChildren<TData extends DataTableRow> =
|
|
122
|
+
| DataTableRowAction<TData>[]
|
|
123
|
+
| ((context: DataTableRowActionContext<TData>) => DataTableRowAction<TData>[])
|
|
124
|
+
|
|
125
|
+
export type DataTableRowAction<TData extends DataTableRow> = {
|
|
126
|
+
button?: DataTableRowActionButtonConfig
|
|
127
|
+
disabled?: boolean | ((context: DataTableRowActionContext<TData>) => boolean)
|
|
128
|
+
hidden?: boolean | ((context: DataTableRowActionContext<TData>) => boolean)
|
|
129
|
+
icon?: React.ComponentType<{ className?: string }>
|
|
130
|
+
id: string
|
|
131
|
+
items?: DataTableRowActionChildren<TData>
|
|
132
|
+
label: string | ((context: DataTableRowActionContext<TData>) => string)
|
|
133
|
+
onSelect?: (
|
|
134
|
+
context: DataTableRowActionContext<TData>
|
|
135
|
+
) => Promise<unknown> | unknown
|
|
136
|
+
scope?: DataTableRowActionScope
|
|
137
|
+
shortcut?: string
|
|
138
|
+
variant?: "default" | "destructive"
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export type DataTableColumnConfig<TData extends DataTableRow> = {
|
|
142
|
+
accessorFn?: (row: TData) => unknown
|
|
143
|
+
accessorKey?: Extract<keyof TData, string>
|
|
144
|
+
align?: DataTableAlign
|
|
145
|
+
cell?: (args: { row: TData; value: unknown }) => React.ReactNode
|
|
146
|
+
cellClassName?: string
|
|
147
|
+
enableFiltering?: boolean
|
|
148
|
+
enableHiding?: boolean
|
|
149
|
+
enableResizing?: boolean
|
|
150
|
+
enableSorting?: boolean
|
|
151
|
+
filterFn?: FilterFn<TData>
|
|
152
|
+
header?: string
|
|
153
|
+
headerClassName?: string
|
|
154
|
+
highlightMatches?: boolean
|
|
155
|
+
id?: string
|
|
156
|
+
kind?: DataTableColumnKind
|
|
157
|
+
maxWidth?: number
|
|
158
|
+
minWidth?: number
|
|
159
|
+
truncate?: boolean
|
|
160
|
+
width?: number
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export type DataTableProps<TData extends DataTableRow> = {
|
|
164
|
+
autoScrollToBottom?: boolean
|
|
165
|
+
autoScrollToBottomThreshold?: number
|
|
166
|
+
columnFilters?: ColumnFiltersState
|
|
167
|
+
columnVisibility?: VisibilityState
|
|
168
|
+
columns?: DataTableColumnConfig<TData>[]
|
|
169
|
+
data: TData[]
|
|
170
|
+
edgeHorizontalPadding?: React.CSSProperties["paddingLeft"]
|
|
171
|
+
enableRowSelection?: boolean
|
|
172
|
+
filterPlaceholder?: string
|
|
173
|
+
globalFilter?: string
|
|
174
|
+
getRowId?: (row: TData, index: number) => string
|
|
175
|
+
height?: React.CSSProperties["height"]
|
|
176
|
+
highlightQuery?: string
|
|
177
|
+
id: string
|
|
178
|
+
onColumnFiltersChange?: (value: ColumnFiltersState) => void
|
|
179
|
+
onColumnVisibilityChange?: (value: VisibilityState) => void
|
|
180
|
+
onGlobalFilterChange?: (value: string) => void
|
|
181
|
+
resolveColumnHighlightTerms?: (columnId: string, query: string) => string[]
|
|
182
|
+
rowActions?: DataTableRowAction<TData>[]
|
|
183
|
+
rowClassName?: (row: TData) => string | undefined
|
|
184
|
+
rowHeight?: number
|
|
185
|
+
statePersistence?: "localStorage" | "none" | "url"
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export type DataTableProviderProps<TData extends DataTableRow> = Omit<
|
|
189
|
+
DataTableProps<TData>,
|
|
190
|
+
| "columnFilters"
|
|
191
|
+
| "columns"
|
|
192
|
+
| "data"
|
|
193
|
+
| "globalFilter"
|
|
194
|
+
| "highlightQuery"
|
|
195
|
+
| "id"
|
|
196
|
+
| "onColumnFiltersChange"
|
|
197
|
+
| "onGlobalFilterChange"
|
|
198
|
+
| "resolveColumnHighlightTerms"
|
|
199
|
+
| "statePersistence"
|
|
200
|
+
> & {
|
|
201
|
+
children: React.ReactNode
|
|
202
|
+
columns: DataTableColumnConfig<TData>[]
|
|
203
|
+
data: TData[]
|
|
204
|
+
fieldOptions?: Partial<Record<string, string[]>>
|
|
205
|
+
id: string
|
|
206
|
+
onSearchChange?: (value: string) => void
|
|
207
|
+
persistSearch?: boolean
|
|
208
|
+
search?: string
|
|
209
|
+
searchPersistence?: SearchStatePersistence | "none"
|
|
210
|
+
statePersistence?: DataTableProps<TData>["statePersistence"]
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
type DataTableContextValue<TData extends DataTableRow> = {
|
|
214
|
+
search: string
|
|
215
|
+
searchFields: DataTableSearchField<TData>[]
|
|
216
|
+
setSearch: (value: string) => void
|
|
217
|
+
tableProps: DataTableProps<TData>
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
type PersistedTableState = {
|
|
221
|
+
columnFilters?: ColumnFiltersState
|
|
222
|
+
columnSizing?: ColumnSizingState
|
|
223
|
+
highlightedColumns?: Record<string, boolean>
|
|
224
|
+
columnVisibility?: VisibilityState
|
|
225
|
+
globalFilter?: string
|
|
226
|
+
sorting?: SortingState
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const LOCAL_STORAGE_PREFIX = "datatable:"
|
|
230
|
+
const URL_PARAM_PREFIX = "datatable-"
|
|
231
|
+
const DataTableContext =
|
|
232
|
+
React.createContext<DataTableContextValue<DataTableRow> | null>(null)
|
|
233
|
+
|
|
234
|
+
export function useOptionalDataTableContext<TData extends DataTableRow>() {
|
|
235
|
+
return React.useContext(
|
|
236
|
+
DataTableContext
|
|
237
|
+
) as DataTableContextValue<TData> | null
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function useDataTableContext<TData extends DataTableRow>() {
|
|
241
|
+
const context = useOptionalDataTableContext<TData>()
|
|
242
|
+
|
|
243
|
+
if (!context) {
|
|
244
|
+
throw new Error(
|
|
245
|
+
"useDataTableContext must be used inside DataTableProvider."
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return context
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function DataTableProvider<TData extends DataTableRow>({
|
|
253
|
+
children,
|
|
254
|
+
columns,
|
|
255
|
+
data,
|
|
256
|
+
fieldOptions,
|
|
257
|
+
id,
|
|
258
|
+
onSearchChange,
|
|
259
|
+
persistSearch = false,
|
|
260
|
+
search: controlledSearch,
|
|
261
|
+
searchPersistence,
|
|
262
|
+
statePersistence,
|
|
263
|
+
...tableProps
|
|
264
|
+
}: DataTableProviderProps<TData>) {
|
|
265
|
+
const resolvedSearchPersistence =
|
|
266
|
+
searchPersistence ?? (persistSearch ? "localStorage" : "none")
|
|
267
|
+
const isSearchControlled = controlledSearch !== undefined
|
|
268
|
+
const [search, setSearch] = React.useState(() =>
|
|
269
|
+
resolvedSearchPersistence === "none"
|
|
270
|
+
? ""
|
|
271
|
+
: readPersistedSearch(id, resolvedSearchPersistence)
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
const resolvedSearch = controlledSearch ?? search
|
|
275
|
+
const handleSearchChange = React.useCallback(
|
|
276
|
+
(value: string) => {
|
|
277
|
+
if (!isSearchControlled) {
|
|
278
|
+
setSearch(value)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
onSearchChange?.(value)
|
|
282
|
+
},
|
|
283
|
+
[isSearchControlled, onSearchChange]
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
React.useEffect(() => {
|
|
287
|
+
if (isSearchControlled || resolvedSearchPersistence === "none") {
|
|
288
|
+
setSearch("")
|
|
289
|
+
return
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
setSearch(readPersistedSearch(id, resolvedSearchPersistence))
|
|
293
|
+
}, [id, isSearchControlled, resolvedSearchPersistence])
|
|
294
|
+
|
|
295
|
+
React.useEffect(() => {
|
|
296
|
+
if (isSearchControlled || resolvedSearchPersistence === "none") {
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
writePersistedSearch(id, resolvedSearchPersistence, search)
|
|
301
|
+
}, [id, isSearchControlled, resolvedSearchPersistence, search])
|
|
302
|
+
|
|
303
|
+
const searchFields = React.useMemo(
|
|
304
|
+
() =>
|
|
305
|
+
buildTableSearchFields(columns, data, {
|
|
306
|
+
fieldOptions,
|
|
307
|
+
}),
|
|
308
|
+
[columns, data, fieldOptions]
|
|
309
|
+
)
|
|
310
|
+
const parsedSearch = React.useMemo(
|
|
311
|
+
() => parseSearchQuery(resolvedSearch, searchFields),
|
|
312
|
+
[resolvedSearch, searchFields]
|
|
313
|
+
)
|
|
314
|
+
const resolvedColumns = React.useMemo(
|
|
315
|
+
() => withColumnSearchFilters(columns, searchFields),
|
|
316
|
+
[columns, searchFields]
|
|
317
|
+
)
|
|
318
|
+
const resolvedTableProps = React.useMemo(
|
|
319
|
+
() =>
|
|
320
|
+
({
|
|
321
|
+
...tableProps,
|
|
322
|
+
columnFilters: parsedSearch.columnFilters,
|
|
323
|
+
columns: resolvedColumns,
|
|
324
|
+
data,
|
|
325
|
+
globalFilter: parsedSearch.globalFilter,
|
|
326
|
+
highlightQuery: resolvedSearch,
|
|
327
|
+
id,
|
|
328
|
+
resolveColumnHighlightTerms: (columnId: string, query: string) =>
|
|
329
|
+
getColumnHighlightTerms(query, columnId, searchFields),
|
|
330
|
+
statePersistence:
|
|
331
|
+
statePersistence ??
|
|
332
|
+
(resolvedSearchPersistence === "none"
|
|
333
|
+
? "none"
|
|
334
|
+
: resolvedSearchPersistence),
|
|
335
|
+
}) satisfies DataTableProps<TData>,
|
|
336
|
+
[
|
|
337
|
+
data,
|
|
338
|
+
id,
|
|
339
|
+
parsedSearch.columnFilters,
|
|
340
|
+
parsedSearch.globalFilter,
|
|
341
|
+
resolvedColumns,
|
|
342
|
+
resolvedSearchPersistence,
|
|
343
|
+
resolvedSearch,
|
|
344
|
+
searchFields,
|
|
345
|
+
statePersistence,
|
|
346
|
+
tableProps,
|
|
347
|
+
]
|
|
348
|
+
)
|
|
349
|
+
const contextValue = React.useMemo(
|
|
350
|
+
() =>
|
|
351
|
+
({
|
|
352
|
+
search: resolvedSearch,
|
|
353
|
+
searchFields,
|
|
354
|
+
setSearch: handleSearchChange,
|
|
355
|
+
tableProps: resolvedTableProps,
|
|
356
|
+
}) satisfies DataTableContextValue<TData>,
|
|
357
|
+
[handleSearchChange, resolvedSearch, resolvedTableProps, searchFields]
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
return (
|
|
361
|
+
<DataTableContext.Provider
|
|
362
|
+
value={contextValue as DataTableContextValue<DataTableRow>}
|
|
363
|
+
>
|
|
364
|
+
{children}
|
|
365
|
+
</DataTableContext.Provider>
|
|
366
|
+
)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function formatHeaderLabel(key: string) {
|
|
370
|
+
return key
|
|
371
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
372
|
+
.replace(/[_-]+/g, " ")
|
|
373
|
+
.replace(/\s+/g, " ")
|
|
374
|
+
.trim()
|
|
375
|
+
.replace(/\b\w/g, (match) => match.toUpperCase())
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function stringifyFilterValue(value: unknown) {
|
|
379
|
+
if (value === null || value === undefined) {
|
|
380
|
+
return ""
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (value instanceof Date) {
|
|
384
|
+
return value.toISOString()
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (typeof value === "object") {
|
|
388
|
+
return JSON.stringify(value)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return String(value)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function getPersistedUrlParam(id: string) {
|
|
395
|
+
return `${URL_PARAM_PREFIX}${id}`
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function isPersistedStateEmpty(state: PersistedTableState) {
|
|
399
|
+
return (
|
|
400
|
+
(state.sorting?.length ?? 0) === 0 &&
|
|
401
|
+
(state.columnFilters?.length ?? 0) === 0 &&
|
|
402
|
+
Object.keys(state.highlightedColumns ?? {}).length === 0 &&
|
|
403
|
+
Object.keys(state.columnVisibility ?? {}).length === 0 &&
|
|
404
|
+
Object.keys(state.columnSizing ?? {}).length === 0 &&
|
|
405
|
+
!state.globalFilter
|
|
406
|
+
)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function readPersistedState(
|
|
410
|
+
id: string,
|
|
411
|
+
statePersistence: DataTableProps<DataTableRow>["statePersistence"]
|
|
412
|
+
): PersistedTableState {
|
|
413
|
+
if (typeof window === "undefined") {
|
|
414
|
+
return {}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
if (statePersistence === "none") {
|
|
419
|
+
return {}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const rawValue =
|
|
423
|
+
statePersistence === "url"
|
|
424
|
+
? new URL(window.location.href).searchParams.get(
|
|
425
|
+
getPersistedUrlParam(id)
|
|
426
|
+
)
|
|
427
|
+
: window.localStorage.getItem(`${LOCAL_STORAGE_PREFIX}${id}`)
|
|
428
|
+
|
|
429
|
+
return rawValue ? (JSON.parse(rawValue) as PersistedTableState) : {}
|
|
430
|
+
} catch {
|
|
431
|
+
return {}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function writePersistedState(
|
|
436
|
+
id: string,
|
|
437
|
+
statePersistence: DataTableProps<DataTableRow>["statePersistence"],
|
|
438
|
+
state: PersistedTableState
|
|
439
|
+
) {
|
|
440
|
+
if (typeof window === "undefined") {
|
|
441
|
+
return
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (statePersistence === "none") {
|
|
445
|
+
return
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (statePersistence === "url") {
|
|
449
|
+
const url = new URL(window.location.href)
|
|
450
|
+
|
|
451
|
+
if (isPersistedStateEmpty(state)) {
|
|
452
|
+
url.searchParams.delete(getPersistedUrlParam(id))
|
|
453
|
+
} else {
|
|
454
|
+
url.searchParams.set(getPersistedUrlParam(id), JSON.stringify(state))
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
window.history.replaceState(window.history.state, "", url)
|
|
458
|
+
return
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
window.localStorage.setItem(
|
|
462
|
+
`${LOCAL_STORAGE_PREFIX}${id}`,
|
|
463
|
+
JSON.stringify(state)
|
|
464
|
+
)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function inferAlignment(kind: DataTableColumnKind): DataTableAlign {
|
|
468
|
+
if (kind === "number") {
|
|
469
|
+
return "right"
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (kind === "boolean" || kind === "selection") {
|
|
473
|
+
return "center"
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return "left"
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function inferWidth(kind: DataTableColumnKind) {
|
|
480
|
+
switch (kind) {
|
|
481
|
+
case "boolean":
|
|
482
|
+
case "selection":
|
|
483
|
+
return 76
|
|
484
|
+
case "number":
|
|
485
|
+
return 132
|
|
486
|
+
case "date":
|
|
487
|
+
return 168
|
|
488
|
+
case "json":
|
|
489
|
+
return 240
|
|
490
|
+
default:
|
|
491
|
+
return 220
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function resolveColumnId<TData extends DataTableRow>(
|
|
496
|
+
column: DataTableColumnConfig<TData>,
|
|
497
|
+
index: number
|
|
498
|
+
) {
|
|
499
|
+
return (
|
|
500
|
+
column.id ??
|
|
501
|
+
column.accessorKey ??
|
|
502
|
+
(column.header
|
|
503
|
+
? column.header.toLowerCase().replace(/\s+/g, "-")
|
|
504
|
+
: `column-${index}`)
|
|
505
|
+
)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function buildColumns<TData extends DataTableRow>(
|
|
509
|
+
data: TData[],
|
|
510
|
+
columns?: DataTableColumnConfig<TData>[],
|
|
511
|
+
showRowSelectionColumn?: boolean,
|
|
512
|
+
showRowActionButtonsColumn?: boolean,
|
|
513
|
+
rowActionsColumnSize?: number
|
|
514
|
+
) {
|
|
515
|
+
const inferredColumns: DataTableColumnConfig<TData>[] = (
|
|
516
|
+
Object.keys(data[0] ?? {}) as Array<Extract<keyof TData, string>>
|
|
517
|
+
).map((accessorKey) => ({
|
|
518
|
+
accessorKey,
|
|
519
|
+
header: formatHeaderLabel(accessorKey),
|
|
520
|
+
}))
|
|
521
|
+
|
|
522
|
+
const sourceColumns =
|
|
523
|
+
columns && columns.length > 0 ? columns : inferredColumns
|
|
524
|
+
|
|
525
|
+
const builtColumns = sourceColumns.map<ColumnDef<TData>>((column, index) => {
|
|
526
|
+
const id = resolveColumnId(column, index)
|
|
527
|
+
const samples = data
|
|
528
|
+
.slice(0, 25)
|
|
529
|
+
.map((row) =>
|
|
530
|
+
column.accessorFn
|
|
531
|
+
? column.accessorFn(row)
|
|
532
|
+
: column.accessorKey
|
|
533
|
+
? row[column.accessorKey]
|
|
534
|
+
: undefined
|
|
535
|
+
)
|
|
536
|
+
const kind = column.kind ?? inferDataTableColumnKind(samples)
|
|
537
|
+
const meta: DataTableColumnMeta = {
|
|
538
|
+
align: column.align ?? inferAlignment(kind),
|
|
539
|
+
cellClassName: column.cellClassName,
|
|
540
|
+
headerClassName: column.headerClassName,
|
|
541
|
+
highlightMatches:
|
|
542
|
+
column.highlightMatches ?? (kind === "text" ? true : false),
|
|
543
|
+
kind,
|
|
544
|
+
truncate: column.truncate ?? true,
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return {
|
|
548
|
+
accessorFn: column.accessorFn,
|
|
549
|
+
accessorKey: column.accessorKey,
|
|
550
|
+
cell: ({ getValue, row }) =>
|
|
551
|
+
column.cell
|
|
552
|
+
? column.cell({ row: row.original, value: getValue() })
|
|
553
|
+
: fallbackCellValue(getValue(), kind),
|
|
554
|
+
enableGlobalFilter: column.enableFiltering ?? true,
|
|
555
|
+
filterFn: column.filterFn,
|
|
556
|
+
enableHiding: column.enableHiding ?? true,
|
|
557
|
+
enableResizing: column.enableResizing ?? true,
|
|
558
|
+
enableSorting: column.enableSorting ?? true,
|
|
559
|
+
header: column.header ?? formatHeaderLabel(id),
|
|
560
|
+
id,
|
|
561
|
+
maxSize: column.maxWidth ?? 420,
|
|
562
|
+
meta,
|
|
563
|
+
minSize: column.minWidth ?? Math.min(inferWidth(kind), 120),
|
|
564
|
+
size: column.width ?? inferWidth(kind),
|
|
565
|
+
}
|
|
566
|
+
})
|
|
567
|
+
|
|
568
|
+
if (!showRowSelectionColumn) {
|
|
569
|
+
if (!showRowActionButtonsColumn) {
|
|
570
|
+
return builtColumns
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return [
|
|
574
|
+
...builtColumns,
|
|
575
|
+
{
|
|
576
|
+
cell: () => null,
|
|
577
|
+
enableGlobalFilter: false,
|
|
578
|
+
enableHiding: false,
|
|
579
|
+
enableResizing: false,
|
|
580
|
+
enableSorting: false,
|
|
581
|
+
header: "Actions",
|
|
582
|
+
id: "__actions",
|
|
583
|
+
maxSize: rowActionsColumnSize ?? 280,
|
|
584
|
+
meta: {
|
|
585
|
+
align: "right",
|
|
586
|
+
truncate: false,
|
|
587
|
+
} satisfies DataTableColumnMeta,
|
|
588
|
+
minSize: Math.min(rowActionsColumnSize ?? 180, 180),
|
|
589
|
+
size: rowActionsColumnSize ?? 220,
|
|
590
|
+
},
|
|
591
|
+
] satisfies ColumnDef<TData>[]
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const withSelectionColumn = [
|
|
595
|
+
{
|
|
596
|
+
cell: ({ row }) => (
|
|
597
|
+
<div className="flex items-center justify-center">
|
|
598
|
+
<DataTableCheckbox
|
|
599
|
+
ariaLabel={`Select row ${row.index + 1}`}
|
|
600
|
+
checked={row.getIsSelected()}
|
|
601
|
+
onCheckedChange={(checked) => row.toggleSelected(checked)}
|
|
602
|
+
/>
|
|
603
|
+
</div>
|
|
604
|
+
),
|
|
605
|
+
enableGlobalFilter: false,
|
|
606
|
+
enableHiding: false,
|
|
607
|
+
enableResizing: false,
|
|
608
|
+
enableSorting: false,
|
|
609
|
+
header: ({ table }) => (
|
|
610
|
+
<div className="flex items-center justify-center">
|
|
611
|
+
<DataTableCheckbox
|
|
612
|
+
ariaLabel="Select all visible rows"
|
|
613
|
+
checked={table.getIsAllPageRowsSelected()}
|
|
614
|
+
indeterminate={table.getIsSomePageRowsSelected()}
|
|
615
|
+
onCheckedChange={(checked) =>
|
|
616
|
+
table.toggleAllPageRowsSelected(checked)
|
|
617
|
+
}
|
|
618
|
+
/>
|
|
619
|
+
</div>
|
|
620
|
+
),
|
|
621
|
+
id: "__select",
|
|
622
|
+
maxSize: 56,
|
|
623
|
+
meta: {
|
|
624
|
+
align: "center",
|
|
625
|
+
kind: "selection",
|
|
626
|
+
sticky: "left",
|
|
627
|
+
truncate: false,
|
|
628
|
+
} satisfies DataTableColumnMeta,
|
|
629
|
+
minSize: 56,
|
|
630
|
+
size: 56,
|
|
631
|
+
},
|
|
632
|
+
...builtColumns,
|
|
633
|
+
] satisfies ColumnDef<TData>[]
|
|
634
|
+
|
|
635
|
+
if (!showRowActionButtonsColumn) {
|
|
636
|
+
return withSelectionColumn
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return [
|
|
640
|
+
...withSelectionColumn,
|
|
641
|
+
{
|
|
642
|
+
cell: () => null,
|
|
643
|
+
enableGlobalFilter: false,
|
|
644
|
+
enableHiding: false,
|
|
645
|
+
enableResizing: false,
|
|
646
|
+
enableSorting: false,
|
|
647
|
+
header: "Actions",
|
|
648
|
+
id: "__actions",
|
|
649
|
+
maxSize: rowActionsColumnSize ?? 280,
|
|
650
|
+
meta: {
|
|
651
|
+
align: "right",
|
|
652
|
+
truncate: false,
|
|
653
|
+
} satisfies DataTableColumnMeta,
|
|
654
|
+
minSize: Math.min(rowActionsColumnSize ?? 180, 180),
|
|
655
|
+
size: rowActionsColumnSize ?? 220,
|
|
656
|
+
},
|
|
657
|
+
] satisfies ColumnDef<TData>[]
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const globalFilterFn: FilterFn<RowData> = (row, columnId, filterValue) => {
|
|
661
|
+
const query = String(filterValue ?? "")
|
|
662
|
+
.trim()
|
|
663
|
+
.toLowerCase()
|
|
664
|
+
|
|
665
|
+
if (!query) {
|
|
666
|
+
return true
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
return stringifyFilterValue(row.getValue(columnId))
|
|
670
|
+
.toLowerCase()
|
|
671
|
+
.includes(query)
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function shouldIgnoreRowSelectionTarget(target: EventTarget | null) {
|
|
675
|
+
return (
|
|
676
|
+
target instanceof Element &&
|
|
677
|
+
Boolean(
|
|
678
|
+
target.closest(
|
|
679
|
+
[
|
|
680
|
+
"a",
|
|
681
|
+
"button",
|
|
682
|
+
"input",
|
|
683
|
+
"label",
|
|
684
|
+
"select",
|
|
685
|
+
"summary",
|
|
686
|
+
"textarea",
|
|
687
|
+
"[contenteditable=true]",
|
|
688
|
+
"[data-no-row-select=true]",
|
|
689
|
+
'[role="button"]',
|
|
690
|
+
'[role="link"]',
|
|
691
|
+
'[role="menuitem"]',
|
|
692
|
+
].join(",")
|
|
693
|
+
)
|
|
694
|
+
)
|
|
695
|
+
)
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function resolveRowActionRows<TData extends DataTableRow>(
|
|
699
|
+
action: DataTableRowAction<TData>,
|
|
700
|
+
row: Row<TData>,
|
|
701
|
+
selectedRows: Row<TData>[]
|
|
702
|
+
) {
|
|
703
|
+
if (action.scope === "selection" && row.getIsSelected()) {
|
|
704
|
+
return selectedRows.length > 0
|
|
705
|
+
? selectedRows.map((selectedRow) => selectedRow.original)
|
|
706
|
+
: [row.original]
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
return [row.original]
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function resolveRowActionRowIds<TData extends DataTableRow>(
|
|
713
|
+
action: DataTableRowAction<TData>,
|
|
714
|
+
row: Row<TData>,
|
|
715
|
+
selectedRows: Row<TData>[]
|
|
716
|
+
) {
|
|
717
|
+
if (action.scope === "selection" && row.getIsSelected()) {
|
|
718
|
+
return selectedRows.length > 0
|
|
719
|
+
? selectedRows.map((selectedRow) => selectedRow.id)
|
|
720
|
+
: [row.id]
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return [row.id]
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function buildRowActionContext<TData extends DataTableRow>(
|
|
727
|
+
action: DataTableRowAction<TData>,
|
|
728
|
+
row: Row<TData>,
|
|
729
|
+
selectedRows: Row<TData>[]
|
|
730
|
+
) {
|
|
731
|
+
return {
|
|
732
|
+
actionRowIds: resolveRowActionRowIds(action, row, selectedRows),
|
|
733
|
+
actionRows: resolveRowActionRows(action, row, selectedRows),
|
|
734
|
+
anchorRow: row.original,
|
|
735
|
+
anchorRowId: row.id,
|
|
736
|
+
selectedRowIds: selectedRows.map((selectedRow) => selectedRow.id),
|
|
737
|
+
selectedRows: selectedRows.map((selectedRow) => selectedRow.original),
|
|
738
|
+
} satisfies DataTableRowActionContext<TData>
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function resolveRowActionState<TData extends DataTableRow>(
|
|
742
|
+
value:
|
|
743
|
+
| boolean
|
|
744
|
+
| ((context: DataTableRowActionContext<TData>) => boolean)
|
|
745
|
+
| undefined,
|
|
746
|
+
context: DataTableRowActionContext<TData>
|
|
747
|
+
) {
|
|
748
|
+
if (typeof value === "function") {
|
|
749
|
+
return value(context)
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
return Boolean(value)
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function resolveRowActionLabel<TData extends DataTableRow>(
|
|
756
|
+
action: DataTableRowAction<TData>,
|
|
757
|
+
context: DataTableRowActionContext<TData>
|
|
758
|
+
) {
|
|
759
|
+
return typeof action.label === "function"
|
|
760
|
+
? action.label(context)
|
|
761
|
+
: action.label
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function resolveRowActionButton(
|
|
765
|
+
action: DataTableRowAction<DataTableRow>
|
|
766
|
+
): Exclude<DataTableRowActionButtonConfig, false> | null {
|
|
767
|
+
if (action.button === undefined || action.button === false) {
|
|
768
|
+
return null
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
return action.button
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function getRowActionStatusKey<TData extends DataTableRow>(
|
|
775
|
+
action: DataTableRowAction<TData>,
|
|
776
|
+
context: DataTableRowActionContext<TData>
|
|
777
|
+
) {
|
|
778
|
+
return `${action.id}:${context.actionRowIds.join(",")}`
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function getRowActionResultMessage(result: unknown) {
|
|
782
|
+
if (typeof result === "string") {
|
|
783
|
+
return result
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
if (
|
|
787
|
+
result &&
|
|
788
|
+
typeof result === "object" &&
|
|
789
|
+
"message" in result &&
|
|
790
|
+
(typeof result.message === "string" || result.message === undefined)
|
|
791
|
+
) {
|
|
792
|
+
return result.message
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
return undefined
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function hasSelectionScopedAction<TData extends DataTableRow>(
|
|
799
|
+
actions: DataTableRowAction<TData>[]
|
|
800
|
+
): boolean {
|
|
801
|
+
return actions.some((action) => {
|
|
802
|
+
if (action.scope === "selection") {
|
|
803
|
+
return true
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const items = Array.isArray(action.items) ? action.items : undefined
|
|
807
|
+
|
|
808
|
+
return items ? hasSelectionScopedAction(items) : false
|
|
809
|
+
})
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function countStaticButtonActions<TData extends DataTableRow>(
|
|
813
|
+
actions: DataTableRowAction<TData>[]
|
|
814
|
+
): number {
|
|
815
|
+
return actions.reduce((count, action) => {
|
|
816
|
+
const nextCount =
|
|
817
|
+
resolveRowActionButton(action as DataTableRowAction<DataTableRow>) !== null
|
|
818
|
+
? count + 1
|
|
819
|
+
: count
|
|
820
|
+
|
|
821
|
+
const items = Array.isArray(action.items) ? action.items : undefined
|
|
822
|
+
|
|
823
|
+
return items ? nextCount + countStaticButtonActions(items) : nextCount
|
|
824
|
+
}, 0)
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
type ResolvedRowAction<TData extends DataTableRow> = {
|
|
828
|
+
action: DataTableRowAction<TData>
|
|
829
|
+
context: DataTableRowActionContext<TData>
|
|
830
|
+
items: ResolvedRowAction<TData>[]
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
type RowActionState = "idle" | "loading" | "success" | "error"
|
|
834
|
+
|
|
835
|
+
type RowActionStatus = {
|
|
836
|
+
message?: string
|
|
837
|
+
state: RowActionState
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function resolveRowActionItems<TData extends DataTableRow>(
|
|
841
|
+
action: DataTableRowAction<TData>,
|
|
842
|
+
context: DataTableRowActionContext<TData>,
|
|
843
|
+
row: Row<TData>,
|
|
844
|
+
selectedRows: Row<TData>[]
|
|
845
|
+
) {
|
|
846
|
+
const items =
|
|
847
|
+
typeof action.items === "function" ? action.items(context) : action.items
|
|
848
|
+
|
|
849
|
+
return resolveVisibleRowActions(items ?? [], row, selectedRows)
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function resolveVisibleRowActions<TData extends DataTableRow>(
|
|
853
|
+
actions: DataTableRowAction<TData>[],
|
|
854
|
+
row: Row<TData>,
|
|
855
|
+
selectedRows: Row<TData>[]
|
|
856
|
+
): ResolvedRowAction<TData>[] {
|
|
857
|
+
return actions.flatMap((action) => {
|
|
858
|
+
const context = buildRowActionContext(action, row, selectedRows)
|
|
859
|
+
|
|
860
|
+
if (resolveRowActionState(action.hidden, context)) {
|
|
861
|
+
return []
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const items = resolveRowActionItems(action, context, row, selectedRows)
|
|
865
|
+
|
|
866
|
+
if (action.items && items.length === 0) {
|
|
867
|
+
return []
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
return [
|
|
871
|
+
{
|
|
872
|
+
action,
|
|
873
|
+
context,
|
|
874
|
+
items,
|
|
875
|
+
},
|
|
876
|
+
]
|
|
877
|
+
})
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function RowActionMenuItem<TData extends DataTableRow>({
|
|
881
|
+
resolvedAction,
|
|
882
|
+
setStatus,
|
|
883
|
+
statuses,
|
|
884
|
+
}: {
|
|
885
|
+
resolvedAction: ResolvedRowAction<TData>
|
|
886
|
+
setStatus: (key: string, status: RowActionStatus, resetAfterMs?: number) => void
|
|
887
|
+
statuses: Record<string, RowActionStatus>
|
|
888
|
+
}) {
|
|
889
|
+
const { action, context, items } = resolvedAction
|
|
890
|
+
const label = resolveRowActionLabel(action, context)
|
|
891
|
+
const isDisabled = resolveRowActionState(action.disabled, context)
|
|
892
|
+
const statusKey = getRowActionStatusKey(action, context)
|
|
893
|
+
const status = statuses[statusKey]
|
|
894
|
+
const button = resolveRowActionButton(
|
|
895
|
+
action as DataTableRowAction<DataTableRow>
|
|
896
|
+
)
|
|
897
|
+
|
|
898
|
+
const handleRun = React.useCallback(async () => {
|
|
899
|
+
if (!action.onSelect) {
|
|
900
|
+
return
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
setStatus(statusKey, {
|
|
904
|
+
state: "loading",
|
|
905
|
+
})
|
|
906
|
+
|
|
907
|
+
try {
|
|
908
|
+
const result = await action.onSelect(context)
|
|
909
|
+
|
|
910
|
+
setStatus(
|
|
911
|
+
statusKey,
|
|
912
|
+
{
|
|
913
|
+
message: getRowActionResultMessage(result),
|
|
914
|
+
state: "success",
|
|
915
|
+
},
|
|
916
|
+
2_500
|
|
917
|
+
)
|
|
918
|
+
} catch (error) {
|
|
919
|
+
setStatus(
|
|
920
|
+
statusKey,
|
|
921
|
+
{
|
|
922
|
+
message: error instanceof Error ? error.message : String(error),
|
|
923
|
+
state: "error",
|
|
924
|
+
},
|
|
925
|
+
4_000
|
|
926
|
+
)
|
|
927
|
+
}
|
|
928
|
+
}, [action, context, setStatus, statusKey])
|
|
929
|
+
|
|
930
|
+
const Icon =
|
|
931
|
+
status?.state === "loading"
|
|
932
|
+
? LoaderCircle
|
|
933
|
+
: status?.state === "success"
|
|
934
|
+
? Check
|
|
935
|
+
: status?.state === "error"
|
|
936
|
+
? CircleAlert
|
|
937
|
+
: action.icon
|
|
938
|
+
const isBusy = status?.state === "loading"
|
|
939
|
+
|
|
940
|
+
if (items.length > 0) {
|
|
941
|
+
return (
|
|
942
|
+
<ContextMenuSub>
|
|
943
|
+
<ContextMenuSubTrigger disabled={isDisabled || isBusy}>
|
|
944
|
+
{Icon ? <Icon className="size-3.5" /> : null}
|
|
945
|
+
<span className="truncate">{label}</span>
|
|
946
|
+
</ContextMenuSubTrigger>
|
|
947
|
+
<ContextMenuSubContent className="w-72">
|
|
948
|
+
{items.map((item) => (
|
|
949
|
+
<RowActionMenuItem
|
|
950
|
+
key={item.action.id}
|
|
951
|
+
resolvedAction={item}
|
|
952
|
+
setStatus={setStatus}
|
|
953
|
+
statuses={statuses}
|
|
954
|
+
/>
|
|
955
|
+
))}
|
|
956
|
+
</ContextMenuSubContent>
|
|
957
|
+
</ContextMenuSub>
|
|
958
|
+
)
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
return (
|
|
962
|
+
<ContextMenuItem
|
|
963
|
+
disabled={isDisabled || isBusy}
|
|
964
|
+
onSelect={() => {
|
|
965
|
+
void handleRun()
|
|
966
|
+
}}
|
|
967
|
+
title={status?.message}
|
|
968
|
+
variant={
|
|
969
|
+
action.variant ??
|
|
970
|
+
((button === "destructive" ||
|
|
971
|
+
(typeof button === "object" && button?.variant === "destructive"))
|
|
972
|
+
? "destructive"
|
|
973
|
+
: "default")
|
|
974
|
+
}
|
|
975
|
+
>
|
|
976
|
+
{Icon ? (
|
|
977
|
+
<Icon
|
|
978
|
+
className={cn("size-3.5", isBusy && "animate-spin")}
|
|
979
|
+
/>
|
|
980
|
+
) : null}
|
|
981
|
+
<span className="truncate">{label}</span>
|
|
982
|
+
{action.shortcut ? (
|
|
983
|
+
<ContextMenuShortcut>{action.shortcut}</ContextMenuShortcut>
|
|
984
|
+
) : null}
|
|
985
|
+
</ContextMenuItem>
|
|
986
|
+
)
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function RowActionButtonGroup<TData extends DataTableRow>({
|
|
990
|
+
resolvedActions,
|
|
991
|
+
setStatus,
|
|
992
|
+
statuses,
|
|
993
|
+
}: {
|
|
994
|
+
resolvedActions: ResolvedRowAction<TData>[]
|
|
995
|
+
setStatus: (key: string, status: RowActionStatus, resetAfterMs?: number) => void
|
|
996
|
+
statuses: Record<string, RowActionStatus>
|
|
997
|
+
}) {
|
|
998
|
+
const buttonActions = resolvedActions.filter(
|
|
999
|
+
(resolvedAction) =>
|
|
1000
|
+
resolvedAction.items.length === 0 &&
|
|
1001
|
+
resolveRowActionButton(
|
|
1002
|
+
resolvedAction.action as DataTableRowAction<DataTableRow>
|
|
1003
|
+
) !== null
|
|
1004
|
+
)
|
|
1005
|
+
|
|
1006
|
+
if (buttonActions.length === 0) {
|
|
1007
|
+
return null
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
return (
|
|
1011
|
+
<div
|
|
1012
|
+
className="flex w-full justify-end gap-1.5"
|
|
1013
|
+
data-no-row-select="true"
|
|
1014
|
+
>
|
|
1015
|
+
{buttonActions.map((resolvedAction) => {
|
|
1016
|
+
const { action, context } = resolvedAction
|
|
1017
|
+
const button = resolveRowActionButton(
|
|
1018
|
+
action as DataTableRowAction<DataTableRow>
|
|
1019
|
+
)
|
|
1020
|
+
const label = resolveRowActionLabel(action, context)
|
|
1021
|
+
const statusKey = getRowActionStatusKey(action, context)
|
|
1022
|
+
const status = statuses[statusKey]
|
|
1023
|
+
const isDisabled =
|
|
1024
|
+
resolveRowActionState(action.disabled, context) ||
|
|
1025
|
+
status?.state === "loading"
|
|
1026
|
+
const Icon =
|
|
1027
|
+
status?.state === "loading"
|
|
1028
|
+
? LoaderCircle
|
|
1029
|
+
: status?.state === "success"
|
|
1030
|
+
? Check
|
|
1031
|
+
: status?.state === "error"
|
|
1032
|
+
? CircleAlert
|
|
1033
|
+
: action.icon
|
|
1034
|
+
const buttonLabel =
|
|
1035
|
+
button && typeof button === "object" && button.label
|
|
1036
|
+
? button.label
|
|
1037
|
+
: label
|
|
1038
|
+
const buttonVariant =
|
|
1039
|
+
status?.state === "error"
|
|
1040
|
+
? "destructive"
|
|
1041
|
+
: typeof button === "string"
|
|
1042
|
+
? button
|
|
1043
|
+
: button && typeof button === "object"
|
|
1044
|
+
? button.variant ?? "outline"
|
|
1045
|
+
: "outline"
|
|
1046
|
+
const buttonSize =
|
|
1047
|
+
button && typeof button === "object" && button.size
|
|
1048
|
+
? button.size
|
|
1049
|
+
: "sm"
|
|
1050
|
+
const buttonClassName =
|
|
1051
|
+
button && typeof button === "object" ? button.className : undefined
|
|
1052
|
+
|
|
1053
|
+
return (
|
|
1054
|
+
<Button
|
|
1055
|
+
aria-busy={status?.state === "loading"}
|
|
1056
|
+
className={buttonClassName}
|
|
1057
|
+
disabled={isDisabled}
|
|
1058
|
+
key={statusKey}
|
|
1059
|
+
onClick={() => {
|
|
1060
|
+
const runAction = action.onSelect
|
|
1061
|
+
|
|
1062
|
+
if (!runAction) {
|
|
1063
|
+
return
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
void (async () => {
|
|
1067
|
+
setStatus(statusKey, {
|
|
1068
|
+
state: "loading",
|
|
1069
|
+
})
|
|
1070
|
+
|
|
1071
|
+
try {
|
|
1072
|
+
const result = await runAction(context)
|
|
1073
|
+
|
|
1074
|
+
setStatus(
|
|
1075
|
+
statusKey,
|
|
1076
|
+
{
|
|
1077
|
+
message: getRowActionResultMessage(result),
|
|
1078
|
+
state: "success",
|
|
1079
|
+
},
|
|
1080
|
+
2_500
|
|
1081
|
+
)
|
|
1082
|
+
} catch (error) {
|
|
1083
|
+
setStatus(
|
|
1084
|
+
statusKey,
|
|
1085
|
+
{
|
|
1086
|
+
message:
|
|
1087
|
+
error instanceof Error ? error.message : String(error),
|
|
1088
|
+
state: "error",
|
|
1089
|
+
},
|
|
1090
|
+
4_000
|
|
1091
|
+
)
|
|
1092
|
+
}
|
|
1093
|
+
})()
|
|
1094
|
+
}}
|
|
1095
|
+
size={buttonSize}
|
|
1096
|
+
title={status?.message}
|
|
1097
|
+
type="button"
|
|
1098
|
+
variant={buttonVariant}
|
|
1099
|
+
>
|
|
1100
|
+
{Icon ? (
|
|
1101
|
+
<Icon
|
|
1102
|
+
className={cn("size-3.5", status?.state === "loading" && "animate-spin")}
|
|
1103
|
+
/>
|
|
1104
|
+
) : null}
|
|
1105
|
+
{buttonLabel}
|
|
1106
|
+
</Button>
|
|
1107
|
+
)
|
|
1108
|
+
})}
|
|
1109
|
+
</div>
|
|
1110
|
+
)
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function DataTableView<TData extends DataTableRow>({
|
|
1114
|
+
autoScrollToBottom = false,
|
|
1115
|
+
autoScrollToBottomThreshold = 96,
|
|
1116
|
+
columnFilters: controlledColumnFilters,
|
|
1117
|
+
columnVisibility: controlledColumnVisibility,
|
|
1118
|
+
columns,
|
|
1119
|
+
data,
|
|
1120
|
+
edgeHorizontalPadding = "16px",
|
|
1121
|
+
enableRowSelection = false,
|
|
1122
|
+
filterPlaceholder = "Search across visible columns",
|
|
1123
|
+
globalFilter,
|
|
1124
|
+
getRowId,
|
|
1125
|
+
height = 620,
|
|
1126
|
+
highlightQuery = "",
|
|
1127
|
+
id,
|
|
1128
|
+
onColumnFiltersChange,
|
|
1129
|
+
onColumnVisibilityChange,
|
|
1130
|
+
onGlobalFilterChange,
|
|
1131
|
+
resolveColumnHighlightTerms,
|
|
1132
|
+
rowActions,
|
|
1133
|
+
rowClassName,
|
|
1134
|
+
rowHeight = 48,
|
|
1135
|
+
statePersistence = "localStorage",
|
|
1136
|
+
}: DataTableProps<TData>) {
|
|
1137
|
+
const persistedState = React.useMemo(
|
|
1138
|
+
() => readPersistedState(id, statePersistence),
|
|
1139
|
+
[id, statePersistence]
|
|
1140
|
+
)
|
|
1141
|
+
const [sorting, setSorting] = React.useState<SortingState>(
|
|
1142
|
+
() => persistedState.sorting ?? []
|
|
1143
|
+
)
|
|
1144
|
+
const [columnSizing, setColumnSizing] = React.useState<ColumnSizingState>(
|
|
1145
|
+
() => persistedState.columnSizing ?? {}
|
|
1146
|
+
)
|
|
1147
|
+
const [columnVisibility, setColumnVisibility] =
|
|
1148
|
+
React.useState<VisibilityState>(() => persistedState.columnVisibility ?? {})
|
|
1149
|
+
const [highlightedColumns, setHighlightedColumns] = React.useState<
|
|
1150
|
+
Record<string, boolean>
|
|
1151
|
+
>(() => persistedState.highlightedColumns ?? {})
|
|
1152
|
+
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
|
1153
|
+
() => persistedState.columnFilters ?? []
|
|
1154
|
+
)
|
|
1155
|
+
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({})
|
|
1156
|
+
const [rowActionStatuses, setRowActionStatuses] = React.useState<
|
|
1157
|
+
Record<string, RowActionStatus>
|
|
1158
|
+
>({})
|
|
1159
|
+
const rowActionStatusTimersRef = React.useRef<Record<string, number>>({})
|
|
1160
|
+
const selectionAnchorIdRef = React.useRef<string | null>(null)
|
|
1161
|
+
const dragSelectionRef = React.useRef<{
|
|
1162
|
+
anchorId: string
|
|
1163
|
+
baseSelection: RowSelectionState
|
|
1164
|
+
} | null>(null)
|
|
1165
|
+
const dragPointerRef = React.useRef<{
|
|
1166
|
+
clientX: number
|
|
1167
|
+
clientY: number
|
|
1168
|
+
} | null>(null)
|
|
1169
|
+
const [isDragSelecting, setIsDragSelecting] = React.useState(false)
|
|
1170
|
+
const [searchDraft, setSearchDraft] = React.useState(
|
|
1171
|
+
() => persistedState.globalFilter ?? ""
|
|
1172
|
+
)
|
|
1173
|
+
const deferredSearch = useDeferredValue(searchDraft)
|
|
1174
|
+
const isColumnFiltersControlled = controlledColumnFilters !== undefined
|
|
1175
|
+
const isColumnVisibilityControlled = controlledColumnVisibility !== undefined
|
|
1176
|
+
const isGlobalFilterControlled = globalFilter !== undefined
|
|
1177
|
+
const resolvedColumnFilters = controlledColumnFilters ?? columnFilters
|
|
1178
|
+
const resolvedColumnVisibility =
|
|
1179
|
+
controlledColumnVisibility ?? columnVisibility
|
|
1180
|
+
const hasSelectionActions = rowActions
|
|
1181
|
+
? hasSelectionScopedAction(rowActions)
|
|
1182
|
+
: false
|
|
1183
|
+
const rowActionButtonCount = rowActions
|
|
1184
|
+
? countStaticButtonActions(rowActions)
|
|
1185
|
+
: 0
|
|
1186
|
+
const canSelectRows = enableRowSelection || hasSelectionActions
|
|
1187
|
+
const showRowSelectionColumn = enableRowSelection
|
|
1188
|
+
const showRowActionButtonsColumn = rowActionButtonCount > 0
|
|
1189
|
+
const rowActionsColumnSize = React.useMemo(
|
|
1190
|
+
() => Math.max(160, Math.min(320, rowActionButtonCount * 96)),
|
|
1191
|
+
[rowActionButtonCount]
|
|
1192
|
+
)
|
|
1193
|
+
const tableColumns = React.useMemo(
|
|
1194
|
+
() =>
|
|
1195
|
+
buildColumns(
|
|
1196
|
+
data,
|
|
1197
|
+
columns,
|
|
1198
|
+
showRowSelectionColumn,
|
|
1199
|
+
showRowActionButtonsColumn,
|
|
1200
|
+
rowActionsColumnSize
|
|
1201
|
+
),
|
|
1202
|
+
[
|
|
1203
|
+
columns,
|
|
1204
|
+
data,
|
|
1205
|
+
rowActionsColumnSize,
|
|
1206
|
+
showRowActionButtonsColumn,
|
|
1207
|
+
showRowSelectionColumn,
|
|
1208
|
+
]
|
|
1209
|
+
)
|
|
1210
|
+
const setRowActionStatus = React.useCallback(
|
|
1211
|
+
(key: string, status: RowActionStatus, resetAfterMs?: number) => {
|
|
1212
|
+
const existingTimer = rowActionStatusTimersRef.current[key]
|
|
1213
|
+
|
|
1214
|
+
if (existingTimer) {
|
|
1215
|
+
window.clearTimeout(existingTimer)
|
|
1216
|
+
delete rowActionStatusTimersRef.current[key]
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
setRowActionStatuses((current) => ({
|
|
1220
|
+
...current,
|
|
1221
|
+
[key]: status,
|
|
1222
|
+
}))
|
|
1223
|
+
|
|
1224
|
+
if (!resetAfterMs) {
|
|
1225
|
+
return
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
rowActionStatusTimersRef.current[key] = window.setTimeout(() => {
|
|
1229
|
+
setRowActionStatuses((current) => {
|
|
1230
|
+
const next = { ...current }
|
|
1231
|
+
|
|
1232
|
+
delete next[key]
|
|
1233
|
+
|
|
1234
|
+
return next
|
|
1235
|
+
})
|
|
1236
|
+
delete rowActionStatusTimersRef.current[key]
|
|
1237
|
+
}, resetAfterMs)
|
|
1238
|
+
},
|
|
1239
|
+
[]
|
|
1240
|
+
)
|
|
1241
|
+
const handleColumnSizingChange = React.useCallback<
|
|
1242
|
+
OnChangeFn<ColumnSizingState>
|
|
1243
|
+
>((updater) => {
|
|
1244
|
+
setColumnSizing((current) => functionalUpdate(updater, current))
|
|
1245
|
+
}, [])
|
|
1246
|
+
const handleColumnFiltersChange = React.useCallback<
|
|
1247
|
+
OnChangeFn<ColumnFiltersState>
|
|
1248
|
+
>(
|
|
1249
|
+
(updater) => {
|
|
1250
|
+
const nextValue = functionalUpdate(updater, resolvedColumnFilters)
|
|
1251
|
+
|
|
1252
|
+
if (!isColumnFiltersControlled) {
|
|
1253
|
+
setColumnFilters(nextValue)
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
onColumnFiltersChange?.(nextValue)
|
|
1257
|
+
},
|
|
1258
|
+
[isColumnFiltersControlled, onColumnFiltersChange, resolvedColumnFilters]
|
|
1259
|
+
)
|
|
1260
|
+
const handleColumnVisibilityChange = React.useCallback<
|
|
1261
|
+
OnChangeFn<VisibilityState>
|
|
1262
|
+
>(
|
|
1263
|
+
(updater) => {
|
|
1264
|
+
const nextValue = functionalUpdate(updater, resolvedColumnVisibility)
|
|
1265
|
+
|
|
1266
|
+
if (!isColumnVisibilityControlled) {
|
|
1267
|
+
setColumnVisibility(nextValue)
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
onColumnVisibilityChange?.(nextValue)
|
|
1271
|
+
},
|
|
1272
|
+
[
|
|
1273
|
+
isColumnVisibilityControlled,
|
|
1274
|
+
onColumnVisibilityChange,
|
|
1275
|
+
resolvedColumnVisibility,
|
|
1276
|
+
]
|
|
1277
|
+
)
|
|
1278
|
+
|
|
1279
|
+
const table = useReactTable({
|
|
1280
|
+
columnResizeMode: "onChange",
|
|
1281
|
+
columns: tableColumns,
|
|
1282
|
+
data,
|
|
1283
|
+
enableColumnResizing: true,
|
|
1284
|
+
enableRowSelection: canSelectRows,
|
|
1285
|
+
getCoreRowModel: getCoreRowModel(),
|
|
1286
|
+
getFilteredRowModel: getFilteredRowModel(),
|
|
1287
|
+
getRowId,
|
|
1288
|
+
getSortedRowModel: getSortedRowModel(),
|
|
1289
|
+
globalFilterFn: globalFilterFn as FilterFn<TData>,
|
|
1290
|
+
onColumnFiltersChange: handleColumnFiltersChange,
|
|
1291
|
+
onColumnSizingChange: handleColumnSizingChange,
|
|
1292
|
+
onColumnVisibilityChange: handleColumnVisibilityChange,
|
|
1293
|
+
onRowSelectionChange: setRowSelection,
|
|
1294
|
+
onSortingChange: setSorting,
|
|
1295
|
+
state: {
|
|
1296
|
+
columnFilters: resolvedColumnFilters,
|
|
1297
|
+
columnSizing,
|
|
1298
|
+
columnVisibility: resolvedColumnVisibility,
|
|
1299
|
+
globalFilter: deferredSearch.trim(),
|
|
1300
|
+
rowSelection,
|
|
1301
|
+
sorting,
|
|
1302
|
+
},
|
|
1303
|
+
})
|
|
1304
|
+
|
|
1305
|
+
const containerRef = React.useRef<HTMLDivElement>(null)
|
|
1306
|
+
const shouldAutoScrollRef = React.useRef(true)
|
|
1307
|
+
const hasAutoScrolledOnMountRef = React.useRef(false)
|
|
1308
|
+
const previousDataLengthRef = React.useRef(0)
|
|
1309
|
+
const rows = table.getRowModel().rows
|
|
1310
|
+
const selectedTableRows = table.getSelectedRowModel().rows
|
|
1311
|
+
const resolvedHeight = typeof height === "number" ? height : 0
|
|
1312
|
+
const initialOffsetRef = React.useRef(
|
|
1313
|
+
autoScrollToBottom
|
|
1314
|
+
? Math.max(rows.length * rowHeight - resolvedHeight, 0)
|
|
1315
|
+
: 0
|
|
1316
|
+
)
|
|
1317
|
+
const rowVirtualizer = useVirtualizer({
|
|
1318
|
+
count: rows.length,
|
|
1319
|
+
estimateSize: () => rowHeight,
|
|
1320
|
+
getScrollElement: () => containerRef.current,
|
|
1321
|
+
initialOffset: initialOffsetRef.current,
|
|
1322
|
+
overscan: 12,
|
|
1323
|
+
})
|
|
1324
|
+
const virtualRows = rowVirtualizer.getVirtualItems()
|
|
1325
|
+
const totalRows = data.length
|
|
1326
|
+
const filteredRows = rows.length
|
|
1327
|
+
const selectedRows = selectedTableRows.length
|
|
1328
|
+
|
|
1329
|
+
React.useEffect(() => {
|
|
1330
|
+
return () => {
|
|
1331
|
+
for (const timerId of Object.values(rowActionStatusTimersRef.current)) {
|
|
1332
|
+
window.clearTimeout(timerId)
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
}, [])
|
|
1336
|
+
|
|
1337
|
+
React.useEffect(() => {
|
|
1338
|
+
if (isColumnFiltersControlled) {
|
|
1339
|
+
return
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
const nextState = readPersistedState(id, statePersistence)
|
|
1343
|
+
|
|
1344
|
+
setSorting(nextState.sorting ?? [])
|
|
1345
|
+
setColumnSizing(nextState.columnSizing ?? {})
|
|
1346
|
+
if (!isColumnVisibilityControlled) {
|
|
1347
|
+
setColumnVisibility(nextState.columnVisibility ?? {})
|
|
1348
|
+
}
|
|
1349
|
+
setHighlightedColumns(nextState.highlightedColumns ?? {})
|
|
1350
|
+
setColumnFilters(nextState.columnFilters ?? [])
|
|
1351
|
+
setSearchDraft(nextState.globalFilter ?? "")
|
|
1352
|
+
setRowSelection({})
|
|
1353
|
+
dragSelectionRef.current = null
|
|
1354
|
+
dragPointerRef.current = null
|
|
1355
|
+
setIsDragSelecting(false)
|
|
1356
|
+
selectionAnchorIdRef.current = null
|
|
1357
|
+
for (const timerId of Object.values(rowActionStatusTimersRef.current)) {
|
|
1358
|
+
window.clearTimeout(timerId)
|
|
1359
|
+
}
|
|
1360
|
+
rowActionStatusTimersRef.current = {}
|
|
1361
|
+
setRowActionStatuses({})
|
|
1362
|
+
}, [id, isColumnFiltersControlled, isColumnVisibilityControlled, statePersistence])
|
|
1363
|
+
|
|
1364
|
+
React.useEffect(() => {
|
|
1365
|
+
if (!isColumnFiltersControlled) {
|
|
1366
|
+
return
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
setColumnFilters(controlledColumnFilters)
|
|
1370
|
+
}, [controlledColumnFilters, isColumnFiltersControlled])
|
|
1371
|
+
|
|
1372
|
+
React.useEffect(() => {
|
|
1373
|
+
if (!isColumnVisibilityControlled) {
|
|
1374
|
+
return
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
setColumnVisibility(controlledColumnVisibility)
|
|
1378
|
+
}, [controlledColumnVisibility, isColumnVisibilityControlled])
|
|
1379
|
+
|
|
1380
|
+
React.useEffect(() => {
|
|
1381
|
+
if (!isGlobalFilterControlled) {
|
|
1382
|
+
return
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
setSearchDraft(globalFilter)
|
|
1386
|
+
}, [globalFilter, isGlobalFilterControlled])
|
|
1387
|
+
|
|
1388
|
+
React.useEffect(() => {
|
|
1389
|
+
if (typeof window === "undefined" || statePersistence !== "url") {
|
|
1390
|
+
return
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
const syncFromUrl = () => {
|
|
1394
|
+
const nextState = readPersistedState(id, "url")
|
|
1395
|
+
|
|
1396
|
+
setSorting(nextState.sorting ?? [])
|
|
1397
|
+
setColumnSizing(nextState.columnSizing ?? {})
|
|
1398
|
+
if (!isColumnVisibilityControlled) {
|
|
1399
|
+
setColumnVisibility(nextState.columnVisibility ?? {})
|
|
1400
|
+
}
|
|
1401
|
+
setHighlightedColumns(nextState.highlightedColumns ?? {})
|
|
1402
|
+
setColumnFilters(nextState.columnFilters ?? [])
|
|
1403
|
+
setSearchDraft(nextState.globalFilter ?? "")
|
|
1404
|
+
setRowSelection({})
|
|
1405
|
+
dragSelectionRef.current = null
|
|
1406
|
+
dragPointerRef.current = null
|
|
1407
|
+
setIsDragSelecting(false)
|
|
1408
|
+
selectionAnchorIdRef.current = null
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
window.addEventListener("popstate", syncFromUrl)
|
|
1412
|
+
|
|
1413
|
+
return () => window.removeEventListener("popstate", syncFromUrl)
|
|
1414
|
+
}, [id, isColumnVisibilityControlled, statePersistence])
|
|
1415
|
+
|
|
1416
|
+
React.useEffect(() => {
|
|
1417
|
+
if (isGlobalFilterControlled || isColumnFiltersControlled) {
|
|
1418
|
+
return
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
writePersistedState(id, statePersistence, {
|
|
1422
|
+
columnFilters,
|
|
1423
|
+
columnSizing,
|
|
1424
|
+
highlightedColumns,
|
|
1425
|
+
columnVisibility: resolvedColumnVisibility,
|
|
1426
|
+
globalFilter: deferredSearch.trim(),
|
|
1427
|
+
sorting,
|
|
1428
|
+
})
|
|
1429
|
+
}, [
|
|
1430
|
+
columnFilters,
|
|
1431
|
+
columnSizing,
|
|
1432
|
+
deferredSearch,
|
|
1433
|
+
highlightedColumns,
|
|
1434
|
+
id,
|
|
1435
|
+
isColumnFiltersControlled,
|
|
1436
|
+
isGlobalFilterControlled,
|
|
1437
|
+
resolvedColumnVisibility,
|
|
1438
|
+
sorting,
|
|
1439
|
+
statePersistence,
|
|
1440
|
+
])
|
|
1441
|
+
|
|
1442
|
+
React.useEffect(() => {
|
|
1443
|
+
const container = containerRef.current
|
|
1444
|
+
|
|
1445
|
+
if (!autoScrollToBottom || !container) {
|
|
1446
|
+
return
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
const updateShouldAutoScroll = () => {
|
|
1450
|
+
const distanceFromBottom =
|
|
1451
|
+
container.scrollHeight - container.scrollTop - container.clientHeight
|
|
1452
|
+
|
|
1453
|
+
shouldAutoScrollRef.current =
|
|
1454
|
+
distanceFromBottom <= autoScrollToBottomThreshold
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
updateShouldAutoScroll()
|
|
1458
|
+
container.addEventListener("scroll", updateShouldAutoScroll, {
|
|
1459
|
+
passive: true,
|
|
1460
|
+
})
|
|
1461
|
+
|
|
1462
|
+
return () => {
|
|
1463
|
+
container.removeEventListener("scroll", updateShouldAutoScroll)
|
|
1464
|
+
}
|
|
1465
|
+
}, [autoScrollToBottom, autoScrollToBottomThreshold])
|
|
1466
|
+
|
|
1467
|
+
React.useLayoutEffect(() => {
|
|
1468
|
+
const container = containerRef.current
|
|
1469
|
+
const previousDataLength = previousDataLengthRef.current
|
|
1470
|
+
|
|
1471
|
+
previousDataLengthRef.current = data.length
|
|
1472
|
+
|
|
1473
|
+
const scrollToBottom = () => {
|
|
1474
|
+
if (rows.length === 0) {
|
|
1475
|
+
return
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
rowVirtualizer.scrollToIndex(rows.length - 1, {
|
|
1479
|
+
align: "end",
|
|
1480
|
+
})
|
|
1481
|
+
container?.scrollTo({
|
|
1482
|
+
top: container.scrollHeight,
|
|
1483
|
+
})
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
const scheduleBottomScroll = () => {
|
|
1487
|
+
const frameIds: number[] = []
|
|
1488
|
+
|
|
1489
|
+
const run = (remainingFrames: number) => {
|
|
1490
|
+
scrollToBottom()
|
|
1491
|
+
|
|
1492
|
+
if (remainingFrames <= 0) {
|
|
1493
|
+
return
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
const frameId = window.requestAnimationFrame(() =>
|
|
1497
|
+
run(remainingFrames - 1)
|
|
1498
|
+
)
|
|
1499
|
+
|
|
1500
|
+
frameIds.push(frameId)
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
run(4)
|
|
1504
|
+
|
|
1505
|
+
return () => {
|
|
1506
|
+
for (const frameId of frameIds) {
|
|
1507
|
+
window.cancelAnimationFrame(frameId)
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
if (
|
|
1513
|
+
autoScrollToBottom &&
|
|
1514
|
+
container &&
|
|
1515
|
+
!hasAutoScrolledOnMountRef.current &&
|
|
1516
|
+
rows.length > 0
|
|
1517
|
+
) {
|
|
1518
|
+
hasAutoScrolledOnMountRef.current = true
|
|
1519
|
+
shouldAutoScrollRef.current = true
|
|
1520
|
+
|
|
1521
|
+
return scheduleBottomScroll()
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
if (
|
|
1525
|
+
!autoScrollToBottom ||
|
|
1526
|
+
!container ||
|
|
1527
|
+
data.length <= previousDataLength ||
|
|
1528
|
+
!shouldAutoScrollRef.current
|
|
1529
|
+
) {
|
|
1530
|
+
return
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
return scheduleBottomScroll()
|
|
1534
|
+
}, [autoScrollToBottom, data.length, rowVirtualizer, rows.length])
|
|
1535
|
+
|
|
1536
|
+
const handleSearchDraftChange = (value: string) => {
|
|
1537
|
+
if (!isGlobalFilterControlled) {
|
|
1538
|
+
setSearchDraft(value)
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
onGlobalFilterChange?.(value)
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
const isColumnHighlightEnabled = React.useCallback(
|
|
1545
|
+
(columnId: string, meta: DataTableColumnMeta) =>
|
|
1546
|
+
highlightedColumns[columnId] ?? Boolean(meta.highlightMatches),
|
|
1547
|
+
[highlightedColumns]
|
|
1548
|
+
)
|
|
1549
|
+
const selectRange = React.useCallback(
|
|
1550
|
+
(
|
|
1551
|
+
anchorId: string,
|
|
1552
|
+
rowId: string,
|
|
1553
|
+
baseSelection: RowSelectionState = {}
|
|
1554
|
+
) => {
|
|
1555
|
+
const anchorIndex = rows.findIndex(
|
|
1556
|
+
(candidateRow) => candidateRow.id === anchorId
|
|
1557
|
+
)
|
|
1558
|
+
const rowIndex = rows.findIndex(
|
|
1559
|
+
(candidateRow) => candidateRow.id === rowId
|
|
1560
|
+
)
|
|
1561
|
+
|
|
1562
|
+
if (anchorIndex === -1 || rowIndex === -1) {
|
|
1563
|
+
return
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
const [start, end] =
|
|
1567
|
+
anchorIndex <= rowIndex
|
|
1568
|
+
? [anchorIndex, rowIndex]
|
|
1569
|
+
: [rowIndex, anchorIndex]
|
|
1570
|
+
|
|
1571
|
+
setRowSelection({
|
|
1572
|
+
...baseSelection,
|
|
1573
|
+
...Object.fromEntries(
|
|
1574
|
+
rows
|
|
1575
|
+
.slice(start, end + 1)
|
|
1576
|
+
.map((candidateRow) => [candidateRow.id, true])
|
|
1577
|
+
),
|
|
1578
|
+
})
|
|
1579
|
+
},
|
|
1580
|
+
[rows]
|
|
1581
|
+
)
|
|
1582
|
+
const selectSingleRow = React.useCallback((rowId: string) => {
|
|
1583
|
+
selectionAnchorIdRef.current = rowId
|
|
1584
|
+
setRowSelection({ [rowId]: true })
|
|
1585
|
+
}, [])
|
|
1586
|
+
const updateDragSelectionFromPointer = React.useCallback(
|
|
1587
|
+
(clientX: number, clientY: number) => {
|
|
1588
|
+
if (!dragSelectionRef.current) {
|
|
1589
|
+
return
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
const container = containerRef.current
|
|
1593
|
+
|
|
1594
|
+
if (!container) {
|
|
1595
|
+
return
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
const rowElement = document
|
|
1599
|
+
.elementFromPoint(clientX, clientY)
|
|
1600
|
+
?.closest<HTMLTableRowElement>("tr[data-row-id]")
|
|
1601
|
+
const renderedRows = Array.from(
|
|
1602
|
+
container.querySelectorAll<HTMLTableRowElement>("tr[data-row-id]")
|
|
1603
|
+
)
|
|
1604
|
+
const targetRowId =
|
|
1605
|
+
rowElement?.dataset.rowId ??
|
|
1606
|
+
(clientY < container.getBoundingClientRect().top
|
|
1607
|
+
? renderedRows[0]?.dataset.rowId
|
|
1608
|
+
: clientY > container.getBoundingClientRect().bottom
|
|
1609
|
+
? renderedRows.at(-1)?.dataset.rowId
|
|
1610
|
+
: undefined)
|
|
1611
|
+
|
|
1612
|
+
if (!targetRowId) {
|
|
1613
|
+
return
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
const { anchorId, baseSelection } = dragSelectionRef.current
|
|
1617
|
+
|
|
1618
|
+
selectRange(anchorId, targetRowId, baseSelection)
|
|
1619
|
+
},
|
|
1620
|
+
[selectRange]
|
|
1621
|
+
)
|
|
1622
|
+
const handleRowMouseDown = React.useCallback(
|
|
1623
|
+
(event: React.MouseEvent<HTMLTableRowElement>, row: Row<TData>) => {
|
|
1624
|
+
if (
|
|
1625
|
+
event.button !== 0 ||
|
|
1626
|
+
!canSelectRows ||
|
|
1627
|
+
shouldIgnoreRowSelectionTarget(event.target)
|
|
1628
|
+
) {
|
|
1629
|
+
return
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
event.preventDefault()
|
|
1633
|
+
dragPointerRef.current = {
|
|
1634
|
+
clientX: event.clientX,
|
|
1635
|
+
clientY: event.clientY,
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
const rowId = row.id
|
|
1639
|
+
const isAdditiveSelection = event.metaKey || event.ctrlKey
|
|
1640
|
+
const currentSelection = table.getState().rowSelection
|
|
1641
|
+
|
|
1642
|
+
if (event.shiftKey) {
|
|
1643
|
+
const anchorId = selectionAnchorIdRef.current ?? rowId
|
|
1644
|
+
|
|
1645
|
+
selectionAnchorIdRef.current = anchorId
|
|
1646
|
+
dragSelectionRef.current = {
|
|
1647
|
+
anchorId,
|
|
1648
|
+
baseSelection: {},
|
|
1649
|
+
}
|
|
1650
|
+
setIsDragSelecting(true)
|
|
1651
|
+
selectRange(anchorId, rowId)
|
|
1652
|
+
return
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
if (isAdditiveSelection) {
|
|
1656
|
+
const baseSelection = { ...currentSelection }
|
|
1657
|
+
|
|
1658
|
+
if (baseSelection[rowId]) {
|
|
1659
|
+
delete baseSelection[rowId]
|
|
1660
|
+
} else {
|
|
1661
|
+
baseSelection[rowId] = true
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
selectionAnchorIdRef.current = rowId
|
|
1665
|
+
dragSelectionRef.current = {
|
|
1666
|
+
anchorId: rowId,
|
|
1667
|
+
baseSelection,
|
|
1668
|
+
}
|
|
1669
|
+
setIsDragSelecting(true)
|
|
1670
|
+
setRowSelection(baseSelection)
|
|
1671
|
+
return
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
selectionAnchorIdRef.current = rowId
|
|
1675
|
+
dragSelectionRef.current = {
|
|
1676
|
+
anchorId: rowId,
|
|
1677
|
+
baseSelection: {},
|
|
1678
|
+
}
|
|
1679
|
+
setIsDragSelecting(true)
|
|
1680
|
+
setRowSelection({ [rowId]: true })
|
|
1681
|
+
},
|
|
1682
|
+
[canSelectRows, selectRange, table]
|
|
1683
|
+
)
|
|
1684
|
+
const handleRowMouseEnter = React.useCallback(
|
|
1685
|
+
(event: React.MouseEvent<HTMLTableRowElement>, row: Row<TData>) => {
|
|
1686
|
+
if (
|
|
1687
|
+
!canSelectRows ||
|
|
1688
|
+
event.buttons !== 1 ||
|
|
1689
|
+
shouldIgnoreRowSelectionTarget(event.target) ||
|
|
1690
|
+
!dragSelectionRef.current
|
|
1691
|
+
) {
|
|
1692
|
+
return
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
const { anchorId, baseSelection } = dragSelectionRef.current
|
|
1696
|
+
|
|
1697
|
+
selectRange(anchorId, row.id, baseSelection)
|
|
1698
|
+
},
|
|
1699
|
+
[canSelectRows, selectRange]
|
|
1700
|
+
)
|
|
1701
|
+
const handleRowContextMenu = React.useCallback(
|
|
1702
|
+
(row: Row<TData>) => {
|
|
1703
|
+
if (!canSelectRows) {
|
|
1704
|
+
return
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
if (row.getIsSelected()) {
|
|
1708
|
+
selectionAnchorIdRef.current = row.id
|
|
1709
|
+
return
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
selectSingleRow(row.id)
|
|
1713
|
+
},
|
|
1714
|
+
[canSelectRows, selectSingleRow]
|
|
1715
|
+
)
|
|
1716
|
+
React.useEffect(() => {
|
|
1717
|
+
if (!isDragSelecting) {
|
|
1718
|
+
return
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
const handleWindowMouseMove = (event: MouseEvent) => {
|
|
1722
|
+
dragPointerRef.current = {
|
|
1723
|
+
clientX: event.clientX,
|
|
1724
|
+
clientY: event.clientY,
|
|
1725
|
+
}
|
|
1726
|
+
updateDragSelectionFromPointer(event.clientX, event.clientY)
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
const stopDragSelection = () => {
|
|
1730
|
+
dragSelectionRef.current = null
|
|
1731
|
+
dragPointerRef.current = null
|
|
1732
|
+
setIsDragSelecting(false)
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
window.addEventListener("mousemove", handleWindowMouseMove)
|
|
1736
|
+
window.addEventListener("mouseup", stopDragSelection)
|
|
1737
|
+
|
|
1738
|
+
return () => {
|
|
1739
|
+
window.removeEventListener("mousemove", handleWindowMouseMove)
|
|
1740
|
+
window.removeEventListener("mouseup", stopDragSelection)
|
|
1741
|
+
}
|
|
1742
|
+
}, [isDragSelecting, updateDragSelectionFromPointer])
|
|
1743
|
+
React.useEffect(() => {
|
|
1744
|
+
if (!isDragSelecting) {
|
|
1745
|
+
return
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
let frameId = 0
|
|
1749
|
+
|
|
1750
|
+
const tick = () => {
|
|
1751
|
+
const container = containerRef.current
|
|
1752
|
+
const pointer = dragPointerRef.current
|
|
1753
|
+
|
|
1754
|
+
if (container && pointer) {
|
|
1755
|
+
const { top, bottom } = container.getBoundingClientRect()
|
|
1756
|
+
const edgeThreshold = 48
|
|
1757
|
+
let delta = 0
|
|
1758
|
+
|
|
1759
|
+
if (pointer.clientY < top + edgeThreshold) {
|
|
1760
|
+
delta = -Math.ceil((top + edgeThreshold - pointer.clientY) / 6)
|
|
1761
|
+
} else if (pointer.clientY > bottom - edgeThreshold) {
|
|
1762
|
+
delta = Math.ceil((pointer.clientY - (bottom - edgeThreshold)) / 6)
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
if (delta !== 0) {
|
|
1766
|
+
const maxDelta = 28
|
|
1767
|
+
const nextDelta = Math.max(-maxDelta, Math.min(maxDelta, delta))
|
|
1768
|
+
|
|
1769
|
+
container.scrollTop += nextDelta
|
|
1770
|
+
updateDragSelectionFromPointer(pointer.clientX, pointer.clientY)
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
frameId = window.requestAnimationFrame(tick)
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
frameId = window.requestAnimationFrame(tick)
|
|
1778
|
+
|
|
1779
|
+
return () => {
|
|
1780
|
+
window.cancelAnimationFrame(frameId)
|
|
1781
|
+
}
|
|
1782
|
+
}, [isDragSelecting, updateDragSelectionFromPointer])
|
|
1783
|
+
|
|
1784
|
+
return (
|
|
1785
|
+
<section
|
|
1786
|
+
className="flex min-h-0 flex-col overflow-hidden border-t border-border bg-card text-card-foreground shadow-sm"
|
|
1787
|
+
style={{ height }}
|
|
1788
|
+
>
|
|
1789
|
+
|
|
1790
|
+
<div
|
|
1791
|
+
className={cn(
|
|
1792
|
+
"min-h-0 flex-1 overflow-auto pb-4",
|
|
1793
|
+
isDragSelecting && "select-none"
|
|
1794
|
+
)}
|
|
1795
|
+
ref={containerRef}
|
|
1796
|
+
style={{
|
|
1797
|
+
scrollbarGutter: "stable",
|
|
1798
|
+
}}
|
|
1799
|
+
>
|
|
1800
|
+
<table
|
|
1801
|
+
className="grid w-full border-separate border-spacing-0"
|
|
1802
|
+
role="grid"
|
|
1803
|
+
>
|
|
1804
|
+
<thead className="sticky top-0 z-20 grid">
|
|
1805
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
|
1806
|
+
<tr
|
|
1807
|
+
className="flex w-full"
|
|
1808
|
+
key={headerGroup.id}
|
|
1809
|
+
style={{
|
|
1810
|
+
width: table.getTotalSize(),
|
|
1811
|
+
}}
|
|
1812
|
+
>
|
|
1813
|
+
{headerGroup.headers.map((header, index) => (
|
|
1814
|
+
<DataTableHeaderCol
|
|
1815
|
+
header={header}
|
|
1816
|
+
highlightEnabled={isColumnHighlightEnabled(
|
|
1817
|
+
header.column.id,
|
|
1818
|
+
(header.column.columnDef.meta ??
|
|
1819
|
+
{}) as DataTableColumnMeta
|
|
1820
|
+
)}
|
|
1821
|
+
key={header.id}
|
|
1822
|
+
onToggleHighlight={() =>
|
|
1823
|
+
setHighlightedColumns((current) => ({
|
|
1824
|
+
...current,
|
|
1825
|
+
[header.column.id]: !isColumnHighlightEnabled(
|
|
1826
|
+
header.column.id,
|
|
1827
|
+
(header.column.columnDef.meta ??
|
|
1828
|
+
{}) as DataTableColumnMeta
|
|
1829
|
+
),
|
|
1830
|
+
}))
|
|
1831
|
+
}
|
|
1832
|
+
paddingLeft={index === 0 ? edgeHorizontalPadding : undefined}
|
|
1833
|
+
paddingRight={
|
|
1834
|
+
index === headerGroup.headers.length - 1
|
|
1835
|
+
? edgeHorizontalPadding
|
|
1836
|
+
: undefined
|
|
1837
|
+
}
|
|
1838
|
+
scrollContainerRef={containerRef}
|
|
1839
|
+
/>
|
|
1840
|
+
))}
|
|
1841
|
+
</tr>
|
|
1842
|
+
))}
|
|
1843
|
+
</thead>
|
|
1844
|
+
|
|
1845
|
+
<tbody
|
|
1846
|
+
className="relative grid"
|
|
1847
|
+
style={{
|
|
1848
|
+
height: rowVirtualizer.getTotalSize(),
|
|
1849
|
+
}}
|
|
1850
|
+
>
|
|
1851
|
+
{virtualRows.length === 0 ? (
|
|
1852
|
+
<tr className="absolute inset-x-0 top-0 flex h-full items-center justify-center">
|
|
1853
|
+
<td className="px-4 py-10 text-center text-sm text-muted-foreground">
|
|
1854
|
+
No rows match the current filters.
|
|
1855
|
+
</td>
|
|
1856
|
+
</tr>
|
|
1857
|
+
) : (
|
|
1858
|
+
virtualRows.map((virtualRow) => {
|
|
1859
|
+
const row = rows[virtualRow.index]
|
|
1860
|
+
const visibleRowActions = resolveVisibleRowActions(
|
|
1861
|
+
rowActions ?? [],
|
|
1862
|
+
row,
|
|
1863
|
+
selectedTableRows
|
|
1864
|
+
)
|
|
1865
|
+
const isSelected = row.getIsSelected()
|
|
1866
|
+
const rowContent = (
|
|
1867
|
+
<tr
|
|
1868
|
+
aria-selected={isSelected}
|
|
1869
|
+
className={cn(
|
|
1870
|
+
"absolute left-0 flex w-full bg-card transition-colors",
|
|
1871
|
+
canSelectRows && "cursor-pointer",
|
|
1872
|
+
rowClassName?.(row.original),
|
|
1873
|
+
isSelected &&
|
|
1874
|
+
"bg-primary/10 before:absolute before:-top-px before:left-0 before:h-px before:w-full before:bg-primary before:content-[''] after:absolute after:bottom-0 after:left-0 after:h-px after:w-full after:bg-primary after:content-['']"
|
|
1875
|
+
)}
|
|
1876
|
+
data-index={virtualRow.index}
|
|
1877
|
+
data-row-id={row.id}
|
|
1878
|
+
data-state={isSelected ? "selected" : undefined}
|
|
1879
|
+
key={row.id}
|
|
1880
|
+
onContextMenu={() => handleRowContextMenu(row)}
|
|
1881
|
+
onMouseDown={(event) => handleRowMouseDown(event, row)}
|
|
1882
|
+
onMouseEnter={(event) => handleRowMouseEnter(event, row)}
|
|
1883
|
+
ref={(node) => {
|
|
1884
|
+
if (node) {
|
|
1885
|
+
rowVirtualizer.measureElement(node)
|
|
1886
|
+
}
|
|
1887
|
+
}}
|
|
1888
|
+
style={{
|
|
1889
|
+
minHeight: rowHeight,
|
|
1890
|
+
transform: `translateY(${virtualRow.start}px)`,
|
|
1891
|
+
width: table.getTotalSize(),
|
|
1892
|
+
}}
|
|
1893
|
+
>
|
|
1894
|
+
{row.getVisibleCells().map((cell, index, visibleCells) => {
|
|
1895
|
+
const meta = (cell.column.columnDef.meta ??
|
|
1896
|
+
{}) as DataTableColumnMeta
|
|
1897
|
+
const isActionsCell = cell.column.id === "__actions"
|
|
1898
|
+
const highlightTerms =
|
|
1899
|
+
meta.kind === "text" &&
|
|
1900
|
+
resolveColumnHighlightTerms &&
|
|
1901
|
+
isColumnHighlightEnabled(cell.column.id, meta)
|
|
1902
|
+
? resolveColumnHighlightTerms(
|
|
1903
|
+
cell.column.id,
|
|
1904
|
+
highlightQuery
|
|
1905
|
+
)
|
|
1906
|
+
: []
|
|
1907
|
+
|
|
1908
|
+
if (isActionsCell) {
|
|
1909
|
+
return (
|
|
1910
|
+
<td
|
|
1911
|
+
className={cn(
|
|
1912
|
+
"flex shrink-0 border-b border-border px-2 py-1.5 align-middle text-sm text-foreground justify-end text-right",
|
|
1913
|
+
meta.cellClassName
|
|
1914
|
+
)}
|
|
1915
|
+
key={cell.id}
|
|
1916
|
+
style={{
|
|
1917
|
+
paddingLeft:
|
|
1918
|
+
index === 0 ? edgeHorizontalPadding : undefined,
|
|
1919
|
+
paddingRight:
|
|
1920
|
+
index === visibleCells.length - 1
|
|
1921
|
+
? edgeHorizontalPadding
|
|
1922
|
+
: undefined,
|
|
1923
|
+
width: cell.column.getSize(),
|
|
1924
|
+
}}
|
|
1925
|
+
>
|
|
1926
|
+
<div className="w-full min-w-0">
|
|
1927
|
+
<RowActionButtonGroup
|
|
1928
|
+
resolvedActions={visibleRowActions}
|
|
1929
|
+
setStatus={setRowActionStatus}
|
|
1930
|
+
statuses={rowActionStatuses}
|
|
1931
|
+
/>
|
|
1932
|
+
</div>
|
|
1933
|
+
</td>
|
|
1934
|
+
)
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
return (
|
|
1938
|
+
<DataTableBodyCell
|
|
1939
|
+
cell={cell}
|
|
1940
|
+
highlightTerms={highlightTerms}
|
|
1941
|
+
key={cell.id}
|
|
1942
|
+
paddingLeft={
|
|
1943
|
+
index === 0 ? edgeHorizontalPadding : undefined
|
|
1944
|
+
}
|
|
1945
|
+
paddingRight={
|
|
1946
|
+
index === visibleCells.length - 1
|
|
1947
|
+
? edgeHorizontalPadding
|
|
1948
|
+
: undefined
|
|
1949
|
+
}
|
|
1950
|
+
/>
|
|
1951
|
+
)
|
|
1952
|
+
})}
|
|
1953
|
+
</tr>
|
|
1954
|
+
)
|
|
1955
|
+
|
|
1956
|
+
if (visibleRowActions.length === 0) {
|
|
1957
|
+
return rowContent
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
return (
|
|
1961
|
+
<ContextMenu key={row.id}>
|
|
1962
|
+
<ContextMenuTrigger asChild>
|
|
1963
|
+
{rowContent}
|
|
1964
|
+
</ContextMenuTrigger>
|
|
1965
|
+
<ContextMenuContent className="w-64">
|
|
1966
|
+
{visibleRowActions.map((resolvedAction) => (
|
|
1967
|
+
<RowActionMenuItem
|
|
1968
|
+
key={resolvedAction.action.id}
|
|
1969
|
+
resolvedAction={resolvedAction}
|
|
1970
|
+
setStatus={setRowActionStatus}
|
|
1971
|
+
statuses={rowActionStatuses}
|
|
1972
|
+
/>
|
|
1973
|
+
))}
|
|
1974
|
+
<ContextMenuSeparator />
|
|
1975
|
+
|
|
1976
|
+
<ContextMenuLabel>
|
|
1977
|
+
{isSelected && selectedTableRows.length > 1
|
|
1978
|
+
? `${selectedTableRows.length} selected rows`
|
|
1979
|
+
: "Row actions"}
|
|
1980
|
+
</ContextMenuLabel>
|
|
1981
|
+
</ContextMenuContent>
|
|
1982
|
+
</ContextMenu>
|
|
1983
|
+
)
|
|
1984
|
+
})
|
|
1985
|
+
)}
|
|
1986
|
+
</tbody>
|
|
1987
|
+
</table>
|
|
1988
|
+
</div>
|
|
1989
|
+
</section>
|
|
1990
|
+
)
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
export function DataTable<TData extends DataTableRow>(
|
|
1994
|
+
props: DataTableProps<TData>
|
|
1995
|
+
): React.JSX.Element
|
|
1996
|
+
export function DataTable<TData extends DataTableRow>(
|
|
1997
|
+
props?: Partial<DataTableProps<TData>>
|
|
1998
|
+
): React.JSX.Element
|
|
1999
|
+
export function DataTable<TData extends DataTableRow>(
|
|
2000
|
+
props: Partial<DataTableProps<TData>> = {}
|
|
2001
|
+
) {
|
|
2002
|
+
const context = useOptionalDataTableContext<TData>()
|
|
2003
|
+
const resolvedProps = context
|
|
2004
|
+
? ({ ...context.tableProps, ...props } as Partial<DataTableProps<TData>>)
|
|
2005
|
+
: props
|
|
2006
|
+
|
|
2007
|
+
if (resolvedProps.data === undefined || resolvedProps.id === undefined) {
|
|
2008
|
+
throw new Error(
|
|
2009
|
+
"DataTable must be used inside DataTableProvider or receive data and id props."
|
|
2010
|
+
)
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
return <DataTableView {...(resolvedProps as DataTableProps<TData>)} />
|
|
2014
|
+
}
|