Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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.`
Comment thread
Shreyag02 marked this conversation as resolved.
Resolved
})
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,9 +25,14 @@ type AlertDialogHandle = ReturnType<typeof AlertDialog.createHandle<DeleteProjec
export interface DeleteProjectDialogProps {
handle: AlertDialogHandle;
refetch: () => 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);

Expand All @@ -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);
}
Expand Down
73 changes: 57 additions & 16 deletions web/sdk/client/views/projects/project-details-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ import {
Dialog,
IconButton,
Image,
Select
Select,
Spinner
} 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,
Expand All @@ -34,9 +36,12 @@ 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';
import { DeleteBlockedDialogRoot } from '../../components/delete-blocked-dialog';
import type { DeleteBlockedPayload } from '../../../shared/types';
import {
EditProjectDialog,
type EditProjectPayload
Expand Down Expand Up @@ -73,8 +78,6 @@ interface ProjectUserRolePair {

const memberMenuHandle = Menu.createHandle<MemberMenuPayload>();
const editProjectDialogHandle = Dialog.createHandle<EditProjectPayload>();
const deleteProjectDialogHandle =
AlertDialog.createHandle<DeleteProjectPayload>();
const removeMemberDialogHandle =
AlertDialog.createHandle<RemoveMemberPayload>();
const updateRoleDialogHandle = AlertDialog.createHandle<UpdateRolePayload>();
Expand All @@ -93,6 +96,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<DeleteProjectPayload>()
);
const [deleteBlockedDialogHandle] = useState(() =>
AlertDialog.createHandle<DeleteBlockedPayload>()
);

const {
data: project,
Expand Down Expand Up @@ -287,6 +300,19 @@ export function ProjectDetailsView({
onDeleteSuccess?.();
}, [onDeleteSuccess]);

const { mutateAsync: checkProjectDelete } = useMutation(
FrontierServiceQueries.checkProjectDelete
);
const { startDelete, isChecking, handleDeleteError } = useDeleteCheck({
entity: 'project',
check: ({ projectId }) =>
checkProjectDelete(
create(CheckProjectDeleteRequestSchema, { id: projectId })
),
confirmDialogHandle: deleteProjectDialogHandle,
blockedDialogHandle: deleteBlockedDialogHandle
});

const projectTitle = project?.title || '';
const projectName = project?.name || '';

Expand Down Expand Up @@ -314,11 +340,17 @@ export function ProjectDetailsView({
>
{!isLoading && (canUpdateProject || canDeleteProject) && (
<ProjectActionsMenu
menuHandle={projectActionsMenuHandle}
projectId={projectId}
projectName={projectName}
projectTitle={projectTitle}
canUpdate={canUpdateProject}
canDelete={canDeleteProject}
isCheckingDelete={isChecking}
onDelete={async () => {
await startDelete({ projectId, projectName: projectTitle });
projectActionsMenuHandle.close();
}}
Comment thread
rohanchkrabrty marked this conversation as resolved.
Resolved
/>
)}
</ViewHeader>
Expand Down Expand Up @@ -409,7 +441,7 @@ export function ProjectDetailsView({
</Menu.Item>
))}
<Menu.Item
leadingIcon={<Image src={deleteIcon as unknown as string} alt="Remove" width={16} height={16} />}
leadingIcon={<Image src={deleteIcon as unknown as string} alt="" width={16} height={16} />}
onClick={() =>
payload &&
removeMemberDialogHandle.openWithPayload({
Expand Down Expand Up @@ -437,7 +469,9 @@ export function ProjectDetailsView({
<DeleteProjectDialog
handle={deleteProjectDialogHandle}
refetch={handleDeleteSuccess}
onDeleteError={handleDeleteError}
/>
<DeleteBlockedDialogRoot handle={deleteBlockedDialogHandle} />
<RemoveMemberDialog
handle={removeMemberDialogHandle}
refetch={refetchMembers}
Expand All @@ -452,26 +486,30 @@ export function ProjectDetailsView({
}

interface ProjectActionsMenuProps {
menuHandle: ReturnType<typeof Menu.createHandle>;
projectId: string;
projectName: string;
projectTitle: string;
canUpdate: boolean;
canDelete: boolean;
isCheckingDelete: boolean;
onDelete: () => void;
}

const projectActionsMenuHandle = Menu.createHandle();

function ProjectActionsMenu({
menuHandle,
projectId,
projectName,
projectTitle,
canUpdate,
canDelete
canDelete,
isCheckingDelete,
onDelete
}: ProjectActionsMenuProps) {
return (
<>
<Menu.Trigger
handle={projectActionsMenuHandle}
handle={menuHandle}
render={
<IconButton
size={2}
Expand All @@ -482,7 +520,7 @@ function ProjectActionsMenu({
>
<DotsHorizontalIcon />
</Menu.Trigger>
<Menu handle={projectActionsMenuHandle} modal={false}>
<Menu handle={menuHandle} modal={false}>
<Menu.Content align="start" className={styles.menuContent}>
{canUpdate && (
<Menu.Item
Expand All @@ -501,13 +539,16 @@ function ProjectActionsMenu({
)}
{canDelete && (
<Menu.Item
leadingIcon={<Image src={deleteIcon as unknown as string} alt="Delete" width={16} height={16} />}
onClick={() =>
deleteProjectDialogHandle.openWithPayload({
projectId,
projectName: projectTitle
})
leadingIcon={
isCheckingDelete ? (
<Spinner size={2} color="danger" />
) : (
<Image src={deleteIcon as unknown as string} alt="" width={16} height={16} />
)
}
closeOnClick={false}
disabled={isCheckingDelete}
Comment thread
rohanchkrabrty marked this conversation as resolved.
Resolved
onClick={onDelete}
data-test-id="frontier-sdk-delete-project-details-btn"
style={{ color: 'var(--rs-color-foreground-danger-primary)' }}
>
Expand Down
66 changes: 56 additions & 10 deletions web/sdk/client/views/projects/projects-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,28 @@ import {
AlertDialog,
Image,
Menu,
Select
Select,
Spinner
} from '@raystack/apsara';
import { create } from '@bufbuild/protobuf';
import { useMutation } from '@connectrpc/connect-query';
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';
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';
import { ViewHeader } from '../../components/view-header';
import { DeleteBlockedDialogRoot } 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';
Expand All @@ -42,10 +52,8 @@ const projectsFilterOptions = [
{ value: 'all-projects', label: 'All Projects' }
];

const projectMenuHandle = Menu.createHandle<ProjectMenuPayload>();
const addProjectDialogHandle = Dialog.createHandle();
const editProjectDialogHandle = Dialog.createHandle<EditProjectPayload>();
const deleteProjectDialogHandle = AlertDialog.createHandle<DeleteProjectPayload>();

export interface ProjectsViewProps {
title?: string;
Expand Down Expand Up @@ -74,6 +82,30 @@ 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<ProjectMenuPayload>()
);
const [deleteProjectDialogHandle] = useState(() =>
AlertDialog.createHandle<DeleteProjectPayload>()
);
const [deleteBlockedDialogHandle] = useState(() =>
AlertDialog.createHandle<DeleteBlockedPayload>()
);
const { mutateAsync: checkProjectDelete } = useMutation(
FrontierServiceQueries.checkProjectDelete
);
const { startDelete, isChecking, handleDeleteError } = useDeleteCheck({
entity: 'project',
check: ({ projectId }) =>
checkProjectDelete(
create(CheckProjectDeleteRequestSchema, { id: projectId })
),
confirmDialogHandle: deleteProjectDialogHandle,
blockedDialogHandle: deleteBlockedDialogHandle
Comment thread
rohanchkrabrty marked this conversation as resolved.
Resolved
});

const resource = `app/organization:${organization?.id}`;
const listOfPermissionsToCheck = useMemo(
Expand Down Expand Up @@ -136,7 +168,7 @@ export function ProjectsView({
userAccessOnProject,
menuHandle: projectMenuHandle
}),
[userAccessOnProject]
[userAccessOnProject, projectMenuHandle]
);

if (hasNoProjects) {
Expand Down Expand Up @@ -304,13 +336,22 @@ export function ProjectsView({
)}
{payload?.canDelete && (
<Menu.Item
leadingIcon={<Image src={deleteIcon as unknown as string} alt="Delete" width={16} height={16} />}
onClick={() =>
deleteProjectDialogHandle.openWithPayload({
leadingIcon={
isChecking ? (
<Spinner size={2} color="danger" />
Comment thread
rohanchkrabrty marked this conversation as resolved.
Resolved
) : (
<Image src={deleteIcon as unknown as string} alt="" width={16} height={16} />
)
}
closeOnClick={false}
disabled={isChecking}
onClick={async () => {
await startDelete({
projectId: payload.projectId,
projectName: payload.title
})
}
});
projectMenuHandle.close();
}}
Comment thread
rohanchkrabrty marked this conversation as resolved.
Resolved
data-test-id="delete-project-dropdown-item"
style={{ color: 'var(--rs-color-foreground-danger-primary)' }}
>
Expand All @@ -324,7 +365,12 @@ export function ProjectsView({

<AddProjectDialog handle={addProjectDialogHandle} refetch={refetch} />
<EditProjectDialog handle={editProjectDialogHandle} refetch={refetch} />
<DeleteProjectDialog handle={deleteProjectDialogHandle} refetch={refetch} />
<DeleteProjectDialog
handle={deleteProjectDialogHandle}
refetch={refetch}
onDeleteError={handleDeleteError}
/>
<DeleteBlockedDialogRoot handle={deleteBlockedDialogHandle} />
</ViewContainer>
);
}
2 changes: 1 addition & 1 deletion web/sdk/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export interface DeleteBlocker {
}

export type DeleteBlockedPayload = {
entity: 'organization';
entity: 'organization' | 'project';
blockers: DeleteBlocker[];
};

Expand Down
Loading