feat: add server-side table sorting

This commit is contained in:
2026-06-17 10:59:50 +05:30
parent db11a512bf
commit bd0782a55f
28 changed files with 209 additions and 29 deletions

View File

@@ -2,6 +2,8 @@
import { flexRender } from '@tanstack/react-table';
import { TableHead, TableHeader as ShadTableHeader, TableRow } from '@/components/ui/table';
import { Table } from '@tanstack/react-table';
import { ArrowDownWideNarrow, ArrowUpDown, ArrowUpNarrowWide } from 'lucide-react';
import { cn } from '@/lib/utils';
const TableHeader = <TData, _>({ table }: { table: Table<TData> }) => {
return (
@@ -9,14 +11,38 @@ const TableHeader = <TData, _>({ table }: { table: Table<TData> }) => {
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
const canSort = header.column.getCanSort();
const sortDirection = header.column.getIsSorted();
const SortIcon =
sortDirection === 'asc'
? ArrowUpNarrowWide
: sortDirection === 'desc'
? ArrowDownWideNarrow
: ArrowUpDown;
return (
<TableHead
key={header.id}
className="h-10 bg-card/80 px-2 text-foreground backdrop-blur-xl"
onClick={canSort ? header.column.getToggleSortingHandler() : undefined}
className={cn(
'h-10 bg-card/80 px-2 text-foreground backdrop-blur-xl transition-colors',
canSort && 'cursor-pointer select-none hover:bg-muted/60',
sortDirection && 'bg-muted/70',
)}
>
{header.isPlaceholder ? null : (
<div className="flex items-center gap-2">
{flexRender(header.column.columnDef.header, header.getContext())}
<div className="flex min-h-8 w-full items-center justify-between gap-2">
<span className="truncate">
{flexRender(header.column.columnDef.header, header.getContext())}
</span>
{canSort ? (
<SortIcon
className={cn(
'size-3.5 shrink-0 transition-colors',
sortDirection ? 'text-foreground' : 'text-muted-foreground/60',
)}
/>
) : null}
</div>
)}
</TableHead>

View File

@@ -38,6 +38,8 @@ export interface DataTableProps<TData, TValue> {
onPageChange: (newSkip: number) => void;
onLimitChange: (newLimit: number) => void;
};
sorting?: SortingState;
onSortingChange?: (sorting: SortingState) => void;
}
export function DataTable<TData, TValue>({
@@ -53,9 +55,24 @@ export function DataTable<TData, TValue>({
emptyTitle = 'No results found.',
emptyDescription = 'Try adjusting your filters or search terms.',
pagination,
sorting: controlledSorting,
onSortingChange,
}: DataTableProps<TData, TValue>) {
const [rowSelection, setRowSelection] = React.useState({});
const [sorting, setSorting] = React.useState<SortingState>([]);
const [internalSorting, setInternalSorting] = React.useState<SortingState>([]);
const sorting = controlledSorting ?? internalSorting;
const isManualSorting = Boolean(onSortingChange);
const handleSortingChange = React.useCallback(
(updater: SortingState | ((old: SortingState) => SortingState)) => {
const nextSorting = typeof updater === 'function' ? updater(sorting) : updater;
if (onSortingChange) {
onSortingChange(nextSorting);
return;
}
setInternalSorting(nextSorting);
},
[onSortingChange, sorting],
);
const columns = React.useMemo(() => {
const cols: ColumnDef<TData, TValue>[] = [...initialColumns];
@@ -105,10 +122,11 @@ export function DataTable<TData, TValue>({
columns,
getCoreRowModel: getCoreRowModel(),
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
onSortingChange: handleSortingChange,
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
manualPagination: !!pagination,
manualSorting: isManualSorting,
pageCount: pagination?.totalItems ? Math.ceil(pagination.totalItems / pagination.limit) : -1,
state: {
rowSelection,

View File

@@ -0,0 +1,14 @@
import type { SortingState } from '@tanstack/react-table';
export function getServerSortParams(sorting: SortingState) {
const activeSort = sorting[0];
if (!activeSort) {
return {};
}
return {
sort_by: activeSort.id,
sort_order: activeSort.desc ? 'desc' : 'asc',
};
}