From 35e58e9ec7bde7489f4bd85f60e0bea4b502f7af Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Mon, 29 Jul 2024 19:08:18 +0530 Subject: [PATCH] [WEB-2043] fix: delete action validation and toast alert (#5254) * dev: canPerformProjectAdminActions helper function added * chore: deleteInboxIssue action updated * dev: bulk delete modal validation updated * chore: issue, intake, cycle and module delete action toast updated * chore: code refactor --- .../command-palette/command-palette.tsx | 22 +++++++++++++++- web/core/components/cycles/delete-modal.tsx | 17 ++++++++++--- .../inbox/modals/delete-issue-modal.tsx | 25 +++++++++++++++++-- .../components/issues/delete-issue-modal.tsx | 14 ++++++++--- .../modules/delete-module-modal.tsx | 15 ++++++----- web/core/constants/project.ts | 19 ++++++++++++++ web/core/store/inbox/project-inbox.store.ts | 3 ++- web/core/store/user/index.ts | 10 ++++++++ 8 files changed, 107 insertions(+), 18 deletions(-) diff --git a/web/core/components/command-palette/command-palette.tsx b/web/core/components/command-palette/command-palette.tsx index ab9215d3e0..099d53f909 100644 --- a/web/core/components/command-palette/command-palette.tsx +++ b/web/core/components/command-palette/command-palette.tsx @@ -46,6 +46,7 @@ export const CommandPalette: FC = observer(() => { canPerformProjectCreateActions, canPerformWorkspaceCreateActions, canPerformAnyCreateAction, + canPerformProjectAdminActions, } = useUser(); const { issues: { removeIssue }, @@ -113,6 +114,19 @@ export const CommandPalette: FC = observer(() => { [canPerformProjectCreateActions] ); + const performProjectBulkDeleteActions = useCallback( + (showToast: boolean = true) => { + if (!canPerformProjectAdminActions && showToast) + setToast({ + type: TOAST_TYPE.ERROR, + title: "You don't have permission to perform this action.", + }); + + return canPerformProjectAdminActions; + }, + [canPerformProjectAdminActions] + ); + const performWorkspaceCreateActions = useCallback( (showToast: boolean = true) => { if (!canPerformWorkspaceCreateActions && showToast) @@ -210,6 +224,7 @@ export const CommandPalette: FC = observer(() => { const keyPressed = key.toLowerCase(); const cmdClicked = ctrlKey || metaKey; const shiftClicked = shiftKey; + const deleteKey = keyPressed === "backspace" || keyPressed === "delete"; if (cmdClicked && keyPressed === "k" && !isAnyModalOpen) { e.preventDefault(); @@ -229,7 +244,11 @@ export const CommandPalette: FC = observer(() => { toggleShortcutModal(true); } - if (cmdClicked) { + if (deleteKey) { + if (performProjectBulkDeleteActions()) { + shortcutsList.project.delete.action(); + } + } else if (cmdClicked) { if (keyPressed === "c" && ((platform === "MacOS" && ctrlKey) || altKey)) { e.preventDefault(); copyIssueUrlToClipboard(); @@ -266,6 +285,7 @@ export const CommandPalette: FC = observer(() => { [ performAnyProjectCreateActions, performProjectCreateActions, + performProjectBulkDeleteActions, performWorkspaceCreateActions, copyIssueUrlToClipboard, isAnyModalOpen, diff --git a/web/core/components/cycles/delete-modal.tsx b/web/core/components/cycles/delete-modal.tsx index 63b8cd4240..933350ab71 100644 --- a/web/core/components/cycles/delete-modal.tsx +++ b/web/core/components/cycles/delete-modal.tsx @@ -9,6 +9,7 @@ import { ICycle } from "@plane/types"; import { AlertModalCore, TOAST_TYPE, setToast } from "@plane/ui"; // constants import { CYCLE_DELETED } from "@/constants/event-tracker"; +import { PROJECT_ERROR_MESSAGES } from "@/constants/project"; // hooks import { useEventTracker, useCycle } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; @@ -51,16 +52,24 @@ export const CycleDeleteModal: React.FC = observer((props) => { payload: { ...cycle, state: "SUCCESS" }, }); }) - .catch(() => { + .catch((errors) => { + const isPermissionError = errors?.error === "Only admin or owner can delete the cycle"; + const currentError = isPermissionError + ? PROJECT_ERROR_MESSAGES.permissionError + : PROJECT_ERROR_MESSAGES.cycleDeleteError; + setToast({ + title: currentError.title, + type: TOAST_TYPE.ERROR, + message: currentError.message, + }); captureCycleEvent({ eventName: CYCLE_DELETED, payload: { ...cycle, state: "FAILED" }, }); - }); + }) + .finally(() => handleClose()); if (cycleId || peekCycle) router.push(`/${workspaceSlug}/projects/${projectId}/cycles`); - - handleClose(); } catch (error) { setToast({ type: TOAST_TYPE.ERROR, diff --git a/web/core/components/inbox/modals/delete-issue-modal.tsx b/web/core/components/inbox/modals/delete-issue-modal.tsx index a7043ea01e..0e98dad23b 100644 --- a/web/core/components/inbox/modals/delete-issue-modal.tsx +++ b/web/core/components/inbox/modals/delete-issue-modal.tsx @@ -3,7 +3,9 @@ import { observer } from "mobx-react"; // types import type { TIssue } from "@plane/types"; // ui -import { AlertModalCore } from "@plane/ui"; +import { AlertModalCore, setToast, TOAST_TYPE } from "@plane/ui"; +// constants +import { PROJECT_ERROR_MESSAGES } from "@/constants/project"; // hooks import { useProject } from "@/hooks/store"; @@ -29,7 +31,26 @@ export const DeleteInboxIssueModal: React.FC = observer(({ isOpen, onClos const handleDelete = async () => { setIsDeleting(true); - await onSubmit().finally(() => handleClose()); + await onSubmit() + .then(() => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: `Issue deleted successfully`, + }); + }) + .catch((errors) => { + const isPermissionError = errors?.error === "Only admin or creator can delete the issue"; + const currentError = isPermissionError + ? PROJECT_ERROR_MESSAGES.permissionError + : PROJECT_ERROR_MESSAGES.issueDeleteError; + setToast({ + title: currentError.title, + type: TOAST_TYPE.ERROR, + message: currentError.message, + }); + }) + .finally(() => handleClose()); }; return ( diff --git a/web/core/components/issues/delete-issue-modal.tsx b/web/core/components/issues/delete-issue-modal.tsx index a6a62d8126..c923241799 100644 --- a/web/core/components/issues/delete-issue-modal.tsx +++ b/web/core/components/issues/delete-issue-modal.tsx @@ -5,6 +5,8 @@ import { useEffect, useState } from "react"; import { TIssue } from "@plane/types"; // ui import { AlertModalCore, TOAST_TYPE, setToast } from "@plane/ui"; +// constants +import { PROJECT_ERROR_MESSAGES } from "@/constants/project"; // hooks import { useIssues, useProject } from "@/hooks/store"; @@ -52,14 +54,18 @@ export const DeleteIssueModal: React.FC = (props) => { }); onClose(); }) - .catch(() => { + .catch((errors) => { + const isPermissionError = errors?.error === "Only admin or creator can delete the issue"; + const currentError = isPermissionError + ? PROJECT_ERROR_MESSAGES.permissionError + : PROJECT_ERROR_MESSAGES.issueDeleteError; setToast({ - title: "Error", + title: currentError.title, type: TOAST_TYPE.ERROR, - message: "Failed to delete issue", + message: currentError.message, }); }) - .finally(() => setIsDeleting(false)); + .finally(() => onClose()); }; return ( diff --git a/web/core/components/modules/delete-module-modal.tsx b/web/core/components/modules/delete-module-modal.tsx index fb9e183989..5995993061 100644 --- a/web/core/components/modules/delete-module-modal.tsx +++ b/web/core/components/modules/delete-module-modal.tsx @@ -9,6 +9,7 @@ import type { IModule } from "@plane/types"; import { AlertModalCore, TOAST_TYPE, setToast } from "@plane/ui"; // constants import { MODULE_DELETED } from "@/constants/event-tracker"; +import { PROJECT_ERROR_MESSAGES } from "@/constants/project"; // hooks import { useEventTracker, useModule } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; @@ -54,20 +55,22 @@ export const DeleteModuleModal: React.FC = observer((props) => { payload: { ...data, state: "SUCCESS" }, }); }) - .catch(() => { + .catch((errors) => { + const isPermissionError = errors?.error === "Only admin or creator can delete the module"; + const currentError = isPermissionError + ? PROJECT_ERROR_MESSAGES.permissionError + : PROJECT_ERROR_MESSAGES.moduleDeleteError; setToast({ + title: currentError.title, type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Module could not be deleted. Please try again.", + message: currentError.message, }); captureModuleEvent({ eventName: MODULE_DELETED, payload: { ...data, state: "FAILED" }, }); }) - .finally(() => { - setIsDeleteLoading(false); - }); + .finally(() => handleClose()); }; return ( diff --git a/web/core/constants/project.ts b/web/core/constants/project.ts index d48d8d1463..0737e77d0d 100644 --- a/web/core/constants/project.ts +++ b/web/core/constants/project.ts @@ -168,3 +168,22 @@ export const PROJECT_DISPLAY_FILTER_OPTIONS: { label: "Archived", }, ]; + +export const PROJECT_ERROR_MESSAGES = { + permissionError: { + title: "You don't have permission to perform this action.", + message: undefined, + }, + cycleDeleteError: { + title: "Error", + message: "Failed to delete project", + }, + moduleDeleteError: { + title: "Error", + message: "Failed to delete module", + }, + issueDeleteError: { + title: "Error", + message: "Failed to delete issue", + }, +}; diff --git a/web/core/store/inbox/project-inbox.store.ts b/web/core/store/inbox/project-inbox.store.ts index fba737f458..9a9668a41e 100644 --- a/web/core/store/inbox/project-inbox.store.ts +++ b/web/core/store/inbox/project-inbox.store.ts @@ -455,11 +455,12 @@ export class ProjectInboxStore implements IProjectInboxStore { ); }); await this.inboxIssueService.destroy(workspaceSlug, projectId, inboxIssueId); - } catch { + } catch (error) { console.error("Error removing the intake issue"); set(this.inboxIssues, [inboxIssueId], currentIssue); set(this, ["inboxIssuePaginationInfo", "total_results"], (this.inboxIssuePaginationInfo?.total_results || 0) + 1); set(this, ["inboxIssueIds"], [...this.inboxIssueIds, inboxIssueId]); + throw error; } }; } diff --git a/web/core/store/user/index.ts b/web/core/store/user/index.ts index d218811716..d087811a76 100644 --- a/web/core/store/user/index.ts +++ b/web/core/store/user/index.ts @@ -43,6 +43,7 @@ export interface IUserStore { signOut: () => Promise; // computed canPerformProjectCreateActions: boolean; + canPerformProjectAdminActions: boolean; canPerformWorkspaceCreateActions: boolean; canPerformAnyCreateAction: boolean; projectsWithCreatePermissions: { [projectId: string]: number } | null; @@ -92,6 +93,7 @@ export class UserStore implements IUserStore { signOut: action, // computed canPerformProjectCreateActions: computed, + canPerformProjectAdminActions: computed, canPerformWorkspaceCreateActions: computed, canPerformAnyCreateAction: computed, projectsWithCreatePermissions: computed, @@ -278,6 +280,14 @@ export class UserStore implements IUserStore { return !!this.membership.currentProjectRole && this.membership.currentProjectRole >= EUserProjectRoles.MEMBER; } + /** + * @description tells if user has project admin actions permissions + * @returns {boolean} + */ + get canPerformProjectAdminActions() { + return !!this.membership.currentProjectRole && this.membership.currentProjectRole === EUserProjectRoles.ADMIN; + } + /** * @description tells if user has workspace create actions permissions * @returns {boolean}