Skip to content

feat(archives): list and restore archived work items#226

Merged
martian56 merged 3 commits into
mainfrom
feat/archive-views
Jun 25, 2026
Merged

feat(archives): list and restore archived work items#226
martian56 merged 3 commits into
mainfrom
feat/archive-views

Conversation

@martian56

@martian56 martian56 commented Jun 25, 2026

Copy link
Copy Markdown
Member

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.ListArchivedByWorkspaceIDIssueService.ListArchivedForWorkspace
    IssueHandler.ListWorkspaceArchived). Workspace-member scoped (mirrors the
    draft-issues endpoint). Per-issue archive/restore already existed and is
    reused for restore.

Frontend

  • issueService.listWorkspaceArchived, and an ArchivesPage that replaces the
    placeholder: 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_at column exists but nothing sets it), so that
section 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 ./..., UI typecheck / lint — pass.
  • Verified in-browser (Playwright): archived a work item → it appears on the
    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

    • Added a workspace archives page that loads archived issues for the whole workspace, showing archived dates and a “Load more” paginated list with links to each issue.
    • Added the ability to restore archived issues from the archives list.
  • Bug Fixes

    • Archived issues are now correctly aggregated at the workspace level, with proper access handling (unauthorized access returns not found).
  • Tests

    • Added coverage to verify archived listing and restore behavior, including access restrictions.

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>
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The 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.

Changes

Workspace archived issues

Layer / File(s) Summary
API route and handler
apps/api/internal/router/router.go, apps/api/internal/handler/issue.go, apps/api/internal/handler/issue_archive_test.go
Registers the archived-issues route, adds authenticated handler logic with slug and pagination parsing plus 404/500 mapping, and adds a test that archives, restores, and checks outsider access to the workspace list.
Workspace archived lookup
apps/api/internal/service/issue.go, apps/api/internal/store/issue.go
Adds workspace-scoped archived listing in the service and store, including access checks, workspace filtering, non-deleted archived issue filtering, archived_at ordering, and pagination.
Archive page loading
apps/web/src/services/issueService.ts, apps/web/src/pages/ArchivesPage.tsx
Adds a workspace archived-issues client method and page-side loading logic that fetches archived issues and projects, formats archived dates, derives display IDs, and tracks loading, error, pagination, and cancellation state.
Archive page restore and render
apps/web/src/pages/ArchivesPage.tsx
Implements per-item restore handling and replaces the placeholder archives view with error, loading, empty, archived-item, and load-more states.

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[]
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

  • Devlaner/devlane#215: Adds per-issue archive/restore and shared archived-list behavior around the same archived_at flow.

Suggested labels

enhancement, API, UI

Poem

A rabbit hopped through archives neat,
Where old issues lined up in a sheet.
Restore, and back they go—
Pagination in a row,
🐇✨ the archive page found its beat.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers summary, testing, and AI assistance, but omits many required template sections like linked issues, type, surface, and rollout notes. Complete the missing template sections: linked issue, type, surface, what changed, why this approach, breaking changes, rollout notes, checklist, and screenshots if relevant.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise, specific, and matches the main change: listing and restoring archived work items.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/archive-views

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/api/internal/store/issue.go (1)

108-123: 🚀 Performance & Scalability | 🔵 Trivial

Consider a supporting index for this workspace-scoped archived query.

The query filters on workspace_id, deleted_at IS NULL, archived_at IS NOT NULL and orders by archived_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 NULL would let Postgres satisfy both the filter and ordering efficiently. Same reasoning applies to the existing ListArchivedByProjectID.

🤖 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

📥 Commits

Reviewing files that changed from the base of the PR and between bcbdcd5 and 5607238.

📒 Files selected for processing (7)
  • apps/api/internal/handler/issue.go
  • apps/api/internal/handler/issue_archive_test.go
  • apps/api/internal/router/router.go
  • apps/api/internal/service/issue.go
  • apps/api/internal/store/issue.go
  • apps/web/src/pages/ArchivesPage.tsx
  • apps/web/src/services/issueService.ts

Comment thread apps/web/src/pages/ArchivesPage.tsx
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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 5607238 and ac276b0.

📒 Files selected for processing (1)
  • apps/web/src/pages/ArchivesPage.tsx

Comment on lines +61 to +77
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);
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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>
@martian56
martian56 merged commit 311184d into main Jun 25, 2026
6 checks passed
@martian56
martian56 deleted the feat/archive-views branch June 25, 2026 17:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant