[Plugin] Previous/Next paging and a page position on both admin lists - #241
Merged
Conversation
Both React lists gain a pager beside `Load more`. `Next` moves forward one page and `Previous` moves back one, both exactly and both free: the console keeps the cursors the service has already handed it and replays one to go back. No reverse keyset read, no new query, nothing new on the wire. Both controls write the page to the address through the path `Load more` already used, so Back and Forward keep walking the pages an operator actually visited. PREVIOUS RE-REQUESTS RATHER THAN RESTORING. The stack holds cursors, not pages. A request under a token the service already issued answers with the collection as it stands now, agrees with a reload of the very same address, and does not grow without bound down a long scan; replaying rows kept in memory would do the opposite of all three, and on Pricing & inventory the column an operator came back to check is stock. THE TWO WAYS FORWARD ARE TWO ACTS. `Previous`/`Next` move a one-page window; `Load more` extends one and keeps the rows above. Both advance the same position, so the page number counts either — what differs is whether the rows stay. A pager step therefore continues from where an accumulated scan reached and collapses the view to that page, which is the stated cost of offering both. The intent rides on the cursor rather than beside it, so a REPLACE cursor can never be read as an EXTEND one by an effect that saw two state writes in the wrong order. M IS DERIVED, NEVER FETCHED. The service already counts the filtered set beside the page it returns and the plugin already states the limit it pages by, so the page count is arithmetic over two values the render is holding. It is refused on exactly the totals the count line refuses — one shared gate — so `Page 2 of 6` and `137 orders` cannot contradict each other, and an absent total leaves an em dash rather than a guess: absent is not one and it is not zero. A render whose response carried no next cursor states its own page as the count, which is direct evidence rather than arithmetic and the only answer available with no total at all. THE STACK RESETS WITH THE PREDICATE. A cursor is only meaningful against the filter it was issued under, so a stack that survived an apply would offer to step back into the set the operator just left. A DEEP LINK CANNOT KNOW ITS PAGE. An address names which page, never how many came before it, so the position reads `Page — of 6` and `Previous` is offered dimmed with the reason: this screen has no record of the page before this one. Paging forward from such a link still comes back to it. Inventing "page 2" because one cursor was seeded would be a number an operator reconciles against and loses. Where paging has stopped — a failed page, or a continuation refused mid-scan — the whole pager is withdrawn with `Load more`. The rows stay exactly where they are; the paging goes, because the screen has just disowned its position in the address. The mid-scan refusal semantics are otherwise untouched. The controls dim with `aria-disabled` rather than `disabled`, so they keep their tab stop and their focus ring: pressing `Next` onto the last page would otherwise take the control out from under the operator's focus. Both go unavailable on the click rather than on the effect, so the commit in between cannot take a second press and push the same cursor twice. Pinned by unit tests over the stack and the pager view, by presentation tests over the page count and its coordination with the count line, and by a DOM suite that walks two real screens: Next twice, Previous, the address after each, a deep link, a filter apply, an absent total, an accumulated scan, and a refusal.
… honest Review follow-ups on the Previous/Next work, all of them cases where the pager stated something it could not back. THE STACK SURVIVES THE BROWSER'S OWN BACK. The address carries one cursor, so a traversal landed on a page with nothing behind it: the position fell to a dash and `Previous` dimmed, two presses into a scan, for no reason visible on screen. A history ENTRY is not a link, and can hold what a link must not — so the walk rides in `history.state`, is read back on `popstate`, and seeds the remounted list. Back mid-walk and Back-then-Forward both keep the position and a live `Previous`. PUSH AND CORRECT ARE NOW SAID, NOT INFERRED. "No cursor" meant page one, and page one is reached both by the recovery from a page that would not open and by an operator pressing `Previous` from page two — so every deliberate step back was overwriting the entry it stepped from. The list states the intent; the screen decides the entry. A FAILED PAGE NEVER CLEARS THE ROWS. The old test was "did a cursor go out", which is false for `Previous` onto page one, so a failure there wiped a screenful of rows that were still a true answer. It is now "did the operator ask for this page", and page one asked for is a request like any other rather than the absence of one. A PAGER STEP KEEPS THE LOW-STOCK LATCH. Every request carrying a cursor reports the filter as available by contract — the predicate rode inside the token — so inheriting that only on `Load more` meant `Next` dropped the banner a settings blip had raised and began captioning the whole catalogue as low stock. The latch rides on the continuation, not on the merge, and the withheld total goes with it. CLAIMS TRIMMED TO WHAT IS KNOWN. The page count now consumes the figure the count line actually stated rather than the raw payload one, so a total a scope withheld cannot reappear beneath it. A last page whose arithmetic insists there are more pages states neither figure instead of picking one. A window of several pages states the range it describes, because "Page 3" over fifty rows beginning at page two tells whoever is reading the top of that list the wrong number. And `Next` says what paging on costs before the click rather than after it. The dimmed `Previous` states ignorance — the page before this one is not known here — instead of asserting a provenance a reload, a bookmark and a link all reach identically. A LINK TO THE LAST PAGE KEEPS ITS PAGER. Neither control can move there, which is exactly when the position is the only thing that can answer the question that operator arrived with. THE CONTROL MOVED TO THE SHARED FILE and gained the accessibility it was claiming. Its reason is an accessible description rather than only a tooltip (the earlier note had that backwards — a tooltip is a pointer affordance); the dimming is carried by the border so the label stays legible; and the unavailable cursor is a sheet rule, since an ARIA state cannot be matched by `:disabled`. Two invariants pinned rather than guarded: the same cursor pushed twice is one page, so a batched double press cannot deepen the position past the walk; and a pop with nowhere to go is inert, so a stack and a cursor that ever disagreed cannot invent a page. The refusal copy names no direction — three controls produce it now, and only one of them is "more".
…very entry Round-two review follow-ups. Three of them were the pager making a claim from the wrong evidence. A REQUEST THAT CARRIED NO CURSOR IS PAGE ONE, whoever asked for it. The classifier read the OPERATOR's intent instead, so `Previous` off the bottom of the stack was treated as a page move — and that was wrong in two directions at once. It captioned a render that IS the first page as not-the-first, which takes the whole-collection empty copy away and puts the "on this page" hedge on a count the render could prove; and on Pricing & inventory it carried the low-stock latch over a response entitled to clear it, so a banner raised by a settings blip could never be dismissed, while a blip happening ON that request could not raise one — reproducing the mislabel where an unfiltered catalogue is captioned as low stock with a whole-catalogue count behind it. The failure classification stays on "did the operator ask for this page", and keeping the two questions apart is what makes answering the first off the wire safe. EVERY HISTORY WRITER CARRIES THE WALK, through one helper rather than five object literals. The record drill-in pushes an entry that is a list entry wearing a record's address, and it was composing that entry from the record id alone — dropping the page one click after it was earned, so `Back to orders` from a reloaded record landed on a list that could not name its own page. The two replacing writers now merge into the entry rather than clobbering it, so a writer that only means "no record is open" cannot take the stack with it, and whatever else shares the entry survives all five. THE DIM WAS OVERCORRECTED. The first cut took the label down with the border; the second made unavailable and live nearly indistinguishable, which is worse — a control that looks pressable and does nothing. The state is now carried by a flat fill, a visibly lighter border, and a label muted through the theme's own foreground: unmistakably off at a glance, and still a word rather than work. Also: the deep-link latch exception is stated as a known limit rather than left implied; the two failure-title constants are renamed to match the direction-neutral copy they already carry; the partial-card comments say "paging bar" rather than "where Load more was"; the span clamp says why it clamps; and the forward step documents why it reads the stack out of its closure and what the busy guard is still load-bearing for. Newly pinned: both latch directions on the page-one path, the two `firstPage` consequences, the drill-in-reload-back walk, Back/Forward around a record, the entry-state merge, hostile and malformed `history.state`, and the last-page link in the DOM tier. Each was checked by mutation — reverted, watched fail, restored.
…tually is Two review follow-ups. THE IDEMPOTENCE CLAIM WAS UNPINNED. `pushedPage` refuses a cursor equal to the top of the stack, and the note beside it argues that this is an invariant rather than a guard — but no test held it: none of the call sites pushes the current top twice, so every existing test passed with the check deleted. It has one now, and the neighbouring "a pop with nowhere to go is inert" claim has one too. Both were checked by deleting the line and watching them fail. AND THE MUTED LABEL'S RATIONALE WAS WRONG. `color-mix(…, transparent)` IS an alpha; the comment claimed it was not, which is dressing up the same technique in better words. What it actually buys is WHERE the alpha lands: an `opacity` fades the whole control, so the fill and the border that carry the unavailable state fade at exactly the rate the word on it does and the two jobs compete. Mixing only the label leaves both at full strength. Stated against `currentColor` so it follows the theme rather than pinning a grey that is legible in one of them.
Contributor
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.












Summary
Both admin lists gain a Previous/Next pager driven by a client-side cursor trail — Next
pushes, Previous re-requests the popped cursor, the walk rides
history.stateso browserBack/Forward and a reload keep the position, and a page opened from a shared link degrades
honestly (dimmed Previous with its reason, position shown; the walk before it is unknown).
Page N of M derives from the same stated total the count line uses — an absent total renders
an em dash, the two lines share one gate and cannot disagree, an accumulated scan states its
window ("Pages 1–2 of 6"), and the last page prefers direct evidence over arithmetic.
The pager composes with Load more (one shared position), the low-stock degradation banner
(page-one answers are authoritative; continuations latch), the refusal recovery from the
cursor work, and the paging-stopped state.
Test plan
history.stateparsing, both latch directions, history-entry semantics, empty-state/countcoordination) — all green
last-page links, filter resets, absent-total rendering, drill-in → reload → back