feat(storefront): paginate the catalogue instead of rendering all of it #269

Closed
opened 2026-09-02 10:57:21 -05:00 by bermudalamb · 2 comments
Owner

Split out of #186, whose test half is fixed by giving the e2e suite a throwaway database. This is the product half, and it is worth doing on its own merits rather than as a side effect of a test fix.

GET /api/items with no filter returns every item, and the storefront renders every one of them. There is no pagination and no limit. On a development database that had accumulated 1,662 items that took long enough to render to outrun a 5 second assertion timeout — but the test failure was the symptom, not the problem. A shop that renders its entire catalogue in one page does not scale, and the grid already has the filter controls that make paging tolerable.

Decisions this needs before implementation

  • Numbered pages, "load more", or infinite scroll. They read differently on a phone, which is where most of this traffic will be, and they differ in how a shared link behaves — a numbered page is linkable, an infinite scroll position is not.
  • Page size, and whether it changes with viewport.
  • Whether the count is shown. There is no result count in the UI today, which is also why the two assertions in #186 could not be rewritten to check "the result set widened" — they had nothing to count. Adding one would make those tests expressible as what they actually mean.

Also worth doing here

Two assertions name a specific fixture and expect it visible in the unfiltered grid:

  • filters.spec.ts → "removing a chip widens the results again"
  • favorites-filter.spec.ts → "shows a removable chip that restores the full catalogue"

Even paginated, "this fixture is on page one of an unfiltered catalogue" is not something a test can rely on. Both are worth rewriting to assert what they mean — that removing the filter widened the result set — which is straightforward once a count exists.

Not in scope

The API. GET /api/items returning everything is fine while the client asks for everything; if paging moves server-side that is a second change and should be measured rather than assumed, since the filter endpoints already do work per request.

Refs #186

Split out of #186, whose test half is fixed by giving the e2e suite a throwaway database. This is the product half, and it is worth doing on its own merits rather than as a side effect of a test fix. `GET /api/items` with no filter returns every item, and the storefront renders every one of them. There is no pagination and no limit. On a development database that had accumulated 1,662 items that took long enough to render to outrun a 5 second assertion timeout — but the test failure was the symptom, not the problem. A shop that renders its entire catalogue in one page does not scale, and the grid already has the filter controls that make paging tolerable. ## Decisions this needs before implementation - **Numbered pages, "load more", or infinite scroll.** They read differently on a phone, which is where most of this traffic will be, and they differ in how a shared link behaves — a numbered page is linkable, an infinite scroll position is not. - **Page size**, and whether it changes with viewport. - **Whether the count is shown.** There is no result count in the UI today, which is also why the two assertions in #186 could not be rewritten to check "the result set widened" — they had nothing to count. Adding one would make those tests expressible as what they actually mean. ## Also worth doing here Two assertions name a specific fixture and expect it visible in the unfiltered grid: - `filters.spec.ts` → "removing a chip widens the results again" - `favorites-filter.spec.ts` → "shows a removable chip that restores the full catalogue" Even paginated, "this fixture is on page one of an unfiltered catalogue" is not something a test can rely on. Both are worth rewriting to assert what they mean — that removing the filter widened the result set — which is straightforward once a count exists. ## Not in scope The API. `GET /api/items` returning everything is fine while the client asks for everything; if paging moves server-side that is a second change and should be measured rather than assumed, since the filter endpoints already do work per request. Refs #186
Author
Owner

Decisions, answering the three questions in the description.

Numbered pages, using Ant Design's pagination controls. Not "load more" and not infinite scroll. A numbered page is linkable and an infinite scroll position is not, which is what settles it for a shop where people send each other links to things.

Without the "Go to … page" jump box. It earns its place on a table of thousands of rows and not on a catalogue someone is browsing.

Page size defaults to 10, and the customer sets their own preference from 10, 20, 50 or 100. So the default is the conservative one, and anyone who wants a denser grid asks for it once.

The total item count is shown. That was the third question, and it is also what unblocks the two rewrites this issue already calls for: filters.spec.ts → "removing a chip widens the results again" and favorites-filter.spec.ts → "shows a removable chip that restores the full catalogue" both currently name a fixture and expect it in the unfiltered grid, which no paginated catalogue can promise. With a count they can assert what they actually mean — that removing the filter widened the result set.

Still not in scope, unchanged from the description: GET /api/items keeps returning everything while the client asks for everything. Moving paging server-side is a second change and should be measured rather than assumed.

Not yet started — no branch, no commits. Recorded here because these decisions were taken in conversation and had not been written down anywhere durable.

Decisions, answering the three questions in the description. **Numbered pages, using Ant Design's pagination controls.** Not "load more" and not infinite scroll. A numbered page is linkable and an infinite scroll position is not, which is what settles it for a shop where people send each other links to things. **Without the "Go to … page" jump box.** It earns its place on a table of thousands of rows and not on a catalogue someone is browsing. **Page size defaults to 10, and the customer sets their own preference** from 10, 20, 50 or 100. So the default is the conservative one, and anyone who wants a denser grid asks for it once. **The total item count is shown.** That was the third question, and it is also what unblocks the two rewrites this issue already calls for: `filters.spec.ts` → "removing a chip widens the results again" and `favorites-filter.spec.ts` → "shows a removable chip that restores the full catalogue" both currently name a fixture and expect it in the unfiltered grid, which no paginated catalogue can promise. With a count they can assert what they actually mean — that removing the filter widened the result set. Still not in scope, unchanged from the description: `GET /api/items` keeps returning everything while the client asks for everything. Moving paging server-side is a second change and should be measured rather than assumed. Not yet started — no branch, no commits. Recorded here because these decisions were taken in conversation and had not been written down anywhere durable.
Author
Owner

Done. Merged as PR #286.

All four decisions from the comment above are implemented: Ant Design's numbered pagination, no jump box, a default of 10 with a remembered 10/20/50/100 choice, and the total item count shown.

Two things worth recording.

The page number went in the URL; the page size did not. A linkable page is the whole reason numbered pages were chosen over infinite scroll, and the URL is already the single source of truth for filter state. Page size is a preference belonging to one person — putting it in the URL would mean sharing a link to an item also imposed your page size on whoever opened it. It lives in localStorage, which also keeps the change inside this issue's own "the API is out of scope" boundary. Returning to page one when a filter changes came for free: filtersToSearchParams builds a fresh URLSearchParams, so applying a filter drops the page while paging keeps the filters.

The two brittle assertions this issue predicted were exactly as predicted. filters.spec.ts and favorites-filter.spec.ts each named a fixture and expected it visible in the unfiltered grid — impossible once ten items make a page over a catalogue of thousands. Both now assert what they always meant, that removing a filter widened the result set, which is what the visible count unblocked.

Writing the tests also found a real defect rather than just covering the feature: the control rendered during loading, showing "0 items" before the real count arrived, because the empty-state early return only fires once loading has finished. It is now suppressed until there is something to count — better UI, and what makes the count usable as a signal in a test.

Confirmed in CI: runs 809/810 on main at b8ea33f passed, end-to-end step included.

Done. Merged as PR #286. All four decisions from the comment above are implemented: Ant Design's numbered pagination, no jump box, a default of 10 with a remembered 10/20/50/100 choice, and the total item count shown. Two things worth recording. **The page number went in the URL; the page size did not.** A linkable page is the whole reason numbered pages were chosen over infinite scroll, and the URL is already the single source of truth for filter state. Page size is a preference belonging to one person — putting it in the URL would mean sharing a link to an item also imposed your page size on whoever opened it. It lives in `localStorage`, which also keeps the change inside this issue's own "the API is out of scope" boundary. Returning to page one when a filter changes came for free: `filtersToSearchParams` builds a fresh `URLSearchParams`, so applying a filter drops the page while paging keeps the filters. **The two brittle assertions this issue predicted were exactly as predicted.** `filters.spec.ts` and `favorites-filter.spec.ts` each named a fixture and expected it visible in the unfiltered grid — impossible once ten items make a page over a catalogue of thousands. Both now assert what they always meant, that removing a filter widened the result set, which is what the visible count unblocked. Writing the tests also found a real defect rather than just covering the feature: the control rendered *during* loading, showing "0 items" before the real count arrived, because the empty-state early return only fires once loading has finished. It is now suppressed until there is something to count — better UI, and what makes the count usable as a signal in a test. Confirmed in CI: runs 809/810 on `main` at `b8ea33f` passed, end-to-end step included.
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bermudalamb/redefined-designs#269