"use client" import * as React from "react" import { flexRender, getCoreRowModel, getPaginationRowModel, getSortedRowModel, useReactTable, type Cell, type ColumnDef, type Header, type OnChangeFn, type Row, type RowSelectionState, type SortingState, type Table as TanStackTable, type VisibilityState, type ExpandedState, type ColumnPinningState, getExpandedRowModel, } from "@tanstack/react-table" import { useVirtualizer, type VirtualItem } from "@tanstack/react-virtual" import { createDataTableActionsColumn } from "@/components/data-table/data-table-actions-column" import { DataTableBulkActions, type DataTableBulkAction } from "@/components/data-table/data-table-bulk-actions" import { DataTableColumnVisibilityMenu } from "@/components/data-table/data-table-column-visibility-menu" import { DataTablePagination, type DataTablePaginationProps } from "@/components/data-table/data-table-pagination" import { type DataTableRowAction } from "@/components/data-table/data-table-row-actions" import { DataTableToolbar, type DataTableToolbarProps } from "@/components/data-table/data-table-toolbar" import { DataState, type DataStateProps } from "@/components/display/data-state" import { StateView, type StateViewProps } from "@/components/feedback/state-view" import { Button } from "@/components/ui/button" import { Input, type InputSearchProps } from "@/components/ui/input" import { useIsMobile } from "@/hooks/use-is-mobile" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table" import { cn } from "@/lib/utils" export type DataTableDensity = "compact" | "default" | "comfortable" export type DataTableLoadingVariant = "skeleton" | "state" export type DataTableMobileLayout = "auto" | "cards" | "scroll" export type DataTableVirtualizationRange = { startIndex: number endIndex: number } export type DataTableVirtualizationConfig = { enabled?: boolean height?: number | string estimateRowHeight?: number overscan?: number measureRows?: boolean onRangeChange?: (range: DataTableVirtualizationRange) => void } export type DataTableFeatureConfig = { search?: boolean columnVisibility?: boolean rowActions?: boolean bulkActions?: boolean refresh?: boolean export?: boolean } export type DataTableSearchConfig = Pick< InputSearchProps, | "value" | "onValueChange" | "placeholder" | "inputClassName" | "disabled" | "clearable" | "clearLabel" | "searchIcon" > & { className?: string wrapperClassName?: string } export type DataTableActionContext = { table: TanStackTable data: TData[] selectedRows: TData[] } export type DataTablePaginationConfig = Pick< DataTablePaginationProps, | "pageIndex" | "pageSize" | "pageCount" | "rowCount" | "pageSizeOptions" | "labels" | "showPageSize" > & { manual?: boolean hidden?: boolean onPageChange?: (pageIndex: number) => void onPageSizeChange?: (pageSize: number) => void } export type DataTableProps = Omit< React.ComponentProps<"div">, "children" > & { columns: ColumnDef[] data: TData[] title?: React.ReactNode description?: React.ReactNode features?: DataTableFeatureConfig search?: DataTableSearchConfig filters?: React.ReactNode | ((context: DataTableActionContext) => React.ReactNode) summary?: React.ReactNode | ((context: DataTableActionContext) => React.ReactNode) toolbarActions?: React.ReactNode | ((context: DataTableActionContext) => React.ReactNode) rowActions?: (row: Row, original: TData) => DataTableRowAction[] bulkActions?: DataTableBulkAction[] onRefresh?: (context: DataTableActionContext) => void onExport?: (context: DataTableActionContext) => void refreshLabel?: React.ReactNode exportLabel?: React.ReactNode getRowId?: (originalRow: TData, index: number, parent?: Row) => string isLoading?: boolean isError?: boolean emptyState?: Omit errorState?: Omit loadingState?: Omit loadingVariant?: DataTableLoadingVariant toolbar?: React.ReactNode | ((table: TanStackTable) => React.ReactNode) toolbarProps?: DataTableToolbarProps | ((table: TanStackTable) => DataTableToolbarProps) pagination?: DataTablePaginationConfig | false sorting?: SortingState onSortingChange?: OnChangeFn columnVisibility?: VisibilityState onColumnVisibilityChange?: OnChangeFn rowSelection?: RowSelectionState onRowSelectionChange?: OnChangeFn enableRowSelection?: boolean | ((row: Row) => boolean) renderMobileCard?: (row: Row) => React.ReactNode mobileLayout?: DataTableMobileLayout onRowClick?: (row: Row) => void onRowDoubleClick?: (row: Row) => void getRowDisabled?: (row: Row) => boolean density?: DataTableDensity striped?: boolean bordered?: boolean stickyHeader?: boolean skeletonRows?: number skeletonCellClassName?: string cellFallback?: React.ReactNode tableClassName?: string tableWrapperClassName?: string headerCellClassName?: string | ((header: Header) => string) cellClassName?: string | ((cell: Cell) => string) rowClassName?: string | ((row: Row) => string) renderExpandedRow?: (row: Row) => React.ReactNode expanded?: ExpandedState onExpandedChange?: OnChangeFn getRowCanExpand?: (row: Row) => boolean columnPinning?: ColumnPinningState onColumnPinningChange?: OnChangeFn virtualization?: DataTableVirtualizationConfig | false } const densityHeadClassName: Record = { compact: "h-8 px-2 py-1.5", default: "h-10 px-2 py-2", comfortable: "h-12 px-3 py-3", } const densityCellClassName: Record = { compact: "px-2 py-1.5", default: "p-2", comfortable: "px-3 py-3", } function getRowClassName( row: Row, rowClassName?: string | ((row: Row) => string) ) { return typeof rowClassName === "function" ? rowClassName(row) : rowClassName } function getHeaderCellClassName( header: Header, headerCellClassName?: string | ((header: Header) => string) ) { return typeof headerCellClassName === "function" ? headerCellClassName(header) : headerCellClassName } function getCellClassName( cell: Cell, cellClassName?: string | ((cell: Cell) => string) ) { return typeof cellClassName === "function" ? cellClassName(cell) : cellClassName } function isEmptyCellContent(content: React.ReactNode) { return content === null || content === undefined || content === "" } function getMobileCellLabel(cell: Cell) { const header = cell.column.columnDef.header if (typeof header === "string") return header if (typeof header === "number") return String(header) return cell.column.id === "actions" ? "Actions" : cell.column.id } function DataTable({ className, columns, data, title, description, features, search, filters, summary, toolbarActions, rowActions, bulkActions, onRefresh, onExport, refreshLabel = "Refresh", exportLabel = "Export", getRowId, isLoading = false, isError = false, emptyState, errorState, loadingState, loadingVariant = "skeleton", toolbar, toolbarProps, pagination, sorting, onSortingChange, columnVisibility, onColumnVisibilityChange, rowSelection, onRowSelectionChange, enableRowSelection, renderMobileCard, mobileLayout = "auto", onRowClick, onRowDoubleClick, getRowDisabled, density = "default", striped = false, bordered = false, stickyHeader = false, skeletonRows = 6, skeletonCellClassName, cellFallback = "-", tableClassName, tableWrapperClassName, headerCellClassName, cellClassName, rowClassName, renderExpandedRow, expanded, onExpandedChange, getRowCanExpand, columnPinning, onColumnPinningChange, virtualization, ...props }: DataTableProps) { const resolvedColumns = React.useMemo[]>(() => { const baseColumns = columns as ColumnDef[] if (!rowActions || features?.rowActions === false) return baseColumns return [ ...baseColumns, createDataTableActionsColumn({ getActions: rowActions, }) as ColumnDef, ] }, [columns, features?.rowActions, rowActions]) const paginationConfig = pagination === false ? undefined : pagination const [internalSorting, setInternalSorting] = React.useState([]) const [internalColumnVisibility, setInternalColumnVisibility] = React.useState({}) const [internalRowSelection, setInternalRowSelection] = React.useState({}) const [internalExpanded, setInternalExpanded] = React.useState({}) const [internalColumnPinning, setInternalColumnPinning] = React.useState({}) const resolvedSorting = sorting ?? internalSorting const resolvedColumnVisibility = columnVisibility ?? internalColumnVisibility const resolvedRowSelection = rowSelection ?? internalRowSelection const resolvedExpanded = expanded ?? internalExpanded const resolvedColumnPinning = columnPinning ?? internalColumnPinning const controlledPagination = paginationConfig ? { pageIndex: paginationConfig.pageIndex, pageSize: paginationConfig.pageSize, } : undefined const manualPagination = Boolean(paginationConfig && paginationConfig.manual !== false) // TanStack Table returns imperative helpers that React Compiler flags by design. // eslint-disable-next-line react-hooks/incompatible-library const table = useReactTable({ data, columns: resolvedColumns, getRowId, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), getPaginationRowModel: paginationConfig && !manualPagination ? getPaginationRowModel() : undefined, manualPagination, pageCount: paginationConfig?.pageCount, state: { sorting: resolvedSorting, columnVisibility: resolvedColumnVisibility, rowSelection: resolvedRowSelection, pagination: controlledPagination, expanded: resolvedExpanded, columnPinning: resolvedColumnPinning, }, onSortingChange: onSortingChange ?? setInternalSorting, onColumnVisibilityChange: onColumnVisibilityChange ?? setInternalColumnVisibility, onRowSelectionChange: onRowSelectionChange ?? setInternalRowSelection, onExpandedChange: onExpandedChange ?? setInternalExpanded, onColumnPinningChange: onColumnPinningChange ?? setInternalColumnPinning, enableRowSelection, getRowCanExpand, getExpandedRowModel: getExpandedRowModel(), }) const rows = table.getRowModel().rows const tableScrollRef = React.useRef(null) const virtualizationConfig = virtualization === false ? undefined : virtualization const virtualizationEnabled = Boolean( virtualizationConfig && virtualizationConfig.enabled !== false && rows.length > 0 ) const virtualizationHeight = virtualizationConfig?.height ?? 480 const onVirtualRangeChange = virtualizationConfig?.onRangeChange const rowVirtualizer = useVirtualizer({ count: virtualizationEnabled ? rows.length : 0, getScrollElement: () => tableScrollRef.current, estimateSize: () => virtualizationConfig?.estimateRowHeight ?? 48, getItemKey: (index) => rows[index]?.id ?? index, overscan: virtualizationConfig?.overscan ?? 8, initialRect: { width: 0, height: typeof virtualizationHeight === "number" ? virtualizationHeight : 480, }, onChange(instance) { if (!onVirtualRangeChange) return const virtualItems = instance.getVirtualItems() onVirtualRangeChange({ startIndex: virtualItems[0]?.index ?? -1, endIndex: virtualItems.at(-1)?.index ?? -1, }) }, }) const virtualRows = virtualizationEnabled ? rowVirtualizer.getVirtualItems() : [] const estimatedRowHeight = virtualizationConfig?.estimateRowHeight ?? 48 const initialVirtualRowCount = virtualizationEnabled ? Math.min( rows.length, Math.ceil( (typeof virtualizationHeight === "number" ? virtualizationHeight : 480) / estimatedRowHeight ) + (virtualizationConfig?.overscan ?? 8) ) : 0 const renderedVirtualRows = virtualRows.length > 0 ? virtualRows : Array.from({ length: initialVirtualRowCount }, (_, index): VirtualItem => ({ key: rows[index]?.id ?? index, index, start: index * estimatedRowHeight, end: (index + 1) * estimatedRowHeight, size: estimatedRowHeight, lane: 0, })) const virtualPaddingTop = renderedVirtualRows[0]?.start ?? 0 const virtualPaddingBottom = renderedVirtualRows.length ? Math.max( rowVirtualizer.getTotalSize() - renderedVirtualRows[renderedVirtualRows.length - 1].end, 0 ) : 0 const selectedRows = table.getSelectedRowModel().rows.map((row) => row.original) const selectedRowCount = selectedRows.length const actionContext = React.useMemo>( () => ({ table, data, selectedRows }), [data, selectedRows, table] ) const visibleColumns = table.getVisibleLeafColumns() const visibleColumnCount = Math.max(visibleColumns.length, 1) const resolvedToolbar = typeof toolbar === "function" ? toolbar(table) : toolbar const resolvedToolbarProps = typeof toolbarProps === "function" ? toolbarProps(table) : toolbarProps const resolvedFilters = typeof filters === "function" ? filters(actionContext) : filters const resolvedSummary = typeof summary === "function" ? summary(actionContext) : summary const shouldShowSearch = Boolean(search && features?.search !== false) const shouldShowColumnVisibility = Boolean(features?.columnVisibility && table.getAllLeafColumns().some((column) => column.getCanHide())) const shouldShowRefresh = Boolean(features?.refresh && onRefresh) const shouldShowExport = Boolean(features?.export && onExport) const shouldShowBulkActions = Boolean(features?.bulkActions !== false && bulkActions?.length) const hasPrimaryToolbarContent = Boolean( title || description || search || resolvedFilters || resolvedSummary || toolbarActions || shouldShowRefresh || shouldShowExport ) const shouldShowColumnVisibilityInToolbar = shouldShowColumnVisibility && hasPrimaryToolbarContent const defaultSearch = shouldShowSearch && search ? ( ) : undefined const defaultActions = ( <> {typeof toolbarActions === "function" ? toolbarActions(actionContext) : toolbarActions} {shouldShowColumnVisibilityInToolbar && } {shouldShowRefresh && ( )} {shouldShowExport && ( )} ) const defaultSelectionActions = shouldShowBulkActions ? ( "Actions"} clearLabel={null} // Toolbar often has its own clear or we don't need double clear onClearSelection={() => table.resetRowSelection()} hideWhenEmpty={false} /> ) : undefined const hasDefaultToolbarContent = Boolean( title || description || defaultSearch || resolvedFilters || resolvedSummary || toolbarActions || shouldShowColumnVisibilityInToolbar || shouldShowRefresh || shouldShowExport || defaultSelectionActions ) const hasToolbar = Boolean(resolvedToolbar || resolvedToolbarProps || hasDefaultToolbarContent) const showPagination = Boolean(paginationConfig && !paginationConfig.hidden) const shouldRenderSkeleton = isLoading && loadingVariant === "skeleton" const isMobileViewport = useIsMobile() const shouldUseMobileCards = mobileLayout !== "scroll" && isMobileViewport const renderVirtualSpacer = (key: string, height: number) => height > 0 ? ( ) : null const renderStateRow = (children: React.ReactNode) => ( {children} ) const renderSkeletonRows = () => Array.from({ length: Math.max(skeletonRows, 1) }, (_, rowIndex) => (