Files
krow_workforce_owliver/src/components/shared/DataTable.tsx
2026-09-16 20:11:27 +05:30

197 lines
5.8 KiB
TypeScript

import {
columnFilteringFeature,
columnVisibilityFeature,
createCoreRowModel,
createFilteredRowModel,
createPaginatedRowModel,
createSortedRowModel,
filterFn_includesString,
flexRender,
globalFilteringFeature,
rowPaginationFeature,
rowSortingFeature,
sortFn_alphanumeric,
sortFn_basic,
sortFn_text,
tableFeatures,
useTable,
type ColumnDef,
type RowData,
type SortingState,
} from "@tanstack/react-table"
import { ArrowUpDown, ChevronLeft, ChevronRight } from "lucide-react"
import { useMemo, useState } from "react"
import { Button } from "@/components/ui/button"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { cn } from "@/lib/utils"
/* ===========================================================================
A sortable, filterable, paginated table built on TanStack Table v9.
Horizontal overflow is contained by the <Table> wrapper, so a wide table
never makes the application itself scroll sideways (Section 21).
=========================================================================== */
const features = tableFeatures({
columnFilteringFeature,
columnVisibilityFeature,
globalFilteringFeature,
rowSortingFeature,
rowPaginationFeature,
coreRowModel: createCoreRowModel(),
sortedRowModel: createSortedRowModel(),
filteredRowModel: createFilteredRowModel(),
paginatedRowModel: createPaginatedRowModel(),
filterFns: { includesString: filterFn_includesString },
sortFns: {
alphanumeric: sortFn_alphanumeric,
basic: sortFn_basic,
text: sortFn_text,
},
})
type Features = typeof features
/** Column definition bound to this demo's feature set. */
export type DemoColumnDef<T extends RowData> = ColumnDef<Features, T>
export function DataTable<T extends RowData>({
columns,
data,
globalFilter,
pageSize = 8,
emptyMessage = "Nothing to show.",
onRowClick,
}: {
columns: DemoColumnDef<T>[]
data: T[]
globalFilter?: string
pageSize?: number
emptyMessage?: string
onRowClick?: (row: T) => void
}) {
const [sorting, setSorting] = useState<SortingState>([])
// Stable identities: a fresh `state`/`initialState` object on every render
// makes the table store notify, re-render, and notify again.
const state = useMemo(
() => ({ sorting, globalFilter: globalFilter ?? "" }),
[sorting, globalFilter]
)
const initialState = useMemo(
() => ({ pagination: { pageIndex: 0, pageSize } }),
[pageSize]
)
const table = useTable({
features,
data,
columns,
state,
onSortingChange: setSorting,
initialState,
globalFilterFn: "includesString",
})
const rows = table.getRowModel().rows
const pageCount = table.getPageCount()
const pageIndex = table.state.pagination?.pageIndex ?? 0
return (
<div>
<Table>
<TableHeader>
{table.getHeaderGroups().map((hg) => (
<TableRow key={hg.id} className="hover:bg-surface-soft">
{hg.headers.map((header) => {
const sortable = header.column.getCanSort()
return (
<TableHead key={header.id}>
{header.isPlaceholder ? null : sortable ? (
<button
type="button"
onClick={header.column.getToggleSortingHandler()}
className="inline-flex items-center gap-1 transition-colors hover:text-ink-2"
>
{flexRender(header.column.columnDef.header, header.getContext())}
<ArrowUpDown
className={cn(
"size-3",
header.column.getIsSorted() ? "text-primary" : "text-ink-3/60"
)}
/>
</button>
) : (
flexRender(header.column.columnDef.header, header.getContext())
)}
</TableHead>
)
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{rows.length === 0 ? (
<TableRow>
<TableCell colSpan={columns.length} className="py-8 text-center text-ink-3">
{emptyMessage}
</TableCell>
</TableRow>
) : (
rows.map((row) => (
<TableRow
key={row.id}
onClick={onRowClick ? () => onRowClick(row.original) : undefined}
className={cn(onRowClick && "cursor-pointer")}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
)}
</TableBody>
</Table>
{pageCount > 1 && (
<div className="flex items-center justify-between gap-3 border-t border-line-soft px-3 py-2.5">
<p className="text-[12px] text-ink-3">
Page {pageIndex + 1} of {pageCount} ·{" "}
{table.getFilteredRowModel().rows.length} records
</p>
<div className="flex gap-1.5">
<Button
size="icon-sm"
variant="outline"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
aria-label="Previous page"
>
<ChevronLeft />
</Button>
<Button
size="icon-sm"
variant="outline"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
aria-label="Next page"
>
<ChevronRight />
</Button>
</div>
</div>
)}
</div>
)
}