diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py index d6bd13fa8..24eac569d 100644 --- a/apiserver/plane/api/views/inbox.py +++ b/apiserver/plane/api/views/inbox.py @@ -210,7 +210,7 @@ class InboxIssueAPIEndpoint(BaseAPIView): ) # Only project members admins and created_by users can access this endpoint - if project_member.role <= 10 and str(inbox_issue.created_by_id) != str( + if project_member.role <= 5 and str(inbox_issue.created_by_id) != str( request.user.id ): return Response( @@ -244,9 +244,8 @@ class InboxIssueAPIEndpoint(BaseAPIView): workspace__slug=slug, project_id=project_id, ) - # Only allow guests and viewers to edit name and description - if project_member.role <= 10: - # viewers and guests since only viewers and guests + # Only allow guests to edit name and description + if project_member.role <= 5: issue_data = { "name": issue_data.get("name", issue.name), "description_html": issue_data.get( @@ -286,7 +285,7 @@ class InboxIssueAPIEndpoint(BaseAPIView): ) # Only project admins and members can edit inbox issue attributes - if project_member.role > 10: + if project_member.role > 5: serializer = InboxIssueSerializer( inbox_issue, data=request.data, partial=True ) diff --git a/apiserver/plane/api/views/member.py b/apiserver/plane/api/views/member.py index 08bbd9a4d..d6e5fed0c 100644 --- a/apiserver/plane/api/views/member.py +++ b/apiserver/plane/api/views/member.py @@ -133,7 +133,7 @@ class ProjectMemberAPIEndpoint(BaseAPIView): workspace_member = WorkspaceMember.objects.create( workspace=workspace, member=user, - role=request.data.get("role", 10), + role=request.data.get("role", 5), ) workspace_member.save() @@ -142,7 +142,7 @@ class ProjectMemberAPIEndpoint(BaseAPIView): project_member = ProjectMember.objects.create( project=project, member=user, - role=request.data.get("role", 10), + role=request.data.get("role", 5), ) project_member.save() diff --git a/apiserver/plane/app/permissions/base.py b/apiserver/plane/app/permissions/base.py index 300bc364f..06faeceb6 100644 --- a/apiserver/plane/app/permissions/base.py +++ b/apiserver/plane/app/permissions/base.py @@ -8,7 +8,6 @@ from enum import Enum class ROLE(Enum): ADMIN = 20 MEMBER = 15 - VIEWER = 10 GUEST = 5 diff --git a/apiserver/plane/app/permissions/project.py b/apiserver/plane/app/permissions/project.py index 25e5aaeb0..11eab008b 100644 --- a/apiserver/plane/app/permissions/project.py +++ b/apiserver/plane/app/permissions/project.py @@ -7,7 +7,6 @@ from plane.db.models import ProjectMember, WorkspaceMember # Permission Mappings Admin = 20 Member = 15 -Viewer = 10 Guest = 5 diff --git a/apiserver/plane/app/permissions/workspace.py b/apiserver/plane/app/permissions/workspace.py index f73ae1f67..c2dcc9f95 100644 --- a/apiserver/plane/app/permissions/workspace.py +++ b/apiserver/plane/app/permissions/workspace.py @@ -6,9 +6,8 @@ from plane.db.models import WorkspaceMember # Permission Mappings -Owner = 20 -Admin = 15 -Member = 10 +Admin = 20 +Member = 15 Guest = 5 @@ -31,7 +30,7 @@ class WorkSpaceBasePermission(BasePermission): return WorkspaceMember.objects.filter( member=request.user, workspace__slug=view.workspace_slug, - role__in=[Owner, Admin], + role__in=[Admin, Member], is_active=True, ).exists() @@ -40,7 +39,7 @@ class WorkSpaceBasePermission(BasePermission): return WorkspaceMember.objects.filter( member=request.user, workspace__slug=view.workspace_slug, - role=Owner, + role=Admin, is_active=True, ).exists() @@ -53,7 +52,7 @@ class WorkspaceOwnerPermission(BasePermission): return WorkspaceMember.objects.filter( workspace__slug=view.workspace_slug, member=request.user, - role=Owner, + role=Admin, ).exists() @@ -65,7 +64,7 @@ class WorkSpaceAdminPermission(BasePermission): return WorkspaceMember.objects.filter( member=request.user, workspace__slug=view.workspace_slug, - role__in=[Owner, Admin], + role__in=[Admin, Member], is_active=True, ).exists() @@ -86,7 +85,7 @@ class WorkspaceEntityPermission(BasePermission): return WorkspaceMember.objects.filter( member=request.user, workspace__slug=view.workspace_slug, - role__in=[Owner, Admin], + role__in=[Admin, Member], is_active=True, ).exists() diff --git a/apiserver/plane/app/views/analytic/base.py b/apiserver/plane/app/views/analytic/base.py index b72935fc2..65ba1469c 100644 --- a/apiserver/plane/app/views/analytic/base.py +++ b/apiserver/plane/app/views/analytic/base.py @@ -21,7 +21,11 @@ from plane.app.permissions import allow_permission, ROLE class AnalyticsEndpoint(BaseAPIView): @allow_permission( - [ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER], level="WORKSPACE" + [ + ROLE.ADMIN, + ROLE.MEMBER, + ], + level="WORKSPACE", ) def get(self, request, slug): x_axis = request.GET.get("x_axis", False) @@ -203,7 +207,11 @@ class AnalyticViewViewset(BaseViewSet): class SavedAnalyticEndpoint(BaseAPIView): @allow_permission( - [ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER], level="WORKSPACE" + [ + ROLE.ADMIN, + ROLE.MEMBER, + ], + level="WORKSPACE", ) def get(self, request, slug, analytic_id): analytic_view = AnalyticView.objects.get( @@ -236,7 +244,11 @@ class SavedAnalyticEndpoint(BaseAPIView): class ExportAnalyticsEndpoint(BaseAPIView): @allow_permission( - [ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER], level="WORKSPACE" + [ + ROLE.ADMIN, + ROLE.MEMBER, + ], + level="WORKSPACE", ) def post(self, request, slug): x_axis = request.data.get("x_axis", False) @@ -302,9 +314,7 @@ class ExportAnalyticsEndpoint(BaseAPIView): class DefaultAnalyticsEndpoint(BaseAPIView): - @allow_permission( - [ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST], level="WORKSPACE" - ) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") def get(self, request, slug): filters = issue_filters(request.GET, "GET") base_issues = Issue.issue_objects.filter( diff --git a/apiserver/plane/app/views/cycle/archive.py b/apiserver/plane/app/views/cycle/archive.py index 22f21f4bf..25ad8a2eb 100644 --- a/apiserver/plane/app/views/cycle/archive.py +++ b/apiserver/plane/app/views/cycle/archive.py @@ -288,7 +288,12 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView): .distinct() ) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER]) + @allow_permission( + [ + ROLE.ADMIN, + ROLE.MEMBER, + ] + ) def get(self, request, slug, project_id, pk=None): if pk is None: queryset = ( diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py index ceeb24560..fc04abe35 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -150,7 +150,7 @@ class CycleViewSet(BaseViewSet): .distinct() ) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def list(self, request, slug, project_id): queryset = self.get_queryset().filter(archived_at__isnull=True) cycle_view = request.GET.get("cycle_view", "all") @@ -370,7 +370,12 @@ class CycleViewSet(BaseViewSet): return Response(cycle, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER]) + @allow_permission( + [ + ROLE.ADMIN, + ROLE.MEMBER, + ] + ) def retrieve(self, request, slug, project_id, pk): queryset = ( self.get_queryset().filter(archived_at__isnull=True).filter(pk=pk) @@ -497,7 +502,6 @@ class CycleViewSet(BaseViewSet): class CycleDateCheckEndpoint(BaseAPIView): - @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def post(self, request, slug, project_id): start_date = request.data.get("start_date", False) @@ -566,7 +570,6 @@ class CycleFavoriteViewSet(BaseViewSet): class TransferCycleIssueEndpoint(BaseAPIView): - @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def post(self, request, slug, project_id, cycle_id): new_cycle_id = request.data.get("new_cycle_id", False) @@ -977,8 +980,7 @@ class TransferCycleIssueEndpoint(BaseAPIView): class CycleUserPropertiesEndpoint(BaseAPIView): - - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def patch(self, request, slug, project_id, cycle_id): cycle_properties = CycleUserProperties.objects.get( user=request.user, @@ -1001,7 +1003,7 @@ class CycleUserPropertiesEndpoint(BaseAPIView): serializer = CycleUserPropertiesSerializer(cycle_properties) return Response(serializer.data, status=status.HTTP_201_CREATED) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def get(self, request, slug, project_id, cycle_id): cycle_properties, _ = CycleUserProperties.objects.get_or_create( user=request.user, @@ -1014,10 +1016,8 @@ class CycleUserPropertiesEndpoint(BaseAPIView): class CycleProgressEndpoint(BaseAPIView): - - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def get(self, request, slug, project_id, cycle_id): - aggregate_estimates = ( Issue.issue_objects.filter( estimate_point__estimate__type="points", @@ -1148,10 +1148,9 @@ class CycleProgressEndpoint(BaseAPIView): status=status.HTTP_200_OK, ) - class CycleAnalyticsEndpoint(BaseAPIView): - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def get(self, request, slug, project_id, cycle_id): analytic_type = request.GET.get("type", "issues") cycle = ( diff --git a/apiserver/plane/app/views/cycle/issue.py b/apiserver/plane/app/views/cycle/issue.py index e8e9230c0..211f5a88a 100644 --- a/apiserver/plane/app/views/cycle/issue.py +++ b/apiserver/plane/app/views/cycle/issue.py @@ -80,7 +80,12 @@ class CycleIssueViewSet(BaseViewSet): ) @method_decorator(gzip_page) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER]) + @allow_permission( + [ + ROLE.ADMIN, + ROLE.MEMBER, + ] + ) def list(self, request, slug, project_id, cycle_id): order_by_param = request.GET.get("order_by", "created_at") filters = issue_filters(request.query_params, "GET") diff --git a/apiserver/plane/app/views/dashboard/base.py b/apiserver/plane/app/views/dashboard/base.py index b3e93a403..4a760ca3b 100644 --- a/apiserver/plane/app/views/dashboard/base.py +++ b/apiserver/plane/app/views/dashboard/base.py @@ -52,23 +52,28 @@ from .. import BaseAPIView def dashboard_overview_stats(self, request, slug): - extra_filters = {} - if WorkspaceMember.objects.filter( - workspace__slug=slug, - member=request.user, - role=5, - is_active=True, - ).exists(): - extra_filters = {"created_by": request.user} - assigned_issues = ( Issue.issue_objects.filter( project__project_projectmember__is_active=True, project__project_projectmember__member=request.user, workspace__slug=slug, assignees__in=[request.user], + ).filter( + Q( + project__project_projectmember__role=5, + project__guest_view_all_features=True, + ) + | Q( + project__project_projectmember__role=5, + project__guest_view_all_features=False, + created_by=self.request.user, + ) + | + # For other roles (role < 5), show all issues + Q(project__project_projectmember__role__gt=5), + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, ) - .filter(**extra_filters) .count() ) @@ -80,8 +85,22 @@ def dashboard_overview_stats(self, request, slug): project__project_projectmember__member=request.user, workspace__slug=slug, assignees__in=[request.user], + ).filter( + Q( + project__project_projectmember__role=5, + project__guest_view_all_features=True, + ) + | Q( + project__project_projectmember__role=5, + project__guest_view_all_features=False, + created_by=self.request.user, + ) + | + # For other roles (role < 5), show all issues + Q(project__project_projectmember__role__gt=5), + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, ) - .filter(**extra_filters) .count() ) @@ -91,8 +110,22 @@ def dashboard_overview_stats(self, request, slug): project__project_projectmember__is_active=True, project__project_projectmember__member=request.user, created_by_id=request.user.id, + ).filter( + Q( + project__project_projectmember__role=5, + project__guest_view_all_features=True, + ) + | Q( + project__project_projectmember__role=5, + project__guest_view_all_features=False, + created_by=self.request.user, + ) + | + # For other roles (role < 5), show all issues + Q(project__project_projectmember__role__gt=5), + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, ) - .filter(**extra_filters) .count() ) @@ -103,8 +136,22 @@ def dashboard_overview_stats(self, request, slug): project__project_projectmember__member=request.user, assignees__in=[request.user], state__group="completed", + ).filter( + Q( + project__project_projectmember__role=5, + project__guest_view_all_features=True, + ) + | Q( + project__project_projectmember__role=5, + project__guest_view_all_features=False, + created_by=self.request.user, + ) + | + # For other roles (role < 5), show all issues + Q(project__project_projectmember__role__gt=5), + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, ) - .filter(**extra_filters) .count() ) diff --git a/apiserver/plane/app/views/estimate/base.py b/apiserver/plane/app/views/estimate/base.py index cd80c6299..f38488001 100644 --- a/apiserver/plane/app/views/estimate/base.py +++ b/apiserver/plane/app/views/estimate/base.py @@ -28,7 +28,12 @@ def generate_random_name(length=10): class ProjectEstimatePointEndpoint(BaseAPIView): - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER]) + @allow_permission( + [ + ROLE.ADMIN, + ROLE.MEMBER, + ] + ) def get(self, request, slug, project_id): project = Project.objects.get(workspace__slug=slug, pk=project_id) if project.estimate_id is not None: diff --git a/apiserver/plane/app/views/inbox/base.py b/apiserver/plane/app/views/inbox/base.py index 08a6acbce..d830a622f 100644 --- a/apiserver/plane/app/views/inbox/base.py +++ b/apiserver/plane/app/views/inbox/base.py @@ -165,7 +165,7 @@ class InboxIssueViewSet(BaseViewSet): ) ).distinct() - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def list(self, request, slug, project_id): inbox_id = Inbox.objects.get( workspace__slug=slug, project_id=project_id @@ -338,7 +338,7 @@ class InboxIssueViewSet(BaseViewSet): is_active=True, ) # Only project members admins and created_by users can access this endpoint - if project_member.role <= 10 and str(inbox_issue.created_by_id) != str( + if project_member.role <= 5 and str(inbox_issue.created_by_id) != str( request.user.id ): return Response( @@ -371,9 +371,8 @@ class InboxIssueViewSet(BaseViewSet): workspace__slug=slug, project_id=project_id, ) - # Only allow guests and viewers to edit name and description - if project_member.role <= 10: - # viewers and guests since only viewers and guests + # Only allow guests to edit name and description + if project_member.role <= 5: issue_data = { "name": issue_data.get("name", issue.name), "description_html": issue_data.get( @@ -415,7 +414,7 @@ class InboxIssueViewSet(BaseViewSet): ) # Only project admins and members can edit inbox issue attributes - if project_member.role > 10: + if project_member.role > 5: serializer = InboxIssueSerializer( inbox_issue, data=request.data, partial=True ) @@ -515,7 +514,10 @@ class InboxIssueViewSet(BaseViewSet): return Response(serializer, status=status.HTTP_200_OK) @allow_permission( - allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER], + allowed_roles=[ + ROLE.ADMIN, + ROLE.MEMBER, + ], creator=True, model=Issue, ) diff --git a/apiserver/plane/app/views/issue/activity.py b/apiserver/plane/app/views/issue/activity.py index 0475cb9ec..f46cb87f3 100644 --- a/apiserver/plane/app/views/issue/activity.py +++ b/apiserver/plane/app/views/issue/activity.py @@ -19,7 +19,11 @@ from plane.app.serializers import ( IssueActivitySerializer, IssueCommentSerializer, ) -from plane.app.permissions import ProjectEntityPermission, allow_permission, ROLE +from plane.app.permissions import ( + ProjectEntityPermission, + allow_permission, + ROLE, +) from plane.db.models import ( IssueActivity, IssueComment, @@ -33,7 +37,13 @@ class IssueActivityEndpoint(BaseAPIView): ] @method_decorator(gzip_page) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST, ROLE.VIEWER]) + @allow_permission( + [ + ROLE.ADMIN, + ROLE.MEMBER, + ROLE.GUEST, + ] + ) def get(self, request, slug, project_id, issue_id): filters = {} if request.GET.get("created_at__gt", None) is not None: diff --git a/apiserver/plane/app/views/issue/archive.py b/apiserver/plane/app/views/issue/archive.py index 811e6f8f9..4817ea90e 100644 --- a/apiserver/plane/app/views/issue/archive.py +++ b/apiserver/plane/app/views/issue/archive.py @@ -97,7 +97,12 @@ class IssueArchiveViewSet(BaseViewSet): ) @method_decorator(gzip_page) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER]) + @allow_permission( + [ + ROLE.ADMIN, + ROLE.MEMBER, + ] + ) def list(self, request, slug, project_id): filters = issue_filters(request.query_params, "GET") show_sub_issues = request.GET.get("show_sub_issues", "true") @@ -213,7 +218,12 @@ class IssueArchiveViewSet(BaseViewSet): ), ) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER]) + @allow_permission( + [ + ROLE.ADMIN, + ROLE.MEMBER, + ] + ) def retrieve(self, request, slug, project_id, pk=None): issue = ( self.get_queryset() diff --git a/apiserver/plane/app/views/issue/attachment.py b/apiserver/plane/app/views/issue/attachment.py index 08e2f1a42..434c72d1d 100644 --- a/apiserver/plane/app/views/issue/attachment.py +++ b/apiserver/plane/app/views/issue/attachment.py @@ -64,7 +64,13 @@ class IssueAttachmentEndpoint(BaseAPIView): return Response(status=status.HTTP_204_NO_CONTENT) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST, ROLE.VIEWER]) + @allow_permission( + [ + ROLE.ADMIN, + ROLE.MEMBER, + ROLE.GUEST, + ] + ) def get(self, request, slug, project_id, issue_id): issue_attachments = IssueAttachment.objects.filter( issue_id=issue_id, workspace__slug=slug, project_id=project_id diff --git a/apiserver/plane/app/views/issue/base.py b/apiserver/plane/app/views/issue/base.py index 69dd32a35..6106acd3d 100644 --- a/apiserver/plane/app/views/issue/base.py +++ b/apiserver/plane/app/views/issue/base.py @@ -62,7 +62,7 @@ from plane.bgtasks.webhook_task import model_activity class IssueListEndpoint(BaseAPIView): - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def get(self, request, slug, project_id): issue_ids = request.GET.get("issues", False) @@ -232,8 +232,9 @@ class IssueViewSet(BaseViewSet): ).distinct() @method_decorator(gzip_page) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def list(self, request, slug, project_id): + project = Project.objects.get(pk=project_id, workspace__slug=slug) filters = issue_filters(request.query_params, "GET") order_by_param = request.GET.get("order_by", "-created_at") @@ -264,13 +265,16 @@ class IssueViewSet(BaseViewSet): entity_identifier=project_id, user_id=request.user.id, ) - if ProjectMember.objects.filter( - workspace__slug=slug, - project_id=project_id, - member=request.user, - role=5, - is_active=True, - ).exists(): + 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 + ): issue_queryset = issue_queryset.filter(created_by=request.user) if group_by: @@ -440,9 +444,17 @@ class IssueViewSet(BaseViewSet): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @allow_permission( - [ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER], creator=True, model=Issue + allowed_roles=[ + ROLE.ADMIN, + ROLE.MEMBER, + ROLE.GUEST, + ], + creator=True, + model=Issue, ) def retrieve(self, request, slug, project_id, pk=None): + project = Project.objects.get(pk=project_id, workspace__slug=slug) + issue = ( self.get_queryset() .filter(pk=pk) @@ -511,6 +523,27 @@ class IssueViewSet(BaseViewSet): status=status.HTTP_404_NOT_FOUND, ) + """ + if the role is guest and guest_view_all_features is false and owned by is not + the requesting user then dont show the issue + """ + + 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 view this issue"}, + status=status.HTTP_400_BAD_REQUEST, + ) + recent_visited_task.delay( slug=slug, entity_name="issue", @@ -522,7 +555,9 @@ class IssueViewSet(BaseViewSet): serializer = IssueDetailSerializer(issue, expand=self.expand) return Response(serializer.data, status=status.HTTP_200_OK) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) + @allow_permission( + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], creator=True, model=Issue + ) def partial_update(self, request, slug, project_id, pk=None): issue = ( self.get_queryset() @@ -618,7 +653,7 @@ class IssueViewSet(BaseViewSet): class IssueUserDisplayPropertyEndpoint(BaseAPIView): - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST, ROLE.VIEWER]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def patch(self, request, slug, project_id): issue_property = IssueUserProperty.objects.get( user=request.user, @@ -638,7 +673,13 @@ class IssueUserDisplayPropertyEndpoint(BaseAPIView): serializer = IssueUserPropertySerializer(issue_property) return Response(serializer.data, status=status.HTTP_201_CREATED) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST, ROLE.VIEWER]) + @allow_permission( + [ + ROLE.ADMIN, + ROLE.MEMBER, + ROLE.GUEST, + ] + ) def get(self, request, slug, project_id): issue_property, _ = IssueUserProperty.objects.get_or_create( user=request.user, project_id=project_id @@ -719,7 +760,7 @@ class IssuePaginatedViewSet(BaseViewSet): return paginated_data - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def list(self, request, slug, project_id): cursor = request.GET.get("cursor", None) is_description_required = request.GET.get("description", False) diff --git a/apiserver/plane/app/views/issue/comment.py b/apiserver/plane/app/views/issue/comment.py index 418ec2682..8bd7daed3 100644 --- a/apiserver/plane/app/views/issue/comment.py +++ b/apiserver/plane/app/views/issue/comment.py @@ -16,7 +16,7 @@ from plane.app.serializers import ( IssueCommentSerializer, CommentReactionSerializer, ) -from plane.app.permissions import ProjectLitePermission, allow_permission, ROLE +from plane.app.permissions import allow_permission, ROLE from plane.db.models import ( IssueComment, ProjectMember, @@ -63,7 +63,12 @@ class IssueCommentViewSet(BaseViewSet): .distinct() ) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST, ROLE.VIEWER]) + @allow_permission( + [ + ROLE.ADMIN, + ROLE.MEMBER, + ] + ) def create(self, request, slug, project_id, issue_id): serializer = IssueCommentSerializer(data=request.data) if serializer.is_valid(): @@ -156,9 +161,6 @@ class IssueCommentViewSet(BaseViewSet): class CommentReactionViewSet(BaseViewSet): serializer_class = CommentReactionSerializer model = CommentReaction - permission_classes = [ - ProjectLitePermission, - ] def get_queryset(self): return ( @@ -176,6 +178,12 @@ class CommentReactionViewSet(BaseViewSet): .distinct() ) + @allow_permission( + [ + ROLE.ADMIN, + ROLE.MEMBER, + ] + ) def create(self, request, slug, project_id, comment_id): serializer = CommentReactionSerializer(data=request.data) if serializer.is_valid(): @@ -198,6 +206,12 @@ class CommentReactionViewSet(BaseViewSet): 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, + ] + ) def destroy(self, request, slug, project_id, comment_id, reaction_code): comment_reaction = CommentReaction.objects.get( workspace__slug=slug, diff --git a/apiserver/plane/app/views/issue/label.py b/apiserver/plane/app/views/issue/label.py index 95ee2a847..7a72932ff 100644 --- a/apiserver/plane/app/views/issue/label.py +++ b/apiserver/plane/app/views/issue/label.py @@ -43,7 +43,7 @@ class LabelViewSet(BaseViewSet): @invalidate_cache( path="/api/workspaces/:slug/labels/", url_params=True, user=False ) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) + @allow_permission([ROLE.ADMIN]) def create(self, request, slug, project_id): try: serializer = LabelSerializer(data=request.data) @@ -66,14 +66,14 @@ class LabelViewSet(BaseViewSet): @invalidate_cache( path="/api/workspaces/:slug/labels/", url_params=True, user=False ) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) + @allow_permission([ROLE.ADMIN]) def partial_update(self, request, *args, **kwargs): return super().partial_update(request, *args, **kwargs) @invalidate_cache( path="/api/workspaces/:slug/labels/", url_params=True, user=False ) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) + @allow_permission([ROLE.ADMIN]) def destroy(self, request, *args, **kwargs): return super().destroy(request, *args, **kwargs) diff --git a/apiserver/plane/app/views/module/base.py b/apiserver/plane/app/views/module/base.py index 975b6359c..d09848fd9 100644 --- a/apiserver/plane/app/views/module/base.py +++ b/apiserver/plane/app/views/module/base.py @@ -317,7 +317,12 @@ class ModuleViewSet(BaseViewSet): .order_by("-is_favorite", "-created_at") ) - allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER]) + allow_permission( + [ + ROLE.ADMIN, + ROLE.MEMBER, + ] + ) def create(self, request, slug, project_id): project = Project.objects.get(workspace__slug=slug, pk=project_id) @@ -381,7 +386,7 @@ class ModuleViewSet(BaseViewSet): return Response(module, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST]) + allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def list(self, request, slug, project_id): queryset = self.get_queryset().filter(archived_at__isnull=True) @@ -430,7 +435,12 @@ class ModuleViewSet(BaseViewSet): ) return Response(modules, status=status.HTTP_200_OK) - allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER]) + allow_permission( + [ + ROLE.ADMIN, + ROLE.MEMBER, + ] + ) def retrieve(self, request, slug, project_id, pk): queryset = ( @@ -861,7 +871,7 @@ class ModuleFavoriteViewSet(BaseViewSet): class ModuleUserPropertiesEndpoint(BaseAPIView): - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def patch(self, request, slug, project_id, module_id): module_properties = ModuleUserProperties.objects.get( user=request.user, @@ -884,7 +894,7 @@ class ModuleUserPropertiesEndpoint(BaseAPIView): serializer = ModuleUserPropertiesSerializer(module_properties) return Response(serializer.data, status=status.HTTP_201_CREATED) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def get(self, request, slug, project_id, module_id): module_properties, _ = ModuleUserProperties.objects.get_or_create( user=request.user, diff --git a/apiserver/plane/app/views/module/issue.py b/apiserver/plane/app/views/module/issue.py index 0f800431d..eb63890d2 100644 --- a/apiserver/plane/app/views/module/issue.py +++ b/apiserver/plane/app/views/module/issue.py @@ -91,7 +91,12 @@ class ModuleIssueViewSet(BaseViewSet): ).distinct() @method_decorator(gzip_page) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER]) + @allow_permission( + [ + ROLE.ADMIN, + ROLE.MEMBER, + ] + ) def list(self, request, slug, project_id, module_id): filters = issue_filters(request.query_params, "GET") issue_queryset = self.get_queryset().filter(**filters) diff --git a/apiserver/plane/app/views/notification/base.py b/apiserver/plane/app/views/notification/base.py index 9218b34c4..743b94b4f 100644 --- a/apiserver/plane/app/views/notification/base.py +++ b/apiserver/plane/app/views/notification/base.py @@ -41,7 +41,7 @@ class NotificationViewSet(BaseViewSet, BasePaginator): ) @allow_permission( - allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST], + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE", ) def list(self, request, slug): @@ -174,7 +174,7 @@ class NotificationViewSet(BaseViewSet, BasePaginator): return Response(serializer.data, status=status.HTTP_200_OK) @allow_permission( - allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST], + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE", ) def partial_update(self, request, slug, pk): @@ -195,8 +195,7 @@ class NotificationViewSet(BaseViewSet, BasePaginator): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @allow_permission( - allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST], - level="WORKSPACE", + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE" ) def mark_read(self, request, slug, pk): notification = Notification.objects.get( @@ -246,7 +245,7 @@ class NotificationViewSet(BaseViewSet, BasePaginator): class UnreadNotificationEndpoint(BaseAPIView): @allow_permission( - allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST], + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE", ) def get(self, request, slug): diff --git a/apiserver/plane/app/views/page/base.py b/apiserver/plane/app/views/page/base.py index dad877626..bb4814e47 100644 --- a/apiserver/plane/app/views/page/base.py +++ b/apiserver/plane/app/views/page/base.py @@ -32,8 +32,10 @@ from plane.db.models import ( UserFavorite, ProjectMember, ProjectPage, + Project, ) from plane.utils.error_codes import ERROR_CODES + # Module imports from ..base import BaseAPIView, BaseViewSet @@ -120,7 +122,7 @@ class PageViewSet(BaseViewSet): .distinct() ) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def create(self, request, slug, project_id): serializer = PageSerializer( data=request.data, @@ -142,7 +144,7 @@ class PageViewSet(BaseViewSet): 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]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def partial_update(self, request, slug, project_id, pk): try: page = Page.objects.get( @@ -208,9 +210,38 @@ class PageViewSet(BaseViewSet): status=status.HTTP_400_BAD_REQUEST, ) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER]) + @allow_permission( + [ + ROLE.ADMIN, + ROLE.MEMBER, + ROLE.GUEST, + ] + ) def retrieve(self, request, slug, project_id, pk=None): page = self.get_queryset().filter(pk=pk).first() + project = Project.objects.get(pk=project_id) + + """ + if the role is guest and guest_view_all_features is false and owned by is not + the requesting user then dont show the page + """ + + 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 page.owned_by == request.user + ): + return Response( + {"error": "You are not allowed to view this page"}, + status=status.HTTP_400_BAD_REQUEST, + ) + if page is None: return Response( {"error": "Page not found"}, @@ -234,7 +265,7 @@ class PageViewSet(BaseViewSet): status=status.HTTP_200_OK, ) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def lock(self, request, slug, project_id, pk): page = Page.objects.filter( pk=pk, workspace__slug=slug, projects__id=project_id @@ -244,7 +275,7 @@ class PageViewSet(BaseViewSet): page.save() return Response(status=status.HTTP_204_NO_CONTENT) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def unlock(self, request, slug, project_id, pk): page = Page.objects.filter( pk=pk, workspace__slug=slug, projects__id=project_id @@ -255,7 +286,7 @@ class PageViewSet(BaseViewSet): return Response(status=status.HTTP_204_NO_CONTENT) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def access(self, request, slug, project_id, pk): access = request.data.get("access", 0) page = Page.objects.filter( @@ -278,13 +309,31 @@ class PageViewSet(BaseViewSet): page.save() return Response(status=status.HTTP_204_NO_CONTENT) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER]) + @allow_permission( + [ + ROLE.ADMIN, + ROLE.MEMBER, + ROLE.GUEST, + ] + ) def list(self, request, slug, project_id): queryset = self.get_queryset() + project = Project.objects.get(pk=project_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 + ): + queryset = queryset.filter(owned_by=request.user) pages = PageSerializer(queryset, many=True).data return Response(pages, status=status.HTTP_200_OK) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def archive(self, request, slug, project_id, pk): page = Page.objects.get( pk=pk, workspace__slug=slug, projects__id=project_id @@ -319,7 +368,7 @@ class PageViewSet(BaseViewSet): status=status.HTTP_200_OK, ) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def unarchive(self, request, slug, project_id, pk): page = Page.objects.get( pk=pk, workspace__slug=slug, projects__id=project_id @@ -477,7 +526,13 @@ class SubPagesEndpoint(BaseAPIView): class PagesDescriptionViewSet(BaseViewSet): - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER]) + @allow_permission( + [ + ROLE.ADMIN, + ROLE.MEMBER, + ROLE.GUEST, + ] + ) def retrieve(self, request, slug, project_id, pk): page = ( Page.objects.filter( @@ -507,7 +562,7 @@ class PagesDescriptionViewSet(BaseViewSet): ) return response - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def partial_update(self, request, slug, project_id, pk): page = ( Page.objects.filter( diff --git a/apiserver/plane/app/views/page/version.py b/apiserver/plane/app/views/page/version.py index 995a06263..1152be4f7 100644 --- a/apiserver/plane/app/views/page/version.py +++ b/apiserver/plane/app/views/page/version.py @@ -15,7 +15,7 @@ from plane.app.permissions import allow_permission, ROLE class PageVersionEndpoint(BaseAPIView): @allow_permission( - allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST] + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST] ) def get(self, request, slug, project_id, page_id, pk=None): # Check if pk is provided diff --git a/apiserver/plane/app/views/project/base.py b/apiserver/plane/app/views/project/base.py index ebc0e83fd..7aba3b023 100644 --- a/apiserver/plane/app/views/project/base.py +++ b/apiserver/plane/app/views/project/base.py @@ -71,13 +71,6 @@ class ProjectViewSet(BaseViewSet): super() .get_queryset() .filter(workspace__slug=self.kwargs.get("slug")) - .filter( - Q( - project_projectmember__member=self.request.user, - project_projectmember__is_active=True, - ) - | Q(network=2) - ) .select_related( "workspace", "workspace__owner", @@ -155,7 +148,7 @@ class ProjectViewSet(BaseViewSet): ) @allow_permission( - allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST], + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE", ) def list(self, request, slug): @@ -165,6 +158,31 @@ class ProjectViewSet(BaseViewSet): if field ] projects = self.get_queryset().order_by("sort_order", "name") + if WorkspaceMember.objects.filter( + member=request.user, + workspace__slug=slug, + is_active=True, + role=5, + ).exists(): + projects = projects.filter( + project_projectmember__member=self.request.user, + project_projectmember__is_active=True, + ) + + if WorkspaceMember.objects.filter( + member=request.user, + workspace__slug=slug, + is_active=True, + role=10, + ).exists(): + projects = projects.filter( + Q( + project_projectmember__member=self.request.user, + project_projectmember__is_active=True, + ) + | Q(network=2) + ) + if request.GET.get("per_page", False) and request.GET.get( "cursor", False ): @@ -177,24 +195,13 @@ class ProjectViewSet(BaseViewSet): ).data, ) - if WorkspaceMember.objects.filter( - member=request.user, - workspace__slug=slug, - is_active=True, - role__in=[5, 10], - ).exists(): - projects = projects.filter( - project_projectmember__member=self.request.user, - project_projectmember__is_active=True, - ) - projects = ProjectListSerializer( projects, many=True, fields=fields if fields else None ).data return Response(projects, status=status.HTTP_200_OK) @allow_permission( - allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST], + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE", ) def retrieve(self, request, slug, pk): diff --git a/apiserver/plane/app/views/project/invite.py b/apiserver/plane/app/views/project/invite.py index 6278c5f3b..4cb5f4333 100644 --- a/apiserver/plane/app/views/project/invite.py +++ b/apiserver/plane/app/views/project/invite.py @@ -70,7 +70,7 @@ class ProjectInvitationsViewset(BaseViewSet): [ email for email in emails - if int(email.get("role", 10)) > requesting_user.role + if int(email.get("role", 5)) > requesting_user.role ] ): return Response( @@ -97,7 +97,7 @@ class ProjectInvitationsViewset(BaseViewSet): settings.SECRET_KEY, algorithm="HS256", ), - role=email.get("role", 10), + role=email.get("role", 5), created_by=request.user, ) ) @@ -170,7 +170,7 @@ class UserProjectInvitationsViewset(BaseViewSet): ProjectMember( project_id=project_id, member=request.user, - role=15 if workspace_role >= 15 else 10, + role=15 if workspace_role >= 15 else 5, workspace=workspace, created_by=request.user, ) diff --git a/apiserver/plane/app/views/project/member.py b/apiserver/plane/app/views/project/member.py index 460e35e2e..e201ab5a7 100644 --- a/apiserver/plane/app/views/project/member.py +++ b/apiserver/plane/app/views/project/member.py @@ -95,9 +95,21 @@ class ProjectMemberViewSet(BaseViewSet): member=member, is_active=True, ).role - if workspace_member_role in [5, 10] and member_roles.get( - member - ) in [15, 20]: + if workspace_member_role in [20] and member_roles.get(member) in [ + 5, + 15, + ]: + return Response( + { + "error": "You cannot add a user with role lower than the workspace role" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + if workspace_member_role in [5] and member_roles.get(member) in [ + 15, + 20, + ]: return Response( { "error": "You cannot add a user with role higher than the workspace role" @@ -143,7 +155,7 @@ class ProjectMemberViewSet(BaseViewSet): bulk_project_members.append( ProjectMember( member_id=member.get("member_id"), - role=member.get("role", 10), + role=member.get("role", 5), project_id=project_id, workspace_id=project.workspace_id, sort_order=( @@ -189,7 +201,7 @@ class ProjectMemberViewSet(BaseViewSet): # Return the serialized data return Response(serializer.data, status=status.HTTP_201_CREATED) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def list(self, request, slug, project_id): # Get the list of project members for the project project_members = ProjectMember.objects.filter( @@ -230,7 +242,7 @@ class ProjectMemberViewSet(BaseViewSet): member=project_member.member, is_active=True, ).role - if workspace_role in [5, 10] and int( + if workspace_role in [5] and int( request.data.get("role", project_member.role) ) in [15, 20]: return Response( @@ -261,7 +273,7 @@ class ProjectMemberViewSet(BaseViewSet): return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) + @allow_permission([ROLE.ADMIN]) def destroy(self, request, slug, project_id, pk): project_member = ProjectMember.objects.get( workspace__slug=slug, @@ -298,7 +310,7 @@ class ProjectMemberViewSet(BaseViewSet): project_member.save() return Response(status=status.HTTP_204_NO_CONTENT) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST]) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def leave(self, request, slug, project_id): project_member = ProjectMember.objects.get( workspace__slug=slug, diff --git a/apiserver/plane/app/views/state/base.py b/apiserver/plane/app/views/state/base.py index b488d9efb..fd94a8dae 100644 --- a/apiserver/plane/app/views/state/base.py +++ b/apiserver/plane/app/views/state/base.py @@ -9,7 +9,8 @@ from rest_framework import status from .. import BaseViewSet from plane.app.serializers import StateSerializer from plane.app.permissions import ( - ProjectEntityPermission, + ROLE, + allow_permission ) from plane.db.models import State, Issue from plane.utils.cache import invalidate_cache @@ -18,9 +19,6 @@ from plane.utils.cache import invalidate_cache class StateViewSet(BaseViewSet): serializer_class = StateSerializer model = State - permission_classes = [ - ProjectEntityPermission, - ] def get_queryset(self): return self.filter_queryset( @@ -42,6 +40,7 @@ class StateViewSet(BaseViewSet): @invalidate_cache( path="workspaces/:slug/states/", url_params=True, user=False ) + @allow_permission([ROLE.ADMIN]) def create(self, request, slug, project_id): serializer = StateSerializer(data=request.data) if serializer.is_valid(): @@ -49,6 +48,7 @@ class StateViewSet(BaseViewSet): return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def list(self, request, slug, project_id): states = StateSerializer(self.get_queryset(), many=True).data grouped = request.GET.get("grouped", False) @@ -65,6 +65,7 @@ class StateViewSet(BaseViewSet): @invalidate_cache( path="workspaces/:slug/states/", url_params=True, user=False ) + @allow_permission([ROLE.ADMIN]) def mark_as_default(self, request, slug, project_id, pk): # Select all the states which are marked as default _ = State.objects.filter( @@ -78,6 +79,7 @@ class StateViewSet(BaseViewSet): @invalidate_cache( path="workspaces/:slug/states/", url_params=True, user=False ) + @allow_permission([ROLE.ADMIN]) def destroy(self, request, slug, project_id, pk): state = State.objects.get( is_triage=False, diff --git a/apiserver/plane/app/views/view/base.py b/apiserver/plane/app/views/view/base.py index 4a571ef25..861aa4292 100644 --- a/apiserver/plane/app/views/view/base.py +++ b/apiserver/plane/app/views/view/base.py @@ -35,6 +35,7 @@ from plane.db.models import ( Workspace, WorkspaceMember, ProjectMember, + Project, ) from plane.utils.grouper import ( issue_group_values, @@ -75,7 +76,7 @@ class WorkspaceViewViewSet(BaseViewSet): ) @allow_permission( - allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST], + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE", ) def list(self, request, slug): @@ -259,7 +260,7 @@ class WorkspaceViewIssuesViewSet(BaseViewSet): @method_decorator(gzip_page) @allow_permission( - allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST], + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE", ) def list(self, request, slug): @@ -272,15 +273,24 @@ class WorkspaceViewIssuesViewSet(BaseViewSet): .annotate(cycle_id=F("issue_cycle__cycle_id")) ) - if WorkspaceMember.objects.filter( - workspace__slug=slug, - member=request.user, - role=5, - is_active=True, - ).exists(): - issue_queryset = issue_queryset.filter( - created_by=request.user, + # check for the project member role, if the role is 5 then check for the guest_view_all_features if it is true then show all the issues else show only the issues created by the user + + issue_queryset = issue_queryset.filter( + Q( + project__project_projectmember__role=5, + project__guest_view_all_features=True, ) + | Q( + project__project_projectmember__role=5, + project__guest_view_all_features=False, + created_by=self.request.user, + ) + | + # For other roles (role < 5), show all issues + Q(project__project_projectmember__role__gt=5), + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) # Issue queryset issue_queryset, order_by_param = order_issue_queryset( @@ -421,19 +431,21 @@ class IssueViewViewSet(BaseViewSet): .distinct() ) - allow_permission( - allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST] - ) + allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def list(self, request, slug, project_id): queryset = self.get_queryset() - if ProjectMember.objects.filter( - workspace__slug=slug, - project_id=project_id, - member=request.user, - role=5, - is_active=True, - ).exists(): + project = Project.objects.get(id=project_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 + ): queryset = queryset.filter(owned_by=request.user) fields = [ field @@ -445,14 +457,34 @@ class IssueViewViewSet(BaseViewSet): ).data return Response(views, status=status.HTTP_200_OK) - allow_permission( - allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST] - ) + allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def retrieve(self, request, slug, project_id, pk): issue_view = ( self.get_queryset().filter(pk=pk, project_id=project_id).first() ) + project = Project.objects.get(id=project_id) + """ + if the role is guest and guest_view_all_features is false and owned by is not + the requesting user then dont show the view + """ + + 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_view.owned_by == request.user + ): + return Response( + {"error": "You are not allowed to view this issue"}, + status=status.HTTP_400_BAD_REQUEST, + ) + serializer = IssueViewSerializer(issue_view) recent_visited_task.delay( slug=slug, diff --git a/apiserver/plane/app/views/workspace/base.py b/apiserver/plane/app/views/workspace/base.py index 6fa0ccc1e..dd6417a05 100644 --- a/apiserver/plane/app/views/workspace/base.py +++ b/apiserver/plane/app/views/workspace/base.py @@ -43,6 +43,7 @@ from plane.db.models import ( WorkspaceMember, WorkspaceTheme, ) +from plane.app.permissions import ROLE, allow_permission from plane.utils.cache import cache_response, invalidate_cache from django.utils.decorators import method_decorator from django.views.decorators.cache import cache_control @@ -147,11 +148,25 @@ class WorkSpaceViewSet(BaseViewSet): ) @cache_response(60 * 60 * 2) + @allow_permission( + [ + ROLE.ADMIN, + ROLE.MEMBER, + ROLE.GUEST, + ], + level="WORKSPACE", + ) def list(self, request, *args, **kwargs): return super().list(request, *args, **kwargs) @invalidate_cache(path="/api/workspaces/", user=False) @invalidate_cache(path="/api/users/me/workspaces/") + @allow_permission( + [ + ROLE.ADMIN, + ], + level="WORKSPACE", + ) def partial_update(self, request, *args, **kwargs): return super().partial_update(request, *args, **kwargs) @@ -162,6 +177,7 @@ class WorkSpaceViewSet(BaseViewSet): @invalidate_cache( path="/api/users/me/settings/", multiple=True, user=False ) + @allow_permission([ROLE.ADMIN], level="WORKSPACE") def destroy(self, request, *args, **kwargs): return super().destroy(request, *args, **kwargs) diff --git a/apiserver/plane/app/views/workspace/invite.py b/apiserver/plane/app/views/workspace/invite.py index d3511a865..79b03d8a0 100644 --- a/apiserver/plane/app/views/workspace/invite.py +++ b/apiserver/plane/app/views/workspace/invite.py @@ -74,7 +74,7 @@ class WorkspaceInvitationsViewset(BaseViewSet): [ email for email in emails - if int(email.get("role", 10)) > requesting_user.role + if int(email.get("role", 5)) > requesting_user.role ] ): return Response( @@ -119,7 +119,7 @@ class WorkspaceInvitationsViewset(BaseViewSet): settings.SECRET_KEY, algorithm="HS256", ), - role=email.get("role", 10), + role=email.get("role", 5), created_by=request.user, ) ) diff --git a/apiserver/plane/app/views/workspace/member.py b/apiserver/plane/app/views/workspace/member.py index 39b2f3d98..146b48b28 100644 --- a/apiserver/plane/app/views/workspace/member.py +++ b/apiserver/plane/app/views/workspace/member.py @@ -13,7 +13,8 @@ from rest_framework.response import Response from plane.app.permissions import ( WorkSpaceAdminPermission, WorkspaceEntityPermission, - WorkspaceUserPermission, + allow_permission, + ROLE ) # Module imports @@ -43,21 +44,6 @@ class WorkSpaceMemberViewSet(BaseViewSet): serializer_class = WorkspaceMemberAdminSerializer model = WorkspaceMember - permission_classes = [ - WorkspaceEntityPermission, - ] - - def get_permissions(self): - if self.action == "leave": - self.permission_classes = [ - WorkspaceUserPermission, - ] - else: - self.permission_classes = [ - WorkspaceEntityPermission, - ] - - return super(WorkSpaceMemberViewSet, self).get_permissions() search_fields = [ "member__display_name", @@ -77,6 +63,9 @@ class WorkSpaceMemberViewSet(BaseViewSet): ) @cache_response(60 * 60 * 2) + @allow_permission( + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE" + ) def list(self, request, slug): workspace_member = WorkspaceMember.objects.get( member=request.user, @@ -87,7 +76,7 @@ class WorkSpaceMemberViewSet(BaseViewSet): # Get all active workspace members workspace_members = self.get_queryset() - if workspace_member.role > 10: + if workspace_member.role > 5: serializer = WorkspaceMemberAdminSerializer( workspace_members, fields=("id", "member", "role"), @@ -107,6 +96,9 @@ class WorkSpaceMemberViewSet(BaseViewSet): user=False, multiple=True, ) + @allow_permission( + allowed_roles=[ROLE.ADMIN], level="WORKSPACE" + ) def partial_update(self, request, slug, pk): workspace_member = WorkspaceMember.objects.get( pk=pk, @@ -159,6 +151,9 @@ class WorkSpaceMemberViewSet(BaseViewSet): @invalidate_cache( path="/api/users/me/workspaces/", user=False, multiple=True ) + @allow_permission( + allowed_roles=[ROLE.ADMIN], level="WORKSPACE" + ) def destroy(self, request, slug, pk): # Check the user role who is deleting the user workspace_member = WorkspaceMember.objects.get( @@ -233,6 +228,9 @@ class WorkSpaceMemberViewSet(BaseViewSet): @invalidate_cache( path="api/users/me/workspaces/", user=False, multiple=True ) + @allow_permission( + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE" + ) def leave(self, request, slug): workspace_member = WorkspaceMember.objects.get( workspace__slug=slug, diff --git a/apiserver/plane/app/views/workspace/user.py b/apiserver/plane/app/views/workspace/user.py index addb8c5ac..5c173f202 100644 --- a/apiserver/plane/app/views/workspace/user.py +++ b/apiserver/plane/app/views/workspace/user.py @@ -288,7 +288,7 @@ class WorkspaceUserProfileEndpoint(BaseAPIView): is_active=True, ) projects = [] - if requesting_workspace_member.role >= 10: + if requesting_workspace_member.role >= 15: projects = ( Project.objects.filter( workspace__slug=slug, diff --git a/apiserver/plane/authentication/utils/workspace_project_join.py b/apiserver/plane/authentication/utils/workspace_project_join.py index 3b6f231ed..4544d9998 100644 --- a/apiserver/plane/authentication/utils/workspace_project_join.py +++ b/apiserver/plane/authentication/utils/workspace_project_join.py @@ -49,7 +49,7 @@ def process_workspace_project_invitations(user): workspace_id=project_member_invite.workspace_id, role=( project_member_invite.role - if project_member_invite.role in [5, 10, 15] + if project_member_invite.role in [5, 15] else 15 ), member=user, @@ -67,7 +67,7 @@ def process_workspace_project_invitations(user): workspace_id=project_member_invite.workspace_id, role=( project_member_invite.role - if project_member_invite.role in [5, 10, 15] + if project_member_invite.role in [5, 15] else 15 ), member=user, diff --git a/apiserver/plane/db/migrations/0076_alter_projectmember_role_and_more.py b/apiserver/plane/db/migrations/0076_alter_projectmember_role_and_more.py new file mode 100644 index 000000000..ad051d56e --- /dev/null +++ b/apiserver/plane/db/migrations/0076_alter_projectmember_role_and_more.py @@ -0,0 +1,60 @@ +# Generated by Django 4.2.15 on 2024-08-30 07:34 + +from django.db import migrations, models + + +def update_workspace_project_member_role(apps, schema_editor): + WorkspaceMember = apps.get_model("db", "WorkspaceMember") + ProjectMember = apps.get_model("db", "ProjectMember") + + # update all existing members with role 10 to role 5 + WorkspaceMember.objects.filter(role=10).update(role=5) + ProjectMember.objects.filter(role=10).update(role=5) + + +class Migration(migrations.Migration): + + dependencies = [ + ("db", "0075_alter_fileasset_asset"), + ] + + operations = [ + migrations.AlterField( + model_name="projectmember", + name="role", + field=models.PositiveSmallIntegerField( + choices=[(20, "Admin"), (15, "Member"), (5, "Guest")], + default=5, + ), + ), + migrations.AlterField( + model_name="projectmemberinvite", + name="role", + field=models.PositiveSmallIntegerField( + choices=[(20, "Admin"), (15, "Member"), (5, "Guest")], + default=5, + ), + ), + migrations.AlterField( + model_name="workspacemember", + name="role", + field=models.PositiveSmallIntegerField( + choices=[(20, "Admin"), (15, "Member"), (5, "Guest")], + default=5, + ), + ), + migrations.AlterField( + model_name="workspacememberinvite", + name="role", + field=models.PositiveSmallIntegerField( + choices=[(20, "Admin"), (15, "Member"), (5, "Guest")], + default=5, + ), + ), + migrations.AddField( + model_name="project", + name="guest_view_all_features", + field=models.BooleanField(default=False), + ), + migrations.RunPython(update_workspace_project_member_role), + ] diff --git a/apiserver/plane/db/models/project.py b/apiserver/plane/db/models/project.py index f8d9379da..bcc168227 100644 --- a/apiserver/plane/db/models/project.py +++ b/apiserver/plane/db/models/project.py @@ -16,7 +16,6 @@ from .base import BaseModel ROLE_CHOICES = ( (20, "Admin"), (15, "Member"), - (10, "Viewer"), (5, "Guest"), ) @@ -98,6 +97,7 @@ class Project(BaseModel): inbox_view = models.BooleanField(default=False) is_time_tracking_enabled = models.BooleanField(default=False) is_issue_type_enabled = models.BooleanField(default=False) + guest_view_all_features = models.BooleanField(default=False) cover_image = models.URLField(blank=True, null=True, max_length=800) estimate = models.ForeignKey( "db.Estimate", @@ -173,7 +173,7 @@ class ProjectMemberInvite(ProjectBaseModel): token = models.CharField(max_length=255) message = models.TextField(null=True) responded_at = models.DateTimeField(null=True) - role = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, default=10) + role = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, default=5) class Meta: verbose_name = "Project Member Invite" @@ -194,7 +194,7 @@ class ProjectMember(ProjectBaseModel): related_name="member_project", ) comment = models.TextField(blank=True, null=True) - role = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, default=10) + role = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, default=5) view_props = models.JSONField(default=get_default_props) default_props = models.JSONField(default=get_default_props) preferences = models.JSONField(default=get_default_preferences) diff --git a/apiserver/plane/db/models/workspace.py b/apiserver/plane/db/models/workspace.py index 2c60dbe5b..50dac6096 100644 --- a/apiserver/plane/db/models/workspace.py +++ b/apiserver/plane/db/models/workspace.py @@ -8,9 +8,8 @@ from .base import BaseModel from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS ROLE_CHOICES = ( - (20, "Owner"), - (15, "Admin"), - (10, "Member"), + (20, "Admin"), + (15, "Member"), (5, "Guest"), ) @@ -177,7 +176,7 @@ class WorkspaceMember(BaseModel): on_delete=models.CASCADE, related_name="member_workspace", ) - role = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, default=10) + role = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, default=5) company_role = models.TextField(null=True, blank=True) view_props = models.JSONField(default=get_default_props) default_props = models.JSONField(default=get_default_props) @@ -214,7 +213,7 @@ class WorkspaceMemberInvite(BaseModel): token = models.CharField(max_length=255) message = models.TextField(null=True) responded_at = models.DateTimeField(null=True) - role = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, default=10) + role = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, default=5) class Meta: unique_together = ["email", "workspace", "deleted_at"] diff --git a/packages/types/src/enums.ts b/packages/types/src/enums.ts index 08949bd17..914ebb0c3 100644 --- a/packages/types/src/enums.ts +++ b/packages/types/src/enums.ts @@ -1,10 +1,14 @@ -export enum EUserProjectRoles { - GUEST = 5, - VIEWER = 10, - MEMBER = 15, +export enum EUserPermissions { ADMIN = 20, + MEMBER = 15, + GUEST = 5, } +export type TUserPermissions = + | EUserPermissions.ADMIN + | EUserPermissions.MEMBER + | EUserPermissions.GUEST; + // project pages export enum EPageAccess { PUBLIC = 0, diff --git a/packages/types/src/project/projects.d.ts b/packages/types/src/project/projects.d.ts index 76ea39ded..a46f490f1 100644 --- a/packages/types/src/project/projects.d.ts +++ b/packages/types/src/project/projects.d.ts @@ -1,4 +1,3 @@ -import { EUserProjectRoles } from "@/constants/project"; import type { IProjectViewProps, IUser, @@ -9,6 +8,7 @@ import type { TLogoProps, TStateGroups, } from ".."; +import { TUserPermissions } from "../enums"; export interface IProject { archive_in: number; @@ -30,6 +30,7 @@ export interface IProject { draft_issues: number; draft_sub_issues: number; estimate: string | null; + guest_view_all_features: boolean; id: string; identifier: string; anchor: string | null; @@ -38,7 +39,7 @@ export interface IProject { is_member: boolean; is_time_tracking_enabled: boolean; logo_props: TLogoProps; - member_role: EUserProjectRoles | null; + member_role: TUserPermissions | null; members: IProjectMemberLite[]; name: string; network: number; @@ -85,7 +86,7 @@ export interface IProjectMember { project: IProjectLite; workspace: IWorkspaceLite; comment: string; - role: EUserProjectRoles; + role: TUserPermissions; preferences: ProjectPreferences; @@ -101,11 +102,11 @@ export interface IProjectMember { export interface IProjectMembership { id: string; member: string; - role: EUserProjectRoles; + role: TUserPermissions; } export interface IProjectBulkAddFormData { - members: { role: EUserProjectRoles; member_id: string }[]; + members: { role: TUserPermissions; member_id: string }[]; } export interface IGithubRepository { diff --git a/packages/types/src/users.d.ts b/packages/types/src/users.d.ts index 99ba7a4a8..4d5db28f9 100644 --- a/packages/types/src/users.d.ts +++ b/packages/types/src/users.d.ts @@ -1,9 +1,5 @@ -import { - EUserProjectRoles, - IIssueActivity, - TIssuePriorities, - TStateGroups, -} from "."; +import { IIssueActivity, TIssuePriorities, TStateGroups } from "."; +import { TUserPermissions } from "./enums"; type TLoginMediums = "email" | "magic-code" | "github" | "gitlab" | "google"; @@ -134,7 +130,6 @@ export interface IUserActivityResponse { export type UserAuth = { isMember: boolean; isOwner: boolean; - isViewer: boolean; isGuest: boolean; }; @@ -175,7 +170,7 @@ export interface IUserProfileProjectSegregation { } export interface IUserProjectsRole { - [projectId: string]: EUserProjectRoles; + [projectId: string]: TUserPermissions; } export interface IUserEmailNotificationSettings { diff --git a/packages/types/src/workspace.d.ts b/packages/types/src/workspace.d.ts index d70dbcc81..8ce3fc56a 100644 --- a/packages/types/src/workspace.d.ts +++ b/packages/types/src/workspace.d.ts @@ -1,4 +1,3 @@ -import {EUserWorkspaceRoles} from "@/constants/workspace"; import type { ICycle, IProjectMember, @@ -6,6 +5,7 @@ import type { IUserLite, IWorkspaceViewProps, } from "@plane/types"; +import { TUserPermissions } from "./enums"; export interface IWorkspace { readonly id: string; @@ -36,7 +36,7 @@ export interface IWorkspaceMemberInvitation { id: string; message: string; responded_at: Date; - role: EUserWorkspaceRoles; + role: TUserPermissions; token: string; workspace: { id: string; @@ -47,7 +47,7 @@ export interface IWorkspaceMemberInvitation { } export interface IWorkspaceBulkInviteFormData { - emails: {email: string; role: EUserWorkspaceRoles}[]; + emails: { email: string; role: TUserPermissions }[]; } export type Properties = { @@ -69,7 +69,7 @@ export type Properties = { export interface IWorkspaceMember { id: string; member: IUserLite; - role: EUserWorkspaceRoles; + role: TUserPermissions; created_at?: string; avatar?: string; email?: string; @@ -86,7 +86,7 @@ export interface IWorkspaceMemberMe { default_props: IWorkspaceViewProps; id: string; member: string; - role: EUserWorkspaceRoles; + role: TUserPermissions; updated_at: Date; updated_by: string; view_props: IWorkspaceViewProps; diff --git a/packages/ui/src/dropdowns/combo-box.tsx b/packages/ui/src/dropdowns/combo-box.tsx index 1ee96480e..e92805ef9 100644 --- a/packages/ui/src/dropdowns/combo-box.tsx +++ b/packages/ui/src/dropdowns/combo-box.tsx @@ -58,7 +58,7 @@ const ComboDropDown = forwardRef((props: Props, ref) => { } return ( - //@ts-ignore + // @ts-ignore {button} {children} @@ -70,4 +70,6 @@ const ComboOptions = Combobox.Options; const ComboOption = Combobox.Option; const ComboInput = Combobox.Input; +ComboDropDown.displayName = "ComboDropDown"; + export { ComboDropDown, ComboOptions, ComboOption, ComboInput }; diff --git a/web/app/[workspaceSlug]/(projects)/notifications/page.tsx b/web/app/[workspaceSlug]/(projects)/notifications/page.tsx index 2e8efd107..1c2036efd 100644 --- a/web/app/[workspaceSlug]/(projects)/notifications/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/notifications/page.tsx @@ -14,7 +14,7 @@ import { IssuePeekOverview } from "@/components/issues"; import { EmptyStateType } from "@/constants/empty-state"; import { ENotificationLoader, ENotificationQueryParamType } from "@/constants/notification"; // hooks -import { useIssueDetail, useUser, useWorkspace, useWorkspaceNotifications } from "@/hooks/store"; +import { useIssueDetail, useUserPermissions, useWorkspace, useWorkspaceNotifications } from "@/hooks/store"; import { useWorkspaceIssueProperties } from "@/hooks/use-workspace-issue-properties"; const WorkspaceDashboardPage = observer(() => { @@ -28,9 +28,7 @@ const WorkspaceDashboardPage = observer(() => { notificationIdsByWorkspaceId, getNotifications, } = useWorkspaceNotifications(); - const { - membership: { fetchUserProjectInfo }, - } = useUser(); + const { fetchUserProjectInfo } = useUserPermissions(); const { setPeekIssue } = useIssueDetail(); // derived values const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Inbox` : undefined; diff --git a/web/app/[workspaceSlug]/(projects)/profile/[userId]/activity/page.tsx b/web/app/[workspaceSlug]/(projects)/profile/[userId]/activity/page.tsx index cae273fd4..bf1f88d15 100644 --- a/web/app/[workspaceSlug]/(projects)/profile/[userId]/activity/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/profile/[userId]/activity/page.tsx @@ -2,16 +2,15 @@ import { useState } from "react"; import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; // ui import { Button } from "@plane/ui"; // components import { PageHead } from "@/components/core"; import { DownloadActivityButton, WorkspaceActivityListPage } from "@/components/profile"; -// constants -import { EUserWorkspaceRoles } from "@/constants/workspace"; // hooks -import { useUser } from "@/hooks/store"; +import { useUserPermissions } from "@/hooks/store"; +// plane-web constants +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; const PER_PAGE = 100; @@ -21,13 +20,7 @@ const ProfileActivityPage = observer(() => { const [totalPages, setTotalPages] = useState(0); const [resultsCount, setResultsCount] = useState(0); // router - - const { userId } = useParams(); - // store hooks - const { data: currentUser } = useUser(); - const { - membership: { currentWorkspaceRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); const updateTotalPages = (count: number) => setTotalPages(count); @@ -47,8 +40,10 @@ const ProfileActivityPage = observer(() => { /> ); - const canDownloadActivity = - currentUser?.id === userId && !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; + const canDownloadActivity = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); return ( <> diff --git a/web/app/[workspaceSlug]/(projects)/profile/[userId]/header.tsx b/web/app/[workspaceSlug]/(projects)/profile/[userId]/header.tsx index 423f7eaaf..13a944c88 100644 --- a/web/app/[workspaceSlug]/(projects)/profile/[userId]/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/profile/[userId]/header.tsx @@ -12,9 +12,9 @@ import { BreadcrumbLink } from "@/components/common"; // components import { ProfileIssuesFilter } from "@/components/profile"; import { PROFILE_ADMINS_TAB, PROFILE_VIEWER_TAB } from "@/constants/profile"; -import { EUserWorkspaceRoles } from "@/constants/workspace"; import { cn } from "@/helpers/common.helper"; -import { useAppTheme, useUser } from "@/hooks/store"; +import { useAppTheme, useUser, useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; type TUserProfileHeader = { userProjectsData: IUserProfileProjectSegregation | undefined; @@ -28,14 +28,15 @@ export const UserProfileHeader: FC = observer((props) => { const { workspaceSlug, userId } = useParams(); // store hooks const { toggleProfileSidebar, profileSidebarCollapsed } = useAppTheme(); - const { - membership: { currentWorkspaceRole }, - data: currentUser, - } = useUser(); + const { data: currentUser } = useUser(); + const { workspaceUserInfo, allowPermissions } = useUserPermissions(); // derived values - const isAuthorized = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.VIEWER; + const isAuthorized = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); - if (!currentWorkspaceRole) return null; + if (!workspaceUserInfo) return null; const tabsList = isAuthorized ? [...PROFILE_VIEWER_TAB, ...PROFILE_ADMINS_TAB] : PROFILE_VIEWER_TAB; diff --git a/web/app/[workspaceSlug]/(projects)/profile/[userId]/layout.tsx b/web/app/[workspaceSlug]/(projects)/profile/[userId]/layout.tsx index d0120eebf..f31ff959d 100644 --- a/web/app/[workspaceSlug]/(projects)/profile/[userId]/layout.tsx +++ b/web/app/[workspaceSlug]/(projects)/profile/[userId]/layout.tsx @@ -9,10 +9,10 @@ import { ProfileSidebar } from "@/components/profile"; // constants import { USER_PROFILE_PROJECT_SEGREGATION } from "@/constants/fetch-keys"; import { PROFILE_ADMINS_TAB, PROFILE_VIEWER_TAB } from "@/constants/profile"; -import { EUserWorkspaceRoles } from "@/constants/workspace"; // hooks -import { useUser } from "@/hooks/store"; +import { useUserPermissions } from "@/hooks/store"; import useSize from "@/hooks/use-window-size"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; // local components import { UserService } from "@/services/user.service"; import { UserProfileHeader } from "./header"; @@ -25,17 +25,18 @@ type Props = { children: React.ReactNode; }; -const AUTHORIZED_ROLES = [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.VIEWER]; - const UseProfileLayout: React.FC = observer((props) => { const { children } = props; // router const { workspaceSlug, userId } = useParams(); const pathname = usePathname(); // store hooks - const { - membership: { currentWorkspaceRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); + // derived values + const isAuthorized = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); const windowSize = useSize(); const isSmallerScreen = windowSize[0] >= 768; @@ -47,7 +48,6 @@ const UseProfileLayout: React.FC = observer((props) => { : null ); // derived values - const isAuthorized = currentWorkspaceRole && AUTHORIZED_ROLES.includes(currentWorkspaceRole); const isAuthorizedPath = pathname.includes("assigned") || pathname.includes("created") || pathname.includes("subscribed"); const isIssuesTab = pathname.includes("assigned") || pathname.includes("created") || pathname.includes("subscribed"); diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/header.tsx index 5a29f9bb4..902f15cde 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/header.tsx @@ -21,7 +21,6 @@ import { EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, } from "@/constants/issue"; -import { EUserProjectRoles } from "@/constants/project"; // helpers import { cn } from "@/helpers/common.helper"; import { isIssueFilterActive } from "@/helpers/filter.helper"; @@ -34,13 +33,14 @@ import { useMember, useProject, useProjectState, - useUser, useIssues, useCommandPalette, + useUserPermissions, } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; import useLocalStorage from "@/hooks/use-local-storage"; import { usePlatformOS } from "@/hooks/use-platform-os"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; const CycleDropdownOption: React.FC<{ cycleId: string }> = ({ cycleId }) => { // router @@ -81,9 +81,6 @@ export const CycleIssuesHeader: React.FC = observer(() => { const { currentProjectCycleIds, getCycleById } = useCycle(); const { toggleCreateIssueModal } = useCommandPalette(); const { setTrackElement } = useEventTracker(); - const { - membership: { currentProjectRole }, - } = useUser(); const { currentProjectDetails, loader } = useProject(); const { projectStates } = useProjectState(); const { projectLabels } = useLabel(); @@ -91,6 +88,7 @@ export const CycleIssuesHeader: React.FC = observer(() => { project: { projectMemberIds }, } = useMember(); const { isMobile } = usePlatformOS(); + const { allowPermissions } = useUserPermissions(); const activeLayout = issueFilters?.displayFilters?.layout; @@ -149,8 +147,10 @@ export const CycleIssuesHeader: React.FC = observer(() => { // derived values const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined; const isCompletedCycle = cycleDetails?.status?.toLocaleLowerCase() === "completed"; - const canUserCreateIssue = - currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); + const canUserCreateIssue = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT + ); const issuesCount = getGroupIssueCount(undefined, undefined, false); diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/header.tsx index 3f3a3e6a3..b2e157ee7 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/header.tsx @@ -8,11 +8,11 @@ import { Breadcrumbs, Button, ContrastIcon, Header } from "@plane/ui"; // components import { BreadcrumbLink, Logo } from "@/components/common"; import { CyclesViewHeader } from "@/components/cycles"; -// constants -import { EUserProjectRoles } from "@/constants/project"; // hooks -import { useCommandPalette, useEventTracker, useProject, useUser } from "@/hooks/store"; +import { useCommandPalette, useEventTracker, useProject, useUserPermissions } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; +// constants +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; export const CyclesListHeader: FC = observer(() => { // router @@ -21,13 +21,13 @@ export const CyclesListHeader: FC = observer(() => { // store hooks const { toggleCreateCycleModal } = useCommandPalette(); const { setTrackElement } = useEventTracker(); - const { - membership: { currentProjectRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); const { currentProjectDetails, loader } = useProject(); - const canUserCreateCycle = - currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); + const canUserCreateCycle = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT + ); return (
diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/inbox/header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/inbox/header.tsx index a811ef3f0..a543eca0b 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/inbox/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/inbox/header.tsx @@ -9,10 +9,9 @@ import { Breadcrumbs, Button, Intake, Header } from "@plane/ui"; // components import { BreadcrumbLink, Logo } from "@/components/common"; import { InboxIssueCreateEditModalRoot } from "@/components/inbox"; -// constants -import { EUserProjectRoles } from "@/constants/project"; // hooks -import { useProject, useProjectInbox, useUser } from "@/hooks/store"; +import { useProject, useProjectInbox, useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; export const ProjectInboxHeader: FC = observer(() => { // states @@ -20,14 +19,16 @@ export const ProjectInboxHeader: FC = observer(() => { // router const { workspaceSlug, projectId } = useParams(); // store hooks - const { - membership: { currentProjectRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); + const { currentProjectDetails, loader: currentProjectDetailsLoader } = useProject(); const { loader } = useProjectInbox(); // derived value - const isViewer = currentProjectRole === EUserProjectRoles.VIEWER; + const isAuthorized = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], + EUserPermissionsLevel.PROJECT + ); return (
@@ -66,7 +67,7 @@ export const ProjectInboxHeader: FC = observer(() => { - {currentProjectDetails?.inbox_view && workspaceSlug && projectId && !isViewer ? ( + {currentProjectDetails?.inbox_view && workspaceSlug && projectId && isAuthorized ? (
{ // router const router = useAppRouter(); const { workspaceSlug, projectId } = useParams() as { workspaceSlug: string; projectId: string }; // store hooks - const { - membership: { currentProjectRole }, - } = useUser(); const { issues: { getGroupIssueCount }, } = useIssues(EIssuesStoreType.PROJECT); @@ -36,14 +33,17 @@ export const ProjectIssuesHeader = observer(() => { const { toggleCreateIssueModal } = useCommandPalette(); const { setTrackElement } = useEventTracker(); + const { allowPermissions } = useUserPermissions(); const { isMobile } = usePlatformOS(); const SPACE_APP_URL = (SPACE_BASE_URL.trim() === "" ? window.location.origin : SPACE_BASE_URL) + SPACE_BASE_PATH; const publishedURL = `${SPACE_APP_URL}/issues/${currentProjectDetails?.anchor}`; const issuesCount = getGroupIssueCount(undefined, undefined, false); - const canUserCreateIssue = - currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); + const canUserCreateIssue = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT + ); return (
diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/header.tsx index c3dc7d81f..59dcae31b 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/header.tsx @@ -21,7 +21,6 @@ import { EIssueLayoutTypes, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, } from "@/constants/issue"; -import { EUserProjectRoles } from "@/constants/project"; // helpers import { cn } from "@/helpers/common.helper"; import { isIssueFilterActive } from "@/helpers/filter.helper"; @@ -34,14 +33,15 @@ import { useModule, useProject, useProjectState, - useUser, useIssues, useCommandPalette, + useUserPermissions, } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; import { useIssuesActions } from "@/hooks/use-issues-actions"; import useLocalStorage from "@/hooks/use-local-storage"; import { usePlatformOS } from "@/hooks/use-platform-os"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; const ModuleDropdownOption: React.FC<{ moduleId: string }> = ({ moduleId }) => { // router @@ -83,9 +83,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => { const { projectModuleIds, getModuleById } = useModule(); const { toggleCreateIssueModal } = useCommandPalette(); const { setTrackElement } = useEventTracker(); - const { - membership: { currentProjectRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); const { currentProjectDetails, loader } = useProject(); const { projectLabels } = useLabel(); const { projectStates } = useProjectState(); @@ -149,8 +147,10 @@ export const ModuleIssuesHeader: React.FC = observer(() => { // derived values const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined; - const canUserCreateIssue = - currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); + const canUserCreateIssue = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT + ); const issuesCount = getGroupIssueCount(undefined, undefined, false); diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/header.tsx index ff268ee97..5190dcced 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/header.tsx @@ -7,11 +7,11 @@ import { Breadcrumbs, Button, DiceIcon, Header } from "@plane/ui"; // components import { BreadcrumbLink, Logo } from "@/components/common"; import { ModuleViewHeader } from "@/components/modules"; -// constants -import { EUserProjectRoles } from "@/constants/project"; // hooks -import { useCommandPalette, useEventTracker, useProject, useUser } from "@/hooks/store"; +import { useCommandPalette, useEventTracker, useProject, useUserPermissions } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; +// constants +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; export const ModulesListHeader: React.FC = observer(() => { // router @@ -20,14 +20,15 @@ export const ModulesListHeader: React.FC = observer(() => { // store hooks const { toggleCreateModuleModal } = useCommandPalette(); const { setTrackElement } = useEventTracker(); - const { - membership: { currentProjectRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); + const { currentProjectDetails, loader } = useProject(); // auth - const canUserCreateModule = - currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); + const canUserCreateModule = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT + ); return (
diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/header.tsx index 57a85eb79..82a063c16 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/header.tsx @@ -9,9 +9,9 @@ import { Breadcrumbs, Button, Header } from "@plane/ui"; import { BreadcrumbLink, Logo } from "@/components/common"; // constants import { EPageAccess } from "@/constants/page"; -import { EUserProjectRoles } from "@/constants/project"; // hooks -import { useCommandPalette, useEventTracker, useProject, useUser } from "@/hooks/store"; +import { useCommandPalette, useEventTracker, useProject, useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; export const PagesListHeader = observer(() => { // router @@ -20,14 +20,15 @@ export const PagesListHeader = observer(() => { const pageType = searchParams.get("type"); // store hooks const { toggleCreatePageModal } = useCommandPalette(); - const { - membership: { currentProjectRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); + const { currentProjectDetails, loader } = useProject(); const { setTrackElement } = useEventTracker(); - const canUserCreatePage = - currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); + const canUserCreatePage = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT + ); return (
diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/automations/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/automations/page.tsx index fcfcc1db5..141cc39fe 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/automations/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/automations/page.tsx @@ -11,18 +11,19 @@ import { NotAuthorizedView } from "@/components/auth-screens"; import { AutoArchiveAutomation, AutoCloseAutomation } from "@/components/automation"; import { PageHead } from "@/components/core"; // hooks -import { useProject, useUser } from "@/hooks/store"; +import { useProject, useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; const AutomationSettingsPage = observer(() => { // router const { workspaceSlug, projectId } = useParams(); // store hooks - const { - canPerformProjectAdminActions, - membership: { currentProjectRole }, - } = useUser(); + const { workspaceUserInfo, allowPermissions } = useUserPermissions(); const { currentProjectDetails: projectDetails, updateProject } = useProject(); + // derived values + const canPerformProjectAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT); + const handleChange = async (formData: Partial) => { if (!workspaceSlug || !projectId || !projectDetails) return; @@ -38,7 +39,7 @@ const AutomationSettingsPage = observer(() => { // derived values const pageTitle = projectDetails?.name ? `${projectDetails?.name} - Automations` : undefined; - if (currentProjectRole && !canPerformProjectAdminActions) { + if (workspaceUserInfo && !canPerformProjectAdminActions) { return ; } diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/estimates/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/estimates/page.tsx index 0823b9d08..14f770767 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/estimates/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/estimates/page.tsx @@ -7,22 +7,22 @@ import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; import { EstimateRoot } from "@/components/estimates"; // hooks -import { useUser, useProject } from "@/hooks/store"; +import { useProject, useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; const EstimatesSettingsPage = observer(() => { const { workspaceSlug, projectId } = useParams(); - const { - canPerformProjectAdminActions, - membership: { currentProjectRole }, - } = useUser(); + // store const { currentProjectDetails } = useProject(); + const { workspaceUserInfo, allowPermissions } = useUserPermissions(); // derived values const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Estimates` : undefined; + const canPerformProjectAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT); if (!workspaceSlug || !projectId) return <>; - if (currentProjectRole && !canPerformProjectAdminActions) { + if (workspaceUserInfo && !canPerformProjectAdminActions) { return ; } diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/features/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/features/page.tsx index 59df2b2eb..05bde8c9e 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/features/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/features/page.tsx @@ -7,22 +7,22 @@ import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; import { ProjectFeaturesList } from "@/components/project"; // hooks -import { useProject, useUser } from "@/hooks/store"; +import { useProject, useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; const FeaturesSettingsPage = observer(() => { const { workspaceSlug, projectId } = useParams(); // store - const { - canPerformProjectAdminActions, - membership: { currentProjectRole }, - } = useUser(); + const { workspaceUserInfo, allowPermissions } = useUserPermissions(); + const { currentProjectDetails } = useProject(); // derived values const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Features` : undefined; + const canPerformProjectAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT); if (!workspaceSlug || !projectId) return null; - if (currentProjectRole && !canPerformProjectAdminActions) { + if (workspaceUserInfo && !canPerformProjectAdminActions) { return ; } diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/header.tsx index ebf159f6b..041d2f34e 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/header.tsx @@ -9,25 +9,21 @@ import { Breadcrumbs, CustomMenu, Header } from "@plane/ui"; // components import { BreadcrumbLink, Logo } from "@/components/common"; // constants -import { EUserProjectRoles } from "@/constants/project"; // hooks -import { useProject, useUser } from "@/hooks/store"; +import { useProject, useUserPermissions } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; // plane web constants import { PROJECT_SETTINGS_LINKS } from "@/plane-web/constants/project"; +import { EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; export const ProjectSettingHeader: FC = observer(() => { // router const router = useAppRouter(); const { workspaceSlug, projectId } = useParams(); // store hooks - const { - membership: { currentProjectRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); const { currentProjectDetails, loader } = useProject(); - const projectMemberInfo = currentProjectRole || EUserProjectRoles.GUEST; - return (
@@ -74,7 +70,12 @@ export const ProjectSettingHeader: FC = observer(() => { > {PROJECT_SETTINGS_LINKS.map( (item) => - projectMemberInfo >= item.access && ( + allowPermissions( + item.access, + EUserPermissionsLevel.PROJECT, + workspaceSlug.toString(), + projectId.toString() + ) && ( router.push(`/${workspaceSlug}/projects/${projectId}${item.href}`)} diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/labels/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/labels/page.tsx index 13405e325..2705ff490 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/labels/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/labels/page.tsx @@ -9,19 +9,24 @@ import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; import { ProjectSettingsLabelList } from "@/components/labels"; // hooks -import { useProject, useUser } from "@/hooks/store"; +import { useProject, useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; const LabelsSettingsPage = observer(() => { // store hooks const { currentProjectDetails } = useProject(); - const { - canPerformProjectMemberActions, - membership: { currentProjectRole }, - } = useUser(); + const { workspaceUserInfo, allowPermissions } = useUserPermissions(); + const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Labels` : undefined; const scrollableContainerRef = useRef(null); + // derived values + const canPerformProjectMemberActions = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT + ); + // Enable Auto Scroll for Labels list useEffect(() => { const element = scrollableContainerRef.current; @@ -35,7 +40,7 @@ const LabelsSettingsPage = observer(() => { ); }, [scrollableContainerRef?.current]); - if (currentProjectRole && !canPerformProjectMemberActions) { + if (workspaceUserInfo && !canPerformProjectMemberActions) { return ; } diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/members/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/members/page.tsx index 3e5840cc6..0d05187b1 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/members/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/members/page.tsx @@ -6,19 +6,21 @@ import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; import { ProjectMemberList, ProjectSettingsMemberDefaults } from "@/components/project"; // hooks -import { useProject, useUser } from "@/hooks/store"; +import { useProject, useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; const MembersSettingsPage = observer(() => { // store const { currentProjectDetails } = useProject(); - const { - canPerformProjectViewerActions, - membership: { currentProjectRole }, - } = useUser(); + const { workspaceUserInfo, allowPermissions } = useUserPermissions(); // derived values const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Members` : undefined; + const canPerformProjectMemberActions = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT + ); - if (currentProjectRole && !canPerformProjectViewerActions) { + if (workspaceUserInfo && !canPerformProjectMemberActions) { return ; } diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/page.tsx index c0a018600..e21532c64 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/page.tsx @@ -15,7 +15,8 @@ import { ProjectDetailsFormLoader, } from "@/components/project"; // hooks -import { useProject } from "@/hooks/store"; +import { useProject, useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; const GeneralSettingsPage = observer(() => { // states @@ -25,6 +26,8 @@ const GeneralSettingsPage = observer(() => { const { workspaceSlug, projectId } = useParams(); // store hooks const { currentProjectDetails, fetchProjectDetails } = useProject(); + const { allowPermissions } = useUserPermissions(); + // api call to fetch project details // TODO: removed this API if not necessary const { isLoading } = useSWR( @@ -32,7 +35,13 @@ const GeneralSettingsPage = observer(() => { workspaceSlug && projectId ? () => fetchProjectDetails(workspaceSlug.toString(), projectId.toString()) : null ); // derived values - const isAdmin = currentProjectDetails?.member_role === 20; + const isAdmin = allowPermissions( + [EUserPermissions.ADMIN], + EUserPermissionsLevel.PROJECT, + workspaceSlug.toString(), + projectId.toString() + ); + const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - General Settings` : undefined; // const currentNetwork = NETWORK_CHOICES.find((n) => n.key === projectDetails?.network); // const selectedNetwork = NETWORK_CHOICES.find((n) => n.key === watch("network")); @@ -69,7 +78,7 @@ const GeneralSettingsPage = observer(() => { )} - {isAdmin && ( + {isAdmin && currentProjectDetails && ( <> { const { workspaceSlug, projectId } = useParams(); const pathname = usePathname(); // mobx store - const { - membership: { currentProjectRole }, - } = useUser(); + const { allowPermissions, projectUserInfo } = useUserPermissions(); - const projectMemberInfo = currentProjectRole || EUserProjectRoles.GUEST; + // derived values + const currentProjectRole = projectUserInfo?.[workspaceSlug?.toString()]?.[projectId?.toString()]?.role; if (!currentProjectRole) { return ( @@ -47,7 +45,12 @@ export const ProjectSettingsSidebar = observer(() => {
{PROJECT_SETTINGS_LINKS.map( (link) => - projectMemberInfo >= link.access && ( + allowPermissions( + link.access, + EUserPermissionsLevel.PROJECT, + workspaceSlug.toString(), + projectId.toString() + ) && ( { const { workspaceSlug, projectId } = useParams(); // store const { currentProjectDetails } = useProject(); - const { - canPerformProjectMemberActions, - membership: { currentProjectRole }, - } = useUser(); + const { workspaceUserInfo, allowPermissions } = useUserPermissions(); + // derived values const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - States` : undefined; + // derived values + const canPerformProjectMemberActions = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT + ); - if (currentProjectRole && !canPerformProjectMemberActions) { + if (workspaceUserInfo && !canPerformProjectMemberActions) { return ; } diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/[viewId]/header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/[viewId]/header.tsx index 5e83cd4dd..71f2d8df4 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/[viewId]/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/[viewId]/header.tsx @@ -19,7 +19,6 @@ import { EIssueLayoutTypes, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, } from "@/constants/issue"; -import { EUserProjectRoles } from "@/constants/project"; import { EViewAccess } from "@/constants/views"; // helpers import { isIssueFilterActive } from "@/helpers/filter.helper"; @@ -35,8 +34,9 @@ import { useProject, useProjectState, useProjectView, - useUser, + useUserPermissions, } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; export const ProjectViewIssuesHeader: React.FC = observer(() => { // router @@ -47,9 +47,8 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => { } = useIssues(EIssuesStoreType.PROJECT_VIEW); const { setTrackElement } = useEventTracker(); const { toggleCreateIssueModal } = useCommandPalette(); - const { - membership: { currentProjectRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); + const { currentProjectDetails, loader } = useProject(); const { projectViewIds, getViewById } = useProjectView(); const { projectStates } = useProjectState(); @@ -131,8 +130,10 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => { const viewDetails = viewId ? getViewById(viewId.toString()) : null; - const canUserCreateIssue = - currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); + const canUserCreateIssue = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT + ); const publishLink = getPublishViewLink(viewDetails?.anchor); return ( diff --git a/web/app/[workspaceSlug]/(projects)/settings/api-tokens/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/api-tokens/page.tsx index 427a7490c..fd2ed9669 100644 --- a/web/app/[workspaceSlug]/(projects)/settings/api-tokens/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/settings/api-tokens/page.tsx @@ -16,7 +16,8 @@ import { APITokenSettingsLoader } from "@/components/ui"; import { EmptyStateType } from "@/constants/empty-state"; import { API_TOKENS_LIST } from "@/constants/fetch-keys"; // store hooks -import { useUser, useWorkspace } from "@/hooks/store"; +import { useUserPermissions, useWorkspace } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; // services import { APITokenService } from "@/services/api_token.service"; @@ -28,11 +29,10 @@ const ApiTokensPage = observer(() => { // router const { workspaceSlug } = useParams(); // store hooks - const { - canPerformWorkspaceAdminActions, - membership: { currentWorkspaceRole }, - } = useUser(); const { currentWorkspace } = useWorkspace(); + const { workspaceUserInfo, allowPermissions } = useUserPermissions(); + // derived values + const canPerformWorkspaceAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); const { data: tokens } = useSWR( workspaceSlug && canPerformWorkspaceAdminActions ? API_TOKENS_LIST(workspaceSlug.toString()) : null, @@ -42,7 +42,7 @@ const ApiTokensPage = observer(() => { const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - API Tokens` : undefined; - if (currentWorkspaceRole && !canPerformWorkspaceAdminActions) { + if (workspaceUserInfo && !canPerformWorkspaceAdminActions) { return ; } diff --git a/web/app/[workspaceSlug]/(projects)/settings/billing/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/billing/page.tsx index 0158c3c98..03f0cce5e 100644 --- a/web/app/[workspaceSlug]/(projects)/settings/billing/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/settings/billing/page.tsx @@ -5,21 +5,20 @@ import { observer } from "mobx-react"; import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; // hooks -import { useUser, useWorkspace } from "@/hooks/store"; +import { useUserPermissions, useWorkspace } from "@/hooks/store"; // plane web components import { BillingRoot } from "@/plane-web/components/workspace"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; const BillingSettingsPage = observer(() => { // store hooks - const { - canPerformWorkspaceAdminActions, - membership: { currentWorkspaceRole }, - } = useUser(); + const { workspaceUserInfo, allowPermissions } = useUserPermissions(); const { currentWorkspace } = useWorkspace(); // derived values + const canPerformWorkspaceAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Billing & Plans` : undefined; - if (currentWorkspaceRole && !canPerformWorkspaceAdminActions) { + if (workspaceUserInfo && !canPerformWorkspaceAdminActions) { return ; } diff --git a/web/app/[workspaceSlug]/(projects)/settings/exports/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/exports/page.tsx index d14d9eb1f..dc3f3aafc 100644 --- a/web/app/[workspaceSlug]/(projects)/settings/exports/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/settings/exports/page.tsx @@ -8,22 +8,23 @@ import ExportGuide from "@/components/exporter/guide"; // helpers import { cn } from "@/helpers/common.helper"; // hooks -import { useUser, useWorkspace } from "@/hooks/store"; +import { useUserPermissions, useWorkspace } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; const ExportsPage = observer(() => { // store hooks - const { - canPerformWorkspaceViewerActions, - canPerformWorkspaceMemberActions, - membership: { currentWorkspaceRole }, - } = useUser(); + const { workspaceUserInfo, allowPermissions } = useUserPermissions(); const { currentWorkspace } = useWorkspace(); // derived values + const canPerformWorkspaceMemberActions = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Exports` : undefined; // if user is not authorized to view this page - if (currentWorkspaceRole && !canPerformWorkspaceViewerActions) { + if (workspaceUserInfo && !canPerformWorkspaceMemberActions) { return ; } diff --git a/web/app/[workspaceSlug]/(projects)/settings/imports/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/imports/page.tsx index a6f73e470..dc0815751 100644 --- a/web/app/[workspaceSlug]/(projects)/settings/imports/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/settings/imports/page.tsx @@ -4,20 +4,17 @@ import { observer } from "mobx-react"; // components import { PageHead } from "@/components/core"; import IntegrationGuide from "@/components/integration/guide"; -// constants -import { EUserWorkspaceRoles } from "@/constants/workspace"; // hooks -import { useUser, useWorkspace } from "@/hooks/store"; +import { useUserPermissions, useWorkspace } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; const ImportsPage = observer(() => { // store hooks - const { - membership: { currentWorkspaceRole }, - } = useUser(); const { currentWorkspace } = useWorkspace(); + const { allowPermissions } = useUserPermissions(); // derived values - const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN; + const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Imports` : undefined; if (!isAdmin) diff --git a/web/app/[workspaceSlug]/(projects)/settings/integrations/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/integrations/page.tsx index 4c6802e50..290eb24ca 100644 --- a/web/app/[workspaceSlug]/(projects)/settings/integrations/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/settings/integrations/page.tsx @@ -8,9 +8,9 @@ import { SingleIntegrationCard } from "@/components/integration"; import { IntegrationAndImportExportBanner, IntegrationsSettingsLoader } from "@/components/ui"; // constants import { APP_INTEGRATIONS } from "@/constants/fetch-keys"; -import { EUserWorkspaceRoles } from "@/constants/workspace"; // hooks -import { useUser, useWorkspace } from "@/hooks/store"; +import { useUserPermissions, useWorkspace } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; // services import { IntegrationService } from "@/services/integrations"; @@ -20,13 +20,11 @@ const WorkspaceIntegrationsPage = observer(() => { // router const { workspaceSlug } = useParams(); // store hooks - const { - membership: { currentWorkspaceRole }, - } = useUser(); const { currentWorkspace } = useWorkspace(); + const { allowPermissions } = useUserPermissions(); // derived values - const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN; + const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Integrations` : undefined; if (!isAdmin) diff --git a/web/app/[workspaceSlug]/(projects)/settings/members/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/members/page.tsx index 25ee83205..ab23261d9 100644 --- a/web/app/[workspaceSlug]/(projects)/settings/members/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/settings/members/page.tsx @@ -18,7 +18,8 @@ import { MEMBER_INVITED } from "@/constants/event-tracker"; import { cn } from "@/helpers/common.helper"; import { getUserRole } from "@/helpers/user.helper"; // hooks -import { useEventTracker, useMember, useUser, useWorkspace } from "@/hooks/store"; +import { useEventTracker, useMember, useUserPermissions, useWorkspace } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; const WorkspaceMembersSettingsPage = observer(() => { // states @@ -27,18 +28,20 @@ const WorkspaceMembersSettingsPage = observer(() => { // router const { workspaceSlug } = useParams(); // store hooks + const { workspaceUserInfo, allowPermissions } = useUserPermissions(); const { captureEvent } = useEventTracker(); - const { - canPerformWorkspaceAdminActions, - canPerformWorkspaceViewerActions, - canPerformWorkspaceMemberActions, - membership: { currentWorkspaceRole }, - } = useUser(); const { workspace: { inviteMembersToWorkspace }, } = useMember(); const { currentWorkspace } = useWorkspace(); + // derived values + const canPerformWorkspaceAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); + const canPerformWorkspaceMemberActions = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); + const handleWorkspaceInvite = (data: IWorkspaceBulkInviteFormData) => { if (!workspaceSlug) return; @@ -49,7 +52,7 @@ const WorkspaceMembersSettingsPage = observer(() => { emails: [ ...data.emails.map((email) => ({ email: email.email, - role: getUserRole(email.role), + role: getUserRole(email.role as unknown as EUserPermissions), })), ], project_id: undefined, @@ -67,7 +70,7 @@ const WorkspaceMembersSettingsPage = observer(() => { emails: [ ...data.emails.map((email) => ({ email: email.email, - role: getUserRole(email.role), + role: getUserRole(email.role as unknown as EUserPermissions), })), ], project_id: undefined, @@ -86,7 +89,7 @@ const WorkspaceMembersSettingsPage = observer(() => { const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Members` : undefined; // if user is not authorized to view this page - if (currentWorkspaceRole && !canPerformWorkspaceViewerActions) { + if (workspaceUserInfo && !canPerformWorkspaceMemberActions) { return ; } diff --git a/web/app/[workspaceSlug]/(projects)/settings/mobile-header-tabs.tsx b/web/app/[workspaceSlug]/(projects)/settings/mobile-header-tabs.tsx index 282acb82f..ee3035220 100644 --- a/web/app/[workspaceSlug]/(projects)/settings/mobile-header-tabs.tsx +++ b/web/app/[workspaceSlug]/(projects)/settings/mobile-header-tabs.tsx @@ -1,11 +1,10 @@ import { observer } from "mobx-react"; import { useParams, usePathname } from "next/navigation"; -// constants -import { EUserWorkspaceRoles } from "@/constants/workspace"; // hooks -import { useUser } from "@/hooks/store"; +import { useUserPermissions } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; // plane web constants +import { EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; import { WORKSPACE_SETTINGS_LINKS } from "@/plane-web/constants/workspace"; // plane web helpers import { shouldRenderSettingLink } from "@/plane-web/helpers/workspace.helper"; @@ -15,19 +14,14 @@ export const MobileWorkspaceSettingsTabs = observer(() => { const { workspaceSlug } = useParams(); const pathname = usePathname(); // mobx store - const { - membership: { currentWorkspaceRole }, - } = useUser(); - - // derived values - const workspaceMemberInfo = currentWorkspaceRole || EUserWorkspaceRoles.GUEST; + const { allowPermissions } = useUserPermissions(); return (
{WORKSPACE_SETTINGS_LINKS.map( (item, index) => shouldRenderSettingLink(item.key) && - workspaceMemberInfo >= item.access && ( + allowPermissions(item.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug.toString()) && (
{ const { workspaceSlug } = useParams(); const pathname = usePathname(); // mobx store - const { - membership: { currentWorkspaceRole }, - } = useUser(); - - // derived values - const workspaceMemberInfo = currentWorkspaceRole || EUserWorkspaceRoles.GUEST; + const { allowPermissions } = useUserPermissions(); return (
@@ -35,7 +29,7 @@ export const WorkspaceSettingsSidebar = observer(() => { {WORKSPACE_SETTINGS_LINKS.map( (link) => shouldRenderSettingLink(link.key) && - workspaceMemberInfo >= link.access && ( + allowPermissions(link.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug.toString()) && ( { // states @@ -20,18 +21,17 @@ const WebhookDetailsPage = observer(() => { // router const { workspaceSlug, webhookId } = useParams(); // mobx store - const { - membership: { currentWorkspaceRole }, - } = useUser(); const { currentWebhook, fetchWebhookById, updateWebhook } = useWebhook(); const { currentWorkspace } = useWorkspace(); + const { allowPermissions } = useUserPermissions(); // TODO: fix this error // useEffect(() => { // if (isCreated !== "true") clearSecretKey(); // }, [clearSecretKey, isCreated]); - const isAdmin = currentWorkspaceRole === 20; + // derived values + const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Webhook` : undefined; useSWR( diff --git a/web/app/[workspaceSlug]/(projects)/settings/webhooks/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/webhooks/page.tsx index 8d3884768..86c922f07 100644 --- a/web/app/[workspaceSlug]/(projects)/settings/webhooks/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/settings/webhooks/page.tsx @@ -15,7 +15,8 @@ import { WebhooksList, CreateWebhookModal } from "@/components/web-hooks"; // constants import { EmptyStateType } from "@/constants/empty-state"; // hooks -import { useUser, useWebhook, useWorkspace } from "@/hooks/store"; +import { useUserPermissions, useWebhook, useWorkspace } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; const WebhooksListPage = observer(() => { // states @@ -23,13 +24,13 @@ const WebhooksListPage = observer(() => { // router const { workspaceSlug } = useParams(); // mobx store - const { - canPerformWorkspaceAdminActions, - membership: { currentWorkspaceRole }, - } = useUser(); + const { workspaceUserInfo, allowPermissions } = useUserPermissions(); + const { fetchWebhooks, webhooks, clearSecretKey, webhookSecretKey, createWebhook } = useWebhook(); const { currentWorkspace } = useWorkspace(); + const canPerformWorkspaceAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); + useSWR( workspaceSlug && canPerformWorkspaceAdminActions ? `WEBHOOKS_LIST_${workspaceSlug}` : null, workspaceSlug && canPerformWorkspaceAdminActions ? () => fetchWebhooks(workspaceSlug.toString()) : null @@ -42,7 +43,7 @@ const WebhooksListPage = observer(() => { if (!showCreateWebhookModal && webhookSecretKey) clearSecretKey(); }, [showCreateWebhookModal, webhookSecretKey, clearSecretKey]); - if (currentWorkspaceRole && !canPerformWorkspaceAdminActions) { + if (workspaceUserInfo && !canPerformWorkspaceAdminActions) { return ; } diff --git a/web/app/[workspaceSlug]/(projects)/sidebar.tsx b/web/app/[workspaceSlug]/(projects)/sidebar.tsx index 349e8445c..c6ea85f31 100644 --- a/web/app/[workspaceSlug]/(projects)/sidebar.tsx +++ b/web/app/[workspaceSlug]/(projects)/sidebar.tsx @@ -13,22 +13,29 @@ import { import { SidebarFavoritesMenu } from "@/components/workspace/sidebar/favorites/favorites-menu"; import { cn } from "@/helpers/common.helper"; // hooks -import { useAppTheme, useUser } from "@/hooks/store"; +import { useAppTheme, useUserPermissions } from "@/hooks/store"; import useOutsideClickDetector from "@/hooks/use-outside-click-detector"; // plane web components import useSize from "@/hooks/use-window-size"; import { SidebarAppSwitcher } from "@/plane-web/components/sidebar"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; export interface IAppSidebar {} export const AppSidebar: FC = observer(() => { // store hooks - const { canPerformWorkspaceMemberActions } = useUser(); + const { allowPermissions } = useUserPermissions(); const { toggleSidebar, sidebarCollapsed } = useAppTheme(); const windowSize = useSize(); // refs const ref = useRef(null); + // derived values + const canPerformWorkspaceMemberActions = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); + useOutsideClickDetector(ref, () => { if (sidebarCollapsed === false) { if (window.innerWidth < 768) { diff --git a/web/app/invitations/page.tsx b/web/app/invitations/page.tsx index a56ff6ad4..580896bb5 100644 --- a/web/app/invitations/page.tsx +++ b/web/app/invitations/page.tsx @@ -27,6 +27,9 @@ import { useEventTracker, useUser, useUserProfile, useWorkspace } from "@/hooks/ import { useAppRouter } from "@/hooks/use-app-router"; // services import { AuthenticationWrapper } from "@/lib/wrappers"; +// plane web constants +import { EUserPermissions } from "@/plane-web/constants/user-permissions"; +// plane web services import { WorkspaceService } from "@/plane-web/services"; // images import emptyInvitation from "@/public/empty-state/invitation.svg"; @@ -88,7 +91,7 @@ const UserInvitationsPage = observer(() => { captureEvent(MEMBER_ACCEPTED, { member_id: invitation?.id, // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain - role: getUserRole(invitation?.role!), + role: getUserRole((invitation?.role as unknown as EUserPermissions)!), project_id: undefined, accepted_from: "App", state: "SUCCESS", diff --git a/web/ce/components/projects/settings/useProjectColumns.tsx b/web/ce/components/projects/settings/useProjectColumns.tsx index 7f450ae65..22e7c8fb7 100644 --- a/web/ce/components/projects/settings/useProjectColumns.tsx +++ b/web/ce/components/projects/settings/useProjectColumns.tsx @@ -1,14 +1,13 @@ import { useState } from "react"; import { useParams } from "next/navigation"; import { IWorkspaceMember } from "@plane/types"; -import { EUserProjectRoles } from "@plane/types/src/enums"; import { AccountTypeColumn, NameColumn } from "@/components/project/settings/member-columns"; -import { EUserWorkspaceRoles } from "@/constants/workspace"; -import { useUser } from "@/hooks/store"; +import { useUser, useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; export interface RowData { member: IWorkspaceMember; - role: EUserWorkspaceRoles; + role: EUserPermissions; } export const useProjectColumns = () => { @@ -17,10 +16,12 @@ export const useProjectColumns = () => { const { workspaceSlug, projectId } = useParams(); - const { - membership: { currentProjectRole }, - data: currentUser, - } = useUser(); + const { data: currentUser } = useUser(); + const { allowPermissions, projectUserInfo } = useUserPermissions(); + + const currentProjectRole = + (projectUserInfo?.[workspaceSlug.toString()]?.[projectId.toString()]?.role as unknown as EUserPermissions) ?? + EUserPermissions.GUEST; const getFormattedDate = (dateStr: string) => { const date = new Date(dateStr); @@ -29,7 +30,13 @@ export const useProjectColumns = () => { return date.toLocaleDateString("en-US", options); }; // derived values - const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN; + const isAdmin = allowPermissions( + [EUserPermissions.ADMIN], + EUserPermissionsLevel.PROJECT, + workspaceSlug.toString(), + projectId.toString() + ); + const columns = [ { key: "Full Name", diff --git a/web/ce/components/workspace/settings/useMemberColumns.tsx b/web/ce/components/workspace/settings/useMemberColumns.tsx index 3fc7cd4c2..a24d0e23c 100644 --- a/web/ce/components/workspace/settings/useMemberColumns.tsx +++ b/web/ce/components/workspace/settings/useMemberColumns.tsx @@ -1,8 +1,8 @@ import { useState } from "react"; import { useParams } from "next/navigation"; import { AccountTypeColumn, NameColumn, RowData } from "@/components/workspace/settings/member-columns"; -import { EUserWorkspaceRoles } from "@/constants/workspace"; -import { useUser } from "@/hooks/store"; +import { useUser, useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; export const useMemberColumns = () => { // states @@ -10,10 +10,8 @@ export const useMemberColumns = () => { const { workspaceSlug } = useParams(); - const { - membership: { currentWorkspaceRole }, - data: currentUser, - } = useUser(); + const { data: currentUser } = useUser(); + const { allowPermissions } = useUserPermissions(); const getFormattedDate = (dateStr: string) => { const date = new Date(dateStr); @@ -22,7 +20,8 @@ export const useMemberColumns = () => { return date.toLocaleDateString("en-US", options); }; - const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN; + // derived values + const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); const columns = [ { @@ -48,13 +47,7 @@ export const useMemberColumns = () => { { key: "Account Type", content: "Account Type", - tdRender: (rowData: RowData) => ( - - ), + tdRender: (rowData: RowData) => , }, { diff --git a/web/ce/constants/project/settings/tabs.ts b/web/ce/constants/project/settings/tabs.ts index d067fba92..4d9207cc6 100644 --- a/web/ce/constants/project/settings/tabs.ts +++ b/web/ce/constants/project/settings/tabs.ts @@ -3,14 +3,14 @@ import { SettingIcon } from "@/components/icons/attachment"; // types import { Props } from "@/components/icons/types"; // constants -import { EUserProjectRoles } from "@/constants/project"; +import { EUserPermissions } from "../../user-permissions"; export const PROJECT_SETTINGS = { general: { key: "general", label: "General", href: `/settings`, - access: EUserProjectRoles.GUEST, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/`, Icon: SettingIcon, }, @@ -18,7 +18,7 @@ export const PROJECT_SETTINGS = { key: "members", label: "Members", href: `/settings/members`, - access: EUserProjectRoles.VIEWER, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/members/`, Icon: SettingIcon, }, @@ -26,7 +26,7 @@ export const PROJECT_SETTINGS = { key: "features", label: "Features", href: `/settings/features`, - access: EUserProjectRoles.ADMIN, + access: [EUserPermissions.ADMIN], highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/features/`, Icon: SettingIcon, }, @@ -34,7 +34,7 @@ export const PROJECT_SETTINGS = { key: "states", label: "States", href: `/settings/states`, - access: EUserProjectRoles.MEMBER, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/states/`, Icon: SettingIcon, }, @@ -42,7 +42,7 @@ export const PROJECT_SETTINGS = { key: "labels", label: "Labels", href: `/settings/labels`, - access: EUserProjectRoles.MEMBER, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/labels/`, Icon: SettingIcon, }, @@ -50,7 +50,7 @@ export const PROJECT_SETTINGS = { key: "estimates", label: "Estimates", href: `/settings/estimates`, - access: EUserProjectRoles.ADMIN, + access: [EUserPermissions.ADMIN], highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/estimates/`, Icon: SettingIcon, }, @@ -58,7 +58,7 @@ export const PROJECT_SETTINGS = { key: "automations", label: "Automations", href: `/settings/automations`, - access: EUserProjectRoles.ADMIN, + access: [EUserPermissions.ADMIN], highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/automations/`, Icon: SettingIcon, }, @@ -68,7 +68,7 @@ export const PROJECT_SETTINGS_LINKS: { key: string; label: string; href: string; - access: EUserProjectRoles; + access: EUserPermissions[]; highlight: (pathname: string, baseUrl: string) => boolean; Icon: React.FC; }[] = [ diff --git a/web/ce/constants/user-permissions/index.ts b/web/ce/constants/user-permissions/index.ts new file mode 100644 index 000000000..e37a2aae9 --- /dev/null +++ b/web/ce/constants/user-permissions/index.ts @@ -0,0 +1,36 @@ +export enum EUserPermissionsLevel { + WORKSPACE = "WORKSPACE", + PROJECT = "PROJECT", +} +export type TUserPermissionsLevel = EUserPermissionsLevel; + +export enum EUserPermissions { + ADMIN = 20, + MEMBER = 15, + GUEST = 5, +} +export type TUserPermissions = EUserPermissions; + +export type TUserAllowedPermissionsObject = { + create: TUserPermissions[]; + update: TUserPermissions[]; + delete: TUserPermissions[]; + read: TUserPermissions[]; +}; +export type TUserAllowedPermissions = { + workspace: { + [key: string]: Partial; + }; + project: { + [key: string]: Partial; + }; +}; + +export const USER_ALLOWED_PERMISSIONS: TUserAllowedPermissions = { + workspace: { + dashboard: { + read: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], + }, + }, + project: {}, +}; diff --git a/web/ce/constants/workspace.ts b/web/ce/constants/workspace.ts index 109a7a4d1..51b876cfe 100644 --- a/web/ce/constants/workspace.ts +++ b/web/ce/constants/workspace.ts @@ -1,15 +1,15 @@ // icons import { SettingIcon } from "@/components/icons/attachment"; import { Props } from "@/components/icons/types"; +import { EUserPermissions } from "./user-permissions"; // constants -import { EUserWorkspaceRoles } from "@/constants/workspace"; export const WORKSPACE_SETTINGS = { general: { key: "general", label: "General", href: `/settings`, - access: EUserWorkspaceRoles.GUEST, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/`, Icon: SettingIcon, }, @@ -17,7 +17,7 @@ export const WORKSPACE_SETTINGS = { key: "members", label: "Members", href: `/settings/members`, - access: EUserWorkspaceRoles.VIEWER, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/members/`, Icon: SettingIcon, }, @@ -25,7 +25,7 @@ export const WORKSPACE_SETTINGS = { key: "billing-and-plans", label: "Billing and plans", href: `/settings/billing`, - access: EUserWorkspaceRoles.ADMIN, + access: [EUserPermissions.ADMIN], highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/billing/`, Icon: SettingIcon, }, @@ -33,7 +33,7 @@ export const WORKSPACE_SETTINGS = { key: "export", label: "Exports", href: `/settings/exports`, - access: EUserWorkspaceRoles.VIEWER, + access: [EUserPermissions.ADMIN], highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/exports/`, Icon: SettingIcon, }, @@ -41,7 +41,7 @@ export const WORKSPACE_SETTINGS = { key: "webhooks", label: "Webhooks", href: `/settings/webhooks`, - access: EUserWorkspaceRoles.ADMIN, + access: [EUserPermissions.ADMIN], highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/webhooks/`, Icon: SettingIcon, }, @@ -49,7 +49,7 @@ export const WORKSPACE_SETTINGS = { key: "api-tokens", label: "API tokens", href: `/settings/api-tokens`, - access: EUserWorkspaceRoles.ADMIN, + access: [EUserPermissions.ADMIN], highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/api-tokens/`, Icon: SettingIcon, }, @@ -59,7 +59,7 @@ export const WORKSPACE_SETTINGS_LINKS: { key: string; label: string; href: string; - access: EUserWorkspaceRoles; + access: EUserPermissions[]; highlight: (pathname: string, baseUrl: string) => boolean; Icon: React.FC; }[] = [ diff --git a/web/core/components/auth-screens/project/join-project.tsx b/web/core/components/auth-screens/project/join-project.tsx index 0056200ff..4ec94acab 100644 --- a/web/core/components/auth-screens/project/join-project.tsx +++ b/web/core/components/auth-screens/project/join-project.tsx @@ -5,7 +5,7 @@ import { useParams } from "next/navigation"; // hooks import { ClipboardList } from "lucide-react"; import { Button } from "@plane/ui"; -import { useProject, useUser } from "@/hooks/store"; +import { useProject, useUserPermissions } from "@/hooks/store"; // ui // icons // images @@ -15,9 +15,7 @@ export const JoinProject: React.FC = () => { // states const [isJoiningProject, setIsJoiningProject] = useState(false); // store hooks - const { - membership: { joinProject }, - } = useUser(); + const { joinProject } = useUserPermissions(); const { fetchProjects } = useProject(); const { workspaceSlug, projectId } = useParams(); @@ -27,7 +25,7 @@ export const JoinProject: React.FC = () => { setIsJoiningProject(true); - joinProject(workspaceSlug.toString(), [projectId.toString()]) + joinProject(workspaceSlug.toString(), projectId.toString()) .then(() => fetchProjects(workspaceSlug.toString())) .finally(() => setIsJoiningProject(false)); }; diff --git a/web/core/components/automation/auto-archive-automation.tsx b/web/core/components/automation/auto-archive-automation.tsx index 968511c1f..cb985b8ce 100644 --- a/web/core/components/automation/auto-archive-automation.tsx +++ b/web/core/components/automation/auto-archive-automation.tsx @@ -10,9 +10,10 @@ import { CustomSelect, Loader, ToggleSwitch } from "@plane/ui"; // component import { SelectMonthModal } from "@/components/automation"; // constants -import { EUserProjectRoles, PROJECT_AUTOMATION_MONTHS } from "@/constants/project"; +import { PROJECT_AUTOMATION_MONTHS } from "@/constants/project"; // hooks -import { useProject, useUser } from "@/hooks/store"; +import { useProject, useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; type Props = { handleChange: (formData: Partial) => Promise; @@ -25,12 +26,16 @@ export const AutoArchiveAutomation: React.FC = observer((props) => { // states const [monthModal, setmonthModal] = useState(false); // store hooks - const { - membership: { currentProjectRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); + const { currentProjectDetails } = useProject(); - const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN; + const isAdmin = allowPermissions( + [EUserPermissions.ADMIN], + EUserPermissionsLevel.PROJECT, + currentProjectDetails?.workspace_detail.slug, + currentProjectDetails?.id + ); return ( <> diff --git a/web/core/components/automation/auto-close-automation.tsx b/web/core/components/automation/auto-close-automation.tsx index 5b983e63c..8d33a1b86 100644 --- a/web/core/components/automation/auto-close-automation.tsx +++ b/web/core/components/automation/auto-close-automation.tsx @@ -11,9 +11,10 @@ import { CustomSelect, CustomSearchSelect, ToggleSwitch, StateGroupIcon, DoubleC // component import { SelectMonthModal } from "@/components/automation"; // constants -import { EUserProjectRoles, PROJECT_AUTOMATION_MONTHS } from "@/constants/project"; +import { PROJECT_AUTOMATION_MONTHS } from "@/constants/project"; // hooks -import { useProject, useProjectState, useUser } from "@/hooks/store"; +import { useProject, useProjectState, useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; type Props = { handleChange: (formData: Partial) => Promise; @@ -24,11 +25,9 @@ export const AutoCloseAutomation: React.FC = observer((props) => { // states const [monthModal, setmonthModal] = useState(false); // store hooks - const { - membership: { currentProjectRole }, - } = useUser(); const { currentProjectDetails } = useProject(); const { projectStates } = useProjectState(); + const { allowPermissions } = useUserPermissions(); // const stateGroups = projectStateStore.groupedProjectStates ?? undefined; @@ -57,7 +56,12 @@ export const AutoCloseAutomation: React.FC = observer((props) => { default_state: defaultState, }; - const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN; + const isAdmin = allowPermissions( + [EUserPermissions.ADMIN], + EUserPermissionsLevel.PROJECT, + currentProjectDetails?.workspace_detail?.slug, + currentProjectDetails?.id + ); return ( <> diff --git a/web/core/components/command-palette/actions/workspace-settings-actions.tsx b/web/core/components/command-palette/actions/workspace-settings-actions.tsx index c2094d296..dee51c1b6 100644 --- a/web/core/components/command-palette/actions/workspace-settings-actions.tsx +++ b/web/core/components/command-palette/actions/workspace-settings-actions.tsx @@ -4,12 +4,12 @@ import { Command } from "cmdk"; // hooks import Link from "next/link"; import { useParams } from "next/navigation"; -// constants -import { EUserWorkspaceRoles } from "@/constants/workspace"; + // hooks -import { useUser } from "@/hooks/store"; +import { useUserPermissions } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; // plane wev constants +import { EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; import { WORKSPACE_SETTINGS_LINKS } from "@/plane-web/constants/workspace"; // plane web helpers import { shouldRenderSettingLink } from "@/plane-web/helpers/workspace.helper"; @@ -25,11 +25,8 @@ export const CommandPaletteWorkspaceSettingsActions: React.FC = (props) = // router params const { workspaceSlug } = useParams(); // mobx store - const { - membership: { currentWorkspaceRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); // derived values - const workspaceMemberInfo = currentWorkspaceRole || EUserWorkspaceRoles.GUEST; const redirect = (path: string) => { closePalette(); @@ -40,7 +37,7 @@ export const CommandPaletteWorkspaceSettingsActions: React.FC = (props) = <> {WORKSPACE_SETTINGS_LINKS.map( (setting) => - workspaceMemberInfo >= setting.access && + allowPermissions(setting.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug.toString()) && shouldRenderSettingLink(setting.key) && ( { const { platform } = usePlatformOS(); const { data: currentUser, - canPerformProjectMemberActions, - canPerformWorkspaceMemberActions, + // canPerformProjectMemberActions, + // canPerformWorkspaceMemberActions, canPerformAnyCreateAction, - canPerformProjectAdminActions, + // canPerformProjectAdminActions, } = useUser(); const { issues: { removeIssue }, @@ -73,6 +74,7 @@ export const CommandPalette: FC = observer(() => { toggleDeleteIssueModal, isAnyModalOpen, } = useCommandPalette(); + const { allowPermissions } = useUserPermissions(); const { data: issueDetails } = useSWR( workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null, @@ -81,6 +83,17 @@ export const CommandPalette: FC = observer(() => { : null ); + // derived values + const canPerformWorkspaceMemberActions = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); + const canPerformProjectMemberActions = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT + ); + const canPerformProjectAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT); + const copyIssueUrlToClipboard = useCallback(() => { if (!issueId) return; diff --git a/web/core/components/cycles/analytics-sidebar/root.tsx b/web/core/components/cycles/analytics-sidebar/root.tsx index 08fb015f5..16a487504 100644 --- a/web/core/components/cycles/analytics-sidebar/root.tsx +++ b/web/core/components/cycles/analytics-sidebar/root.tsx @@ -17,16 +17,16 @@ import { DateRangeDropdown } from "@/components/dropdowns"; // constants import { CYCLE_STATUS } from "@/constants/cycle"; import { CYCLE_UPDATED } from "@/constants/event-tracker"; -import { EUserWorkspaceRoles } from "@/constants/workspace"; // helpers import { findHowManyDaysLeft, getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; import { copyUrlToClipboard } from "@/helpers/string.helper"; // hooks -import { useEventTracker, useCycle, useUser, useMember, useProjectEstimates } from "@/hooks/store"; +import { useEventTracker, useCycle, useMember, useProjectEstimates, useUserPermissions } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; // plane web constants import { EEstimateSystem } from "@/plane-web/constants/estimates"; // services +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; import { CycleService } from "@/services/cycle.service"; type Props = { @@ -55,9 +55,8 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { // store hooks const { setTrackElement, captureCycleEvent } = useEventTracker(); const { areEstimateEnabledByProjectId, currentActiveEstimateId, estimateById } = useProjectEstimates(); - const { - membership: { currentProjectRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); + const { getCycleById, updateCycleDetails, restoreCycle } = useCycle(); const { getUserDetails } = useMember(); // derived values @@ -236,7 +235,10 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { const daysLeft = findHowManyDaysLeft(cycleDetails.end_date); - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; + const isEditingAllowed = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT + ); return (
diff --git a/web/core/components/cycles/applied-filters/root.tsx b/web/core/components/cycles/applied-filters/root.tsx index b86180db0..9fa74c393 100644 --- a/web/core/components/cycles/applied-filters/root.tsx +++ b/web/core/components/cycles/applied-filters/root.tsx @@ -4,9 +4,10 @@ import { TCycleFilters } from "@plane/types"; // hooks import { Tag } from "@plane/ui"; import { AppliedDateFilters, AppliedStatusFilters } from "@/components/cycles"; -import { EUserProjectRoles } from "@/constants/project"; import { replaceUnderscoreIfSnakeCase } from "@/helpers/string.helper"; -import { useUser } from "@/hooks/store"; +import { useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; + // components // helpers // types @@ -24,15 +25,15 @@ const DATE_FILTERS = ["start_date", "end_date"]; export const CycleAppliedFiltersList: React.FC = observer((props) => { const { appliedFilters, handleClearAllFilters, handleRemoveFilter, alwaysAllowEditing } = props; // store hooks - const { - membership: { currentProjectRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); if (!appliedFilters) return null; if (Object.keys(appliedFilters).length === 0) return null; - const isEditingAllowed = alwaysAllowEditing || (currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER); + const isEditingAllowed = + alwaysAllowEditing || + allowPermissions([EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.PROJECT); return (
diff --git a/web/core/components/cycles/board/cycles-board-card.tsx b/web/core/components/cycles/board/cycles-board-card.tsx index 5c7730560..1f755089a 100644 --- a/web/core/components/cycles/board/cycles-board-card.tsx +++ b/web/core/components/cycles/board/cycles-board-card.tsx @@ -14,14 +14,14 @@ import { CycleQuickActions } from "@/components/cycles"; // constants import { CYCLE_STATUS } from "@/constants/cycle"; import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "@/constants/event-tracker"; -import { EUserWorkspaceRoles } from "@/constants/workspace"; // helpers import { findHowManyDaysLeft, getDate, renderFormattedDate } from "@/helpers/date-time.helper"; import { generateQueryParams } from "@/helpers/router.helper"; // hooks -import { useEventTracker, useCycle, useUser, useMember } from "@/hooks/store"; +import { useEventTracker, useCycle, useMember, useUserPermissions } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; import { usePlatformOS } from "@/hooks/use-platform-os"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; export interface ICyclesBoardCard { workspaceSlug: string; @@ -39,9 +39,8 @@ export const CyclesBoardCard: FC = observer((props) => { const pathname = usePathname(); // store const { captureEvent } = useEventTracker(); - const { - membership: { currentProjectRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); + const { addCycleToFavorites, removeCycleFromFavorites, getCycleById } = useCycle(); const { getUserDetails } = useMember(); // computed @@ -57,7 +56,10 @@ export const CyclesBoardCard: FC = observer((props) => { const startDate = getDate(cycleDetails.start_date); const isDateValid = cycleDetails.start_date || cycleDetails.end_date; - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; + const isEditingAllowed = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT + ); const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus); diff --git a/web/core/components/cycles/list/cycle-list-item-action.tsx b/web/core/components/cycles/list/cycle-list-item-action.tsx index 89ac1bdc7..e00f29ad0 100644 --- a/web/core/components/cycles/list/cycle-list-item-action.tsx +++ b/web/core/components/cycles/list/cycle-list-item-action.tsx @@ -15,13 +15,14 @@ import { ButtonAvatars } from "@/components/dropdowns/member/avatar"; // constants import { CYCLE_STATUS } from "@/constants/cycle"; import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "@/constants/event-tracker"; -import { EUserProjectRoles } from "@/constants/project"; // helpers import { findHowManyDaysLeft, getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; // hooks -import { useCycle, useEventTracker, useMember, useUser } from "@/hooks/store"; +import { useCycle, useEventTracker, useMember, useUserPermissions } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; import { CycleService } from "@/services/cycle.service"; + const cycleService = new CycleService(); type Props = { @@ -44,9 +45,8 @@ export const CycleListItemAction: FC = observer((props) => { // store hooks const { addCycleToFavorites, removeCycleFromFavorites, updateCycleDetails } = useCycle(); const { captureEvent } = useEventTracker(); - const { - membership: { currentProjectRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); + const { getUserDetails } = useMember(); // form @@ -56,7 +56,10 @@ export const CycleListItemAction: FC = observer((props) => { // derived values const cycleStatus = cycleDetails.status ? (cycleDetails.status.toLocaleLowerCase() as TCycleGroups) : "draft"; - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + const isEditingAllowed = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT + ); const renderIcon = Boolean(cycleDetails.start_date) || Boolean(cycleDetails.end_date); const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus); const daysLeft = findHowManyDaysLeft(cycleDetails.end_date) ?? 0; diff --git a/web/core/components/cycles/quick-actions.tsx b/web/core/components/cycles/quick-actions.tsx index a84d1e790..6d6e9ed15 100644 --- a/web/core/components/cycles/quick-actions.tsx +++ b/web/core/components/cycles/quick-actions.tsx @@ -9,14 +9,13 @@ import { ArchiveRestoreIcon, ExternalLink, LinkIcon, Pencil, Trash2 } from "luci import { ArchiveIcon, ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui"; // components import { ArchiveCycleModal, CycleCreateUpdateModal, CycleDeleteModal } from "@/components/cycles"; -// constants -import { EUserProjectRoles } from "@/constants/project"; // helpers import { cn } from "@/helpers/common.helper"; import { copyUrlToClipboard } from "@/helpers/string.helper"; // hooks -import { useCycle, useEventTracker, useUser } from "@/hooks/store"; +import { useCycle, useEventTracker, useUserPermissions } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; type Props = { parentRef: React.RefObject; @@ -35,17 +34,19 @@ export const CycleQuickActions: React.FC = observer((props) => { const [deleteModal, setDeleteModal] = useState(false); // store hooks const { setTrackElement } = useEventTracker(); - const { - membership: { currentWorkspaceAllProjectsRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); const { getCycleById, restoreCycle } = useCycle(); // derived values const cycleDetails = getCycleById(cycleId); const isArchived = !!cycleDetails?.archived_at; const isCompleted = cycleDetails?.status?.toLowerCase() === "completed"; // auth - const isEditingAllowed = - !!currentWorkspaceAllProjectsRole && currentWorkspaceAllProjectsRole[projectId] >= EUserProjectRoles.MEMBER; + const isEditingAllowed = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT, + workspaceSlug, + projectId + ); const cycleLink = `${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`; const handleCopyText = () => diff --git a/web/core/components/dashboard/project-empty-state.tsx b/web/core/components/dashboard/project-empty-state.tsx index a42f5a5b0..c8cba817a 100644 --- a/web/core/components/dashboard/project-empty-state.tsx +++ b/web/core/components/dashboard/project-empty-state.tsx @@ -4,10 +4,9 @@ import { observer } from "mobx-react"; import Image from "next/image"; // ui import { Button } from "@plane/ui"; -// constants -import { EUserWorkspaceRoles } from "@/constants/workspace"; // hooks -import { useCommandPalette, useEventTracker, useUser } from "@/hooks/store"; +import { useCommandPalette, useEventTracker, useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; // assets import ProjectEmptyStateImage from "@/public/empty-state/onboarding/dashboard-light.webp"; @@ -15,11 +14,10 @@ export const DashboardProjectEmptyState = observer(() => { // store hooks const { toggleCreateProjectModal } = useCommandPalette(); const { setTrackElement } = useEventTracker(); - const { - membership: { currentWorkspaceRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); + // derived values - const canCreateProject = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN; + const canCreateProject = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); return (
diff --git a/web/core/components/dashboard/widgets/recent-projects.tsx b/web/core/components/dashboard/widgets/recent-projects.tsx index d9970e11d..a390f3ac2 100644 --- a/web/core/components/dashboard/widgets/recent-projects.tsx +++ b/web/core/components/dashboard/widgets/recent-projects.tsx @@ -14,9 +14,9 @@ import { Logo } from "@/components/common"; import { WidgetLoader, WidgetProps } from "@/components/dashboard/widgets"; // constants import { PROJECT_BACKGROUND_COLORS } from "@/constants/dashboard"; -import { EUserWorkspaceRoles } from "@/constants/workspace"; // hooks -import { useEventTracker, useDashboard, useProject, useUser, useCommandPalette } from "@/hooks/store"; +import { useEventTracker, useDashboard, useProject, useCommandPalette, useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; const WIDGET_KEY = "recent_projects"; @@ -65,13 +65,14 @@ export const RecentProjectsWidget: React.FC = observer((props) => { // store hooks const { toggleCreateProjectModal } = useCommandPalette(); const { setTrackElement } = useEventTracker(); - const { - membership: { currentWorkspaceRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); const { fetchWidgetStats, getWidgetStats } = useDashboard(); // derived values const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); - const canCreateProject = currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; + const canCreateProject = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); useEffect(() => { fetchWidgetStats(workspaceSlug, dashboardId, { diff --git a/web/core/components/empty-state/empty-state.tsx b/web/core/components/empty-state/empty-state.tsx index 64bf248d8..883faab38 100644 --- a/web/core/components/empty-state/empty-state.tsx +++ b/web/core/components/empty-state/empty-state.tsx @@ -13,7 +13,8 @@ import { Button, TButtonVariant } from "@plane/ui"; import { EMPTY_STATE_DETAILS, EmptyStateType } from "@/constants/empty-state"; // helpers import { cn } from "@/helpers/common.helper"; -import { useUser } from "@/hooks/store"; +import { useUserPermissions } from "@/hooks/store"; +import { EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; import { ComicBoxButton } from "./comic-box-button"; export type EmptyStateProps = { @@ -37,9 +38,7 @@ export const EmptyState: React.FC = observer((props) => { secondaryButtonOnClick, } = props; // store - const { - membership: { currentWorkspaceRole, currentProjectRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); // theme const { resolvedTheme } = useTheme(); @@ -53,10 +52,14 @@ export const EmptyState: React.FC = observer((props) => { const resolvedEmptyStatePath = `${additionalPath && additionalPath !== "" ? `${path}${additionalPath}` : path}-${ resolvedTheme === "light" ? "light" : "dark" }.webp`; - // current access type - const currentAccessType = accessType === "workspace" ? currentWorkspaceRole : currentProjectRole; // permission - const isEditingAllowed = currentAccessType && access && currentAccessType >= access; + const isEditingAllowed = + access && + accessType && + allowPermissions( + access, + accessType === "workspace" ? EUserPermissionsLevel.WORKSPACE : EUserPermissionsLevel.PROJECT + ); const anyButton = primaryButton || secondaryButton; // primary button diff --git a/web/core/components/exporter/guide.tsx b/web/core/components/exporter/guide.tsx index 628c1f275..e5386d219 100644 --- a/web/core/components/exporter/guide.tsx +++ b/web/core/components/exporter/guide.tsx @@ -17,10 +17,11 @@ import { ImportExportSettingsLoader } from "@/components/ui"; // constants import { EmptyStateType } from "@/constants/empty-state"; import { EXPORT_SERVICES_LIST } from "@/constants/fetch-keys"; -import { EUserWorkspaceRoles, EXPORTERS_LIST } from "@/constants/workspace"; +import { EXPORTERS_LIST } from "@/constants/workspace"; // hooks -import { useProject, useUser } from "@/hooks/store"; +import { useProject, useUser, useUserPermissions } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; // services import { IntegrationService } from "@/services/integrations"; @@ -37,11 +38,9 @@ const IntegrationGuide = observer(() => { const searchParams = useSearchParams(); const provider = searchParams.get("provider"); // store hooks - const { - data: currentUser, - canPerformAnyCreateAction, - membership: { currentWorkspaceRole }, - } = useUser(); + const { data: currentUser, canPerformAnyCreateAction } = useUser(); + const { allowPermissions } = useUserPermissions(); + const { workspaceProjectIds } = useProject(); const { data: exporterServices } = useSWR( @@ -61,7 +60,7 @@ const IntegrationGuide = observer(() => { }; const hasProjects = workspaceProjectIds && workspaceProjectIds.length > 0; - const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN; + const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); useEffect(() => { const interval = setInterval(() => { diff --git a/web/core/components/inbox/content/inbox-issue-header.tsx b/web/core/components/inbox/content/inbox-issue-header.tsx index 25ca47106..0f6fe3600 100644 --- a/web/core/components/inbox/content/inbox-issue-header.tsx +++ b/web/core/components/inbox/content/inbox-issue-header.tsx @@ -27,15 +27,14 @@ import { SelectDuplicateInboxIssueModal, } from "@/components/inbox"; import { IssueUpdateStatus } from "@/components/issues"; -// constants -import { EUserProjectRoles } from "@/constants/project"; // helpers import { findHowManyDaysLeft } from "@/helpers/date-time.helper"; import { EInboxIssueStatus } from "@/helpers/inbox.helper"; import { copyUrlToClipboard } from "@/helpers/string.helper"; // hooks -import { useUser, useProjectInbox, useProject } from "@/hooks/store"; +import { useUser, useProjectInbox, useProject, useUserPermissions } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; // store types import type { IInboxIssueStore } from "@/store/inbox/inbox-issue.store"; @@ -70,23 +69,26 @@ export const InboxIssueActionsHeader: FC = observer((p // store const { currentTab, deleteInboxIssue, filteredInboxIssueIds } = useProjectInbox(); const { data: currentUser } = useUser(); - const { - membership: { currentProjectRoleByProjectId }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); const router = useAppRouter(); const { getProjectById } = useProject(); const issue = inboxIssue?.issue; // derived values - const currentProjectRole = currentProjectRoleByProjectId(projectId) || undefined; - const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + const isAllowed = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT, + workspaceSlug, + projectId + ); const canMarkAsDuplicate = isAllowed && (inboxIssue?.status === 0 || inboxIssue?.status === -2); const canMarkAsAccepted = isAllowed && (inboxIssue?.status === 0 || inboxIssue?.status === -2); const canMarkAsDeclined = isAllowed && (inboxIssue?.status === 0 || inboxIssue?.status === -2); // can delete only if admin or is creator of the issue const canDelete = - (!!currentProjectRole && currentProjectRole >= EUserProjectRoles.ADMIN) || issue?.created_by === currentUser?.id; + allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT, workspaceSlug, projectId) || + issue?.created_by === currentUser?.id; const isAcceptedOrDeclined = inboxIssue?.status ? [-1, 1, 2].includes(inboxIssue.status) : undefined; // days left for snooze const numberOfDaysLeft = findHowManyDaysLeft(inboxIssue?.snoozed_till); diff --git a/web/core/components/inbox/content/root.tsx b/web/core/components/inbox/content/root.tsx index 2278a88b0..cf6cbd7a1 100644 --- a/web/core/components/inbox/content/root.tsx +++ b/web/core/components/inbox/content/root.tsx @@ -4,11 +4,10 @@ import useSWR from "swr"; // components import { ContentWrapper } from "@plane/ui"; import { InboxIssueActionsHeader, InboxIssueMainContent } from "@/components/inbox"; -// constants -import { EUserProjectRoles } from "@/constants/project"; // hooks -import { useProjectInbox, useUser } from "@/hooks/store"; +import { useProjectInbox, useUserPermissions } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; type TInboxContentRoot = { workspaceSlug: string; @@ -37,9 +36,8 @@ export const InboxContentRoot: FC = observer((props) => { // hooks const { currentTab, fetchInboxIssueById, getIssueInboxByIssueId, getIsIssueAvailable } = useProjectInbox(); const inboxIssue = getIssueInboxByIssueId(inboxIssueId); - const { - membership: { currentProjectRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); + // derived values const isIssueAvailable = getIsIssueAvailable(inboxIssueId?.toString() || ""); @@ -63,7 +61,7 @@ export const InboxContentRoot: FC = observer((props) => { } ); - const isEditable = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + const isEditable = allowPermissions([EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.PROJECT); if (!inboxIssue) return <>; diff --git a/web/core/components/integration/single-integration-card.tsx b/web/core/components/integration/single-integration-card.tsx index 5159c4fc2..25392d113 100644 --- a/web/core/components/integration/single-integration-card.tsx +++ b/web/core/components/integration/single-integration-card.tsx @@ -12,9 +12,10 @@ import { Button, Loader, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // constants import { WORKSPACE_INTEGRATIONS } from "@/constants/fetch-keys"; // hooks -import { useUser, useInstance } from "@/hooks/store"; +import { useInstance, useUserPermissions } from "@/hooks/store"; import useIntegrationPopup from "@/hooks/use-integration-popup"; import { usePlatformOS } from "@/hooks/use-platform-os"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; // services // icons import GithubLogo from "@/public/services/github.png"; @@ -48,11 +49,9 @@ export const SingleIntegrationCard: React.FC = observer(({ integration }) const { workspaceSlug } = useParams(); // store hooks const { config } = useInstance(); - const { - membership: { currentWorkspaceRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); - const isUserAdmin = currentWorkspaceRole === 20; + const isUserAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); const { isMobile } = usePlatformOS(); const { startAuth, isConnecting: isInstalling } = useIntegrationPopup({ provider: integration.provider, diff --git a/web/core/components/issues/delete-issue-modal.tsx b/web/core/components/issues/delete-issue-modal.tsx index e6109a028..2f61495ed 100644 --- a/web/core/components/issues/delete-issue-modal.tsx +++ b/web/core/components/issues/delete-issue-modal.tsx @@ -8,8 +8,8 @@ import { AlertModalCore, TOAST_TYPE, setToast } from "@plane/ui"; // constants import { PROJECT_ERROR_MESSAGES } from "@/constants/project"; // hooks -import { useIssues, useProject, useUser } from "@/hooks/store"; - +import { useIssues, useProject, useUser, useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; type Props = { isOpen: boolean; handleClose: () => void; @@ -26,7 +26,12 @@ export const DeleteIssueModal: React.FC = (props) => { // store hooks const { issueMap } = useIssues(); const { getProjectById } = useProject(); - const { data: currentUser, canPerformProjectAdminActions } = useUser(); + const { allowPermissions } = useUserPermissions(); + + const { data: currentUser } = useUser(); + + // derived values + const canPerformProjectAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT); useEffect(() => { setIsDeleting(false); diff --git a/web/core/components/issues/issue-detail/issue-detail-quick-actions.tsx b/web/core/components/issues/issue-detail/issue-detail-quick-actions.tsx index be6b56770..e0dedfaa5 100644 --- a/web/core/components/issues/issue-detail/issue-detail-quick-actions.tsx +++ b/web/core/components/issues/issue-detail/issue-detail-quick-actions.tsx @@ -10,15 +10,22 @@ import { ArchiveIssueModal, DeleteIssueModal, IssueSubscription } from "@/compon // constants import { ISSUE_ARCHIVED, ISSUE_DELETED } from "@/constants/event-tracker"; import { EIssuesStoreType } from "@/constants/issue"; -import { EUserProjectRoles } from "@/constants/project"; import { ARCHIVABLE_STATE_GROUPS } from "@/constants/state"; // helpers import { cn } from "@/helpers/common.helper"; import { copyTextToClipboard } from "@/helpers/string.helper"; // hooks -import { useEventTracker, useIssueDetail, useIssues, useProjectState, useUser } from "@/hooks/store"; +import { + useEventTracker, + useIssueDetail, + useIssues, + useProjectState, + useUser, + useUserPermissions, +} from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; import { usePlatformOS } from "@/hooks/use-platform-os"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; type Props = { workspaceSlug: string; @@ -38,10 +45,8 @@ export const IssueDetailQuickActions: FC = observer((props) => { const router = useAppRouter(); // hooks - const { - data: currentUser, - membership: { currentProjectRole }, - } = useUser(); + const { data: currentUser } = useUser(); + const { allowPermissions } = useUserPermissions(); const { isMobile } = usePlatformOS(); const { getStateById } = useProjectState(); const { @@ -149,8 +154,11 @@ export const IssueDetailQuickActions: FC = observer((props) => { }; // auth - const isEditable = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - const canRestoreIssue = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + const isEditable = allowPermissions([EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.PROJECT); + const canRestoreIssue = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT + ); const isArchivingAllowed = !issue?.archived_at && isEditable; const isInArchivableGroup = !!stateDetails && ARCHIVABLE_STATE_GROUPS.includes(stateDetails?.group); diff --git a/web/core/components/issues/issue-detail/root.tsx b/web/core/components/issues/issue-detail/root.tsx index e520da2ac..0e6a962df 100644 --- a/web/core/components/issues/issue-detail/root.tsx +++ b/web/core/components/issues/issue-detail/root.tsx @@ -13,10 +13,10 @@ import { IssuePeekOverview } from "@/components/issues"; // constants import { ISSUE_UPDATED, ISSUE_DELETED, ISSUE_ARCHIVED } from "@/constants/event-tracker"; import { EIssuesStoreType } from "@/constants/issue"; -import { EUserProjectRoles } from "@/constants/project"; // hooks -import { useAppTheme, useEventTracker, useIssueDetail, useIssues, useUser } from "@/hooks/store"; +import { useAppTheme, useEventTracker, useIssueDetail, useIssues, useUserPermissions } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; // images import emptyIssue from "@/public/empty-state/issue.svg"; // local components @@ -77,9 +77,7 @@ export const IssueDetailRoot: FC = observer((props) => { issues: { removeIssue: removeArchivedIssue }, } = useIssues(EIssuesStoreType.ARCHIVED); const { captureIssueEvent } = useEventTracker(); - const { - membership: { currentProjectRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); const { issueDetailSidebarCollapsed } = useAppTheme(); const issueOperations: TIssueOperations = useMemo( @@ -332,7 +330,7 @@ export const IssueDetailRoot: FC = observer((props) => { // issue details const issue = getIssueById(issueId); // checking if issue is editable, based on user role - const isEditable = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + const isEditable = allowPermissions([EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.PROJECT); return ( <> diff --git a/web/core/components/issues/issue-layouts/calendar/base-calendar-root.tsx b/web/core/components/issues/issue-layouts/calendar/base-calendar-root.tsx index 063904430..288a3a2ec 100644 --- a/web/core/components/issues/issue-layouts/calendar/base-calendar-root.tsx +++ b/web/core/components/issues/issue-layouts/calendar/base-calendar-root.tsx @@ -10,11 +10,11 @@ import { TOAST_TYPE, setToast } from "@plane/ui"; import { CalendarChart } from "@/components/issues"; //constants import { EIssuesStoreType } from "@/constants/issue"; -import { EUserProjectRoles } from "@/constants/project"; // hooks -import { useIssues, useUser, useCalendarView } from "@/hooks/store"; +import { useIssues, useCalendarView, useUserPermissions } from "@/hooks/store"; import { useIssueStoreType } from "@/hooks/use-issue-layout-store"; import { useIssuesActions } from "@/hooks/use-issues-actions"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; // types import { IQuickActionProps } from "../list/list-view-types"; import { handleDragDrop } from "./utils"; @@ -40,9 +40,7 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { // hooks const storeType = useIssueStoreType() as CalendarStoreType; - const { - membership: { currentProjectRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); const { issues, issuesFilter, issueMap } = useIssues(storeType); const { fetchIssues, @@ -58,7 +56,10 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { const issueCalendarView = useCalendarView(); - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + const isEditingAllowed = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT + ); const displayFilters = issuesFilter.issueFilters?.displayFilters; diff --git a/web/core/components/issues/issue-layouts/calendar/calendar.tsx b/web/core/components/issues/issue-layouts/calendar/calendar.tsx index 1a17edbd0..9bac26879 100644 --- a/web/core/components/issues/issue-layouts/calendar/calendar.tsx +++ b/web/core/components/issues/issue-layouts/calendar/calendar.tsx @@ -22,13 +22,13 @@ import { CalendarHeader, CalendarIssueBlocks, CalendarWeekDays, CalendarWeekHead // constants import { MONTHS_LIST } from "@/constants/calendar"; import { EIssueFilterType, EIssueLayoutTypes, EIssuesStoreType } from "@/constants/issue"; -import { EUserProjectRoles } from "@/constants/project"; // helpers import { cn } from "@/helpers/common.helper"; import { renderFormattedPayloadDate } from "@/helpers/date-time.helper"; // hooks -import { useIssues, useUser } from "@/hooks/store"; +import { useIssues, useUserPermissions } from "@/hooks/store"; import useSize from "@/hooks/use-window-size"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; // store import { ICycleIssuesFilter } from "@/store/issue/cycle"; import { ICalendarStore } from "@/store/issue/issue_calendar_view.store"; @@ -91,14 +91,15 @@ export const CalendarChart: React.FC = observer((props) => { const { issues: { viewFlags }, } = useIssues(EIssuesStoreType.PROJECT); + const { allowPermissions } = useUserPermissions(); - const { - membership: { currentProjectRole }, - } = useUser(); const [windowWidth] = useSize(); const { enableIssueCreation } = viewFlags || {}; - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + const isEditingAllowed = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT + ); const calendarPayload = issueCalendarView.calendarPayload; diff --git a/web/core/components/issues/issue-layouts/empty-states/project-view.tsx b/web/core/components/issues/issue-layouts/empty-states/project-view.tsx index d6c6d52c0..160834ea7 100644 --- a/web/core/components/issues/issue-layouts/empty-states/project-view.tsx +++ b/web/core/components/issues/issue-layouts/empty-states/project-view.tsx @@ -4,9 +4,9 @@ import { PlusIcon } from "lucide-react"; import { EmptyState } from "@/components/common"; // constants import { EIssuesStoreType } from "@/constants/issue"; -import { EUserProjectRoles } from "@/constants/project"; // hooks -import { useCommandPalette, useEventTracker, useUser } from "@/hooks/store"; +import { useCommandPalette, useEventTracker, useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; // assets import emptyIssue from "@/public/empty-state/issue.svg"; @@ -14,11 +14,13 @@ export const ProjectViewEmptyState: React.FC = observer(() => { // store hooks const { toggleCreateIssueModal } = useCommandPalette(); const { setTrackElement } = useEventTracker(); - const { - membership: { currentProjectRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); + // auth - const isCreatingIssueAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + const isCreatingIssueAllowed = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT + ); return (
diff --git a/web/core/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx b/web/core/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx index c7514cf2d..268358efe 100644 --- a/web/core/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx +++ b/web/core/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx @@ -16,13 +16,13 @@ import { AppliedStateGroupFilters, } from "@/components/issues"; // constants -import { EUserProjectRoles } from "@/constants/project"; // helpers import { replaceUnderscoreIfSnakeCase } from "@/helpers/string.helper"; // hooks -import { useUser } from "@/hooks/store"; +import { useUserPermissions } from "@/hooks/store"; // plane web components import { AppliedIssueTypeFilters } from "@/plane-web/components/issues"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; type Props = { appliedFilters: IIssueFilterOptions; @@ -48,16 +48,16 @@ export const AppliedFiltersList: React.FC = observer((props) => { disableEditing = false, } = props; // store hooks - const { - membership: { currentProjectRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); if (!appliedFilters) return null; if (Object.keys(appliedFilters).length === 0) return null; const isEditingAllowed = - !disableEditing && (alwaysAllowEditing || (currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER)); + !disableEditing && + (alwaysAllowEditing || + allowPermissions([EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.PROJECT)); return (
diff --git a/web/core/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx b/web/core/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx index 3c6a0662f..7b3e8669e 100644 --- a/web/core/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx +++ b/web/core/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx @@ -17,9 +17,10 @@ import { CreateUpdateWorkspaceViewModal } from "@/components/workspace"; import { GLOBAL_VIEW_UPDATED } from "@/constants/event-tracker"; import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue"; import { EViewAccess } from "@/constants/views"; -import { DEFAULT_GLOBAL_VIEWS_LIST, EUserWorkspaceRoles } from "@/constants/workspace"; +import { DEFAULT_GLOBAL_VIEWS_LIST } from "@/constants/workspace"; // hooks -import { useEventTracker, useGlobalView, useIssues, useLabel, useUser } from "@/hooks/store"; +import { useEventTracker, useGlobalView, useIssues, useLabel, useUser, useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; import { getAreFiltersEqual } from "../../../utils"; type Props = { @@ -37,10 +38,8 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => { const { workspaceLabels } = useLabel(); const { globalViewMap, updateGlobalView } = useGlobalView(); const { captureEvent } = useEventTracker(); - const { - data, - membership: { currentWorkspaceRole }, - } = useUser(); + const { data } = useUser(); + const { allowPermissions } = useUserPermissions(); const [isModalOpen, setIsModalOpen] = useState(false); @@ -120,7 +119,10 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => { // add a placeholder object instead of appliedFilters if it is undefined const areFiltersEqual = getAreFiltersEqual(appliedFilters ?? {}, issueFilters, viewDetails); - const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; + const isAuthorizedUser = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); const isDefaultView = DEFAULT_GLOBAL_VIEWS_LIST.map((view) => view.key).includes(globalViewId as TStaticViewTypes); diff --git a/web/core/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx b/web/core/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx index a40216df7..57175ae1d 100644 --- a/web/core/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx +++ b/web/core/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx @@ -7,9 +7,10 @@ import { Header, EHeaderVariant } from "@plane/ui"; import { AppliedFiltersList, SaveFilterView } from "@/components/issues"; // constants import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue"; -import { EUserProjectRoles } from "@/constants/project"; -import { useLabel, useProjectState, useUser } from "@/hooks/store"; +import { useLabel, useProjectState, useUserPermissions } from "@/hooks/store"; import { useIssues } from "@/hooks/store/use-issues"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; + // types export const ProjectAppliedFiltersRoot: React.FC = observer(() => { @@ -23,12 +24,14 @@ export const ProjectAppliedFiltersRoot: React.FC = observer(() => { const { issuesFilter: { issueFilters, updateFilters }, } = useIssues(EIssuesStoreType.PROJECT); - const { - membership: { currentProjectRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); + const { projectStates } = useProjectState(); // derived values - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + const isEditingAllowed = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT + ); const userFilters = issueFilters?.filters; // filters whose value not null or empty array const appliedFilters: IIssueFilterOptions = {}; diff --git a/web/core/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx b/web/core/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx index 86a93ad62..395d6044a 100644 --- a/web/core/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx +++ b/web/core/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx @@ -15,9 +15,9 @@ import { UpdateViewComponent } from "@/components/views/update-view-component"; // constants import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue"; import { EViewAccess } from "@/constants/views"; -import { EUserWorkspaceRoles } from "@/constants/workspace"; // hooks -import { useIssues, useLabel, useProjectState, useProjectView, useUser } from "@/hooks/store"; +import { useIssues, useLabel, useProjectState, useProjectView, useUser, useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; import { getAreFiltersEqual } from "../../../utils"; export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => { @@ -30,10 +30,8 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => { const { projectLabels } = useLabel(); const { projectStates } = useProjectState(); const { viewMap, updateView } = useProjectView(); - const { - data, - membership: { currentWorkspaceRole }, - } = useUser(); + const { data } = useUser(); + const { allowPermissions } = useUserPermissions(); const [isModalOpen, setIsModalOpen] = useState(false); // derived values @@ -108,7 +106,10 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => { updateView(workspaceSlug.toString(), projectId.toString(), viewId.toString(), viewFilters); }; - const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; + const isAuthorizedUser = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); const isLocked = !!viewDetails?.is_locked; const isOwner = viewDetails?.owned_by === data?.id; diff --git a/web/core/components/issues/issue-layouts/gantt/base-gantt-root.tsx b/web/core/components/issues/issue-layouts/gantt/base-gantt-root.tsx index 62467102a..8d7d33872 100644 --- a/web/core/components/issues/issue-layouts/gantt/base-gantt-root.tsx +++ b/web/core/components/issues/issue-layouts/gantt/base-gantt-root.tsx @@ -10,15 +10,15 @@ import { getMonthChartItemPositionWidthInMonth } from "@/components/gantt-chart/ import { QuickAddIssueRoot, IssueGanttBlock, GanttQuickAddIssueButton } from "@/components/issues"; //constants import { EIssueLayoutTypes, EIssuesStoreType } from "@/constants/issue"; -import { EUserProjectRoles } from "@/constants/project"; // helpers import { renderFormattedPayloadDate } from "@/helpers/date-time.helper"; import { getIssueBlocksStructure } from "@/helpers/issue.helper"; //hooks -import { useIssues, useUser } from "@/hooks/store"; +import { useIssues, useUserPermissions } from "@/hooks/store"; import { useIssueStoreType } from "@/hooks/use-issue-layout-store"; import { useIssuesActions } from "@/hooks/use-issues-actions"; // plane web hooks +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; import { useBulkOperationStatus } from "@/plane-web/hooks/use-bulk-operation-status"; import { IssueLayoutHOC } from "../issue-layout-HOC"; @@ -43,9 +43,8 @@ export const BaseGanttRoot: React.FC = observer((props: IBaseGan const { issues, issuesFilter, issueMap } = useIssues(storeType); const { fetchIssues, fetchNextIssues, updateIssue, quickAddIssue } = useIssuesActions(storeType); // store hooks - const { - membership: { currentProjectRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); + const appliedDisplayFilters = issuesFilter.issueFilters?.displayFilters; // plane web hooks const isBulkOperationsEnabled = useBulkOperationStatus(); @@ -90,7 +89,7 @@ export const BaseGanttRoot: React.FC = observer((props: IBaseGan updateIssue && (await updateIssue(issue.project_id, issue.id, payload)); }; - const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + const isAllowed = allowPermissions([EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.PROJECT); const quickAdd = enableIssueCreation && isAllowed && !isCompletedCycle ? ( diff --git a/web/core/components/issues/issue-layouts/kanban/base-kanban-root.tsx b/web/core/components/issues/issue-layouts/kanban/base-kanban-root.tsx index 7121169ee..0578348a4 100644 --- a/web/core/components/issues/issue-layouts/kanban/base-kanban-root.tsx +++ b/web/core/components/issues/issue-layouts/kanban/base-kanban-root.tsx @@ -11,12 +11,12 @@ import { DeleteIssueModal } from "@/components/issues"; //constants import { ISSUE_DELETED } from "@/constants/event-tracker"; import { EIssueFilterType, EIssueLayoutTypes, EIssuesStoreType } from "@/constants/issue"; -import { EUserProjectRoles } from "@/constants/project"; //hooks -import { useEventTracker, useIssueDetail, useIssues, useKanbanView, useUser } from "@/hooks/store"; +import { useEventTracker, useIssueDetail, useIssues, useKanbanView, useUserPermissions } from "@/hooks/store"; import { useGroupIssuesDragNDrop } from "@/hooks/use-group-dragndrop"; import { useIssueStoreType } from "@/hooks/use-issue-layout-store"; import { useIssuesActions } from "@/hooks/use-issues-actions"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; // store // ui // types @@ -49,9 +49,7 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas const pathname = usePathname(); // store hooks const storeType = useIssueStoreType() as KanbanStoreType; - const { - membership: { currentProjectRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); const { captureIssueEvent } = useEventTracker(); const { issueMap, issuesFilter, issues } = useIssues(storeType); const { @@ -115,7 +113,10 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas const [draggedIssueId, setDraggedIssueId] = useState(undefined); const [deleteIssueModal, setDeleteIssueModal] = useState(false); - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + const isEditingAllowed = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT + ); const handleOnDrop = useGroupIssuesDragNDrop(storeType, orderBy, group_by, sub_group_by); diff --git a/web/core/components/issues/issue-layouts/kanban/roots/cycle-root.tsx b/web/core/components/issues/issue-layouts/kanban/roots/cycle-root.tsx index fc6b94f21..e0d87bfe7 100644 --- a/web/core/components/issues/issue-layouts/kanban/roots/cycle-root.tsx +++ b/web/core/components/issues/issue-layouts/kanban/roots/cycle-root.tsx @@ -5,9 +5,9 @@ import { useParams } from "next/navigation"; import { CycleIssueQuickActions } from "@/components/issues"; // constants import { EIssuesStoreType } from "@/constants/issue"; -import { EUserProjectRoles } from "@/constants/project"; // hooks -import { useCycle, useIssues, useUser } from "@/hooks/store"; +import { useCycle, useIssues, useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; // components import { BaseKanBanRoot } from "../base-kanban-root"; @@ -19,13 +19,14 @@ export const CycleKanBanLayout: React.FC = observer(() => { // store const { issues } = useIssues(EIssuesStoreType.CYCLE); const { currentProjectCompletedCycleIds } = useCycle(); - const { - membership: { currentProjectRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); const isCompletedCycle = cycleId && currentProjectCompletedCycleIds ? currentProjectCompletedCycleIds.includes(cycleId.toString()) : false; - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + const isEditingAllowed = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT + ); const canEditIssueProperties = useCallback( () => !isCompletedCycle && isEditingAllowed, diff --git a/web/core/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx b/web/core/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx index 6fb9b78d9..58ff4154b 100644 --- a/web/core/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx +++ b/web/core/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx @@ -2,8 +2,9 @@ import { observer } from "mobx-react"; // hooks import { useParams } from "next/navigation"; import { ProjectIssueQuickActions } from "@/components/issues"; -import { EUserProjectRoles } from "@/constants/project"; -import { useUser } from "@/hooks/store"; +import { useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; + // components // types // constants @@ -11,16 +12,16 @@ import { BaseKanBanRoot } from "../base-kanban-root"; export const ProfileIssuesKanBanLayout: React.FC = observer(() => { // router - const { profileViewId } = useParams(); - const { - membership: { currentWorkspaceAllProjectsRole }, - } = useUser(); + const { workspaceSlug, profileViewId } = useParams(); + const { allowPermissions } = useUserPermissions(); - const canEditPropertiesBasedOnProject = (projectId: string) => { - const currentProjectRole = currentWorkspaceAllProjectsRole && currentWorkspaceAllProjectsRole[projectId]; - - return !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - }; + const canEditPropertiesBasedOnProject = (projectId: string) => + allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT, + workspaceSlug.toString(), + projectId + ); return ( { restoreIssue, } = useIssuesActions(storeType); // mobx store - const { - membership: { currentProjectRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); const { issueMap } = useIssues(); const displayFilters = issuesFilter?.issueFilters?.displayFilters; @@ -67,7 +65,10 @@ export const BaseListRoot = observer((props: IBaseListRoot) => { const groupedIssueIds = issues?.groupedIssueIds as TGroupedIssues | undefined; // auth - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + const isEditingAllowed = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT + ); const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issues?.viewFlags || {}; const canEditProperties = useCallback( diff --git a/web/core/components/issues/issue-layouts/list/roots/cycle-root.tsx b/web/core/components/issues/issue-layouts/list/roots/cycle-root.tsx index bd5f7bad9..169284995 100644 --- a/web/core/components/issues/issue-layouts/list/roots/cycle-root.tsx +++ b/web/core/components/issues/issue-layouts/list/roots/cycle-root.tsx @@ -5,9 +5,9 @@ import { useParams } from "next/navigation"; import { CycleIssueQuickActions } from "@/components/issues"; // constants import { EIssuesStoreType } from "@/constants/issue"; -import { EUserProjectRoles } from "@/constants/project"; // hooks -import { useCycle, useIssues, useUser } from "@/hooks/store"; +import { useCycle, useIssues, useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; // types import { BaseListRoot } from "../base-list-root"; @@ -18,13 +18,14 @@ export const CycleListLayout: React.FC = observer(() => { // store const { issues } = useIssues(EIssuesStoreType.CYCLE); const { currentProjectCompletedCycleIds } = useCycle(); // mobx store - const { - membership: { currentProjectRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); const isCompletedCycle = cycleId && currentProjectCompletedCycleIds ? currentProjectCompletedCycleIds.includes(cycleId.toString()) : false; - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + const isEditingAllowed = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT + ); const canEditIssueProperties = useCallback( () => !isCompletedCycle && isEditingAllowed, diff --git a/web/core/components/issues/issue-layouts/list/roots/profile-issues-root.tsx b/web/core/components/issues/issue-layouts/list/roots/profile-issues-root.tsx index f2134957a..af3c65fe9 100644 --- a/web/core/components/issues/issue-layouts/list/roots/profile-issues-root.tsx +++ b/web/core/components/issues/issue-layouts/list/roots/profile-issues-root.tsx @@ -3,8 +3,9 @@ import { observer } from "mobx-react"; // hooks import { useParams } from "next/navigation"; import { ProjectIssueQuickActions } from "@/components/issues"; -import { EUserProjectRoles } from "@/constants/project"; -import { useUser } from "@/hooks/store"; +import { useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; + // components // types // constants @@ -12,17 +13,17 @@ import { BaseListRoot } from "../base-list-root"; export const ProfileIssuesListLayout: FC = observer(() => { // router - const { profileViewId } = useParams(); + const { workspaceSlug, profileViewId } = useParams(); // store - const { - membership: { currentWorkspaceAllProjectsRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); - const canEditPropertiesBasedOnProject = (projectId: string) => { - const currentProjectRole = currentWorkspaceAllProjectsRole && currentWorkspaceAllProjectsRole[projectId]; - - return !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - }; + const canEditPropertiesBasedOnProject = (projectId: string) => + allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT, + workspaceSlug.toString(), + projectId + ); return ( = observer(( // router const { workspaceSlug } = useParams(); // store hooks - const { - membership: { currentProjectRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); + const { setTrackElement } = useEventTracker(); const { issuesFilter } = useIssues(EIssuesStoreType.ARCHIVED); // derived values const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`; // auth - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER && !readOnly; + const isEditingAllowed = + allowPermissions([EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.PROJECT) && !readOnly; const isRestoringAllowed = handleRestore && isEditingAllowed; const issueLink = `${workspaceSlug}/projects/${issue.project_id}/archives/issues/${issue.id}`; diff --git a/web/core/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx b/web/core/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx index 2a08bac9d..3f455f990 100644 --- a/web/core/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx +++ b/web/core/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx @@ -13,13 +13,13 @@ import { ArchiveIcon, ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, set import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "@/components/issues"; // constants import { EIssuesStoreType } from "@/constants/issue"; -import { EUserProjectRoles } from "@/constants/project"; import { ARCHIVABLE_STATE_GROUPS } from "@/constants/state"; // helpers import { cn } from "@/helpers/common.helper"; import { copyUrlToClipboard } from "@/helpers/string.helper"; // hooks -import { useEventTracker, useIssues, useProjectState, useUser } from "@/hooks/store"; +import { useEventTracker, useIssues, useProjectState, useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; // types import { IQuickActionProps } from "../list/list-view-types"; @@ -46,14 +46,13 @@ export const CycleIssueQuickActions: React.FC = observer((pro // store hooks const { setTrackElement } = useEventTracker(); const { issuesFilter } = useIssues(EIssuesStoreType.CYCLE); - const { - membership: { currentProjectRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); const { getStateById } = useProjectState(); // derived values const stateDetails = getStateById(issue.state_id); // auth - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER && !readOnly; + const isEditingAllowed = + allowPermissions([EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.PROJECT) && !readOnly; const isArchivingAllowed = handleArchive && isEditingAllowed; const isInArchivableGroup = !!stateDetails && ARCHIVABLE_STATE_GROUPS.includes(stateDetails?.group); const isDeletingAllowed = isEditingAllowed; diff --git a/web/core/components/issues/issue-layouts/quick-action-dropdowns/draft-issue.tsx b/web/core/components/issues/issue-layouts/quick-action-dropdowns/draft-issue.tsx index 58807aee2..d9bc00c5f 100644 --- a/web/core/components/issues/issue-layouts/quick-action-dropdowns/draft-issue.tsx +++ b/web/core/components/issues/issue-layouts/quick-action-dropdowns/draft-issue.tsx @@ -13,11 +13,11 @@ import { ContextMenu, CustomMenu, TContextMenuItem } from "@plane/ui"; import { CreateUpdateIssueModal, DeleteIssueModal } from "@/components/issues"; // constant import { EIssuesStoreType } from "@/constants/issue"; -import { EUserProjectRoles } from "@/constants/project"; // helpers import { cn } from "@/helpers/common.helper"; // hooks -import { useEventTracker, useIssues, useUser } from "@/hooks/store"; +import { useEventTracker, useIssues, useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; // types import { IQuickActionProps } from "../list/list-view-types"; @@ -37,15 +37,14 @@ export const DraftIssueQuickActions: React.FC = observer((pro const [issueToEdit, setIssueToEdit] = useState(undefined); const [deleteIssueModal, setDeleteIssueModal] = useState(false); // store hooks - const { - membership: { currentProjectRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); const { setTrackElement } = useEventTracker(); const { issuesFilter } = useIssues(EIssuesStoreType.PROJECT); // derived values const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`; // auth - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER && !readOnly; + const isEditingAllowed = + allowPermissions([EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.PROJECT) && !readOnly; const isDeletingAllowed = isEditingAllowed; const duplicateIssuePayload = omit( diff --git a/web/core/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx b/web/core/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx index 8ec30042b..c4ce4ce52 100644 --- a/web/core/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx +++ b/web/core/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx @@ -13,13 +13,13 @@ import { ArchiveIcon, ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, set import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "@/components/issues"; // constants import { EIssuesStoreType } from "@/constants/issue"; -import { EUserProjectRoles } from "@/constants/project"; import { ARCHIVABLE_STATE_GROUPS } from "@/constants/state"; // helpers import { cn } from "@/helpers/common.helper"; import { copyUrlToClipboard } from "@/helpers/string.helper"; // hooks -import { useIssues, useEventTracker, useUser, useProjectState } from "@/hooks/store"; +import { useIssues, useEventTracker, useProjectState, useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; // types import { IQuickActionProps } from "../list/list-view-types"; @@ -46,14 +46,13 @@ export const ModuleIssueQuickActions: React.FC = observer((pr // store hooks const { setTrackElement } = useEventTracker(); const { issuesFilter } = useIssues(EIssuesStoreType.MODULE); - const { - membership: { currentProjectRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); const { getStateById } = useProjectState(); // derived values const stateDetails = getStateById(issue.state_id); // auth - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER && !readOnly; + const isEditingAllowed = + allowPermissions([EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.PROJECT) && !readOnly; const isArchivingAllowed = handleArchive && isEditingAllowed; const isInArchivableGroup = !!stateDetails && ARCHIVABLE_STATE_GROUPS.includes(stateDetails?.group); const isDeletingAllowed = isEditingAllowed; diff --git a/web/core/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx b/web/core/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx index 40b53090b..a1f8ff7c0 100644 --- a/web/core/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx +++ b/web/core/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx @@ -13,13 +13,13 @@ import { ArchiveIcon, ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, set import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "@/components/issues"; // constants import { EIssuesStoreType } from "@/constants/issue"; -import { EUserProjectRoles } from "@/constants/project"; import { ARCHIVABLE_STATE_GROUPS } from "@/constants/state"; // helpers import { cn } from "@/helpers/common.helper"; import { copyUrlToClipboard } from "@/helpers/string.helper"; // hooks -import { useEventTracker, useIssues, useProjectState, useUser } from "@/hooks/store"; +import { useEventTracker, useIssues, useProjectState, useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; // types import { IQuickActionProps } from "../list/list-view-types"; @@ -44,9 +44,7 @@ export const ProjectIssueQuickActions: React.FC = observer((p const [deleteIssueModal, setDeleteIssueModal] = useState(false); const [archiveIssueModal, setArchiveIssueModal] = useState(false); // store hooks - const { - membership: { currentProjectRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); const { setTrackElement } = useEventTracker(); const { issuesFilter } = useIssues(EIssuesStoreType.PROJECT); const { getStateById } = useProjectState(); @@ -54,7 +52,8 @@ export const ProjectIssueQuickActions: React.FC = observer((p const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`; const stateDetails = getStateById(issue.state_id); // auth - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER && !readOnly; + const isEditingAllowed = + allowPermissions([EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.PROJECT) && !readOnly; const isArchivingAllowed = handleArchive && isEditingAllowed; const isInArchivableGroup = !!stateDetails && ARCHIVABLE_STATE_GROUPS.includes(stateDetails?.group); const isDeletingAllowed = isEditingAllowed; diff --git a/web/core/components/issues/issue-layouts/roots/all-issue-layout-root.tsx b/web/core/components/issues/issue-layouts/roots/all-issue-layout-root.tsx index eeae019cb..d9d6e45f9 100644 --- a/web/core/components/issues/issue-layouts/roots/all-issue-layout-root.tsx +++ b/web/core/components/issues/issue-layouts/roots/all-issue-layout-root.tsx @@ -19,13 +19,13 @@ import { EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, } from "@/constants/issue"; -import { EUserProjectRoles } from "@/constants/project"; // hooks -import { useGlobalView, useIssues, useUser } from "@/hooks/store"; +import { useGlobalView, useIssues, useUserPermissions } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; import { IssuesStoreContext } from "@/hooks/use-issue-layout-store"; import { useIssuesActions } from "@/hooks/use-issues-actions"; import { useWorkspaceIssueProperties } from "@/hooks/use-workspace-issue-properties"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; // store import emptyView from "@/public/empty-state/view.svg"; import { IssuePeekOverview } from "../../peek-overview"; @@ -57,9 +57,8 @@ export const AllIssueLayoutRoot: React.FC = observer((props: Props) => { } = useIssues(EIssuesStoreType.GLOBAL); const { updateIssue, removeIssue, archiveIssue } = useIssuesActions(EIssuesStoreType.GLOBAL); - const { - membership: { currentWorkspaceAllProjectsRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); + const { fetchAllGlobalViews, getViewDetailsById } = useGlobalView(); const viewDetails = getViewDetailsById(globalViewId?.toString()); @@ -132,12 +131,14 @@ export const AllIssueLayoutRoot: React.FC = observer((props: Props) => { const canEditProperties = useCallback( (projectId: string | undefined) => { if (!projectId) return false; - - const currentProjectRole = currentWorkspaceAllProjectsRole && currentWorkspaceAllProjectsRole[projectId]; - - return !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + return allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT, + workspaceSlug.toString(), + projectId + ); }, - [currentWorkspaceAllProjectsRole] + [workspaceSlug] ); const issueFilters = globalViewId ? filters?.[globalViewId.toString()] : undefined; diff --git a/web/core/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx b/web/core/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx index 83481134e..332b61b62 100644 --- a/web/core/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx +++ b/web/core/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx @@ -6,11 +6,11 @@ import { ALL_ISSUES } from "@plane/constants"; import { IIssueDisplayFilterOptions } from "@plane/types"; // hooks import { EIssueFilterType, EIssueLayoutTypes, EIssuesStoreType } from "@/constants/issue"; -import { EUserProjectRoles } from "@/constants/project"; // hooks -import { useIssues, useUser } from "@/hooks/store"; +import { useIssues, useUserPermissions } from "@/hooks/store"; import { useIssueStoreType } from "@/hooks/use-issue-layout-store"; import { useIssuesActions } from "@/hooks/use-issues-actions"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; // views // stores // components @@ -38,9 +38,7 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => { const { projectId } = useParams(); // store hooks const storeType = useIssueStoreType() as SpreadsheetStoreType; - const { - membership: { currentProjectRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); const { issues, issuesFilter } = useIssues(storeType); const { fetchIssues, @@ -56,7 +54,10 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => { // derived values const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issues?.viewFlags || {}; // user role validation - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + const isEditingAllowed = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT + ); useEffect(() => { fetchIssues("init-loader", { canGroup: false, perPageCount: 100 }, viewId); diff --git a/web/core/components/issues/issue-layouts/spreadsheet/roots/cycle-root.tsx b/web/core/components/issues/issue-layouts/spreadsheet/roots/cycle-root.tsx index ff2b8c8ac..cf0f92bdf 100644 --- a/web/core/components/issues/issue-layouts/spreadsheet/roots/cycle-root.tsx +++ b/web/core/components/issues/issue-layouts/spreadsheet/roots/cycle-root.tsx @@ -1,10 +1,9 @@ import React, { useCallback } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; -// constants -import { EUserProjectRoles } from "@/constants/project"; // hooks -import { useCycle, useUser } from "@/hooks/store"; +import { useCycle, useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; // components import { CycleIssueQuickActions } from "../../quick-action-dropdowns"; import { BaseSpreadsheetRoot } from "../base-spreadsheet-root"; @@ -14,13 +13,14 @@ export const CycleSpreadsheetLayout: React.FC = observer(() => { const { cycleId } = useParams(); // store hooks const { currentProjectCompletedCycleIds } = useCycle(); - const { - membership: { currentProjectRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); // auth const isCompletedCycle = cycleId && currentProjectCompletedCycleIds ? currentProjectCompletedCycleIds.includes(cycleId.toString()) : false; - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + const isEditingAllowed = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT + ); const canEditIssueProperties = useCallback( () => !isCompletedCycle && isEditingAllowed, diff --git a/web/core/components/issues/peek-overview/root.tsx b/web/core/components/issues/peek-overview/root.tsx index a55b0d546..15dafab66 100644 --- a/web/core/components/issues/peek-overview/root.tsx +++ b/web/core/components/issues/peek-overview/root.tsx @@ -10,10 +10,10 @@ import { IssueView, TIssueOperations } from "@/components/issues"; // constants import { ISSUE_UPDATED, ISSUE_DELETED, ISSUE_ARCHIVED, ISSUE_RESTORED } from "@/constants/event-tracker"; import { EIssuesStoreType } from "@/constants/issue"; -import { EUserProjectRoles } from "@/constants/project"; // hooks -import { useEventTracker, useIssueDetail, useIssues, useUser } from "@/hooks/store"; +import { useEventTracker, useIssueDetail, useIssues, useUserPermissions } from "@/hooks/store"; import { useIssuesStore } from "@/hooks/use-issue-layout-store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; interface IIssuePeekOverview { embedIssue?: boolean; @@ -26,9 +26,9 @@ export const IssuePeekOverview: FC = observer((props) => { const { embedIssue = false, embedRemoveCurrentNotification, is_archived = false, is_draft = false } = props; // router const pathname = usePathname(); - const { - membership: { currentWorkspaceAllProjectsRole }, - } = useUser(); + // store hook + const { allowPermissions } = useUserPermissions(); + const { issues: { restoreIssue }, } = useIssues(EIssuesStoreType.ARCHIVED); @@ -335,9 +335,13 @@ export const IssuePeekOverview: FC = observer((props) => { if (!peekIssue?.workspaceSlug || !peekIssue?.projectId || !peekIssue?.issueId) return <>; - const currentProjectRole = currentWorkspaceAllProjectsRole?.[peekIssue?.projectId]; // Check if issue is editable, based on user role - const isEditable = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + const isEditable = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT, + peekIssue?.workspaceSlug, + peekIssue?.projectId + ); return ( = { lead_id: "", @@ -84,9 +84,9 @@ export const ModuleAnalyticsSidebar: React.FC = observer((props) => { const { workspaceSlug, projectId } = useParams(); // store hooks - const { - membership: { currentProjectRole }, - } = useUser(); + + const { allowPermissions } = useUserPermissions(); + const { getModuleById, updateModuleDetails, createModuleLink, updateModuleLink, deleteModuleLink, restoreModule } = useModule(); const { setTrackElement, captureModuleEvent, captureEvent } = useEventTracker(); @@ -264,7 +264,10 @@ export const ModuleAnalyticsSidebar: React.FC = observer((props) => { ? "0 Issue" : `${moduleDetails.completed_estimate_points}/${moduleDetails.total_estimate_points}`; - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + const isEditingAllowed = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT + ); return (
@@ -550,7 +553,7 @@ export const ModuleAnalyticsSidebar: React.FC = observer((props) => {
- {currentProjectRole && moduleDetails.link_module && moduleDetails.link_module.length > 0 ? ( + {isEditingAllowed && moduleDetails.link_module && moduleDetails.link_module.length > 0 ? ( <> {isEditingAllowed && !isArchived && (
@@ -569,13 +572,7 @@ export const ModuleAnalyticsSidebar: React.FC = observer((props) => { moduleId={moduleId} handleEditLink={handleEditLink} handleDeleteLink={handleDeleteLink} - userAuth={{ - isGuest: currentProjectRole === EUserProjectRoles.GUEST, - isViewer: currentProjectRole === EUserProjectRoles.VIEWER, - isMember: currentProjectRole === EUserProjectRoles.MEMBER, - isOwner: currentProjectRole === EUserProjectRoles.ADMIN, - }} - disabled={isArchived} + disabled={!isEditingAllowed || isArchived} /> )} diff --git a/web/core/components/modules/links/list.tsx b/web/core/components/modules/links/list.tsx index e6e67b022..36a468d5f 100644 --- a/web/core/components/modules/links/list.tsx +++ b/web/core/components/modules/links/list.tsx @@ -2,7 +2,7 @@ import { useCallback } from "react"; import { observer } from "mobx-react"; // plane types -import { ILinkDetails, UserAuth } from "@plane/types"; +import { ILinkDetails } from "@plane/types"; // components import { ModulesLinksListItem } from "@/components/modules"; // hooks @@ -13,11 +13,10 @@ type Props = { handleDeleteLink: (linkId: string) => void; handleEditLink: (link: ILinkDetails) => void; moduleId: string; - userAuth: UserAuth; }; export const ModuleLinksList: React.FC = observer((props) => { - const { moduleId, handleDeleteLink, handleEditLink, userAuth, disabled } = props; + const { moduleId, handleDeleteLink, handleEditLink, disabled } = props; // store hooks const { getModuleById } = useModule(); // derived values @@ -36,7 +35,7 @@ export const ModuleLinksList: React.FC = observer((props) => { key={link.id} handleDeleteLink={() => memoizedDeleteLink(link.id)} handleEditLink={() => memoizedEditLink(link)} - isEditingAllowed={(userAuth.isMember || userAuth.isOwner) && !disabled} + isEditingAllowed={!disabled} link={link} /> ))} diff --git a/web/core/components/modules/module-card-item.tsx b/web/core/components/modules/module-card-item.tsx index d71bd52ed..ca85d65f7 100644 --- a/web/core/components/modules/module-card-item.tsx +++ b/web/core/components/modules/module-card-item.tsx @@ -17,16 +17,16 @@ import { ModuleStatusDropdown } from "@/components/modules/module-status-dropdo import { PROGRESS_STATE_GROUPS_DETAILS } from "@/constants/common"; import { MODULE_FAVORITED, MODULE_UNFAVORITED } from "@/constants/event-tracker"; import { MODULE_STATUS } from "@/constants/module"; -import { EUserProjectRoles } from "@/constants/project"; // helpers import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; import { generateQueryParams } from "@/helpers/router.helper"; // hooks -import { useEventTracker, useMember, useModule, useProjectEstimates, useUser } from "@/hooks/store"; +import { useEventTracker, useMember, useModule, useProjectEstimates, useUserPermissions } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; import { usePlatformOS } from "@/hooks/use-platform-os"; // plane web constants import { EEstimateSystem } from "@/plane-web/constants/estimates"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; type Props = { moduleId: string; @@ -42,9 +42,7 @@ export const ModuleCardItem: React.FC = observer((props) => { const searchParams = useSearchParams(); const pathname = usePathname(); // store hooks - const { - membership: { currentProjectRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); const { getModuleById, addModuleToFavorites, removeModuleFromFavorites, updateModuleDetails } = useModule(); const { getUserDetails } = useMember(); const { captureEvent } = useEventTracker(); @@ -52,7 +50,10 @@ export const ModuleCardItem: React.FC = observer((props) => { // derived values const moduleDetails = getModuleById(moduleId); - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + const isEditingAllowed = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT + ); const isDisabled = !isEditingAllowed || !!moduleDetails?.archived_at; const renderIcon = Boolean(moduleDetails?.start_date) || Boolean(moduleDetails?.target_date); diff --git a/web/core/components/modules/module-list-item-action.tsx b/web/core/components/modules/module-list-item-action.tsx index fcba7363b..a63b361eb 100644 --- a/web/core/components/modules/module-list-item-action.tsx +++ b/web/core/components/modules/module-list-item-action.tsx @@ -12,14 +12,14 @@ import { FavoriteStar, TOAST_TYPE, Tooltip, setPromiseToast, setToast } from "@p // components import { DateRangeDropdown } from "@/components/dropdowns"; import { ModuleQuickActions } from "@/components/modules"; -import { ModuleStatusDropdown } from "@/components/modules/module-status-dropdown"; +import { ModuleStatusDropdown } from "@/components/modules/module-status-dropdown"; // constants import { MODULE_FAVORITED, MODULE_UNFAVORITED } from "@/constants/event-tracker"; import { MODULE_STATUS } from "@/constants/module"; -import { EUserProjectRoles } from "@/constants/project"; // hooks import { renderFormattedPayloadDate, getDate } from "@/helpers/date-time.helper"; -import { useEventTracker, useMember, useModule, useUser } from "@/hooks/store"; +import { useEventTracker, useMember, useModule, useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; import { ButtonAvatars } from "../dropdowns/member/avatar"; type Props = { @@ -33,9 +33,7 @@ export const ModuleListItemAction: FC = observer((props) => { // router const { workspaceSlug, projectId } = useParams(); // store hooks - const { - membership: { currentProjectRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); const { addModuleToFavorites, removeModuleFromFavorites, updateModuleDetails } = useModule(); const { getUserDetails } = useMember(); const { captureEvent } = useEventTracker(); @@ -43,7 +41,10 @@ export const ModuleListItemAction: FC = observer((props) => { // derived values const moduleStatus = MODULE_STATUS.find((status) => status.value === moduleDetails.status); - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + const isEditingAllowed = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT + ); const isDisabled = !isEditingAllowed || !!moduleDetails?.archived_at; const renderIcon = Boolean(moduleDetails.start_date) || Boolean(moduleDetails.target_date); @@ -140,9 +141,9 @@ export const ModuleListItemAction: FC = observer((props) => { }} onSelect={(val) => { handleModuleDetailsChange({ - start_date: (val?.from ? renderFormattedPayloadDate(val.from) : null), - target_date: (val?.to ? renderFormattedPayloadDate(val.to) : null) - }) + start_date: val?.from ? renderFormattedPayloadDate(val.from) : null, + target_date: val?.to ? renderFormattedPayloadDate(val.to) : null, + }); }} placeholder={{ from: "Start date", @@ -154,9 +155,9 @@ export const ModuleListItemAction: FC = observer((props) => { {moduleStatus && ( )} @@ -191,4 +192,4 @@ export const ModuleListItemAction: FC = observer((props) => { )} ); -}); \ No newline at end of file +}); diff --git a/web/core/components/modules/quick-actions.tsx b/web/core/components/modules/quick-actions.tsx index c68b55826..178c8337b 100644 --- a/web/core/components/modules/quick-actions.tsx +++ b/web/core/components/modules/quick-actions.tsx @@ -9,14 +9,13 @@ import { ArchiveRestoreIcon, ExternalLink, LinkIcon, Pencil, Trash2 } from "luci import { ArchiveIcon, ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui"; // components import { ArchiveModuleModal, CreateUpdateModuleModal, DeleteModuleModal } from "@/components/modules"; -// constants -import { EUserProjectRoles } from "@/constants/project"; // helpers import { cn } from "@/helpers/common.helper"; import { copyUrlToClipboard } from "@/helpers/string.helper"; // hooks -import { useModule, useEventTracker, useUser } from "@/hooks/store"; +import { useModule, useEventTracker, useUserPermissions } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; type Props = { parentRef: React.RefObject; @@ -35,16 +34,19 @@ export const ModuleQuickActions: React.FC = observer((props) => { const [deleteModal, setDeleteModal] = useState(false); // store hooks const { setTrackElement } = useEventTracker(); - const { - membership: { currentWorkspaceAllProjectsRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); + const { getModuleById, restoreModule } = useModule(); // derived values const moduleDetails = getModuleById(moduleId); const isArchived = !!moduleDetails?.archived_at; // auth - const isEditingAllowed = - !!currentWorkspaceAllProjectsRole && currentWorkspaceAllProjectsRole[projectId] >= EUserProjectRoles.MEMBER; + const isEditingAllowed = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT, + workspaceSlug, + projectId + ); const moduleState = moduleDetails?.status?.toLocaleLowerCase(); const isInArchivableGroup = !!moduleState && ["completed", "cancelled"].includes(moduleState); diff --git a/web/core/components/onboarding/invite-members.tsx b/web/core/components/onboarding/invite-members.tsx index b300a2140..695b6f248 100644 --- a/web/core/components/onboarding/invite-members.tsx +++ b/web/core/components/onboarding/invite-members.tsx @@ -23,12 +23,14 @@ import { IUser, IWorkspace } from "@plane/types"; import { Button, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui"; // constants import { MEMBER_INVITED } from "@/constants/event-tracker"; -import { EUserWorkspaceRoles, ROLE, ROLE_DETAILS } from "@/constants/workspace"; +import { ROLE, ROLE_DETAILS } from "@/constants/workspace"; // helpers import { getUserRole } from "@/helpers/user.helper"; // hooks import { useEventTracker } from "@/hooks/store"; import useDynamicDropdownPosition from "@/hooks/use-dynamic-dropdown"; +// plane web constants +import { EUserPermissions } from "@/plane-web/constants/user-permissions"; // services import { WorkspaceService } from "@/plane-web/services"; // assets @@ -47,7 +49,7 @@ type Props = { type EmailRole = { email: string; - role: EUserWorkspaceRoles; + role: EUserPermissions; role_active: boolean; }; diff --git a/web/core/components/project/card.tsx b/web/core/components/project/card.tsx index 678958af8..b0444742a 100644 --- a/web/core/components/project/card.tsx +++ b/web/core/components/project/card.tsx @@ -24,7 +24,6 @@ import { import { Logo } from "@/components/common"; import { ArchiveRestoreProjectModal, DeleteProjectModal, JoinProjectModal } from "@/components/project"; // constants -import { EUserProjectRoles } from "@/constants/project"; // helpers import { cn } from "@/helpers/common.helper"; import { renderFormattedDate } from "@/helpers/date-time.helper"; @@ -33,6 +32,8 @@ import { copyUrlToClipboard } from "@/helpers/string.helper"; import { useProject } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; import { usePlatformOS } from "@/hooks/use-platform-os"; +// plane-web constants +import { EUserPermissions } from "@/plane-web/constants/user-permissions"; type Props = { project: IProject; @@ -57,8 +58,8 @@ export const ProjectCard: React.FC = observer((props) => { // derived values const projectMembersIds = project.members?.map((member) => member.member_id); // auth - const isOwner = project.member_role === EUserProjectRoles.ADMIN; - const isMember = project.member_role === EUserProjectRoles.MEMBER; + const isOwner = project.member_role === EUserPermissions.ADMIN; + const isMember = project.member_role === EUserPermissions.MEMBER; // archive const isArchived = !!project.archived_at; diff --git a/web/core/components/project/header.tsx b/web/core/components/project/header.tsx index 0ffbfa0ed..302d3d405 100644 --- a/web/core/components/project/header.tsx +++ b/web/core/components/project/header.tsx @@ -8,13 +8,12 @@ import { Search, Briefcase, X } from "lucide-react"; import { Breadcrumbs, Button, Header } from "@plane/ui"; // components import { BreadcrumbLink } from "@/components/common"; -// constants -import { EUserWorkspaceRoles } from "@/constants/workspace"; // helpers import { cn } from "@/helpers/common.helper"; // hooks -import { useCommandPalette, useEventTracker, useProjectFilter, useUser } from "@/hooks/store"; +import { useCommandPalette, useEventTracker, useProjectFilter, useUserPermissions } from "@/hooks/store"; import useOutsideClickDetector from "@/hooks/use-outside-click-detector"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; import HeaderFilters from "./filters"; export const ProjectsBaseHeader = observer(() => { @@ -25,9 +24,8 @@ export const ProjectsBaseHeader = observer(() => { // store hooks const { toggleCreateProjectModal } = useCommandPalette(); const { setTrackElement } = useEventTracker(); - const { - membership: { currentWorkspaceRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); + const pathname = usePathname(); const { searchQuery, updateSearchQuery } = useProjectFilter(); @@ -37,7 +35,10 @@ export const ProjectsBaseHeader = observer(() => { if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false); }); // auth - const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; + const isAuthorizedUser = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); const isArchived = pathname.includes("/archives"); const handleInputKeyDown = (e: React.KeyboardEvent) => { diff --git a/web/core/components/project/join-project-modal.tsx b/web/core/components/project/join-project-modal.tsx index 7109b174a..2006c491c 100644 --- a/web/core/components/project/join-project-modal.tsx +++ b/web/core/components/project/join-project-modal.tsx @@ -8,7 +8,7 @@ import type { IProject } from "@plane/types"; // ui import { Button } from "@plane/ui"; // hooks -import { useProject, useUser } from "@/hooks/store"; +import { useProject, useUserPermissions } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; // type @@ -24,9 +24,7 @@ export const JoinProjectModal: React.FC = (props) => { // states const [isJoiningLoading, setIsJoiningLoading] = useState(false); // store hooks - const { - membership: { joinProject }, - } = useUser(); + const { joinProject } = useUserPermissions(); const { fetchProjects } = useProject(); // router const router = useAppRouter(); @@ -34,7 +32,7 @@ export const JoinProjectModal: React.FC = (props) => { const handleJoin = () => { setIsJoiningLoading(true); - joinProject(workspaceSlug, [project.id]) + joinProject(workspaceSlug, project.id) .then(() => { router.push(`/${workspaceSlug}/projects/${project.id}/issues`); fetchProjects(workspaceSlug); diff --git a/web/core/components/project/leave-project-modal.tsx b/web/core/components/project/leave-project-modal.tsx index 788dbd492..74dd18939 100644 --- a/web/core/components/project/leave-project-modal.tsx +++ b/web/core/components/project/leave-project-modal.tsx @@ -14,7 +14,7 @@ import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // constants import { PROJECT_MEMBER_LEAVE } from "@/constants/event-tracker"; // hooks -import { useEventTracker, useUser } from "@/hooks/store"; +import { useEventTracker, useUserPermissions } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; type FormData = { @@ -40,9 +40,7 @@ export const LeaveProjectModal: FC = observer((props) => { const { workspaceSlug } = useParams(); // store hooks const { captureEvent } = useEventTracker(); - const { - membership: { leaveProject }, - } = useUser(); + const { leaveProject } = useUserPermissions(); const { control, diff --git a/web/core/components/project/member-list-item.tsx b/web/core/components/project/member-list-item.tsx index ae831cc8f..956e95db3 100644 --- a/web/core/components/project/member-list-item.tsx +++ b/web/core/components/project/member-list-item.tsx @@ -9,7 +9,7 @@ import { ConfirmProjectMemberRemove } from "@/components/project"; import { PROJECT_MEMBER_LEAVE } from "@/constants/event-tracker"; // hooks -import { useEventTracker, useMember, useProject, useUser } from "@/hooks/store"; +import { useEventTracker, useMember, useProject, useUser, useUserPermissions } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; import { useProjectColumns } from "@/plane-web/components/projects/settings/useProjectColumns"; import { IProjectMemberDetails } from "@/store/member/project-member.store"; @@ -25,9 +25,7 @@ export const ProjectMemberListItem: React.FC = observer((props) => { // router const router = useAppRouter(); // store hooks - const { - membership: { leaveProject }, - } = useUser(); + const { leaveProject } = useUserPermissions(); const { data: currentUser } = useUser(); const { fetchProjects } = useProject(); const { diff --git a/web/core/components/project/member-list.tsx b/web/core/components/project/member-list.tsx index a2dc81dbe..5f75fa326 100644 --- a/web/core/components/project/member-list.tsx +++ b/web/core/components/project/member-list.tsx @@ -9,8 +9,9 @@ import { Button } from "@plane/ui"; import { ProjectMemberListItem, SendProjectInvitationModal } from "@/components/project"; // ui import { MembersSettingsLoader } from "@/components/ui"; -import { EUserProjectRoles } from "@/constants/project"; -import { useEventTracker, useMember, useUser } from "@/hooks/store"; +import { useEventTracker, useMember, useUserPermissions } from "@/hooks/store"; +// plane-web constants +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; export const ProjectMemberList: React.FC = observer(() => { // states @@ -21,9 +22,7 @@ export const ProjectMemberList: React.FC = observer(() => { const { project: { projectMemberIds, getProjectMemberDetails }, } = useMember(); - const { - membership: { currentProjectRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); const searchedMembers = (projectMemberIds ?? []).filter((userId) => { const memberDetails = getProjectMemberDetails(userId); @@ -36,6 +35,8 @@ export const ProjectMemberList: React.FC = observer(() => { }); const memberDetails = searchedMembers?.map((memberId) => getProjectMemberDetails(memberId)); + const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT); + return ( <> setInviteModal(false)} /> @@ -52,7 +53,7 @@ export const ProjectMemberList: React.FC = observer(() => { onChange={(e) => setSearchQuery(e.target.value)} />
- {currentProjectRole === EUserProjectRoles.ADMIN && ( + {isAdmin && (
+ {currentProjectDetails && ( +
+
+

+ Grant view access to all issues for guest users: +

+

+ This will allow guests to have view access to all the project issues. +

+
+ toggleGuestViewAllIssues(!currentProjectDetails?.guest_view_all_features)} + disabled={!isAdmin} + size="md" + /> +
+ )} ); }); diff --git a/web/core/components/project/send-project-invitation-modal.tsx b/web/core/components/project/send-project-invitation-modal.tsx index c6cc912df..4105ef8b0 100644 --- a/web/core/components/project/send-project-invitation-modal.tsx +++ b/web/core/components/project/send-project-invitation-modal.tsx @@ -6,15 +6,15 @@ import { useParams } from "next/navigation"; import { useForm, Controller, useFieldArray } from "react-hook-form"; import { ChevronDown, Plus, X } from "lucide-react"; import { Dialog, Transition } from "@headlessui/react"; -// hooks // ui import { Avatar, Button, CustomSelect, CustomSearchSelect, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { PROJECT_MEMBER_ADDED } from "@/constants/event-tracker"; -import { EUserProjectRoles } from "@/constants/project"; -import { EUserWorkspaceRoles, ROLE } from "@/constants/workspace"; -import { useEventTracker, useMember, useUser } from "@/hooks/store"; -// constants +import { ROLE } from "@/constants/workspace"; +// hooks +import { useEventTracker, useMember, useUserPermissions } from "@/hooks/store"; +// plane-web constants +import { EUserPermissions } from "@/plane-web/constants/user-permissions"; type Props = { isOpen: boolean; @@ -23,7 +23,7 @@ type Props = { }; type member = { - role: EUserProjectRoles; + role: EUserPermissions; member_id: string; }; @@ -46,9 +46,7 @@ export const SendProjectInvitationModal: React.FC = observer((props) => { const { workspaceSlug, projectId } = useParams(); // store hooks const { captureEvent } = useEventTracker(); - const { - membership: { currentProjectRole }, - } = useUser(); + const { projectUserInfo } = useUserPermissions(); const { project: { projectMemberIds, bulkAddMembersToProject }, workspace: { workspaceMemberIds, getWorkspaceMemberDetails }, @@ -68,6 +66,8 @@ export const SendProjectInvitationModal: React.FC = observer((props) => { name: "members", }); + const currentProjectRole = projectUserInfo?.[workspaceSlug?.toString()]?.[projectId?.toString()]?.role; + const uninvitedPeople = workspaceMemberIds?.filter((userId) => { const isInvited = projectMemberIds?.find((u) => u === userId); @@ -173,12 +173,10 @@ export const SendProjectInvitationModal: React.FC = observer((props) => { const currentMemberWorkspaceRole = getWorkspaceMemberDetails(value)?.role; if (!value || !currentMemberWorkspaceRole) return ROLE; - const isGuestOrViewer = [EUserWorkspaceRoles.GUEST, EUserWorkspaceRoles.VIEWER].includes( - currentMemberWorkspaceRole - ); + const isGuest = [EUserPermissions.GUEST].includes(currentMemberWorkspaceRole); return Object.fromEntries( - Object.entries(ROLE).filter(([key]) => !isGuestOrViewer || [5, 10].includes(parseInt(key))) + Object.entries(ROLE).filter(([key]) => !isGuest || [5].includes(parseInt(key))) ); }; @@ -258,7 +256,7 @@ export const SendProjectInvitationModal: React.FC = observer((props) => { const newValue = ROLE[workspaceRole].toUpperCase(); setValue( `members.${index}.role`, - EUserProjectRoles[newValue as keyof typeof EUserProjectRoles] + EUserPermissions[newValue as keyof typeof EUserPermissions] ); }} options={options} @@ -297,7 +295,7 @@ export const SendProjectInvitationModal: React.FC = observer((props) => { {Object.entries( checkCurrentOptionWorkspaceRole(watch(`members.${index}.member_id`)) ).map(([key, label]) => { - if (parseInt(key) > (currentProjectRole ?? EUserProjectRoles.GUEST)) return null; + if (parseInt(key) > (currentProjectRole ?? EUserPermissions.GUEST)) return null; return ( diff --git a/web/core/components/project/settings/member-columns.tsx b/web/core/components/project/settings/member-columns.tsx index 51f510d30..118311b73 100644 --- a/web/core/components/project/settings/member-columns.tsx +++ b/web/core/components/project/settings/member-columns.tsx @@ -5,13 +5,13 @@ import { Trash2 } from "lucide-react"; import { Disclosure } from "@headlessui/react"; import { IUser, IWorkspaceMember } from "@plane/types"; import { CustomSelect, PopoverMenu, TOAST_TYPE, setToast } from "@plane/ui"; -import { EUserProjectRoles } from "@/constants/project"; -import { EUserWorkspaceRoles, ROLE } from "@/constants/workspace"; +import { ROLE } from "@/constants/workspace"; import { useMember, useUser } from "@/hooks/store"; +import { EUserPermissions } from "@/plane-web/constants/user-permissions"; export interface RowData { member: IWorkspaceMember; - role: EUserWorkspaceRoles; + role: EUserPermissions; } type NameProps = { @@ -24,7 +24,7 @@ type NameProps = { type AccountTypeProps = { rowData: RowData; - currentProjectRole: EUserProjectRoles | undefined; + currentProjectRole: EUserPermissions | undefined; workspaceSlug: string; projectId: string; }; @@ -97,16 +97,14 @@ export const AccountTypeColumn: React.FC = observer((props) => // derived values const isCurrentUser = currentUser?.id === rowData.member.id; - const isAdminRole = currentProjectRole === EUserProjectRoles.ADMIN; + const isAdminRole = currentProjectRole === EUserPermissions.ADMIN; const isRoleNonEditable = isCurrentUser || !isAdminRole; const checkCurrentOptionWorkspaceRole = (value: string) => { - const currentMemberWorkspaceRole = getWorkspaceMemberDetails(value)?.role; + const currentMemberWorkspaceRole = getWorkspaceMemberDetails(value)?.role as EUserPermissions | undefined; if (!value || !currentMemberWorkspaceRole) return ROLE; - const isGuestOrViewer = [EUserWorkspaceRoles.GUEST, EUserWorkspaceRoles.VIEWER].includes( - currentMemberWorkspaceRole - ); + const isGuestOrViewer = [EUserPermissions.GUEST].includes(currentMemberWorkspaceRole); return Object.fromEntries( Object.entries(ROLE).filter(([key]) => !isGuestOrViewer || [5, 10].includes(parseInt(key))) @@ -127,11 +125,11 @@ export const AccountTypeColumn: React.FC = observer((props) => render={({ field: { value } }) => ( { + onChange={(value: EUserPermissions) => { if (!workspaceSlug) return; updateMember(workspaceSlug.toString(), projectId.toString(), rowData.member.id, { - role: value as unknown as EUserProjectRoles, // Cast value to unknown first, then to EUserWorkspaceRoles + role: value as unknown as EUserPermissions, // Cast value to unknown first, then to EUserPermissions }).catch((err) => { console.log(err, "err"); const error = err.error; @@ -155,7 +153,7 @@ export const AccountTypeColumn: React.FC = observer((props) => input > {Object.entries(checkCurrentOptionWorkspaceRole(rowData.member.id)).map(([key, label]) => { - if (parseInt(key) > (currentProjectRole ?? EUserProjectRoles.GUEST)) return null; + if (parseInt(key) > (currentProjectRole ?? EUserPermissions.GUEST)) return null; return ( {label} diff --git a/web/core/components/views/quick-actions.tsx b/web/core/components/views/quick-actions.tsx index 287fcad91..1a9cfa607 100644 --- a/web/core/components/views/quick-actions.tsx +++ b/web/core/components/views/quick-actions.tsx @@ -9,14 +9,13 @@ import { IProjectView } from "@plane/types"; import { ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui"; // components import { CreateUpdateProjectViewModal, DeleteProjectViewModal } from "@/components/views"; -// constants -import { EUserProjectRoles } from "@/constants/project"; // helpers import { cn } from "@/helpers/common.helper"; import { copyUrlToClipboard } from "@/helpers/string.helper"; // hooks -import { useUser } from "@/hooks/store"; +import { useUser, useUserPermissions } from "@/hooks/store"; import { PublishViewModal, useViewPublish } from "@/plane-web/components/views/publish"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; type Props = { parentRef: React.RefObject; @@ -31,13 +30,11 @@ export const ViewQuickActions: React.FC = observer((props) => { const [createUpdateViewModal, setCreateUpdateViewModal] = useState(false); const [deleteViewModal, setDeleteViewModal] = useState(false); // store hooks - const { - membership: { currentProjectRole }, - data, - } = useUser(); + const { data } = useUser(); + const { allowPermissions } = useUserPermissions(); // auth const isOwner = view?.owned_by === data?.id; - const isAdmin = !!currentProjectRole && currentProjectRole == EUserProjectRoles.ADMIN; + const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT, workspaceSlug, projectId); const { isPublishModalOpen, setPublishModalOpen, publishContextMenu } = useViewPublish( !!view.anchor, diff --git a/web/core/components/views/view-list-item-action.tsx b/web/core/components/views/view-list-item-action.tsx index 7740520ba..2d2829adb 100644 --- a/web/core/components/views/view-list-item-action.tsx +++ b/web/core/components/views/view-list-item-action.tsx @@ -9,12 +9,12 @@ import { Tooltip, FavoriteStar } from "@plane/ui"; // components import { DeleteProjectViewModal, CreateUpdateProjectViewModal, ViewQuickActions } from "@/components/views"; // constants -import { EUserProjectRoles } from "@/constants/project"; import { EViewAccess } from "@/constants/views"; // helpers import { calculateTotalFilters } from "@/helpers/filter.helper"; // hooks -import { useMember, useProjectView, useUser } from "@/hooks/store"; +import { useMember, useProjectView, useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; import { ButtonAvatars } from "../dropdowns/member/avatar"; type Props = { @@ -30,14 +30,16 @@ export const ViewListItemAction: FC = observer((props) => { // router const { workspaceSlug, projectId } = useParams(); // store - const { - membership: { currentProjectRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); + const { addViewToFavorites, removeViewFromFavorites } = useProjectView(); const { getUserDetails } = useMember(); // derived values - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + const isEditingAllowed = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT + ); const totalFilters = calculateTotalFilters(view.filters ?? {}); diff --git a/web/core/components/workspace/send-workspace-invitation-modal.tsx b/web/core/components/workspace/send-workspace-invitation-modal.tsx index 704112f5d..27fb26e89 100644 --- a/web/core/components/workspace/send-workspace-invitation-modal.tsx +++ b/web/core/components/workspace/send-workspace-invitation-modal.tsx @@ -2,6 +2,7 @@ import React, { useEffect } from "react"; import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; import { Controller, useFieldArray, useForm } from "react-hook-form"; import { Plus, X } from "lucide-react"; import { Dialog, Transition } from "@headlessui/react"; @@ -9,9 +10,10 @@ import { IWorkspaceBulkInviteFormData } from "@plane/types"; // ui import { Button, CustomSelect, Input } from "@plane/ui"; // constants -import { EUserWorkspaceRoles, ROLE } from "@/constants/workspace"; +import { ROLE } from "@/constants/workspace"; // hooks -import { useUser } from "@/hooks/store"; +import { useUserPermissions } from "@/hooks/store"; +import { EUserPermissions } from "@/plane-web/constants/user-permissions"; // types type Props = { @@ -22,7 +24,7 @@ type Props = { type EmailRole = { email: string; - role: EUserWorkspaceRoles; + role: EUserPermissions; }; type FormValues = { @@ -40,10 +42,10 @@ const defaultValues: FormValues = { export const SendWorkspaceInvitationModal: React.FC = observer((props) => { const { isOpen, onClose, onSubmit } = props; - // mobx store - const { - membership: { currentWorkspaceRole }, - } = useUser(); + // store hooks + const { workspaceInfoBySlug } = useUserPermissions(); + // router + const { workspaceSlug } = useParams(); // form info const { control, @@ -57,6 +59,8 @@ export const SendWorkspaceInvitationModal: React.FC = observer((props) => name: "emails", }); + const currentWorkspaceRole = workspaceInfoBySlug(workspaceSlug.toString())?.role; + const handleClose = () => { onClose(); diff --git a/web/core/components/workspace/settings/invitations-list-item.tsx b/web/core/components/workspace/settings/invitations-list-item.tsx index 13b742e1c..09ca653f3 100644 --- a/web/core/components/workspace/settings/invitations-list-item.tsx +++ b/web/core/components/workspace/settings/invitations-list-item.tsx @@ -9,10 +9,11 @@ import { CustomSelect, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // components import { ConfirmWorkspaceMemberRemove } from "@/components/workspace"; // constants -import { EUserWorkspaceRoles, ROLE } from "@/constants/workspace"; +import { ROLE } from "@/constants/workspace"; // hooks -import { useMember, useUser } from "@/hooks/store"; +import { useMember, useUserPermissions } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; type Props = { invitationId: string; @@ -25,15 +26,16 @@ export const WorkspaceInvitationsListItem: FC = observer((props) => { // router const { workspaceSlug } = useParams(); // store hooks - const { - membership: { currentWorkspaceMemberInfo, currentWorkspaceRole }, - } = useUser(); + const { allowPermissions, workspaceInfoBySlug } = useUserPermissions(); + const { workspace: { updateMemberInvitation, deleteMemberInvitation, getWorkspaceInvitationDetails }, } = useMember(); const { isMobile } = usePlatformOS(); // derived values const invitationDetails = getWorkspaceInvitationDetails(invitationId); + const currentWorkspaceMemberInfo = workspaceInfoBySlug(workspaceSlug.toString()); + const currentWorkspaceRole = currentWorkspaceMemberInfo?.role; const handleRemoveInvitation = async () => { if (!workspaceSlug || !invitationDetails) return; @@ -58,13 +60,16 @@ export const WorkspaceInvitationsListItem: FC = observer((props) => { if (!invitationDetails) return null; // is the current logged in user admin - const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN; + const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); + // role change access- // 1. user cannot change their own role // 2. only admin or member can change role // 3. user cannot change role of higher role - const hasRoleChangeAccess = - currentWorkspaceRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentWorkspaceRole); + const hasRoleChangeAccess = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); if (!currentWorkspaceMemberInfo) return null; @@ -110,7 +115,7 @@ export const WorkspaceInvitationsListItem: FC = observer((props) => {
} value={invitationDetails.role} - onChange={(value: EUserWorkspaceRoles) => { + onChange={(value: EUserPermissions) => { if (!workspaceSlug || !value) return; updateMemberInvitation(workspaceSlug.toString(), invitationDetails.id, { diff --git a/web/core/components/workspace/settings/member-columns.tsx b/web/core/components/workspace/settings/member-columns.tsx index 85e45feef..70491351d 100644 --- a/web/core/components/workspace/settings/member-columns.tsx +++ b/web/core/components/workspace/settings/member-columns.tsx @@ -5,13 +5,13 @@ import { Trash2 } from "lucide-react"; import { Disclosure } from "@headlessui/react"; import { IUser, IWorkspaceMember } from "@plane/types"; import { CustomSelect, PopoverMenu, TOAST_TYPE, setToast } from "@plane/ui"; -import { EUserProjectRoles } from "@/constants/project"; -import { EUserWorkspaceRoles, ROLE } from "@/constants/workspace"; -import { useMember, useUser } from "@/hooks/store"; +import { ROLE } from "@/constants/workspace"; +import { useMember, useUser, useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; export interface RowData { member: IWorkspaceMember; - role: EUserWorkspaceRoles; + role: EUserPermissions; } type NameProps = { @@ -24,7 +24,6 @@ type NameProps = { type AccountTypeProps = { rowData: RowData; - currentWorkspaceRole: EUserWorkspaceRoles | undefined; workspaceSlug: string; }; @@ -81,13 +80,15 @@ export const NameColumn: React.FC = (props) => { }; export const AccountTypeColumn: React.FC = observer((props) => { - const { rowData, currentWorkspaceRole, workspaceSlug } = props; + const { rowData, workspaceSlug } = props; // form info const { control, formState: { errors }, } = useForm(); // store hooks + const { allowPermissions } = useUserPermissions(); + const { workspace: { updateMember }, } = useMember(); @@ -95,7 +96,7 @@ export const AccountTypeColumn: React.FC = observer((props) => // derived values const isCurrentUser = currentUser?.id === rowData.member.id; - const isAdminRole = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN; + const isAdminRole = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); const isRoleNonEditable = isCurrentUser || !isAdminRole; return ( @@ -112,12 +113,12 @@ export const AccountTypeColumn: React.FC = observer((props) => render={({ field: { value } }) => ( { + onChange={(value: EUserPermissions) => { console.log({ value, workspaceSlug }, "onChange"); if (!workspaceSlug) return; updateMember(workspaceSlug.toString(), rowData.member.id, { - role: value as unknown as EUserWorkspaceRoles, // Cast value to unknown first, then to EUserWorkspaceRoles + role: value as unknown as EUserPermissions, // Cast value to unknown first, then to EUserPermissions }).catch((err) => { console.log(err, "err"); const error = err.error; @@ -141,7 +142,7 @@ export const AccountTypeColumn: React.FC = observer((props) => input > {Object.keys(ROLE).map((item) => ( - + {ROLE[item as unknown as keyof typeof ROLE]} ))} diff --git a/web/core/components/workspace/settings/members-list-item.tsx b/web/core/components/workspace/settings/members-list-item.tsx index 95a329606..d4ee6dafe 100644 --- a/web/core/components/workspace/settings/members-list-item.tsx +++ b/web/core/components/workspace/settings/members-list-item.tsx @@ -12,7 +12,7 @@ import { ConfirmWorkspaceMemberRemove } from "@/components/workspace"; // constants import { WORKSPACE_MEMBER_LEAVE } from "@/constants/event-tracker"; // hooks -import { useEventTracker, useMember, useUser } from "@/hooks/store"; +import { useEventTracker, useMember, useUser, useUserPermissions } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; import { useMemberColumns } from "@/plane-web/components/workspace/settings/useMemberColumns"; @@ -26,13 +26,11 @@ export const WorkspaceMembersListItem: FC = observer((props) => { // router const router = useAppRouter(); // store hooks - const { - membership: { leaveWorkspace }, - } = useUser(); const { data: currentUser } = useUser(); const { workspace: { removeMemberFromWorkspace }, } = useMember(); + const { leaveWorkspace } = useUserPermissions(); const { captureEvent } = useEventTracker(); // derived values diff --git a/web/core/components/workspace/settings/workspace-details.tsx b/web/core/components/workspace/settings/workspace-details.tsx index 36dbc0d79..b05f328c5 100644 --- a/web/core/components/workspace/settings/workspace-details.tsx +++ b/web/core/components/workspace/settings/workspace-details.tsx @@ -12,13 +12,14 @@ import { LogoSpinner } from "@/components/common"; import { WorkspaceImageUploadModal } from "@/components/core"; // constants import { WORKSPACE_UPDATED } from "@/constants/event-tracker"; -import { EUserWorkspaceRoles, ORGANIZATION_SIZE } from "@/constants/workspace"; +import { ORGANIZATION_SIZE } from "@/constants/workspace"; // helpers import { copyUrlToClipboard } from "@/helpers/string.helper"; // hooks -import { useEventTracker, useUser, useWorkspace } from "@/hooks/store"; +import { useEventTracker, useUserPermissions, useWorkspace } from "@/hooks/store"; // plane web components import { DeleteWorkspaceSection } from "@/plane-web/components/workspace"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; // services import { FileService } from "@/services/file.service"; @@ -39,10 +40,9 @@ export const WorkspaceDetails: FC = observer(() => { const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false); // store hooks const { captureWorkspaceEvent } = useEventTracker(); - const { - membership: { currentWorkspaceRole }, - } = useUser(); const { currentWorkspace, updateWorkspace } = useWorkspace(); + const { allowPermissions } = useUserPermissions(); + // form info const { handleSubmit, @@ -141,7 +141,7 @@ export const WorkspaceDetails: FC = observer(() => { if (currentWorkspace) reset({ ...currentWorkspace }); }, [currentWorkspace, reset]); - const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN; + const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); if (!currentWorkspace) return ( diff --git a/web/core/components/workspace/sidebar/projects-list-item.tsx b/web/core/components/workspace/sidebar/projects-list-item.tsx index c232baa8b..42c6d10d4 100644 --- a/web/core/components/workspace/sidebar/projects-list-item.tsx +++ b/web/core/components/workspace/sidebar/projects-list-item.tsx @@ -41,15 +41,14 @@ import { import { Logo } from "@/components/common"; import { LeaveProjectModal, PublishProjectModal } from "@/components/project"; import { SidebarNavItem } from "@/components/sidebar"; -// constants -import { EUserProjectRoles } from "@/constants/project"; // helpers import { cn } from "@/helpers/common.helper"; // hooks -import { useAppTheme, useEventTracker, useProject, useUser } from "@/hooks/store"; +import { useAppTheme, useEventTracker, useProject, useUserPermissions } from "@/hooks/store"; import useOutsideClickDetector from "@/hooks/use-outside-click-detector"; import { usePlatformOS } from "@/hooks/use-platform-os"; // constants +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; import { HIGHLIGHT_CLASS, highlightIssueOnDrop } from "../../issues/issue-layouts/utils"; type Props = { @@ -71,37 +70,37 @@ const navigation = (workspaceSlug: string, projectId: string) => [ name: "Issues", href: `/${workspaceSlug}/projects/${projectId}/issues`, Icon: LayersIcon, - access: EUserProjectRoles.GUEST, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], }, { name: "Cycles", href: `/${workspaceSlug}/projects/${projectId}/cycles`, Icon: ContrastIcon, - access: EUserProjectRoles.VIEWER, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], }, { name: "Modules", href: `/${workspaceSlug}/projects/${projectId}/modules`, Icon: DiceIcon, - access: EUserProjectRoles.VIEWER, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], }, { name: "Views", href: `/${workspaceSlug}/projects/${projectId}/views`, Icon: Layers, - access: EUserProjectRoles.GUEST, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], }, { name: "Pages", href: `/${workspaceSlug}/projects/${projectId}/pages`, Icon: FileText, - access: EUserProjectRoles.VIEWER, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], }, { name: "Intake", href: `/${workspaceSlug}/projects/${projectId}/inbox`, Icon: Intake, - access: EUserProjectRoles.GUEST, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], }, ]; @@ -113,9 +112,7 @@ export const SidebarProjectsListItem: React.FC = observer((props) => { const { setTrackElement } = useEventTracker(); const { addProjectToFavorites, removeProjectFromFavorites, getProjectById } = useProject(); const { isMobile } = usePlatformOS(); - const { - membership: { currentWorkspaceAllProjectsRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); // states const [leaveProjectModalOpen, setLeaveProjectModal] = useState(false); const [publishModalOpen, setPublishModal] = useState(false); @@ -135,9 +132,18 @@ export const SidebarProjectsListItem: React.FC = observer((props) => { // derived values const project = getProjectById(projectId); // auth - const isAdmin = project?.member_role === EUserProjectRoles.ADMIN; - const isViewerOrGuest = - project?.member_role && [EUserProjectRoles.VIEWER, EUserProjectRoles.GUEST].includes(project.member_role); + const isAdmin = allowPermissions( + [EUserPermissions.ADMIN], + EUserPermissionsLevel.PROJECT, + workspaceSlug.toString(), + project?.id + ); + const isAuthorized = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT, + workspaceSlug.toString(), + project?.id + ); const handleAddToFavorites = () => { if (!workspaceSlug || !project) return; @@ -395,7 +401,7 @@ export const SidebarProjectsListItem: React.FC = observer((props) => { placement="bottom-start" useCaptureForOutsideClick > - {!isViewerOrGuest && ( + {isAuthorized && ( @@ -421,7 +427,7 @@ export const SidebarProjectsListItem: React.FC = observer((props) => {
)} - {!isViewerOrGuest && ( + {isAuthorized && (
@@ -437,7 +443,7 @@ export const SidebarProjectsListItem: React.FC = observer((props) => { Copy link - {!isViewerOrGuest && ( + {isAuthorized && (
@@ -456,7 +462,7 @@ export const SidebarProjectsListItem: React.FC = observer((props) => { {/* leave project */} - {isViewerOrGuest && ( + {!isAuthorized && (
@@ -505,12 +511,14 @@ export const SidebarProjectsListItem: React.FC = observer((props) => { (item.name === "Intake" && !project.inbox_view) ) return; - const currentRole = currentWorkspaceAllProjectsRole - ? currentWorkspaceAllProjectsRole[projectId] - : undefined; return ( <> - {currentRole >= item.access && ( + {allowPermissions( + item.access, + EUserPermissionsLevel.PROJECT, + workspaceSlug.toString(), + project.id + ) && ( { // get local storage data for isFavoriteProjectsListOpen and isAllProjectsListOpen @@ -37,16 +36,18 @@ export const SidebarProjectsList: FC = observer(() => { const { toggleCreateProjectModal } = useCommandPalette(); const { sidebarCollapsed } = useAppTheme(); const { setTrackElement } = useEventTracker(); - const { - membership: { currentWorkspaceRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); + const { getProjectById, joinedProjectIds: joinedProjects, updateProjectView } = useProject(); // router params const { workspaceSlug } = useParams(); const pathname = usePathname(); // auth - const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; + const isAuthorizedUser = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); const handleCopyText = (projectId: string) => { copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/issues`).then(() => { diff --git a/web/core/components/workspace/sidebar/user-menu.tsx b/web/core/components/workspace/sidebar/user-menu.tsx index dccb50f64..d9716a805 100644 --- a/web/core/components/workspace/sidebar/user-menu.tsx +++ b/web/core/components/workspace/sidebar/user-menu.tsx @@ -11,28 +11,25 @@ import { NotificationAppSidebarOption } from "@/components/workspace-notificatio // constants import { SIDEBAR_USER_MENU_ITEMS } from "@/constants/dashboard"; import { SIDEBAR_CLICKED } from "@/constants/event-tracker"; -import { EUserWorkspaceRoles } from "@/constants/workspace"; // helpers import { cn } from "@/helpers/common.helper"; // hooks -import { useAppTheme, useEventTracker, useUser } from "@/hooks/store"; +import { useAppTheme, useEventTracker, useUser, useUserPermissions } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; +import { EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; export const SidebarUserMenu = observer(() => { // store hooks const { toggleSidebar, sidebarCollapsed } = useAppTheme(); const { captureEvent } = useEventTracker(); const { isMobile } = usePlatformOS(); - const { - membership: { currentWorkspaceRole }, - data: currentUser, - } = useUser(); + const { data: currentUser } = useUser(); + const { allowPermissions } = useUserPermissions(); // router params const { workspaceSlug } = useParams(); // pathname const pathname = usePathname(); // computed - const workspaceMemberInfo = currentWorkspaceRole || EUserWorkspaceRoles.GUEST; const getHref = (link: any) => `/${workspaceSlug}${link.href}${link.key === "your-work" ? `/${currentUser?.id}` : ""}`; @@ -61,7 +58,7 @@ export const SidebarUserMenu = observer(() => { > {SIDEBAR_USER_MENU_ITEMS.map( (link) => - workspaceMemberInfo >= link.access && ( + allowPermissions(link.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug.toString()) && ( { // state @@ -33,9 +33,7 @@ export const SidebarWorkspaceMenu = observer(() => { const { toggleSidebar, sidebarCollapsed } = useAppTheme(); const { captureEvent } = useEventTracker(); const { isMobile } = usePlatformOS(); - const { - membership: { currentWorkspaceRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); // router params const { workspaceSlug } = useParams(); // pathname @@ -44,8 +42,6 @@ export const SidebarWorkspaceMenu = observer(() => { const { setValue: toggleWorkspaceMenu, storedValue } = useLocalStorage("is_workspace_menu_open", true); // derived values const isWorkspaceMenuOpen = !!storedValue; - // auth - const workspaceMemberInfo = currentWorkspaceRole || EUserWorkspaceRoles.GUEST; const handleLinkClick = (itemKey: string) => { if (window.innerWidth < 768) { @@ -157,7 +153,7 @@ export const SidebarWorkspaceMenu = observer(() => { > {SIDEBAR_WORKSPACE_MENU_ITEMS.map( (link) => - workspaceMemberInfo >= link.access && ( + allowPermissions(link.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug.toString()) && ( { const { viewId } = props; @@ -77,9 +78,8 @@ export const GlobalViewsHeader: React.FC = observer(() => { const { globalViewId } = useParams(); // store hooks const { currentWorkspaceViews } = useGlobalView(); - const { - membership: { currentWorkspaceRole }, - } = useUser(); + const { allowPermissions } = useUserPermissions(); + const { captureEvent } = useEventTracker(); // bring the active view to the centre of the header @@ -101,7 +101,10 @@ export const GlobalViewsHeader: React.FC = observer(() => { } }, [globalViewId, currentWorkspaceViews, containerRef, captureEvent]); - const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; + const isAuthorizedUser = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); return (
diff --git a/web/core/components/workspace/views/quick-action.tsx b/web/core/components/workspace/views/quick-action.tsx index a60d15a5d..68dff8d34 100644 --- a/web/core/components/workspace/views/quick-action.tsx +++ b/web/core/components/workspace/views/quick-action.tsx @@ -12,12 +12,12 @@ import { ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from import { CreateUpdateWorkspaceViewModal, DeleteGlobalViewModal } from "@/components/workspace"; // constants import { EViewAccess } from "@/constants/views"; -import { EUserWorkspaceRoles } from "@/constants/workspace"; // helpers import { cn } from "@/helpers/common.helper"; import { copyUrlToClipboard } from "@/helpers/string.helper"; // hooks -import { useUser } from "@/hooks/store"; +import { useUser, useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; type Props = { parentRef: React.RefObject; @@ -33,13 +33,11 @@ export const WorkspaceViewQuickActions: React.FC = observer((props) => { const [updateViewModal, setUpdateViewModal] = useState(false); const [deleteViewModal, setDeleteViewModal] = useState(false); // store hooks - const { - membership: { currentWorkspaceRole }, - data, - } = useUser(); + const { data } = useUser(); + const { allowPermissions } = useUserPermissions(); // auth const isOwner = view?.owned_by === data?.id; - const isAdmin = !!currentWorkspaceRole && currentWorkspaceRole === EUserWorkspaceRoles.ADMIN; + const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); const viewLink = `${workspaceSlug}/workspace-views/${view.id}`; const handleCopyText = () => diff --git a/web/core/constants/dashboard.ts b/web/core/constants/dashboard.ts index 977557072..ef3b6fda3 100644 --- a/web/core/constants/dashboard.ts +++ b/web/core/constants/dashboard.ts @@ -8,6 +8,7 @@ import { TIssuesListTypes, TStateGroups } from "@plane/types"; // ui import { ContrastIcon, UserActivityIcon } from "@plane/ui"; import { Props } from "@/components/icons/types"; +import { EUserPermissions } from "@/plane-web/constants/user-permissions"; // assets import CompletedIssuesDark from "@/public/empty-state/dashboard/dark/completed-issues.svg"; import OverdueIssuesDark from "@/public/empty-state/dashboard/dark/overdue-issues.svg"; @@ -15,8 +16,6 @@ import UpcomingIssuesDark from "@/public/empty-state/dashboard/dark/upcoming-iss import CompletedIssuesLight from "@/public/empty-state/dashboard/light/completed-issues.svg"; import OverdueIssuesLight from "@/public/empty-state/dashboard/light/overdue-issues.svg"; import UpcomingIssuesLight from "@/public/empty-state/dashboard/light/upcoming-issues.svg"; -// constants -import { EUserWorkspaceRoles } from "./workspace"; // gradients for issues by priority widget graph bars export const PRIORITY_GRAPH_GRADIENTS = [ @@ -255,7 +254,7 @@ export const SIDEBAR_WORKSPACE_MENU_ITEMS: { key: string; label: string; href: string; - access: EUserWorkspaceRoles; + access: EUserPermissions[]; highlight: (pathname: string, baseUrl: string) => boolean; Icon: React.FC; }[] = [ @@ -263,7 +262,7 @@ export const SIDEBAR_WORKSPACE_MENU_ITEMS: { key: "projects", label: "Projects", href: `/projects`, - access: EUserWorkspaceRoles.GUEST, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/projects/`, Icon: Briefcase, }, @@ -271,7 +270,7 @@ export const SIDEBAR_WORKSPACE_MENU_ITEMS: { key: "all-issues", label: "Views", href: `/workspace-views/all-issues`, - access: EUserWorkspaceRoles.GUEST, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/workspace-views/`), Icon: Layers, }, @@ -279,7 +278,7 @@ export const SIDEBAR_WORKSPACE_MENU_ITEMS: { key: "active-cycles", label: "Cycles", href: `/active-cycles`, - access: EUserWorkspaceRoles.MEMBER, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/active-cycles/`, Icon: ContrastIcon, }, @@ -287,7 +286,7 @@ export const SIDEBAR_WORKSPACE_MENU_ITEMS: { key: "analytics", label: "Analytics", href: `/analytics`, - access: EUserWorkspaceRoles.MEMBER, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/analytics/`), Icon: BarChart2, }, @@ -301,7 +300,7 @@ export const SIDEBAR_USER_MENU_ITEMS: { key: string; label: string; href: string; - access: EUserWorkspaceRoles; + access: EUserPermissions[]; highlight: (pathname: string, baseUrl: string, options?: TLinkOptions) => boolean; Icon: React.FC; }[] = [ @@ -309,7 +308,7 @@ export const SIDEBAR_USER_MENU_ITEMS: { key: "home", label: "Home", href: ``, - access: EUserWorkspaceRoles.GUEST, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/`, Icon: Home, }, @@ -317,7 +316,7 @@ export const SIDEBAR_USER_MENU_ITEMS: { key: "your-work", label: "Your work", href: "/profile", - access: EUserWorkspaceRoles.MEMBER, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], highlight: (pathname: string, baseUrl: string, options?: TLinkOptions) => options?.userId ? pathname.includes(`${baseUrl}/profile/${options?.userId}`) : false, Icon: UserActivityIcon, @@ -326,7 +325,7 @@ export const SIDEBAR_USER_MENU_ITEMS: { key: "notifications", label: "Inbox", href: `/notifications`, - access: EUserWorkspaceRoles.GUEST, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/notifications/`), Icon: Inbox, }, diff --git a/web/core/constants/empty-state.ts b/web/core/constants/empty-state.ts index 984f6f96d..99b1e4f97 100644 --- a/web/core/constants/empty-state.ts +++ b/web/core/constants/empty-state.ts @@ -1,5 +1,4 @@ -import { EUserProjectRoles } from "./project"; -import { EUserWorkspaceRoles } from "./workspace"; +import { EUserPermissions } from "ee/constants/user-permissions"; export interface EmptyStateDetails { key: EmptyStateType; @@ -23,7 +22,7 @@ export interface EmptyStateDetails { }; }; accessType?: "workspace" | "project"; - access?: EUserWorkspaceRoles | EUserProjectRoles; + access?: any; } export enum EmptyStateType { @@ -126,7 +125,7 @@ const emptyStateDetails = { }, accessType: "workspace", - access: EUserWorkspaceRoles.MEMBER, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], }, [EmptyStateType.WORKSPACE_ANALYTICS]: { key: EmptyStateType.WORKSPACE_ANALYTICS, @@ -143,7 +142,7 @@ const emptyStateDetails = { }, }, accessType: "workspace", - access: EUserWorkspaceRoles.MEMBER, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], }, [EmptyStateType.WORKSPACE_PROJECTS]: { key: EmptyStateType.WORKSPACE_PROJECTS, @@ -159,7 +158,7 @@ const emptyStateDetails = { }, }, accessType: "workspace", - access: EUserWorkspaceRoles.MEMBER, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], }, // all-issues [EmptyStateType.WORKSPACE_ALL_ISSUES]: { @@ -171,7 +170,7 @@ const emptyStateDetails = { text: "Create new issue", }, accessType: "workspace", - access: EUserWorkspaceRoles.MEMBER, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], }, [EmptyStateType.WORKSPACE_ASSIGNED]: { key: EmptyStateType.WORKSPACE_ASSIGNED, @@ -182,7 +181,7 @@ const emptyStateDetails = { text: "Create new issue", }, accessType: "workspace", - access: EUserWorkspaceRoles.MEMBER, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], }, [EmptyStateType.WORKSPACE_CREATED]: { key: EmptyStateType.WORKSPACE_CREATED, @@ -193,7 +192,7 @@ const emptyStateDetails = { text: "Create new issue", }, accessType: "workspace", - access: EUserWorkspaceRoles.MEMBER, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], }, [EmptyStateType.WORKSPACE_SUBSCRIBED]: { key: EmptyStateType.WORKSPACE_SUBSCRIBED, @@ -220,7 +219,7 @@ const emptyStateDetails = { }, }, accessType: "workspace", - access: EUserWorkspaceRoles.MEMBER, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], }, // workspace settings [EmptyStateType.WORKSPACE_SETTINGS_API_TOKENS]: { @@ -309,7 +308,7 @@ const emptyStateDetails = { }, }, accessType: "project", - access: EUserProjectRoles.MEMBER, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], }, [EmptyStateType.PROJECT_CYCLE_NO_ISSUES]: { key: EmptyStateType.PROJECT_CYCLE_NO_ISSUES, @@ -323,7 +322,7 @@ const emptyStateDetails = { text: "Add an existing issue", }, accessType: "project", - access: EUserProjectRoles.MEMBER, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], }, [EmptyStateType.PROJECT_CYCLE_ACTIVE]: { key: EmptyStateType.PROJECT_CYCLE_ACTIVE, @@ -361,7 +360,7 @@ const emptyStateDetails = { text: "Clear all filters", }, accessType: "project", - access: EUserProjectRoles.MEMBER, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], }, [EmptyStateType.PROJECT_ARCHIVED_EMPTY_FILTER]: { key: EmptyStateType.PROJECT_ARCHIVED_EMPTY_FILTER, @@ -371,7 +370,7 @@ const emptyStateDetails = { text: "Clear all filters", }, accessType: "project", - access: EUserProjectRoles.MEMBER, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], }, [EmptyStateType.PROJECT_DRAFT_EMPTY_FILTER]: { key: EmptyStateType.PROJECT_DRAFT_EMPTY_FILTER, @@ -381,7 +380,7 @@ const emptyStateDetails = { text: "Clear all filters", }, accessType: "project", - access: EUserProjectRoles.MEMBER, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], }, // project issues [EmptyStateType.PROJECT_NO_ISSUES]: { @@ -399,7 +398,7 @@ const emptyStateDetails = { }, }, accessType: "project", - access: EUserProjectRoles.MEMBER, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], }, [EmptyStateType.PROJECT_ARCHIVED_NO_ISSUES]: { key: EmptyStateType.PROJECT_ARCHIVED_NO_ISSUES, @@ -411,7 +410,7 @@ const emptyStateDetails = { text: "Set automation", }, accessType: "project", - access: EUserProjectRoles.MEMBER, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], }, [EmptyStateType.PROJECT_DRAFT_NO_ISSUES]: { key: EmptyStateType.PROJECT_DRAFT_NO_ISSUES, @@ -451,7 +450,7 @@ const emptyStateDetails = { text: "Add an existing issue", }, accessType: "project", - access: EUserProjectRoles.MEMBER, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], }, [EmptyStateType.PROJECT_MODULE]: { key: EmptyStateType.PROJECT_MODULE, @@ -467,7 +466,7 @@ const emptyStateDetails = { }, }, accessType: "project", - access: EUserProjectRoles.MEMBER, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], }, [EmptyStateType.PROJECT_ARCHIVED_NO_MODULES]: { key: EmptyStateType.PROJECT_ARCHIVED_NO_MODULES, @@ -490,7 +489,7 @@ const emptyStateDetails = { }, }, accessType: "project", - access: EUserProjectRoles.GUEST, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], }, // project pages [EmptyStateType.PROJECT_PAGE]: { @@ -503,7 +502,7 @@ const emptyStateDetails = { text: "Create your first page", }, accessType: "project", - access: EUserProjectRoles.MEMBER, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], }, [EmptyStateType.PROJECT_PAGE_PRIVATE]: { key: EmptyStateType.PROJECT_PAGE_PRIVATE, @@ -514,7 +513,7 @@ const emptyStateDetails = { text: "Create your first page", }, accessType: "project", - access: EUserProjectRoles.MEMBER, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], }, [EmptyStateType.PROJECT_PAGE_PUBLIC]: { key: EmptyStateType.PROJECT_PAGE_PUBLIC, @@ -525,7 +524,7 @@ const emptyStateDetails = { text: "Create your first page", }, accessType: "project", - access: EUserProjectRoles.MEMBER, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], }, [EmptyStateType.PROJECT_PAGE_ARCHIVED]: { key: EmptyStateType.PROJECT_PAGE_ARCHIVED, @@ -543,7 +542,7 @@ const emptyStateDetails = { text: "Create your first page", }, accessType: "workspace", - access: EUserProjectRoles.GUEST, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], }, [EmptyStateType.WORKSPACE_PAGE_PRIVATE]: { key: EmptyStateType.WORKSPACE_PAGE_PRIVATE, @@ -554,7 +553,7 @@ const emptyStateDetails = { text: "Create your first page", }, accessType: "workspace", - access: EUserProjectRoles.GUEST, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], }, [EmptyStateType.WORKSPACE_PAGE_PUBLIC]: { key: EmptyStateType.WORKSPACE_PAGE_PUBLIC, @@ -565,7 +564,7 @@ const emptyStateDetails = { text: "Create your first page", }, accessType: "workspace", - access: EUserProjectRoles.GUEST, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], }, [EmptyStateType.WORKSPACE_PAGE_ARCHIVED]: { key: EmptyStateType.WORKSPACE_PAGE_ARCHIVED, @@ -681,7 +680,7 @@ const emptyStateDetails = { description: "Intake helps you manage incoming requests to your project and add them as issues in your workflow. Enable intake \n from project settings to manage requests.", accessType: "project", - access: EUserProjectRoles.ADMIN, + access: [EUserPermissions.ADMIN], path: "/empty-state/disabled-feature/intake", primaryButton: { text: "Manage features", @@ -693,7 +692,7 @@ const emptyStateDetails = { description: "Break work down by timeboxed chunks, work backwards from your project deadline to set dates, and make tangible progress as a team. Enable the cycles feature for your project to start using them.", accessType: "project", - access: EUserProjectRoles.ADMIN, + access: [EUserPermissions.ADMIN], path: "/empty-state/disabled-feature/cycles", primaryButton: { text: "Manage features", @@ -705,7 +704,7 @@ const emptyStateDetails = { description: "A group of issues that belong to a logical, hierarchical parent form a module. Think of them as a way to track work by project milestones. Enable modules from project settings.", accessType: "project", - access: EUserProjectRoles.ADMIN, + access: [EUserPermissions.ADMIN], path: "/empty-state/disabled-feature/modules", primaryButton: { text: "Manage features", @@ -717,7 +716,7 @@ const emptyStateDetails = { description: "Pages are thought spotting space in Plane. Take down meeting notes, format them easily, embed issues, lay them out using a library of components, and keep them all in your project’s context. Enable the pages feature to start creating them in your project.", accessType: "project", - access: EUserProjectRoles.ADMIN, + access: [EUserPermissions.ADMIN], path: "/empty-state/disabled-feature/pages", primaryButton: { text: "Manage features", @@ -729,7 +728,7 @@ const emptyStateDetails = { description: "Views are a set of saved filters that you use frequently or want easy access to. All your colleagues in a project can see everyone’s views and choose whichever suits their needs best. Enable views in the project settings to start using them.", accessType: "project", - access: EUserProjectRoles.ADMIN, + access: [EUserPermissions.ADMIN], path: "/empty-state/disabled-feature/views", primaryButton: { text: "Manage features", diff --git a/web/core/constants/project.ts b/web/core/constants/project.ts index 4a7899fd9..83f3c34a1 100644 --- a/web/core/constants/project.ts +++ b/web/core/constants/project.ts @@ -2,13 +2,6 @@ import { Globe2, Lock, LucideIcon } from "lucide-react"; import { TProjectAppliedDisplayFilterKeys, TProjectOrderByOptions } from "@plane/types"; -export enum EUserProjectRoles { - GUEST = 5, - VIEWER = 10, - MEMBER = 15, - ADMIN = 20, -} - export const NETWORK_CHOICES: { key: 0 | 2; label: string; diff --git a/web/core/constants/workspace.ts b/web/core/constants/workspace.ts index 7345993f2..405c680d7 100644 --- a/web/core/constants/workspace.ts +++ b/web/core/constants/workspace.ts @@ -1,5 +1,6 @@ // types import { TStaticViewTypes } from "@plane/types"; +import { EUserPermissions } from "@/plane-web/constants/user-permissions"; // services images import CSVLogo from "@/public/services/csv.svg"; import ExcelLogo from "@/public/services/excel.svg"; @@ -7,34 +8,22 @@ import GithubLogo from "@/public/services/github.png"; import JiraLogo from "@/public/services/jira.svg"; import JSONLogo from "@/public/services/json.svg"; -export enum EUserWorkspaceRoles { - GUEST = 5, - VIEWER = 10, - MEMBER = 15, - ADMIN = 20, -} - export const ROLE = { - 5: "Guest", - 10: "Viewer", - 15: "Member", - 20: "Admin", + [EUserPermissions.GUEST]: "Guest", + [EUserPermissions.MEMBER]: "Member", + [EUserPermissions.ADMIN]: "Admin", }; export const ROLE_DETAILS = { - 5: { + [EUserPermissions.GUEST]: { title: "Guest", description: "External members of organizations can be invited as guests.", }, - 10: { - title: "Viewer", - description: "External members of organizations can be invited as guests.", - }, - 15: { + [EUserPermissions.MEMBER]: { title: "Member", description: "Ability to read, write, edit, and delete entities inside projects, cycles, and modules", }, - 20: { + [EUserPermissions.ADMIN]: { title: "Admin", description: "All permissions set to true within the workspace.", }, diff --git a/web/core/hooks/store/user/index.ts b/web/core/hooks/store/user/index.ts index f616bb4ef..8b00f82b0 100644 --- a/web/core/hooks/store/user/index.ts +++ b/web/core/hooks/store/user/index.ts @@ -1,3 +1,4 @@ export * from "./user-user"; export * from "./user-user-profile"; export * from "./user-user-settings"; +export * from "./user-permissions"; diff --git a/web/core/hooks/store/user/user-permissions.ts b/web/core/hooks/store/user/user-permissions.ts new file mode 100644 index 000000000..555e47674 --- /dev/null +++ b/web/core/hooks/store/user/user-permissions.ts @@ -0,0 +1,12 @@ +import { useContext } from "react"; +// mobx store +import { StoreContext } from "@/lib/store-context"; +// types +import { IUserPermissionStore } from "@/store/user/permissions.store"; + +export const useUserPermissions = (): IUserPermissionStore => { + const context = useContext(StoreContext); + if (context === undefined) throw new Error("useUserPermissions must be used within StoreProvider"); + + return context.user.permission; +}; diff --git a/web/core/layouts/auth-layout/project-wrapper.tsx b/web/core/layouts/auth-layout/project-wrapper.tsx index 095edfc85..1c7e241d9 100644 --- a/web/core/layouts/auth-layout/project-wrapper.tsx +++ b/web/core/layouts/auth-layout/project-wrapper.tsx @@ -16,9 +16,11 @@ import { useProject, useProjectState, useProjectView, - useUser, useCommandPalette, + useUserPermissions, } from "@/hooks/store"; +// plane web constants +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; // images import emptyProject from "@/public/empty-state/onboarding/dashboard-light.webp"; @@ -32,9 +34,7 @@ export const ProjectAuthWrapper: FC = observer((props) => { // const { fetchInboxes } = useInbox(); const { toggleCreateProjectModal } = useCommandPalette(); const { setTrackElement } = useEventTracker(); - const { - membership: { fetchUserProjectInfo, projectMemberInfo, hasPermissionToProject }, - } = useUser(); + const { fetchUserProjectInfo, allowPermissions, projectUserInfo } = useUserPermissions(); const { loader, getProjectById, fetchProjectDetails } = useProject(); const { fetchAllCycles } = useCycle(); const { fetchModules } = useModule(); @@ -48,6 +48,8 @@ export const ProjectAuthWrapper: FC = observer((props) => { // router const { workspaceSlug, projectId } = useParams(); + const projectMemberInfo = projectUserInfo?.[workspaceSlug.toString()]?.[projectId.toString()]; + // fetching project details useSWR( workspaceSlug && projectId ? `PROJECT_DETAILS_${workspaceSlug.toString()}_${projectId.toString()}` : null, @@ -55,7 +57,7 @@ export const ProjectAuthWrapper: FC = observer((props) => { ); // fetching user project member information useSWR( - workspaceSlug && projectId ? `PROJECT_MEMBERS_ME_${workspaceSlug}_${projectId}` : null, + workspaceSlug && projectId ? `PROJECT_ME_INFORMATION_${workspaceSlug}_${projectId}` : null, workspaceSlug && projectId ? () => fetchUserProjectInfo(workspaceSlug.toString(), projectId.toString()) : null ); // fetching project labels @@ -100,10 +102,18 @@ export const ProjectAuthWrapper: FC = observer((props) => { workspaceSlug && projectId ? () => fetchViews(workspaceSlug.toString(), projectId.toString()) : null, { revalidateIfStale: false, revalidateOnFocus: false } ); + + // derived values const projectExists = projectId ? getProjectById(projectId.toString()) : null; + const hasPermissionToCurrentProject = projectId + ? allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], + EUserPermissionsLevel.PROJECT + ) + : undefined; // check if the project member apis is loading - if (!projectMemberInfo && projectId && hasPermissionToProject[projectId.toString()] === null) + if (!projectMemberInfo && projectId && hasPermissionToCurrentProject === null) return (
@@ -113,10 +123,10 @@ export const ProjectAuthWrapper: FC = observer((props) => { ); // check if the user don't have permission to access the project - if (projectExists && projectId && hasPermissionToProject[projectId.toString()] === false) return ; + if (projectExists && projectId && hasPermissionToCurrentProject === false) return ; // check if the project info is not found. - if (!loader && !projectExists && projectId && !!hasPermissionToProject[projectId.toString()] === false) + if (!loader && !projectExists && projectId && !!hasPermissionToCurrentProject === false) return (
= observer((props) // next themes const { resolvedTheme } = useTheme(); // store hooks - const { membership, signOut, data: currentUser, canPerformWorkspaceMemberActions } = useUser(); + const { signOut, data: currentUser } = useUser(); const { fetchProjects } = useProject(); const { fetchFavorite } = useFavorite(); const { @@ -38,6 +40,13 @@ export const WorkspaceAuthWrapper: FC = observer((props) } = useMember(); const { workspaces } = useWorkspace(); const { isMobile } = usePlatformOS(); + const { workspaceInfoBySlug, fetchUserWorkspaceInfo, fetchUserProjectPermissions, allowPermissions } = + useUserPermissions(); + // derived values + const canPerformWorkspaceMemberActions = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); const planeLogo = resolvedTheme === "dark" ? PlaneWhiteLogo : PlaneBlackLogo; const allWorkspaces = workspaces ? Object.values(workspaces) : undefined; @@ -46,10 +55,16 @@ export const WorkspaceAuthWrapper: FC = observer((props) // fetching user workspace information useSWR( - workspaceSlug && currentWorkspace ? `WORKSPACE_MEMBERS_ME_${workspaceSlug}` : null, - workspaceSlug && currentWorkspace ? () => membership.fetchUserWorkspaceInfo(workspaceSlug.toString()) : null, + workspaceSlug && currentWorkspace ? `WORKSPACE_MEMBER_ME_INFORMATION_${workspaceSlug}` : null, + workspaceSlug && currentWorkspace ? () => fetchUserWorkspaceInfo(workspaceSlug.toString()) : null, { revalidateIfStale: false, revalidateOnFocus: false } ); + useSWR( + workspaceSlug && currentWorkspace ? `WORKSPACE_PROJECTS_ROLES_INFORMATION_${workspaceSlug}` : null, + workspaceSlug && currentWorkspace ? () => fetchUserProjectPermissions(workspaceSlug.toString()) : null, + { revalidateIfStale: false, revalidateOnFocus: false } + ); + // fetching workspace projects useSWR( workspaceSlug && currentWorkspace ? `WORKSPACE_PROJECTS_${workspaceSlug}` : null, @@ -62,14 +77,6 @@ export const WorkspaceAuthWrapper: FC = observer((props) workspaceSlug && currentWorkspace ? () => fetchWorkspaceMembers(workspaceSlug.toString()) : null, { revalidateIfStale: false, revalidateOnFocus: false } ); - // fetch workspace user projects role - useSWR( - workspaceSlug && currentWorkspace ? `WORKSPACE_PROJECTS_ROLE_${workspaceSlug}` : null, - workspaceSlug && currentWorkspace - ? () => membership.fetchUserWorkspaceProjectsRole(workspaceSlug.toString()) - : null, - { revalidateIfStale: false, revalidateOnFocus: false } - ); // fetch workspace favorite useSWR( workspaceSlug && currentWorkspace && canPerformWorkspaceMemberActions @@ -91,6 +98,9 @@ export const WorkspaceAuthWrapper: FC = observer((props) ); }; + // derived values + const currentWorkspaceInfo = workspaceSlug && workspaceInfoBySlug(workspaceSlug.toString()); + // if list of workspaces are not there then we have to render the spinner if (allWorkspaces === undefined) { return ( @@ -103,11 +113,7 @@ export const WorkspaceAuthWrapper: FC = observer((props) } // if workspaces are there and we are trying to access the workspace that we are not part of then show the existing workspaces - if ( - currentWorkspace === undefined && - !membership.currentWorkspaceMemberInfo && - membership.hasPermissionToCurrentWorkspace === undefined - ) { + if (currentWorkspace === undefined && !currentWorkspaceInfo) { return (
@@ -154,10 +160,7 @@ export const WorkspaceAuthWrapper: FC = observer((props) } // while user does not have access to view that workspace - if ( - membership.hasPermissionToCurrentWorkspace !== undefined && - membership.hasPermissionToCurrentWorkspace === false - ) { + if (currentWorkspaceInfo === undefined) { return (
diff --git a/web/core/lib/posthog-provider.tsx b/web/core/lib/posthog-provider.tsx index 92ae0bddc..563cc2c8e 100644 --- a/web/core/lib/posthog-provider.tsx +++ b/web/core/lib/posthog-provider.tsx @@ -3,6 +3,7 @@ import { FC, ReactNode, useEffect } from "react"; import { observer } from "mobx-react"; import dynamic from "next/dynamic"; +import { useParams } from "next/navigation"; import posthog from "posthog-js"; import { PostHogProvider as PHProvider } from "posthog-js/react"; // constants @@ -10,7 +11,7 @@ import { GROUP_WORKSPACE } from "@/constants/event-tracker"; // helpers import { getUserRole } from "@/helpers/user.helper"; // hooks -import { useWorkspace, useUser, useInstance } from "@/hooks/store"; +import { useWorkspace, useUser, useInstance, useUserPermissions } from "@/hooks/store"; // dynamic imports const PostHogPageView = dynamic(() => import("@/lib/posthog-view"), { ssr: false }); @@ -20,13 +21,14 @@ export interface IPosthogWrapper { const PostHogProvider: FC = observer((props) => { const { children } = props; - const { - data: user, - membership: { currentProjectRole, currentWorkspaceRole }, - } = useUser(); + const { data: user } = useUser(); const { currentWorkspace } = useWorkspace(); const { instance } = useInstance(); + const { workspaceSlug, projectId } = useParams(); + const { workspaceInfoBySlug, projectUserInfo } = useUserPermissions(); + const currentProjectRole = projectUserInfo?.[workspaceSlug?.toString()]?.[projectId?.toString()]?.role; + const currentWorkspaceRole = workspaceInfoBySlug(workspaceSlug?.toString())?.role; const is_telemetry_enabled = instance?.is_telemetry_enabled || false; useEffect(() => { diff --git a/web/core/services/project/project-member.service.ts b/web/core/services/project/project-member.service.ts index cdf656778..2a60a7b3e 100644 --- a/web/core/services/project/project-member.service.ts +++ b/web/core/services/project/project-member.service.ts @@ -66,3 +66,7 @@ export class ProjectMemberService extends APIService { }); } } + +const projectMemberService = new ProjectMemberService(); + +export default projectMemberService; diff --git a/web/core/services/user.service.ts b/web/core/services/user.service.ts index 6c75de94c..fef39f7e3 100644 --- a/web/core/services/user.service.ts +++ b/web/core/services/user.service.ts @@ -258,3 +258,7 @@ export class UserService extends APIService { }); } } + +const userService = new UserService(); + +export default userService; diff --git a/web/core/store/member/project-member.store.ts b/web/core/store/member/project-member.store.ts index 04752cf74..af6f74df0 100644 --- a/web/core/store/member/project-member.store.ts +++ b/web/core/store/member/project-member.store.ts @@ -10,8 +10,8 @@ import { IProjectMembership, IUserLite, } from "@plane/types"; -// constants -import { EUserProjectRoles } from "@/constants/project"; +// plane-web constants +import { EUserPermissions } from "@/plane-web/constants/user-permissions"; // services import { ProjectMemberService } from "@/services/project"; // store @@ -25,7 +25,7 @@ import { IMemberRootStore } from "."; export interface IProjectMemberDetails { id: string; member: IUserLite; - role: EUserProjectRoles; + role: EUserPermissions; } export interface IProjectMemberStore { @@ -51,7 +51,7 @@ export interface IProjectMemberStore { workspaceSlug: string, projectId: string, userId: string, - data: { role: EUserProjectRoles } + data: { role: EUserPermissions } ) => Promise; removeMemberFromProject: (workspaceSlug: string, projectId: string, userId: string) => Promise; } @@ -182,12 +182,7 @@ export class ProjectMemberStore implements IProjectMemberStore { * @param userId * @param data */ - updateMember = async ( - workspaceSlug: string, - projectId: string, - userId: string, - data: { role: EUserProjectRoles } - ) => { + updateMember = async (workspaceSlug: string, projectId: string, userId: string, data: { role: EUserPermissions }) => { const memberDetails = this.getProjectMemberDetails(userId); if (!memberDetails) throw new Error("Member not found"); // original data to revert back in case of error diff --git a/web/core/store/member/workspace-member.store.ts b/web/core/store/member/workspace-member.store.ts index 1550b9552..2b8a8ede7 100644 --- a/web/core/store/member/workspace-member.store.ts +++ b/web/core/store/member/workspace-member.store.ts @@ -4,8 +4,8 @@ import { action, computed, makeObservable, observable, runInAction } from "mobx" import { computedFn } from "mobx-utils"; // types import { IWorkspaceBulkInviteFormData, IWorkspaceMember, IWorkspaceMemberInvitation } from "@plane/types"; -// constants -import { EUserWorkspaceRoles } from "@/constants/workspace"; +// plane-web constants +import { EUserPermissions } from "@/plane-web/constants/user-permissions"; // services import { WorkspaceService } from "@/plane-web/services"; // types @@ -18,7 +18,7 @@ import { IMemberRootStore } from "."; export interface IWorkspaceMembership { id: string; member: string; - role: EUserWorkspaceRoles; + role: EUserPermissions; } export interface IWorkspaceMemberStore { @@ -38,7 +38,7 @@ export interface IWorkspaceMemberStore { fetchWorkspaceMembers: (workspaceSlug: string) => Promise; fetchWorkspaceMemberInvitations: (workspaceSlug: string) => Promise; // crud actions - updateMember: (workspaceSlug: string, userId: string, data: { role: EUserWorkspaceRoles }) => Promise; + updateMember: (workspaceSlug: string, userId: string, data: { role: EUserPermissions }) => Promise; removeMemberFromWorkspace: (workspaceSlug: string, userId: string) => Promise; // invite actions inviteMembersToWorkspace: (workspaceSlug: string, data: IWorkspaceBulkInviteFormData) => Promise; @@ -213,7 +213,7 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore { * @param userId * @param data */ - updateMember = async (workspaceSlug: string, userId: string, data: { role: EUserWorkspaceRoles }) => { + updateMember = async (workspaceSlug: string, userId: string, data: { role: EUserPermissions }) => { const memberDetails = this.getWorkspaceMemberDetails(userId); if (!memberDetails) throw new Error("Member not found"); // original data to revert back in case of error diff --git a/web/core/store/pages/page.ts b/web/core/store/pages/page.ts index d7f28c0f8..dd772ad01 100644 --- a/web/core/store/pages/page.ts +++ b/web/core/store/pages/page.ts @@ -4,7 +4,7 @@ import { action, computed, makeObservable, observable, reaction, runInAction } f import { TLogoProps, TPage } from "@plane/types"; // constants import { EPageAccess } from "@/constants/page"; -import { EUserProjectRoles } from "@/constants/project"; +import { EUserPermissions } from "@/plane-web/constants/user-permissions"; // services import { ProjectPageService } from "@/services/page"; // store @@ -214,24 +214,39 @@ export class Page implements IPage { * @description returns true if the current logged in user can edit the page */ get canCurrentUserEditPage() { - const currentUserProjectRole = this.store.user.membership.currentProjectRole; - return this.isCurrentUserOwner || (!!currentUserProjectRole && currentUserProjectRole >= EUserProjectRoles.MEMBER); + const { workspaceSlug, projectId } = this.store.router; + + const currentUserProjectRole = this.store.user.permission.projectPermissionsByWorkspaceSlugAndProjectId( + workspaceSlug?.toString() || "", + projectId?.toString() || "" + ); + return this.isCurrentUserOwner || (!!currentUserProjectRole && currentUserProjectRole >= EUserPermissions.MEMBER); } /** * @description returns true if the current logged in user can create a duplicate the page */ get canCurrentUserDuplicatePage() { - const currentUserProjectRole = this.store.user.membership.currentProjectRole; - return this.isCurrentUserOwner || (!!currentUserProjectRole && currentUserProjectRole >= EUserProjectRoles.MEMBER); + const { workspaceSlug, projectId } = this.store.router; + + const currentUserProjectRole = this.store.user.permission.projectPermissionsByWorkspaceSlugAndProjectId( + workspaceSlug?.toString() || "", + projectId?.toString() || "" + ); + return this.isCurrentUserOwner || (!!currentUserProjectRole && currentUserProjectRole >= EUserPermissions.MEMBER); } /** * @description returns true if the current logged in user can lock the page */ get canCurrentUserLockPage() { - const currentUserProjectRole = this.store.user.membership.currentProjectRole; - return this.isCurrentUserOwner || (!!currentUserProjectRole && currentUserProjectRole >= EUserProjectRoles.MEMBER); + const { workspaceSlug, projectId } = this.store.router; + + const currentUserProjectRole = this.store.user.permission.projectPermissionsByWorkspaceSlugAndProjectId( + workspaceSlug?.toString() || "", + projectId?.toString() || "" + ); + return this.isCurrentUserOwner || (!!currentUserProjectRole && currentUserProjectRole >= EUserPermissions.MEMBER); } /** @@ -245,32 +260,52 @@ export class Page implements IPage { * @description returns true if the current logged in user can archive the page */ get canCurrentUserArchivePage() { - const currentUserProjectRole = this.store.user.membership.currentProjectRole; - return this.isCurrentUserOwner || currentUserProjectRole === EUserProjectRoles.ADMIN; + const { workspaceSlug, projectId } = this.store.router; + + const currentUserProjectRole = this.store.user.permission.projectPermissionsByWorkspaceSlugAndProjectId( + workspaceSlug?.toString() || "", + projectId?.toString() || "" + ); + return this.isCurrentUserOwner || currentUserProjectRole === EUserPermissions.ADMIN; } /** * @description returns true if the current logged in user can delete the page */ get canCurrentUserDeletePage() { - const currentUserProjectRole = this.store.user.membership.currentProjectRole; - return this.isCurrentUserOwner || currentUserProjectRole === EUserProjectRoles.ADMIN; + const { workspaceSlug, projectId } = this.store.router; + + const currentUserProjectRole = this.store.user.permission.projectPermissionsByWorkspaceSlugAndProjectId( + workspaceSlug?.toString() || "", + projectId?.toString() || "" + ); + return this.isCurrentUserOwner || currentUserProjectRole === EUserPermissions.ADMIN; } /** * @description returns true if the current logged in user can favorite the page */ get canCurrentUserFavoritePage() { - const currentUserProjectRole = this.store.user.membership.currentProjectRole; - return !!currentUserProjectRole && currentUserProjectRole >= EUserProjectRoles.MEMBER; + const { workspaceSlug, projectId } = this.store.router; + + const currentUserProjectRole = this.store.user.permission.projectPermissionsByWorkspaceSlugAndProjectId( + workspaceSlug?.toString() || "", + projectId?.toString() || "" + ); + return !!currentUserProjectRole && currentUserProjectRole >= EUserPermissions.MEMBER; } /** * @description returns true if the page can be edited */ get isContentEditable() { + const { workspaceSlug, projectId } = this.store.router; + const isOwner = this.isCurrentUserOwner; - const currentUserRole = this.store.user.membership.currentProjectRole; + const currentUserRole = this.store.user.permission.projectPermissionsByWorkspaceSlugAndProjectId( + workspaceSlug?.toString() || "", + projectId?.toString() || "" + ); const isPublic = this.access === EPageAccess.PUBLIC; const isArchived = this.archived_at; const isLocked = this.is_locked; @@ -278,7 +313,7 @@ export class Page implements IPage { return ( !isArchived && !isLocked && - (isOwner || (isPublic && !!currentUserRole && currentUserRole >= EUserProjectRoles.MEMBER)) + (isOwner || (isPublic && !!currentUserRole && currentUserRole >= EUserPermissions.MEMBER)) ); } diff --git a/web/core/store/project/project.store.ts b/web/core/store/project/project.store.ts index ef817dab1..6bab9e2b3 100644 --- a/web/core/store/project/project.store.ts +++ b/web/core/store/project/project.store.ts @@ -354,7 +354,12 @@ export class ProjectStore implements IProjectStore { const response = await this.projectService.createProject(workspaceSlug, data); runInAction(() => { set(this.projectMap, [response.id], response); - set(this.rootStore.user.membership.workspaceProjectsRole, [workspaceSlug, response.id], response.member_role); + // updating the user project role in workspaceProjectsPermissions + set( + this.rootStore.user.permission.workspaceProjectsPermissions, + [workspaceSlug, response.id], + response.member_role + ); }); return response; } catch (error) { diff --git a/web/core/store/user/index.ts b/web/core/store/user/index.ts index 56eee659a..dee239c38 100644 --- a/web/core/store/user/index.ts +++ b/web/core/store/user/index.ts @@ -4,10 +4,9 @@ import { action, makeObservable, observable, runInAction, computed } from "mobx" // types import { IUser } from "@plane/types"; // constants -import { EUserProjectRoles } from "@/constants/project"; -import { EUserWorkspaceRoles } from "@/constants/workspace"; // helpers import { API_BASE_URL } from "@/helpers/common.helper"; +import { EUserPermissions } from "@/plane-web/constants/user-permissions"; // services import { AuthService } from "@/services/auth.service"; import { UserService } from "@/services/user.service"; @@ -15,8 +14,9 @@ import { UserService } from "@/services/user.service"; import { CoreRootStore } from "@/store/root.store"; import { IAccountStore } from "@/store/user/account.store"; import { ProfileStore, IUserProfileStore } from "@/store/user/profile.store"; -import { IUserMembershipStore, UserMembershipStore } from "@/store/user/user-membership.store"; +import { IUserPermissionStore, UserPermissionStore } from "./permissions.store"; import { IUserSettingsStore, UserSettingsStore } from "./settings.store"; +import { TUserPermissions } from "@plane/types/src/enums"; type TUserErrorStatus = { status: string; @@ -33,7 +33,7 @@ export interface IUserStore { userProfile: IUserProfileStore; userSettings: IUserSettingsStore; accounts: Record; - membership: IUserMembershipStore; + permission: IUserPermissionStore; // actions fetchCurrentUser: () => Promise; updateCurrentUser: (data: Partial) => Promise; @@ -42,18 +42,6 @@ export interface IUserStore { reset: () => void; signOut: () => Promise; // computed - - // workspace level - canPerformWorkspaceAdminActions: boolean; - canPerformWorkspaceMemberActions: boolean; - canPerformWorkspaceViewerActions: boolean; - canPerformWorkspaceGuestActions: boolean; - - // project level - canPerformProjectAdminActions: boolean; - canPerformProjectMemberActions: boolean; - canPerformProjectViewerActions: boolean; - canPerformProjectGuestActions: boolean; canPerformAnyCreateAction: boolean; projectsWithCreatePermissions: { [projectId: string]: number } | null; } @@ -68,7 +56,7 @@ export class UserStore implements IUserStore { userProfile: IUserProfileStore; userSettings: IUserSettingsStore; accounts: Record = {}; - membership: IUserMembershipStore; + permission: IUserPermissionStore; // service userService: UserService; authService: AuthService; @@ -77,7 +65,7 @@ export class UserStore implements IUserStore { // stores this.userProfile = new ProfileStore(store); this.userSettings = new UserSettingsStore(); - this.membership = new UserMembershipStore(store); + this.permission = new UserPermissionStore(store); // service this.userService = new UserService(); this.authService = new AuthService(); @@ -92,7 +80,7 @@ export class UserStore implements IUserStore { userProfile: observable, userSettings: observable, accounts: observable, - membership: observable, + permission: observable, // actions fetchCurrentUser: action, updateCurrentUser: action, @@ -101,16 +89,6 @@ export class UserStore implements IUserStore { reset: action, signOut: action, // computed - canPerformWorkspaceAdminActions: computed, - canPerformWorkspaceMemberActions: computed, - canPerformWorkspaceViewerActions: computed, - canPerformWorkspaceGuestActions: computed, - - canPerformProjectAdminActions: computed, - canPerformProjectMemberActions: computed, - canPerformProjectViewerActions: computed, - canPerformProjectGuestActions: computed, - canPerformAnyCreateAction: computed, projectsWithCreatePermissions: computed, }); @@ -158,26 +136,6 @@ export class UserStore implements IUserStore { } }; - /** - * @description fetches the prjects with write permissions - * @returns {{[projectId: string]: number} || null} - */ - fetchProjectsWithCreatePermissions() { - const allWorkspaceRoles = - this.membership.workspaceProjectsRole && - this.membership.workspaceProjectsRole[this.membership.router.workspaceSlug || ""]; - return ( - (allWorkspaceRoles && - Object.keys(allWorkspaceRoles) - .filter((key) => allWorkspaceRoles[key] >= EUserProjectRoles.MEMBER) - .reduce( - (res: { [projectId: string]: number }, key: string) => ((res[key] = allWorkspaceRoles[key]), res), - {} - )) || - null - ); - } - /** * @description updates the current user * @param data @@ -258,7 +216,7 @@ export class UserStore implements IUserStore { this.data = undefined; this.userProfile = new ProfileStore(this.store); this.userSettings = new UserSettingsStore(); - this.membership = new UserMembershipStore(this.store); + this.permission = new UserPermissionStore(this.store); }); }; @@ -271,6 +229,30 @@ export class UserStore implements IUserStore { this.store.resetOnSignOut(); }; + // helper actions + /** + * @description fetches the prjects with write permissions + * @returns {{[projectId: string]: number} || null} + */ + fetchProjectsWithCreatePermissions = (): { [key: string]: TUserPermissions } => { + const { workspaceSlug } = this.store.router; + + const allWorkspaceProjectRoles = + this.permission.workspaceProjectsPermissions && this.permission.workspaceProjectsPermissions[workspaceSlug || ""]; + + const userPermissions = + (allWorkspaceProjectRoles && + Object.keys(allWorkspaceProjectRoles) + .filter((key) => allWorkspaceProjectRoles[key] >= EUserPermissions.MEMBER) + .reduce( + (res: { [projectId: string]: number }, key: string) => ((res[key] = allWorkspaceProjectRoles[key]), res), + {} + )) || + null; + + return userPermissions; + }; + /** * @description returns projects where user has permissions * @returns {{[projectId: string]: number} || null} @@ -287,70 +269,4 @@ export class UserStore implements IUserStore { const filteredProjects = this.fetchProjectsWithCreatePermissions(); return filteredProjects ? Object.keys(filteredProjects).length > 0 : false; } - - /** - * @description returns true if user has workspace admin actions permissions - * @returns {boolean} - */ - get canPerformWorkspaceAdminActions() { - return !!this.membership.currentWorkspaceRole && this.membership.currentWorkspaceRole === EUserWorkspaceRoles.ADMIN; - } - - /** - * @description returns true if user has workspace member actions permissions - * @returns {boolean} - */ - get canPerformWorkspaceMemberActions() { - return !!this.membership.currentWorkspaceRole && this.membership.currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; - } - - /** - * @description returns true if user has workspace viewer actions permissions - * @returns {boolean} - */ - - get canPerformWorkspaceViewerActions() { - return !!this.membership.currentWorkspaceRole && this.membership.currentWorkspaceRole >= EUserWorkspaceRoles.VIEWER; - } - - /** - * @description returns true if user has workspace guest actions permissions - * @returns {boolean} - */ - get canPerformWorkspaceGuestActions() { - return !!this.membership.currentWorkspaceRole && this.membership.currentWorkspaceRole >= EUserWorkspaceRoles.GUEST; - } - - /** - * @description returns true if user has project admin actions permissions - * @returns {boolean} - */ - get canPerformProjectAdminActions() { - return !!this.membership.currentProjectRole && this.membership.currentProjectRole === EUserProjectRoles.ADMIN; - } - - /** - * @description returns true if user has project member actions permissions - * @returns {boolean} - */ - get canPerformProjectMemberActions() { - return !!this.membership.currentProjectRole && this.membership.currentProjectRole >= EUserProjectRoles.MEMBER; - } - - /** - * @description returns true if user has project viewer actions permissions - * @returns {boolean} - */ - - get canPerformProjectViewerActions() { - return !!this.membership.currentProjectRole && this.membership.currentProjectRole >= EUserProjectRoles.VIEWER; - } - - /** - * @description returns true if user has project guest actions permissions - * @returns {boolean} - */ - get canPerformProjectGuestActions() { - return !!this.membership.currentProjectRole && this.membership.currentProjectRole >= EUserProjectRoles.GUEST; - } } diff --git a/web/core/store/user/permissions.store.ts b/web/core/store/user/permissions.store.ts new file mode 100644 index 000000000..0fd89af8e --- /dev/null +++ b/web/core/store/user/permissions.store.ts @@ -0,0 +1,270 @@ +import set from "lodash/set"; +import unset from "lodash/unset"; +import { action, makeObservable, observable, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; +// types +import { IProjectMember, IUserProjectsRole, IWorkspaceMemberMe } from "@plane/types"; +// plane web types +import { + EUserPermissions, + EUserPermissionsLevel, + TUserPermissions, + TUserPermissionsLevel, +} from "@/plane-web/constants/user-permissions"; +// plane web services +import { WorkspaceService } from "@/plane-web/services/workspace.service"; +// services +import projectMemberService from "@/services/project/project-member.service"; +import userService from "@/services/user.service"; +// store +import { CoreRootStore } from "@/store/root.store"; + +// derived services +const workspaceService = new WorkspaceService(); + +export interface IUserPermissionStore { + // observables + workspaceUserInfo: Record; // workspaceSlug -> IWorkspaceMemberMe + projectUserInfo: Record>; // workspaceSlug -> projectId -> IProjectMember + workspaceProjectsPermissions: Record; // workspaceSlug -> IUserProjectsRole + // computed + // computed helpers + workspaceInfoBySlug: (workspaceSlug: string) => IWorkspaceMemberMe | undefined; + projectPermissionsByWorkspaceSlugAndProjectId: ( + workspaceSlug: string, + projectId: string + ) => TUserPermissions | undefined; + allowPermissions: ( + allowPermissions: TUserPermissions[], + level: TUserPermissionsLevel, + workspaceSlug?: string, + projectId?: string, + onPermissionAllowed?: () => boolean + ) => boolean; + // action helpers + // actions + fetchUserWorkspaceInfo: (workspaceSlug: string) => Promise; + leaveWorkspace: (workspaceSlug: string) => Promise; + fetchUserProjectInfo: (workspaceSlug: string, projectId: string) => Promise; + fetchUserProjectPermissions: (workspaceSlug: string) => Promise; + joinProject: (workspaceSlug: string, projectId: string) => Promise; + leaveProject: (workspaceSlug: string, projectId: string) => Promise; +} + +export class UserPermissionStore implements IUserPermissionStore { + // constants + workspaceUserInfo: Record = {}; + projectUserInfo: Record> = {}; + workspaceProjectsPermissions: Record = {}; + // observables + + constructor(private store: CoreRootStore) { + makeObservable(this, { + // observables + workspaceUserInfo: observable, + projectUserInfo: observable, + workspaceProjectsPermissions: observable, + // computed + // actions + fetchUserWorkspaceInfo: action, + leaveWorkspace: action, + fetchUserProjectInfo: action, + fetchUserProjectPermissions: action, + joinProject: action, + leaveProject: action, + }); + } + + // computed + + // computed helpers + /** + * @description Returns the current workspace information + * @param { string } workspaceSlug + * @returns { IWorkspaceMemberMe | undefined } + */ + workspaceInfoBySlug = computedFn((workspaceSlug: string): IWorkspaceMemberMe | undefined => { + if (!workspaceSlug) return undefined; + return this.workspaceUserInfo[workspaceSlug] || undefined; + }); + + /** + * @description Returns the current project permissions + * @param { string } workspaceSlug + * @param { string } projectId + * @returns { IUserProjectsRole | undefined } + */ + projectPermissionsByWorkspaceSlugAndProjectId = computedFn( + (workspaceSlug: string, projectId: string): TUserPermissions | undefined => { + if (!workspaceSlug || !projectId) return undefined; + return this.workspaceProjectsPermissions?.[workspaceSlug]?.[projectId] || undefined; + } + ); + + // action helpers + /** + * @description Returns whether the user has the permission to perform an action + * @param { TUserPermissions[] } allowPermissions + * @param { TUserPermissionsLevel } level + * @param { string } workspaceSlug + * @param { string } projectId + * @param { () => boolean } onPermissionAllowed + * @returns { boolean } + */ + allowPermissions = ( + allowPermissions: TUserPermissions[], + level: TUserPermissionsLevel, + workspaceSlug?: string, + projectId?: string, + onPermissionAllowed?: () => boolean + ): boolean => { + const { workspaceSlug: currentWorkspaceSlug, projectId: currentProjectId } = this.store.router; + if (!workspaceSlug) workspaceSlug = currentWorkspaceSlug; + if (!projectId) projectId = currentProjectId; + + let currentUserRole: TUserPermissions | undefined = undefined; + + if (level === EUserPermissionsLevel.WORKSPACE) { + const workspaceInfoBySlug = workspaceSlug && this.workspaceInfoBySlug(workspaceSlug); + if (workspaceInfoBySlug) { + currentUserRole = workspaceInfoBySlug?.role as unknown as EUserPermissions; + } + } + + if (level === EUserPermissionsLevel.PROJECT) { + currentUserRole = (workspaceSlug && + projectId && + this.projectPermissionsByWorkspaceSlugAndProjectId(workspaceSlug, projectId)) as EUserPermissions | undefined; + } + + if (currentUserRole && allowPermissions.includes(currentUserRole)) { + if (onPermissionAllowed) { + return onPermissionAllowed(); + } else { + return true; + } + } + + return false; + }; + + // actions + /** + * @description Fetches the user's workspace information + * @param { string } workspaceSlug + * @returns { Promise } + */ + fetchUserWorkspaceInfo = async (workspaceSlug: string): Promise => { + try { + const response = await workspaceService.workspaceMemberMe(workspaceSlug); + if (response) { + runInAction(() => { + set(this.workspaceUserInfo, [workspaceSlug], response); + }); + } + return response; + } catch (error) { + console.error("Error fetching user workspace information", error); + throw error; + } + }; + + /** + * @description Leaves a workspace + * @param { string } workspaceSlug + * @returns { Promise } + */ + leaveWorkspace = async (workspaceSlug: string): Promise => { + try { + await userService.leaveWorkspace(workspaceSlug); + runInAction(() => { + unset(this.workspaceUserInfo, workspaceSlug); + unset(this.projectUserInfo, workspaceSlug); + unset(this.workspaceProjectsPermissions, workspaceSlug); + }); + } catch (error) { + console.error("Error user leaving the workspace", error); + throw error; + } + }; + + /** + * @description Fetches the user's project information + * @param { string } workspaceSlug + * @param { string } projectId + * @returns { Promise } + */ + fetchUserProjectInfo = async (workspaceSlug: string, projectId: string): Promise => { + try { + const response = await projectMemberService.projectMemberMe(workspaceSlug, projectId); + if (response) { + runInAction(() => { + set(this.projectUserInfo, [workspaceSlug, projectId], response); + set(this.workspaceProjectsPermissions, [workspaceSlug, projectId], response.role); + }); + } + return response; + } catch (error) { + console.error("Error fetching user project information", error); + throw error; + } + }; + + /** + * @description Fetches the user's project permissions + * @param { string } workspaceSlug + * @returns { Promise } + */ + fetchUserProjectPermissions = async (workspaceSlug: string): Promise => { + try { + const response = await workspaceService.getWorkspaceUserProjectsRole(workspaceSlug); + runInAction(() => { + set(this.workspaceProjectsPermissions, [workspaceSlug], response); + }); + return response; + } catch (error) { + console.error("Error fetching user project permissions", error); + throw error; + } + }; + + /** + * @description Joins a project + * @param { string } workspaceSlug + * @param { string } projectId + * @returns { Promise } + */ + joinProject = async (workspaceSlug: string, projectId: string): Promise => { + try { + const response = await userService.joinProject(workspaceSlug, [projectId]); + if (response) { + runInAction(() => { + set(this.workspaceProjectsPermissions, [workspaceSlug, projectId], response); + }); + } + return response; + } catch (error) { + console.error("Error user joining the project", error); + throw error; + } + }; + + /** + * @description Leaves a project + * @param { string } workspaceSlug + * @param { string } projectId + * @returns { Promise } + */ + leaveProject = async (workspaceSlug: string, projectId: string): Promise => { + try { + await userService.leaveProject(workspaceSlug, projectId); + runInAction(() => { + unset(this.workspaceProjectsPermissions, [workspaceSlug, projectId]); + unset(this.projectUserInfo, [workspaceSlug, projectId]); + }); + } catch (error) { + console.error("Error user leaving the project", error); + throw error; + } + }; +} diff --git a/web/core/store/user/user-membership.store.ts b/web/core/store/user/user-membership.store.ts deleted file mode 100644 index 071d5c4b6..000000000 --- a/web/core/store/user/user-membership.store.ts +++ /dev/null @@ -1,283 +0,0 @@ -import set from "lodash/set"; -import update from "lodash/update"; -import { action, observable, runInAction, makeObservable, computed } from "mobx"; -// types -import { IWorkspaceMemberMe, IProjectMember, IUserProjectsRole, IProjectMemberLite } from "@plane/types"; -// constants -import { EUserProjectRoles } from "@/constants/project"; -import { EUserWorkspaceRoles } from "@/constants/workspace"; -// services -import { WorkspaceService } from "@/plane-web/services"; -import { ProjectMemberService } from "@/services/project"; -import { UserService } from "@/services/user.service"; -// plane web store -import { CoreRootStore } from "../root.store"; -import { IRouterStore } from "../router.store"; - -export interface IUserMembershipStore { - // observables - workspaceMemberInfo: { - [workspaceSlug: string]: IWorkspaceMemberMe; - }; - hasPermissionToWorkspace: { - [workspaceSlug: string]: boolean | null; - }; - projectMemberInfo: { - [projectId: string]: IProjectMember; - }; - hasPermissionToProject: { - [projectId: string]: boolean | null; - }; - workspaceProjectsRole: { [workspaceSlug: string]: IUserProjectsRole }; - // computed - currentProjectMemberInfo: IProjectMember | undefined; - currentWorkspaceMemberInfo: IWorkspaceMemberMe | undefined; - currentProjectRole: EUserProjectRoles | undefined; - currentWorkspaceRole: EUserWorkspaceRoles | undefined; - currentWorkspaceAllProjectsRole: IUserProjectsRole | undefined; - - // computed functions - currentProjectRoleByProjectId: (projectId: string) => EUserProjectRoles | undefined; - - hasPermissionToCurrentWorkspace: boolean | undefined; - hasPermissionToCurrentProject: boolean | undefined; - // fetch actions - fetchUserWorkspaceInfo: (workspaceSlug: string) => Promise; - fetchUserProjectInfo: (workspaceSlug: string, projectId: string) => Promise; - fetchUserWorkspaceProjectsRole: (workspaceSlug: string) => Promise; - // crud actions - leaveWorkspace: (workspaceSlug: string) => Promise; - joinProject: (workspaceSlug: string, projectIds: string[]) => Promise; - leaveProject: (workspaceSlug: string, projectId: string) => Promise; - - router: IRouterStore; -} - -export class UserMembershipStore implements IUserMembershipStore { - workspaceMemberInfo: { - [workspaceSlug: string]: IWorkspaceMemberMe; - } = {}; - hasPermissionToWorkspace: { - [workspaceSlug: string]: boolean; - } = {}; - projectMemberInfo: { - [projectId: string]: IProjectMember; - } = {}; - hasPermissionToProject: { - [projectId: string]: boolean; - } = {}; - workspaceProjectsRole: { [workspaceSlug: string]: IUserProjectsRole } = {}; - // stores - router; - store; - // services - userService; - workspaceService; - projectMemberService; - - constructor(_rootStore: CoreRootStore) { - makeObservable(this, { - // observables - workspaceMemberInfo: observable, - hasPermissionToWorkspace: observable, - projectMemberInfo: observable, - hasPermissionToProject: observable, - workspaceProjectsRole: observable, - // computed - currentWorkspaceMemberInfo: computed, - currentWorkspaceRole: computed, - currentProjectMemberInfo: computed, - currentProjectRole: computed, - currentWorkspaceAllProjectsRole: computed, - hasPermissionToCurrentWorkspace: computed, - hasPermissionToCurrentProject: computed, - // actions - fetchUserWorkspaceInfo: action, - fetchUserProjectInfo: action, - leaveWorkspace: action, - joinProject: action, - leaveProject: action, - fetchUserWorkspaceProjectsRole: action, - }); - this.router = _rootStore.router; - this.store = _rootStore; - // services - this.userService = new UserService(); - this.workspaceService = new WorkspaceService(); - this.projectMemberService = new ProjectMemberService(); - } - - /** - * Returns the current workspace member info - */ - get currentWorkspaceMemberInfo() { - if (!this.router.workspaceSlug) return; - return this.workspaceMemberInfo[this.router.workspaceSlug]; - } - - /** - * Returns the current workspace role - */ - get currentWorkspaceRole() { - if (!this.router.workspaceSlug) return; - return this.workspaceMemberInfo[this.router.workspaceSlug]?.role; - } - - /** - * Returns the current project member info - */ - get currentProjectMemberInfo() { - if (!this.router.projectId) return; - return this.projectMemberInfo[this.router.projectId]; - } - - /** - * Returns the current project role - */ - get currentProjectRole() { - if (!this.router.projectId) return; - return this.projectMemberInfo[this.router.projectId]?.role; - } - - /** - * Returns all projects role for the current workspace - */ - get currentWorkspaceAllProjectsRole() { - if (!this.router.workspaceSlug) return; - return this.workspaceProjectsRole?.[this.router.workspaceSlug]; - } - - /** - * Returns if the user has permission to the current workspace - */ - get hasPermissionToCurrentWorkspace() { - if (!this.router.workspaceSlug) return; - return this.hasPermissionToWorkspace[this.router.workspaceSlug]; - } - - /** - * Returns if the user has permission to the current project - */ - get hasPermissionToCurrentProject() { - if (!this.router.projectId) return; - return this.hasPermissionToProject[this.router.projectId]; - } - - // computed functions - /** - * Returns the current project role by project id - * @param projectId - * @returns EUserProjectRoles - */ - currentProjectRoleByProjectId = (projectId: string) => this.projectMemberInfo[projectId]?.role || undefined; - - /** - * Fetches the current user workspace info - * @param workspaceSlug - * @returns Promise - */ - fetchUserWorkspaceInfo = async (workspaceSlug: string) => - await this.workspaceService.workspaceMemberMe(workspaceSlug).then((response) => { - runInAction(() => { - set(this.workspaceMemberInfo, [workspaceSlug], response); - set(this.hasPermissionToWorkspace, [workspaceSlug], true); - }); - return response; - }); - - /** - * Fetches the current user project info - * @param workspaceSlug - * @param projectId - * @returns Promise - */ - fetchUserProjectInfo = async (workspaceSlug: string, projectId: string) => - await this.projectMemberService.projectMemberMe(workspaceSlug, projectId).then((response) => { - runInAction(() => { - this.projectMemberInfo = { - ...this.projectMemberInfo, - [projectId]: response, - }; - this.hasPermissionToProject = { - ...this.hasPermissionToProject, - [projectId]: true, - }; - }); - return response; - }); - - /** - * Leaves a workspace - * @param workspaceSlug - * @returns Promise - */ - leaveWorkspace = async (workspaceSlug: string) => { - const currentWorksSpace = this.store.workspaceRoot?.currentWorkspace; - await this.userService.leaveWorkspace(workspaceSlug).then(() => { - runInAction(() => { - if (currentWorksSpace) delete this.store.workspaceRoot?.workspaces?.[currentWorksSpace?.id]; - delete this.workspaceMemberInfo[workspaceSlug]; - delete this.hasPermissionToWorkspace[workspaceSlug]; - }); - }); - }; - - /** - * Joins a project - * @param workspaceSlug - * @param projectIds - * @returns Promise - */ - joinProject = async (workspaceSlug: string, projectIds: string[]) => - await this.userService.joinProject(workspaceSlug, projectIds).then(() => { - const newPermissions: { [projectId: string]: boolean } = {}; - projectIds.forEach((projectId) => { - newPermissions[projectId] = true; - }); - runInAction(() => { - this.hasPermissionToProject = { - ...this.hasPermissionToProject, - ...newPermissions, - }; - projectIds.forEach((projectId) => { - set(this.workspaceProjectsRole, [workspaceSlug, projectId], EUserProjectRoles.MEMBER); - }); - }); - }); - - /** - * Leaves a project - * @param workspaceSlug - * @param projectId - * @returns Promise - */ - leaveProject = async (workspaceSlug: string, projectId: string) => - await this.userService.leaveProject(workspaceSlug, projectId).then(() => { - // remove the user membership for a project - set(this.hasPermissionToProject, [projectId], false); - // update the project member list with the new permissions - set(this.store.projectRoot.project.projectMap, [projectId, "is_member"], false); - // remove user from project members list - update(this.store.projectRoot.project.projectMap, projectId, (project) => { - if (project) { - project.members = project.members.filter( - (member: IProjectMemberLite) => member.member_id !== this.store.user?.data?.id - ); - } - return project; - }); - }); - - /** - * Fetches the current user workspace projects role - * @param workspaceSlug - * @returns Promise - */ - fetchUserWorkspaceProjectsRole = async (workspaceSlug: string) => - await this.workspaceService.getWorkspaceUserProjectsRole(workspaceSlug).then((response) => { - runInAction(() => { - set(this.workspaceProjectsRole, [workspaceSlug], response); - }); - return response; - }); -} diff --git a/web/ee/constants/user-permissions/index.ts b/web/ee/constants/user-permissions/index.ts new file mode 100644 index 000000000..3647e0351 --- /dev/null +++ b/web/ee/constants/user-permissions/index.ts @@ -0,0 +1 @@ +export * from "ce/constants/user-permissions"; diff --git a/web/helpers/project.helper.ts b/web/helpers/project.helper.ts index 4e1489a6d..19082f9c3 100644 --- a/web/helpers/project.helper.ts +++ b/web/helpers/project.helper.ts @@ -2,10 +2,10 @@ import sortBy from "lodash/sortBy"; // types import { IProject, TProjectDisplayFilters, TProjectFilters, TProjectOrderByOptions } from "@plane/types"; // constants -import { EUserProjectRoles } from "@/constants/project"; // helpers import { getDate } from "@/helpers/date-time.helper"; import { satisfiesDateFilter } from "@/helpers/filter.helper"; +import { EUserPermissions } from "@/plane-web/constants/user-permissions"; /** * Updates the sort order of the project. @@ -53,7 +53,7 @@ export const projectIdentifierSanitizer = (identifier: string): string => * @returns {boolean} */ export const shouldRenderProject = (project: IProject): boolean => - !!project.member_role && project.member_role >= EUserProjectRoles.MEMBER; + !!project.member_role && project.member_role >= EUserPermissions.MEMBER; /** * @description filters projects based on the filter diff --git a/web/helpers/user.helper.ts b/web/helpers/user.helper.ts index 286fdc73a..9f60b15b8 100644 --- a/web/helpers/user.helper.ts +++ b/web/helpers/user.helper.ts @@ -1,15 +1,12 @@ -import { EUserProjectRoles } from "@/constants/project"; -import { EUserWorkspaceRoles } from "@/constants/workspace"; +import { EUserPermissions } from "@/plane-web/constants/user-permissions"; -export const getUserRole = (role: EUserWorkspaceRoles | EUserProjectRoles) => { +export const getUserRole = (role: EUserPermissions) => { switch (role) { - case EUserWorkspaceRoles.GUEST: + case EUserPermissions.GUEST: return "GUEST"; - case EUserWorkspaceRoles.VIEWER: - return "VIEWER"; - case EUserWorkspaceRoles.MEMBER: + case EUserPermissions.MEMBER: return "MEMBER"; - case EUserWorkspaceRoles.ADMIN: + case EUserPermissions.ADMIN: return "ADMIN"; } }; diff --git a/yarn.lock b/yarn.lock index bf42480ed..b1922e392 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3342,74 +3342,74 @@ resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-8.2.9.tgz#6eb066f8957272c0bcb0078a8a9bc378ca9311d3" integrity sha512-OL0NFvowPX85N5zIYdgeKKaFm7V4Vgtci093vL3cDZT13LGH6GuEzJKkUFGuUGNPFlJc+EgTj0o6PYKrOLyQ6w== -"@swc/core-darwin-arm64@1.7.24": - version "1.7.24" - resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.24.tgz#a2de30805965baa4e0d915472cf7009c091c7ce1" - integrity sha512-s0k09qAcsoa8jIncwgRRd43VApYqXu28R4OmICtDffV4S01HtsRLRarXsMuLutoZk3tbxqitep+A8MPBuqNgdg== +"@swc/core-darwin-arm64@1.7.26": + version "1.7.26" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.26.tgz#5f4096c00e71771ca1b18c824f0c92a052c70760" + integrity sha512-FF3CRYTg6a7ZVW4yT9mesxoVVZTrcSWtmZhxKCYJX9brH4CS/7PRPjAKNk6kzWgWuRoglP7hkjQcd6EpMcZEAw== -"@swc/core-darwin-x64@1.7.24": - version "1.7.24" - resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.7.24.tgz#855a20f6140908cc07ea6ccf8f65929d7d8af4c2" - integrity sha512-1dlsulJ/fiOoJoJyQgaCewIEaZ7Sh6aJN4r5Uhl4lIZuNWa27XOb28A3K29/6HDO9JML3IJrvXPnl5o0vxDQuQ== +"@swc/core-darwin-x64@1.7.26": + version "1.7.26" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.7.26.tgz#867b7a4f094e6b64201090ca5fcbf3da7d0f3e22" + integrity sha512-az3cibZdsay2HNKmc4bjf62QVukuiMRh5sfM5kHR/JMTrLyS6vSw7Ihs3UTkZjUxkLTT8ro54LI6sV6sUQUbLQ== -"@swc/core-linux-arm-gnueabihf@1.7.24": - version "1.7.24" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.24.tgz#895887705323cd29f86f4056b4c8906fcb9071b9" - integrity sha512-2ft1NmxyvHCu5CY4r2rNVybPqZtJaxpRSzvCcPlVjN/2D5Q3QgM5kBoo1t+0RCFfk4TS2V0KWJhtqKz0CNX62Q== +"@swc/core-linux-arm-gnueabihf@1.7.26": + version "1.7.26" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.26.tgz#35bb43894def296d92aaa2cc9372d48042f37777" + integrity sha512-VYPFVJDO5zT5U3RpCdHE5v1gz4mmR8BfHecUZTmD2v1JeFY6fv9KArJUpjrHEEsjK/ucXkQFmJ0jaiWXmpOV9Q== -"@swc/core-linux-arm64-gnu@1.7.24": - version "1.7.24" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.24.tgz#e76145b6e29ba1e22143a2f67407039c42f82105" - integrity sha512-v/Z8I9tUUNkNHKa1Sw4r1Q7Wp66ezbRhe6xMIxvPNKVJQFaMOsRpe0t8T5qbk5sV2hJGOCKpQynSpZqQXLcJDQ== +"@swc/core-linux-arm64-gnu@1.7.26": + version "1.7.26" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.26.tgz#8e2321cc4ec84cbfed8f8e16ff1ed7b854450443" + integrity sha512-YKevOV7abpjcAzXrhsl+W48Z9mZvgoVs2eP5nY+uoMAdP2b3GxC0Df1Co0I90o2lkzO4jYBpTMcZlmUXLdXn+Q== -"@swc/core-linux-arm64-musl@1.7.24": - version "1.7.24" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.24.tgz#b4e042ae4f949ed5d5b4cad65aacbf39ec222948" - integrity sha512-0jJx0IcajcyOXaJsx1jXy86lYVrbupyy2VUj/OiJux/ic4oBJLjfL+WOuc8T8/hZj2p6X0X4jvfSCqWSuic4kA== +"@swc/core-linux-arm64-musl@1.7.26": + version "1.7.26" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.26.tgz#b1c16e4b23ffa9ff19973eda6ffee35d2a7de7b0" + integrity sha512-3w8iZICMkQQON0uIcvz7+Q1MPOW6hJ4O5ETjA0LSP/tuKqx30hIniCGOgPDnv3UTMruLUnQbtBwVCZTBKR3Rkg== -"@swc/core-linux-x64-gnu@1.7.24": - version "1.7.24" - resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.24.tgz#b5b619aa7b73cbe6348e01fb982e093c97bbbaf6" - integrity sha512-2+3aKQpSGjVnWKDTKUPuJzitQlTQrGorg+PVFMRkv6l+RcNCHZQNe/8VYpMhyBhxDMb3LUlbp7776FRevcruxg== +"@swc/core-linux-x64-gnu@1.7.26": + version "1.7.26" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.26.tgz#388e2cc13a010cd28787aead2cecf31eb491836d" + integrity sha512-c+pp9Zkk2lqb06bNGkR2Looxrs7FtGDMA4/aHjZcCqATgp348hOKH5WPvNLBl+yPrISuWjbKDVn3NgAvfvpH4w== -"@swc/core-linux-x64-musl@1.7.24": - version "1.7.24" - resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.24.tgz#7c026225df4763b220061d69802a3e5fe9a2285a" - integrity sha512-PMQ6SkCtMoj0Ks77DiishpEmIuHpYjFLDuVOzzJCzGeGoii0yRP5lKy/VeglFYLPqJzmhK9BHlpVehVf/8ZpvA== +"@swc/core-linux-x64-musl@1.7.26": + version "1.7.26" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.26.tgz#51e0ff30981f26d7a5b97a7a7b5b291bad050d1a" + integrity sha512-PgtyfHBF6xG87dUSSdTJHwZ3/8vWZfNIXQV2GlwEpslrOkGqy+WaiiyE7Of7z9AvDILfBBBcJvJ/r8u980wAfQ== -"@swc/core-win32-arm64-msvc@1.7.24": - version "1.7.24" - resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.24.tgz#0cc829761f92343d9bdeeb0262a67c8d088b704d" - integrity sha512-SNdCa4DtGXNWrPVHqctVUxgEVZVETuqERpqF50KFHO0Bvf5V/m1IJ4hFr2BxXlrzgnIW4t1Dpi6YOJbcGbEmnA== +"@swc/core-win32-arm64-msvc@1.7.26": + version "1.7.26" + resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.26.tgz#a7fdcc4074c34ee6a026506b594d00323383c11f" + integrity sha512-9TNXPIJqFynlAOrRD6tUQjMq7KApSklK3R/tXgIxc7Qx+lWu8hlDQ/kVPLpU7PWvMMwC/3hKBW+p5f+Tms1hmA== -"@swc/core-win32-ia32-msvc@1.7.24": - version "1.7.24" - resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.24.tgz#7d6cb2aaa0f4203ec1090f607223d0dbabc66459" - integrity sha512-5p3olHqwibMfrVFg2yVuSIPh9HArDYYlJXNZ9JKqeZk23A19J1pl9MuPmXDw+sxsiPfYJ/nUedIGeUHPF/+EDw== +"@swc/core-win32-ia32-msvc@1.7.26": + version "1.7.26" + resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.26.tgz#ae7be6dde798eebee2000b8fd84e01a439b5bd6a" + integrity sha512-9YngxNcG3177GYdsTum4V98Re+TlCeJEP4kEwEg9EagT5s3YejYdKwVAkAsJszzkXuyRDdnHUpYbTrPG6FiXrQ== -"@swc/core-win32-x64-msvc@1.7.24": - version "1.7.24" - resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.24.tgz#dd8f7688f2f1404566fc252d4f45bf1f84426a3d" - integrity sha512-gRyPIxDznS8d2ClfmWbytjp2d48bij6swHnDLWhukNuOvXdQkEmaIzjEsionFG/zhcFLnz8zKfTvjEjInAMzxg== +"@swc/core-win32-x64-msvc@1.7.26": + version "1.7.26" + resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.26.tgz#310d607004d7319085a4dec20c0c38c3405cc05b" + integrity sha512-VR+hzg9XqucgLjXxA13MtV5O3C0bK0ywtLIBw/+a+O+Oc6mxFWHtdUeXDbIi5AiPbn0fjgVJMqYnyjGyyX8u0w== "@swc/core@^1.7.3": - version "1.7.24" - resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.7.24.tgz#f2a9f8ec44169d5bce374c0a4b59fde5bcc73bc1" - integrity sha512-FzJaai6z6DYdICAY1UKNN5pzTn296ksK2zzEjjaXlpZtoMkGktWT0ttS7hbdBCPGhLOu5Q9TA2zdPejKUFjgig== + version "1.7.26" + resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.7.26.tgz#beda9b82063fcec7b56c958804a4d175aecf9a9d" + integrity sha512-f5uYFf+TmMQyYIoxkn/evWhNGuUzC730dFwAKGwBVHHVoPyak1/GvJUm6i1SKl+2Hrj9oN0i3WSoWWZ4pgI8lw== dependencies: "@swc/counter" "^0.1.3" "@swc/types" "^0.1.12" optionalDependencies: - "@swc/core-darwin-arm64" "1.7.24" - "@swc/core-darwin-x64" "1.7.24" - "@swc/core-linux-arm-gnueabihf" "1.7.24" - "@swc/core-linux-arm64-gnu" "1.7.24" - "@swc/core-linux-arm64-musl" "1.7.24" - "@swc/core-linux-x64-gnu" "1.7.24" - "@swc/core-linux-x64-musl" "1.7.24" - "@swc/core-win32-arm64-msvc" "1.7.24" - "@swc/core-win32-ia32-msvc" "1.7.24" - "@swc/core-win32-x64-msvc" "1.7.24" + "@swc/core-darwin-arm64" "1.7.26" + "@swc/core-darwin-x64" "1.7.26" + "@swc/core-linux-arm-gnueabihf" "1.7.26" + "@swc/core-linux-arm64-gnu" "1.7.26" + "@swc/core-linux-arm64-musl" "1.7.26" + "@swc/core-linux-x64-gnu" "1.7.26" + "@swc/core-linux-x64-musl" "1.7.26" + "@swc/core-win32-arm64-msvc" "1.7.26" + "@swc/core-win32-ia32-msvc" "1.7.26" + "@swc/core-win32-x64-msvc" "1.7.26" "@swc/counter@^0.1.3": version "0.1.3" @@ -4038,9 +4038,9 @@ "@types/pg" "*" "@types/pg@*": - version "8.11.8" - resolved "https://registry.yarnpkg.com/@types/pg/-/pg-8.11.8.tgz#bc712f1ad8ca664acb1d321b42691d1a166a88d6" - integrity sha512-IqpCf8/569txXN/HoP5i1LjXfKZWL76Yr2R77xgeIICUbAYHeoaEZFhYHo2uDftecLWrTJUq63JvQu8q3lnDyA== + version "8.11.9" + resolved "https://registry.yarnpkg.com/@types/pg/-/pg-8.11.9.tgz#3e92f7edbe4df9de9397f5308d7fe80c31faefe8" + integrity sha512-M4mYeJZRBD9lCBCGa72F44uKSV9eJrAFfjlPJagdA6pgIr2OPJULFB7nqnZzOdqXG0qzHlgtZKzTdIgbmHitSg== dependencies: "@types/node" "*" pg-protocol "*" @@ -5078,12 +5078,12 @@ bare-path@^2.0.0, bare-path@^2.1.0: bare-os "^2.1.0" bare-stream@^2.0.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/bare-stream/-/bare-stream-2.2.1.tgz#0315ee6aa8e1b1d3bcc66e96e1ab427089496a0e" - integrity sha512-YTB47kHwBW9zSG8LD77MIBAAQXjU2WjAkMHeeb7hUplVs6+IoM5I7uEVQNPMB7lj9r8I76UMdoMkGnCodHOLqg== + version "2.3.0" + resolved "https://registry.yarnpkg.com/bare-stream/-/bare-stream-2.3.0.tgz#5bef1cab8222517315fca1385bd7f08dff57f435" + integrity sha512-pVRWciewGUeCyKEuRxwv06M079r+fRjAQjBEK2P6OYGrO43O+Z0LrPZZEjlc4mB6C2RpZ9AxJ1s7NLEtOHO6eA== dependencies: b4a "^1.6.6" - streamx "^2.18.0" + streamx "^2.20.0" base64-js@^1.3.1: version "1.5.1" @@ -5378,9 +5378,9 @@ chownr@^2.0.0: integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== chromatic@^11.4.0: - version "11.7.1" - resolved "https://registry.yarnpkg.com/chromatic/-/chromatic-11.7.1.tgz#9de59dd9d0e2a847627bccd959f05881335b524e" - integrity sha512-LvgPimdQdnQB07ZDxLEC2KtxgYeqTw0X71GA7fi3zhgtKLxZcE+BSZ/5I9rrQp1V8ydmfElfw0ZwnUH4fVgUAQ== + version "11.8.0" + resolved "https://registry.yarnpkg.com/chromatic/-/chromatic-11.8.0.tgz#acc560b91d8b520b92ee4978cf1671749d18342b" + integrity sha512-i60lgq/3UPpNwnuCgaYNiiRTSjPNv7YLTCRDvCQm+1SvdwyvG8N0R0SnFD08qHwvntZ16JyjJ/oXTaNEbYApMw== chrome-trace-event@^1.0.2: version "1.0.4" @@ -6271,9 +6271,9 @@ ee-first@1.1.1: integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== electron-to-chromium@^1.5.4: - version "1.5.18" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.18.tgz#5fe62b9d21efbcfa26571066502d94f3ed97e495" - integrity sha512-1OfuVACu+zKlmjsNdcJuVQuVE61sZOLbNM4JAQ1Rvh6EOj0/EUKhMJjRH73InPlXSh8HIJk1cVZ8pyOV/FMdUQ== + version "1.5.19" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.19.tgz#aeaa0a076f3f0f0e8db2c57fd10158508f00725a" + integrity sha512-kpLJJi3zxTR1U828P+LIUDZ5ohixyo68/IcYOHLqnbTPr/wdgn4i1ECvmALN9E16JPA6cvCG5UG79gVwVdEK5w== emoji-picker-react@^4.5.16: version "4.12.0" @@ -10454,9 +10454,9 @@ pstree.remy@^1.1.8: integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== pump@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + version "3.0.2" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.2.tgz#836f3edd6bc2ee599256c924ffe0d88573ddcbf8" + integrity sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw== dependencies: end-of-stream "^1.1.0" once "^1.3.1" @@ -11609,7 +11609,7 @@ streamsearch@^1.1.0: resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== -streamx@^2.15.0, streamx@^2.18.0: +streamx@^2.15.0, streamx@^2.20.0: version "2.20.0" resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.20.0.tgz#5f3608483499a9346852122b26042f964ceec931" integrity sha512-ZGd1LhDeGFucr1CUCTBOS58ZhEendd0ttpGT3usTvosS4ntIwKN9LJFp+OeCSprsCPL14BXVRZlHGRY1V9PVzQ== @@ -11625,16 +11625,7 @@ string-argv@~0.3.2: resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -11730,14 +11721,7 @@ string_decoder@^1.1.1, string_decoder@^1.3.0: dependencies: safe-buffer "~5.2.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -12631,9 +12615,9 @@ unplugin@1.0.1: webpack-virtual-modules "^0.5.0" unplugin@^1.3.1: - version "1.14.0" - resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-1.14.0.tgz#4455bdff958a0f29fbbb82bc143fd61688ff40d9" - integrity sha512-cfkZeALGyW7tKYjZbi0G+pn0XnUFa0QvLIeLJEUUlnU0R8YYsBQnt5+h9Eu1B7AB7KETld+UBFI5lOeBL+msoQ== + version "1.14.1" + resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-1.14.1.tgz#c76d6155a661e43e6a897bce6b767a1ecc344c1a" + integrity sha512-lBlHbfSFPToDYp9pjXlUEFVxYLaue9f9T1HC+4OHlmj+HnMDdz9oZY+erXfoCe/5V/7gKUSY2jpXPb9S7f0f/w== dependencies: acorn "^8.12.1" webpack-virtual-modules "^0.6.2" @@ -13011,16 +12995,7 @@ word-wrap@^1.2.5: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==