Ubisam-MobileGit/tasks/028-issues-prs-numbered-pagination.md
choiminyeong 2818dab09a TASK-027/028: image+CSV viewer with gallery swipe, numbered Issues/PR pagination
TASK-027: opening an image (PNG/JPEG/GIF/BMP/WebP, detected by magic bytes)
now decodes and shows it, with swipe navigation to the previous/next image
in the same folder; opening a .csv file shows a scrollable table instead of
raw text. Both reuse the tab-swipe/content-scroll gesture-conflict fix so
neither ever triggers the Files/History tab to change underneath it.

TASK-028: the Issues/PR list shows up to 5 numbered page buttons plus a
[ page / total ] jump field, backed by Gitea's X-Total-Count header
(verified against Gitea's own source) and a server-side type=issues filter
so the count excludes pull requests. Every other paginated list is
unchanged, per this session's earlier scoping decision.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 21:50:24 +09:00

7.9 KiB
Raw Blame History

TASK-028: Numbered, jump-to-page pagination for the Issues/PR list

Status: implemented; all required checks pass; live device verification pending (no repository with existing issues/PRs was found this session — see Known limitations) Owner: Claude Reviewer: Codex

Outcome

The Issues/PR list now shows up to 5 numbered page buttons (tap to jump directly) plus previous/ next, and a [ page / total ]-style input field to type an exact page number and jump straight there — replacing the plain previous/next-only footer, on this screen only. Per the earlier AskUserQuestion resolution in this session ("이 기능은 전체 커밋/이슈 수를 알 수 있는 화면에만 적용"), every other paginated list in the app (repositories, history, notifications) keeps plain previous/next, since Gitea's endpoints for those don't report a total count — Issues/PRs do.

Scope

  • Included: domain/model/RepositoryModels.kt (Page.totalCount: Int? = null), data/workitems/GiteaWorkItemsApi.kt (X-Total-Count header parsing; listIssues now sends type=issues so both the returned items and that header exclude pull requests server-side), feature/workitems/WorkItemsViewModel.kt (totalPages on IssueList/PullRequestList, goToPage), feature/workitems/WorkItemsScreen.kt (NumberedPageFooter, page-window logic, the [ input / total ] jump field), MainActivity.kt (onGoToPage wiring).
  • Excluded: No change to repository/history/notification pagination (deliberately kept as plain previous/next — see Outcome). No change to how issue/PR detail screens work, only the list.

Design notes

  • type=issues is a real, documented Gitea query parameter, not a guess: verified directly against Gitea's own source (routers/api/v1/repo/issue.go's ListIssues swagger doc comment lists type with enum: [issues, pulls]) before relying on it — this project's established convention for anything not obviously provable from the client alone. The class-level doc comment on GiteaWorkItemsApi previously explained not using this parameter as a deliberate choice against "a guessed, unverified" one; that reasoning no longer applies now that it's verified, so the comment was rewritten rather than left stale. The existing client-side pullRequest == null filter is kept anyway, as defense in depth.
  • Why type=issues specifically matters for this task: without it, Gitea's issue-list endpoint returns issues and pull requests mixed together (a PR is internally an issue with a non-null pull_request field), and its X-Total-Count header would count both — producing a total-page count that overstates how many pages of genuine issues actually exist. Sending type=issues makes the count (and the returned items) issues-only at the source.
  • Page<T>.totalCount is optional and defaulted (totalCount: Int? = null on both the class and Page.of(...)), specifically so none of the other 9+ existing Page.of(...) call sites (repositories, branches, tags, commits, notifications, comments, reviews — none of which have a total count available) needed to change.
  • The numbered-pagination UI falls back to the plain previous/next footer whenever totalPages is null (e.g. a hypothetical Gitea instance old enough to not send the header) — a defensive fallback, not the expected path, but one that avoids ever showing a broken or missing page count instead of just quietly degrading to what every other list already does.
  • The 5-button page window slides, centered on the current page, clamped to stay within [1, totalPages] — page 1 of a large list shows pages 1–5 (matching "1페이지에선 5까지 보여줌" exactly), and the window keeps recentering as the user navigates rather than freezing once it nears either end.
  • No icon library dependency was added for the "🔍"-style jump control asked for — this project has never used androidx.compose.material.icons (every other screen uses plain text labels, e.g. "다시 시도" instead of a retry icon), so the jump button uses a plain text label ("이동") consistent with that, rather than pulling in material-icons-core for one button.

Acceptance criteria

  • The Issues list and the PR list each show up to 5 numbered page buttons, previous/next, and a page-jump input field, when the server reports a total count.
  • Tapping a numbered page button jumps directly to that page.
  • Typing a page number and submitting (IME "Go" or the "이동" button) jumps to that page, clamped to [1, totalPages].
  • X-Total-Count is correctly excluded of pull requests for the Issues list (via type=issues) and reflects genuine issue count.
  • A fractional last page rounds up (45 items at page size 20 is 3 pages, not 2).
  • Every other paginated list (repositories, history, notifications) is unchanged — still plain previous/next.
  • Build, unit tests, and lint pass.
  • Live device confirmation against a repository with enough real issues/PRs to see the numbered UI populated — not yet possible this session; see Known limitations.

Required checks

./gradlew assembleDebug
./gradlew testDebugUnitTest
./gradlew lintDebug

Evidence

  • RepositoryModels.kt: Page<T> gained totalCount: Int? = null; Page.of(...) validates it's non-negative when present.
  • GiteaWorkItemsApi.kt: listIssues now requests type=issues; toPage parses X-Total-Count (case-insensitive header lookup, matching the existing X-HasMore pattern) into Page.totalCount.
  • WorkItemsViewModel.kt: IssueList/PullRequestList gained totalPages: Int? = null, computed via a new private Page<T>.totalPages(pageSize) extension (ceil(total / pageSize), at least 1); new goToPage(page: Int).
  • WorkItemsScreen.kt: PageFooter now branches on totalPages — null falls back to the original previous/next row unchanged; non-null renders the new NumberedPageFooter (page-window buttons + [ input / total ] jump row) instead. New private pageWindow(current, totalPages, windowSize=5).
  • New tests: GiteaWorkItemsApiTest.kt (type=issues sent, X-Total-Count parsed for both issues and pull requests, absent header leaves totalCount null), WorkItemsViewModelTest.kt (45 items/page size 20 rounds up to 3 pages, missing total count leaves totalPages null, goToPage jumps directly to the requested page), RepositoryModelsTest.kt (totalCount defaults to null, is exposed when given, rejects a negative value).
  • ./gradlew compileDebugKotlin compileDebugUnitTestKotlin: BUILD SUCCESSFUL.
  • ./gradlew testDebugUnitTest: BUILD SUCCESSFUL, all unit tests passing (including the new ones above).
  • ./gradlew assembleDebug lintDebug: BUILD SUCCESSFUL, 0 lint errors.
  • Live device: build installed over wireless adb. Checked the Issues and Pull Request tabs on four of the human product owner's actual repositories; every one had zero issues and zero pull requests, so the numbered-pagination UI itself was never actually populated with real data this session (see Known limitations) — confirmed instead that the screen doesn't crash and correctly shows the existing empty state on all four.

Known limitations

  • Not yet confirmed against a real repository with enough issues/PRs to populate the numbered UI with real data. Every repository checked live this session had zero issues and zero pull requests. The header-parsing, total-page-count, and jump-to-page logic are covered by unit tests instead, but the actual rendered page-button row and jump field haven't been seen with real multi-page data yet.
  • The page-jump text field accepts any positive integer typed, including one exceeding totalPages — it's clamped before being submitted (coerceIn(1, totalPages)), so this can't request an out-of-range page, but the field itself doesn't visually reject the keystroke.

Review findings

None yet — pending Codex/human review.