From 5a4f18c9178adaeecaf683f7f3e6ff7913748d4f Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Tue, 12 Mar 2024 19:15:39 +0530 Subject: [PATCH 1/4] chore: show authorized projects in the dropdown --- web/components/cycles/form.tsx | 4 +++ web/components/dropdowns/project.tsx | 46 ++++++++++++++++------------ 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/web/components/cycles/form.tsx b/web/components/cycles/form.tsx index 4e2f55ef986..014152fd165 100644 --- a/web/components/cycles/form.tsx +++ b/web/components/cycles/form.tsx @@ -8,6 +8,7 @@ import { DateRangeDropdown, ProjectDropdown } from "components/dropdowns"; import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // types import { ICycle } from "@plane/types"; +import { EUserProjectRoles } from "constants/project"; type Props = { handleFormSubmit: (values: Partial, dirtyFields: any) => Promise; @@ -66,6 +67,9 @@ export const CycleForm: React.FC = (props) => { setActiveProject(val); }} buttonVariant="background-with-text" + renderCondition={(project) => + !!project.member_role && project.member_role >= EUserProjectRoles.MEMBER + } tabIndex={7} /> )} diff --git a/web/components/dropdowns/project.tsx b/web/components/dropdowns/project.tsx index 719b898029e..ce490583a11 100644 --- a/web/components/dropdowns/project.tsx +++ b/web/components/dropdowns/project.tsx @@ -15,6 +15,7 @@ import { ProjectLogo } from "components/project"; // types import { BUTTON_VARIANTS_WITH_TEXT } from "./constants"; import { TDropdownProps } from "./types"; +import { IProject } from "@plane/types"; // constants type Props = TDropdownProps & { @@ -23,6 +24,7 @@ type Props = TDropdownProps & { dropdownArrowClassName?: string; onChange: (val: string) => void; onClose?: () => void; + renderCondition?: (project: IProject) => boolean; value: string | null; }; @@ -41,6 +43,7 @@ export const ProjectDropdown: React.FC = observer((props) => { onClose, placeholder = "Project", placement, + renderCondition, showTooltip = false, tabIndex, value, @@ -71,7 +74,7 @@ export const ProjectDropdown: React.FC = observer((props) => { const options = joinedProjectIds?.map((projectId) => { const projectDetails = getProjectById(projectId); - + if (renderCondition && projectDetails && !renderCondition(projectDetails)) return; return { value: projectId, query: `${projectDetails?.name}`, @@ -89,7 +92,7 @@ export const ProjectDropdown: React.FC = observer((props) => { }); const filteredOptions = - query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); + query === "" ? options : options?.filter((o) => o?.query.toLowerCase().includes(query.toLowerCase())); const selectedProject = value ? getProjectById(value) : null; @@ -205,24 +208,27 @@ export const ProjectDropdown: React.FC = observer((props) => {
{filteredOptions ? ( filteredOptions.length > 0 ? ( - filteredOptions.map((option) => ( - - `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ - active ? "bg-custom-background-80" : "" - } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` - } - > - {({ selected }) => ( - <> - {option.content} - {selected && } - - )} - - )) + filteredOptions.map((option) => { + if (!option) return; + return ( + + `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ + active ? "bg-custom-background-80" : "" + } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && } + + )} + + ); + }) ) : (

No matching results

) From 7927994d727a39fafed64b157d48e7d3b86fb83a Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Wed, 13 Mar 2024 12:20:35 +0530 Subject: [PATCH 2/4] refactor: command palette logic --- .../command-palette/command-palette.tsx | 184 +++++++++++++----- web/components/issues/issue-modal/form.tsx | 5 +- web/components/modules/form.tsx | 4 + .../project/create-project-modal.tsx | 35 +--- 4 files changed, 145 insertions(+), 83 deletions(-) diff --git a/web/components/command-palette/command-palette.tsx b/web/components/command-palette/command-palette.tsx index ab2743afdee..6a1f20044a1 100644 --- a/web/components/command-palette/command-palette.tsx +++ b/web/components/command-palette/command-palette.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, FC } from "react"; +import React, { useCallback, useEffect, FC, useMemo } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import useSWR from "swr"; @@ -23,24 +23,29 @@ import { EIssuesStoreType } from "constants/issue"; import { copyTextToClipboard } from "helpers/string.helper"; import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store"; import { IssueService } from "services/issue"; +import { EUserProjectRoles } from "constants/project"; +import { EUserWorkspaceRoles } from "constants/workspace"; // services const issueService = new IssueService(); export const CommandPalette: FC = observer(() => { + // router const router = useRouter(); const { workspaceSlug, projectId, issueId, cycleId, moduleId } = router.query; - + // store hooks const { commandPalette, theme: { toggleSidebar }, } = useApplication(); const { setTrackElement } = useEventTracker(); - const { currentUser } = useUser(); + const { + currentUser, + membership: { currentWorkspaceRole, currentProjectRole }, + } = useUser(); const { issues: { removeIssue }, } = useIssues(EIssuesStoreType.PROJECT); - const { toggleCommandPaletteModal, isCreateIssueModalOpen, @@ -91,6 +96,105 @@ export const CommandPalette: FC = observer(() => { }); }, [issueId]); + // auth + const canPerformProjectCreateActions = useCallback( + (showToast: boolean = true) => { + const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + if (!isAllowed && showToast) + setToast({ + type: TOAST_TYPE.ERROR, + title: "You don't have permission to perform this action.", + }); + + return isAllowed; + }, + [currentProjectRole] + ); + const canPerformWorkspaceCreateActions = useCallback( + (showToast: boolean = true) => { + const isAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; + console.log("currentWorkspaceRole", currentWorkspaceRole); + console.log("isAllowed", isAllowed); + if (!isAllowed && showToast) + setToast({ + type: TOAST_TYPE.ERROR, + title: "You don't have permission to perform this action.", + }); + return isAllowed; + }, + [currentWorkspaceRole] + ); + + const shortcutsList: { + global: Record void }>; + workspace: Record void }>; + project: Record void }>; + } = useMemo( + () => ({ + global: { + c: { + title: "Create a new issue", + description: "Create a new issue in the current project", + action: () => toggleCreateIssueModal(true), + }, + h: { + title: "Show shortcuts", + description: "Show all the available shortcuts", + action: () => toggleShortcutModal(true), + }, + }, + workspace: { + p: { + title: "Create a new project", + description: "Create a new project in the current workspace", + action: () => toggleCreateProjectModal(true), + }, + }, + project: { + d: { + title: "Create a new page", + description: "Create a new page in the current project", + action: () => toggleCreatePageModal(true), + }, + m: { + title: "Create a new module", + description: "Create a new module in the current project", + action: () => toggleCreateModuleModal(true), + }, + q: { + title: "Create a new cycle", + description: "Create a new cycle in the current project", + action: () => toggleCreateCycleModal(true), + }, + v: { + title: "Create a new view", + description: "Create a new view in the current project", + action: () => toggleCreateViewModal(true), + }, + backspace: { + title: "Bulk delete issues", + description: "Bulk delete issues in the current project", + action: () => toggleBulkDeleteIssueModal(true), + }, + delete: { + title: "Bulk delete issues", + description: "Bulk delete issues in the current project", + action: () => toggleBulkDeleteIssueModal(true), + }, + }, + }), + [ + toggleBulkDeleteIssueModal, + toggleCreateCycleModal, + toggleCreateIssueModal, + toggleCreateModuleModal, + toggleCreatePageModal, + toggleCreateProjectModal, + toggleCreateViewModal, + toggleShortcutModal, + ] + ); + const handleKeyDown = useCallback( (e: KeyboardEvent) => { const { key, ctrlKey, metaKey, altKey } = e; @@ -102,7 +206,7 @@ export const CommandPalette: FC = observer(() => { if ( e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLInputElement || - (e.target as Element).classList?.contains("ProseMirror") + (e.target as Element)?.classList?.contains("ProseMirror") ) return; @@ -119,42 +223,35 @@ export const CommandPalette: FC = observer(() => { } } else if (!isAnyModalOpen) { setTrackElement("Shortcut key"); - if (keyPressed === "c") { - toggleCreateIssueModal(true); - } else if (keyPressed === "p") { - toggleCreateProjectModal(true); - } else if (keyPressed === "h") { - toggleShortcutModal(true); - } else if (keyPressed === "v" && workspaceSlug && projectId) { - toggleCreateViewModal(true); - } else if (keyPressed === "d" && workspaceSlug && projectId) { - toggleCreatePageModal(true); - } else if (keyPressed === "q" && workspaceSlug && projectId) { - toggleCreateCycleModal(true); - } else if (keyPressed === "m" && workspaceSlug && projectId) { - toggleCreateModuleModal(true); - } else if (keyPressed === "backspace" || keyPressed === "delete") { - e.preventDefault(); - toggleBulkDeleteIssueModal(true); - } + if (Object.keys(shortcutsList.global).includes(keyPressed)) shortcutsList.global[keyPressed].action(); + // workspace authorized actions + else if ( + Object.keys(shortcutsList.workspace).includes(keyPressed) && + workspaceSlug && + canPerformWorkspaceCreateActions() + ) + shortcutsList.workspace[keyPressed].action(); + // project authorized actions + else if ( + Object.keys(shortcutsList.project).includes(keyPressed) && + projectId && + canPerformProjectCreateActions() + ) + // actions that can be performed only inside a project + shortcutsList.project[keyPressed].action(); } }, [ + canPerformProjectCreateActions, + canPerformWorkspaceCreateActions, copyIssueUrlToClipboard, - toggleCreateProjectModal, - toggleCreateViewModal, - toggleCreatePageModal, - toggleShortcutModal, - toggleCreateCycleModal, - toggleCreateModuleModal, - toggleBulkDeleteIssueModal, + isAnyModalOpen, + projectId, + setTrackElement, + shortcutsList, toggleCommandPaletteModal, toggleSidebar, - toggleCreateIssueModal, - projectId, workspaceSlug, - isAnyModalOpen, - setTrackElement, ] ); @@ -169,18 +266,11 @@ export const CommandPalette: FC = observer(() => { return ( <> - { - toggleShortcutModal(false); - }} - /> + toggleShortcutModal(false)} /> {workspaceSlug && ( { - toggleCreateProjectModal(false); - }} + onClose={() => toggleCreateProjectModal(false)} workspaceSlug={workspaceSlug.toString()} /> )} @@ -194,9 +284,7 @@ export const CommandPalette: FC = observer(() => { /> { - toggleCreateModuleModal(false); - }} + onClose={() => toggleCreateModuleModal(false)} workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} /> @@ -236,9 +324,7 @@ export const CommandPalette: FC = observer(() => { { - toggleBulkDeleteIssueModal(false); - }} + onClose={() => toggleBulkDeleteIssueModal(false)} user={currentUser} /> diff --git a/web/components/issues/issue-modal/form.tsx b/web/components/issues/issue-modal/form.tsx index f585fae5588..6fd57c615ef 100644 --- a/web/components/issues/issue-modal/form.tsx +++ b/web/components/issues/issue-modal/form.tsx @@ -32,6 +32,7 @@ import { FileService } from "services/file.service"; import { getChangedIssuefields } from "helpers/issue.helper"; // types import type { TIssue, ISearchIssueResponse } from "@plane/types"; +import { EUserProjectRoles } from "constants/project"; const defaultValues: Partial = { project_id: "", @@ -304,7 +305,9 @@ export const IssueFormRoot: FC = observer((props) => { handleFormChange(); }} buttonVariant="border-with-text" - // TODO: update tabIndex logic + renderCondition={(project) => + !!project.member_role && project.member_role >= EUserProjectRoles.MEMBER + } tabIndex={getTabIndex("project_id")} />
diff --git a/web/components/modules/form.tsx b/web/components/modules/form.tsx index 4af097591ea..cebf4221fc9 100644 --- a/web/components/modules/form.tsx +++ b/web/components/modules/form.tsx @@ -9,6 +9,7 @@ import { ModuleStatusSelect } from "components/modules"; import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // types import { IModule } from "@plane/types"; +import { EUserProjectRoles } from "constants/project"; type Props = { handleFormSubmit: (values: Partial, dirtyFields: any) => Promise; @@ -78,6 +79,9 @@ export const ModuleForm: React.FC = (props) => { setActiveProject(val); }} buttonVariant="border-with-text" + renderCondition={(project) => + !!project.member_role && project.member_role >= EUserProjectRoles.MEMBER + } tabIndex={10} /> diff --git a/web/components/project/create-project-modal.tsx b/web/components/project/create-project-modal.tsx index 93f3d065a6d..5e8cf1895e7 100644 --- a/web/components/project/create-project-modal.tsx +++ b/web/components/project/create-project-modal.tsx @@ -1,15 +1,8 @@ import { useEffect, Fragment, FC, useState } from "react"; -import { observer } from "mobx-react-lite"; import { Dialog, Transition } from "@headlessui/react"; -// ui -import { setToast, TOAST_TYPE } from "@plane/ui"; // components import { CreateProjectForm } from "./create-project-form"; import { ProjectFeatureUpdate } from "./project-feature-update"; -// constants -import { EUserWorkspaceRoles } from "constants/workspace"; -// hooks -import { useUser } from "hooks/store"; type Props = { isOpen: boolean; @@ -23,32 +16,11 @@ enum EProjectCreationSteps { FEATURE_SELECTION = "FEATURE_SELECTION", } -interface IIsGuestCondition { - onClose: () => void; -} - -const IsGuestCondition: FC = ({ onClose }) => { - useEffect(() => { - onClose(); - setToast({ - title: "Error", - type: TOAST_TYPE.ERROR, - message: "You don't have permission to create project.", - }); - }, [onClose]); - - return null; -}; - -export const CreateProjectModal: FC = observer((props) => { +export const CreateProjectModal: FC = (props) => { const { isOpen, onClose, setToFavorite = false, workspaceSlug } = props; // states const [currentStep, setCurrentStep] = useState(EProjectCreationSteps.CREATE_PROJECT); const [createdProjectId, setCreatedProjectId] = useState(null); - // hooks - const { - membership: { currentWorkspaceRole }, - } = useUser(); useEffect(() => { if (isOpen) { @@ -57,9 +29,6 @@ export const CreateProjectModal: FC = observer((props) => { } }, [isOpen]); - if (currentWorkspaceRole && isOpen) - if (currentWorkspaceRole < EUserWorkspaceRoles.MEMBER) return ; - const handleNextStep = (projectId: string) => { if (!projectId) return; setCreatedProjectId(projectId); @@ -111,4 +80,4 @@ export const CreateProjectModal: FC = observer((props) => { ); -}); +}; From 02d85171715527dc4b9974fdb5e2e98a542f1cdd Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Fri, 15 Mar 2024 14:22:33 +0530 Subject: [PATCH 3/4] chore: add helper function to check for project role --- web/components/cycles/form.tsx | 6 ++---- web/components/issues/issue-modal/form.tsx | 6 ++---- web/components/modules/form.tsx | 6 ++---- web/helpers/project.helper.ts | 11 +++++++++++ 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/web/components/cycles/form.tsx b/web/components/cycles/form.tsx index 014152fd165..d470b1bb938 100644 --- a/web/components/cycles/form.tsx +++ b/web/components/cycles/form.tsx @@ -6,9 +6,9 @@ import { DateRangeDropdown, ProjectDropdown } from "components/dropdowns"; // ui // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { shouldRenderProject } from "helpers/project.helper"; // types import { ICycle } from "@plane/types"; -import { EUserProjectRoles } from "constants/project"; type Props = { handleFormSubmit: (values: Partial, dirtyFields: any) => Promise; @@ -67,9 +67,7 @@ export const CycleForm: React.FC = (props) => { setActiveProject(val); }} buttonVariant="background-with-text" - renderCondition={(project) => - !!project.member_role && project.member_role >= EUserProjectRoles.MEMBER - } + renderCondition={(project) => shouldRenderProject(project)} tabIndex={7} /> )} diff --git a/web/components/issues/issue-modal/form.tsx b/web/components/issues/issue-modal/form.tsx index 6fd57c615ef..7e38acc1cd1 100644 --- a/web/components/issues/issue-modal/form.tsx +++ b/web/components/issues/issue-modal/form.tsx @@ -30,9 +30,9 @@ import { FileService } from "services/file.service"; // ui // helpers import { getChangedIssuefields } from "helpers/issue.helper"; +import { shouldRenderProject } from "helpers/project.helper"; // types import type { TIssue, ISearchIssueResponse } from "@plane/types"; -import { EUserProjectRoles } from "constants/project"; const defaultValues: Partial = { project_id: "", @@ -305,9 +305,7 @@ export const IssueFormRoot: FC = observer((props) => { handleFormChange(); }} buttonVariant="border-with-text" - renderCondition={(project) => - !!project.member_role && project.member_role >= EUserProjectRoles.MEMBER - } + renderCondition={(project) => shouldRenderProject(project)} tabIndex={getTabIndex("project_id")} /> diff --git a/web/components/modules/form.tsx b/web/components/modules/form.tsx index cebf4221fc9..8fa657b991a 100644 --- a/web/components/modules/form.tsx +++ b/web/components/modules/form.tsx @@ -7,9 +7,9 @@ import { ModuleStatusSelect } from "components/modules"; // ui // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { shouldRenderProject } from "helpers/project.helper"; // types import { IModule } from "@plane/types"; -import { EUserProjectRoles } from "constants/project"; type Props = { handleFormSubmit: (values: Partial, dirtyFields: any) => Promise; @@ -79,9 +79,7 @@ export const ModuleForm: React.FC = (props) => { setActiveProject(val); }} buttonVariant="border-with-text" - renderCondition={(project) => - !!project.member_role && project.member_role >= EUserProjectRoles.MEMBER - } + renderCondition={(project) => shouldRenderProject(project)} tabIndex={10} /> diff --git a/web/helpers/project.helper.ts b/web/helpers/project.helper.ts index 441c14a42b4..9cadaae8ab7 100644 --- a/web/helpers/project.helper.ts +++ b/web/helpers/project.helper.ts @@ -1,4 +1,7 @@ +// types import { IProject } from "@plane/types"; +// constants +import { EUserProjectRoles } from "constants/project"; /** * Updates the sort order of the project. @@ -46,3 +49,11 @@ export const orderJoinedProjects = ( export const projectIdentifierSanitizer = (identifier: string): string => identifier.replace(/[^ÇŞĞIİÖÜA-Za-z0-9]/g, ""); + +/** + * @description Checks if the project should be rendered or not based on the user role + * @param {IProject} project + * @returns {boolean} + */ +export const shouldRenderProject = (project: IProject): boolean => + !!project.member_role && project.member_role >= EUserProjectRoles.MEMBER; From 6b822c975f88752561615fc57fcbd24f43cc0aea Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Fri, 15 Mar 2024 14:24:28 +0530 Subject: [PATCH 4/4] fix: add preventDefault for shortcuts --- web/components/command-palette/command-palette.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/components/command-palette/command-palette.tsx b/web/components/command-palette/command-palette.tsx index 6a1f20044a1..0d02614ae45 100644 --- a/web/components/command-palette/command-palette.tsx +++ b/web/components/command-palette/command-palette.tsx @@ -236,9 +236,11 @@ export const CommandPalette: FC = observer(() => { Object.keys(shortcutsList.project).includes(keyPressed) && projectId && canPerformProjectCreateActions() - ) + ) { + e.preventDefault(); // actions that can be performed only inside a project shortcutsList.project[keyPressed].action(); + } } }, [