mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 12:41:04 +00:00
feat(data-table): typeable rows-per-page control
The footer offered a fixed <select> of 5/10/25/50, so a table could not be paged in anything larger without a code change. Replace it with a Mantine Autocomplete: the presets go up to 500, and any other size can be typed. A typed value is clamped to [1, MAX_PAGE_SIZE] on commit, with MAX_PAGE_SIZE set to 500 to match @Max on the freight API's PaginationQueryDto and MAX_PAGE_SIZE in its pagination.util. Raising it here alone would turn the top preset into a 400. Changing the size also resets to the first page -- page 12 of 50-row pages does not exist once the page holds 500, and a manual-pagination consumer would happily request it. Done as one setPagination call so consumers see a single fetch rather than two.
This commit is contained in:
@@ -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<TData>({
|
||||
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<TData>({
|
||||
<label htmlFor="page-size" className="font-medium text-slate-700">
|
||||
{labels.rowsPerPage}
|
||||
</label>
|
||||
<select
|
||||
<Autocomplete
|
||||
id="page-size"
|
||||
value={pageSize}
|
||||
onChange={(e) => handlePageSizeChange(Number(e.target.value))}
|
||||
className="h-9 rounded-xl border border-slate-200 bg-white px-3 text-sm text-slate-700 outline-none transition focus:border-[#33578D]/50 focus:ring-2 focus:ring-[#33578D]/20"
|
||||
>
|
||||
{opts.pageSizeOptions?.map((size) => (
|
||||
<option key={size} value={size}>
|
||||
{size}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
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 && (
|
||||
|
||||
Reference in New Issue
Block a user