From 634747640a5b4f5fed497af1fb2d05c22cbafd61 Mon Sep 17 00:00:00 2001 From: ghost2023 Date: Tue, 19 May 2026 16:50:01 +0300 Subject: [PATCH] feat(data-table): Introduce comprehensive DataTable component with full feature set --- .../src/components/data-table/error.tsx | 40 +++++ .../src/components/data-table/footer.tsx | 119 ++++++++++++++ .../src/components/data-table/hooks.ts | 25 +++ .../src/components/data-table/index.ts | 12 ++ .../src/components/data-table/skeleton.tsx | 16 ++ .../src/components/data-table/table.tsx | 149 ++++++++++++++++++ .../src/components/data-table/types.ts | 38 +++++ 7 files changed, 399 insertions(+) create mode 100644 packages/ui-common/src/components/data-table/error.tsx create mode 100644 packages/ui-common/src/components/data-table/footer.tsx create mode 100644 packages/ui-common/src/components/data-table/hooks.ts create mode 100644 packages/ui-common/src/components/data-table/index.ts create mode 100644 packages/ui-common/src/components/data-table/skeleton.tsx create mode 100644 packages/ui-common/src/components/data-table/table.tsx create mode 100644 packages/ui-common/src/components/data-table/types.ts diff --git a/packages/ui-common/src/components/data-table/error.tsx b/packages/ui-common/src/components/data-table/error.tsx new file mode 100644 index 000000000..81bc860b9 --- /dev/null +++ b/packages/ui-common/src/components/data-table/error.tsx @@ -0,0 +1,40 @@ +import { Table as TanstackTable } from "@tanstack/react-table"; + +import { TableCell, TableRow } from "#components/table"; +import { Button } from "#components/button.tsx"; + +export function DataTableError({ + table, + message, + description, + onRetry, +}: { + table: TanstackTable; + message?: string; + description?: string; + onRetry?: () => void; +}) { + return ( + + +
+
+
+

+ {message ?? "No results"} +

+

+ {description ?? "No results found"} +

+
+ {onRetry && ( + + )} +
+
+
+
+ ); +} diff --git a/packages/ui-common/src/components/data-table/footer.tsx b/packages/ui-common/src/components/data-table/footer.tsx new file mode 100644 index 000000000..daa818eaa --- /dev/null +++ b/packages/ui-common/src/components/data-table/footer.tsx @@ -0,0 +1,119 @@ +import { Button } from "../button"; +import { DataTableFooterProps } from "./types"; + +export interface DataTableFooterOptions { + pageSizeOptions?: number[]; + showPageSizeSelector?: boolean; + showRowCount?: boolean; + showPagination?: boolean; + labels?: { + rowsPerPage?: string; + page?: string; + of?: string; + showing?: string; + ofLabel?: string; + items?: string; + previous?: string; + next?: string; + }; +} + +interface DataTableFooterComponentProps< + TData, +> extends DataTableFooterProps { + options?: DataTableFooterOptions; +} + +const defaultOptions: DataTableFooterOptions = { + pageSizeOptions: [5, 10, 25, 50], + showPageSizeSelector: true, + showRowCount: true, + showPagination: true, + labels: { + rowsPerPage: "Rows per page", + page: "Page", + of: "of", + showing: "Showing", + ofLabel: "of", + items: "items", + previous: "Previous", + next: "Next", + }, +}; + +export function DataTableFooter({ + table, + pagination, + options = {}, +}: DataTableFooterComponentProps) { + const opts = { ...defaultOptions, ...options }; + const labels = { ...defaultOptions.labels, ...options.labels }; + + const pageIndex = pagination.pageIndex ?? 0; + const pageSize = pagination.pageSize ?? 10; + const totalCount = pagination.totalCount ?? 0; + + const start = totalCount === 0 ? 0 : pageIndex * pageSize + 1; + const end = Math.min((pageIndex + 1) * pageSize, totalCount); + + const handlePageSizeChange = (newPageSize: number) => { + table?.setPageSize(newPageSize); + }; + + return ( +
+ {(opts.showPageSizeSelector || opts.showRowCount) && ( +
+ {opts.showPageSizeSelector && ( + <> + + + + )} + {opts.showRowCount && ( + + {labels.showing} {start}–{end} {labels.ofLabel} {totalCount}{" "} + {labels.items} + + )} +
+ )} + + {opts.showPagination && ( +
+
+ + +
+
+ )} +
+ ); +} diff --git a/packages/ui-common/src/components/data-table/hooks.ts b/packages/ui-common/src/components/data-table/hooks.ts new file mode 100644 index 000000000..0ce1ad339 --- /dev/null +++ b/packages/ui-common/src/components/data-table/hooks.ts @@ -0,0 +1,25 @@ +import { PaginationState } from "@tanstack/react-table"; +import { useState } from "react"; + +export const usePagination = ({ + pageSize, + pageIndex, +}: { + pageSize?: number; + pageIndex?: number; +} = {}) => { + const [pagination, setPagination] = useState({ + pageIndex: pageIndex ?? 0, + pageSize: pageSize ?? 10, + }); + + const setPage = (pageIndex: number) => { + setPagination((prev) => ({ ...prev, pageIndex })); + }; + + return { + pagination, + setPagination, + setPage, + }; +}; diff --git a/packages/ui-common/src/components/data-table/index.ts b/packages/ui-common/src/components/data-table/index.ts new file mode 100644 index 000000000..c07c1ab8a --- /dev/null +++ b/packages/ui-common/src/components/data-table/index.ts @@ -0,0 +1,12 @@ +export { DataTableError } from "./error"; +export { DataTableSkeleton } from "./skeleton"; +export { DataTableFooter, type DataTableFooterOptions } from "./footer"; +export type { + DataTableProps, + DataTablePagination, + DataTableFooterProps, + DataTableFooterComponent, +} from "./types"; +export { usePagination } from "./hooks"; +export { DataTable } from "./table"; +export * from "@tanstack/react-table"; diff --git a/packages/ui-common/src/components/data-table/skeleton.tsx b/packages/ui-common/src/components/data-table/skeleton.tsx new file mode 100644 index 000000000..40861a987 --- /dev/null +++ b/packages/ui-common/src/components/data-table/skeleton.tsx @@ -0,0 +1,16 @@ +import { Table as TanstackTable } from "@tanstack/react-table"; + +import { Skeleton } from "../skeleton"; +import { TableCell, TableRow } from "#components/table"; + +export function DataTableSkeleton({ table }: { table: TanstackTable }) { + return Array.from({ length: 10 }).map((_, i) => ( + + {table.getAllColumns().map((column) => ( + + + + ))} + + )); +} diff --git a/packages/ui-common/src/components/data-table/table.tsx b/packages/ui-common/src/components/data-table/table.tsx new file mode 100644 index 000000000..0dcc5c044 --- /dev/null +++ b/packages/ui-common/src/components/data-table/table.tsx @@ -0,0 +1,149 @@ +import { + flexRender, + getCoreRowModel, + getPaginationRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "#components/table"; +import { DataTableProps } from "./types"; +import { DataTableSkeleton } from "./skeleton"; +import { DataTableError } from "./error"; +import { DataTableFooter } from "./footer"; + +export function DataTable({ + columns, + data, + status, + onRowClick, + tableOptions, + pagination, + footer, + footerClassName, + containerClassName, + error, + emptyMessage, +}: DataTableProps) { + const { state, ...otherOptions } = tableOptions ?? {}; + const baseState = state ?? {}; + if (pagination) { + baseState.pagination = { + pageIndex: pagination.pageIndex ?? 0, + pageSize: pagination.pageSize ?? 1, + }; + } + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + + manualPagination: true, + ...(pagination && { + getPaginationRowModel: getPaginationRowModel(), + pageCount: pagination.pageCount, + }), + state: baseState, + ...otherOptions, + }); + + return ( + <> +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + ) + ?.headerClassName ?? "" + } + style={{ width: `${header.getSize()}px` }} + > + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} + + ))} + + + {status === "loading" && } + {status === "error" && ( + + )} + {status === "success" && + (table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + onRowClick?.(row.original)} + role={onRowClick ? "button" : ""} + className={ + onRowClick + ? "cursor-pointer hover:bg-accent hover:text-foreground " + : "" + } + > + {row.getVisibleCells().map((cell) => ( + ) + ?.cellClassName ?? "" + } + > + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + + {emptyMessage ?? "No data"} + + + ))} + +
+
+ + {pagination && ( +
+ {footer ? ( + footer({ table, pagination }) + ) : ( + + )} +
+ )} + + ); +} diff --git a/packages/ui-common/src/components/data-table/types.ts b/packages/ui-common/src/components/data-table/types.ts new file mode 100644 index 000000000..886718b96 --- /dev/null +++ b/packages/ui-common/src/components/data-table/types.ts @@ -0,0 +1,38 @@ +import type { Table as TanstackTable, TableOptions, ColumnDef } from "@tanstack/react-table"; + +export interface DataTablePagination { + pageSize?: number; + pageIndex?: number; + pageCount?: number; + totalCount?: number; +} + +export interface DataTableFooterProps { + table: TanstackTable; + pagination: DataTablePagination; +} + +export type DataTableFooterComponent = ( + props: DataTableFooterProps +) => React.ReactNode; + +export interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; + status?: "loading" | "error" | "success"; + onRowClick?: (row: TData) => void; + tableOptions?: Omit< + TableOptions, + "data" | "columns" | "getCoreRowModel" + >; + pagination?: DataTablePagination; + footer?: DataTableFooterComponent; + footerClassName?: string; + containerClassName?: string; + emptyMessage?: string; + error?: { + message: string; + description?: string; + onRetry?: () => void; + }; +}