diff --git a/packages/ui-common/src/components/data-table/footer.tsx b/packages/ui-common/src/components/data-table/footer.tsx index daa818eaa..80cc951c8 100644 --- a/packages/ui-common/src/components/data-table/footer.tsx +++ b/packages/ui-common/src/components/data-table/footer.tsx @@ -1,6 +1,24 @@ +import { useEffect, useMemo, useState } from "react"; +import { Autocomplete } from "@mantine/core"; import { Button } from "../button"; import { DataTableFooterProps } from "./types"; +/** + * Upper bound on a hand-typed page size. Not a free choice — the server has to + * accept it. It is kept in step with `@Max` on the freight API's + * `PaginationQueryDto.pageSize` and with `MAX_PAGE_SIZE` in its + * `common/utils/pagination.util.ts`; raising it here alone turns the top preset + * into a 400 on every endpoint that validates against those. + */ +export const MAX_PAGE_SIZE = 500; + +/** Clamp a typed page size to a whole number in [1, MAX_PAGE_SIZE]; null if unusable. */ +export function clampPageSize(raw: string | number): number | null { + const parsed = Math.trunc(Number(raw)); + if (!Number.isFinite(parsed) || parsed < 1) return null; + return Math.min(parsed, MAX_PAGE_SIZE); +} + export interface DataTableFooterOptions { pageSizeOptions?: number[]; showPageSizeSelector?: boolean; @@ -25,7 +43,7 @@ interface DataTableFooterComponentProps< } const defaultOptions: DataTableFooterOptions = { - pageSizeOptions: [5, 10, 25, 50], + pageSizeOptions: [5, 10, 25, 50, 100, 200, 500], showPageSizeSelector: true, showRowCount: true, showPagination: true, @@ -56,8 +74,36 @@ export function DataTableFooter({ const start = totalCount === 0 ? 0 : pageIndex * pageSize + 1; const end = Math.min((pageIndex + 1) * pageSize, totalCount); + // Back to the first page: page 12 of 50-row pages does not exist once the + // rows per page becomes 500, and a manual-pagination consumer would happily + // request it. One state update, so consumers see a single fetch. const handlePageSizeChange = (newPageSize: number) => { - table?.setPageSize(newPageSize); + table?.setPagination({ pageIndex: 0, pageSize: newPageSize }); + }; + + // The presets, with the active size folded in when it was typed by hand, so + // the dropdown always contains what the input shows. + const sizeOptions = useMemo(() => { + const presets = opts.pageSizeOptions ?? []; + const all = presets.includes(pageSize) ? presets : [...presets, pageSize]; + return [...all].sort((a, b) => a - b).map(String); + }, [opts.pageSizeOptions, pageSize]); + + const [sizeDraft, setSizeDraft] = useState(`${pageSize}`); + + // Keep the input honest when the page size changes from anywhere else. + useEffect(() => { + setSizeDraft(`${pageSize}`); + }, [pageSize]); + + const commitPageSize = (raw: string) => { + const size = clampPageSize(raw); + if (size === null) { + setSizeDraft(`${pageSize}`); + return; + } + setSizeDraft(`${size}`); + if (size !== pageSize) handlePageSizeChange(size); }; return ( @@ -69,18 +115,23 @@ export function DataTableFooter({ - + aria-label={labels.rowsPerPage} + size="sm" + w={96} + inputMode="numeric" + value={sizeDraft} + data={sizeOptions} + comboboxProps={{ position: "top", withinPortal: true }} + onChange={setSizeDraft} + onOptionSubmit={commitPageSize} + onBlur={() => commitPageSize(sizeDraft)} + onKeyDown={(e) => { + if (e.key === "Enter") commitPageSize(sizeDraft); + if (e.key === "Escape") setSizeDraft(`${pageSize}`); + }} + /> )} {opts.showRowCount && (