// ----- Imports ----- import { Box, Checkbox, HStack, Spacer as ChakraSpacer, Table as ChakraTable, Tbody, Td, Text, Th, Thead, Tr, VStack, } from "@chakra-ui/react"; import { Pagination as UIKitPagination } from "../pagination/Pagination.jsx"; import { toFaDigits } from "../utils/numbers.js"; import { flexRender, getCoreRowModel, useReactTable, } from "@tanstack/react-table"; import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react"; // ----- Context ----- const DataTableContext = createContext(null); function useDataTable() { const context = useContext(DataTableContext); if (!context) { throw new Error("DataTable components must be used inside "); } return context; } // ----- Root Component ----- function Root({ children, data = [], columns = [], page = 1, size = 10, pagination, onPageChange, getRowId = (row) => String(row?.id), enableSelection = false, showIndex = true, defaultSelectedIds = [], onSelectionChange, ...props }) { // ----- Normalized Pagination Props ----- const normalizedPage = Number(page) || 1; const normalizedSize = Number(size) || 10; const totalCount = pagination?.total ?? pagination?.totalCount ?? 0; const totalPages = pagination?.total_pages ?? pagination?.totalPages ?? (totalCount > 0 ? Math.ceil(totalCount / normalizedSize) : 0); // ----- TanStack Pagination State ----- const paginationState = useMemo(() => { return { pageIndex: Math.max(normalizedPage - 1, 0), pageSize: normalizedSize, }; }, [normalizedPage, normalizedSize]); const handlePaginationChange = useCallback( (updater) => { const next = typeof updater === "function" ? updater(paginationState) : updater; const nextPage = next.pageIndex + 1; if (nextPage !== normalizedPage) { onPageChange?.(nextPage); } }, [paginationState, normalizedPage, onPageChange], ); // ----- Selection State ----- const [selectedIds, setSelectedIdsState] = useState(defaultSelectedIds); const setSelectedIds = useCallback( (updater) => { setSelectedIdsState((prev) => { const next = typeof updater === "function" ? updater(prev) : updater; onSelectionChange?.(next); return next; }); }, [onSelectionChange], ); // ----- Reset Selection On Page Change ----- const previousPageRef = useRef({ page: normalizedPage, size: normalizedSize, }); useEffect(() => { if (!enableSelection) { return; } const previous = previousPageRef.current; const pageChanged = previous.page !== normalizedPage || previous.size !== normalizedSize; previousPageRef.current = { page: normalizedPage, size: normalizedSize, }; if (pageChanged) { setSelectedIds([]); } }, [enableSelection, normalizedPage, normalizedSize, setSelectedIds]); // ----- Page Row Ids ----- const allPageIds = useMemo(() => { return data .map((item) => getRowId(item)) .filter((id) => id !== undefined && id !== null && id !== ""); }, [data, getRowId]); const pageSelectedIds = useMemo(() => { return selectedIds.filter((id) => allPageIds.includes(id)); }, [allPageIds, selectedIds]); // ----- Selection Status ----- const isAllSelected = allPageIds.length > 0 && pageSelectedIds.length === allPageIds.length; const isIndeterminate = pageSelectedIds.length > 0 && pageSelectedIds.length < allPageIds.length; // ----- Selection Handlers ----- const clearSelection = useCallback(() => { setSelectedIds([]); }, [setSelectedIds]); const onSelectionChangedId = useCallback( (id) => { setSelectedIds((prev) => { if (!allPageIds.includes(id)) { return prev; } if (prev.includes(id)) { return prev.filter((item) => item !== id); } return [...prev, id]; }); }, [allPageIds, setSelectedIds], ); const toggleRowSelection = onSelectionChangedId; const selectAll = useCallback(() => { setSelectedIds(allPageIds); }, [allPageIds, setSelectedIds]); const unselectAll = useCallback(() => { setSelectedIds([]); }, [setSelectedIds]); const toggleSelectAll = useCallback( (checked) => { if (checked) { selectAll(); return; } unselectAll(); }, [selectAll, unselectAll], ); // ----- Table Columns ----- const tableColumns = useMemo(() => { const result = []; if (enableSelection) { result.push({ id: "__selection", header: ({ table }) => { const meta = table.options.meta; return ( meta.toggleSelectAll(e.target.checked)} colorScheme="purple" size="md" /> ); }, cell: ({ row, table }) => { const meta = table.options.meta; const rowId = meta.getRowId(row.original); return ( meta.onSelectionChangedId(rowId)} colorScheme="purple" size="md" /> ); }, meta: { isCentered: true }, }); } if (showIndex) { result.push({ id: "__index", header: "ردیف", cell: ({ row, table }) => { const { pageIndex, pageSize } = table.getState().pagination; const rowNumber = pageIndex * pageSize + row.index + 1; return ( {toFaDigits(rowNumber)} ); }, meta: { isCentered: true }, }); } return [...result, ...columns]; }, [columns, enableSelection, showIndex]); // ----- Table Instance ----- const table = useReactTable({ data, columns: tableColumns, getCoreRowModel: getCoreRowModel(), getRowId, manualPagination: true, pageCount: totalPages, rowCount: totalCount, state: { pagination: paginationState, }, onPaginationChange: handlePaginationChange, meta: { selectedIds, onSelectionChangedId, getRowId, allPageIds, pageSelectedIds, isAllSelected, isIndeterminate, selectAll, unselectAll, toggleSelectAll, clearSelection, totalCount, totalPages, }, }); // ----- Provider Value ----- const value = { table, data, columns: tableColumns, page: normalizedPage, size: normalizedSize, pagination, onPageChange, paginationState, totalCount, totalPages, getRowId, enableSelection, selectedIds, setSelectedIds, clearSelection, allPageIds, pageSelectedIds, isAllSelected, isIndeterminate, selectAll, unselectAll, toggleSelectAll, onSelectionChangedId, toggleRowSelection, }; // ----- Root Render ----- return ( {children} ); } // ----- Topbar Components ----- function Topbar({ children, ...props }) { return ( {children} ); } function Title({ children, ...props }) { return ( {children} ); } function Spacer(props) { return ; } function Actions({ children, ...props }) { const context = useDataTable(); return ( {typeof children === "function" ? children(context) : children} ); } // ----- Selection Components ----- function SelectionActions({ children }) { const context = useDataTable(); if (context.selectedIds.length === 0) { return null; } return typeof children === "function" ? children(context) : children; } function SelectAll({ children = "انتخاب همه", ...props }) { const { data, enableSelection, isAllSelected, isIndeterminate, toggleSelectAll, } = useDataTable(); if (!enableSelection || data.length === 0) { return null; } return ( toggleSelectAll(e.target.checked)} colorScheme="purple" size="lg" {...props} > {children} ); } function SelectedCount({ children, ...props }) { const { selectedIds } = useDataTable(); if (selectedIds.length === 0) { return null; } if (typeof children === "function") { return children({ count: selectedIds.length, selectedIds }); } return ( {toFaDigits(selectedIds.length)} مورد انتخاب شده ); } // ----- Content Wrapper ----- function Content({ children, ...props }) { return ( {children} ); } // ----- Table Container ----- function Table({ children, minW = "1200px", heightOffset = 355, containerProps, tableProps, }) { return ( {children ?? ( <> )} ); } // ----- Table Head ----- function Head() { const { table } = useDataTable(); return ( {table.getHeaderGroups().map((group) => ( {group.headers.map((header) => ( {header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext(), )} ))} ))} ); } // ----- Table Body ----- function Body({ emptyText = "موردی وجود ندارد" }) { const { table, columns } = useDataTable(); const rows = table.getRowModel().rows; return ( {rows.length > 0 ? ( rows.map((row) => ( {row.getVisibleCells().map((cell) => ( {cell.column.columnDef.cell ? flexRender(cell.column.columnDef.cell, cell.getContext()) : cell.getValue()} ))} )) ) : ( {emptyText} )} ); } // ----- Pagination ----- function Pagination({ pagination: paginationProp, onPageChange: onPageChangeProp, }) { const context = useDataTable(); const { table } = context; const pagination = paginationProp ?? context.pagination; const totalCount = pagination?.total ?? pagination?.totalCount ?? context.totalCount ?? 0; const totalPages = pagination?.total_pages ?? pagination?.totalPages ?? context.totalPages ?? table.getPageCount() ?? 0; const currentPage = table.getState().pagination.pageIndex + 1; const handlePageChange = (nextPage) => { if (onPageChangeProp) { onPageChangeProp(nextPage); return; } table.setPageIndex(nextPage - 1); }; if (!context.onPageChange && !onPageChangeProp) { return null; } if (totalPages <= 0) { return null; } return ( ); } // ----- Compound Export ----- export const DataTable = Object.assign(Root, { Topbar, Title, Spacer, Actions, SelectionActions, SelectAll, SelectedCount, Content, Table, Head, Body, Pagination, });