From e2770c52e6884bb5ae459aa9b931aae6cc4fb314 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Tue, 3 Nov 2026 20:21:59 +0530 Subject: [PATCH 1/6] feat(sdk): check project delete blockers before the confirmation Both project menus now run CheckProjectDelete on click. The menu item shows a spinner while the check runs, then the menu closes and the confirmation or the blocked dialog opens. A refused delete closes the confirmation and shows the blocked dialog. The project flow uses the same useDeleteFlow hook and deleteBlockedDialog slot as the organization flow, so a host app words both in one place. --- .../delete-blocked-dialog.tsx | 4 ++ .../components/delete-project-dialog.tsx | 14 +++--- .../components/delete-project-menu-item.tsx | 34 ++++++++++++++ .../projects/hooks/useProjectDeleteFlow.ts | 45 +++++++++++++++++++ .../views/projects/project-details-view.tsx | 45 ++++++++++++------- .../client/views/projects/projects-view.tsx | 32 +++++++++---- web/sdk/shared/types.ts | 2 +- 7 files changed, 144 insertions(+), 32 deletions(-) create mode 100644 web/sdk/client/views/projects/components/delete-project-menu-item.tsx create mode 100644 web/sdk/client/views/projects/hooks/useProjectDeleteFlow.ts diff --git a/web/sdk/client/components/delete-blocked-dialog/delete-blocked-dialog.tsx b/web/sdk/client/components/delete-blocked-dialog/delete-blocked-dialog.tsx index 89781913b6..213f717dd1 100644 --- a/web/sdk/client/components/delete-blocked-dialog/delete-blocked-dialog.tsx +++ b/web/sdk/client/components/delete-blocked-dialog/delete-blocked-dialog.tsx @@ -23,6 +23,10 @@ const DEFAULT_COPY: Record< organization: entityLabel => ({ title: `Unable to delete ${entityLabel}`, description: `Please resolve the following issues before deleting this ${entityLabel}:` + }), + project: entityLabel => ({ + title: `Unable to delete ${entityLabel}`, + description: `This ${entityLabel} cannot be deleted because it contains resources that need to be addressed first.` }) }; diff --git a/web/sdk/client/views/projects/components/delete-project-dialog.tsx b/web/sdk/client/views/projects/components/delete-project-dialog.tsx index 11aaa52ca1..65152cce7e 100644 --- a/web/sdk/client/views/projects/components/delete-project-dialog.tsx +++ b/web/sdk/client/views/projects/components/delete-project-dialog.tsx @@ -14,7 +14,6 @@ import { DeleteProjectRequestSchema } from '@raystack/proton/frontier'; import { create } from '@bufbuild/protobuf'; -import { handleConnectError } from '~/utils/error'; export interface DeleteProjectPayload { projectId: string; @@ -26,9 +25,14 @@ type AlertDialogHandle = ReturnType void; + onDeleteError: (error: unknown) => void; } -export function DeleteProjectDialog({ handle, refetch }: DeleteProjectDialogProps) { +export function DeleteProjectDialog({ + handle, + refetch, + onDeleteError +}: DeleteProjectDialogProps) { const { activeOrganization: organization } = useFrontier(); const [isLoading, setIsLoading] = useState(false); @@ -47,11 +51,7 @@ export function DeleteProjectDialog({ handle, refetch }: DeleteProjectDialogProp refetch(); handle.close(); } catch (error) { - handleConnectError(error, { - PermissionDenied: () => toastManager.add({ title: "You don't have permission to perform this action", type: 'error' }), - NotFound: (err) => toastManager.add({ title: 'Not found', description: err.message, type: 'error' }), - Default: (err) => toastManager.add({ title: 'Something went wrong', description: err.message, type: 'error' }), - }); + onDeleteError(error); } finally { setIsLoading(false); } diff --git a/web/sdk/client/views/projects/components/delete-project-menu-item.tsx b/web/sdk/client/views/projects/components/delete-project-menu-item.tsx new file mode 100644 index 0000000000..66404fb62a --- /dev/null +++ b/web/sdk/client/views/projects/components/delete-project-menu-item.tsx @@ -0,0 +1,34 @@ +'use client'; + +import { Image, Menu, Spinner } from '@raystack/apsara'; +import deleteIcon from '../../../assets/delete.svg'; + +export interface DeleteProjectMenuItemProps { + isChecking: boolean; + onClick: () => void; + 'data-test-id': string; +} + +// Stays open with a spinner while the delete check runs. +export function DeleteProjectMenuItem({ + isChecking, + ...props +}: DeleteProjectMenuItemProps) { + return ( + + ) : ( + Delete + ) + } + closeOnClick={false} + disabled={isChecking} + style={{ color: 'var(--rs-color-foreground-danger-primary)' }} + {...props} + > + Delete project + + ); +} diff --git a/web/sdk/client/views/projects/hooks/useProjectDeleteFlow.ts b/web/sdk/client/views/projects/hooks/useProjectDeleteFlow.ts new file mode 100644 index 0000000000..a53f36e962 --- /dev/null +++ b/web/sdk/client/views/projects/hooks/useProjectDeleteFlow.ts @@ -0,0 +1,45 @@ +import { create } from '@bufbuild/protobuf'; +import { useMutation } from '@connectrpc/connect-query'; +import { + CheckProjectDeleteRequestSchema, + FrontierServiceQueries +} from '@raystack/proton/frontier'; +import { + useDeleteFlow, + type UseDeleteFlowOptions +} from '../../../hooks/useDeleteFlow'; +import type { DeleteProjectPayload } from '../components/delete-project-dialog'; + +export interface UseProjectDeleteFlowOptions + extends Pick< + UseDeleteFlowOptions, + 'confirmDialogHandle' | 'blockedDialogHandle' + > { + menuHandle: { close: () => void }; +} + +// The project delete flow for a menu item. The menu stays open while the +// check runs and closes when the next dialog opens. +export function useProjectDeleteFlow({ + menuHandle, + ...handles +}: UseProjectDeleteFlowOptions) { + const { mutateAsync: checkProjectDelete } = useMutation( + FrontierServiceQueries.checkProjectDelete + ); + const { startDelete, ...flow } = useDeleteFlow({ + entity: 'project', + check: ({ projectId }) => + checkProjectDelete( + create(CheckProjectDeleteRequestSchema, { id: projectId }) + ), + ...handles + }); + + async function startDeleteFromMenu(payload: DeleteProjectPayload) { + await startDelete(payload); + menuHandle.close(); + } + + return { ...flow, startDelete: startDeleteFromMenu }; +} diff --git a/web/sdk/client/views/projects/project-details-view.tsx b/web/sdk/client/views/projects/project-details-view.tsx index ffad9ef2e4..1b64e47f5b 100644 --- a/web/sdk/client/views/projects/project-details-view.tsx +++ b/web/sdk/client/views/projects/project-details-view.tsx @@ -37,6 +37,10 @@ import { usePermissions } from '../../hooks/usePermissions'; import { PERMISSIONS, shouldShowComponent } from '../../../utils'; import { ViewContainer } from '../../components/view-container'; import { ViewHeader } from '../../components/view-header'; +import { + DeleteBlockedDialogHost, + type DeleteBlockedPayload +} from '../../components/delete-blocked-dialog'; import { EditProjectDialog, type EditProjectPayload @@ -45,6 +49,8 @@ import { DeleteProjectDialog, type DeleteProjectPayload } from './components/delete-project-dialog'; +import { DeleteProjectMenuItem } from './components/delete-project-menu-item'; +import { useProjectDeleteFlow } from './hooks/useProjectDeleteFlow'; import { RemoveMemberDialog, type RemoveMemberPayload @@ -75,6 +81,9 @@ const memberMenuHandle = Menu.createHandle(); const editProjectDialogHandle = Dialog.createHandle(); const deleteProjectDialogHandle = AlertDialog.createHandle(); +const deleteBlockedDialogHandle = + AlertDialog.createHandle(); +const projectActionsMenuHandle = Menu.createHandle(); const removeMemberDialogHandle = AlertDialog.createHandle(); const updateRoleDialogHandle = AlertDialog.createHandle(); @@ -287,6 +296,12 @@ export function ProjectDetailsView({ onDeleteSuccess?.(); }, [onDeleteSuccess]); + const { startDelete, isChecking, handleDeleteError } = useProjectDeleteFlow({ + menuHandle: projectActionsMenuHandle, + confirmDialogHandle: deleteProjectDialogHandle, + blockedDialogHandle: deleteBlockedDialogHandle + }); + const projectTitle = project?.title || ''; const projectName = project?.name || ''; @@ -319,6 +334,10 @@ export function ProjectDetailsView({ projectTitle={projectTitle} canUpdate={canUpdateProject} canDelete={canDeleteProject} + isCheckingDelete={isChecking} + onDelete={() => + startDelete({ projectId, projectName: projectTitle }) + } /> )} @@ -437,7 +456,9 @@ export function ProjectDetailsView({ + void; } -const projectActionsMenuHandle = Menu.createHandle(); - function ProjectActionsMenu({ projectId, projectName, projectTitle, canUpdate, - canDelete + canDelete, + isCheckingDelete, + onDelete }: ProjectActionsMenuProps) { return ( <> @@ -500,19 +523,11 @@ function ProjectActionsMenu({ )} {canDelete && ( - } - onClick={() => - deleteProjectDialogHandle.openWithPayload({ - projectId, - projectName: projectTitle - }) - } + - Delete project - + /> )} diff --git a/web/sdk/client/views/projects/projects-view.tsx b/web/sdk/client/views/projects/projects-view.tsx index 6f3bfae5f5..ecbb420648 100644 --- a/web/sdk/client/views/projects/projects-view.tsx +++ b/web/sdk/client/views/projects/projects-view.tsx @@ -19,7 +19,6 @@ import { Menu, Select } from '@raystack/apsara'; -import deleteIcon from '../../assets/delete.svg'; import inboxStackIcon from '../../assets/inbox-stack.svg'; import { toastManager } from '@raystack/apsara'; import { useFrontier } from '../../contexts/FrontierContext'; @@ -29,10 +28,16 @@ import { AuthTooltipMessage } from '../../utils'; import { PERMISSIONS, shouldShowComponent } from '../../../utils'; import { ViewContainer } from '../../components/view-container'; import { ViewHeader } from '../../components/view-header'; +import { + DeleteBlockedDialogHost, + type DeleteBlockedPayload +} from '../../components/delete-blocked-dialog'; import { getColumns, type ProjectMenuPayload } from './components/project-columns'; import { AddProjectDialog } from './components/add-project-dialog'; import { EditProjectDialog, type EditProjectPayload } from './components/edit-project-dialog'; import { DeleteProjectDialog, type DeleteProjectPayload } from './components/delete-project-dialog'; +import { DeleteProjectMenuItem } from './components/delete-project-menu-item'; +import { useProjectDeleteFlow } from './hooks/useProjectDeleteFlow'; import { AddMemberMenuContent } from './components/add-member-menu'; import styles from './projects-view.module.css'; import { useTerminology } from '~/client/hooks/useTerminology'; @@ -46,6 +51,8 @@ const projectMenuHandle = Menu.createHandle(); const addProjectDialogHandle = Dialog.createHandle(); const editProjectDialogHandle = Dialog.createHandle(); const deleteProjectDialogHandle = AlertDialog.createHandle(); +const deleteBlockedDialogHandle = + AlertDialog.createHandle(); export interface ProjectsViewProps { title?: string; @@ -74,6 +81,11 @@ export function ProjectsView({ const { activeOrganization: organization } = useFrontier(); const t = useTerminology(); + const { startDelete, isChecking, handleDeleteError } = useProjectDeleteFlow({ + menuHandle: projectMenuHandle, + confirmDialogHandle: deleteProjectDialogHandle, + blockedDialogHandle: deleteBlockedDialogHandle + }); const resource = `app/organization:${organization?.id}`; const listOfPermissionsToCheck = useMemo( @@ -303,19 +315,16 @@ export function ProjectsView({ )} {payload?.canDelete && ( - } + - deleteProjectDialogHandle.openWithPayload({ + startDelete({ projectId: payload.projectId, projectName: payload.title }) } data-test-id="delete-project-dropdown-item" - style={{ color: 'var(--rs-color-foreground-danger-primary)' }} - > - Delete project - + /> )} ); @@ -324,7 +333,12 @@ export function ProjectsView({ - + + ); } diff --git a/web/sdk/shared/types.ts b/web/sdk/shared/types.ts index e190512ada..bcb28924e4 100644 --- a/web/sdk/shared/types.ts +++ b/web/sdk/shared/types.ts @@ -73,7 +73,7 @@ export interface DeleteBlocker { } export type DeleteBlockedPayload = { - entity: 'organization'; + entity: 'organization' | 'project'; blockers: DeleteBlocker[]; }; From 4dc2a24dc4363a6e301002d201984d3ea224df15 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Tue, 3 Nov 2026 22:42:05 +0530 Subject: [PATCH 2/6] chore(sdk): drop comments from the project delete changes --- .../views/projects/components/delete-project-menu-item.tsx | 1 - web/sdk/client/views/projects/hooks/useProjectDeleteFlow.ts | 2 -- 2 files changed, 3 deletions(-) diff --git a/web/sdk/client/views/projects/components/delete-project-menu-item.tsx b/web/sdk/client/views/projects/components/delete-project-menu-item.tsx index 66404fb62a..b194fc7b8e 100644 --- a/web/sdk/client/views/projects/components/delete-project-menu-item.tsx +++ b/web/sdk/client/views/projects/components/delete-project-menu-item.tsx @@ -9,7 +9,6 @@ export interface DeleteProjectMenuItemProps { 'data-test-id': string; } -// Stays open with a spinner while the delete check runs. export function DeleteProjectMenuItem({ isChecking, ...props diff --git a/web/sdk/client/views/projects/hooks/useProjectDeleteFlow.ts b/web/sdk/client/views/projects/hooks/useProjectDeleteFlow.ts index a53f36e962..94d4e99281 100644 --- a/web/sdk/client/views/projects/hooks/useProjectDeleteFlow.ts +++ b/web/sdk/client/views/projects/hooks/useProjectDeleteFlow.ts @@ -18,8 +18,6 @@ export interface UseProjectDeleteFlowOptions menuHandle: { close: () => void }; } -// The project delete flow for a menu item. The menu stays open while the -// check runs and closes when the next dialog opens. export function useProjectDeleteFlow({ menuHandle, ...handles From fd97d2c8ac14b64c295a8721aeeed42ef4531095 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Wed, 4 Nov 2026 04:32:17 +0530 Subject: [PATCH 3/6] refactor(sdk): rename useProjectDeleteFlow to useProjectDeleteCheck The project delete now uses useDeleteCheck, the same as the organization delete. Both views import DeleteBlockedPayload from shared/types. --- ...tDeleteFlow.ts => useProjectDeleteCheck.ts} | 18 +++++++++--------- .../views/projects/project-details-view.tsx | 10 ++++------ .../client/views/projects/projects-view.tsx | 10 ++++------ 3 files changed, 17 insertions(+), 21 deletions(-) rename web/sdk/client/views/projects/hooks/{useProjectDeleteFlow.ts => useProjectDeleteCheck.ts} (69%) diff --git a/web/sdk/client/views/projects/hooks/useProjectDeleteFlow.ts b/web/sdk/client/views/projects/hooks/useProjectDeleteCheck.ts similarity index 69% rename from web/sdk/client/views/projects/hooks/useProjectDeleteFlow.ts rename to web/sdk/client/views/projects/hooks/useProjectDeleteCheck.ts index 94d4e99281..b2d957b6b5 100644 --- a/web/sdk/client/views/projects/hooks/useProjectDeleteFlow.ts +++ b/web/sdk/client/views/projects/hooks/useProjectDeleteCheck.ts @@ -5,27 +5,27 @@ import { FrontierServiceQueries } from '@raystack/proton/frontier'; import { - useDeleteFlow, - type UseDeleteFlowOptions -} from '../../../hooks/useDeleteFlow'; + useDeleteCheck, + type UseDeleteCheckOptions +} from '../../../hooks/useDeleteCheck'; import type { DeleteProjectPayload } from '../components/delete-project-dialog'; -export interface UseProjectDeleteFlowOptions +export interface UseProjectDeleteCheckOptions extends Pick< - UseDeleteFlowOptions, + UseDeleteCheckOptions, 'confirmDialogHandle' | 'blockedDialogHandle' > { menuHandle: { close: () => void }; } -export function useProjectDeleteFlow({ +export function useProjectDeleteCheck({ menuHandle, ...handles -}: UseProjectDeleteFlowOptions) { +}: UseProjectDeleteCheckOptions) { const { mutateAsync: checkProjectDelete } = useMutation( FrontierServiceQueries.checkProjectDelete ); - const { startDelete, ...flow } = useDeleteFlow({ + const { startDelete, ...deleteCheck } = useDeleteCheck({ entity: 'project', check: ({ projectId }) => checkProjectDelete( @@ -39,5 +39,5 @@ export function useProjectDeleteFlow({ menuHandle.close(); } - return { ...flow, startDelete: startDeleteFromMenu }; + return { ...deleteCheck, startDelete: startDeleteFromMenu }; } diff --git a/web/sdk/client/views/projects/project-details-view.tsx b/web/sdk/client/views/projects/project-details-view.tsx index 1b64e47f5b..80deda0980 100644 --- a/web/sdk/client/views/projects/project-details-view.tsx +++ b/web/sdk/client/views/projects/project-details-view.tsx @@ -37,10 +37,8 @@ import { usePermissions } from '../../hooks/usePermissions'; import { PERMISSIONS, shouldShowComponent } from '../../../utils'; import { ViewContainer } from '../../components/view-container'; import { ViewHeader } from '../../components/view-header'; -import { - DeleteBlockedDialogHost, - type DeleteBlockedPayload -} from '../../components/delete-blocked-dialog'; +import { DeleteBlockedDialogHost } from '../../components/delete-blocked-dialog'; +import type { DeleteBlockedPayload } from '../../../shared/types'; import { EditProjectDialog, type EditProjectPayload @@ -50,7 +48,7 @@ import { type DeleteProjectPayload } from './components/delete-project-dialog'; import { DeleteProjectMenuItem } from './components/delete-project-menu-item'; -import { useProjectDeleteFlow } from './hooks/useProjectDeleteFlow'; +import { useProjectDeleteCheck } from './hooks/useProjectDeleteCheck'; import { RemoveMemberDialog, type RemoveMemberPayload @@ -296,7 +294,7 @@ export function ProjectDetailsView({ onDeleteSuccess?.(); }, [onDeleteSuccess]); - const { startDelete, isChecking, handleDeleteError } = useProjectDeleteFlow({ + const { startDelete, isChecking, handleDeleteError } = useProjectDeleteCheck({ menuHandle: projectActionsMenuHandle, confirmDialogHandle: deleteProjectDialogHandle, blockedDialogHandle: deleteBlockedDialogHandle diff --git a/web/sdk/client/views/projects/projects-view.tsx b/web/sdk/client/views/projects/projects-view.tsx index ecbb420648..d68b5979d3 100644 --- a/web/sdk/client/views/projects/projects-view.tsx +++ b/web/sdk/client/views/projects/projects-view.tsx @@ -28,16 +28,14 @@ import { AuthTooltipMessage } from '../../utils'; import { PERMISSIONS, shouldShowComponent } from '../../../utils'; import { ViewContainer } from '../../components/view-container'; import { ViewHeader } from '../../components/view-header'; -import { - DeleteBlockedDialogHost, - type DeleteBlockedPayload -} from '../../components/delete-blocked-dialog'; +import { DeleteBlockedDialogHost } from '../../components/delete-blocked-dialog'; +import type { DeleteBlockedPayload } from '../../../shared/types'; import { getColumns, type ProjectMenuPayload } from './components/project-columns'; import { AddProjectDialog } from './components/add-project-dialog'; import { EditProjectDialog, type EditProjectPayload } from './components/edit-project-dialog'; import { DeleteProjectDialog, type DeleteProjectPayload } from './components/delete-project-dialog'; import { DeleteProjectMenuItem } from './components/delete-project-menu-item'; -import { useProjectDeleteFlow } from './hooks/useProjectDeleteFlow'; +import { useProjectDeleteCheck } from './hooks/useProjectDeleteCheck'; import { AddMemberMenuContent } from './components/add-member-menu'; import styles from './projects-view.module.css'; import { useTerminology } from '~/client/hooks/useTerminology'; @@ -81,7 +79,7 @@ export function ProjectsView({ const { activeOrganization: organization } = useFrontier(); const t = useTerminology(); - const { startDelete, isChecking, handleDeleteError } = useProjectDeleteFlow({ + const { startDelete, isChecking, handleDeleteError } = useProjectDeleteCheck({ menuHandle: projectMenuHandle, confirmDialogHandle: deleteProjectDialogHandle, blockedDialogHandle: deleteBlockedDialogHandle From 0508838449de10e88caeeb54f26b92597d5341a4 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Wed, 4 Nov 2026 04:32:51 +0530 Subject: [PATCH 4/6] fix(sdk): create the project delete handles per mount The same fix as the organization delete: a handle created at module scope keeps its open state after the view unmounts, so a late delete check or a dialog left open on browser Back showed the dialog again on the next visit (mui/base-ui#4951). The project menus are included. They stay open while the delete check runs, and a menu handle's close() does nothing once the menu has unmounted, so leaving during the check brought the menu back open. --- .../views/projects/project-details-view.tsx | 22 +++++++++++++------ .../client/views/projects/projects-view.tsx | 18 ++++++++++----- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/web/sdk/client/views/projects/project-details-view.tsx b/web/sdk/client/views/projects/project-details-view.tsx index 80deda0980..58539bb3c8 100644 --- a/web/sdk/client/views/projects/project-details-view.tsx +++ b/web/sdk/client/views/projects/project-details-view.tsx @@ -77,11 +77,6 @@ interface ProjectUserRolePair { const memberMenuHandle = Menu.createHandle(); const editProjectDialogHandle = Dialog.createHandle(); -const deleteProjectDialogHandle = - AlertDialog.createHandle(); -const deleteBlockedDialogHandle = - AlertDialog.createHandle(); -const projectActionsMenuHandle = Menu.createHandle(); const removeMemberDialogHandle = AlertDialog.createHandle(); const updateRoleDialogHandle = AlertDialog.createHandle(); @@ -100,6 +95,16 @@ export function ProjectDetailsView({ onDeleteSuccess }: ProjectDetailsViewProps) { const { activeOrganization: organization } = useFrontier(); + // Created per mount: a module-level handle keeps a dialog open across + // remounts (https://github.com/mui/base-ui/issues/4951). + // TODO @rohanchkrabrty: This is fixed in Base UI v1.7.0. We can update this after Apsara updates + const [projectActionsMenuHandle] = useState(() => Menu.createHandle()); + const [deleteProjectDialogHandle] = useState(() => + AlertDialog.createHandle() + ); + const [deleteBlockedDialogHandle] = useState(() => + AlertDialog.createHandle() + ); const { data: project, @@ -327,6 +332,7 @@ export function ProjectDetailsView({ > {!isLoading && (canUpdateProject || canDeleteProject) && ( ; projectId: string; projectName: string; projectTitle: string; @@ -481,6 +488,7 @@ interface ProjectActionsMenuProps { } function ProjectActionsMenu({ + menuHandle, projectId, projectName, projectTitle, @@ -492,7 +500,7 @@ function ProjectActionsMenu({ return ( <> - + {canUpdate && ( (); const addProjectDialogHandle = Dialog.createHandle(); const editProjectDialogHandle = Dialog.createHandle(); -const deleteProjectDialogHandle = AlertDialog.createHandle(); -const deleteBlockedDialogHandle = - AlertDialog.createHandle(); export interface ProjectsViewProps { title?: string; @@ -79,6 +75,18 @@ export function ProjectsView({ const { activeOrganization: organization } = useFrontier(); const t = useTerminology(); + // Created per mount: a module-level handle keeps a dialog open across + // remounts (https://github.com/mui/base-ui/issues/4951). + // TODO @rohanchkrabrty: This is fixed in Base UI v1.7.0. We can update this after Apsara updates + const [projectMenuHandle] = useState(() => + Menu.createHandle() + ); + const [deleteProjectDialogHandle] = useState(() => + AlertDialog.createHandle() + ); + const [deleteBlockedDialogHandle] = useState(() => + AlertDialog.createHandle() + ); const { startDelete, isChecking, handleDeleteError } = useProjectDeleteCheck({ menuHandle: projectMenuHandle, confirmDialogHandle: deleteProjectDialogHandle, @@ -146,7 +154,7 @@ export function ProjectsView({ userAccessOnProject, menuHandle: projectMenuHandle }), - [userAccessOnProject] + [userAccessOnProject, projectMenuHandle] ); if (hasNoProjects) { From 643864dfea1ccbca39e6db1132258af208a8ddac Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Wed, 4 Nov 2026 10:53:53 +0530 Subject: [PATCH 5/6] refactor(sdk): remove useProjectDeleteCheck Both project views call useDeleteCheck directly, the same as the organization view. Each menu still stays open with the spinner while the check runs and closes once the dialog opens. --- .../projects/hooks/useProjectDeleteCheck.ts | 43 ------------------- .../views/projects/project-details-view.tsx | 23 +++++++--- .../client/views/projects/projects-view.tsx | 28 +++++++++--- 3 files changed, 37 insertions(+), 57 deletions(-) delete mode 100644 web/sdk/client/views/projects/hooks/useProjectDeleteCheck.ts diff --git a/web/sdk/client/views/projects/hooks/useProjectDeleteCheck.ts b/web/sdk/client/views/projects/hooks/useProjectDeleteCheck.ts deleted file mode 100644 index b2d957b6b5..0000000000 --- a/web/sdk/client/views/projects/hooks/useProjectDeleteCheck.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { useMutation } from '@connectrpc/connect-query'; -import { - CheckProjectDeleteRequestSchema, - FrontierServiceQueries -} from '@raystack/proton/frontier'; -import { - useDeleteCheck, - type UseDeleteCheckOptions -} from '../../../hooks/useDeleteCheck'; -import type { DeleteProjectPayload } from '../components/delete-project-dialog'; - -export interface UseProjectDeleteCheckOptions - extends Pick< - UseDeleteCheckOptions, - 'confirmDialogHandle' | 'blockedDialogHandle' - > { - menuHandle: { close: () => void }; -} - -export function useProjectDeleteCheck({ - menuHandle, - ...handles -}: UseProjectDeleteCheckOptions) { - const { mutateAsync: checkProjectDelete } = useMutation( - FrontierServiceQueries.checkProjectDelete - ); - const { startDelete, ...deleteCheck } = useDeleteCheck({ - entity: 'project', - check: ({ projectId }) => - checkProjectDelete( - create(CheckProjectDeleteRequestSchema, { id: projectId }) - ), - ...handles - }); - - async function startDeleteFromMenu(payload: DeleteProjectPayload) { - await startDelete(payload); - menuHandle.close(); - } - - return { ...deleteCheck, startDelete: startDeleteFromMenu }; -} diff --git a/web/sdk/client/views/projects/project-details-view.tsx b/web/sdk/client/views/projects/project-details-view.tsx index 58539bb3c8..c01e3a875d 100644 --- a/web/sdk/client/views/projects/project-details-view.tsx +++ b/web/sdk/client/views/projects/project-details-view.tsx @@ -22,8 +22,9 @@ import { } from '@raystack/apsara'; import deleteIcon from '../../assets/delete.svg'; import { toastManager } from '@raystack/apsara'; -import { useQuery } from '@connectrpc/connect-query'; +import { useMutation, useQuery } from '@connectrpc/connect-query'; import { + CheckProjectDeleteRequestSchema, FrontierServiceQueries, ListProjectGroupsRequestSchema, ListProjectUsersRequestSchema, @@ -34,6 +35,7 @@ import { import { create } from '@bufbuild/protobuf'; import { useFrontier } from '../../contexts/FrontierContext'; import { usePermissions } from '../../hooks/usePermissions'; +import { useDeleteCheck } from '../../hooks/useDeleteCheck'; import { PERMISSIONS, shouldShowComponent } from '../../../utils'; import { ViewContainer } from '../../components/view-container'; import { ViewHeader } from '../../components/view-header'; @@ -48,7 +50,6 @@ import { type DeleteProjectPayload } from './components/delete-project-dialog'; import { DeleteProjectMenuItem } from './components/delete-project-menu-item'; -import { useProjectDeleteCheck } from './hooks/useProjectDeleteCheck'; import { RemoveMemberDialog, type RemoveMemberPayload @@ -299,8 +300,15 @@ export function ProjectDetailsView({ onDeleteSuccess?.(); }, [onDeleteSuccess]); - const { startDelete, isChecking, handleDeleteError } = useProjectDeleteCheck({ - menuHandle: projectActionsMenuHandle, + const { mutateAsync: checkProjectDelete } = useMutation( + FrontierServiceQueries.checkProjectDelete + ); + const { startDelete, isChecking, handleDeleteError } = useDeleteCheck({ + entity: 'project', + check: ({ projectId }) => + checkProjectDelete( + create(CheckProjectDeleteRequestSchema, { id: projectId }) + ), confirmDialogHandle: deleteProjectDialogHandle, blockedDialogHandle: deleteBlockedDialogHandle }); @@ -339,9 +347,10 @@ export function ProjectDetailsView({ canUpdate={canUpdateProject} canDelete={canDeleteProject} isCheckingDelete={isChecking} - onDelete={() => - startDelete({ projectId, projectName: projectTitle }) - } + onDelete={async () => { + await startDelete({ projectId, projectName: projectTitle }); + projectActionsMenuHandle.close(); + }} /> )} diff --git a/web/sdk/client/views/projects/projects-view.tsx b/web/sdk/client/views/projects/projects-view.tsx index d44aa01691..09da31036e 100644 --- a/web/sdk/client/views/projects/projects-view.tsx +++ b/web/sdk/client/views/projects/projects-view.tsx @@ -19,11 +19,18 @@ import { Menu, Select } from '@raystack/apsara'; +import { create } from '@bufbuild/protobuf'; +import { useMutation } from '@connectrpc/connect-query'; +import { + CheckProjectDeleteRequestSchema, + FrontierServiceQueries +} from '@raystack/proton/frontier'; import inboxStackIcon from '../../assets/inbox-stack.svg'; import { toastManager } from '@raystack/apsara'; import { useFrontier } from '../../contexts/FrontierContext'; import { useOrganizationProjects } from '../../hooks/useOrganizationProjects'; import { usePermissions } from '../../hooks/usePermissions'; +import { useDeleteCheck } from '../../hooks/useDeleteCheck'; import { AuthTooltipMessage } from '../../utils'; import { PERMISSIONS, shouldShowComponent } from '../../../utils'; import { ViewContainer } from '../../components/view-container'; @@ -35,7 +42,6 @@ import { AddProjectDialog } from './components/add-project-dialog'; import { EditProjectDialog, type EditProjectPayload } from './components/edit-project-dialog'; import { DeleteProjectDialog, type DeleteProjectPayload } from './components/delete-project-dialog'; import { DeleteProjectMenuItem } from './components/delete-project-menu-item'; -import { useProjectDeleteCheck } from './hooks/useProjectDeleteCheck'; import { AddMemberMenuContent } from './components/add-member-menu'; import styles from './projects-view.module.css'; import { useTerminology } from '~/client/hooks/useTerminology'; @@ -87,8 +93,15 @@ export function ProjectsView({ const [deleteBlockedDialogHandle] = useState(() => AlertDialog.createHandle() ); - const { startDelete, isChecking, handleDeleteError } = useProjectDeleteCheck({ - menuHandle: projectMenuHandle, + const { mutateAsync: checkProjectDelete } = useMutation( + FrontierServiceQueries.checkProjectDelete + ); + const { startDelete, isChecking, handleDeleteError } = useDeleteCheck({ + entity: 'project', + check: ({ projectId }) => + checkProjectDelete( + create(CheckProjectDeleteRequestSchema, { id: projectId }) + ), confirmDialogHandle: deleteProjectDialogHandle, blockedDialogHandle: deleteBlockedDialogHandle }); @@ -323,12 +336,13 @@ export function ProjectsView({ {payload?.canDelete && ( - startDelete({ + onClick={async () => { + await startDelete({ projectId: payload.projectId, projectName: payload.title - }) - } + }); + projectMenuHandle.close(); + }} data-test-id="delete-project-dropdown-item" /> )} From 936eb3e63f827943a297b6986c7e4573968e0b4a Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Wed, 4 Nov 2026 11:48:55 +0530 Subject: [PATCH 6/6] refactor(sdk): use DeleteBlockedDialogRoot in the project views --- web/sdk/client/views/projects/project-details-view.tsx | 4 ++-- web/sdk/client/views/projects/projects-view.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/web/sdk/client/views/projects/project-details-view.tsx b/web/sdk/client/views/projects/project-details-view.tsx index c01e3a875d..372f272203 100644 --- a/web/sdk/client/views/projects/project-details-view.tsx +++ b/web/sdk/client/views/projects/project-details-view.tsx @@ -39,7 +39,7 @@ import { useDeleteCheck } from '../../hooks/useDeleteCheck'; import { PERMISSIONS, shouldShowComponent } from '../../../utils'; import { ViewContainer } from '../../components/view-container'; import { ViewHeader } from '../../components/view-header'; -import { DeleteBlockedDialogHost } from '../../components/delete-blocked-dialog'; +import { DeleteBlockedDialogRoot } from '../../components/delete-blocked-dialog'; import type { DeleteBlockedPayload } from '../../../shared/types'; import { EditProjectDialog, @@ -471,7 +471,7 @@ export function ProjectDetailsView({ refetch={handleDeleteSuccess} onDeleteError={handleDeleteError} /> - + - + ); }