From 65b4324507be523d1b03294c7193228af10c2794 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Tue, 3 Nov 2026 20:21:59 +0530 Subject: [PATCH 1/9] 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 6c7bf81064c76a38b12e40c9ddda64ad11fcb190 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Tue, 3 Nov 2026 22:42:05 +0530 Subject: [PATCH 2/9] 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 948d9dcfbe5c420fd6e04b5acc4eb1707e0f201e Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Wed, 4 Nov 2026 04:32:17 +0530 Subject: [PATCH 3/9] 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 88917af26ba05c356aa67ce64ecbadfe66faebf4 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Wed, 4 Nov 2026 04:32:51 +0530 Subject: [PATCH 4/9] 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 6566cbd75196dec77a801cbffc5149e023be0aa9 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Wed, 4 Nov 2026 10:53:53 +0530 Subject: [PATCH 5/9] 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 eede20e37656856494d10c5e00b0f7cca6fa7836 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Wed, 4 Nov 2026 11:48:55 +0530 Subject: [PATCH 6/9] 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} /> - + - + ); } From 0153878280f35dd409b8355fa02281ccdcdad7db Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Thu, 5 Nov 2026 10:55:54 +0530 Subject: [PATCH 7/9] fix(sdk): make the project delete spinner red The spinner used the neutral color. The menu leading icon slot sets a secondary color, so color="default" (currentColor) is grey there. color="danger" uses the same token as the Delete project label. --- .../views/projects/components/delete-project-menu-item.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b194fc7b8e..920bc9b5e1 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 @@ -17,7 +17,7 @@ export function DeleteProjectMenuItem({ + ) : ( Delete ) From d43f0487b587786a0a51076b4c8f40cee067024e Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Thu, 5 Nov 2026 11:22:45 +0530 Subject: [PATCH 8/9] refactor(sdk): inline the project delete menu item Both project views render their Delete project item in place again, with the spinner and the check state. Removes DeleteProjectMenuItem. --- .../components/delete-project-menu-item.tsx | 33 ------------------- .../views/projects/project-details-view.tsx | 21 +++++++++--- .../client/views/projects/projects-view.tsx | 22 ++++++++++--- 3 files changed, 33 insertions(+), 43 deletions(-) delete mode 100644 web/sdk/client/views/projects/components/delete-project-menu-item.tsx 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 deleted file mode 100644 index 920bc9b5e1..0000000000 --- a/web/sdk/client/views/projects/components/delete-project-menu-item.tsx +++ /dev/null @@ -1,33 +0,0 @@ -'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; -} - -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/project-details-view.tsx b/web/sdk/client/views/projects/project-details-view.tsx index 372f272203..f930237c5e 100644 --- a/web/sdk/client/views/projects/project-details-view.tsx +++ b/web/sdk/client/views/projects/project-details-view.tsx @@ -18,7 +18,8 @@ import { Dialog, IconButton, Image, - Select + Select, + Spinner } from '@raystack/apsara'; import deleteIcon from '../../assets/delete.svg'; import { toastManager } from '@raystack/apsara'; @@ -49,7 +50,6 @@ import { DeleteProjectDialog, type DeleteProjectPayload } from './components/delete-project-dialog'; -import { DeleteProjectMenuItem } from './components/delete-project-menu-item'; import { RemoveMemberDialog, type RemoveMemberPayload @@ -538,11 +538,22 @@ function ProjectActionsMenu({ )} {canDelete && ( - + ) : ( + Delete + ) + } + closeOnClick={false} + disabled={isCheckingDelete} onClick={onDelete} data-test-id="frontier-sdk-delete-project-details-btn" - /> + style={{ color: 'var(--rs-color-foreground-danger-primary)' }} + > + Delete project + )} diff --git a/web/sdk/client/views/projects/projects-view.tsx b/web/sdk/client/views/projects/projects-view.tsx index 47d4a3d046..90daa4a49c 100644 --- a/web/sdk/client/views/projects/projects-view.tsx +++ b/web/sdk/client/views/projects/projects-view.tsx @@ -17,7 +17,8 @@ import { AlertDialog, Image, Menu, - Select + Select, + Spinner } from '@raystack/apsara'; import { create } from '@bufbuild/protobuf'; import { useMutation } from '@connectrpc/connect-query'; @@ -25,6 +26,7 @@ import { CheckProjectDeleteRequestSchema, FrontierServiceQueries } from '@raystack/proton/frontier'; +import deleteIcon from '../../assets/delete.svg'; import inboxStackIcon from '../../assets/inbox-stack.svg'; import { toastManager } from '@raystack/apsara'; import { useFrontier } from '../../contexts/FrontierContext'; @@ -41,7 +43,6 @@ import { getColumns, type ProjectMenuPayload } from './components/project-column 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 { AddMemberMenuContent } from './components/add-member-menu'; import styles from './projects-view.module.css'; import { useTerminology } from '~/client/hooks/useTerminology'; @@ -334,8 +335,16 @@ export function ProjectsView({ )} {payload?.canDelete && ( - + ) : ( + Delete + ) + } + closeOnClick={false} + disabled={isChecking} onClick={async () => { await startDelete({ projectId: payload.projectId, @@ -344,7 +353,10 @@ export function ProjectsView({ projectMenuHandle.close(); }} data-test-id="delete-project-dropdown-item" - /> + style={{ color: 'var(--rs-color-foreground-danger-primary)' }} + > + Delete project + )} ); From 68379ca5c4d44986ae05e38a674d80d6d5b09e5c Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Thu, 5 Nov 2026 11:41:58 +0530 Subject: [PATCH 9/9] fix(sdk): hide the decorative icons in the project menus The Delete project and Remove items read as "Delete Delete project" and "Remove Remove" because the icon alt repeated the label. The icons now have an empty alt. --- web/sdk/client/views/projects/project-details-view.tsx | 4 ++-- web/sdk/client/views/projects/projects-view.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web/sdk/client/views/projects/project-details-view.tsx b/web/sdk/client/views/projects/project-details-view.tsx index f930237c5e..ce41fd2f61 100644 --- a/web/sdk/client/views/projects/project-details-view.tsx +++ b/web/sdk/client/views/projects/project-details-view.tsx @@ -441,7 +441,7 @@ export function ProjectDetailsView({ ))} } + leadingIcon={} onClick={() => payload && removeMemberDialogHandle.openWithPayload({ @@ -543,7 +543,7 @@ function ProjectActionsMenu({ isCheckingDelete ? ( ) : ( - Delete + ) } closeOnClick={false} diff --git a/web/sdk/client/views/projects/projects-view.tsx b/web/sdk/client/views/projects/projects-view.tsx index 90daa4a49c..b880f31b4a 100644 --- a/web/sdk/client/views/projects/projects-view.tsx +++ b/web/sdk/client/views/projects/projects-view.tsx @@ -340,7 +340,7 @@ export function ProjectsView({ isChecking ? ( ) : ( - Delete + ) } closeOnClick={false}