"use client"; import { type ColumnDef, type ColumnFiltersState, flexRender, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, type SortingState, useReactTable, type 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/composed/empty"; import { ColumnFilter, type IParams, } from "@workspace/ui/composed/pro-table/column-filter"; import { ColumnHeader } from "@workspace/ui/composed/pro-table/column-header"; import { ColumnToggle } from "@workspace/ui/composed/pro-table/column-toggle"; import { Pagination } from "@workspace/ui/composed/pro-table/pagination"; import { SortableRow } from "@workspace/ui/composed/pro-table/sortable-row"; import { ProTableWrapper } from "@workspace/ui/composed/pro-table/wrapper"; import { cn } from "@workspace/ui/lib/utils"; import { useSize } from "ahooks"; import { CircleAlert, GripVertical, ListRestart, Loader, RefreshCcw, } from "lucide-react"; import type React from "react"; import { 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; error?: 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 | number }, TValue extends Record, >({ columns, request, params, header, actions, action, texts, empty, error, 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 [isLoading, setIsLoading] = useState(false); const [requestError, setRequestError] = useState(false); const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 200, }); 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, manualSorting: true, }); const fetchData = async () => { if (loading.current) return; loading.current = true; setIsLoading(true); setRequestError(false); 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) { setData([]); setRowCount(0); setRequestError(true); } finally { loading.current = false; setIsLoading(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]) )} params={params} table={table} /> ) : ( 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) => ( ))} ))} {requestError ? ( {error || } ) : table.getRowModel()?.rows?.length ? ( onSort ? ( table.getRowModel().rows.map((row) => ( {row .getVisibleCells() .filter((cell) => 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 || } )}
{isLoading && (
)}
{rowCount > 0 && }
); } function ProTableErrorState() { return ( ); } function createSelectColumn(): ColumnDef { return { id: "selected", header: ({ table }) => ( table.toggleAllPageRowsSelected(!!value)} /> ), cell: ({ row }) => ( row.toggleSelected(!!value)} /> ), 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"; } 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)]"; } if (columnId === "actions") { return "sticky right-0 bg-background shadow-[-2px_0_5px_-2px_rgba(0,0,0,0.1)]"; } return "truncate"; } function getColumnClassName( columnLike: { column: { columnDef: { meta?: { className?: string; }; }; }; } ) { return columnLike.column.columnDef.meta?.className || ""; }