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.
This commit is contained in:
@@ -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<MediaItem>[] = [
|
||||
},
|
||||
];
|
||||
|
||||
// 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<PaginationState>;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 p-4 text-sm">
|
||||
<div className="text-muted-foreground">
|
||||
{`${totalRows} row${totalRows === 1 ? "" : "s"}`}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-muted-foreground">Rows per page</span>
|
||||
<Select
|
||||
value={String(pageSize)}
|
||||
onValueChange={(value) =>
|
||||
onPaginationChange(() => ({
|
||||
pageIndex: 0,
|
||||
pageSize: Number(value),
|
||||
}))
|
||||
}
|
||||
>
|
||||
<SelectTrigger
|
||||
size="sm"
|
||||
className="w-[70px]"
|
||||
aria-label="Rows per page"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{pageSizeOptions.map((option) => (
|
||||
<SelectItem key={option} value={String(option)}>
|
||||
{option}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<span className="text-muted-foreground">
|
||||
Page {pageIndex + 1} of {pageCount}
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
onPaginationChange((prev) => ({
|
||||
...prev,
|
||||
pageIndex: Math.max(0, prev.pageIndex - 1),
|
||||
}))
|
||||
}
|
||||
disabled={pageIndex <= 0}
|
||||
aria-label="Previous page"
|
||||
className="mobile-touch-target"
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
onPaginationChange((prev) => ({
|
||||
...prev,
|
||||
pageIndex: prev.pageIndex + 1,
|
||||
}))
|
||||
}
|
||||
disabled={pageIndex >= pageCount - 1}
|
||||
className="mobile-touch-target"
|
||||
aria-label="Next page"
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// 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() {
|
||||
/>
|
||||
</div>
|
||||
{queryResult && (
|
||||
<MediaMobilePagination
|
||||
<TablePagination
|
||||
pageIndex={pageIndex}
|
||||
pageSize={pageSize}
|
||||
pageSizeOptions={[50, 100, 200]}
|
||||
totalRows={total}
|
||||
pageCount={totalPages}
|
||||
onPaginationChange={handlePaginationChange}
|
||||
className="p-4"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user