Skip to content

feat(frontend): add Load more to History and queue from all pages - #1358

Merged
IAmJSD merged 3 commits into
LeanerCloud:mainfrom
IAmJSD:ui/issue-676
Oct 24, 2026
Merged

IAmJSD merged 3 commits into
LeanerCloud:mainfrom
IAmJSD:ui/issue-676

Conversation

@IAmJSD

@IAmJSD IAmJSD commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator

The Purchase History view could only show the first page of rows; when the API hit its row cap the UI showed a "Results are capped... Narrow the date range" banner and older rows were unreachable. This PR adds a "Load more" button that appends the next page using the keyset cursor from backend PR #1277 and re-renders both the history table and the Approval Queue from every loaded row. On servers that don't page yet it behaves exactly as today.

Refs #676
Partial: the visible fix depends on backend PR #1277 (keyset paging for #674), which is still a draft and not merged. Until it lands, the API never returns has_more, so the button never appears.

What changed

  • frontend/src/types.ts, frontend/src/api/types.ts: HistoryResponse gains has_more, next_cursor and actionable_executions_limit; HistoryFilters gains cursor. frontend/src/api/history.ts sends cursor.
  • New frontend/src/history-paging.ts: pure helpers (nextHistoryCursor, mergeHistoryPages, which de-duplicates by purchase_id keeping the newest response, and historyTruncationMessage) plus the button state.
  • frontend/src/history.ts:
    • loadHistory/viewPlanHistory reset paging and remember the first page's filters and cursor.
    • New exported loadMoreHistory() fetches the next page with the same filters plus cursor=next_cursor, appends the rows, and re-renders the history table and the Approval Queue from all loaded rows. The summary cards keep the first page's totals.
    • A generation counter drops a Load more response that lands after a reload or filter change. A failed page toasts and keeps the button for retry.
    • The "results capped" banner is driven by has_more: hidden when Load more can reach the older rows, reworded to quote the actionable cap when truncated without more pages, and unchanged when the server doesn't send has_more.
  • frontend/src/index.html: a hidden "Load more" button under #history-list; styles/tables.css centers it.
  • Tests: new __tests__/history-load-more.test.ts, a cursor case in api-history.test.ts, and tests-e2e/history-load-more.spec.ts.

Why this approach

It follows the "Frontend follow-up" section of #1277 exactly: cursor rather than offset, de-dupe by purchase_id, banner only when truncated and !has_more, summary from page 1. That way the two halves fit when the backend lands. The issue text asks for offset, but #1277 deliberately uses a keyset cursor and rejects offset with a 400, so the frontend follows the backend contract. With no has_more field there is no button and the old banner text verbatim, so this is safe to merge before #1277. New logic lives in a small module because history.ts is already about 2030 lines.

Screenshots

Screenshots were taken before this branch was merged with main; before = main at a14dc8f. After is this branch. Both are headless Chromium at 1440x900 against the repo's mocked e2e API, with a mocked paging API (page 1: 3 rows with has_more, page 2: 2 rows).

Before After
history-has-more before history-has-more after
(new behavior, no before) history-load-more-clicked after
  • With the API reporting more rows, main shows only page 1 and the "Results are capped... Narrow the date range" banner. After, the banner is gone and a "Load more" button sits under the table.
  • After clicking Load more: page 2's rows (elasticache, expired) are appended, the chips count 5 rows, and the summary cards are unchanged.

Verification

Run on this branch's head 184dab7a:

  • Merged with main at 5b52c4e; conflicts in frontend/src/history.ts resolved, see the merge commit.
  • Lint: 0 errors. Typecheck and build pass.
  • Jest: 3874 of 3875 passed, 1 skipped. New history-load-more.test.ts (9 cases: two-page append with the same filters plus cursor, queue built from both pages, newest-copy-wins de-dupe, reload discards an in-flight page with no double fetch, failed page retry, button wiring, banner text per has_more shape) and a cursor case in api-history.test.ts. Before the merge, the new tests passed with --randomize (34).
  • Playwright via e2e-local.sh: 201 passed, including the new history-load-more.spec.ts (clicks Load more against a mocked two-page /api/history).
  • git-secrets scan clean (planted key flagged); scripts/test-git-secrets-allowlist.sh: 137 pass, 0 fail.

Notes for review

Overlaps

Note for the UI owner

This changes UI code that @cristim owns. IAmJSD reviewed the before/after screenshots and asked for it to be merged; any design point you disagree with can be a follow-up.

🤖 Generated with Claude Code

IAmJSD and others added 2 commits October 23, 2026 23:11
…l loaded pages

The History view could only show the first page of rows and told the
user to narrow the date range when the API capped the result. The
History API is gaining keyset paging (has_more / next_cursor, LeanerCloud#674),
so the UI now offers a "Load more" button under the Purchase History
table when the response reports has_more. Each click repeats the
first page's filters with cursor=next_cursor, appends the rows, and
re-renders both the history table and the Approval Queue from every
loaded row. Rows repeated on a later page (an approval swept to
expired between loads) replace their stale copy by purchase_id. The
summary cards keep the first page's totals.

The "results capped" banner is driven by has_more: hidden when Load
more can reach the older rows, reworded to quote the actionable cap
when truncated without more pages, and unchanged on servers that do
not page yet, so this is a no-op until the backend ships LeanerCloud#674.

Refs LeanerCloud#676

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Jest cases with two pages: cursor and filters on the second request,
row append, Approval Queue built from both pages, newest copy wins on
a repeated purchase_id, a reload discarding an in-flight page, a
failed page keeping the button for retry, and the banner text for
each has_more shape. A Playwright spec clicks Load more against a
mocked two-page /api/history.

Refs LeanerCloud#676

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IAmJSD IAmJSD added priority/p2 Backlog-worthy severity/medium Moderate harm triaged Item has been triaged urgency/this-quarter Within the quarter impact/many Affects most users effort/s Hours type/feat New capability labels Oct 24, 2026
@coderabbitai

coderabbitai Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Caution

Review failed

The pull request is closed.

Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

Conflicts:
- frontend/src/history.ts: add/add in the import block; kept both the
  history-paging imports (this branch, LeanerCloud#676) and main's
  renderApprovalQueueLink/wireApprovalQueueLinks import (LeanerCloud#1331). The rest
  merged cleanly: loadHistory/viewPlanHistory keep main's fail-closed
  four-eyes default, queue links and per-table popovers, and still reset and
  update Load more paging; loadMoreHistory re-renders main's Approval Queue
  from all loaded pages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IAmJSD

IAmJSD commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): APPROVE 184dab7a2e5211b1f8e13ae444ab174ca493f2db

Blocking:

  • none
    Non-blocking:
  • frontend/src/history.ts:445/470: loadMoreInFlight is module-global and not tied to historyLoadGeneration. Scenario: the user clicks Load more, then changes a filter before that page returns. resetHistoryPaging (422) re-enables the button and the new first page has has_more. Clicking Load more then does nothing until the stale request settles, even though the button looks enabled. Fix: block only when the in-flight generation matches the current one, or clear the flag in resetHistoryPaging.
  • frontend/src/history-paging.ts:71-74: during a load the focused button is disabled, and after the last page it is hidden. Keyboard focus drops to and nothing announces the appended rows. Fix: focus the first new row, or add a polite status line.
  • frontend/src/history.ts:460: loadMoreHistory re-renders the tables without calling closeOpenHistoryPopover() first, unlike loadHistory (370). Harmless in practice, because the click is an outside click and closes the popover.
  • mergeHistoryPages moves a row that appears again to the end of the list instead of keeping its original position. Cosmetic, and tested.
    Attacked: The merge parents are febc800 and 5b52c4e. git show --cc has only an import add/add conflict, and both imports were kept. git diff HEAD^2 HEAD removes nothing of main except the 6 lines the branch intends to replace: the getHistory call, the inline banner ternary and el.hidden = !data.truncated. Main's fail-closed _fourEyesMode, the feat(frontend): make the Approval queue the one approval surface #1331 queue links, the fix(frontend): scope the Purchases tab filter popovers per table #1342 popovers and the fix(frontend): show the monthly charge and term total before purchase #1364 upfrontCell all survive.
    Full origin/main...HEAD review: interpolation goes through existing escaped renderers, the button uses textContent, there are no inline styles, the listener is registered once, and generation/epoch guards cover reloads and mutations. Param names match backend PRs feat(api): page the History view with a keyset cursor (has_more, next_cursor) #1277/fix(history): flag a failed execution class listing as truncated and expose the actionable cap #1276. merge-tree with main 51aa5b1 is clean. Scratch jest: 16 suites / 212 tests passed, and history-load-more passes under --randomize.

@IAmJSD

IAmJSD commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

Local verification at 184dab7a merged onto current main (f9c85b7c; merge-tree 59e7adc1; Linux, Node, headless Chromium):

  • This branch's own checks passed at this head: lint, typecheck, build, the full jest suite, the full Playwright e2e suite, and the git-secrets checks (see Verification in the PR body).
  • The combined tree of every UI PR in this merge wave (tree dc090cc2, main plus 17 PRs) passes npm ci, lint, typecheck, build, jest (1 skipped, 4097 passed, 4098 total), and the full Playwright suite (232 passed).
  • CI is green on 184dab7a (CI Success, Run pre-commit hooks).
  • The independent review above approves this exact head.
  • Nothing was applied to any cloud.

@IAmJSD
IAmJSD marked this pull request as ready for review October 24, 2026 19:08
@IAmJSD
IAmJSD merged commit f5cb7ae into LeanerCloud:main Oct 24, 2026
29 checks passed
@IAmJSD

IAmJSD commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

Marked ready and merged by IAmJSD's agent, at IAmJSD's request (IAmJSD reviewed the before/after screenshots; cristim is traveling), after the independent review and local verification above, all at 184dab7a.

IAmJSD added a commit to IAmJSD/cloud-commitments-platform that referenced this pull request Oct 24, 2026
Conflicts:
- frontend/src/history.ts: imports kept both (this branch's iconSvg and
  main's history-paging helpers from LeanerCloud#1358). Main (LeanerCloud#1394) moved the
  retry lineage links into renderLineageLinks() and appends them to the
  ops-hint badge; took main's structure and applied this branch's icons
  inside it: the ops hint uses iconSvg('alert-triangle') and both lineage
  links use iconSvg('rotate-cw') instead of the restored glyphs.
- frontend/src/ladder.ts: imports kept both (iconSvg and main's
  openModal/closeModal from LeanerCloud#1372).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/s Hours impact/many Affects most users priority/p2 Backlog-worthy severity/medium Moderate harm triaged Item has been triaged type/feat New capability urgency/this-quarter Within the quarter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant