"use client"; import * as React from "react"; import { type ColumnDef, type OnChangeFn, type PaginationState, type RowSelectionState, type Table as TableInstance, type VisibilityState, flexRender, getCoreRowModel, getPaginationRowModel, useReactTable, } from "@tanstack/react-table"; import { Columns3 } from "lucide-react"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; export interface DataTableProps { columns: ColumnDef[]; data: TData[]; /** Stable row identity; Media derives it from `path` so selection survives paging. */ getRowId?: (row: TData, index: number) => string; /** Visibility-only feature set (no sorting, no resizing — locked, design §3.3). */ enableRowSelection?: boolean; rowSelection?: RowSelectionState; onRowSelectionChange?: OnChangeFn; onRowClick?: (row: TData) => void; columnVisibility?: VisibilityState; onColumnVisibilityChange?: OnChangeFn; enableColumnVisibilityToggle?: boolean; /** Pagination (Media only; FileBrowser does not paginate). */ enablePagination?: boolean; manualPagination?: boolean; pagination?: PaginationState; onPaginationChange?: OnChangeFn; pageSizeOptions?: number[]; /** Server total for Media (manual pagination). */ rowCount?: number; emptyMessage?: string; } /** * Reusable TanStack Table wrapper built on the shadcn `Table` primitive. * * Visibility-only feature scope (locked, design §3): pagination, row selection, * row click, column visibility. A sorting row model is deliberately never * wired and column resizing/sizing is never enabled — both are explicit * non-goals. */ export function DataTable({ columns, data, getRowId, enableRowSelection = false, rowSelection, onRowSelectionChange, onRowClick, columnVisibility, onColumnVisibilityChange, enableColumnVisibilityToggle = false, enablePagination = false, manualPagination = false, pagination, onPaginationChange, pageSizeOptions = [10, 20, 30, 50], rowCount, emptyMessage = "No results.", }: DataTableProps) { const pageSize = pagination?.pageSize ?? pageSizeOptions[0] ?? 10; // Selection column is a *display* column (no accessor); only rendered when // the consumer opts in. Its checkbox handlers stopPropagation so toggling a // row never also fires onRowClick navigation. const tableColumns = React.useMemo[]>(() => { if (!enableRowSelection) return columns; const selectColumn: ColumnDef = { id: "__select__", enableSorting: false, header: ({ table }) => ( table.toggleAllPageRowsSelected(!!value)} onClick={(e) => e.stopPropagation()} /> ), cell: ({ row }) => ( row.toggleSelected(!!value)} onClick={(e) => e.stopPropagation()} /> ), enableHiding: false, }; return [selectColumn as ColumnDef, ...columns]; }, [columns, enableRowSelection]); /* eslint-disable react-hooks/incompatible-library -- TanStack's useReactTable intentionally returns non-memoizable updater fns (controlled state). */ const table = useReactTable({ data, columns: tableColumns, getRowId, enableRowSelection, onRowSelectionChange, onColumnVisibilityChange, manualPagination: enablePagination ? manualPagination : false, rowCount: enablePagination && manualPagination ? rowCount : undefined, getCoreRowModel: getCoreRowModel(), // Client pagination model ONLY when paginating locally (FileBrowser does // not paginate; Media drives the page from the server via limit/offset). getPaginationRowModel: enablePagination && !manualPagination ? getPaginationRowModel() : undefined, state: { ...(rowSelection !== undefined ? { rowSelection } : {}), ...(columnVisibility !== undefined ? { columnVisibility } : {}), ...(enablePagination ? { pagination: pagination ?? { pageIndex: 0, pageSize } } : {}), }, onPaginationChange, // Visibility-only: deliberately NO sorting model / sorting state. }); const pageCount = enablePagination && rowCount !== undefined && pageSize > 0 ? Math.max(1, Math.ceil(rowCount / pageSize)) : table.getPageCount(); return (
{enableColumnVisibilityToggle && ( Toggle columns {table .getAllColumns() .filter((column) => column.getCanHide()) .map((column) => { return ( column.toggleVisibility(!!value) } onSelect={(e) => e.preventDefault()} > {column.id} ); })} )}
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( {header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext(), )} ))} ))} {table.getRowModel().rows.length ? ( table.getRowModel().rows.map((row) => ( onRowClick?.(row.original)} > {row.getVisibleCells().map((cell) => ( {flexRender( cell.column.columnDef.cell, cell.getContext(), )} ))} )) ) : ( {emptyMessage} )}
{enablePagination && ( )}
); } interface PaginationProps { table: TableInstance; pageSizeOptions: number[]; pageCount: number; manual: boolean; rowCount?: number; } function DataTablePagination({ table, pageSizeOptions, pageCount, manual, rowCount, }: PaginationProps) { const pageIndex = table.getState().pagination.pageIndex; const pageSize = table.getState().pagination.pageSize; const visibleRows = table.getRowModel().rows.length; const totalRows = manual ? (rowCount ?? 0) : visibleRows; return (
{`${totalRows} row${totalRows === 1 ? "" : "s"}`}
Rows per page
Page {pageIndex + 1} of {pageCount}
); }