feat(archives): list and restore archived work items#226
Conversation
Make the workspace Archives page real (#128): - backend: GET /api/workspaces/:slug/archived-issues/ lists archived work items across the workspace (IssueStore.ListArchivedByWorkspaceID -> IssueService.ListArchivedForWorkspace -> handler), workspace-member scoped. Per-issue archive/restore already existed and is reused for restore. - frontend: issueService.listWorkspaceArchived + an ArchivesPage that lists archived work items (identifier, name, archived date), links to each item, and restores them, with loading/empty/error states (replacing the placeholder). Archived *projects* are out of scope here (there is no project-archive action yet); this covers archived work items, which is the page's main content. AI assistance: implemented with Claude Code (Claude Opus 4.8); verified with a backend integration test (workspace archived list + restore + non-member 404) and in-browser (archive an item -> it appears with an Archived date -> Restore removes it). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughThe PR adds a workspace archived-issues API endpoint and connects the web archives page to it. The page now loads archived issues, supports pagination and restore actions, and updates the list after restores. ChangesWorkspace archived issues
Sequence Diagram(s)sequenceDiagram
participant ArchivesPage
participant issueService
participant IssueHandler
participant IssueService
participant IssueStore
ArchivesPage->>issueService: listWorkspaceArchived(workspaceSlug, { limit: 100 })
issueService->>IssueHandler: GET /api/workspaces/:slug/archived-issues/
IssueHandler->>IssueService: ListArchivedForWorkspace(ctx, slug, userID, limit, offset)
IssueService->>IssueStore: ListArchivedByWorkspaceID(ctx, workspaceID, limit, offset)
IssueStore-->>IssueService: []model.Issue
IssueService-->>IssueHandler: archived issues
IssueHandler-->>issueService: 200 OK
issueService-->>ArchivesPage: IssueApiResponse[]
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
apps/api/internal/store/issue.go (1)
108-123: 🚀 Performance & Scalability | 🔵 TrivialConsider a supporting index for this workspace-scoped archived query.
The query filters on
workspace_id,deleted_at IS NULL,archived_at IS NOT NULLand orders byarchived_at DESC. On large workspaces this can scan and sort many rows. A partial index such as(workspace_id, archived_at DESC) WHERE deleted_at IS NULL AND archived_at IS NOT NULLwould let Postgres satisfy both the filter and ordering efficiently. Same reasoning applies to the existingListArchivedByProjectID.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/api/internal/store/issue.go` around lines 108 - 123, The archived issue listing query in IssueStore.ListArchivedByWorkspaceID can be slow on large datasets because it filters by workspace_id/deleted_at/archived_at and orders by archived_at DESC without a supporting index. Add a supporting partial index for this access pattern, and make sure the same optimization is applied to the existing ListArchivedByProjectID query as well; use the query shapes in IssueStore to choose the indexed columns and partial WHERE clause.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/web/src/pages/ArchivesPage.tsx`:
- Around line 30-53: The ArchivesPage useEffect currently hard-caps archived
work items at 100 via issueService.listWorkspaceArchived, which can silently
hide additional results. Update the archive loading flow in ArchivesPage to
either paginate through all archived items or surface that the first page is
truncated when the 100-item limit is reached, using the existing
workspaceSlug/issueService.listWorkspaceArchived and setIssues state update
path.
---
Nitpick comments:
In `@apps/api/internal/store/issue.go`:
- Around line 108-123: The archived issue listing query in
IssueStore.ListArchivedByWorkspaceID can be slow on large datasets because it
filters by workspace_id/deleted_at/archived_at and orders by archived_at DESC
without a supporting index. Add a supporting partial index for this access
pattern, and make sure the same optimization is applied to the existing
ListArchivedByProjectID query as well; use the query shapes in IssueStore to
choose the indexed columns and partial WHERE clause.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 7237c3a8-1932-45d4-9555-2124e434ef10
📒 Files selected for processing (7)
apps/api/internal/handler/issue.goapps/api/internal/handler/issue_archive_test.goapps/api/internal/router/router.goapps/api/internal/service/issue.goapps/api/internal/store/issue.goapps/web/src/pages/ArchivesPage.tsxapps/web/src/services/issueService.ts
Address CodeRabbit review on #226: the Archives page fetched a single page of 100 items with no way to see the rest. It now loads 50 at a time with a "Load more" button (fetching one extra to detect further pages), so archived items beyond the first page are reachable instead of silently hidden. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/web/src/pages/ArchivesPage.tsx`:
- Around line 61-77: The pagination offset in loadMore is being derived from
issues.length, which changes when restore removes items and causes duplicate or
skipped archived items. Update ArchivesPage to track the server-side loaded
count separately in a dedicated ref/state (or adjust it during restore), and use
that value for the offset in issueService.listWorkspaceArchived so pagination
stays aligned even after restores. Ensure the counter is incremented only by
fetched items in loadMore and not by UI removals from restore.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: ddb1e648-8e2b-4c87-981f-cbca5afd43e6
📒 Files selected for processing (1)
apps/web/src/pages/ArchivesPage.tsx
| const loadMore = async () => { | ||
| if (!workspaceSlug || loadingMore) return; | ||
| setLoadingMore(true); | ||
| try { | ||
| const next = await issueService.listWorkspaceArchived(workspaceSlug, { | ||
| limit: ARCHIVES_PAGE_SIZE + 1, | ||
| offset: issues.length, | ||
| }); | ||
| setHasMore(next.length > ARCHIVES_PAGE_SIZE); | ||
| setIssues((prev) => [...prev, ...next.slice(0, ARCHIVES_PAGE_SIZE)]); | ||
| } catch { | ||
| setError('Could not load more archived work items.'); | ||
| } finally { | ||
| setLoadingMore(false); | ||
| } | ||
| }; | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
offset: issues.length becomes inaccurate after a restore, causing duplicate or skipped items.
loadMore derives the next offset from issues.length, but restore (Line 85) removes items from issues. After a user restores an item and then clicks "Load more", the offset is now smaller than the actual server position, so the next page overlaps the already-shown items. That yields duplicate rows and React duplicate-key warnings on Line 127 (or skipped items in the opposite direction).
Track the fetched count in a dedicated counter (or decrement it on restore) instead of reading issues.length.
♻️ One approach: maintain an explicit offset ref/state
- const next = await issueService.listWorkspaceArchived(workspaceSlug, {
- limit: ARCHIVES_PAGE_SIZE + 1,
- offset: issues.length,
- });
+ const next = await issueService.listWorkspaceArchived(workspaceSlug, {
+ limit: ARCHIVES_PAGE_SIZE + 1,
+ offset: loadedCount, // advance only by fetched pages, not array length
+ });Then bump loadedCount by the fetched page size on each successful load (initial + loadMore), independent of restore-driven removals.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/src/pages/ArchivesPage.tsx` around lines 61 - 77, The pagination
offset in loadMore is being derived from issues.length, which changes when
restore removes items and causes duplicate or skipped archived items. Update
ArchivesPage to track the server-side loaded count separately in a dedicated
ref/state (or adjust it during restore), and use that value for the offset in
issueService.listWorkspaceArchived so pagination stays aligned even after
restores. Ensure the counter is incremented only by fetched items in loadMore
and not by UI removals from restore.
Address CodeRabbit review on #226: loadMore derived its offset from issues.length, but restoring an item removes it from the list, so the next page's offset drifted and could duplicate or skip rows. Track a dedicated fetchedCount for the server offset (advanced only by fetched pages) and de-dup appended items by id. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Makes the workspace Archives page real (part of #128). It now lists
archived work items across the workspace and lets you open or restore them.
Backend
GET /api/workspaces/:slug/archived-issues/— lists archived (non-deleted)work items across all projects in the workspace, newest-archived first
(
IssueStore.ListArchivedByWorkspaceID→IssueService.ListArchivedForWorkspace→
IssueHandler.ListWorkspaceArchived). Workspace-member scoped (mirrors thedraft-issues endpoint). Per-issue archive/restore already existed and is
reused for restore.
Frontend
issueService.listWorkspaceArchived, and anArchivesPagethat replaces theplaceholder: lists archived items (identifier, name, archived date), links to
each item, and restores them — with loading/empty/error states.
Scope
Archived projects are out of scope here: there is no project-archive action
yet (the
projects.archived_atcolumn exists but nothing sets it), so thatsection would always be empty. This covers archived work items, the page's main
content; project archiving can follow once a project-archive action exists.
Testing
go test ./internal/handler -run "TestIssue_WorkspaceArchivedList|TestIssue_ArchiveRestore"— pass (real Postgres via testcontainers): workspace archived list reflects
archive/restore, and non-members get 404.
go vet ./..., UItypecheck/lint— pass.Archives page with an "Archived " label → Restore removes it and the
backend no longer lists it as archived.
AI assistance
Implemented with the assistance of Claude Code (Claude Opus 4.8); verified via
the test suite and in-browser checks.
Summary by CodeRabbit
New Features
Bug Fixes
Tests