'use client'; import { ColumnDef, ColumnFiltersState, flexRender, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, SortingState, useReactTable, VisibilityState, } from '@tanstack/react-table'; import { Alert, AlertDescription, AlertTitle } from '@workspace/ui/components/alert'; import { Button } from '@workspace/ui/components/button'; import { Checkbox } from '@workspace/ui/components/checkbox'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@workspace/ui/components/table'; import Empty from '@workspace/ui/custom-components/empty'; import { ColumnFilter, IParams } from '@workspace/ui/custom-components/pro-table/column-filter'; import { ColumnHeader } from '@workspace/ui/custom-components/pro-table/column-header'; import { ColumnToggle } from '@workspace/ui/custom-components/pro-table/column-toggle'; import { Pagination } from '@workspace/ui/custom-components/pro-table/pagination'; import { SortableRow } from '@workspace/ui/custom-components/pro-table/sortable-row'; import { ProTableWrapper } from '@workspace/ui/custom-components/pro-table/wrapper'; import { cn } from '@workspace/ui/lib/utils'; import { useSize } from 'ahooks'; import { GripVertical, ListRestart, Loader, RefreshCcw } from 'lucide-react'; import React, { Fragment, useEffect, useImperativeHandle, useRef, useState } from 'react'; export interface ProTableProps { columns: ColumnDef[]; request: ( pagination: { page: number; size: number; }, filter: TValue, ) => Promise<{ list: TData[]; total: number }>; params?: IParams[]; header?: { title?: React.ReactNode; toolbar?: React.ReactNode | React.ReactNode[]; hidden?: boolean; }; actions?: { render?: (row: TData) => React.ReactNode[]; batchRender?: (rows: TData[]) => React.ReactNode[]; }; action?: React.Ref; texts?: Partial<{ actions: string; asc: string; desc: string; hide: string; textRowsPerPage: string; textPageOf: (current: number, total: number) => string; selectedRowsText: (total: number) => string; }>; empty?: React.ReactNode; onSort?: ( sourceId: string | number, targetId: string | number | null, items: TData[], ) => Promise; initialFilters?: Record; } export interface ProTableActions { refresh: () => void; reset: () => void; } export function ProTable< TData extends Record & { id?: string }, TValue extends Record, >({ columns, request, params, header, actions, action, texts, empty, onSort, initialFilters, }: ProTableProps) { const [sorting, setSorting] = useState([]); const [columnFilters, setColumnFilters] = useState(() => { if (initialFilters) { return Object.entries(initialFilters).map(([id, value]) => ({ id, value, })) as ColumnFiltersState; } return []; }); const [columnVisibility, setColumnVisibility] = useState({}); const [rowSelection, setRowSelection] = useState({}); const [data, setData] = useState([]); const [rowCount, setRowCount] = useState(0); const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10, }); const loading = useRef(false); const table = useReactTable({ data, columns: [ ...(onSort ? [ { id: 'sortable', header: ( ), enableSorting: false, enableHiding: false, }, ] : []), ...(actions?.batchRender ? [createSelectColumn()] : []), ...columns.map( (column) => ({ enableSorting: false, ...column, }) as ColumnDef, ), ...(actions?.render ? ([ { id: 'actions', header: texts?.actions, cell: ({ row }) => (
{actions?.render?.(row.original).map((item, index) => ( {item} ))}
), enableSorting: false, enableHiding: false, }, ] as ColumnDef[]) : []), ] as ColumnDef[], onPaginationChange: setPagination, onSortingChange: setSorting, onColumnFiltersChange: setColumnFilters, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), onColumnVisibilityChange: setColumnVisibility, onRowSelectionChange: setRowSelection, state: { sorting, columnFilters, columnVisibility, rowSelection, pagination, }, manualPagination: true, manualFiltering: true, rowCount: rowCount, manualSorting: true, }); const fetchData = async () => { if (loading.current) return; loading.current = true; try { const response = await request( { page: pagination.pageIndex + 1, size: pagination.pageSize, }, Object.fromEntries(columnFilters.map((item) => [item.id, item.value])) as TValue, ); setData(response.list); setRowCount(response.total); } catch (error) { console.log('Fetch data error:', error); } finally { loading.current = false; } }; const reset = async () => { table.resetSorting(); table.resetColumnFilters(); table.resetGlobalFilter(true); table.resetColumnVisibility(); table.resetRowSelection(); table.resetPagination(); }; const ref = useRef(null); const size = useSize(ref); useImperativeHandle(action, () => ({ refresh: fetchData, reset, })); useEffect(() => { fetchData(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [pagination.pageIndex, pagination.pageSize, JSON.stringify(columnFilters)]); const selectedRows = table.getSelectedRowModel().flatRows.map((row) => row.original); const selectedCount = selectedRows.length; return (
{!header?.hidden && (
{params ? ( [item.id, item.value]))} /> ) : ( header?.title )}
{header?.toolbar}
)} {selectedCount > 0 && actions?.batchRender && ( {texts?.selectedRowsText?.(selectedCount) || `Selected ${selectedCount} rows`} {actions.batchRender(selectedRows)} )}
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( ))} ))} {table.getRowModel()?.rows?.length ? ( onSort ? ( table.getRowModel().rows.map((row) => ( {row .getVisibleCells() .filter((cell) => { return cell.column.id !== 'sortable'; }) .map((cell) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} )) ) : ( table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} )) ) ) : ( {empty || } )}
{loading.current && (
)}
{rowCount > 0 && ( )}
); } function createSelectColumn(): ColumnDef { return { id: 'selected', header: ({ table }) => ( table.toggleAllPageRowsSelected(!!value)} aria-label='Select all' /> ), cell: ({ row }) => ( row.toggleSelected(!!value)} aria-label='Select row' /> ), enableSorting: false, enableHiding: false, }; } function getTableHeaderClass(columnId: string) { if (['sortable', 'selected'].includes(columnId)) { return 'sticky left-0 z-10 bg-background shadow-[2px_0_5px_-2px_rgba(0,0,0,0.1)] [&:has([role=checkbox])]:pr-2'; } else if (columnId === 'actions') { return 'sticky right-0 z-10 text-right bg-background shadow-[-2px_0_5px_-2px_rgba(0,0,0,0.1)]'; } return 'truncate'; } function getTableCellClass(columnId: string) { if (['sortable', 'selected'].includes(columnId)) { return 'sticky left-0 bg-background shadow-[2px_0_5px_-2px_rgba(0,0,0,0.1)]'; } else if (columnId === 'actions') { return 'sticky right-0 bg-background shadow-[-2px_0_5px_-2px_rgba(0,0,0,0.1)]'; } return 'truncate'; }