From 37e09f35ec8558cc20f7e0dbb4853062374e45d6 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Mon, 16 Sep 2024 15:00:58 +0530 Subject: [PATCH 01/12] fix: workspace menu quick action --- .../workspace/sidebar/workspace-menu.tsx | 92 ++++++++++--------- .../layouts/auth-layout/project-wrapper.tsx | 2 +- 2 files changed, 50 insertions(+), 44 deletions(-) diff --git a/web/core/components/workspace/sidebar/workspace-menu.tsx b/web/core/components/workspace/sidebar/workspace-menu.tsx index 62cf672cfef..ff80f4d94df 100644 --- a/web/core/components/workspace/sidebar/workspace-menu.tsx +++ b/web/core/components/workspace/sidebar/workspace-menu.tsx @@ -23,7 +23,7 @@ import useLocalStorage from "@/hooks/use-local-storage"; import { usePlatformOS } from "@/hooks/use-platform-os"; // plane web components import { UpgradeBadge } from "@/plane-web/components/workspace"; -import { EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; export const SidebarWorkspaceMenu = observer(() => { // state @@ -43,6 +43,7 @@ export const SidebarWorkspaceMenu = observer(() => { const { setValue: toggleWorkspaceMenu, storedValue } = useLocalStorage("is_workspace_menu_open", true); // derived values const isWorkspaceMenuOpen = !!storedValue; + const isAdmin = allowPermissions([EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.WORKSPACE); const handleLinkClick = (itemKey: string) => { if (window.innerWidth < 768) { @@ -67,11 +68,14 @@ export const SidebarWorkspaceMenu = observer(() => { return ( {!sidebarCollapsed && ( -
+
{" "} { > WORKSPACE - { - setIsMenuActive(!isMenuActive); - }} - > - - - } - className={cn( - "h-full flex items-center opacity-0 z-20 pointer-events-none flex-shrink-0 group-hover/workspace-button:opacity-100 group-hover/workspace-button:pointer-events-auto my-auto", - { - "opacity-100 pointer-events-auto": isMenuActive, + {isAdmin && ( + { + setIsMenuActive(!isMenuActive); + }} + > + + } - )} - customButtonClassName="grid place-items-center" - placement="bottom-start" - > - - -
- - Archives -
- -
+ className={cn( + "h-full flex items-center opacity-0 z-20 pointer-events-none flex-shrink-0 group-hover/workspace-button:opacity-100 group-hover/workspace-button:pointer-events-auto my-auto", + { + "opacity-100 pointer-events-auto": isMenuActive, + } + )} + customButtonClassName="grid place-items-center" + placement="bottom-start" + > + + +
+ + Archives +
+ +
- - -
- - Settings -
- -
-
+ + +
+ + Settings +
+ +
+
+ )} = observer((props) => { // router const { workspaceSlug, projectId } = useParams(); - const projectMemberInfo = projectUserInfo?.[workspaceSlug.toString()]?.[projectId.toString()]; + const projectMemberInfo = projectUserInfo?.[workspaceSlug?.toString()]?.[projectId?.toString()]; // fetching project details useSWR( From 7fccd746db9aed07f36fd598dc80e5547393c674 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Mon, 16 Sep 2024 15:33:14 +0530 Subject: [PATCH 02/12] fix: guest role upgrade flow validation --- web/core/components/project/settings/member-columns.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/core/components/project/settings/member-columns.tsx b/web/core/components/project/settings/member-columns.tsx index dae12532385..f6699d71188 100644 --- a/web/core/components/project/settings/member-columns.tsx +++ b/web/core/components/project/settings/member-columns.tsx @@ -98,7 +98,8 @@ export const AccountTypeColumn: React.FC = observer((props) => // derived values const isCurrentUser = currentUser?.id === rowData.member.id; const isAdminOrGuest = [EUserPermissions.ADMIN, EUserPermissions.GUEST].includes(rowData.role); - const isRoleNonEditable = isCurrentUser || isAdminOrGuest; + const userWorkspaceRole = getWorkspaceMemberDetails(rowData.member.id)?.role; + const isRoleNonEditable = isCurrentUser || (isAdminOrGuest && userWorkspaceRole !== EUserPermissions.MEMBER); const checkCurrentOptionWorkspaceRole = (value: string) => { const currentMemberWorkspaceRole = getWorkspaceMemberDetails(value)?.role as EUserPermissions | undefined; From 5cbff5516e0483a62768c2deb2b7faeb3f328268 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Mon, 16 Sep 2024 15:45:16 +0530 Subject: [PATCH 03/12] fix: create issue validation --- .../components/workspace/sidebar/quick-actions.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/web/core/components/workspace/sidebar/quick-actions.tsx b/web/core/components/workspace/sidebar/quick-actions.tsx index f0eca5cf091..8931639b6bc 100644 --- a/web/core/components/workspace/sidebar/quick-actions.tsx +++ b/web/core/components/workspace/sidebar/quick-actions.tsx @@ -10,8 +10,9 @@ import { CreateUpdateIssueModal } from "@/components/issues"; // helpers import { cn } from "@/helpers/common.helper"; // hooks -import { useAppTheme, useCommandPalette, useEventTracker, useProject } from "@/hooks/store"; +import { useAppTheme, useCommandPalette, useEventTracker, useProject, useUserPermissions } from "@/hooks/store"; import useLocalStorage from "@/hooks/use-local-storage"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; export const SidebarQuickActions = observer(() => { // states @@ -28,11 +29,16 @@ export const SidebarQuickActions = observer(() => { const { sidebarCollapsed: isSidebarCollapsed } = useAppTheme(); const { setTrackElement } = useEventTracker(); const { joinedProjectIds } = useProject(); + const { allowPermissions } = useUserPermissions(); // local storage const { storedValue, setValue } = useLocalStorage>>("draftedIssue", {}); // derived values - const disabled = joinedProjectIds.length === 0; - const workspaceDraftIssue = workspaceSlug ? storedValue?.[workspaceSlug] ?? undefined : undefined; + const canCreateIssue = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); + const disabled = joinedProjectIds.length === 0 || !canCreateIssue; + const workspaceDraftIssue = workspaceSlug ? (storedValue?.[workspaceSlug] ?? undefined) : undefined; const handleMouseEnter = () => { // if enter before time out clear the timeout From 21c168778796c4f4ca1764b4787247a9df26cd64 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Mon, 16 Sep 2024 15:46:03 +0530 Subject: [PATCH 04/12] fix: create issue validation --- web/core/components/workspace/sidebar/quick-actions.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/core/components/workspace/sidebar/quick-actions.tsx b/web/core/components/workspace/sidebar/quick-actions.tsx index 8931639b6bc..e416c537ece 100644 --- a/web/core/components/workspace/sidebar/quick-actions.tsx +++ b/web/core/components/workspace/sidebar/quick-actions.tsx @@ -36,7 +36,7 @@ export const SidebarQuickActions = observer(() => { const canCreateIssue = allowPermissions( [EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.WORKSPACE - ); + ); const disabled = joinedProjectIds.length === 0 || !canCreateIssue; const workspaceDraftIssue = workspaceSlug ? (storedValue?.[workspaceSlug] ?? undefined) : undefined; From 471f438c0e347741673380cbca982d29efe5be06 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Mon, 16 Sep 2024 16:02:13 +0530 Subject: [PATCH 05/12] fix: cmd k permission validation --- .../command-palette/command-modal.tsx | 49 +++++++++++-------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/web/core/components/command-palette/command-modal.tsx b/web/core/components/command-palette/command-modal.tsx index d0e5010e8eb..8e324ee9d2b 100644 --- a/web/core/components/command-palette/command-modal.tsx +++ b/web/core/components/command-palette/command-modal.tsx @@ -31,7 +31,7 @@ import { ISSUE_DETAILS } from "@/constants/fetch-keys"; // helpers import { getTabIndex } from "@/helpers/tab-indices.helper"; // hooks -import { useCommandPalette, useEventTracker, useProject, useUser } from "@/hooks/store"; +import { useCommandPalette, useEventTracker, useProject, useUser, useUserPermissions } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; import useDebounce from "@/hooks/use-debounce"; import { usePlatformOS } from "@/hooks/use-platform-os"; @@ -41,6 +41,7 @@ import { IssueIdentifier } from "@/plane-web/components/issues"; import { WorkspaceService } from "@/plane-web/services"; // services import { IssueService } from "@/services/issue"; +import { EUserPermissions, EUserPermissionsLevel } from "ee/constants/user-permissions"; const workspaceService = new WorkspaceService(); const issueService = new IssueService(); @@ -71,6 +72,7 @@ export const CommandModal: React.FC = observer(() => { const [pages, setPages] = useState([]); const { isCommandPaletteOpen, toggleCommandPaletteModal, toggleCreateIssueModal, toggleCreateProjectModal } = useCommandPalette(); + const { allowPermissions } = useUserPermissions(); const { setTrackElement } = useEventTracker(); // router @@ -84,6 +86,11 @@ export const CommandModal: React.FC = observer(() => { const { baseTabIndex } = getTabIndex(undefined, isMobile); + const canPerformWorkspaceAction = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); + // TODO: update this to mobx store const { data: issueDetails } = useSWR( workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId.toString()) : null, @@ -314,8 +321,7 @@ export const CommandModal: React.FC = observer(() => { )} - - {workspaceSlug && ( + {workspaceSlug && canPerformWorkspaceAction && ( { @@ -335,23 +341,26 @@ export const CommandModal: React.FC = observer(() => { )} {/* project actions */} - {projectId && } - - - { - setPlaceholder("Search workspace settings..."); - setSearchTerm(""); - setPages([...pages, "settings"]); - }} - className="focus:outline-none" - > -
- - Search settings... -
-
-
+ {projectId && canPerformAnyCreateAction && ( + + )} + {canPerformWorkspaceAction && ( + + { + setPlaceholder("Search workspace settings..."); + setSearchTerm(""); + setPages([...pages, "settings"]); + }} + className="focus:outline-none" + > +
+ + Search settings... +
+
+
+ )}
From ec8b919d27b6be7c6f3730c93c0aef2857037561 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Mon, 16 Sep 2024 16:21:52 +0530 Subject: [PATCH 06/12] fix: subscription validation --- .../components/issues/issue-detail/subscription.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/web/core/components/issues/issue-detail/subscription.tsx b/web/core/components/issues/issue-detail/subscription.tsx index f4e799e7d7e..bc04a643230 100644 --- a/web/core/components/issues/issue-detail/subscription.tsx +++ b/web/core/components/issues/issue-detail/subscription.tsx @@ -7,7 +7,8 @@ import { Bell, BellOff } from "lucide-react"; // UI import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui"; // hooks -import { useIssueDetail } from "@/hooks/store"; +import { useIssueDetail, useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; export type TIssueSubscription = { workspaceSlug: string; @@ -25,8 +26,16 @@ export const IssueSubscription: FC = observer((props) => { } = useIssueDetail(); // state const [loading, setLoading] = useState(false); + // hooks + const { allowPermissions } = useUserPermissions(); const isSubscribed = getSubscriptionByIssueId(issueId); + const isEditable = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT, + workspaceSlug, + projectId + ); const handleSubscription = async () => { setLoading(true); @@ -64,6 +73,7 @@ export const IssueSubscription: FC = observer((props) => { variant="outline-primary" className="hover:!bg-custom-primary-100/20" onClick={handleSubscription} + disabled={!isEditable} > {loading ? ( From 2211440dbe5740e7c159aa45bc443bbe6725a676 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Mon, 16 Sep 2024 16:32:39 +0530 Subject: [PATCH 07/12] fix: create label permission validation --- web/core/components/issues/issue-detail/label/root.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/web/core/components/issues/issue-detail/label/root.tsx b/web/core/components/issues/issue-detail/label/root.tsx index afff6c0f6a0..464a4b6c262 100644 --- a/web/core/components/issues/issue-detail/label/root.tsx +++ b/web/core/components/issues/issue-detail/label/root.tsx @@ -6,10 +6,11 @@ import { IIssueLabel, TIssue } from "@plane/types"; // components import { TOAST_TYPE, setToast } from "@plane/ui"; // hooks -import { useIssueDetail, useLabel, useProjectInbox } from "@/hooks/store"; +import { useIssueDetail, useLabel, useProjectInbox, useUserPermissions } from "@/hooks/store"; // ui // types import { LabelList, LabelCreate, IssueLabelSelectRoot } from "./"; +import { EUserPermissions, EUserPermissionsLevel } from "ee/constants/user-permissions"; export type TIssueLabel = { workspaceSlug: string; @@ -34,7 +35,9 @@ export const IssueLabel: FC = observer((props) => { issue: { getIssueById }, } = useIssueDetail(); const { getIssueInboxByIssueId } = useProjectInbox(); + const { allowPermissions } = useUserPermissions(); + const canCreateLabel = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT); const issue = isInboxIssue ? getIssueInboxByIssueId(issueId)?.issue : getIssueById(issueId); const labelOperations: TLabelOperations = useMemo( @@ -99,7 +102,7 @@ export const IssueLabel: FC = observer((props) => { /> )} - {!disabled && ( + {!disabled && canCreateLabel && ( Date: Mon, 16 Sep 2024 16:38:43 +0530 Subject: [PATCH 08/12] fix: build error --- web/app/[workspaceSlug]/(projects)/settings/layout.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/app/[workspaceSlug]/(projects)/settings/layout.tsx b/web/app/[workspaceSlug]/(projects)/settings/layout.tsx index 6e947c4877b..5e1f68c7bf9 100644 --- a/web/app/[workspaceSlug]/(projects)/settings/layout.tsx +++ b/web/app/[workspaceSlug]/(projects)/settings/layout.tsx @@ -3,14 +3,14 @@ import { FC, ReactNode } from "react"; import { observer } from "mobx-react"; // components +import { NotAuthorizedView } from "@/components/auth-screens"; import { AppHeader } from "@/components/core"; +import { useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; // local components import { WorkspaceSettingHeader } from "./header"; import { MobileWorkspaceSettingsTabs } from "./mobile-header-tabs"; import { WorkspaceSettingsSidebar } from "./sidebar"; -import { NotAuthorizedView } from "@/components/auth-screens"; -import { useUserPermissions } from "@/hooks/store"; -import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; export interface IWorkspaceSettingLayout { children: ReactNode; From ab5e80e062262468e3a01bad0b6768f46336d76a Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Mon, 16 Sep 2024 17:12:50 +0530 Subject: [PATCH 09/12] chore: guest can comment in their created issues --- apiserver/plane/app/views/issue/comment.py | 24 ++++++++++++++++++++- apiserver/plane/app/views/issue/reaction.py | 7 +++--- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/apiserver/plane/app/views/issue/comment.py b/apiserver/plane/app/views/issue/comment.py index 8bd7daed367..f6d43d2097a 100644 --- a/apiserver/plane/app/views/issue/comment.py +++ b/apiserver/plane/app/views/issue/comment.py @@ -21,6 +21,8 @@ IssueComment, ProjectMember, CommentReaction, + Project, + Issue, ) from plane.bgtasks.issue_activities_task import issue_activity @@ -67,9 +69,27 @@ def get_queryset(self): [ ROLE.ADMIN, ROLE.MEMBER, + ROLE.GUEST, ] ) def create(self, request, slug, project_id, issue_id): + project = Project.objects.filter(pk=project_id) + issue = Issue.objects.filter(pk=issue_id) + if ( + ProjectMember.objects.filter( + workspace__slug=slug, + project_id=project_id, + member=request.user, + role=5, + is_active=True, + ).exists() + and not project.guest_view_all_features + and not issue.created_by == request.user + ): + return Response( + {"error": "You are not allowed to comment on the issue"}, + status=status.HTTP_400_BAD_REQUEST, + ) serializer = IssueCommentSerializer(data=request.data) if serializer.is_valid(): serializer.save( @@ -94,7 +114,7 @@ def create(self, request, slug, project_id, issue_id): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @allow_permission( - allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], + allowed_roles=[ROLE.ADMIN], creator=True, model=IssueComment, ) @@ -182,6 +202,7 @@ def get_queryset(self): [ ROLE.ADMIN, ROLE.MEMBER, + ROLE.GUEST, ] ) def create(self, request, slug, project_id, comment_id): @@ -210,6 +231,7 @@ def create(self, request, slug, project_id, comment_id): [ ROLE.ADMIN, ROLE.MEMBER, + ROLE.GUEST, ] ) def destroy(self, request, slug, project_id, comment_id, reaction_code): diff --git a/apiserver/plane/app/views/issue/reaction.py b/apiserver/plane/app/views/issue/reaction.py index 655771013ce..5146118f302 100644 --- a/apiserver/plane/app/views/issue/reaction.py +++ b/apiserver/plane/app/views/issue/reaction.py @@ -12,7 +12,7 @@ # Module imports from .. import BaseViewSet from plane.app.serializers import IssueReactionSerializer -from plane.app.permissions import ProjectLitePermission +from plane.app.permissions import allow_permission, ROLE from plane.db.models import IssueReaction from plane.bgtasks.issue_activities_task import issue_activity @@ -20,9 +20,6 @@ class IssueReactionViewSet(BaseViewSet): serializer_class = IssueReactionSerializer model = IssueReaction - permission_classes = [ - ProjectLitePermission, - ] def get_queryset(self): return ( @@ -40,6 +37,7 @@ def get_queryset(self): .distinct() ) + @allow_permission(ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST) def create(self, request, slug, project_id, issue_id): serializer = IssueReactionSerializer(data=request.data) if serializer.is_valid(): @@ -62,6 +60,7 @@ def create(self, request, slug, project_id, issue_id): return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + @allow_permission(ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST) def destroy(self, request, slug, project_id, issue_id, reaction_code): issue_reaction = IssueReaction.objects.get( workspace__slug=slug, From a4c1f7f99aa32ad2993d56cb0c13b5e90a0bf32a Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Mon, 16 Sep 2024 17:50:53 +0530 Subject: [PATCH 10/12] chore: changed the queryset --- apiserver/plane/app/views/issue/comment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/app/views/issue/comment.py b/apiserver/plane/app/views/issue/comment.py index f6d43d2097a..12964dc3a81 100644 --- a/apiserver/plane/app/views/issue/comment.py +++ b/apiserver/plane/app/views/issue/comment.py @@ -73,8 +73,8 @@ def get_queryset(self): ] ) def create(self, request, slug, project_id, issue_id): - project = Project.objects.filter(pk=project_id) - issue = Issue.objects.filter(pk=issue_id) + project = Project.objects.get(pk=project_id) + issue = Issue.objects.get(pk=issue_id) if ( ProjectMember.objects.filter( workspace__slug=slug, From 8adaa718ccc942be23d125b661b3ecdf47a31d64 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Mon, 16 Sep 2024 18:27:38 +0530 Subject: [PATCH 11/12] chore: code refactor --- web/core/components/command-palette/command-modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/core/components/command-palette/command-modal.tsx b/web/core/components/command-palette/command-modal.tsx index 8e324ee9d2b..1f753b98ab1 100644 --- a/web/core/components/command-palette/command-modal.tsx +++ b/web/core/components/command-palette/command-modal.tsx @@ -86,7 +86,7 @@ export const CommandModal: React.FC = observer(() => { const { baseTabIndex } = getTabIndex(undefined, isMobile); - const canPerformWorkspaceAction = allowPermissions( + const canPerformWorkspaceActions = allowPermissions( [EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.WORKSPACE ); From a857c9f709dc376ab8b9052ad9307ba11ea38820 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Mon, 16 Sep 2024 18:38:03 +0530 Subject: [PATCH 12/12] chore: code refactor --- web/core/components/command-palette/command-modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/core/components/command-palette/command-modal.tsx b/web/core/components/command-palette/command-modal.tsx index 1f753b98ab1..87c86477248 100644 --- a/web/core/components/command-palette/command-modal.tsx +++ b/web/core/components/command-palette/command-modal.tsx @@ -321,7 +321,7 @@ export const CommandModal: React.FC = observer(() => { )} - {workspaceSlug && canPerformWorkspaceAction && ( + {workspaceSlug && canPerformWorkspaceActions && ( {