The /history page fetched a fixed first 200 POs in one flat table with no
way to page further and no control over page size. Replace that with real
pagination:
- page.tsx: read page/perPage from searchParams; clamp perPage to 25/50/100
(default 25) and page to [1, totalPages] via a new shared resolvePagination
helper. Swap the fixed take:200 for skip/take + a count() for totals.
Replace the "first 200 results" notice with a footer ("Showing X-Y of N",
Prev/Next, page indicator) that preserves all filters. Export PDF/CSV links
stay on the full filtered set.
- history-filters.tsx: add a Per-page dropdown; changing it or any filter
resets to page 1 while preserving perPage in the URL.
- lib/pagination.ts: dependency-free clamp/skip/take helper, unit-tested.
Verified: type-check clean, 272 unit tests pass (9 new), skip/take windows
and clamping checked against the test DB.
Fixes #104
65 lines
2.3 KiB
TypeScript
65 lines
2.3 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { resolvePagination } from "@/lib/pagination";
|
|
|
|
const OPTIONS = [25, 50, 100];
|
|
const DEFAULT = 25;
|
|
|
|
function resolve(perPageParam: string | number | undefined, pageParam: string | number | undefined, total: number) {
|
|
return resolvePagination({ perPageParam, pageParam, total, options: OPTIONS, defaultPerPage: DEFAULT });
|
|
}
|
|
|
|
describe("resolvePagination", () => {
|
|
it("defaults perPage and page when params are missing", () => {
|
|
expect(resolve(undefined, undefined, 200)).toEqual({
|
|
perPage: 25,
|
|
page: 1,
|
|
totalPages: 8,
|
|
skip: 0,
|
|
take: 25,
|
|
});
|
|
});
|
|
|
|
it("accepts allowed page sizes", () => {
|
|
expect(resolve("50", "1", 200).perPage).toBe(50);
|
|
expect(resolve("100", "1", 200).perPage).toBe(100);
|
|
expect(resolve(50, "1", 200).perPage).toBe(50);
|
|
});
|
|
|
|
it("falls back to the default for disallowed or non-numeric page sizes", () => {
|
|
expect(resolve("10", "1", 200).perPage).toBe(25);
|
|
expect(resolve("999", "1", 200).perPage).toBe(25);
|
|
expect(resolve("abc", "1", 200).perPage).toBe(25);
|
|
expect(resolve("0", "1", 200).perPage).toBe(25);
|
|
});
|
|
|
|
it("computes skip/take for a middle page", () => {
|
|
const p = resolve("25", "3", 200);
|
|
expect(p).toMatchObject({ page: 3, skip: 50, take: 25, totalPages: 8 });
|
|
});
|
|
|
|
it("clamps a page beyond the last page to the last page", () => {
|
|
expect(resolve("25", "99", 200)).toMatchObject({ page: 8, totalPages: 8, skip: 175 });
|
|
});
|
|
|
|
it("clamps non-positive / non-numeric page to 1", () => {
|
|
expect(resolve("25", "0", 200).page).toBe(1);
|
|
expect(resolve("25", "-5", 200).page).toBe(1);
|
|
expect(resolve("25", "abc", 200).page).toBe(1);
|
|
expect(resolve("25", undefined, 200).page).toBe(1);
|
|
});
|
|
|
|
it("floors fractional page numbers", () => {
|
|
expect(resolve("25", "2.9", 200).page).toBe(2);
|
|
});
|
|
|
|
it("always yields at least one page, even with zero rows", () => {
|
|
expect(resolve("25", "1", 0)).toMatchObject({ page: 1, totalPages: 1, skip: 0 });
|
|
});
|
|
|
|
it("handles a partial final page", () => {
|
|
// 23 rows, 25 per page -> single page holding all rows
|
|
expect(resolve("25", "1", 23)).toMatchObject({ page: 1, totalPages: 1, take: 25 });
|
|
// 60 rows, 25 per page -> 3 pages, last page holds 10
|
|
expect(resolve("25", "3", 60)).toMatchObject({ page: 3, totalPages: 3, skip: 50 });
|
|
});
|
|
});
|