import React, { useState, useMemo } from 'react'; import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '../Table/Table'; import { Checkbox } from '../Checkbox/Checkbox'; import { Pagination } from '../Pagination/Pagination'; import { Search } from '../Search/Search'; import { Stack } from '../Stack/Stack'; import { Button } from '../Button/Button'; import { Text } from '../Text/Text'; import { Icon } from '../Icon/Icon'; import { Card } from '../Card/Card'; import { Dropdown, DropdownTrigger, DropdownContent } from '../Dropdown/Dropdown'; import { useTheme } from '../../core'; import { SortAscendingIcon, SortDescendingIcon, SelectorIcon, LayoutIcon } from '../../icons'; // Column definition export interface ColumnDef { accessorKey: keyof T; header: string; cell?: (value: T[keyof T], row: T) => React.ReactNode; enableSorting?: boolean; enableHiding?: boolean; } // Main component props export interface DataTableProps { data: T[]; columns: ColumnDef[]; pageSize?: number; enableFiltering?: boolean; enableSorting?: boolean; enableSelection?: boolean; actions?: (selectedItems: T[], clearSelection: () => void) => React.ReactNode; className?: string; } export const DataTable = ({ data, columns, pageSize = 10, enableFiltering = true, enableSorting = true, enableSelection = true, actions, className = '', }: DataTableProps) => { const { theme } = useTheme(); const [filter, setFilter] = useState(''); const [sortConfig, setSortConfig] = useState<{ key: keyof T; direction: 'asc' | 'desc' } | null>(null); const [currentPage, setCurrentPage] = useState(1); const [selection, setSelection] = useState>(new Set()); const [columnVisibility, setColumnVisibility] = useState>(() => { const visibility: Record = {}; columns.forEach(col => { visibility[col.accessorKey as string] = true; }); return visibility; }); const visibleColumns = useMemo(() => { return columns.filter(col => columnVisibility[col.accessorKey as string]); }, [columns, columnVisibility]); // 1. Filtering const filteredData = useMemo(() => { if (!filter) return data; return data.filter(row => columns.some(col => { const value = row[col.accessorKey]; return String(value).toLowerCase().includes(filter.toLowerCase()); }) ); }, [data, columns, filter]); // 2. Sorting const sortedData = useMemo(() => { if (!sortConfig) return filteredData; const sorted = [...filteredData].sort((a, b) => { const aValue = a[sortConfig.key]; const bValue = b[sortConfig.key]; if (aValue < bValue) return sortConfig.direction === 'asc' ? -1 : 1; if (aValue > bValue) return sortConfig.direction === 'asc' ? 1 : -1; return 0; }); return sorted; }, [filteredData, sortConfig]); // 3. Pagination const totalPages = Math.ceil(sortedData.length / pageSize); const paginatedData = useMemo(() => { const start = (currentPage - 1) * pageSize; const end = start + pageSize; return sortedData.slice(start, end); }, [sortedData, currentPage, pageSize]); const handleSort = (key: keyof T) => { if (!enableSorting) return; let direction: 'asc' | 'desc' = 'asc'; if (sortConfig && sortConfig.key === key && sortConfig.direction === 'asc') { direction = 'desc'; } setSortConfig({ key, direction }); setCurrentPage(1); // Reset to first page on sort }; const handleSelect = (id: string | number) => { setSelection(prev => { const newSelection = new Set(prev); if (newSelection.has(id)) { newSelection.delete(id); } else { newSelection.add(id); } return newSelection; }); }; const handleSelectAll = () => { setSelection(prevSelection => { const newSelection = new Set(prevSelection); const pageIds = paginatedData.map(item => item.id); const allOnPageSelected = pageIds.length > 0 && pageIds.every(id => newSelection.has(id)); if (allOnPageSelected) { // If all are selected, deselect them pageIds.forEach(id => newSelection.delete(id)); } else { // Otherwise, select them all pageIds.forEach(id => newSelection.add(id)); } return newSelection; }); }; const clearSelection = () => { setSelection(new Set()); }; const selectedItems = useMemo(() => data.filter(item => selection.has(item.id)), [data, selection]); const isAllOnPageSelected = paginatedData.length > 0 && paginatedData.every(item => selection.has(item.id)); return ( {enableFiltering && (
{ setFilter(e.target.value); setCurrentPage(1); // Reset to first page on filter }} />
)}
Toggle columns
{columns.map(col => (
{ setColumnVisibility(prev => ({ ...prev, [col.accessorKey as string]: !prev[col.accessorKey as string] })); }} disabled={col.enableHiding === false} />
))}
{actions && (
0 ? 1 : 0, transform: selection.size > 0 ? 'translateY(0)' : 'translateY(-10px)', pointerEvents: selection.size > 0 ? 'auto' : 'none' }}> {selection.size} selected {actions(selectedItems, clearSelection)}
)}
{enableSelection && ( )} {visibleColumns.map(col => ( col.enableSorting !== false && handleSort(col.accessorKey)} style={{ cursor: (col.enableSorting !== false && enableSorting) ? 'pointer' : 'default', whiteSpace: 'nowrap' }} > {col.header} {enableSorting && col.enableSorting !== false && ( )} ))} {paginatedData.map(row => ( {enableSelection && ( handleSelect(row.id)} aria-label={`Select row ${row.id}`} /> )} {visibleColumns.map(col => ( {col.cell ? col.cell(row[col.accessorKey], row) : String(row[col.accessorKey])} ))} ))}
{paginatedData.length === 0 && ( {filter ? 'No results found.' : 'No data available.'} )} {totalPages > 1 && ( Page {currentPage} of {totalPages} )}
); };