Skip to content

feat: add leave workspace for shared collaborators#473

Open
samvitchoudhary wants to merge 2 commits intoThinkEx-OSS:mainfrom
samvitchoudhary:feat/leave-workspace
Open

feat: add leave workspace for shared collaborators#473
samvitchoudhary wants to merge 2 commits intoThinkEx-OSS:mainfrom
samvitchoudhary:feat/leave-workspace

Conversation

@samvitchoudhary
Copy link
Copy Markdown
Collaborator

@samvitchoudhary samvitchoudhary commented Apr 28, 2026

What

Adds a "Leave Workspace" affordance for users on workspaces shared with them. Replaces the Danger Zone "Delete" button in the workspace settings modal with "Leave" when the workspace is shared (not owned). Owners see the existing Delete flow unchanged.

Why

Previously there was no way for a collaborator to remove themselves from a workspace shared with them — only the owner could kick someone. The settings modal also rendered a "Delete" button on shared workspaces that silently 403'd, which was a latent UX bug.

Approach

3 files, no schema changes, no Zero changes (workspace_collaborators is REST-served, not Zero-synced):

  • New endpoint POST /api/workspaces/[id]/leave — auth check, rejects owner self-leave with 400, otherwise deletes the user's workspace_collaborators row. Returns 404 if the user wasn't a collaborator.
  • WorkspaceContext — adds leaveWorkspace(workspaceId) mirroring the existing deleteWorkspace mutation: optimistic cache update, redirect-if-current-workspace, success/error toasts.
  • WorkspaceSettingsModal — branches the Danger Zone on workspace.isShared. Shared → "Leave Workspace" with LogOut icon and a parallel AlertDialog. Owned → existing Delete flow untouched.

Verification

Tested locally with two Google accounts:

  • ✅ Owner sees "Delete Workspace" in settings modal (unchanged)
  • ✅ Collaborator sees "Leave Workspace" with proper copy
  • ✅ Confirmation dialog → leave → success toast → workspace removed from grid
  • ✅ Redirect to /home if user was inside the workspace when they left
  • POST /api/workspaces/<owned-id>/leave returns 400 with clear error (owner-leave guard)
  • pnpm lint clean, pnpm tc:tsc clean

Out of scope (known follow-ups)

  • Bulk-delete bar in WorkspaceGrid.tsx still calls owner-only DELETE /api/workspaces/[id] for shared workspaces, silently 403'ing. Should either filter shared out of bulk delete or split into "Delete (owned) / Leave (shared)". Worth its own PR.
  • Settings modal Save fields (Name, Icon, Color) are editable for shared workspaces but Save 403s. Pre-existing bug, not introduced here, but more visible now that shared workspaces have a real reason to open the modal. Suggested fix: gate Save on ownership or permissionLevel.
  • Zero local-cache eviction on leave — server-side reads/writes are correctly gated (hasWorkspaceReadAccess + assertWorkspaceWriteAccess both check the collaborator row, so post-leave requests 403). Locally cached workspace_items rows persist in IndexedDB until Zero's GC runs, but the UI never reads them since navigation paths are also gated. Not a leak, but worth flagging.

Summary by CodeRabbit

  • New Features
    • Users can leave shared workspaces from Workspace Settings via a new "Leave Workspace" flow with confirmation.
    • While leaving, actions are disabled and a loading state is shown; success and error toasts are displayed.
    • After leaving, the app automatically switches to another workspace or returns to Home if none remain.
    • Workspace owners are prevented from leaving a workspace.

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 28, 2026

@samvitchoudhary is attempting to deploy a commit to the ThinkEx Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 28, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d0f56aa8-73ae-4100-84b2-0b15e038388d

📥 Commits

Reviewing files that changed from the base of the PR and between 15740e5 and bb8307d.

📒 Files selected for processing (2)
  • src/app/api/workspaces/[id]/leave/route.ts
  • src/contexts/WorkspaceContext.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/contexts/WorkspaceContext.tsx
  • src/app/api/workspaces/[id]/leave/route.ts

📝 Walkthrough
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
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 (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add leave workspace for shared collaborators' directly and clearly summarizes the main change—adding a leave workspace feature for shared collaborators, which aligns with all three files modified (new API endpoint, context hook, and UI modal).
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 unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@samvitchoudhary samvitchoudhary requested a review from urjitc April 28, 2026 01:17
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 28, 2026

Greptile Summary

This PR adds a "Leave Workspace" flow for collaborators (not owners): a new POST /api/workspaces/[id]/leave endpoint, a leaveWorkspace mutation in WorkspaceContext, and a branched Danger Zone UI in WorkspaceSettingsModal that shows Leave vs. Delete based on isShared. The implementation is clean and correctly mirrors the existing delete-workspace pattern with optimistic cache updates and redirect-on-leave.

Confidence Score: 4/5

Safe to merge; all findings are P2 quality/UX suggestions with no breaking logic errors.

No P0 or P1 issues found. Three P2 findings: workspace-ID enumeration via distinct 404 responses, generic server error swallowing in the mutation, and the AlertDialogAction auto-close behaviour masking the loading/error state in the confirmation dialog.

The 404 response distinction in src/app/api/workspaces/[id]/leave/route.ts is the most actionable item before merge.

Security Review

  • Workspace ID enumeration (route.ts lines 34–35 vs. 58–62): The endpoint returns distinct 404 bodies for "workspace not found" vs. "not a collaborator", allowing any authenticated user to probe whether an arbitrary workspace ID exists. Merging the two responses into a single message would close this.

Important Files Changed

Filename Overview
src/app/api/workspaces/[id]/leave/route.ts New POST endpoint for collaborator self-removal; auth, owner-guard, and DB delete are all correct, but the two distinct 404 responses allow workspace-ID enumeration by any authenticated user.
src/contexts/WorkspaceContext.tsx Adds leaveWorkspace mutation mirroring deleteWorkspace; cache update and redirect logic are correct; server error body is not surfaced to the caller.
src/components/workspace/WorkspaceSettingsModal.tsx Branches Danger Zone on isShared, adds Leave confirmation dialog; AlertDialogAction auto-close means spinner/error state inside the dialog are not reliably shown.

Reviews (1): Last reviewed commit: "feat: add leave workspace for shared col..." | Re-trigger Greptile

Comment on lines +58 to +63
if (!deleted) {
return NextResponse.json(
{ error: "You are not a collaborator of this workspace." },
{ status: 404 }
);
}
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.

P2 security Workspace existence leaks information to authenticated users

The endpoint returns a 404 with "Workspace not found" when the workspace doesn't exist, and a 404 with "You are not a collaborator of this workspace." when the user is just not a member. Any authenticated user can distinguish these two cases and enumerate valid workspace IDs by probing this endpoint — they never need to be a collaborator. Consider returning the same 404 body in both cases ("Workspace not found or you are not a collaborator") to avoid the distinction.

Comment on lines +218 to +220
if (!response.ok) {
throw new Error("Failed to leave workspace");
}
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.

P2 Server error body is not surfaced to the caller

When response.ok is false, the mutation throws a generic "Failed to leave workspace" string and ignores the server's JSON body. The API returns useful messages (e.g. "Workspace owners cannot leave their own workspace..." for a 400), but those are silently swallowed. The onError toast also shows the same generic message. Consider reading response.json() and including the server's error field in the thrown Error, so the handleLeaveConfirm catch block (and the toast) can surface the real reason to the user.

Comment on lines +383 to +390
<AlertDialogCancel disabled={isLeaving}>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={handleLeaveConfirm}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
disabled={isLeaving}
>
{isLeaving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Leave
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.

P2 AlertDialogAction closes the dialog before the async handler resolves

Radix UI's AlertDialogAction calls onOpenChange(false) synchronously on click (before onClick resolves), so the confirmation dialog closes immediately. The loading spinner (isLeaving) and the disabled={isLeaving} guard on the Cancel button inside the dialog are both rendered invisible before the fetch completes. This mirrors the existing delete-dialog pattern, but it means any error from handleLeaveConfirm silently surfaces in the outer settings modal's error state rather than inside the confirmation dialog where the user expects it. Consider using a plain Button instead of AlertDialogAction, or call event.preventDefault() and gate dialog close on completion.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/contexts/WorkspaceContext.tsx (1)

158-200: Delete/leave mutation success paths are now duplicated.

Both mutations implement nearly identical cache pruning + redirect logic. Extracting one shared helper would reduce risk of behavior drift.

Also applies to: 210-258

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/contexts/WorkspaceContext.tsx` around lines 158 - 200, The delete and
leave mutation handlers duplicate the same cache-pruning and redirect logic;
extract a shared helper (e.g., pruneWorkspacesAfterRemoval or
handleWorkspaceRemoval) that accepts the removedWorkspaceId and performs the
queryClient.setQueryData update, checks currentWorkspaceId/currentSlug, calls
switchWorkspace or router.push("/home") as needed, and shows the toast; then
replace the inline onSuccess blocks in deleteWorkspaceMutation and the
corresponding leave mutation (lines ~210-258) to call this helper so behavior is
centralized and de-duplicated.
src/components/workspace/WorkspaceSettingsModal.tsx (1)

163-184: Consider extracting a reusable destructive-action section/dialog.

The delete/leave flows now duplicate very similar handler + card + dialog structures. A small reusable component/helper would reduce drift and future maintenance overhead.

Also applies to: 289-328, 344-394

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/workspace/WorkspaceSettingsModal.tsx` around lines 163 - 184,
Extract the repeated delete/leave UI and logic into a reusable DestructiveAction
component that encapsulates the dialog/card, confirm/cancel buttons and handles
loading/error state; replace the duplicated code in WorkspaceSettingsModal by
having handleLeaveClick call setShowLeaveDialog on the DestructiveAction and
move the async confirm logic (currently in handleLeaveConfirm) into a prop
callback (e.g., onConfirm) that invokes leaveWorkspace(workspace.id), toggles
isLeaving via a passed setIsLoading or internal state, closes the modal via
onOpenChange(false), calls onUpdate() if provided, and surfaces errors via a
passed onError or returned promise; reuse the new component for the other
similar flows (the handlers and state setters setShowLeaveDialog, setIsLeaving,
setError, and functions leaveWorkspace, onOpenChange, onUpdate are the key
symbols to wire into the reusable component).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/api/workspaces/`[id]/leave/route.ts:
- Around line 25-32: The handler currently uses params' workspaceId directly in
the query (const { id: workspaceId } = await params) which can throw on
malformed UUIDs; add an early validation step to check that workspaceId is a
valid UUID (before calling db.select / eq(workspaces.id, workspaceId)) and
return a 400 response when invalid. Locate the route function that reads params
and uses db.select({ userId: workspaces.userId
}).from(workspaces).where(eq(workspaces.id, workspaceId)) and insert a
validation using your project's UUID utility or a simple regex/validator,
returning a 400 error if validation fails so the DB query is never executed with
a bad ID.

In `@src/contexts/WorkspaceContext.tsx`:
- Around line 226-240: The early return on null cache prevents the redirect when
the workspaces list isn't loaded; update the updater to compute
remainingWorkspaces using const existing = old || []; (i.e. don't return early),
then perform the redirect logic that checks leftWorkspaceId ===
currentWorkspaceId and currentSlug and calls
switchWorkspace(remainingWorkspaces[0].slug || remainingWorkspaces[0].id) or
router.push("/home") using remainingWorkspaces, and finally return
remainingWorkspaces; reference the updater closure variables leftWorkspaceId,
currentWorkspaceId, currentSlug, switchWorkspace, router.push and ensure the
function returns the updated array instead of returning early when old is
undefined.
- Around line 223-247: The onSuccess handler updates the ["workspaces"] list but
doesn't evict per-workspace queries so stale ["workspace-by-slug", ...] data can
persist; inside the same onSuccess (where queryClient.setQueryData is used) find
the removed workspace from the old array by matching leftWorkspaceId (e.g.,
const removed = old.find(w => w.id === leftWorkspaceId)), then call
queryClient.removeQueries(["workspace-by-slug", removed.slug]) and also remove
any id-based key you use (e.g., queryClient.removeQueries(["workspace-by-id",
leftWorkspaceId])) before calling switchWorkspace/router.push and toast.success
to ensure per-workspace cache is cleared.

---

Nitpick comments:
In `@src/components/workspace/WorkspaceSettingsModal.tsx`:
- Around line 163-184: Extract the repeated delete/leave UI and logic into a
reusable DestructiveAction component that encapsulates the dialog/card,
confirm/cancel buttons and handles loading/error state; replace the duplicated
code in WorkspaceSettingsModal by having handleLeaveClick call
setShowLeaveDialog on the DestructiveAction and move the async confirm logic
(currently in handleLeaveConfirm) into a prop callback (e.g., onConfirm) that
invokes leaveWorkspace(workspace.id), toggles isLeaving via a passed
setIsLoading or internal state, closes the modal via onOpenChange(false), calls
onUpdate() if provided, and surfaces errors via a passed onError or returned
promise; reuse the new component for the other similar flows (the handlers and
state setters setShowLeaveDialog, setIsLeaving, setError, and functions
leaveWorkspace, onOpenChange, onUpdate are the key symbols to wire into the
reusable component).

In `@src/contexts/WorkspaceContext.tsx`:
- Around line 158-200: The delete and leave mutation handlers duplicate the same
cache-pruning and redirect logic; extract a shared helper (e.g.,
pruneWorkspacesAfterRemoval or handleWorkspaceRemoval) that accepts the
removedWorkspaceId and performs the queryClient.setQueryData update, checks
currentWorkspaceId/currentSlug, calls switchWorkspace or router.push("/home") as
needed, and shows the toast; then replace the inline onSuccess blocks in
deleteWorkspaceMutation and the corresponding leave mutation (lines ~210-258) to
call this helper so behavior is centralized and de-duplicated.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 41807883-2ada-48dd-b089-56829f3e0623

📥 Commits

Reviewing files that changed from the base of the PR and between e9e64c7 and 15740e5.

📒 Files selected for processing (3)
  • src/app/api/workspaces/[id]/leave/route.ts
  • src/components/workspace/WorkspaceSettingsModal.tsx
  • src/contexts/WorkspaceContext.tsx

Comment thread src/app/api/workspaces/[id]/leave/route.ts
Comment on lines +223 to +247
onSuccess: (leftWorkspaceId) => {
queryClient.setQueryData(
["workspaces"],
(old: WorkspaceWithState[] | undefined) => {
if (!old) return [];
const remainingWorkspaces = old.filter(
(w) => w.id !== leftWorkspaceId,
);

if (leftWorkspaceId === currentWorkspaceId && currentSlug) {
if (remainingWorkspaces.length > 0) {
switchWorkspace(
remainingWorkspaces[0].slug || remainingWorkspaces[0].id,
);
} else {
router.push("/home");
}
}

return remainingWorkspaces;
},
);

toast.success("Left workspace successfully");
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Evict left-workspace metadata cache after successful leave.

Only the ["workspaces"] list is updated. The per-workspace query (["workspace-by-slug", ...]) can remain fresh and serve stale data after leave (especially on back-nav).

Proposed fix
     onSuccess: (leftWorkspaceId) => {
+      const previous = queryClient.getQueryData<WorkspaceWithState[]>(["workspaces"]) ?? [];
+      const leftWorkspace = previous.find((w) => w.id === leftWorkspaceId);
+
       queryClient.setQueryData(
         ["workspaces"],
         (old: WorkspaceWithState[] | undefined) => {
           if (!old) return [];
           const remainingWorkspaces = old.filter(
             (w) => w.id !== leftWorkspaceId,
           );
@@
           return remainingWorkspaces;
         },
       );
+
+      if (leftWorkspace?.slug) {
+        queryClient.removeQueries({
+          queryKey: ["workspace-by-slug", leftWorkspace.slug],
+          exact: true,
+        });
+      }
 
       toast.success("Left workspace successfully");
     },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/contexts/WorkspaceContext.tsx` around lines 223 - 247, The onSuccess
handler updates the ["workspaces"] list but doesn't evict per-workspace queries
so stale ["workspace-by-slug", ...] data can persist; inside the same onSuccess
(where queryClient.setQueryData is used) find the removed workspace from the old
array by matching leftWorkspaceId (e.g., const removed = old.find(w => w.id ===
leftWorkspaceId)), then call queryClient.removeQueries(["workspace-by-slug",
removed.slug]) and also remove any id-based key you use (e.g.,
queryClient.removeQueries(["workspace-by-id", leftWorkspaceId])) before calling
switchWorkspace/router.push and toast.success to ensure per-workspace cache is
cleared.

Comment on lines +226 to +240
(old: WorkspaceWithState[] | undefined) => {
if (!old) return [];
const remainingWorkspaces = old.filter(
(w) => w.id !== leftWorkspaceId,
);

if (leftWorkspaceId === currentWorkspaceId && currentSlug) {
if (remainingWorkspaces.length > 0) {
switchWorkspace(
remainingWorkspaces[0].slug || remainingWorkspaces[0].id,
);
} else {
router.push("/home");
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Redirect logic is skipped when ["workspaces"] cache is empty.

At Line 227, if (!old) return [] exits before the current-workspace redirect check. If the list cache isn’t ready, leaving the active workspace can succeed without navigation fallback.

Proposed fix
       queryClient.setQueryData(
         ["workspaces"],
         (old: WorkspaceWithState[] | undefined) => {
-          if (!old) return [];
-          const remainingWorkspaces = old.filter(
+          const remainingWorkspaces = (old ?? []).filter(
             (w) => w.id !== leftWorkspaceId,
           );
 
           if (leftWorkspaceId === currentWorkspaceId && currentSlug) {
             if (remainingWorkspaces.length > 0) {
               switchWorkspace(
                 remainingWorkspaces[0].slug || remainingWorkspaces[0].id,
               );
             } else {
               router.push("/home");
             }
           }
 
           return remainingWorkspaces;
         },
       );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
(old: WorkspaceWithState[] | undefined) => {
if (!old) return [];
const remainingWorkspaces = old.filter(
(w) => w.id !== leftWorkspaceId,
);
if (leftWorkspaceId === currentWorkspaceId && currentSlug) {
if (remainingWorkspaces.length > 0) {
switchWorkspace(
remainingWorkspaces[0].slug || remainingWorkspaces[0].id,
);
} else {
router.push("/home");
}
}
(old: WorkspaceWithState[] | undefined) => {
const remainingWorkspaces = (old ?? []).filter(
(w) => w.id !== leftWorkspaceId,
);
if (leftWorkspaceId === currentWorkspaceId && currentSlug) {
if (remainingWorkspaces.length > 0) {
switchWorkspace(
remainingWorkspaces[0].slug || remainingWorkspaces[0].id,
);
} else {
router.push("/home");
}
}
return remainingWorkspaces;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/contexts/WorkspaceContext.tsx` around lines 226 - 240, The early return
on null cache prevents the redirect when the workspaces list isn't loaded;
update the updater to compute remainingWorkspaces using const existing = old ||
[]; (i.e. don't return early), then perform the redirect logic that checks
leftWorkspaceId === currentWorkspaceId and currentSlug and calls
switchWorkspace(remainingWorkspaces[0].slug || remainingWorkspaces[0].id) or
router.push("/home") using remainingWorkspaces, and finally return
remainingWorkspaces; reference the updater closure variables leftWorkspaceId,
currentWorkspaceId, currentSlug, switchWorkspace, router.push and ensure the
function returns the updated array instead of returning early when old is
undefined.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/contexts/WorkspaceContext.tsx">

<violation number="1" location="src/contexts/WorkspaceContext.tsx:227">
P2: Early return `if (!old) return []` skips the redirect-if-current-workspace check. If the `["workspaces"]` cache is not yet populated when the leave mutation succeeds, this exits before evaluating whether the user needs to be navigated away. Use `(old ?? []).filter(...)` instead so the redirect logic always executes.</violation>
</file>

<file name="src/app/api/workspaces/[id]/leave/route.ts">

<violation number="1" location="src/app/api/workspaces/[id]/leave/route.ts:35">
P2: The two 404 responses have different error messages (`"Workspace not found"` vs `"You are not a collaborator of this workspace."`), allowing any authenticated user to enumerate valid workspace IDs by probing this endpoint. Use the same message for both cases, e.g. `"Workspace not found or you are not a collaborator"`.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread src/contexts/WorkspaceContext.tsx Outdated
Comment thread src/app/api/workspaces/[id]/leave/route.ts Outdated
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

0 issues found across 2 files (changes from recent commits).

Requires human review: This PR adds a new API endpoint and business logic for workspace membership. New feature implementation and access control changes should be reviewed by a human.

@samvitchoudhary
Copy link
Copy Markdown
Collaborator Author

Pushed fixes for the three concerns I considered most actionable. Triaged the rest as either pre-existing patterns I'd rather not diverge from in this PR, or scope creep.

Fixed:

  1. Workspace ID enumeration (Greptile + cubic). Both 404 paths now return the same generic body: "Workspace not found or you are not a collaborator". Owner-rejection 400 stays as-is — different status, different semantic.
  2. Malformed UUID → 400, not 500 (CodeRabbit). Added a UUID regex check at the top of the handler. Invalid IDs now return 400 instead of falling into the 500 catch-all.
  3. Cold-cache redirect skip (CodeRabbit + cubic). Replaced if (!old) return [] with (old ?? []).filter(...) in leaveWorkspaceMutation.onSuccess so the redirect-if-current-workspace branch always evaluates. Only changed the new mutation, not the existing deleteWorkspaceMutation (see "Skipped" below).

Skipped, with reasoning:

  • Per-workspace cache eviction (workspace-by-slug) (CodeRabbit). Real concern, but deleteWorkspaceMutation doesn't evict it either — fixing only the leave path would create asymmetry between two near-identical flows. Better as a separate PR that fixes both.
  • AlertDialogAction auto-close masking the spinner (Greptile). Same reasoning — the existing delete flow has the same behavior. Worth standardizing across both as a separate UX consistency PR.
  • Surface server error body in the toast (Greptile). The only meaningful server error path is the owner-leave-guard, which the UI already prevents end-users from reaching. The current generic toast is fine in practice.
  • Extract shared helper for delete/leave logic (CodeRabbit). Two callers with diverging toast text and HTTP method — premature abstraction. Three+ callers is when extraction earns its keep.

pnpm lint and pnpm tc:tsc clean. Manually retested the happy path, owner-leave guard, and the two new error paths (malformed UUID → 400, nonexistent UUID → generic 404).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

1 participant