Skip to content

Commit 0813111

Browse files
committed
feat: improve tables and sync table states with search param (#199)
* refactor: use columns directly in data-table Use columns directly in the data-table to avoid unnecessary prop drilling. * refactor: rename data-table to tasks-table * fix: replace column filtering with global filtering in tasks-table * feat: sync task table states with search param * refactor: extract table synced state logic into custom hook * refactor: improve tasks rotue search param type safety * feat: sync user table states with search param * feat: update search and navigation route API in users-table Closes #184
1 parent b33a184 commit 0813111

10 files changed

Lines changed: 379 additions & 50 deletions

File tree

src/features/tasks/components/data-table-toolbar.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,16 @@ type DataTableToolbarProps<TData> = {
1313
export function DataTableToolbar<TData>({
1414
table,
1515
}: DataTableToolbarProps<TData>) {
16-
const isFiltered = table.getState().columnFilters.length > 0
16+
const isFiltered =
17+
table.getState().columnFilters.length > 0 || table.getState().globalFilter
1718

1819
return (
1920
<div className='flex items-center justify-between'>
2021
<div className='flex flex-1 flex-col-reverse items-start gap-y-2 sm:flex-row sm:items-center sm:space-x-2'>
2122
<Input
22-
placeholder='Filter tasks...'
23-
value={(table.getColumn('title')?.getFilterValue() as string) ?? ''}
24-
onChange={(event) =>
25-
table.getColumn('title')?.setFilterValue(event.target.value)
26-
}
23+
placeholder='Filter by title or ID...'
24+
value={table.getState().globalFilter ?? ''}
25+
onChange={(event) => table.setGlobalFilter(event.target.value)}
2726
className='h-8 w-[150px] lg:w-[250px]'
2827
/>
2928
<div className='flex gap-x-2'>
@@ -45,7 +44,10 @@ export function DataTableToolbar<TData>({
4544
{isFiltered && (
4645
<Button
4746
variant='ghost'
48-
onClick={() => table.resetColumnFilters()}
47+
onClick={() => {
48+
table.resetColumnFilters()
49+
table.setGlobalFilter('')
50+
}}
4951
className='h-8 px-2 lg:px-3'
5052
>
5153
Reset

src/features/tasks/components/data-table.tsx renamed to src/features/tasks/components/tasks-table.tsx

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import * as React from 'react'
1+
import { useEffect, useState } from 'react'
2+
import { getRouteApi } from '@tanstack/react-router'
23
import {
3-
type ColumnDef,
4-
type ColumnFiltersState,
54
type SortingState,
65
type VisibilityState,
76
flexRender,
@@ -13,6 +12,7 @@ import {
1312
getSortedRowModel,
1413
useReactTable,
1514
} from '@tanstack/react-table'
15+
import { useTableUrlState } from '@/hooks/use-table-url-state'
1616
import {
1717
Table,
1818
TableBody,
@@ -21,26 +21,43 @@ import {
2121
TableHeader,
2222
TableRow,
2323
} from '@/components/ui/table'
24+
import { type Task } from '../data/schema'
2425
import { DataTableBulkActions } from './data-table-bulk-actions'
2526
import { DataTablePagination } from './data-table-pagination'
2627
import { DataTableToolbar } from './data-table-toolbar'
28+
import { tasksColumns as columns } from './tasks-columns'
2729

28-
type DataTableProps<TData, TValue> = {
29-
columns: ColumnDef<TData, TValue>[]
30-
data: TData[]
30+
const route = getRouteApi('/_authenticated/tasks/')
31+
32+
type DataTableProps = {
33+
data: Task[]
3134
}
3235

33-
export function TasksTable<TData, TValue>({
34-
columns,
35-
data,
36-
}: DataTableProps<TData, TValue>) {
37-
const [rowSelection, setRowSelection] = React.useState({})
38-
const [columnVisibility, setColumnVisibility] =
39-
React.useState<VisibilityState>({})
40-
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
41-
[]
42-
)
43-
const [sorting, setSorting] = React.useState<SortingState>([])
36+
export function TasksTable({ data }: DataTableProps) {
37+
// Local UI-only states
38+
const [rowSelection, setRowSelection] = useState({})
39+
const [sorting, setSorting] = useState<SortingState>([])
40+
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
41+
42+
// Synced with URL states
43+
const {
44+
globalFilter,
45+
onGlobalFilterChange,
46+
columnFilters,
47+
onColumnFiltersChange,
48+
pagination,
49+
onPaginationChange,
50+
ensurePageInRange,
51+
} = useTableUrlState({
52+
search: route.useSearch(),
53+
navigate: route.useNavigate(),
54+
pagination: { defaultPage: 1, defaultPageSize: 10 },
55+
globalFilter: { enabled: true, key: 'filter' },
56+
columnFilters: [
57+
{ columnId: 'status', searchKey: 'status', type: 'array' },
58+
{ columnId: 'priority', searchKey: 'priority', type: 'array' },
59+
],
60+
})
4461

4562
const table = useReactTable({
4663
data,
@@ -50,20 +67,36 @@ export function TasksTable<TData, TValue>({
5067
columnVisibility,
5168
rowSelection,
5269
columnFilters,
70+
globalFilter,
71+
pagination,
5372
},
5473
enableRowSelection: true,
5574
onRowSelectionChange: setRowSelection,
5675
onSortingChange: setSorting,
57-
onColumnFiltersChange: setColumnFilters,
5876
onColumnVisibilityChange: setColumnVisibility,
77+
globalFilterFn: (row, _columnId, filterValue) => {
78+
const id = String(row.getValue('id')).toLowerCase()
79+
const title = String(row.getValue('title')).toLowerCase()
80+
const searchValue = String(filterValue).toLowerCase()
81+
82+
return id.includes(searchValue) || title.includes(searchValue)
83+
},
5984
getCoreRowModel: getCoreRowModel(),
6085
getFilteredRowModel: getFilteredRowModel(),
6186
getPaginationRowModel: getPaginationRowModel(),
6287
getSortedRowModel: getSortedRowModel(),
6388
getFacetedRowModel: getFacetedRowModel(),
6489
getFacetedUniqueValues: getFacetedUniqueValues(),
90+
onPaginationChange,
91+
onGlobalFilterChange,
92+
onColumnFiltersChange,
6593
})
6694

95+
const pageCount = table.getPageCount()
96+
useEffect(() => {
97+
ensurePageInRange(pageCount)
98+
}, [pageCount, ensurePageInRange])
99+
67100
return (
68101
<div className='space-y-4 max-sm:has-[div[role="toolbar"]]:mb-16'>
69102
<DataTableToolbar table={table} />

src/features/tasks/data/data.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,51 +27,51 @@ export const labels = [
2727

2828
export const statuses = [
2929
{
30-
value: 'backlog',
3130
label: 'Backlog',
31+
value: 'backlog' as const,
3232
icon: HelpCircle,
3333
},
3434
{
35-
value: 'todo',
3635
label: 'Todo',
36+
value: 'todo' as const,
3737
icon: Circle,
3838
},
3939
{
40-
value: 'in progress',
4140
label: 'In Progress',
41+
value: 'in progress' as const,
4242
icon: Timer,
4343
},
4444
{
45-
value: 'done',
4645
label: 'Done',
46+
value: 'done' as const,
4747
icon: CheckCircle,
4848
},
4949
{
50-
value: 'canceled',
5150
label: 'Canceled',
51+
value: 'canceled' as const,
5252
icon: CircleOff,
5353
},
5454
]
5555

5656
export const priorities = [
5757
{
5858
label: 'Low',
59-
value: 'low',
59+
value: 'low' as const,
6060
icon: ArrowDown,
6161
},
6262
{
6363
label: 'Medium',
64-
value: 'medium',
64+
value: 'medium' as const,
6565
icon: ArrowRight,
6666
},
6767
{
6868
label: 'High',
69-
value: 'high',
69+
value: 'high' as const,
7070
icon: ArrowUp,
7171
},
7272
{
7373
label: 'Critical',
74-
value: 'critical',
74+
value: 'critical' as const,
7575
icon: AlertCircle,
7676
},
7777
]

src/features/tasks/index.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@ import { Main } from '@/components/layout/main'
44
import { ProfileDropdown } from '@/components/profile-dropdown'
55
import { Search } from '@/components/search'
66
import { ThemeSwitch } from '@/components/theme-switch'
7-
import { TasksTable } from './components/data-table'
8-
import { tasksColumns } from './components/tasks-columns'
97
import { TasksDialogs } from './components/tasks-dialogs'
108
import { TasksPrimaryButtons } from './components/tasks-primary-buttons'
119
import { TasksProvider } from './components/tasks-provider'
10+
import { TasksTable } from './components/tasks-table'
1211
import { tasks } from './data/tasks'
1312

1413
export function Tasks() {
@@ -34,7 +33,7 @@ export function Tasks() {
3433
<TasksPrimaryButtons />
3534
</div>
3635
<div className='-mx-4 flex-1 overflow-auto px-4 py-1 lg:flex-row lg:space-y-0 lg:space-x-12'>
37-
<TasksTable data={tasks} columns={tasksColumns} />
36+
<TasksTable data={tasks} />
3837
</div>
3938
</Main>
4039

src/features/users/components/users-table.tsx

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
import { useState } from 'react'
1+
import { useEffect, useState } from 'react'
22
import {
3-
type ColumnDef,
4-
type ColumnFiltersState,
53
type SortingState,
64
type VisibilityState,
75
flexRender,
@@ -14,6 +12,7 @@ import {
1412
useReactTable,
1513
} from '@tanstack/react-table'
1614
import { cn } from '@/lib/utils'
15+
import { type NavigateFn, useTableUrlState } from '@/hooks/use-table-url-state'
1716
import {
1817
Table,
1918
TableBody,
@@ -26,6 +25,7 @@ import { type User } from '../data/schema'
2625
import { DataTableBulkActions } from './data-table-bulk-actions'
2726
import { DataTablePagination } from './data-table-pagination'
2827
import { DataTableToolbar } from './data-table-toolbar'
28+
import { usersColumns as columns } from './users-columns'
2929

3030
declare module '@tanstack/react-table' {
3131
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -35,38 +35,63 @@ declare module '@tanstack/react-table' {
3535
}
3636

3737
type DataTableProps = {
38-
columns: ColumnDef<User>[]
3938
data: User[]
39+
search: Record<string, unknown>
40+
navigate: NavigateFn
4041
}
4142

42-
export function UsersTable({ columns, data }: DataTableProps) {
43+
export function UsersTable({ data, search, navigate }: DataTableProps) {
4344
const [rowSelection, setRowSelection] = useState({})
4445
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
45-
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
4646
const [sorting, setSorting] = useState<SortingState>([])
4747

48+
const {
49+
columnFilters,
50+
onColumnFiltersChange,
51+
pagination,
52+
onPaginationChange,
53+
ensurePageInRange,
54+
} = useTableUrlState({
55+
search,
56+
navigate,
57+
pagination: { defaultPage: 1, defaultPageSize: 10 },
58+
globalFilter: { enabled: false },
59+
columnFilters: [
60+
// username per-column text filter
61+
{ columnId: 'username', searchKey: 'username', type: 'string' },
62+
{ columnId: 'status', searchKey: 'status', type: 'array' },
63+
{ columnId: 'role', searchKey: 'role', type: 'array' },
64+
],
65+
})
66+
4867
const table = useReactTable({
4968
data,
5069
columns,
5170
state: {
5271
sorting,
53-
columnVisibility,
72+
pagination,
5473
rowSelection,
5574
columnFilters,
75+
columnVisibility,
5676
},
5777
enableRowSelection: true,
78+
onPaginationChange,
79+
onColumnFiltersChange,
5880
onRowSelectionChange: setRowSelection,
5981
onSortingChange: setSorting,
60-
onColumnFiltersChange: setColumnFilters,
6182
onColumnVisibilityChange: setColumnVisibility,
83+
getPaginationRowModel: getPaginationRowModel(),
6284
getCoreRowModel: getCoreRowModel(),
6385
getFilteredRowModel: getFilteredRowModel(),
64-
getPaginationRowModel: getPaginationRowModel(),
6586
getSortedRowModel: getSortedRowModel(),
6687
getFacetedRowModel: getFacetedRowModel(),
6788
getFacetedUniqueValues: getFacetedUniqueValues(),
6889
})
6990

91+
useEffect(() => {
92+
ensurePageInRange(table.getPageCount())
93+
}, [table, ensurePageInRange])
94+
7095
return (
7196
<div className='space-y-4 max-sm:has-[div[role="toolbar"]]:mb-16'>
7297
<DataTableToolbar table={table} />

src/features/users/index.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1+
import { getRouteApi } from '@tanstack/react-router'
12
import { Header } from '@/components/layout/header'
23
import { Main } from '@/components/layout/main'
34
import { ProfileDropdown } from '@/components/profile-dropdown'
45
import { Search } from '@/components/search'
56
import { ThemeSwitch } from '@/components/theme-switch'
6-
import { usersColumns } from './components/users-columns'
77
import { UsersDialogs } from './components/users-dialogs'
88
import { UsersPrimaryButtons } from './components/users-primary-buttons'
99
import { UsersProvider } from './components/users-provider'
1010
import { UsersTable } from './components/users-table'
1111
import { userListSchema } from './data/schema'
1212
import { users } from './data/users'
1313

14+
const route = getRouteApi('/_authenticated/users/')
15+
1416
export function Users() {
1517
// Parse user list
1618
const userList = userListSchema.parse(users)
@@ -36,7 +38,11 @@ export function Users() {
3638
<UsersPrimaryButtons />
3739
</div>
3840
<div className='-mx-4 flex-1 overflow-auto px-4 py-1 lg:flex-row lg:space-y-0 lg:space-x-12'>
39-
<UsersTable data={userList} columns={usersColumns} />
41+
<UsersTable
42+
data={userList}
43+
search={route.useSearch()}
44+
navigate={route.useNavigate()}
45+
/>
4046
</div>
4147
</Main>
4248

0 commit comments

Comments
 (0)