"use client" import * as React from "react" import { rankItem } from "@tanstack/match-sorter-utils" import { useReactTable, getCoreRowModel, getSortedRowModel, getFilteredRowModel, getPaginationRowModel, ColumnDef, SortingState, ColumnFiltersState, flexRender, PaginationState, OnChangeFn, ColumnOrderState, VisibilityState, ColumnPinningState, FilterFn, Table, RowSelectionState, Row, } from "@tanstack/react-table" import { useVirtualizer } from "@tanstack/react-virtual" import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, DragEndEvent, } from "@dnd-kit/core" import { arrayMove, SortableContext, horizontalListSortingStrategy, } from "@dnd-kit/sortable" import { cn } from "@/lib/utils" import { ClientTableToolbar } from "./client-table-toolbar" import { exportToExcel } from "./export-utils" import { ClientDataTablePagination } from "@/components/client-data-table/data-table-pagination" import { ClientTableColumnHeader } from "./client-table-column-header" import { ClientTableViewOptions } from "./client-table-view-options" export interface ClientVirtualTableProps { data: TData[] columns: ColumnDef[] height?: string | number className?: string actions?: React.ReactNode enableExport?: boolean onExport?: (data: TData[]) => void // Pagination Props enablePagination?: boolean manualPagination?: boolean pageCount?: number rowCount?: number pagination?: PaginationState onPaginationChange?: OnChangeFn // Style Props getRowClassName?: (originalRow: TData, index: number) => string // Table Meta meta?: any // Row ID getRowId?: (originalRow: TData, index: number, parent?: any) => string // Selection Props enableRowSelection?: boolean | ((row: Row) => boolean) enableMultiRowSelection?: boolean | ((row: Row) => boolean) rowSelection?: RowSelectionState onRowSelectionChange?: OnChangeFn } function ClientVirtualTableInner( { data, columns, height = "500px", // Default height className, actions, enableExport = true, onExport, // Pagination defaults enablePagination = false, manualPagination = false, pageCount, rowCount, pagination: controlledPagination, onPaginationChange, // Style defaults getRowClassName, // Meta & RowID meta, getRowId, // Selection defaults enableRowSelection, enableMultiRowSelection, rowSelection: controlledRowSelection, onRowSelectionChange, }: ClientVirtualTableProps, ref: React.Ref> ) { // State const [sorting, setSorting] = React.useState([]) const [columnFilters, setColumnFilters] = React.useState([]) const [globalFilter, setGlobalFilter] = React.useState("") const [columnVisibility, setColumnVisibility] = React.useState({}) const [columnPinning, setColumnPinning] = React.useState({ left: [], right: [] }) const [columnOrder, setColumnOrder] = React.useState( () => columns.map((c) => c.id || (c as any).accessorKey) as string[] ) // Internal Pagination State const [internalPagination, setInternalPagination] = React.useState({ pageIndex: 0, pageSize: 50, }) // Internal Row Selection State const [internalRowSelection, setInternalRowSelection] = React.useState({}) // Fuzzy Filter const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { const itemRank = rankItem(row.getValue(columnId), value) addMeta({ itemRank }) return itemRank.passed } // Combine controlled and uncontrolled states const pagination = controlledPagination ?? internalPagination const setPagination = onPaginationChange ?? setInternalPagination const rowSelection = controlledRowSelection ?? internalRowSelection const setRowSelection = onRowSelectionChange ?? setInternalRowSelection // Table Instance const table = useReactTable({ data, columns, state: { sorting, columnFilters, globalFilter, pagination, columnVisibility, columnPinning, columnOrder, rowSelection, }, manualPagination, pageCount: manualPagination ? pageCount : undefined, rowCount: manualPagination ? rowCount : undefined, onSortingChange: setSorting, onColumnFiltersChange: setColumnFilters, onGlobalFilterChange: setGlobalFilter, onPaginationChange: setPagination, onColumnVisibilityChange: setColumnVisibility, onColumnPinningChange: setColumnPinning, onColumnOrderChange: setColumnOrder, onRowSelectionChange: setRowSelection, enableRowSelection, enableMultiRowSelection, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), getPaginationRowModel: enablePagination ? getPaginationRowModel() : undefined, columnResizeMode: "onChange", filterFns: { fuzzy: fuzzyFilter, }, globalFilterFn: fuzzyFilter, meta, getRowId, }) // Expose table instance via ref React.useImperativeHandle(ref, () => table, [table]) // DnD Sensors const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8, // 8px movement required to start drag }, }), useSensor(KeyboardSensor) ) // Handle Drag End const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event if (active && over && active.id !== over.id) { setColumnOrder((items) => { const oldIndex = items.indexOf(active.id as string) const newIndex = items.indexOf(over.id as string) return arrayMove(items, oldIndex, newIndex) }) } } // Virtualization const tableContainerRef = React.useRef(null) const { rows } = table.getRowModel() const rowVirtualizer = useVirtualizer({ count: rows.length, getScrollElement: () => tableContainerRef.current, estimateSize: () => 40, // Estimated row height overscan: 10, }) const virtualRows = rowVirtualizer.getVirtualItems() const totalSize = rowVirtualizer.getTotalSize() const paddingTop = virtualRows.length > 0 ? virtualRows?.[0]?.start || 0 : 0 const paddingBottom = virtualRows.length > 0 ? totalSize - (virtualRows?.[virtualRows.length - 1]?.end || 0) : 0 // Export Handler const handleExport = async () => { if (onExport) { onExport(data) return } const currentData = table.getFilteredRowModel().rows.map((row) => row.original) await exportToExcel(currentData, columns, `export-${new Date().toISOString().slice(0,10)}.xlsx`) } return (
{actions} } />
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( ))} ))} {paddingTop > 0 && ( )} {virtualRows.map((virtualRow) => { const row = rows[virtualRow.index] return ( {row.getVisibleCells().map((cell) => { // Handle pinned cells const isPinned = cell.column.getIsPinned() const style: React.CSSProperties = { width: cell.column.getSize(), } if (isPinned === "left") { style.position = "sticky" style.left = `${cell.column.getStart("left")}px` style.zIndex = 10 style.backgroundColor = "var(--background)" // Ensure opacity } else if (isPinned === "right") { style.position = "sticky" style.right = `${cell.column.getAfter("right")}px` style.zIndex = 10 style.backgroundColor = "var(--background)" } return ( ) })} ) })} {paddingBottom > 0 && ( )}
{flexRender( cell.column.columnDef.cell, cell.getContext() )}
{enablePagination && ( )}
) } export const ClientVirtualTable = React.memo( React.forwardRef(ClientVirtualTableInner) ) as ( props: ClientVirtualTableProps & { ref?: React.Ref> } ) => React.ReactElement