import { DEFAULT_PAGE_SIZE, OffsetPagination, PAGE_SIZE_OPTIONS } from "@ivtui/base" import { type ColumnFiltersState, flexRender, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, type SortingState, useReactTable, type VisibilityState, } from "@tanstack/react-table" import clsx from "clsx" import type { JSX } from "react" import { useCallback, useMemo, useState } from "react" import { LoadingOverlay, TableCellComponent, TableHeaderComponent, TableRowComponent, } from "./components" import { DEFAULT_EMPTY_MESSAGE, DEFAULT_SEARCH_PLACEHOLDER } from "./DataTable.const" import type { DataTableProps, DataTableRowAction } from "./DataTable.types" import { Table, TableBody, TableFooter, TableHeader, TableRow } from "./Table" import { TableRowSelection } from "./TableRowSelection" import { TableToolbar } from "./TableToolbar" import { createAllColumns, getRowId, getWidthStyle } from "./utils/tableUtils" export function DataTable, TValue>({ columns, data, searchValue, onSearchChange, searchPlaceholder = DEFAULT_SEARCH_PLACEHOLDER, filters = [], selectedRowActions = [], rowActions = [], pageSize: initialPageSize = DEFAULT_PAGE_SIZE, pageSizeOptions = PAGE_SIZE_OPTIONS, showPageSizeSelector = true, className, emptyMessage = DEFAULT_EMPTY_MESSAGE, tableClassName, headerClassName, rowClassName, cellClassName, footerClassName, footer, // Toolbar visibility control showToolbar, showSearchInput, showFilters, showColumnVisibility, showResetButton, // External reset control onReset, // Row selection control rowSelection: rowSelectionProp, onRowSelectionChange, showBuiltInSelection, // Controlled/server-side props sorting: sortingProp, onSortingChange, columnFilters: columnFiltersProp, onColumnFiltersChange, columnVisibility: columnVisibilityProp, onColumnVisibilityChange, globalFilter: globalFilterProp, onGlobalFilterChange, pageIndex: pageIndexProp, onPageIndexChange, pageSizeControlled, onPageSizeChange, totalItems, isLoading, }: Readonly>): JSX.Element { // State management const [sortingState, setSortingState] = useState([]) const [columnFiltersState, setColumnFiltersState] = useState([]) const [globalFilterState, setGlobalFilterState] = useState("") const [pageIndexState, setPageIndexState] = useState(0) const [pageSizeState, setPageSizeState] = useState(initialPageSize) const [columnVisibilityState, setColumnVisibilityState] = useState({}) const [rowSelectionState, setRowSelectionState] = useState>({}) // Controlled vs uncontrolled state const sorting = sortingProp ?? sortingState const columnFilters = columnFiltersProp ?? columnFiltersState const columnVisibility = columnVisibilityProp ?? columnVisibilityState const globalFilter = globalFilterProp ?? globalFilterState const pageIndex = pageIndexProp ?? pageIndexState const pageSize = pageSizeControlled ?? pageSizeState const rowSelection = rowSelectionProp ?? rowSelectionState const setSorting = onSortingChange ?? setSortingState const setColumnFilters = onColumnFiltersChange ?? setColumnFiltersState const setGlobalFilter = onGlobalFilterChange ?? setGlobalFilterState const setPageIndex = onPageIndexChange ?? setPageIndexState const setPageSize = onPageSizeChange ?? setPageSizeState // Memoized values const allColumns = useMemo( () => createAllColumns(columns, selectedRowActions, rowActions), [columns, selectedRowActions, rowActions], ) // Search behavior logic const hasSearch = onSearchChange !== undefined // Table instance const table = useReactTable({ data, columns: allColumns, onSortingChange: (updater: any) => { const value = typeof updater === "function" ? updater(sorting) : updater setSorting(value) }, onColumnFiltersChange: (updater: any) => { const value = typeof updater === "function" ? updater(columnFilters) : updater setColumnFilters(value) }, onGlobalFilterChange: setGlobalFilter, state: { sorting, columnFilters, globalFilter, columnVisibility, rowSelection, pagination: { pageIndex, pageSize }, }, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), getPaginationRowModel: getPaginationRowModel(), onColumnVisibilityChange: (updater: any) => { const value = typeof updater === "function" ? updater(columnVisibility) : updater if (onColumnVisibilityChange) { onColumnVisibilityChange(value) } else { setColumnVisibilityState(value) } }, onRowSelectionChange: (updater: any) => { const value = typeof updater === "function" ? updater(rowSelection) : updater // Update internal state if not controlled if (!rowSelectionProp) { setRowSelectionState(value) } // Call external callback immediately with correct data if (onRowSelectionChange) { // Use the value directly to calculate selected rows const selectedRowIds = Object.keys(value).filter(id => value[id]) const selectedRows = data.filter((row, index) => { // Get row ID using the same logic as the table let rowId: string if (row.id !== undefined) rowId = String(row.id) else if (row.ID !== undefined) rowId = String(row.ID) else if (row._id !== undefined) rowId = String(row._id) else if (row.key !== undefined) rowId = String(row.key) else rowId = String(index) return selectedRowIds.includes(rowId) }) onRowSelectionChange(value, selectedRows) } }, globalFilterFn: "includesString", getRowId, initialState: { pagination: { pageSize: initialPageSize, }, }, manualPagination: totalItems !== undefined, pageCount: totalItems !== undefined ? Math.ceil(totalItems / pageSize) : undefined, manualSorting: !!sortingProp, manualFiltering: !!columnFiltersProp || !!globalFilterProp, }) const toolbarColumns = table .getAllLeafColumns() .filter(col => col.getCanHide()) .map(col => ({ id: col.id, label: (col.columnDef.header as string) ?? col.id, visible: col.getIsVisible(), toggle: () => col.toggleVisibility(), canHide: col.getCanHide(), })) // Callbacks const handleSearchChange = useCallback( (value: string) => { if (onSearchChange) { onSearchChange(value) } }, [onSearchChange], ) const getSelectedRowsData = useCallback((): TData[] => { const selectedRows = table.getFilteredSelectedRowModel().rows return selectedRows.map(row => row.original) }, [table]) const handleRowActionClick = useCallback( (action: DataTableRowAction) => { return () => action.onClick(getSelectedRowsData()) }, [getSelectedRowsData], ) const handleSelectAll = useCallback(() => { table.toggleAllPageRowsSelected(true) }, [table]) const handleClearSelection = useCallback(() => { table.toggleAllPageRowsSelected(false) }, [table]) const handleReset = useCallback(() => { if (onReset) { // Use external reset handler if provided onReset() } else { // Fallback to internal reset if (onSearchChange) { onSearchChange("") } // Reset filters for (const filter of filters) { if (filter.onValueChange) { filter.onValueChange([]) } } } }, [onReset, onSearchChange, filters]) const renderHeaderContent = useCallback((header: any) => { if (header.isPlaceholder) return null try { return flexRender(header.column.columnDef.header, header.getContext()) as React.ReactNode } catch (error) { console.warn("Error rendering header content:", error) return header.column.id } }, []) const renderDataCell = useCallback( (cell: any) => ( ), [cellClassName], ) const renderDataRow = useCallback( (row: any) => ( ), [rowClassName, renderDataCell], ) const renderHeaderRow = useCallback( (headerGroup: any) => ( {headerGroup.headers.map((header: any) => ( ))} ), [renderHeaderContent], ) // Computed values const selectedRowsCount = table.getFilteredSelectedRowModel().rows.length const totalCount = totalItems ?? table.getFilteredRowModel().rows.length // Built-in selection visibility logic const shouldShowBuiltInSelection = showBuiltInSelection !== false && // Not explicitly disabled selectedRowsCount > 0 && // Has selected rows (showBuiltInSelection === true || !onRowSelectionChange) // Explicitly enabled OR no external selection // Toolbar visibility logic const shouldShowToolbar = showToolbar !== false && (showToolbar === true || (hasSearch && showSearchInput !== false) || (filters.length > 0 && showFilters !== false) || (toolbarColumns.length > 0 && showColumnVisibility !== false) || (showResetButton !== false && (searchValue || filters.some(f => f.value && f.value.length > 0)))) const shouldShowSearch = showSearchInput !== false && hasSearch const shouldShowFilters = showFilters !== false && filters.length > 0 const shouldShowColumnVisibility = showColumnVisibility !== false && toolbarColumns.length > 0 const shouldShowResetButton = showResetButton !== false && (searchValue || filters.some(f => f.value && f.value.length > 0)) return (
{shouldShowToolbar && (
)} {shouldShowBuiltInSelection && (
({ ...action, onClick: handleRowActionClick(action), }))} />
)}
{isLoading ? (
) : table.getRowModel().rows.length === 0 ? (
{emptyMessage}
) : ( {table.getHeaderGroups().map(renderHeaderRow)} {table.getRowModel().rows.map(renderDataRow)} {footer && {footer}}
)}
{table.getRowModel().rows.length > 0 && (
setPageIndex(page - 1)} onPageSizeChange={ showPageSizeSelector ? (pageSize: number) => setPageSize(pageSize) : undefined } pageSizeOptions={pageSizeOptions} />
)}
) }