import { flexRender, getCoreRowModel, getExpandedRowModel, getSortedRowModel, useReactTable, type ColumnDef, type Header, type Row, type Table, type TableOptions, } from "@tanstack/react-table"; import { useRef, type MouseEvent, type MutableRefObject, type ReactNode, } from "react"; import { applyShiftRangeSelection, type SelectionAnchor, } from "./tableSelection"; type AppColumnMeta = { headerClassName?: string; cellClassName?: string | ((row: Row) => string | undefined); }; declare module "@tanstack/react-table" { interface ColumnMeta extends AppColumnMeta { readonly __valueType?: TValue; } } type UseAppTableOptions = Omit< TableOptions, "getCoreRowModel" > & { withSorting?: boolean; withExpanded?: boolean; }; export function useAppTable(options: UseAppTableOptions) { const { withSorting, withExpanded, ...tableOptions } = options; return useReactTable({ ...tableOptions, getCoreRowModel: getCoreRowModel(), ...(withSorting ? { getSortedRowModel: getSortedRowModel() } : {}), ...(withExpanded ? { getExpandedRowModel: getExpandedRowModel() } : {}), }); } export function useSelectionAnchor(): MutableRefObject { return useRef(null); } export function makeSelectionColumn( anchorRef: MutableRefObject, ): ColumnDef { return { id: "select", size: 32, enableSorting: false, header: ({ table }) => ( ), cell: ({ row, table }) => ( ), }; } function SelectionCheckbox({ row, table, anchorRef, }: { row: Row; table: Table; anchorRef: MutableRefObject; }) { const rangeHandledRef = useRef(false); return ( { event.stopPropagation(); rangeHandledRef.current = applyShiftRangeSelection( event, row, table, anchorRef, ); }} onChange={(event) => { if (rangeHandledRef.current) { rangeHandledRef.current = false; return; } row.getToggleSelectedHandler()(event); }} /> ); } export function AppDataTable({ table, className = "table table-sm", wrapperClassName = "overflow-x-auto", empty, isLoading, loading, getRowClassName, getRowProps, getCellClassName, fixedLayout, stickyHeader, }: { table: Table; className?: string; wrapperClassName?: string; empty?: ReactNode; isLoading?: boolean; loading?: ReactNode; getRowClassName?: (row: Row) => string | undefined; getRowProps?: (row: Row) => { onClick?: (event: MouseEvent) => void; className?: string; }; getCellClassName?: (row: Row, columnId: string) => string | undefined; fixedLayout?: boolean; stickyHeader?: boolean; }) { if (isLoading && loading) return <>{loading}; if (table.getRowModel().rows.length === 0 && empty) return <>{empty}; return (
{fixedLayout ? ( {table.getVisibleLeafColumns().map((column) => ( ))} ) : null} {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( ))} ))} {table.getRowModel().rows.map((row) => { const rowProps = getRowProps?.(row); return ( {row.getVisibleCells().map((cell) => { const rawMeta: unknown = cell.column.columnDef.meta; const meta = isAppColumnMeta(rawMeta) ? rawMeta : undefined; const metaClass = meta?.cellClassName; return ( ); })} ); })}
{flexRender( cell.column.columnDef.cell, cell.getContext(), )}
); } function HeaderCell({ header, fixedLayout, stickyHeader, }: { header: Header; fixedLayout?: boolean; stickyHeader?: boolean; }) { const rawMeta: unknown = header.column.columnDef.meta; const meta = isAppColumnMeta(rawMeta) ? rawMeta : undefined; return ( {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} ); } function isAppColumnMeta(value: unknown): value is AppColumnMeta { return typeof value === "object" && value !== null; }