From 8e8fde67bcaa6181fd54c03c5c8df91d9e5c0443 Mon Sep 17 00:00:00 2001 From: Mohamadzadeh <7796733@gmail.com> Date: Sun, 14 Jun 2026 14:49:16 +0330 Subject: [PATCH] add table --- package.json | 10 +- src/components/table/DataTable.jsx | 688 +++++++++++++++++++++++++++++ src/components/table/index.js | 1 + yarn.lock | 12 + 4 files changed, 710 insertions(+), 1 deletion(-) create mode 100644 src/components/table/DataTable.jsx create mode 100644 src/components/table/index.js diff --git a/package.json b/package.json index c007ee2..40cbbe3 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,9 @@ "exports": { ".": "./dist/index.js" }, - "files": ["dist"], + "files": [ + "dist" + ], "scripts": { "build": "vite build" }, @@ -19,10 +21,16 @@ "react-dom": "^18.0.0 || ^19.0.0", "react-icons": "^5.0.0" }, + "peerDependenciesMeta": { + "@tanstack/react-table": { + "optional": true + } + }, "devDependencies": { "@chakra-ui/react": "^2.0.0", "@emotion/react": "^11.14.0", "@tanstack/react-query": "^5.0.0", + "@tanstack/react-table": "^8.21.3", "@vitejs/plugin-react": "4.3.4", "axios": "^1.17.0", "keycloak-js": "^26.2.4", diff --git a/src/components/table/DataTable.jsx b/src/components/table/DataTable.jsx new file mode 100644 index 0000000..a1b3e9e --- /dev/null +++ b/src/components/table/DataTable.jsx @@ -0,0 +1,688 @@ +// ----- 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, +}); diff --git a/src/components/table/index.js b/src/components/table/index.js new file mode 100644 index 0000000..e60f38a --- /dev/null +++ b/src/components/table/index.js @@ -0,0 +1 @@ +export { DataTable } from "./DataTable.jsx"; diff --git a/yarn.lock b/yarn.lock index b155304..50025f9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -609,6 +609,18 @@ dependencies: "@tanstack/query-core" "5.101.0" +"@tanstack/react-table@^8.21.3": + version "8.21.3" + resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.21.3.tgz#2c38c747a5731c1a07174fda764b9c2b1fb5e91b" + integrity sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww== + dependencies: + "@tanstack/table-core" "8.21.3" + +"@tanstack/table-core@8.21.3": + version "8.21.3" + resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.21.3.tgz#2977727d8fc8dfa079112d9f4d4c019110f1732c" + integrity sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg== + "@types/babel__core@^7.20.5": version "7.20.5" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017"