From 4544b919d7260a37ad6ce75500ea20eb60f66ca0 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Mon, 29 Jul 2024 12:35:39 +0530 Subject: [PATCH 1/5] dev: canPerformProjectAdminActions helper function added --- web/core/store/user/index.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/web/core/store/user/index.ts b/web/core/store/user/index.ts index d2188117168..d087811a76b 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} From f823b93fdf958001b9ddfe53eada40177018a567 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Mon, 29 Jul 2024 12:36:29 +0530 Subject: [PATCH 2/5] chore: deleteInboxIssue action updated --- web/core/store/inbox/project-inbox.store.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/core/store/inbox/project-inbox.store.ts b/web/core/store/inbox/project-inbox.store.ts index fba737f4587..9a9668a41e5 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; } }; } From 965b14b9545f8095587733a45a55d6fba4615219 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Mon, 29 Jul 2024 12:43:39 +0530 Subject: [PATCH 3/5] dev: bulk delete modal validation updated --- .../command-palette/command-palette.tsx | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/web/core/components/command-palette/command-palette.tsx b/web/core/components/command-palette/command-palette.tsx index ab9215d3e0b..099d53f909b 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, From e3202de39b14db611205253898d2eaa8ac377efc Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Mon, 29 Jul 2024 12:55:38 +0530 Subject: [PATCH 4/5] chore: issue, intake, cycle and module delete action toast updated --- web/core/components/cycles/delete-modal.tsx | 15 +++++++++---- .../inbox/modals/delete-issue-modal.tsx | 22 +++++++++++++++++-- .../components/issues/delete-issue-modal.tsx | 11 ++++++---- .../modules/delete-module-modal.tsx | 13 ++++++----- 4 files changed, 45 insertions(+), 16 deletions(-) diff --git a/web/core/components/cycles/delete-modal.tsx b/web/core/components/cycles/delete-modal.tsx index 63b8cd42402..01d530a2e9c 100644 --- a/web/core/components/cycles/delete-modal.tsx +++ b/web/core/components/cycles/delete-modal.tsx @@ -51,16 +51,23 @@ 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 toastTitle = isPermissionError ? "You don't have permission to perform this action." : "Error"; + const toastMessage = isPermissionError ? undefined : "Failed to delete cycle"; + setToast({ + title: toastTitle, + type: TOAST_TYPE.ERROR, + message: toastMessage, + }); 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 a7043ea01e1..8d8ffa8fb71 100644 --- a/web/core/components/inbox/modals/delete-issue-modal.tsx +++ b/web/core/components/inbox/modals/delete-issue-modal.tsx @@ -3,7 +3,7 @@ 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"; // hooks import { useProject } from "@/hooks/store"; @@ -29,7 +29,25 @@ 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 toastTitle = isPermissionError ? "You don't have permission to perform this action." : "Error"; + const toastMessage = isPermissionError ? undefined : "Failed to delete issue"; + setToast({ + title: toastTitle, + type: TOAST_TYPE.ERROR, + message: toastMessage, + }); + }) + .finally(() => handleClose()); }; return ( diff --git a/web/core/components/issues/delete-issue-modal.tsx b/web/core/components/issues/delete-issue-modal.tsx index a6a62d81269..4645301405e 100644 --- a/web/core/components/issues/delete-issue-modal.tsx +++ b/web/core/components/issues/delete-issue-modal.tsx @@ -52,14 +52,17 @@ export const DeleteIssueModal: React.FC = (props) => { }); onClose(); }) - .catch(() => { + .catch((errors) => { + const isPermissionError = errors?.error === "Only admin or creator can delete the issue"; + const toastTitle = isPermissionError ? "You don't have permission to perform this action." : "Error"; + const toastMessage = isPermissionError ? undefined : "Failed to delete issue"; setToast({ - title: "Error", + title: toastTitle, type: TOAST_TYPE.ERROR, - message: "Failed to delete issue", + message: toastMessage, }); }) - .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 fb9e183989a..bd9e2d6a9d1 100644 --- a/web/core/components/modules/delete-module-modal.tsx +++ b/web/core/components/modules/delete-module-modal.tsx @@ -54,20 +54,21 @@ 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 toastTitle = isPermissionError ? "You don't have permission to perform this action." : "Error!"; + const toastMessage = isPermissionError ? undefined : "Module could not be deleted. Please try again."; setToast({ + title: toastTitle, type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Module could not be deleted. Please try again.", + message: toastMessage, }); captureModuleEvent({ eventName: MODULE_DELETED, payload: { ...data, state: "FAILED" }, }); }) - .finally(() => { - setIsDeleteLoading(false); - }); + .finally(() => handleClose()); }; return ( From 0a4e0e53de0ab1e5bbd188eca83faa04eea7c59b Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Mon, 29 Jul 2024 18:37:26 +0530 Subject: [PATCH 5/5] chore: code refactor --- web/core/components/cycles/delete-modal.tsx | 10 ++++++---- .../inbox/modals/delete-issue-modal.tsx | 11 +++++++---- .../components/issues/delete-issue-modal.tsx | 11 +++++++---- .../modules/delete-module-modal.tsx | 10 ++++++---- web/core/constants/project.ts | 19 +++++++++++++++++++ 5 files changed, 45 insertions(+), 16 deletions(-) diff --git a/web/core/components/cycles/delete-modal.tsx b/web/core/components/cycles/delete-modal.tsx index 01d530a2e9c..933350ab710 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"; @@ -53,12 +54,13 @@ export const CycleDeleteModal: React.FC = observer((props) => { }) .catch((errors) => { const isPermissionError = errors?.error === "Only admin or owner can delete the cycle"; - const toastTitle = isPermissionError ? "You don't have permission to perform this action." : "Error"; - const toastMessage = isPermissionError ? undefined : "Failed to delete cycle"; + const currentError = isPermissionError + ? PROJECT_ERROR_MESSAGES.permissionError + : PROJECT_ERROR_MESSAGES.cycleDeleteError; setToast({ - title: toastTitle, + title: currentError.title, type: TOAST_TYPE.ERROR, - message: toastMessage, + message: currentError.message, }); captureCycleEvent({ eventName: CYCLE_DELETED, diff --git a/web/core/components/inbox/modals/delete-issue-modal.tsx b/web/core/components/inbox/modals/delete-issue-modal.tsx index 8d8ffa8fb71..0e98dad23ba 100644 --- a/web/core/components/inbox/modals/delete-issue-modal.tsx +++ b/web/core/components/inbox/modals/delete-issue-modal.tsx @@ -4,6 +4,8 @@ import { observer } from "mobx-react"; import type { TIssue } from "@plane/types"; // ui import { AlertModalCore, setToast, TOAST_TYPE } from "@plane/ui"; +// constants +import { PROJECT_ERROR_MESSAGES } from "@/constants/project"; // hooks import { useProject } from "@/hooks/store"; @@ -39,12 +41,13 @@ export const DeleteInboxIssueModal: React.FC = observer(({ isOpen, onClos }) .catch((errors) => { const isPermissionError = errors?.error === "Only admin or creator can delete the issue"; - const toastTitle = isPermissionError ? "You don't have permission to perform this action." : "Error"; - const toastMessage = isPermissionError ? undefined : "Failed to delete issue"; + const currentError = isPermissionError + ? PROJECT_ERROR_MESSAGES.permissionError + : PROJECT_ERROR_MESSAGES.issueDeleteError; setToast({ - title: toastTitle, + title: currentError.title, type: TOAST_TYPE.ERROR, - message: toastMessage, + message: currentError.message, }); }) .finally(() => handleClose()); diff --git a/web/core/components/issues/delete-issue-modal.tsx b/web/core/components/issues/delete-issue-modal.tsx index 4645301405e..c9232417997 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"; @@ -54,12 +56,13 @@ export const DeleteIssueModal: React.FC = (props) => { }) .catch((errors) => { const isPermissionError = errors?.error === "Only admin or creator can delete the issue"; - const toastTitle = isPermissionError ? "You don't have permission to perform this action." : "Error"; - const toastMessage = isPermissionError ? undefined : "Failed to delete issue"; + const currentError = isPermissionError + ? PROJECT_ERROR_MESSAGES.permissionError + : PROJECT_ERROR_MESSAGES.issueDeleteError; setToast({ - title: toastTitle, + title: currentError.title, type: TOAST_TYPE.ERROR, - message: toastMessage, + message: currentError.message, }); }) .finally(() => onClose()); diff --git a/web/core/components/modules/delete-module-modal.tsx b/web/core/components/modules/delete-module-modal.tsx index bd9e2d6a9d1..59959930610 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"; @@ -56,12 +57,13 @@ export const DeleteModuleModal: React.FC = observer((props) => { }) .catch((errors) => { const isPermissionError = errors?.error === "Only admin or creator can delete the module"; - const toastTitle = isPermissionError ? "You don't have permission to perform this action." : "Error!"; - const toastMessage = isPermissionError ? undefined : "Module could not be deleted. Please try again."; + const currentError = isPermissionError + ? PROJECT_ERROR_MESSAGES.permissionError + : PROJECT_ERROR_MESSAGES.moduleDeleteError; setToast({ - title: toastTitle, + title: currentError.title, type: TOAST_TYPE.ERROR, - message: toastMessage, + message: currentError.message, }); captureModuleEvent({ eventName: MODULE_DELETED, diff --git a/web/core/constants/project.ts b/web/core/constants/project.ts index d48d8d1463a..0737e77d0d3 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", + }, +};