/** * DataTable — WealthX Design System * * A headless, composable data-table built on \@tanstack/react-table. * Composes existing WealthX shadcn components: * - Table, TableHeader, TableBody, TableRow, TableHead, TableCell * - Input (column filter) * - Button (pagination actions, column toggle) * - Checkbox (row selection) * - Select (page-size selector) * - Pagination (page navigation) * - Skeleton (loading state) * - DropdownMenu (column visibility toggle) * * All colors use design tokens — no hex codes. */ import { type ReactElement } from "react"; import * as React from "react"; import { type ColumnDef, type ColumnFiltersState, type PaginationState, type SortingState, type VisibilityState, type RowSelectionState, type Table as TanstackTable, flexRender, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, useReactTable, } from "@tanstack/react-table"; import { ArrowUpDown, ArrowUp, ArrowDown, SlidersHorizontal, } from "lucide-react"; import { cn } from "@/lib/utils"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { PaginationNavButtons } from "@/components/ui/pagination"; import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Skeleton } from "@/components/ui/skeleton"; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- interface DataTableProps { /** Column definitions — pass ColumnDef[] from \@tanstack/react-table */ columns: ColumnDef[]; /** Row data */ data: TData[]; /** Show a global search/filter input above the table */ searchKey?: string; /** Placeholder for the search input */ searchPlaceholder?: string; /** Enable row selection with checkboxes */ enableRowSelection?: boolean; /** Enable column visibility toggle dropdown */ enableColumnVisibility?: boolean; /** Page size options for the page-size selector */ pageSizeOptions?: number[]; /** Show loading skeleton */ loading?: boolean; /** Number of skeleton rows to show while loading */ skeletonRows?: number; /** Callback when row selection changes */ onRowSelectionChange?: (selection: RowSelectionState) => void; /** Additional className for the root wrapper */ className?: string; /** Render a toolbar above the table (replaces default toolbar) */ toolbar?: (table: TanstackTable) => React.ReactNode; /** Text shown when no results found */ emptyText?: string; /** Enable server-side pagination (disables client-side pagination model) */ manualPagination?: boolean; /** Enable server-side sorting (disables client-side sorting model) */ manualSorting?: boolean; /** Enable server-side filtering (disables client-side filtering model) */ manualFiltering?: boolean; /** Total page count (required for server-side pagination) */ pageCount?: number; /** Total row count (alternative to pageCount for server-side pagination) */ rowCount?: number; /** Controlled sorting state */ sorting?: SortingState; /** Controlled pagination state */ pagination?: PaginationState; /** Controlled column filters state */ columnFilters?: ColumnFiltersState; /** Callback when sorting state changes */ onSortingChange?: (sorting: SortingState) => void; /** Callback when pagination state changes */ onPaginationChange?: (pagination: PaginationState) => void; /** Callback when column filters state changes */ onColumnFiltersChange?: (filters: ColumnFiltersState) => void; } // --------------------------------------------------------------------------- // Helper: sortable header // --------------------------------------------------------------------------- function DataTableColumnHeader({ column, title, className, }: { column: { getIsSorted: () => false | "asc" | "desc"; toggleSorting: (desc?: boolean) => void; getCanSort: () => boolean; }; title: string; className?: string; }): ReactElement { if (!column.getCanSort()) { return {title}; } const sorted = column.getIsSorted(); let sortIcon: ReactElement; if (sorted === "asc") { sortIcon = ; } else if (sorted === "desc") { sortIcon = ; } else { sortIcon = ; } return ( ); } // --------------------------------------------------------------------------- // Helper: selection column // --------------------------------------------------------------------------- function getSelectionColumn(): ColumnDef { return { id: "select", header: ({ table }) => ( { table.toggleAllPageRowsSelected(Boolean(checked)); }} /> ), cell: ({ row }) => ( { row.toggleSelected(Boolean(checked)); }} /> ), enableSorting: false, enableHiding: false, }; } // --------------------------------------------------------------------------- // Sub-components // --------------------------------------------------------------------------- function DataTableToolbar({ table, searchKey, searchPlaceholder, enableColumnVisibility, }: { table: TanstackTable; searchKey?: string; searchPlaceholder?: string; enableColumnVisibility?: boolean; }): ReactElement { return (
{searchKey ? ( table.getColumn(searchKey)?.setFilterValue(e.target.value) } placeholder={searchPlaceholder ?? `Filter ${searchKey}...`} value={table.getColumn(searchKey)?.getFilterValue() as string} /> ) : null}
{enableColumnVisibility ? ( } > Columns Toggle columns {table .getAllColumns() .filter((col) => col.getCanHide()) .map((col) => ( { col.toggleVisibility(Boolean(value)); }} > {col.id} ))} ) : null}
); } function DataTablePagination({ table, pageSizeOptions = [10, 20, 30, 50], }: { table: TanstackTable; pageSizeOptions?: number[]; }): ReactElement { return (
{table.getFilteredSelectedRowModel().rows.length > 0 && ( {table.getFilteredSelectedRowModel().rows.length} of{" "} {table.getFilteredRowModel().rows.length} row(s) selected. )}
Rows per page
Page {table.getState().pagination.pageIndex + 1} of{" "} {table.getPageCount()} table.setPageIndex(0)} onLast={() => table.setPageIndex(table.getPageCount() - 1)} onNext={() => table.nextPage()} onPrev={() => table.previousPage()} />
); } // --------------------------------------------------------------------------- // Loading skeleton // --------------------------------------------------------------------------- function DataTableSkeleton({ columnCount, rowCount = 5, }: { columnCount: number; rowCount?: number; }): ReactElement { return ( <> {Array.from({ length: rowCount }).map((_, rowIdx) => ( {Array.from({ length: columnCount }).map((_inner, colIdx) => ( ))} ))} ); } // --------------------------------------------------------------------------- // Main DataTable // --------------------------------------------------------------------------- function DataTable({ columns: userColumns, data, searchKey, searchPlaceholder, enableRowSelection = false, enableColumnVisibility = false, pageSizeOptions = [10, 20, 30, 50], loading = false, skeletonRows = 5, onRowSelectionChange, className, toolbar, emptyText = "No results.", manualPagination = false, manualSorting = false, manualFiltering = false, pageCount, rowCount, sorting: controlledSorting, pagination: controlledPagination, columnFilters: controlledColumnFilters, onSortingChange: onSortingChangeProp, onPaginationChange: onPaginationChangeProp, onColumnFiltersChange: onColumnFiltersChangeProp, }: DataTableProps): ReactElement { // Internal state (used when not externally controlled) const [internalSorting, setInternalSorting] = React.useState( [], ); const [internalColumnFilters, setInternalColumnFilters] = React.useState([]); const [internalPagination, setInternalPagination] = React.useState({ pageIndex: 0, pageSize: pageSizeOptions[0] ?? 10, }); const [columnVisibility, setColumnVisibility] = React.useState({}); const [rowSelection, setRowSelection] = React.useState({}); // Resolve controlled vs uncontrolled const sorting = controlledSorting ?? internalSorting; const columnFilters = controlledColumnFilters ?? internalColumnFilters; const pagination = controlledPagination ?? internalPagination; // Prepend selection column if enabled const resolvedColumns = React.useMemo(() => { if (!enableRowSelection) return userColumns; return [getSelectionColumn(), ...userColumns] as ColumnDef< TData, TValue >[]; }, [userColumns, enableRowSelection]); const table = useReactTable({ data, columns: resolvedColumns, state: { sorting, columnFilters, columnVisibility, rowSelection, pagination, }, onSortingChange: (updater) => { const next = typeof updater === "function" ? updater(sorting) : updater; if (controlledSorting === undefined) setInternalSorting(next); onSortingChangeProp?.(next); }, onColumnFiltersChange: (updater) => { const next = typeof updater === "function" ? updater(columnFilters) : updater; if (controlledColumnFilters === undefined) setInternalColumnFilters(next); onColumnFiltersChangeProp?.(next); }, onPaginationChange: (updater) => { const next = typeof updater === "function" ? updater(pagination) : updater; if (controlledPagination === undefined) setInternalPagination(next); onPaginationChangeProp?.(next); }, onColumnVisibilityChange: setColumnVisibility, onRowSelectionChange: (updater) => { const next = typeof updater === "function" ? updater(rowSelection) : updater; setRowSelection(next); onRowSelectionChange?.(next); }, getCoreRowModel: getCoreRowModel(), ...(manualFiltering ? { manualFiltering: true as const } : { getFilteredRowModel: getFilteredRowModel() }), ...(manualPagination ? { manualPagination: true as const } : { getPaginationRowModel: getPaginationRowModel() }), ...(manualSorting ? { manualSorting: true as const } : { getSortedRowModel: getSortedRowModel() }), ...(pageCount !== undefined && { pageCount }), ...(rowCount !== undefined && { rowCount }), enableRowSelection, }); function renderTableBody(): ReactElement { if (loading) { return ( ); } if (table.getRowModel().rows.length) { return ( <> {table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} ))} ); } return ( {emptyText} ); } return (
{/* Toolbar */} {toolbar ? toolbar(table) : (searchKey || enableColumnVisibility) && ( )} {/* Table */}
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( {header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext(), )} ))} ))} {renderTableBody()}
{/* Pagination */}
); } export { DataTable, DataTableToolbar, DataTablePagination, DataTableSkeleton, DataTableColumnHeader, getSelectionColumn, }; // Re-export tanstack types for consumer convenience export type { DataTableProps, ColumnDef, PaginationState, SortingState, ColumnFiltersState, VisibilityState, RowSelectionState, };