No task waits on a Codex review; the redesign tasks are confirmed on the product owner's Z Fold7; TASK-016's leftovers are on hold; no biometric reauthentication. Agent: main (opus) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
9.1 KiB
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-Countheader parsing;listIssuesnow sendstype=issuesso both the returned items and that header exclude pull requests server-side),feature/workitems/WorkItemsViewModel.kt(totalPagesonIssueList/PullRequestList,goToPage),feature/workitems/WorkItemsScreen.kt(NumberedPageFooter, page-window logic, the[ input / total ]jump field),MainActivity.kt(onGoToPagewiring). - 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=issuesis a real, documented Gitea query parameter, not a guess: verified directly against Gitea's own source (routers/api/v1/repo/issue.go'sListIssuesswagger doc comment liststypewithenum: [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 onGiteaWorkItemsApipreviously 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-sidepullRequest == nullfilter is kept anyway, as defense in depth.- Why
type=issuesspecifically 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-nullpull_requestfield), and itsX-Total-Countheader would count both — producing a total-page count that overstates how many pages of genuine issues actually exist. Sendingtype=issuesmakes the count (and the returned items) issues-only at the source. Page<T>.totalCountis optional and defaulted (totalCount: Int? = nullon both the class andPage.of(...)), specifically so none of the other 9+ existingPage.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
totalPagesis 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 inmaterial-icons-corefor 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-Countis correctly excluded of pull requests for the Issues list (viatype=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>gainedtotalCount: Int? = null;Page.of(...)validates it's non-negative when present.GiteaWorkItemsApi.kt:listIssuesnow requeststype=issues;toPageparsesX-Total-Count(case-insensitive header lookup, matching the existingX-HasMorepattern) intoPage.totalCount.WorkItemsViewModel.kt:IssueList/PullRequestListgainedtotalPages: Int? = null, computed via a new privatePage<T>.totalPages(pageSize)extension (ceil(total / pageSize), at least 1); newgoToPage(page: Int).WorkItemsScreen.kt:PageFooternow branches ontotalPages— null falls back to the original previous/next row unchanged; non-null renders the newNumberedPageFooter(page-window buttons +[ input / total ]jump row) instead. New privatepageWindow(current, totalPages, windowSize=5).- New tests:
GiteaWorkItemsApiTest.kt(type=issuessent,X-Total-Countparsed for both issues and pull requests, absent header leavestotalCountnull),WorkItemsViewModelTest.kt(45 items/page size 20 rounds up to 3 pages, missing total count leavestotalPagesnull,goToPagejumps directly to the requested page),RepositoryModelsTest.kt(totalCountdefaults 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. - Same-session follow-up:
WorkItemsScreen.kt's page-jump field gainedisError/supportingTextfor an out-of-range typed value;strings_workitems.xmlgainedworkitems_page_jump_out_of_range../gradlew compileDebugKotlin compileDebugUnitTestKotlin testDebugUnitTest assembleDebug lintDebugBUILD SUCCESSFUL; installed on-device, no crash.
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 exceedingAddressed in a same-session follow-up: the field now setstotalPages— 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.isError/supportingText(a new string,workitems_page_jump_out_of_range) whenever the currently-typed number falls outside[1, totalPages], so the clamp-on-submit behavior is now visible before the tap instead of a silent "999 quietly becomes the last page." Submitting still clamps regardless — this only adds visible feedback, it doesn't change what actually happens on submit. Like the rest of this feature, this specific change hasn't been seen live against real multi-page data either (same blocker as the item above), but it's a small, self-contained Compose-only change consistent with this file's existing, unit-testedcoerceInbehavior — build/lint pass, installed on-device with no crash.
Review findings
None yet — no Codex review pending (Codex retired, 2026-09-29).