Skip to content

feat(sdk): org delete blockers UI - #2025

Open
rohanchkrabrty wants to merge 16 commits into
mainfrom
feat/workspace-delete-blockers-dialog
Open

rohanchkrabrty wants to merge 16 commits into
mainfrom
feat/workspace-delete-blockers-dialog

Conversation

@rohanchkrabrty

@rohanchkrabrty rohanchkrabrty commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • The delete organization button runs CheckOrganizationDelete on click, with a spinner on the button. When something blocks the delete, an "Unable to delete" dialog lists the blockers. Otherwise the confirmation opens.
  • A refused delete (FAILED_PRECONDITION with PreconditionFailure violations) closes the confirmation and opens the same blocked dialog. A FAILED_PRECONDITION with no violations comes from a disabled organization and shows as its own blocker.
  • FrontierProvider takes a slots prop. Its deleteBlockedDialog slot lets a host app word the blocked dialog in one place. The slot returns DeleteBlockedDialog (title, description, blockers, messages), its own AlertDialog.Content to replace the whole dialog, or undefined for the SDK's own dialog. A bullet is messages[subject], else messages[type], else the SDK line. Identical lines show once, and blockers with no line share one generic line.
  • A refused delete whose blockers can't be read shows a toast instead of the dialog.
  • DeleteBlocker (type, subject, count) is the SDK's own shape. A check response and a refused delete both map to it. The server's developer message is left out.
  • useDeleteCheck runs the check, then opens the confirmation or the blocked dialog. The project flow in the next PR uses it too.
  • Bumps @raystack/proton to 94668c36, the commit the server builds from.
<FrontierProvider
  config={config}
  slots={{
    deleteBlockedDialog: ctx =>
      ctx.entity === 'organization' ? (
        <DeleteBlockedDialog messages={{ 'app/report': 'Reports exist: Delete them first.' }} />
      ) : undefined
  }}
>

Stack

  1. This PR: shared pieces and the organization delete
  2. feat(sdk): project delete blockers UI #2041: project delete
  3. feat(sdk): user delete blockers UI #2042: account delete

Test plan

  • tsc --noEmit shows no new errors, eslint, pnpm build
  • Delete a workspace with an active paid subscription: the blocked dialog lists it
  • Delete a clean workspace: the confirmation opens and the delete goes through
  • Delete a disabled workspace: the blocked dialog says it is disabled

The delete organization button no longer checks the blockers on page load
and stays disabled with a tooltip. On click it runs checkOrganizationDelete
with a loading state. If the delete is blocked, a dialog lists the
blockers; otherwise the delete dialog opens.
Brings in CheckProjectDelete, CheckCurrentUserDelete, DeleteCurrentUser,
the top-level Blocker message with count, and the org resource RPCs.
A disabled organization fails the check with FAILED_PRECONDITION, so it
shows as a blocker in the same dialog. Any other check error shows a
toast. The delete button keeps its label while the check runs.
@vercel

vercel Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
frontier Ready Ready Preview Nov 4, 2026 6:32am UTC

@coderabbitai

coderabbitai Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • Resume reviews
  • Trigger review
Summary

Summary by CodeRabbit

  • New Features
    • Added an organization deletion flow that checks eligibility before showing the confirmation dialog and displays a checking state.
    • Added a blocked-deletion dialog that explains why deletion cannot proceed, with customizable content and messages.
    • Added SDK options for customizing the blocked-deletion experience.
  • Bug Fixes
    • Improved deletion error feedback for blocked deletions, permission issues, and organizations that cannot be found.

The SDK now parses deletion blockers into typed data, formats blocker messages, and supports a customizable blocked-delete dialog. The organization view checks deletion eligibility when deletion starts and routes the result to the confirmation or blocked dialog.

Changes: Organization deletion flow

Layer / File(s) Summary
Blocker data and messages
web/sdk/shared/types.ts, web/sdk/client/utils/delete-blockers.ts, web/sdk/client/utils/delete-blockers.test.ts
The SDK adds a typed blocker shape and maps precondition violations to blockers. Message formatting supports subject-specific and type-specific text, removes duplicate lines, and adds a generic line when needed. The previous tests for blocker wording and error parsing are removed.
Blocked-delete dialog and provider slot
web/sdk/shared/types.ts, web/sdk/client/contexts/FrontierContext.tsx, web/sdk/client/components/delete-blocked-dialog/*, web/sdk/client/index.ts
The provider accepts optional slots, including a blocked-delete dialog slot. The dialog host passes blocker context to the slot and renders the default dialog when the slot result is null or boolean. The SDK exports the dialog and related types.
Deletion check and confirmation flow
web/sdk/client/hooks/useDeleteFlow.ts, web/sdk/client/views/general/general-view.tsx, web/sdk/client/views/general/components/delete-organization-dialog.tsx, web/sdk/client/views/general/general-view.module.css, web/sdk/package.json
The organization view starts an eligibility check from the Delete button and shows a loading state while checking. The flow opens the confirmation or blocked dialog and routes errors. The confirmation dialog uses an alert-dialog handle and forwards deletion errors. The pinned Proton revision changes.

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

Change: Feature

Possibly related PRs

  • raystack/frontier#2017: Adds organization resource blockers to the check response and delete precondition path consumed by this SDK flow.
  • raystack/frontier#2005: Adds the blocker type and count field that the SDK blocker model carries.
  • raystack/frontier#2011: Passes blocker counts through the check response that the SDK copies into DeleteBlocker.
Merge Risk: Low · previous review 5cacc

The change is mergeable with awareness of remaining dialog issues: blocker explanations can be missing or stale, and switching organizations during a check can open an unchecked confirmation. Server-side deletion safeguards remain intact.

Pre-merge checks: Passed 2
Check name Status Explanation
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.
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Tip

🎯 Customize your risk assessments

Guide how CodeRabbit assesses merge risk and security architecture risk. Highlight sensitive changes and explain your security boundaries using Merge Risk Instructions and Security Risk Instructions in your review settings. Security architecture risk assessments require the Advanced plan or above.

✅ Automatic approvals, with your rules

Let CodeRabbit approve pull requests once required threads are resolved, pre-merge checks pass, and your risk limits are met. Enable Automatic approvals in review settings, with overrides for teams or file paths.

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
Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 56698ff and 5cacc30.

Files selected for processing: 9
  • web/sdk/client/components/delete-blocked-dialog/delete-blocked-dialog.tsx
  • web/sdk/client/components/delete-blocked-dialog/index.ts
  • web/sdk/client/hooks/useDeleteFlow.ts
  • web/sdk/client/utils/delete-blockers.test.ts
  • web/sdk/client/utils/delete-blockers.ts
  • web/sdk/client/views/general/components/delete-organization-dialog.tsx
  • web/sdk/client/views/general/general-view.module.css
  • web/sdk/client/views/general/general-view.tsx
  • web/sdk/shared/types.ts
Files with no reviewable changes: 1
  • web/sdk/client/utils/delete-blockers.test.ts
Files skipped from review as they are similar to previous changes: 1
  • web/sdk/client/views/general/components/delete-organization-dialog.tsx

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 1 remain after this review.

The form now mounts only while the dialog is open, so it starts empty on
every open and fetches the token balance then, without an open prop.
@coveralls

coveralls commented Oct 26, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 41786171251

Coverage remained the same at 56.053%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 42189
Covered Lines: 23648
Line Coverage: 56.05%
Coverage Strength: 19.7 hits per line

💛 - Coveralls

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

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

Minor · Close the shared handle before invoking the success… · delete-organization-dialog.tsx:84-94

web/sdk/client/views/general/components/delete-organization-dialog.tsx:84-94
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Close the shared handle before invoking the success callback.

DeleteOrganizationForm invokes onDeleteSuccess without closing the AlertDialog. The demo callback navigates away, but the module-scoped handle retains its open state. Returning to GeneralView can reopen the confirmation dialog.

Close the handle in DeleteOrganizationDialog, where handle is in scope:

Suggested fix
-      <DeleteOrganizationForm onDeleteSuccess={onDeleteSuccess} />
+      <DeleteOrganizationForm
+        onDeleteSuccess={() => {
+          handle.close();
+          onDeleteSuccess?.();
+        }}
+      />

Review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 9655c98 and 488d0ca.

Files ignored due to path filters: 1
  • web/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
Files selected for processing: 3
  • web/sdk/client/utils/delete-blockers.ts
  • web/sdk/client/views/general/components/delete-organization-dialog.tsx
  • web/sdk/client/views/general/general-view.tsx

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 0 remain after this review.

}: DeleteOrganizationDialogProps) => (
<AlertDialog handle={handle}>
<AlertDialog.Content width={480}>
<DeleteOrganizationForm onDeleteSuccess={onDeleteSuccess} />

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@rohanchkrabrty CodeRabbit flagged this outside the diff, and it's real. The handles live at module level, and Base UI doesn't reset open when the dialog unmounts. After a delete, onDeleteSuccess navigates away but the handle still says open, so the confirm dialog pops up again the next time the settings page mounts. Can we close the handle before calling onDeleteSuccess? Creating the handles inside GeneralView with useState(() => AlertDialog.createHandle()) would also stop two views from sharing state.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. handle.close() now runs before onDeleteSuccess (63d4fe4). The handles are also created per mount in GeneralView with useState (a9080d3).

// button below stays disabled until the fetch finishes, so the user
// cannot confirm before the token warning had a chance to appear.
const { tokenBalance, isTokensFetching } = useTokens({ enabled: open });
const { tokenBalance, isTokensFetching } = useTokens();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nothing passes enabled to useTokens anymore, but the comment on that option in client/hooks/useTokens.ts still says the delete dialog uses it. Can we drop the option and its comment?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 6277d9b. Removed the enabled option and its comment from useTokens.

}
}


Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Extra blank line here. The new delete-blockers-dialog.tsx also fails prettier --check, so a prettier run on the touched files would clean both up.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 6277d9b. Ran prettier on this file, on components/delete-blocked-dialog/delete-blocked-dialog.tsx (the renamed blockers dialog), and on hooks/useDeleteCheck.ts. The extra blank line is gone.

deleteBlockersDialogHandle.openWithPayload({
blockers: [{ type: ORGANIZATION_DISABLED }]
}),
PermissionDenied: () => toastManager.add({ title: "You don't have permission to perform this action", type: 'error' }),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

These three toasts are copies of the ones in delete-organization-dialog.tsx, and the lines run past 130 characters. Could we move them into one shared handler and run prettier?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Moved the three toasts into one handler, handleDeleteError in hooks/useDeleteCheck.ts (63d4fe4).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The long lines are fixed in 6277d9b with prettier.

return (
<AlertDialog handle={handle}>
{({ payload: rawPayload }) => {
const payload = rawPayload as DeleteBlockersPayload | undefined;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: the cast isn't needed. The payload type comes from the handle, and tsc passes with {({ payload }) => ...}.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 6277d9b. Removed the cast.

);
expect(lines).toEqual([
'Please pay your 2 outstanding invoices before deleting this workspace.'
expect(items).toHaveLength(1);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: this only checks the length, so it would still pass if the wrong item came back. Can we assert the item itself?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This no longer applies. The test file is removed in 5cacc30.

}: DeleteOrganizationDialogProps) => {
}: DeleteOrganizationDialogProps) => (
<AlertDialog handle={handle}>
<AlertDialog.Content width={480}>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: AlertDialog.Content has no width prop. tsc flags it, and it ends up as an unused attribute on the div, so it doesn't set the width. The new blockers dialog uses a class for this. Could this one do the same?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 2c01e92. The dialog now uses the deleteDialog class in general-view.module.css.

FrontierProvider takes a slots prop. Its deleteBlockedDialog slot gets the
blockers of a delete and returns a DeleteBlockedDialog with the host app's
title, description and one line per resource namespace or blocker type.
With no slot, or when the slot returns undefined, the SDK shows its own
dialog.

DeleteBlocker is the SDK's own shape for a blocker. A delete check and a
refused delete both give it, without the server's developer message.

The organization delete uses a shared useDeleteFlow hook: check on click,
then open the confirmation or the blocked dialog. A refused delete now
closes the confirmation and shows the blocked dialog instead of a toast.
A failed_precondition with no violations means a disabled organization.

Bumps @raystack/proton to 94668c36, the commit the server builds from.
@rohanchkrabrty rohanchkrabrty changed the title feat(sdk): check org delete blockers on click and show a dialog feat(sdk): check org delete blockers on click and let the host app word the dialog Nov 3, 2026
@rohanchkrabrty rohanchkrabrty changed the title feat(sdk): check org delete blockers on click and let the host app word the dialog feat(sdk): org delete blockers UI Nov 3, 2026

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

Follow-up Comments

Outside diff Invalidate obsolete delete checks when the hook owner unmounts.

44-56: Minor · Stability & Availability · Quick win

Invalidate obsolete delete checks when the hook owner unmounts.

startDelete can publish after GeneralView unmounts. If the user returns to general before check(payload) resolves, the old continuation can open the new instance's confirmation or blocked dialog through the module-scoped handle. Add a request-generation guard and invalidate it during cleanup. Guard the error and finally paths so an obsolete request cannot publish or clear isChecking for a replacement request.

Suggested fix
-import { useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
...
   const t = useTerminology();
   const [isChecking, setIsChecking] = useState(false);
+  const requestGeneration = useRef(0);
+
+  useEffect(() => {
+    return () => {
+      requestGeneration.current += 1;
+    };
+  }, []);
...
   async function startDelete(payload: TPayload) {
+    const generation = ++requestGeneration.current;
     setIsChecking(true);
     try {
       const { canDelete, blockers } = await check(payload);
+      if (generation !== requestGeneration.current) return;
       if (canDelete) {
         confirmDialogHandle.openWithPayload(payload);
       } else {
         showBlocked(toDeleteBlockers(blockers));
       }
     } catch (error) {
-      handleDeleteError(error);
+      if (generation === requestGeneration.current) {
+        handleDeleteError(error);
+      }
     } finally {
-      setIsChecking(false);
+      if (generation === requestGeneration.current) {
+        setIsChecking(false);
+      }
     }
   }

Open the source location

Review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 63d4fe4 and 56698ff.

Files selected for processing: 5
  • web/sdk/client/components/delete-blocked-dialog/delete-blocked-dialog.tsx
  • web/sdk/client/hooks/useDeleteFlow.ts
  • web/sdk/client/utils/delete-blockers.ts
  • web/sdk/client/views/general/components/delete-organization-dialog.tsx
  • web/sdk/shared/types.ts
Files skipped from review as they are similar to previous changes: 5
  • web/sdk/shared/types.ts
  • web/sdk/client/hooks/useDeleteFlow.ts
  • web/sdk/client/components/delete-blocked-dialog/delete-blocked-dialog.tsx
  • web/sdk/client/views/general/components/delete-organization-dialog.tsx
  • web/sdk/client/utils/delete-blockers.ts

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 1 remain after this review.

The delete organization dialog gets its 480px width from a CSS class.
Apsara AlertDialog.Content has no width prop, so width={480} did nothing.

DeleteBlocker.count is optional. A refused delete carries no count, so
blockersFromError leaves it out instead of setting 0.

RESOURCES_EXIST has its own default line instead of the generic one.

toDeleteBlockers and the useDeleteFlow check take the proton Blocker,
DeleteBlockedPayload is imported from shared/types only, and the dialog
context is named DeleteBlockedDialogContext.
Comment thread web/sdk/client/hooks/useDeleteCheck.ts Resolved
Comment thread web/sdk/client/utils/delete-blockers.ts Resolved
A handle created at module scope keeps its open state after the view
unmounts. A delete check that returned after the user left General, or
a dialog left open on browser Back, showed the dialog again on the next
visit. Base UI fixes this in v1.7.0 (mui/base-ui#4951), which Apsara
does not ship yet.
…iles

- Remove the enabled option from useTokens. Nothing passes it now.
- Remove the payload cast in DeleteBlockedDialogHost. The handle gives the type.
- Run prettier on the delete blocked dialog, useDeleteCheck and the delete organization dialog.
blockersFromError returns an empty list instead of an UNKNOWN blocker.
useDeleteCheck then shows an "Unable to delete" toast with the generic
message, so the blocked dialog opens only when it has something to list.
@coderabbitai coderabbitai Bot mentioned this pull request Nov 4, 2026
1 of 6 tasks
DeleteBlockedDialog now renders AlertDialog.Content, so a slot replaces
the whole visible dialog: it returns DeleteBlockedDialog, or its own
AlertDialog.Content with any width, layout and buttons. The SDK keeps
the dialog root, which owns the open state.

DeleteBlockedDialogHost is renamed to DeleteBlockedDialogRoot.

This branch was successfully deployed

1 active deployment
Preview — 7a874bfa Deployed Nov 4, 2026 by vercel[bot]
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.

3 participants