1
1
import { SetStateAction , useEffect , useRef , useState } from 'react' ;
2
2
import { Table , TableBody , TableCell , TableHead , TableHeader , TableRow } from "@/components/ui/table" ;
3
3
import { getCoreRowModel , useReactTable , flexRender , getFilteredRowModel , getPaginationRowModel , getExpandedRowModel , getSortedRowModel } from '@tanstack/react-table' ;
4
- import type { ColumnDef , Row , PaginationState , RowSelectionState , ColumnFiltersState , ExpandedState , ColumnDefTemplate , HeaderContext , SortingState } from '@tanstack/react-table' ;
4
+ import type { ColumnDef , Row , PaginationState , RowSelectionState , ColumnFiltersState , ExpandedState , ColumnDefTemplate , HeaderContext , SortingState , VisibilityState , Header , Column } from '@tanstack/react-table' ;
5
5
import { cn } from '@/lib/utils' ;
6
6
import { Select , SelectContent , SelectGroup , SelectItem , SelectLabel } from './select' ;
7
7
import { Button } from './button' ;
8
- import { ArrowDownIcon , ArrowUpIcon , ChevronLeftIcon , ChevronRightIcon , DoubleArrowLeftIcon , DoubleArrowRightIcon } from '@radix-ui/react-icons' ;
8
+ import { ArrowDownIcon , ArrowUpIcon , ChevronLeftIcon , ChevronRightIcon , DotsHorizontalIcon , DoubleArrowLeftIcon , DoubleArrowRightIcon } from '@radix-ui/react-icons' ;
9
9
import * as SelectPrimitive from "@radix-ui/react-select"
10
10
import type { Table as ReactTable } from '@tanstack/react-table' ;
11
11
import { useTranslation } from 'react-i18next' ;
12
12
import { useResizeObserver } from '@/hooks/useResizeObserver' ;
13
13
import useKeyboardShortcut from 'use-keyboard-shortcut' ;
14
+ import { DropdownMenu , DropdownMenuContent , DropdownMenuItem , DropdownMenuLabel , DropdownMenuSeparator , DropdownMenuTrigger } from './dropdown-menu' ;
15
+ import { triblerService } from '@/services/tribler.service' ;
16
+
17
+
18
+ declare module '@tanstack/table-core/build/lib/types' {
19
+ export interface ColumnMeta < TData extends RowData , TValue > {
20
+ hide_by_default : boolean ;
21
+ }
22
+ }
14
23
15
24
16
25
export function getHeader < T > ( name : string , translate : boolean = true , addSorting : boolean = true ) : ColumnDefTemplate < HeaderContext < T , unknown > > | undefined {
@@ -42,15 +51,22 @@ export function getHeader<T>(name: string, translate: boolean = true, addSorting
42
51
}
43
52
}
44
53
45
- function getStoredSortingState ( key ?: string ) {
46
- if ( key ) {
47
- let sortingString = localStorage . getItem ( key ) ;
48
- if ( sortingString ) {
49
- return JSON . parse ( sortingString ) ;
50
- }
54
+ function getState ( type : "columns" | "sorting" , name ?: string ) {
55
+ let stateString = triblerService . guiSettings [ type ] ;
56
+ if ( stateString && name ) {
57
+ return JSON . parse ( stateString ) [ name ] ;
51
58
}
52
59
}
53
60
61
+ function setState ( type : "columns" | "sorting" , name : string , state : SortingState | VisibilityState ) {
62
+ let stateString = triblerService . guiSettings [ type ] ;
63
+ let stateSettings = stateString ? JSON . parse ( stateString ) : { } ;
64
+ stateSettings [ name ] = state ;
65
+
66
+ triblerService . guiSettings [ type ] = JSON . stringify ( stateSettings ) ;
67
+ triblerService . setSettings ( { ui : triblerService . guiSettings } ) ;
68
+ }
69
+
54
70
interface ReactTableProps < T extends object > {
55
71
data : T [ ] ;
56
72
columns : ColumnDef < T > [ ] ;
@@ -65,6 +81,7 @@ interface ReactTableProps<T extends object> {
65
81
allowSelect ?: boolean ;
66
82
allowSelectCheckbox ?: boolean ;
67
83
allowMultiSelect ?: boolean ;
84
+ allowColumnToggle ?: string ;
68
85
filters ?: { id : string , value : string } [ ] ;
69
86
maxHeight ?: string | number ;
70
87
expandable ?: boolean ;
@@ -85,6 +102,7 @@ function SimpleTable<T extends object>({
85
102
allowSelect,
86
103
allowSelectCheckbox,
87
104
allowMultiSelect,
105
+ allowColumnToggle,
88
106
filters,
89
107
maxHeight,
90
108
expandable,
@@ -98,21 +116,47 @@ function SimpleTable<T extends object>({
98
116
const [ rowSelection , setRowSelection ] = useState < RowSelectionState > ( initialRowSelection || { } ) ;
99
117
const [ columnFilters , setColumnFilters ] = useState < ColumnFiltersState > ( filters || [ ] )
100
118
const [ expanded , setExpanded ] = useState < ExpandedState > ( { } ) ;
101
- const [ sorting , setSorting ] = useState < SortingState > ( getStoredSortingState ( storeSortingState ) || [ ] ) ;
119
+ const [ sorting , setSorting ] = useState < SortingState > ( getState ( "sorting" , storeSortingState ) || [ ] ) ;
102
120
103
- useKeyboardShortcut (
104
- [ "Control" , "A" ] ,
105
- keys => {
106
- if ( allowMultiSelect ) {
107
- table . toggleAllRowsSelected ( true ) ;
108
- }
109
- } ,
110
- {
111
- overrideSystem : true ,
112
- ignoreInputFields : true ,
113
- repeatOnHold : false
121
+ //Get stored column visibility and add missing visibilities with their defaults.
122
+ const visibilityState = getState ( "columns" , allowColumnToggle ) || { } ;
123
+ let col : any ;
124
+ for ( col of columns ) {
125
+ if ( col . accessorKey && col . accessorKey in visibilityState === false ) {
126
+ visibilityState [ col . accessorKey ] = col . meta ?. hide_by_default !== true ;
127
+ }
128
+ }
129
+ const [ columnVisibility , setColumnVisibility ] = useState < VisibilityState > ( visibilityState ) ;
130
+
131
+ useKeyboardShortcut ( [ "Control" , "A" ] , ( ) => {
132
+ if ( allowMultiSelect ) {
133
+ table . toggleAllRowsSelected ( true ) ;
114
134
}
115
- ) ;
135
+ } , { overrideSystem : true , repeatOnHold : false } ) ;
136
+ useKeyboardShortcut ( [ "ArrowUp" ] , ( ) => {
137
+ let ids = Object . keys ( rowSelection ) ;
138
+ let rows = table . getSortedRowModel ( ) . rows ;
139
+ let index = rows . findIndex ( ( row ) => ids . includes ( row . id ) ) ;
140
+ let next = rows [ index - 1 ] || rows [ 0 ] ;
141
+
142
+ let selection : any = { } ;
143
+ selection [ next . id . toString ( ) ] = true ;
144
+ table . setRowSelection ( selection ) ;
145
+
146
+ document . querySelector ( "[data-state='selected']" ) ?. scrollIntoView ( { behavior : 'smooth' , block : 'center' , inline : 'center' } ) ;
147
+ } ) ;
148
+ useKeyboardShortcut ( [ "ArrowDown" ] , ( ) => {
149
+ let ids = Object . keys ( rowSelection ) ;
150
+ let rows = table . getSortedRowModel ( ) . rows ;
151
+ let index = rows . findLastIndex ( ( row ) => ids . includes ( row . id ) ) ;
152
+ let next = rows [ index + 1 ] || rows [ rows . length - 1 ] ;
153
+
154
+ let selection : any = { } ;
155
+ selection [ next . id . toString ( ) ] = true ;
156
+ table . setRowSelection ( selection ) ;
157
+
158
+ document . querySelector ( "[data-state='selected']" ) ?. scrollIntoView ( { behavior : 'smooth' , block : 'center' , inline : 'center' } ) ;
159
+ } ) ;
116
160
117
161
const table = useReactTable ( {
118
162
data,
@@ -127,11 +171,13 @@ function SimpleTable<T extends object>({
127
171
pagination,
128
172
rowSelection,
129
173
columnFilters,
174
+ columnVisibility,
130
175
expanded,
131
176
sorting
132
177
} ,
133
178
getFilteredRowModel : getFilteredRowModel ( ) ,
134
179
onColumnFiltersChange : setColumnFilters ,
180
+ onColumnVisibilityChange : setColumnVisibility ,
135
181
onPaginationChange : setPagination ,
136
182
onRowSelectionChange : ( arg : SetStateAction < RowSelectionState > ) => {
137
183
if ( allowSelect || allowSelectCheckbox || allowMultiSelect ) setRowSelection ( arg ) ;
@@ -173,10 +219,16 @@ function SimpleTable<T extends object>({
173
219
174
220
useEffect ( ( ) => {
175
221
if ( storeSortingState ) {
176
- localStorage . setItem ( storeSortingState , JSON . stringify ( sorting ) ) ;
222
+ setState ( "sorting" , storeSortingState , sorting ) ;
177
223
}
178
224
} , [ sorting ] ) ;
179
225
226
+ useEffect ( ( ) => {
227
+ if ( allowColumnToggle ) {
228
+ setState ( "columns" , allowColumnToggle , columnVisibility ) ;
229
+ }
230
+ } , [ columnVisibility ] ) ;
231
+
180
232
// For some reason the ScrollArea scrollbar is only shown when it's set to a specific height.
181
233
// So, we wrap it in a parent div, monitor its size, and set the height of the table accordingly.
182
234
const parentRef = useRef < HTMLTableElement > ( null ) ;
@@ -186,12 +238,16 @@ function SimpleTable<T extends object>({
186
238
< >
187
239
< div ref = { parentRef } className = 'flex-grow flex' >
188
240
< Table maxHeight = { maxHeight ?? ( parentRect ?. height ?? 200 ) } >
189
- < TableHeader >
241
+ < TableHeader className = 'z-10' >
190
242
{ table . getHeaderGroups ( ) . map ( ( headerGroup ) => (
191
243
< TableRow key = { headerGroup . id } className = "bg-neutral-100 hover:bg-neutral-100 dark:bg-neutral-900 dark:hover:bg-neutral-900" >
192
244
{ headerGroup . headers . map ( ( header , index ) => {
193
245
return (
194
- < TableHead key = { header . id } className = { cn ( { 'pl-4' : index === 0 , 'pr-4' : index + 1 === headerGroup . headers . length , } ) } >
246
+ < TableHead key = { header . id } className = { cn ( {
247
+ 'pl-4' : index === 0 ,
248
+ 'pr-4' : ! allowColumnToggle && index + 1 === headerGroup . headers . length ,
249
+ 'pr-0' : ! ! allowColumnToggle
250
+ } ) } >
195
251
{ header . isPlaceholder
196
252
? null
197
253
: flexRender (
@@ -201,6 +257,41 @@ function SimpleTable<T extends object>({
201
257
</ TableHead >
202
258
)
203
259
} ) }
260
+ { allowColumnToggle && < TableHead key = "toggleColumns" className = "w-2 pl-1 pr-3 cursor-pointer hover:text-black dark:hover:text-white" >
261
+ < DropdownMenu >
262
+ < DropdownMenuTrigger asChild >
263
+ < DotsHorizontalIcon className = "h-4 w-4" />
264
+ </ DropdownMenuTrigger >
265
+ < DropdownMenuContent align = "end" >
266
+ < DropdownMenuLabel > { t ( 'Toggle columns' ) } </ DropdownMenuLabel >
267
+ < DropdownMenuSeparator />
268
+ { table . getAllLeafColumns ( ) . map ( column => {
269
+ const fakeColumn = {
270
+ ...column ,
271
+ toggleSorting : ( ) => { } ,
272
+ getIsSorted : ( ) => { } ,
273
+ } as Column < any , unknown > ;
274
+ return (
275
+ < DropdownMenuItem key = { `toggleColumns-${ column . id } ` } >
276
+ < label onClick = { ( evt ) => evt . stopPropagation ( ) } className = 'flex space-x-1' >
277
+ < input
278
+ { ...{
279
+ type : 'checkbox' ,
280
+ checked : column . getIsVisible ( ) ,
281
+ onChange : column . getToggleVisibilityHandler ( ) ,
282
+ } }
283
+ /> { flexRender ( column . columnDef . header , {
284
+ table,
285
+ column : fakeColumn ,
286
+ header : { column : fakeColumn } as Header < any , unknown > ,
287
+ } ) }
288
+ </ label >
289
+ </ DropdownMenuItem >
290
+ )
291
+ } ) }
292
+ </ DropdownMenuContent >
293
+ </ DropdownMenu >
294
+ </ TableHead > }
204
295
</ TableRow >
205
296
) ) }
206
297
</ TableHeader >
0 commit comments