'use client'; import React from 'react'; import { ColumnDef, ColumnPinningState, ColumnSizingState, SortingState, VisibilityState, flexRender, getCoreRowModel, useReactTable, getSortedRowModel, getPaginationRowModel, } from '@tanstack/react-table'; import { Table, TableBody, TableCell, TableRow } from '@/components/ui/table'; import { Skeleton } from '@/components/ui/skeleton'; import type { PermissionInput } from '@/hooks/usePermissions'; import { PermissionGuard } from '@/guards'; import TopHeader from './Header'; import TableHeader from './TableHeader'; import { TableFooter } from './Footer'; import { TableActionButton } from './TableActionButton'; import { ColumnViewOptions } from './ColumnViewOptions'; import { getPinnedColumnStyles } from './columnStyles'; import { cn } from '@/lib/utils'; function shouldTruncateCell(column: { id: string; columnDef: { meta?: unknown }; }) { if (column.id === 'actions') return false; const meta = column.columnDef.meta as | { disableTruncate?: boolean } | undefined; return !meta?.disableTruncate; } export type ColumnAlign = 'left' | 'right'; export interface DataTableAction { label: string | ((item: TData) => string); icon: React.ReactNode; onClick: (item: TData) => void; permission?: PermissionInput; disabled?: (item: TData) => boolean; hidden?: (item: TData) => boolean; className?: string; } export interface DataTableProps { columns: ColumnDef[]; data: TData[]; title?: string; onAddNew?: () => void; addButtonText?: string; isLoading?: boolean; actions?: DataTableAction[]; /** Sticky-pin the actions column (use with actionsAlign). */ actionsSticky?: boolean; actionsAlign?: ColumnAlign; toolbar?: React.ReactNode; emptyTitle?: string; emptyDescription?: string; pagination?: { skip: number; limit: number; totalItems?: number; onPageChange: (newSkip: number) => void; onLimitChange: (newLimit: number) => void; }; sorting?: SortingState; onSortingChange?: (sorting: SortingState) => void; enableColumnControls?: boolean; onRowClick?: (row: TData) => void; /** Minimum width (px) for every column unless overridden in the column def. */ minColumnSize?: number; } export function DataTable(props: DataTableProps) { const { actions, ...rest } = props; if (!actions?.length) { return ; } const actionPermissions = actions.map((action) => action.permission); return ( } > ); } function DataTableContent({ columns: initialColumns, data, title, onAddNew, addButtonText, isLoading = false, actions, actionsSticky = false, actionsAlign = 'right', toolbar, emptyTitle = 'No results found.', emptyDescription = 'Try adjusting your filters or search terms.', pagination, onRowClick, sorting: controlledSorting, onSortingChange, enableColumnControls = true, minColumnSize = 120, }: DataTableProps) { const [rowSelection, setRowSelection] = React.useState({}); const [columnVisibility, setColumnVisibility] = React.useState({}); const [columnSizing, setColumnSizing] = React.useState({}); const [internalSorting, setInternalSorting] = React.useState( [], ); const sorting = controlledSorting ?? internalSorting; const isManualSorting = Boolean(onSortingChange); const handleSortingChange = React.useCallback( (updater: SortingState | ((old: SortingState) => SortingState)) => { const nextSorting = typeof updater === 'function' ? updater(sorting) : updater; if (onSortingChange) { onSortingChange(nextSorting); return; } setInternalSorting(nextSorting); }, [onSortingChange, sorting], ); const columns = React.useMemo(() => { const cols: ColumnDef[] = [...initialColumns]; if (actions?.length) { cols.push({ id: 'actions', size: 110, minSize: 90, enableHiding: false, enableSorting: false, enablePinning: false, header: () =>
Actions
, cell: ({ row }) => { const item = row.original; return (
{actions.map((action) => { if (action.hidden?.(item)) return null; const label = typeof action.label === 'function' ? action.label(item) : action.label; return ( { event.stopPropagation(); action.onClick(item); }} > {action.icon} ); })}
); }, }); } return cols; }, [actions, initialColumns]); const columnPinning = React.useMemo( () => actionsSticky && actions?.length ? { [actionsAlign]: ['actions'] } : {}, [actions, actionsAlign, actionsSticky], ); const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(), onRowSelectionChange: setRowSelection, onColumnVisibilityChange: setColumnVisibility, onColumnSizingChange: setColumnSizing, onSortingChange: handleSortingChange, getSortedRowModel: getSortedRowModel(), getPaginationRowModel: getPaginationRowModel(), enableColumnResizing: true, enableColumnPinning: true, columnResizeMode: 'onChange', defaultColumn: { minSize: minColumnSize, size: minColumnSize, }, manualPagination: !!pagination, manualSorting: isManualSorting, pageCount: pagination ? Math.ceil((pagination.totalItems ?? data.length) / pagination.limit) : undefined, state: { rowSelection, columnVisibility, columnPinning, columnSizing, sorting, ...(pagination ? { pagination: { pageIndex: Math.floor(pagination.skip / pagination.limit), pageSize: pagination.limit, }, } : {}), }, onPaginationChange: pagination ? (updater) => { const currentState = { pageIndex: Math.floor(pagination.skip / pagination.limit), pageSize: pagination.limit, }; const newState = typeof updater === 'function' ? updater(currentState) : updater; const pageSizeChanged = newState.pageSize !== pagination.limit; if (pageSizeChanged) { pagination.onLimitChange(newState.pageSize); pagination.onPageChange(0); return; } pagination.onPageChange(newState.pageIndex * newState.pageSize); } : undefined, }); return ( <>
: null } />
{isLoading ? ( Array.from({ length: 5 }).map((_, idx) => ( {columns.map((_, colIdx) => ( ))} )) ) : table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => { const truncate = shouldTruncateCell(cell.column); const pinned = cell.column.getIsPinned(); const isActionsCell = cell.column.id === 'actions'; const isRowClickable = Boolean( onRowClick && !isActionsCell, ); const content = flexRender( cell.column.columnDef.cell, cell.getContext(), ); return ( onRowClick?.(row.original) : undefined } className={cn( 'h-13 max-w-0 overflow-hidden border-b border-border px-4 py-3 text-sm', pinned && 'bg-background', isRowClickable && 'cursor-pointer', isActionsCell && 'cursor-default', )} > {truncate ? (
{content}
) : ( content )}
); })}
)) ) : (

{emptyTitle}

{emptyDescription}

)}
); }