From 32fa01cc120a370497ef85110a97ae1363e4e978 Mon Sep 17 00:00:00 2001 From: Developer Date: Fri, 26 Jun 2026 15:58:12 +0000 Subject: [PATCH] Extract shared TablePagination (dedupe DataTable + Media mobile) Pull the duplicated pagination footer into a single shared component at frontend/src/components/ui/table-pagination.tsx. Both the desktop DataTable (which had an internal DataTablePagination driven by a TanStack table instance) and the Media mobile card list (which had a standalone MediaMobilePagination driven by raw PaginationState) now consume it. The shared component takes the raw primitives (pageIndex, pageSize, pageCount, totalRows, pageSizeOptions, onPaginationChange, optional className) so it backs both an adapter view (DataTable extracts state from its table instance and passes table.setPagination) and a direct state view (Media passes its pagination state directly). Includes the 44px mobile-touch-target on prev/next buttons (previously only on the Media mobile variant). Removes ~90 lines of duplication across data-table.tsx and Media.tsx; adds the focused 122-line shared component. The DataTable Select imports are dropped (now unused). 122 tests pass; lint/build green. Refs openspec/changes/mobile-responsive-parity/verify-report.md residual risk #5. --- frontend/src/components/ui/data-table.tsx | 96 ++------------ .../src/components/ui/table-pagination.tsx | 122 ++++++++++++++++++ frontend/src/pages/Media.tsx | 97 +------------- 3 files changed, 136 insertions(+), 179 deletions(-) create mode 100644 frontend/src/components/ui/table-pagination.tsx diff --git a/frontend/src/components/ui/data-table.tsx b/frontend/src/components/ui/data-table.tsx index a311ab5..b12d10b 100644 --- a/frontend/src/components/ui/data-table.tsx +++ b/frontend/src/components/ui/data-table.tsx @@ -6,7 +6,6 @@ import { type OnChangeFn, type PaginationState, type RowSelectionState, - type Table as TableInstance, type VisibilityState, flexRender, getCoreRowModel, @@ -18,6 +17,7 @@ import { Columns3 } from "lucide-react"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; +import { TablePagination } from "@/components/ui/table-pagination"; import { Table, TableBody, @@ -34,13 +34,6 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; export interface DataTableProps { columns: ColumnDef[]; @@ -253,90 +246,19 @@ export function DataTable({ {enablePagination && ( - )} ); } -interface PaginationProps { - table: TableInstance; - pageSizeOptions: number[]; - pageCount: number; - manual: boolean; - rowCount?: number; -} - -function DataTablePagination({ - table, - pageSizeOptions, - pageCount, - manual, - rowCount, -}: PaginationProps) { - const pageIndex = table.getState().pagination.pageIndex; - const pageSize = table.getState().pagination.pageSize; - const visibleRows = table.getRowModel().rows.length; - const totalRows = manual ? (rowCount ?? 0) : visibleRows; - - return ( -
-
- {`${totalRows} row${totalRows === 1 ? "" : "s"}`} -
-
-
- Rows per page - -
- - Page {pageIndex + 1} of {pageCount} - -
- - -
-
-
- ); -} +// DataTablePagination was extracted into the shared TablePagination component +// (frontend/src/components/ui/table-pagination.tsx). Both the desktop DataTable +// and the Media mobile card list consume it. diff --git a/frontend/src/components/ui/table-pagination.tsx b/frontend/src/components/ui/table-pagination.tsx new file mode 100644 index 0000000..40de358 --- /dev/null +++ b/frontend/src/components/ui/table-pagination.tsx @@ -0,0 +1,122 @@ +import type { OnChangeFn } from "@tanstack/react-table"; +import type { PaginationState } from "@tanstack/react-table"; + +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +/** + * Shared pagination footer for table-style views. + * + * Renders the rows count, page-size select, page indicator, and prev/next + * buttons. Works off the raw {@link PaginationState} primitives so it can back + * both a TanStack `Table` instance (via a thin adapter) and standalone card + * layouts that drive pagination directly (e.g. MediaMobilePagination). + * + * The Desktop DataTable and the Media mobile card list both consume this to + * avoid the duplication flagged in + * `openspec/changes/mobile-responsive-parity/verify-report.md` residual risk #5. + */ +export interface TablePaginationProps { + pageIndex: number; + pageSize: number; + pageSizeOptions: number[]; + totalRows: number; + pageCount: number; + onPaginationChange: OnChangeFn; + /** Optional extra className on the outer container (e.g. "p-4"). */ + className?: string; +} + +export function TablePagination({ + pageIndex, + pageSize, + pageSizeOptions, + totalRows, + pageCount, + onPaginationChange, + className, +}: TablePaginationProps) { + return ( +
+
+ {`${totalRows} row${totalRows === 1 ? "" : "s"}`} +
+
+
+ Rows per page + +
+ + Page {pageIndex + 1} of {pageCount} + +
+ + +
+
+
+ ); +} diff --git a/frontend/src/pages/Media.tsx b/frontend/src/pages/Media.tsx index c367664..cdf8c8a 100644 --- a/frontend/src/pages/Media.tsx +++ b/frontend/src/pages/Media.tsx @@ -13,6 +13,7 @@ import { MobileCardRow, type MobileCardField, } from "@/components/ui/mobile-card"; +import { TablePagination } from "@/components/ui/table-pagination"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; @@ -100,97 +101,8 @@ const mediaCardFields: MobileCardField[] = [ }, ]; -// Standalone pagination for the mobile card layout. The DataTable renders its -// own pagination internally; this mirrors that UI (rows count, page-size -// select, page indicator, prev/next) but works off the raw pagination state -// instead of a TanStack table instance. See spec R3.3. -function MediaMobilePagination({ - pageIndex, - pageSize, - pageSizeOptions, - totalRows, - pageCount, - onPaginationChange, -}: { - pageIndex: number; - pageSize: number; - pageSizeOptions: number[]; - totalRows: number; - pageCount: number; - onPaginationChange: OnChangeFn; -}) { - return ( -
-
- {`${totalRows} row${totalRows === 1 ? "" : "s"}`} -
-
-
- Rows per page - -
- - Page {pageIndex + 1} of {pageCount} - -
- - -
-
-
- ); -} +// Mobile pagination uses the shared TablePagination component +// (frontend/src/components/ui/table-pagination.tsx). const MEDIA_TAB_STATE_KEY = "manage.media.tabState"; const SMALL_BREAKPOINT = "(max-width: 900px)"; @@ -662,13 +574,14 @@ export function Media() { /> {queryResult && ( - )}