Skip to content

fix(deck): tolerate stale deleted category in move-card menu#94

Merged
jcserv merged 2 commits into
mainfrom
fm/stale-cat-menu-w3
Jul 16, 2026
Merged

fix(deck): tolerate stale deleted category in move-card menu#94
jcserv merged 2 commits into
mainfrom
fm/stale-cat-menu-w3

Conversation

@jcserv

@jcserv jcserv commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Intent

Fix issue #88: a stale deleted category left in the open move-card menu threw an unhandled error inside startTransition and tripped the error boundary when the user (in another tab) or a collaborator deleted that category while the menu was open. Fix degrades gracefully on BOTH sides: (1) client filter (filterLiveCategories in move-card-menu.tsx) drops any category name no longer in the live subcategories registry before dispatch/send; (2) server action (setCardCategories, moveCardTo effectiveCategory fallback in app/_actions/deck/categories.ts) silently drops unknown category names instead of throwing. Two previously throw-asserting tests were intentionally flipped to assert graceful drop/degrade, plus new filterLiveCategories tests. THIS FOLLOW-UP COMMIT (a7145da) is a review nitpick: the category-existence lookup near effectiveCategory duplicated assertCategoryExists, so I extracted the shared deckCategory.findUnique into a checkCategoryExists helper that both assertCategoryExists and the effectiveCategory fallback reuse — behavior preserved exactly, minimal change. 58 tests pass, lint clean.

What Changed

  • Client move-card-menu.tsx now filters category names against the live subcategories registry (filterLiveCategories) before dispatch/send, so a category deleted while the menu is open no longer throws inside startTransition and trips the error boundary.
  • Server actions setCardCategories and the moveCardTo effectiveCategory fallback (app/_actions/deck/categories.ts) silently drop unknown category names instead of throwing; the shared deckCategory.findUnique lookup is extracted into a checkCategoryExists helper reused by assertCategoryExists and the fallback.
  • Two previously throw-asserting tests flipped to assert graceful drop/degrade, plus new filterLiveCategories tests.

Risk Assessment

✅ Low: Small, well-bounded graceful-degrade fix plus a behavior-preserving helper extraction; matches stated intent with tests updated, no material bugs found.

Testing

Installed deps (pnpm, frozen lockfile) and generated the Prisma client with a dummy DATABASE_URL to unblock the mocked-prisma unit suites, then ran the two affected test files: 64/64 pass. The suites directly reproduce the issue #88 race — a stale/deleted category left in the open move-card menu — and assert graceful degradation on both sides (client filter drops the stale name; server actions drop unknown names instead of throwing into the error boundary), including the two intentionally-flipped throw→drop assertions and the new filterLiveCategories tests. The follow-up commit under test is a pure helper-extraction refactor and the moveCardTo degrade test still exercises the shared checkCategoryExists path, confirming behavior is preserved. No browser screenshot/GIF was captured: this commit adds no rendered UI surface — move-card-menu.tsx's change is a defensive pure function and the fix's user-facing outcome is a non-event (the absence of an error-boundary crash), which the targeted client/server tests reproduce exactly; a full two-tab browser reproduction of the cross-tab deletion race would need a live Postgres + dev server and adds little over the tests that reproduce the precise race outcome. Working tree is clean; install artifacts (node_modules, lib/generated) are gitignored.

Evidence: Graceful-degrade test transcript (issue #88, both sides)

✓ server setCardCategories > silently drops a stale category no longer in the deck registry (issue #88) ✓ server setCardCategories > no-ops when every requested category is stale (issue #88) ✓ server moveCardTo > degrades to an uncategorized MAINBOARD move when the target subcategory was deleted (issue #88) ✓ client filterLiveCategories > drops a stale membership no longer in the live registry ✓ client filterLiveCategories > preserves order of the surviving memberships ✓ client filterLiveCategories > returns an empty list when every name is stale Test Files 2 passed (2) | Tests 64 passed (64)

 ✓ |server| app/_actions/deck/__tests__/categories.test.ts > setCardCategories > silently drops a stale category no longer in the deck registry (issue #88) 0ms
 ✓ |server| app/_actions/deck/__tests__/categories.test.ts > setCardCategories > no-ops when every requested category is stale (issue #88) 0ms
 ✓ |server| app/_actions/deck/__tests__/categories.test.ts > moveCardTo > degrades to an uncategorized MAINBOARD move when the target subcategory was deleted (issue #88) 0ms
 ✓ |client| app/_components/builder/__tests__/move-card-menu.test.tsx > filterLiveCategories (issue #88) > drops a stale membership no longer in the live registry 0ms
 ✓ |client| app/_components/builder/__tests__/move-card-menu.test.tsx > filterLiveCategories (issue #88) > preserves order of the surviving memberships 0ms
 ✓ |client| app/_components/builder/__tests__/move-card-menu.test.tsx > filterLiveCategories (issue #88) > returns an empty list when every name is stale 0ms
 Test Files  2 passed (2)
      Tests  64 passed (64)
   Duration  1.37s (transform 250ms, setup 122ms, import 851ms, tests 21ms, environment 476ms)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • ℹ️ app/_actions/deck/categories.ts:303 - moveCardTo's degrade path sets effectiveCategory=null when the target category was deleted, which makes nextCategories=[] and clears ALL of the card's existing valid category memberships, not just the deleted one. setCardCategories (the other graceful-drop path) only filters out the unknown name and keeps the rest. This divergence is authorized by the stated intent ('degrade to an uncategorized MAINBOARD move') but is more destructive than the sibling path — a collaborator deleting one category could wipe a card's other memberships on a move.
✅ **Test** - passed

✅ No issues found.

  • vitest run app/_actions/deck/__tests__/categories.test.ts app/_components/builder/__tests__/move-card-menu.test.tsx — 64 passed
  • server graceful-degrade: setCardCategories > silently drops a stale category (issue #88), setCardCategories > no-ops when every requested category is stale (issue #88), moveCardTo > degrades to an uncategorized MAINBOARD move when the target subcategory was deleted (issue #88)
  • client pure-fn: filterLiveCategories (issue #88) — drops stale membership, preserves order, returns empty when all stale
  • Confirmed the extracted checkCategoryExists path still drives the moveCardTo degrade test (helper reuse, behavior preserved)
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

A category can be deleted (by this user in another tab or by a
collaborator) while the move-card menu is open and still showing it.
Toggling that stale category previously threw inside startTransition,
tripping the error boundary instead of failing gracefully.

Defense on both sides:
- Client (`applyCategories`) filters memberships against the live
  `subcategories` list before dispatch via the new exported
  `filterLiveCategories` helper, so a stale entry is never sent.
- Server actions silently drop / degrade unknown category names instead
  of throwing: `setCardCategories` filters them out, and `moveCardTo`
  degrades a deleted target to an uncategorized MAINBOARD move.

Regression tests cover the client filter and both server-action paths.
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@jcserv, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 41 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9e22c896-99be-4617-81b2-48cf6fc5a9be

📥 Commits

Reviewing files that changed from the base of the PR and between c57e5b7 and a7145da.

📒 Files selected for processing (1)
  • app/_actions/deck/categories.ts
📝 Walkthrough

Walkthrough

Category updates now tolerate deleted categories. The builder filters stale memberships before dispatching, while deck actions drop unknown names and convert missing MAINBOARD targets into uncategorized moves. Tests cover surviving memberships, no-op behavior, and fallback moves.

Changes

Stale Category Tolerance

Layer / File(s) Summary
Live membership filtering
app/_components/builder/move-card-menu.tsx, app/_components/builder/__tests__/move-card-menu.test.tsx
filterLiveCategories removes stale memberships while preserving order, and applyCategories uses the filtered list for optimistic and server updates.
Tolerant deck actions
app/_actions/deck/categories.ts, app/_actions/deck/__tests__/categories.test.ts
setCardCategories ignores unknown categories, while moveCardTo falls back to an uncategorized MAINBOARD move when its target is missing; tests cover both behaviors.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

  • jcserv/maindeck#88 — The changes directly implement stale-category filtering and tolerant action behavior.

Possibly related PRs

  • jcserv/maindeck#90 — This PR extends the earlier multi-category membership behavior with stale-category filtering and fallback handling.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: handling stale deleted categories in the move-card menu.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fm/stale-cat-menu-w3

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@jcserv

jcserv commented Jul 16, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
app/_actions/deck/categories.ts (1)

286-297: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract duplicate category existence logic into a shared helper.

The query to verify whether a category exists duplicates the logic found in assertCategoryExists (lines 51-62). Consider extracting a shared helper function (e.g., checkCategoryExists) to centralize this database lookup.

🤖 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 `@app/_actions/deck/categories.ts` around lines 286 - 297, The category
existence lookup near effectiveCategory duplicates assertCategoryExists. Extract
the shared deckCategory findUnique query into a helper such as
checkCategoryExists, update assertCategoryExists and the effectiveCategory
fallback logic to reuse it, and preserve the existing behavior for missing
categories.
🤖 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.

Nitpick comments:
In `@app/_actions/deck/categories.ts`:
- Around line 286-297: The category existence lookup near effectiveCategory
duplicates assertCategoryExists. Extract the shared deckCategory findUnique
query into a helper such as checkCategoryExists, update assertCategoryExists and
the effectiveCategory fallback logic to reuse it, and preserve the existing
behavior for missing categories.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 674cbe40-a0cd-4bf6-a46e-daceb2ca2979

📥 Commits

Reviewing files that changed from the base of the PR and between bc3f1f9 and c57e5b7.

📒 Files selected for processing (4)
  • app/_actions/deck/__tests__/categories.test.ts
  • app/_actions/deck/categories.ts
  • app/_components/builder/__tests__/move-card-menu.test.tsx
  • app/_components/builder/move-card-menu.tsx

assertCategoryExists and the moveCardTo effectiveCategory fallback ran
the identical deckCategory.findUnique existence lookup. Extract it into
a shared checkCategoryExists helper both reuse; missing-category
behavior is unchanged (assert still throws, moveCardTo still degrades to
an uncategorized move).
@jcserv
jcserv merged commit 7695de7 into main Jul 16, 2026
5 checks passed
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