[WEB-2357] fix: update and redefine user roles across the platform (#5466)

* chore: removed viewer role

* chore: indentation

* chore: remove viewer role

* chore: handled user permissions in store

* chore: updated the migration file

* chore: updated user permissions store

* chore: removed the owner key

* chore: code refactor

* chore: code refactor

* chore: code refactor

* chore: code refactor

* chore: code refactor

* fix: build error

* chore: updated user permissions store and handled the permissions fetch in workspace and project wrappers

* chore: package user enum updated

* chore: user permission updated

* chore: user permission updated

* chore: resolved build errors

* chore: resolved build error

* chore: resolved build errors

* chore: computedFn deep map issue resolved

* chore: added back migration

* chore: added new field in project table

* chore: removed member store in users

* chore: private project for admins

* chore: workspace notification access validation updated

* fix: workspace member edit option

* fix: project intake permission validation updated

* chore: workspace export settings permission updated

* chore: guest_view_all_issues added

* chore: guest_view_all_issues added

* chore: key changed for guest access

* chore: added validation for individual issues

* chore: changed the dashboard issues count

* chore: added new yarn file

* chore: modified yarn file

* chore: project page permission updated

* chore: project page permission updated

* chore: member setting ux updated

* chore: build error

* fix: yarn lock

* fix: build error

---------

Co-authored-by: gurusainath <gurusainath007@gmail.com>
Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
This commit is contained in:
Bavisetti Narayan 2024-09-11 17:10:15 +05:30 committed by GitHub
parent 7013a36629
commit fdcd9a376c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
172 changed files with 2057 additions and 1627 deletions

View file

@ -210,7 +210,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
) )
# Only project members admins and created_by users can access this endpoint # 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 request.user.id
): ):
return Response( return Response(
@ -244,9 +244,8 @@ class InboxIssueAPIEndpoint(BaseAPIView):
workspace__slug=slug, workspace__slug=slug,
project_id=project_id, project_id=project_id,
) )
# Only allow guests and viewers to edit name and description # Only allow guests to edit name and description
if project_member.role <= 10: if project_member.role <= 5:
# viewers and guests since only viewers and guests
issue_data = { issue_data = {
"name": issue_data.get("name", issue.name), "name": issue_data.get("name", issue.name),
"description_html": issue_data.get( "description_html": issue_data.get(
@ -286,7 +285,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
) )
# Only project admins and members can edit inbox issue attributes # Only project admins and members can edit inbox issue attributes
if project_member.role > 10: if project_member.role > 5:
serializer = InboxIssueSerializer( serializer = InboxIssueSerializer(
inbox_issue, data=request.data, partial=True inbox_issue, data=request.data, partial=True
) )

View file

@ -133,7 +133,7 @@ class ProjectMemberAPIEndpoint(BaseAPIView):
workspace_member = WorkspaceMember.objects.create( workspace_member = WorkspaceMember.objects.create(
workspace=workspace, workspace=workspace,
member=user, member=user,
role=request.data.get("role", 10), role=request.data.get("role", 5),
) )
workspace_member.save() workspace_member.save()
@ -142,7 +142,7 @@ class ProjectMemberAPIEndpoint(BaseAPIView):
project_member = ProjectMember.objects.create( project_member = ProjectMember.objects.create(
project=project, project=project,
member=user, member=user,
role=request.data.get("role", 10), role=request.data.get("role", 5),
) )
project_member.save() project_member.save()

View file

@ -8,7 +8,6 @@ from enum import Enum
class ROLE(Enum): class ROLE(Enum):
ADMIN = 20 ADMIN = 20
MEMBER = 15 MEMBER = 15
VIEWER = 10
GUEST = 5 GUEST = 5

View file

@ -7,7 +7,6 @@ from plane.db.models import ProjectMember, WorkspaceMember
# Permission Mappings # Permission Mappings
Admin = 20 Admin = 20
Member = 15 Member = 15
Viewer = 10
Guest = 5 Guest = 5

View file

@ -6,9 +6,8 @@ from plane.db.models import WorkspaceMember
# Permission Mappings # Permission Mappings
Owner = 20 Admin = 20
Admin = 15 Member = 15
Member = 10
Guest = 5 Guest = 5
@ -31,7 +30,7 @@ class WorkSpaceBasePermission(BasePermission):
return WorkspaceMember.objects.filter( return WorkspaceMember.objects.filter(
member=request.user, member=request.user,
workspace__slug=view.workspace_slug, workspace__slug=view.workspace_slug,
role__in=[Owner, Admin], role__in=[Admin, Member],
is_active=True, is_active=True,
).exists() ).exists()
@ -40,7 +39,7 @@ class WorkSpaceBasePermission(BasePermission):
return WorkspaceMember.objects.filter( return WorkspaceMember.objects.filter(
member=request.user, member=request.user,
workspace__slug=view.workspace_slug, workspace__slug=view.workspace_slug,
role=Owner, role=Admin,
is_active=True, is_active=True,
).exists() ).exists()
@ -53,7 +52,7 @@ class WorkspaceOwnerPermission(BasePermission):
return WorkspaceMember.objects.filter( return WorkspaceMember.objects.filter(
workspace__slug=view.workspace_slug, workspace__slug=view.workspace_slug,
member=request.user, member=request.user,
role=Owner, role=Admin,
).exists() ).exists()
@ -65,7 +64,7 @@ class WorkSpaceAdminPermission(BasePermission):
return WorkspaceMember.objects.filter( return WorkspaceMember.objects.filter(
member=request.user, member=request.user,
workspace__slug=view.workspace_slug, workspace__slug=view.workspace_slug,
role__in=[Owner, Admin], role__in=[Admin, Member],
is_active=True, is_active=True,
).exists() ).exists()
@ -86,7 +85,7 @@ class WorkspaceEntityPermission(BasePermission):
return WorkspaceMember.objects.filter( return WorkspaceMember.objects.filter(
member=request.user, member=request.user,
workspace__slug=view.workspace_slug, workspace__slug=view.workspace_slug,
role__in=[Owner, Admin], role__in=[Admin, Member],
is_active=True, is_active=True,
).exists() ).exists()

View file

@ -21,7 +21,11 @@ from plane.app.permissions import allow_permission, ROLE
class AnalyticsEndpoint(BaseAPIView): class AnalyticsEndpoint(BaseAPIView):
@allow_permission( @allow_permission(
[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER], level="WORKSPACE" [
ROLE.ADMIN,
ROLE.MEMBER,
],
level="WORKSPACE",
) )
def get(self, request, slug): def get(self, request, slug):
x_axis = request.GET.get("x_axis", False) x_axis = request.GET.get("x_axis", False)
@ -203,7 +207,11 @@ class AnalyticViewViewset(BaseViewSet):
class SavedAnalyticEndpoint(BaseAPIView): class SavedAnalyticEndpoint(BaseAPIView):
@allow_permission( @allow_permission(
[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER], level="WORKSPACE" [
ROLE.ADMIN,
ROLE.MEMBER,
],
level="WORKSPACE",
) )
def get(self, request, slug, analytic_id): def get(self, request, slug, analytic_id):
analytic_view = AnalyticView.objects.get( analytic_view = AnalyticView.objects.get(
@ -236,7 +244,11 @@ class SavedAnalyticEndpoint(BaseAPIView):
class ExportAnalyticsEndpoint(BaseAPIView): class ExportAnalyticsEndpoint(BaseAPIView):
@allow_permission( @allow_permission(
[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER], level="WORKSPACE" [
ROLE.ADMIN,
ROLE.MEMBER,
],
level="WORKSPACE",
) )
def post(self, request, slug): def post(self, request, slug):
x_axis = request.data.get("x_axis", False) x_axis = request.data.get("x_axis", False)
@ -302,9 +314,7 @@ class ExportAnalyticsEndpoint(BaseAPIView):
class DefaultAnalyticsEndpoint(BaseAPIView): class DefaultAnalyticsEndpoint(BaseAPIView):
@allow_permission( @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST], level="WORKSPACE"
)
def get(self, request, slug): def get(self, request, slug):
filters = issue_filters(request.GET, "GET") filters = issue_filters(request.GET, "GET")
base_issues = Issue.issue_objects.filter( base_issues = Issue.issue_objects.filter(

View file

@ -288,7 +288,12 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
.distinct() .distinct()
) )
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER]) @allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
]
)
def get(self, request, slug, project_id, pk=None): def get(self, request, slug, project_id, pk=None):
if pk is None: if pk is None:
queryset = ( queryset = (

View file

@ -150,7 +150,7 @@ class CycleViewSet(BaseViewSet):
.distinct() .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): def list(self, request, slug, project_id):
queryset = self.get_queryset().filter(archived_at__isnull=True) queryset = self.get_queryset().filter(archived_at__isnull=True)
cycle_view = request.GET.get("cycle_view", "all") 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(cycle, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 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): def retrieve(self, request, slug, project_id, pk):
queryset = ( queryset = (
self.get_queryset().filter(archived_at__isnull=True).filter(pk=pk) self.get_queryset().filter(archived_at__isnull=True).filter(pk=pk)
@ -497,7 +502,6 @@ class CycleViewSet(BaseViewSet):
class CycleDateCheckEndpoint(BaseAPIView): class CycleDateCheckEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER]) @allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def post(self, request, slug, project_id): def post(self, request, slug, project_id):
start_date = request.data.get("start_date", False) start_date = request.data.get("start_date", False)
@ -566,7 +570,6 @@ class CycleFavoriteViewSet(BaseViewSet):
class TransferCycleIssueEndpoint(BaseAPIView): class TransferCycleIssueEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER]) @allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def post(self, request, slug, project_id, cycle_id): def post(self, request, slug, project_id, cycle_id):
new_cycle_id = request.data.get("new_cycle_id", False) new_cycle_id = request.data.get("new_cycle_id", False)
@ -977,8 +980,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
class CycleUserPropertiesEndpoint(BaseAPIView): class CycleUserPropertiesEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
def patch(self, request, slug, project_id, cycle_id): def patch(self, request, slug, project_id, cycle_id):
cycle_properties = CycleUserProperties.objects.get( cycle_properties = CycleUserProperties.objects.get(
user=request.user, user=request.user,
@ -1001,7 +1003,7 @@ class CycleUserPropertiesEndpoint(BaseAPIView):
serializer = CycleUserPropertiesSerializer(cycle_properties) serializer = CycleUserPropertiesSerializer(cycle_properties)
return Response(serializer.data, status=status.HTTP_201_CREATED) 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): def get(self, request, slug, project_id, cycle_id):
cycle_properties, _ = CycleUserProperties.objects.get_or_create( cycle_properties, _ = CycleUserProperties.objects.get_or_create(
user=request.user, user=request.user,
@ -1014,10 +1016,8 @@ class CycleUserPropertiesEndpoint(BaseAPIView):
class CycleProgressEndpoint(BaseAPIView): class CycleProgressEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
def get(self, request, slug, project_id, cycle_id): def get(self, request, slug, project_id, cycle_id):
aggregate_estimates = ( aggregate_estimates = (
Issue.issue_objects.filter( Issue.issue_objects.filter(
estimate_point__estimate__type="points", estimate_point__estimate__type="points",
@ -1148,10 +1148,9 @@ class CycleProgressEndpoint(BaseAPIView):
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )
class CycleAnalyticsEndpoint(BaseAPIView): 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): def get(self, request, slug, project_id, cycle_id):
analytic_type = request.GET.get("type", "issues") analytic_type = request.GET.get("type", "issues")
cycle = ( cycle = (

View file

@ -80,7 +80,12 @@ class CycleIssueViewSet(BaseViewSet):
) )
@method_decorator(gzip_page) @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): def list(self, request, slug, project_id, cycle_id):
order_by_param = request.GET.get("order_by", "created_at") order_by_param = request.GET.get("order_by", "created_at")
filters = issue_filters(request.query_params, "GET") filters = issue_filters(request.query_params, "GET")

View file

@ -52,23 +52,28 @@ from .. import BaseAPIView
def dashboard_overview_stats(self, request, slug): 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 = ( assigned_issues = (
Issue.issue_objects.filter( Issue.issue_objects.filter(
project__project_projectmember__is_active=True, project__project_projectmember__is_active=True,
project__project_projectmember__member=request.user, project__project_projectmember__member=request.user,
workspace__slug=slug, workspace__slug=slug,
assignees__in=[request.user], 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() .count()
) )
@ -80,8 +85,22 @@ def dashboard_overview_stats(self, request, slug):
project__project_projectmember__member=request.user, project__project_projectmember__member=request.user,
workspace__slug=slug, workspace__slug=slug,
assignees__in=[request.user], 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() .count()
) )
@ -91,8 +110,22 @@ def dashboard_overview_stats(self, request, slug):
project__project_projectmember__is_active=True, project__project_projectmember__is_active=True,
project__project_projectmember__member=request.user, project__project_projectmember__member=request.user,
created_by_id=request.user.id, 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() .count()
) )
@ -103,8 +136,22 @@ def dashboard_overview_stats(self, request, slug):
project__project_projectmember__member=request.user, project__project_projectmember__member=request.user,
assignees__in=[request.user], assignees__in=[request.user],
state__group="completed", 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() .count()
) )

View file

@ -28,7 +28,12 @@ def generate_random_name(length=10):
class ProjectEstimatePointEndpoint(BaseAPIView): class ProjectEstimatePointEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER]) @allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
]
)
def get(self, request, slug, project_id): def get(self, request, slug, project_id):
project = Project.objects.get(workspace__slug=slug, pk=project_id) project = Project.objects.get(workspace__slug=slug, pk=project_id)
if project.estimate_id is not None: if project.estimate_id is not None:

View file

@ -165,7 +165,7 @@ class InboxIssueViewSet(BaseViewSet):
) )
).distinct() ).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): def list(self, request, slug, project_id):
inbox_id = Inbox.objects.get( inbox_id = Inbox.objects.get(
workspace__slug=slug, project_id=project_id workspace__slug=slug, project_id=project_id
@ -338,7 +338,7 @@ class InboxIssueViewSet(BaseViewSet):
is_active=True, is_active=True,
) )
# Only project members admins and created_by users can access this endpoint # 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 request.user.id
): ):
return Response( return Response(
@ -371,9 +371,8 @@ class InboxIssueViewSet(BaseViewSet):
workspace__slug=slug, workspace__slug=slug,
project_id=project_id, project_id=project_id,
) )
# Only allow guests and viewers to edit name and description # Only allow guests to edit name and description
if project_member.role <= 10: if project_member.role <= 5:
# viewers and guests since only viewers and guests
issue_data = { issue_data = {
"name": issue_data.get("name", issue.name), "name": issue_data.get("name", issue.name),
"description_html": issue_data.get( "description_html": issue_data.get(
@ -415,7 +414,7 @@ class InboxIssueViewSet(BaseViewSet):
) )
# Only project admins and members can edit inbox issue attributes # Only project admins and members can edit inbox issue attributes
if project_member.role > 10: if project_member.role > 5:
serializer = InboxIssueSerializer( serializer = InboxIssueSerializer(
inbox_issue, data=request.data, partial=True inbox_issue, data=request.data, partial=True
) )
@ -515,7 +514,10 @@ class InboxIssueViewSet(BaseViewSet):
return Response(serializer, status=status.HTTP_200_OK) return Response(serializer, status=status.HTTP_200_OK)
@allow_permission( @allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER], allowed_roles=[
ROLE.ADMIN,
ROLE.MEMBER,
],
creator=True, creator=True,
model=Issue, model=Issue,
) )

View file

@ -19,7 +19,11 @@ from plane.app.serializers import (
IssueActivitySerializer, IssueActivitySerializer,
IssueCommentSerializer, IssueCommentSerializer,
) )
from plane.app.permissions import ProjectEntityPermission, allow_permission, ROLE from plane.app.permissions import (
ProjectEntityPermission,
allow_permission,
ROLE,
)
from plane.db.models import ( from plane.db.models import (
IssueActivity, IssueActivity,
IssueComment, IssueComment,
@ -33,7 +37,13 @@ class IssueActivityEndpoint(BaseAPIView):
] ]
@method_decorator(gzip_page) @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): def get(self, request, slug, project_id, issue_id):
filters = {} filters = {}
if request.GET.get("created_at__gt", None) is not None: if request.GET.get("created_at__gt", None) is not None:

View file

@ -97,7 +97,12 @@ class IssueArchiveViewSet(BaseViewSet):
) )
@method_decorator(gzip_page) @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): def list(self, request, slug, project_id):
filters = issue_filters(request.query_params, "GET") filters = issue_filters(request.query_params, "GET")
show_sub_issues = request.GET.get("show_sub_issues", "true") 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): def retrieve(self, request, slug, project_id, pk=None):
issue = ( issue = (
self.get_queryset() self.get_queryset()

View file

@ -64,7 +64,13 @@ class IssueAttachmentEndpoint(BaseAPIView):
return Response(status=status.HTTP_204_NO_CONTENT) 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): def get(self, request, slug, project_id, issue_id):
issue_attachments = IssueAttachment.objects.filter( issue_attachments = IssueAttachment.objects.filter(
issue_id=issue_id, workspace__slug=slug, project_id=project_id issue_id=issue_id, workspace__slug=slug, project_id=project_id

View file

@ -62,7 +62,7 @@ from plane.bgtasks.webhook_task import model_activity
class IssueListEndpoint(BaseAPIView): 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): def get(self, request, slug, project_id):
issue_ids = request.GET.get("issues", False) issue_ids = request.GET.get("issues", False)
@ -232,8 +232,9 @@ class IssueViewSet(BaseViewSet):
).distinct() ).distinct()
@method_decorator(gzip_page) @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): def list(self, request, slug, project_id):
project = Project.objects.get(pk=project_id, workspace__slug=slug)
filters = issue_filters(request.query_params, "GET") filters = issue_filters(request.query_params, "GET")
order_by_param = request.GET.get("order_by", "-created_at") order_by_param = request.GET.get("order_by", "-created_at")
@ -264,13 +265,16 @@ class IssueViewSet(BaseViewSet):
entity_identifier=project_id, entity_identifier=project_id,
user_id=request.user.id, user_id=request.user.id,
) )
if ProjectMember.objects.filter( if (
workspace__slug=slug, ProjectMember.objects.filter(
project_id=project_id, workspace__slug=slug,
member=request.user, project_id=project_id,
role=5, member=request.user,
is_active=True, role=5,
).exists(): is_active=True,
).exists()
and not project.guest_view_all_features
):
issue_queryset = issue_queryset.filter(created_by=request.user) issue_queryset = issue_queryset.filter(created_by=request.user)
if group_by: if group_by:
@ -440,9 +444,17 @@ class IssueViewSet(BaseViewSet):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@allow_permission( @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): def retrieve(self, request, slug, project_id, pk=None):
project = Project.objects.get(pk=project_id, workspace__slug=slug)
issue = ( issue = (
self.get_queryset() self.get_queryset()
.filter(pk=pk) .filter(pk=pk)
@ -511,6 +523,27 @@ class IssueViewSet(BaseViewSet):
status=status.HTTP_404_NOT_FOUND, 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( recent_visited_task.delay(
slug=slug, slug=slug,
entity_name="issue", entity_name="issue",
@ -522,7 +555,9 @@ class IssueViewSet(BaseViewSet):
serializer = IssueDetailSerializer(issue, expand=self.expand) serializer = IssueDetailSerializer(issue, expand=self.expand)
return Response(serializer.data, status=status.HTTP_200_OK) 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): def partial_update(self, request, slug, project_id, pk=None):
issue = ( issue = (
self.get_queryset() self.get_queryset()
@ -618,7 +653,7 @@ class IssueViewSet(BaseViewSet):
class IssueUserDisplayPropertyEndpoint(BaseAPIView): 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): def patch(self, request, slug, project_id):
issue_property = IssueUserProperty.objects.get( issue_property = IssueUserProperty.objects.get(
user=request.user, user=request.user,
@ -638,7 +673,13 @@ class IssueUserDisplayPropertyEndpoint(BaseAPIView):
serializer = IssueUserPropertySerializer(issue_property) serializer = IssueUserPropertySerializer(issue_property)
return Response(serializer.data, status=status.HTTP_201_CREATED) 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): def get(self, request, slug, project_id):
issue_property, _ = IssueUserProperty.objects.get_or_create( issue_property, _ = IssueUserProperty.objects.get_or_create(
user=request.user, project_id=project_id user=request.user, project_id=project_id
@ -719,7 +760,7 @@ class IssuePaginatedViewSet(BaseViewSet):
return paginated_data 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): def list(self, request, slug, project_id):
cursor = request.GET.get("cursor", None) cursor = request.GET.get("cursor", None)
is_description_required = request.GET.get("description", False) is_description_required = request.GET.get("description", False)

View file

@ -16,7 +16,7 @@ from plane.app.serializers import (
IssueCommentSerializer, IssueCommentSerializer,
CommentReactionSerializer, CommentReactionSerializer,
) )
from plane.app.permissions import ProjectLitePermission, allow_permission, ROLE from plane.app.permissions import allow_permission, ROLE
from plane.db.models import ( from plane.db.models import (
IssueComment, IssueComment,
ProjectMember, ProjectMember,
@ -63,7 +63,12 @@ class IssueCommentViewSet(BaseViewSet):
.distinct() .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): def create(self, request, slug, project_id, issue_id):
serializer = IssueCommentSerializer(data=request.data) serializer = IssueCommentSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
@ -156,9 +161,6 @@ class IssueCommentViewSet(BaseViewSet):
class CommentReactionViewSet(BaseViewSet): class CommentReactionViewSet(BaseViewSet):
serializer_class = CommentReactionSerializer serializer_class = CommentReactionSerializer
model = CommentReaction model = CommentReaction
permission_classes = [
ProjectLitePermission,
]
def get_queryset(self): def get_queryset(self):
return ( return (
@ -176,6 +178,12 @@ class CommentReactionViewSet(BaseViewSet):
.distinct() .distinct()
) )
@allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
]
)
def create(self, request, slug, project_id, comment_id): def create(self, request, slug, project_id, comment_id):
serializer = CommentReactionSerializer(data=request.data) serializer = CommentReactionSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
@ -198,6 +206,12 @@ class CommentReactionViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 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): def destroy(self, request, slug, project_id, comment_id, reaction_code):
comment_reaction = CommentReaction.objects.get( comment_reaction = CommentReaction.objects.get(
workspace__slug=slug, workspace__slug=slug,

View file

@ -43,7 +43,7 @@ class LabelViewSet(BaseViewSet):
@invalidate_cache( @invalidate_cache(
path="/api/workspaces/:slug/labels/", url_params=True, user=False 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): def create(self, request, slug, project_id):
try: try:
serializer = LabelSerializer(data=request.data) serializer = LabelSerializer(data=request.data)
@ -66,14 +66,14 @@ class LabelViewSet(BaseViewSet):
@invalidate_cache( @invalidate_cache(
path="/api/workspaces/:slug/labels/", url_params=True, user=False 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): def partial_update(self, request, *args, **kwargs):
return super().partial_update(request, *args, **kwargs) return super().partial_update(request, *args, **kwargs)
@invalidate_cache( @invalidate_cache(
path="/api/workspaces/:slug/labels/", url_params=True, user=False 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): def destroy(self, request, *args, **kwargs):
return super().destroy(request, *args, **kwargs) return super().destroy(request, *args, **kwargs)

View file

@ -317,7 +317,12 @@ class ModuleViewSet(BaseViewSet):
.order_by("-is_favorite", "-created_at") .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): def create(self, request, slug, project_id):
project = Project.objects.get(workspace__slug=slug, pk=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(module, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 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): def list(self, request, slug, project_id):
queryset = self.get_queryset().filter(archived_at__isnull=True) queryset = self.get_queryset().filter(archived_at__isnull=True)
@ -430,7 +435,12 @@ class ModuleViewSet(BaseViewSet):
) )
return Response(modules, status=status.HTTP_200_OK) 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): def retrieve(self, request, slug, project_id, pk):
queryset = ( queryset = (
@ -861,7 +871,7 @@ class ModuleFavoriteViewSet(BaseViewSet):
class ModuleUserPropertiesEndpoint(BaseAPIView): 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): def patch(self, request, slug, project_id, module_id):
module_properties = ModuleUserProperties.objects.get( module_properties = ModuleUserProperties.objects.get(
user=request.user, user=request.user,
@ -884,7 +894,7 @@ class ModuleUserPropertiesEndpoint(BaseAPIView):
serializer = ModuleUserPropertiesSerializer(module_properties) serializer = ModuleUserPropertiesSerializer(module_properties)
return Response(serializer.data, status=status.HTTP_201_CREATED) 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): def get(self, request, slug, project_id, module_id):
module_properties, _ = ModuleUserProperties.objects.get_or_create( module_properties, _ = ModuleUserProperties.objects.get_or_create(
user=request.user, user=request.user,

View file

@ -91,7 +91,12 @@ class ModuleIssueViewSet(BaseViewSet):
).distinct() ).distinct()
@method_decorator(gzip_page) @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): def list(self, request, slug, project_id, module_id):
filters = issue_filters(request.query_params, "GET") filters = issue_filters(request.query_params, "GET")
issue_queryset = self.get_queryset().filter(**filters) issue_queryset = self.get_queryset().filter(**filters)

View file

@ -41,7 +41,7 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
) )
@allow_permission( @allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST], allowed_roles=[ROLE.ADMIN, ROLE.MEMBER],
level="WORKSPACE", level="WORKSPACE",
) )
def list(self, request, slug): def list(self, request, slug):
@ -174,7 +174,7 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
@allow_permission( @allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST], allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST],
level="WORKSPACE", level="WORKSPACE",
) )
def partial_update(self, request, slug, pk): 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) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@allow_permission( @allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST], allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
level="WORKSPACE",
) )
def mark_read(self, request, slug, pk): def mark_read(self, request, slug, pk):
notification = Notification.objects.get( notification = Notification.objects.get(
@ -246,7 +245,7 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
class UnreadNotificationEndpoint(BaseAPIView): class UnreadNotificationEndpoint(BaseAPIView):
@allow_permission( @allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST], allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST],
level="WORKSPACE", level="WORKSPACE",
) )
def get(self, request, slug): def get(self, request, slug):

View file

@ -32,8 +32,10 @@ from plane.db.models import (
UserFavorite, UserFavorite,
ProjectMember, ProjectMember,
ProjectPage, ProjectPage,
Project,
) )
from plane.utils.error_codes import ERROR_CODES from plane.utils.error_codes import ERROR_CODES
# Module imports # Module imports
from ..base import BaseAPIView, BaseViewSet from ..base import BaseAPIView, BaseViewSet
@ -120,7 +122,7 @@ class PageViewSet(BaseViewSet):
.distinct() .distinct()
) )
@allow_permission([ROLE.ADMIN, ROLE.MEMBER]) @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def create(self, request, slug, project_id): def create(self, request, slug, project_id):
serializer = PageSerializer( serializer = PageSerializer(
data=request.data, data=request.data,
@ -142,7 +144,7 @@ class PageViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 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): def partial_update(self, request, slug, project_id, pk):
try: try:
page = Page.objects.get( page = Page.objects.get(
@ -208,9 +210,38 @@ class PageViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST, 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): def retrieve(self, request, slug, project_id, pk=None):
page = self.get_queryset().filter(pk=pk).first() 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: if page is None:
return Response( return Response(
{"error": "Page not found"}, {"error": "Page not found"},
@ -234,7 +265,7 @@ class PageViewSet(BaseViewSet):
status=status.HTTP_200_OK, 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): def lock(self, request, slug, project_id, pk):
page = Page.objects.filter( page = Page.objects.filter(
pk=pk, workspace__slug=slug, projects__id=project_id pk=pk, workspace__slug=slug, projects__id=project_id
@ -244,7 +275,7 @@ class PageViewSet(BaseViewSet):
page.save() page.save()
return Response(status=status.HTTP_204_NO_CONTENT) 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): def unlock(self, request, slug, project_id, pk):
page = Page.objects.filter( page = Page.objects.filter(
pk=pk, workspace__slug=slug, projects__id=project_id pk=pk, workspace__slug=slug, projects__id=project_id
@ -255,7 +286,7 @@ class PageViewSet(BaseViewSet):
return Response(status=status.HTTP_204_NO_CONTENT) 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): def access(self, request, slug, project_id, pk):
access = request.data.get("access", 0) access = request.data.get("access", 0)
page = Page.objects.filter( page = Page.objects.filter(
@ -278,13 +309,31 @@ class PageViewSet(BaseViewSet):
page.save() page.save()
return Response(status=status.HTTP_204_NO_CONTENT) 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): def list(self, request, slug, project_id):
queryset = self.get_queryset() 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 pages = PageSerializer(queryset, many=True).data
return Response(pages, status=status.HTTP_200_OK) 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): def archive(self, request, slug, project_id, pk):
page = Page.objects.get( page = Page.objects.get(
pk=pk, workspace__slug=slug, projects__id=project_id pk=pk, workspace__slug=slug, projects__id=project_id
@ -319,7 +368,7 @@ class PageViewSet(BaseViewSet):
status=status.HTTP_200_OK, 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): def unarchive(self, request, slug, project_id, pk):
page = Page.objects.get( page = Page.objects.get(
pk=pk, workspace__slug=slug, projects__id=project_id pk=pk, workspace__slug=slug, projects__id=project_id
@ -477,7 +526,13 @@ class SubPagesEndpoint(BaseAPIView):
class PagesDescriptionViewSet(BaseViewSet): 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): def retrieve(self, request, slug, project_id, pk):
page = ( page = (
Page.objects.filter( Page.objects.filter(
@ -507,7 +562,7 @@ class PagesDescriptionViewSet(BaseViewSet):
) )
return response 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): def partial_update(self, request, slug, project_id, pk):
page = ( page = (
Page.objects.filter( Page.objects.filter(

View file

@ -15,7 +15,7 @@ from plane.app.permissions import allow_permission, ROLE
class PageVersionEndpoint(BaseAPIView): class PageVersionEndpoint(BaseAPIView):
@allow_permission( @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): def get(self, request, slug, project_id, page_id, pk=None):
# Check if pk is provided # Check if pk is provided

View file

@ -71,13 +71,6 @@ class ProjectViewSet(BaseViewSet):
super() super()
.get_queryset() .get_queryset()
.filter(workspace__slug=self.kwargs.get("slug")) .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( .select_related(
"workspace", "workspace",
"workspace__owner", "workspace__owner",
@ -155,7 +148,7 @@ class ProjectViewSet(BaseViewSet):
) )
@allow_permission( @allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST], allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST],
level="WORKSPACE", level="WORKSPACE",
) )
def list(self, request, slug): def list(self, request, slug):
@ -165,6 +158,31 @@ class ProjectViewSet(BaseViewSet):
if field if field
] ]
projects = self.get_queryset().order_by("sort_order", "name") 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( if request.GET.get("per_page", False) and request.GET.get(
"cursor", False "cursor", False
): ):
@ -177,24 +195,13 @@ class ProjectViewSet(BaseViewSet):
).data, ).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 = ProjectListSerializer(
projects, many=True, fields=fields if fields else None projects, many=True, fields=fields if fields else None
).data ).data
return Response(projects, status=status.HTTP_200_OK) return Response(projects, status=status.HTTP_200_OK)
@allow_permission( @allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST], allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST],
level="WORKSPACE", level="WORKSPACE",
) )
def retrieve(self, request, slug, pk): def retrieve(self, request, slug, pk):

View file

@ -70,7 +70,7 @@ class ProjectInvitationsViewset(BaseViewSet):
[ [
email email
for email in emails for email in emails
if int(email.get("role", 10)) > requesting_user.role if int(email.get("role", 5)) > requesting_user.role
] ]
): ):
return Response( return Response(
@ -97,7 +97,7 @@ class ProjectInvitationsViewset(BaseViewSet):
settings.SECRET_KEY, settings.SECRET_KEY,
algorithm="HS256", algorithm="HS256",
), ),
role=email.get("role", 10), role=email.get("role", 5),
created_by=request.user, created_by=request.user,
) )
) )
@ -170,7 +170,7 @@ class UserProjectInvitationsViewset(BaseViewSet):
ProjectMember( ProjectMember(
project_id=project_id, project_id=project_id,
member=request.user, member=request.user,
role=15 if workspace_role >= 15 else 10, role=15 if workspace_role >= 15 else 5,
workspace=workspace, workspace=workspace,
created_by=request.user, created_by=request.user,
) )

View file

@ -95,9 +95,21 @@ class ProjectMemberViewSet(BaseViewSet):
member=member, member=member,
is_active=True, is_active=True,
).role ).role
if workspace_member_role in [5, 10] and member_roles.get( if workspace_member_role in [20] and member_roles.get(member) in [
member 5,
) in [15, 20]: 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( return Response(
{ {
"error": "You cannot add a user with role higher than the workspace role" "error": "You cannot add a user with role higher than the workspace role"
@ -143,7 +155,7 @@ class ProjectMemberViewSet(BaseViewSet):
bulk_project_members.append( bulk_project_members.append(
ProjectMember( ProjectMember(
member_id=member.get("member_id"), member_id=member.get("member_id"),
role=member.get("role", 10), role=member.get("role", 5),
project_id=project_id, project_id=project_id,
workspace_id=project.workspace_id, workspace_id=project.workspace_id,
sort_order=( sort_order=(
@ -189,7 +201,7 @@ class ProjectMemberViewSet(BaseViewSet):
# Return the serialized data # Return the serialized data
return Response(serializer.data, status=status.HTTP_201_CREATED) 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): def list(self, request, slug, project_id):
# Get the list of project members for the project # Get the list of project members for the project
project_members = ProjectMember.objects.filter( project_members = ProjectMember.objects.filter(
@ -230,7 +242,7 @@ class ProjectMemberViewSet(BaseViewSet):
member=project_member.member, member=project_member.member,
is_active=True, is_active=True,
).role ).role
if workspace_role in [5, 10] and int( if workspace_role in [5] and int(
request.data.get("role", project_member.role) request.data.get("role", project_member.role)
) in [15, 20]: ) in [15, 20]:
return Response( return Response(
@ -261,7 +273,7 @@ class ProjectMemberViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 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): def destroy(self, request, slug, project_id, pk):
project_member = ProjectMember.objects.get( project_member = ProjectMember.objects.get(
workspace__slug=slug, workspace__slug=slug,
@ -298,7 +310,7 @@ class ProjectMemberViewSet(BaseViewSet):
project_member.save() project_member.save()
return Response(status=status.HTTP_204_NO_CONTENT) 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): def leave(self, request, slug, project_id):
project_member = ProjectMember.objects.get( project_member = ProjectMember.objects.get(
workspace__slug=slug, workspace__slug=slug,

View file

@ -9,7 +9,8 @@ from rest_framework import status
from .. import BaseViewSet from .. import BaseViewSet
from plane.app.serializers import StateSerializer from plane.app.serializers import StateSerializer
from plane.app.permissions import ( from plane.app.permissions import (
ProjectEntityPermission, ROLE,
allow_permission
) )
from plane.db.models import State, Issue from plane.db.models import State, Issue
from plane.utils.cache import invalidate_cache from plane.utils.cache import invalidate_cache
@ -18,9 +19,6 @@ from plane.utils.cache import invalidate_cache
class StateViewSet(BaseViewSet): class StateViewSet(BaseViewSet):
serializer_class = StateSerializer serializer_class = StateSerializer
model = State model = State
permission_classes = [
ProjectEntityPermission,
]
def get_queryset(self): def get_queryset(self):
return self.filter_queryset( return self.filter_queryset(
@ -42,6 +40,7 @@ class StateViewSet(BaseViewSet):
@invalidate_cache( @invalidate_cache(
path="workspaces/:slug/states/", url_params=True, user=False path="workspaces/:slug/states/", url_params=True, user=False
) )
@allow_permission([ROLE.ADMIN])
def create(self, request, slug, project_id): def create(self, request, slug, project_id):
serializer = StateSerializer(data=request.data) serializer = StateSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
@ -49,6 +48,7 @@ class StateViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 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): def list(self, request, slug, project_id):
states = StateSerializer(self.get_queryset(), many=True).data states = StateSerializer(self.get_queryset(), many=True).data
grouped = request.GET.get("grouped", False) grouped = request.GET.get("grouped", False)
@ -65,6 +65,7 @@ class StateViewSet(BaseViewSet):
@invalidate_cache( @invalidate_cache(
path="workspaces/:slug/states/", url_params=True, user=False path="workspaces/:slug/states/", url_params=True, user=False
) )
@allow_permission([ROLE.ADMIN])
def mark_as_default(self, request, slug, project_id, pk): def mark_as_default(self, request, slug, project_id, pk):
# Select all the states which are marked as default # Select all the states which are marked as default
_ = State.objects.filter( _ = State.objects.filter(
@ -78,6 +79,7 @@ class StateViewSet(BaseViewSet):
@invalidate_cache( @invalidate_cache(
path="workspaces/:slug/states/", url_params=True, user=False path="workspaces/:slug/states/", url_params=True, user=False
) )
@allow_permission([ROLE.ADMIN])
def destroy(self, request, slug, project_id, pk): def destroy(self, request, slug, project_id, pk):
state = State.objects.get( state = State.objects.get(
is_triage=False, is_triage=False,

View file

@ -35,6 +35,7 @@ from plane.db.models import (
Workspace, Workspace,
WorkspaceMember, WorkspaceMember,
ProjectMember, ProjectMember,
Project,
) )
from plane.utils.grouper import ( from plane.utils.grouper import (
issue_group_values, issue_group_values,
@ -75,7 +76,7 @@ class WorkspaceViewViewSet(BaseViewSet):
) )
@allow_permission( @allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST], allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST],
level="WORKSPACE", level="WORKSPACE",
) )
def list(self, request, slug): def list(self, request, slug):
@ -259,7 +260,7 @@ class WorkspaceViewIssuesViewSet(BaseViewSet):
@method_decorator(gzip_page) @method_decorator(gzip_page)
@allow_permission( @allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST], allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST],
level="WORKSPACE", level="WORKSPACE",
) )
def list(self, request, slug): def list(self, request, slug):
@ -272,15 +273,24 @@ class WorkspaceViewIssuesViewSet(BaseViewSet):
.annotate(cycle_id=F("issue_cycle__cycle_id")) .annotate(cycle_id=F("issue_cycle__cycle_id"))
) )
if WorkspaceMember.objects.filter( # 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
workspace__slug=slug,
member=request.user, issue_queryset = issue_queryset.filter(
role=5, Q(
is_active=True, project__project_projectmember__role=5,
).exists(): project__guest_view_all_features=True,
issue_queryset = issue_queryset.filter(
created_by=request.user,
) )
| 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
issue_queryset, order_by_param = order_issue_queryset( issue_queryset, order_by_param = order_issue_queryset(
@ -421,19 +431,21 @@ class IssueViewViewSet(BaseViewSet):
.distinct() .distinct()
) )
allow_permission( allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST]
)
def list(self, request, slug, project_id): def list(self, request, slug, project_id):
queryset = self.get_queryset() queryset = self.get_queryset()
if ProjectMember.objects.filter( project = Project.objects.get(id=project_id)
workspace__slug=slug, if (
project_id=project_id, ProjectMember.objects.filter(
member=request.user, workspace__slug=slug,
role=5, project_id=project_id,
is_active=True, member=request.user,
).exists(): role=5,
is_active=True,
).exists()
and not project.guest_view_all_features
):
queryset = queryset.filter(owned_by=request.user) queryset = queryset.filter(owned_by=request.user)
fields = [ fields = [
field field
@ -445,14 +457,34 @@ class IssueViewViewSet(BaseViewSet):
).data ).data
return Response(views, status=status.HTTP_200_OK) return Response(views, status=status.HTTP_200_OK)
allow_permission( allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST]
)
def retrieve(self, request, slug, project_id, pk): def retrieve(self, request, slug, project_id, pk):
issue_view = ( issue_view = (
self.get_queryset().filter(pk=pk, project_id=project_id).first() 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) serializer = IssueViewSerializer(issue_view)
recent_visited_task.delay( recent_visited_task.delay(
slug=slug, slug=slug,

View file

@ -43,6 +43,7 @@ from plane.db.models import (
WorkspaceMember, WorkspaceMember,
WorkspaceTheme, WorkspaceTheme,
) )
from plane.app.permissions import ROLE, allow_permission
from plane.utils.cache import cache_response, invalidate_cache from plane.utils.cache import cache_response, invalidate_cache
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_control from django.views.decorators.cache import cache_control
@ -147,11 +148,25 @@ class WorkSpaceViewSet(BaseViewSet):
) )
@cache_response(60 * 60 * 2) @cache_response(60 * 60 * 2)
@allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
ROLE.GUEST,
],
level="WORKSPACE",
)
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs) return super().list(request, *args, **kwargs)
@invalidate_cache(path="/api/workspaces/", user=False) @invalidate_cache(path="/api/workspaces/", user=False)
@invalidate_cache(path="/api/users/me/workspaces/") @invalidate_cache(path="/api/users/me/workspaces/")
@allow_permission(
[
ROLE.ADMIN,
],
level="WORKSPACE",
)
def partial_update(self, request, *args, **kwargs): def partial_update(self, request, *args, **kwargs):
return super().partial_update(request, *args, **kwargs) return super().partial_update(request, *args, **kwargs)
@ -162,6 +177,7 @@ class WorkSpaceViewSet(BaseViewSet):
@invalidate_cache( @invalidate_cache(
path="/api/users/me/settings/", multiple=True, user=False path="/api/users/me/settings/", multiple=True, user=False
) )
@allow_permission([ROLE.ADMIN], level="WORKSPACE")
def destroy(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs):
return super().destroy(request, *args, **kwargs) return super().destroy(request, *args, **kwargs)

View file

@ -74,7 +74,7 @@ class WorkspaceInvitationsViewset(BaseViewSet):
[ [
email email
for email in emails for email in emails
if int(email.get("role", 10)) > requesting_user.role if int(email.get("role", 5)) > requesting_user.role
] ]
): ):
return Response( return Response(
@ -119,7 +119,7 @@ class WorkspaceInvitationsViewset(BaseViewSet):
settings.SECRET_KEY, settings.SECRET_KEY,
algorithm="HS256", algorithm="HS256",
), ),
role=email.get("role", 10), role=email.get("role", 5),
created_by=request.user, created_by=request.user,
) )
) )

View file

@ -13,7 +13,8 @@ from rest_framework.response import Response
from plane.app.permissions import ( from plane.app.permissions import (
WorkSpaceAdminPermission, WorkSpaceAdminPermission,
WorkspaceEntityPermission, WorkspaceEntityPermission,
WorkspaceUserPermission, allow_permission,
ROLE
) )
# Module imports # Module imports
@ -43,21 +44,6 @@ class WorkSpaceMemberViewSet(BaseViewSet):
serializer_class = WorkspaceMemberAdminSerializer serializer_class = WorkspaceMemberAdminSerializer
model = WorkspaceMember 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 = [ search_fields = [
"member__display_name", "member__display_name",
@ -77,6 +63,9 @@ class WorkSpaceMemberViewSet(BaseViewSet):
) )
@cache_response(60 * 60 * 2) @cache_response(60 * 60 * 2)
@allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
def list(self, request, slug): def list(self, request, slug):
workspace_member = WorkspaceMember.objects.get( workspace_member = WorkspaceMember.objects.get(
member=request.user, member=request.user,
@ -87,7 +76,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
# Get all active workspace members # Get all active workspace members
workspace_members = self.get_queryset() workspace_members = self.get_queryset()
if workspace_member.role > 10: if workspace_member.role > 5:
serializer = WorkspaceMemberAdminSerializer( serializer = WorkspaceMemberAdminSerializer(
workspace_members, workspace_members,
fields=("id", "member", "role"), fields=("id", "member", "role"),
@ -107,6 +96,9 @@ class WorkSpaceMemberViewSet(BaseViewSet):
user=False, user=False,
multiple=True, multiple=True,
) )
@allow_permission(
allowed_roles=[ROLE.ADMIN], level="WORKSPACE"
)
def partial_update(self, request, slug, pk): def partial_update(self, request, slug, pk):
workspace_member = WorkspaceMember.objects.get( workspace_member = WorkspaceMember.objects.get(
pk=pk, pk=pk,
@ -159,6 +151,9 @@ class WorkSpaceMemberViewSet(BaseViewSet):
@invalidate_cache( @invalidate_cache(
path="/api/users/me/workspaces/", user=False, multiple=True path="/api/users/me/workspaces/", user=False, multiple=True
) )
@allow_permission(
allowed_roles=[ROLE.ADMIN], level="WORKSPACE"
)
def destroy(self, request, slug, pk): def destroy(self, request, slug, pk):
# Check the user role who is deleting the user # Check the user role who is deleting the user
workspace_member = WorkspaceMember.objects.get( workspace_member = WorkspaceMember.objects.get(
@ -233,6 +228,9 @@ class WorkSpaceMemberViewSet(BaseViewSet):
@invalidate_cache( @invalidate_cache(
path="api/users/me/workspaces/", user=False, multiple=True 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): def leave(self, request, slug):
workspace_member = WorkspaceMember.objects.get( workspace_member = WorkspaceMember.objects.get(
workspace__slug=slug, workspace__slug=slug,

View file

@ -288,7 +288,7 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
is_active=True, is_active=True,
) )
projects = [] projects = []
if requesting_workspace_member.role >= 10: if requesting_workspace_member.role >= 15:
projects = ( projects = (
Project.objects.filter( Project.objects.filter(
workspace__slug=slug, workspace__slug=slug,

View file

@ -49,7 +49,7 @@ def process_workspace_project_invitations(user):
workspace_id=project_member_invite.workspace_id, workspace_id=project_member_invite.workspace_id,
role=( role=(
project_member_invite.role project_member_invite.role
if project_member_invite.role in [5, 10, 15] if project_member_invite.role in [5, 15]
else 15 else 15
), ),
member=user, member=user,
@ -67,7 +67,7 @@ def process_workspace_project_invitations(user):
workspace_id=project_member_invite.workspace_id, workspace_id=project_member_invite.workspace_id,
role=( role=(
project_member_invite.role project_member_invite.role
if project_member_invite.role in [5, 10, 15] if project_member_invite.role in [5, 15]
else 15 else 15
), ),
member=user, member=user,

View file

@ -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),
]

View file

@ -16,7 +16,6 @@ from .base import BaseModel
ROLE_CHOICES = ( ROLE_CHOICES = (
(20, "Admin"), (20, "Admin"),
(15, "Member"), (15, "Member"),
(10, "Viewer"),
(5, "Guest"), (5, "Guest"),
) )
@ -98,6 +97,7 @@ class Project(BaseModel):
inbox_view = models.BooleanField(default=False) inbox_view = models.BooleanField(default=False)
is_time_tracking_enabled = models.BooleanField(default=False) is_time_tracking_enabled = models.BooleanField(default=False)
is_issue_type_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) cover_image = models.URLField(blank=True, null=True, max_length=800)
estimate = models.ForeignKey( estimate = models.ForeignKey(
"db.Estimate", "db.Estimate",
@ -173,7 +173,7 @@ class ProjectMemberInvite(ProjectBaseModel):
token = models.CharField(max_length=255) token = models.CharField(max_length=255)
message = models.TextField(null=True) message = models.TextField(null=True)
responded_at = models.DateTimeField(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: class Meta:
verbose_name = "Project Member Invite" verbose_name = "Project Member Invite"
@ -194,7 +194,7 @@ class ProjectMember(ProjectBaseModel):
related_name="member_project", related_name="member_project",
) )
comment = models.TextField(blank=True, null=True) 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) view_props = models.JSONField(default=get_default_props)
default_props = models.JSONField(default=get_default_props) default_props = models.JSONField(default=get_default_props)
preferences = models.JSONField(default=get_default_preferences) preferences = models.JSONField(default=get_default_preferences)

View file

@ -8,9 +8,8 @@ from .base import BaseModel
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
ROLE_CHOICES = ( ROLE_CHOICES = (
(20, "Owner"), (20, "Admin"),
(15, "Admin"), (15, "Member"),
(10, "Member"),
(5, "Guest"), (5, "Guest"),
) )
@ -177,7 +176,7 @@ class WorkspaceMember(BaseModel):
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name="member_workspace", 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) company_role = models.TextField(null=True, blank=True)
view_props = models.JSONField(default=get_default_props) view_props = models.JSONField(default=get_default_props)
default_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) token = models.CharField(max_length=255)
message = models.TextField(null=True) message = models.TextField(null=True)
responded_at = models.DateTimeField(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: class Meta:
unique_together = ["email", "workspace", "deleted_at"] unique_together = ["email", "workspace", "deleted_at"]

View file

@ -1,10 +1,14 @@
export enum EUserProjectRoles { export enum EUserPermissions {
GUEST = 5,
VIEWER = 10,
MEMBER = 15,
ADMIN = 20, ADMIN = 20,
MEMBER = 15,
GUEST = 5,
} }
export type TUserPermissions =
| EUserPermissions.ADMIN
| EUserPermissions.MEMBER
| EUserPermissions.GUEST;
// project pages // project pages
export enum EPageAccess { export enum EPageAccess {
PUBLIC = 0, PUBLIC = 0,

View file

@ -1,4 +1,3 @@
import { EUserProjectRoles } from "@/constants/project";
import type { import type {
IProjectViewProps, IProjectViewProps,
IUser, IUser,
@ -9,6 +8,7 @@ import type {
TLogoProps, TLogoProps,
TStateGroups, TStateGroups,
} from ".."; } from "..";
import { TUserPermissions } from "../enums";
export interface IProject { export interface IProject {
archive_in: number; archive_in: number;
@ -30,6 +30,7 @@ export interface IProject {
draft_issues: number; draft_issues: number;
draft_sub_issues: number; draft_sub_issues: number;
estimate: string | null; estimate: string | null;
guest_view_all_features: boolean;
id: string; id: string;
identifier: string; identifier: string;
anchor: string | null; anchor: string | null;
@ -38,7 +39,7 @@ export interface IProject {
is_member: boolean; is_member: boolean;
is_time_tracking_enabled: boolean; is_time_tracking_enabled: boolean;
logo_props: TLogoProps; logo_props: TLogoProps;
member_role: EUserProjectRoles | null; member_role: TUserPermissions | null;
members: IProjectMemberLite[]; members: IProjectMemberLite[];
name: string; name: string;
network: number; network: number;
@ -85,7 +86,7 @@ export interface IProjectMember {
project: IProjectLite; project: IProjectLite;
workspace: IWorkspaceLite; workspace: IWorkspaceLite;
comment: string; comment: string;
role: EUserProjectRoles; role: TUserPermissions;
preferences: ProjectPreferences; preferences: ProjectPreferences;
@ -101,11 +102,11 @@ export interface IProjectMember {
export interface IProjectMembership { export interface IProjectMembership {
id: string; id: string;
member: string; member: string;
role: EUserProjectRoles; role: TUserPermissions;
} }
export interface IProjectBulkAddFormData { export interface IProjectBulkAddFormData {
members: { role: EUserProjectRoles; member_id: string }[]; members: { role: TUserPermissions; member_id: string }[];
} }
export interface IGithubRepository { export interface IGithubRepository {

View file

@ -1,9 +1,5 @@
import { import { IIssueActivity, TIssuePriorities, TStateGroups } from ".";
EUserProjectRoles, import { TUserPermissions } from "./enums";
IIssueActivity,
TIssuePriorities,
TStateGroups,
} from ".";
type TLoginMediums = "email" | "magic-code" | "github" | "gitlab" | "google"; type TLoginMediums = "email" | "magic-code" | "github" | "gitlab" | "google";
@ -134,7 +130,6 @@ export interface IUserActivityResponse {
export type UserAuth = { export type UserAuth = {
isMember: boolean; isMember: boolean;
isOwner: boolean; isOwner: boolean;
isViewer: boolean;
isGuest: boolean; isGuest: boolean;
}; };
@ -175,7 +170,7 @@ export interface IUserProfileProjectSegregation {
} }
export interface IUserProjectsRole { export interface IUserProjectsRole {
[projectId: string]: EUserProjectRoles; [projectId: string]: TUserPermissions;
} }
export interface IUserEmailNotificationSettings { export interface IUserEmailNotificationSettings {

View file

@ -1,4 +1,3 @@
import {EUserWorkspaceRoles} from "@/constants/workspace";
import type { import type {
ICycle, ICycle,
IProjectMember, IProjectMember,
@ -6,6 +5,7 @@ import type {
IUserLite, IUserLite,
IWorkspaceViewProps, IWorkspaceViewProps,
} from "@plane/types"; } from "@plane/types";
import { TUserPermissions } from "./enums";
export interface IWorkspace { export interface IWorkspace {
readonly id: string; readonly id: string;
@ -36,7 +36,7 @@ export interface IWorkspaceMemberInvitation {
id: string; id: string;
message: string; message: string;
responded_at: Date; responded_at: Date;
role: EUserWorkspaceRoles; role: TUserPermissions;
token: string; token: string;
workspace: { workspace: {
id: string; id: string;
@ -47,7 +47,7 @@ export interface IWorkspaceMemberInvitation {
} }
export interface IWorkspaceBulkInviteFormData { export interface IWorkspaceBulkInviteFormData {
emails: {email: string; role: EUserWorkspaceRoles}[]; emails: { email: string; role: TUserPermissions }[];
} }
export type Properties = { export type Properties = {
@ -69,7 +69,7 @@ export type Properties = {
export interface IWorkspaceMember { export interface IWorkspaceMember {
id: string; id: string;
member: IUserLite; member: IUserLite;
role: EUserWorkspaceRoles; role: TUserPermissions;
created_at?: string; created_at?: string;
avatar?: string; avatar?: string;
email?: string; email?: string;
@ -86,7 +86,7 @@ export interface IWorkspaceMemberMe {
default_props: IWorkspaceViewProps; default_props: IWorkspaceViewProps;
id: string; id: string;
member: string; member: string;
role: EUserWorkspaceRoles; role: TUserPermissions;
updated_at: Date; updated_at: Date;
updated_by: string; updated_by: string;
view_props: IWorkspaceViewProps; view_props: IWorkspaceViewProps;

View file

@ -58,7 +58,7 @@ const ComboDropDown = forwardRef((props: Props, ref) => {
} }
return ( return (
//@ts-ignore // @ts-ignore
<Combobox {...rest} ref={ref}> <Combobox {...rest} ref={ref}>
<Combobox.Button as={Fragment}>{button}</Combobox.Button> <Combobox.Button as={Fragment}>{button}</Combobox.Button>
{children} {children}
@ -70,4 +70,6 @@ const ComboOptions = Combobox.Options;
const ComboOption = Combobox.Option; const ComboOption = Combobox.Option;
const ComboInput = Combobox.Input; const ComboInput = Combobox.Input;
ComboDropDown.displayName = "ComboDropDown";
export { ComboDropDown, ComboOptions, ComboOption, ComboInput }; export { ComboDropDown, ComboOptions, ComboOption, ComboInput };

View file

@ -14,7 +14,7 @@ import { IssuePeekOverview } from "@/components/issues";
import { EmptyStateType } from "@/constants/empty-state"; import { EmptyStateType } from "@/constants/empty-state";
import { ENotificationLoader, ENotificationQueryParamType } from "@/constants/notification"; import { ENotificationLoader, ENotificationQueryParamType } from "@/constants/notification";
// hooks // 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"; import { useWorkspaceIssueProperties } from "@/hooks/use-workspace-issue-properties";
const WorkspaceDashboardPage = observer(() => { const WorkspaceDashboardPage = observer(() => {
@ -28,9 +28,7 @@ const WorkspaceDashboardPage = observer(() => {
notificationIdsByWorkspaceId, notificationIdsByWorkspaceId,
getNotifications, getNotifications,
} = useWorkspaceNotifications(); } = useWorkspaceNotifications();
const { const { fetchUserProjectInfo } = useUserPermissions();
membership: { fetchUserProjectInfo },
} = useUser();
const { setPeekIssue } = useIssueDetail(); const { setPeekIssue } = useIssueDetail();
// derived values // derived values
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Inbox` : undefined; const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Inbox` : undefined;

View file

@ -2,16 +2,15 @@
import { useState } from "react"; import { useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// ui // ui
import { Button } from "@plane/ui"; import { Button } from "@plane/ui";
// components // components
import { PageHead } from "@/components/core"; import { PageHead } from "@/components/core";
import { DownloadActivityButton, WorkspaceActivityListPage } from "@/components/profile"; import { DownloadActivityButton, WorkspaceActivityListPage } from "@/components/profile";
// constants
import { EUserWorkspaceRoles } from "@/constants/workspace";
// hooks // 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; const PER_PAGE = 100;
@ -21,13 +20,7 @@ const ProfileActivityPage = observer(() => {
const [totalPages, setTotalPages] = useState(0); const [totalPages, setTotalPages] = useState(0);
const [resultsCount, setResultsCount] = useState(0); const [resultsCount, setResultsCount] = useState(0);
// router // router
const { allowPermissions } = useUserPermissions();
const { userId } = useParams();
// store hooks
const { data: currentUser } = useUser();
const {
membership: { currentWorkspaceRole },
} = useUser();
const updateTotalPages = (count: number) => setTotalPages(count); const updateTotalPages = (count: number) => setTotalPages(count);
@ -47,8 +40,10 @@ const ProfileActivityPage = observer(() => {
/> />
); );
const canDownloadActivity = const canDownloadActivity = allowPermissions(
currentUser?.id === userId && !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.WORKSPACE
);
return ( return (
<> <>

View file

@ -12,9 +12,9 @@ import { BreadcrumbLink } from "@/components/common";
// components // components
import { ProfileIssuesFilter } from "@/components/profile"; import { ProfileIssuesFilter } from "@/components/profile";
import { PROFILE_ADMINS_TAB, PROFILE_VIEWER_TAB } from "@/constants/profile"; import { PROFILE_ADMINS_TAB, PROFILE_VIEWER_TAB } from "@/constants/profile";
import { EUserWorkspaceRoles } from "@/constants/workspace";
import { cn } from "@/helpers/common.helper"; 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 = { type TUserProfileHeader = {
userProjectsData: IUserProfileProjectSegregation | undefined; userProjectsData: IUserProfileProjectSegregation | undefined;
@ -28,14 +28,15 @@ export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
const { workspaceSlug, userId } = useParams(); const { workspaceSlug, userId } = useParams();
// store hooks // store hooks
const { toggleProfileSidebar, profileSidebarCollapsed } = useAppTheme(); const { toggleProfileSidebar, profileSidebarCollapsed } = useAppTheme();
const { const { data: currentUser } = useUser();
membership: { currentWorkspaceRole }, const { workspaceUserInfo, allowPermissions } = useUserPermissions();
data: currentUser,
} = useUser();
// derived values // 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; const tabsList = isAuthorized ? [...PROFILE_VIEWER_TAB, ...PROFILE_ADMINS_TAB] : PROFILE_VIEWER_TAB;

View file

@ -9,10 +9,10 @@ import { ProfileSidebar } from "@/components/profile";
// constants // constants
import { USER_PROFILE_PROJECT_SEGREGATION } from "@/constants/fetch-keys"; import { USER_PROFILE_PROJECT_SEGREGATION } from "@/constants/fetch-keys";
import { PROFILE_ADMINS_TAB, PROFILE_VIEWER_TAB } from "@/constants/profile"; import { PROFILE_ADMINS_TAB, PROFILE_VIEWER_TAB } from "@/constants/profile";
import { EUserWorkspaceRoles } from "@/constants/workspace";
// hooks // hooks
import { useUser } from "@/hooks/store"; import { useUserPermissions } from "@/hooks/store";
import useSize from "@/hooks/use-window-size"; import useSize from "@/hooks/use-window-size";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
// local components // local components
import { UserService } from "@/services/user.service"; import { UserService } from "@/services/user.service";
import { UserProfileHeader } from "./header"; import { UserProfileHeader } from "./header";
@ -25,17 +25,18 @@ type Props = {
children: React.ReactNode; children: React.ReactNode;
}; };
const AUTHORIZED_ROLES = [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.VIEWER];
const UseProfileLayout: React.FC<Props> = observer((props) => { const UseProfileLayout: React.FC<Props> = observer((props) => {
const { children } = props; const { children } = props;
// router // router
const { workspaceSlug, userId } = useParams(); const { workspaceSlug, userId } = useParams();
const pathname = usePathname(); const pathname = usePathname();
// store hooks // store hooks
const { const { allowPermissions } = useUserPermissions();
membership: { currentWorkspaceRole }, // derived values
} = useUser(); const isAuthorized = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.WORKSPACE
);
const windowSize = useSize(); const windowSize = useSize();
const isSmallerScreen = windowSize[0] >= 768; const isSmallerScreen = windowSize[0] >= 768;
@ -47,7 +48,6 @@ const UseProfileLayout: React.FC<Props> = observer((props) => {
: null : null
); );
// derived values // derived values
const isAuthorized = currentWorkspaceRole && AUTHORIZED_ROLES.includes(currentWorkspaceRole);
const isAuthorizedPath = const isAuthorizedPath =
pathname.includes("assigned") || pathname.includes("created") || pathname.includes("subscribed"); pathname.includes("assigned") || pathname.includes("created") || pathname.includes("subscribed");
const isIssuesTab = pathname.includes("assigned") || pathname.includes("created") || pathname.includes("subscribed"); const isIssuesTab = pathname.includes("assigned") || pathname.includes("created") || pathname.includes("subscribed");

View file

@ -21,7 +21,6 @@ import {
EIssuesStoreType, EIssuesStoreType,
ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_DISPLAY_FILTERS_BY_LAYOUT,
} from "@/constants/issue"; } from "@/constants/issue";
import { EUserProjectRoles } from "@/constants/project";
// helpers // helpers
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
import { isIssueFilterActive } from "@/helpers/filter.helper"; import { isIssueFilterActive } from "@/helpers/filter.helper";
@ -34,13 +33,14 @@ import {
useMember, useMember,
useProject, useProject,
useProjectState, useProjectState,
useUser,
useIssues, useIssues,
useCommandPalette, useCommandPalette,
useUserPermissions,
} from "@/hooks/store"; } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router"; import { useAppRouter } from "@/hooks/use-app-router";
import useLocalStorage from "@/hooks/use-local-storage"; import useLocalStorage from "@/hooks/use-local-storage";
import { usePlatformOS } from "@/hooks/use-platform-os"; import { usePlatformOS } from "@/hooks/use-platform-os";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
const CycleDropdownOption: React.FC<{ cycleId: string }> = ({ cycleId }) => { const CycleDropdownOption: React.FC<{ cycleId: string }> = ({ cycleId }) => {
// router // router
@ -81,9 +81,6 @@ export const CycleIssuesHeader: React.FC = observer(() => {
const { currentProjectCycleIds, getCycleById } = useCycle(); const { currentProjectCycleIds, getCycleById } = useCycle();
const { toggleCreateIssueModal } = useCommandPalette(); const { toggleCreateIssueModal } = useCommandPalette();
const { setTrackElement } = useEventTracker(); const { setTrackElement } = useEventTracker();
const {
membership: { currentProjectRole },
} = useUser();
const { currentProjectDetails, loader } = useProject(); const { currentProjectDetails, loader } = useProject();
const { projectStates } = useProjectState(); const { projectStates } = useProjectState();
const { projectLabels } = useLabel(); const { projectLabels } = useLabel();
@ -91,6 +88,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
project: { projectMemberIds }, project: { projectMemberIds },
} = useMember(); } = useMember();
const { isMobile } = usePlatformOS(); const { isMobile } = usePlatformOS();
const { allowPermissions } = useUserPermissions();
const activeLayout = issueFilters?.displayFilters?.layout; const activeLayout = issueFilters?.displayFilters?.layout;
@ -149,8 +147,10 @@ export const CycleIssuesHeader: React.FC = observer(() => {
// derived values // derived values
const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined; const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined;
const isCompletedCycle = cycleDetails?.status?.toLocaleLowerCase() === "completed"; const isCompletedCycle = cycleDetails?.status?.toLocaleLowerCase() === "completed";
const canUserCreateIssue = const canUserCreateIssue = allowPermissions(
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.PROJECT
);
const issuesCount = getGroupIssueCount(undefined, undefined, false); const issuesCount = getGroupIssueCount(undefined, undefined, false);

View file

@ -8,11 +8,11 @@ import { Breadcrumbs, Button, ContrastIcon, Header } from "@plane/ui";
// components // components
import { BreadcrumbLink, Logo } from "@/components/common"; import { BreadcrumbLink, Logo } from "@/components/common";
import { CyclesViewHeader } from "@/components/cycles"; import { CyclesViewHeader } from "@/components/cycles";
// constants
import { EUserProjectRoles } from "@/constants/project";
// hooks // hooks
import { useCommandPalette, useEventTracker, useProject, useUser } from "@/hooks/store"; import { useCommandPalette, useEventTracker, useProject, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router"; import { useAppRouter } from "@/hooks/use-app-router";
// constants
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
export const CyclesListHeader: FC = observer(() => { export const CyclesListHeader: FC = observer(() => {
// router // router
@ -21,13 +21,13 @@ export const CyclesListHeader: FC = observer(() => {
// store hooks // store hooks
const { toggleCreateCycleModal } = useCommandPalette(); const { toggleCreateCycleModal } = useCommandPalette();
const { setTrackElement } = useEventTracker(); const { setTrackElement } = useEventTracker();
const { const { allowPermissions } = useUserPermissions();
membership: { currentProjectRole },
} = useUser();
const { currentProjectDetails, loader } = useProject(); const { currentProjectDetails, loader } = useProject();
const canUserCreateCycle = const canUserCreateCycle = allowPermissions(
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.PROJECT
);
return ( return (
<Header> <Header>

View file

@ -9,10 +9,9 @@ import { Breadcrumbs, Button, Intake, Header } from "@plane/ui";
// components // components
import { BreadcrumbLink, Logo } from "@/components/common"; import { BreadcrumbLink, Logo } from "@/components/common";
import { InboxIssueCreateEditModalRoot } from "@/components/inbox"; import { InboxIssueCreateEditModalRoot } from "@/components/inbox";
// constants
import { EUserProjectRoles } from "@/constants/project";
// hooks // 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(() => { export const ProjectInboxHeader: FC = observer(() => {
// states // states
@ -20,14 +19,16 @@ export const ProjectInboxHeader: FC = observer(() => {
// router // router
const { workspaceSlug, projectId } = useParams(); const { workspaceSlug, projectId } = useParams();
// store hooks // store hooks
const { const { allowPermissions } = useUserPermissions();
membership: { currentProjectRole },
} = useUser();
const { currentProjectDetails, loader: currentProjectDetailsLoader } = useProject(); const { currentProjectDetails, loader: currentProjectDetailsLoader } = useProject();
const { loader } = useProjectInbox(); const { loader } = useProjectInbox();
// derived value // derived value
const isViewer = currentProjectRole === EUserProjectRoles.VIEWER; const isAuthorized = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
EUserPermissionsLevel.PROJECT
);
return ( return (
<Header> <Header>
@ -66,7 +67,7 @@ export const ProjectInboxHeader: FC = observer(() => {
</div> </div>
</Header.LeftItem> </Header.LeftItem>
<Header.RightItem> <Header.RightItem>
{currentProjectDetails?.inbox_view && workspaceSlug && projectId && !isViewer ? ( {currentProjectDetails?.inbox_view && workspaceSlug && projectId && isAuthorized ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<InboxIssueCreateEditModalRoot <InboxIssueCreateEditModalRoot
workspaceSlug={workspaceSlug.toString()} workspaceSlug={workspaceSlug.toString()}

View file

@ -12,22 +12,19 @@ import { BreadcrumbLink, CountChip, Logo } from "@/components/common";
import HeaderFilters from "@/components/issues/filters"; import HeaderFilters from "@/components/issues/filters";
import { EIssuesStoreType } from "@/constants/issue"; import { EIssuesStoreType } from "@/constants/issue";
// helpers // helpers
import { EUserProjectRoles } from "@/constants/project";
import { SPACE_BASE_PATH, SPACE_BASE_URL } from "@/helpers/common.helper"; import { SPACE_BASE_PATH, SPACE_BASE_URL } from "@/helpers/common.helper";
// hooks // hooks
import { useCommandPalette, useEventTracker, useProject, useUser } from "@/hooks/store"; import { useEventTracker, useProject, useCommandPalette, useUserPermissions } from "@/hooks/store";
import { useIssues } from "@/hooks/store/use-issues"; import { useIssues } from "@/hooks/store/use-issues";
import { useAppRouter } from "@/hooks/use-app-router"; import { useAppRouter } from "@/hooks/use-app-router";
import { usePlatformOS } from "@/hooks/use-platform-os"; import { usePlatformOS } from "@/hooks/use-platform-os";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
export const ProjectIssuesHeader = observer(() => { export const ProjectIssuesHeader = observer(() => {
// router // router
const router = useAppRouter(); const router = useAppRouter();
const { workspaceSlug, projectId } = useParams() as { workspaceSlug: string; projectId: string }; const { workspaceSlug, projectId } = useParams() as { workspaceSlug: string; projectId: string };
// store hooks // store hooks
const {
membership: { currentProjectRole },
} = useUser();
const { const {
issues: { getGroupIssueCount }, issues: { getGroupIssueCount },
} = useIssues(EIssuesStoreType.PROJECT); } = useIssues(EIssuesStoreType.PROJECT);
@ -36,14 +33,17 @@ export const ProjectIssuesHeader = observer(() => {
const { toggleCreateIssueModal } = useCommandPalette(); const { toggleCreateIssueModal } = useCommandPalette();
const { setTrackElement } = useEventTracker(); const { setTrackElement } = useEventTracker();
const { allowPermissions } = useUserPermissions();
const { isMobile } = usePlatformOS(); const { isMobile } = usePlatformOS();
const SPACE_APP_URL = (SPACE_BASE_URL.trim() === "" ? window.location.origin : SPACE_BASE_URL) + SPACE_BASE_PATH; 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 publishedURL = `${SPACE_APP_URL}/issues/${currentProjectDetails?.anchor}`;
const issuesCount = getGroupIssueCount(undefined, undefined, false); const issuesCount = getGroupIssueCount(undefined, undefined, false);
const canUserCreateIssue = const canUserCreateIssue = allowPermissions(
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.PROJECT
);
return ( return (
<Header> <Header>

View file

@ -21,7 +21,6 @@ import {
EIssueLayoutTypes, EIssueLayoutTypes,
ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_DISPLAY_FILTERS_BY_LAYOUT,
} from "@/constants/issue"; } from "@/constants/issue";
import { EUserProjectRoles } from "@/constants/project";
// helpers // helpers
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
import { isIssueFilterActive } from "@/helpers/filter.helper"; import { isIssueFilterActive } from "@/helpers/filter.helper";
@ -34,14 +33,15 @@ import {
useModule, useModule,
useProject, useProject,
useProjectState, useProjectState,
useUser,
useIssues, useIssues,
useCommandPalette, useCommandPalette,
useUserPermissions,
} from "@/hooks/store"; } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router"; import { useAppRouter } from "@/hooks/use-app-router";
import { useIssuesActions } from "@/hooks/use-issues-actions"; import { useIssuesActions } from "@/hooks/use-issues-actions";
import useLocalStorage from "@/hooks/use-local-storage"; import useLocalStorage from "@/hooks/use-local-storage";
import { usePlatformOS } from "@/hooks/use-platform-os"; import { usePlatformOS } from "@/hooks/use-platform-os";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
const ModuleDropdownOption: React.FC<{ moduleId: string }> = ({ moduleId }) => { const ModuleDropdownOption: React.FC<{ moduleId: string }> = ({ moduleId }) => {
// router // router
@ -83,9 +83,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
const { projectModuleIds, getModuleById } = useModule(); const { projectModuleIds, getModuleById } = useModule();
const { toggleCreateIssueModal } = useCommandPalette(); const { toggleCreateIssueModal } = useCommandPalette();
const { setTrackElement } = useEventTracker(); const { setTrackElement } = useEventTracker();
const { const { allowPermissions } = useUserPermissions();
membership: { currentProjectRole },
} = useUser();
const { currentProjectDetails, loader } = useProject(); const { currentProjectDetails, loader } = useProject();
const { projectLabels } = useLabel(); const { projectLabels } = useLabel();
const { projectStates } = useProjectState(); const { projectStates } = useProjectState();
@ -149,8 +147,10 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
// derived values // derived values
const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined; const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined;
const canUserCreateIssue = const canUserCreateIssue = allowPermissions(
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.PROJECT
);
const issuesCount = getGroupIssueCount(undefined, undefined, false); const issuesCount = getGroupIssueCount(undefined, undefined, false);

View file

@ -7,11 +7,11 @@ import { Breadcrumbs, Button, DiceIcon, Header } from "@plane/ui";
// components // components
import { BreadcrumbLink, Logo } from "@/components/common"; import { BreadcrumbLink, Logo } from "@/components/common";
import { ModuleViewHeader } from "@/components/modules"; import { ModuleViewHeader } from "@/components/modules";
// constants
import { EUserProjectRoles } from "@/constants/project";
// hooks // hooks
import { useCommandPalette, useEventTracker, useProject, useUser } from "@/hooks/store"; import { useCommandPalette, useEventTracker, useProject, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router"; import { useAppRouter } from "@/hooks/use-app-router";
// constants
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
export const ModulesListHeader: React.FC = observer(() => { export const ModulesListHeader: React.FC = observer(() => {
// router // router
@ -20,14 +20,15 @@ export const ModulesListHeader: React.FC = observer(() => {
// store hooks // store hooks
const { toggleCreateModuleModal } = useCommandPalette(); const { toggleCreateModuleModal } = useCommandPalette();
const { setTrackElement } = useEventTracker(); const { setTrackElement } = useEventTracker();
const { const { allowPermissions } = useUserPermissions();
membership: { currentProjectRole },
} = useUser();
const { currentProjectDetails, loader } = useProject(); const { currentProjectDetails, loader } = useProject();
// auth // auth
const canUserCreateModule = const canUserCreateModule = allowPermissions(
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.PROJECT
);
return ( return (
<Header> <Header>

View file

@ -9,9 +9,9 @@ import { Breadcrumbs, Button, Header } from "@plane/ui";
import { BreadcrumbLink, Logo } from "@/components/common"; import { BreadcrumbLink, Logo } from "@/components/common";
// constants // constants
import { EPageAccess } from "@/constants/page"; import { EPageAccess } from "@/constants/page";
import { EUserProjectRoles } from "@/constants/project";
// hooks // 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(() => { export const PagesListHeader = observer(() => {
// router // router
@ -20,14 +20,15 @@ export const PagesListHeader = observer(() => {
const pageType = searchParams.get("type"); const pageType = searchParams.get("type");
// store hooks // store hooks
const { toggleCreatePageModal } = useCommandPalette(); const { toggleCreatePageModal } = useCommandPalette();
const { const { allowPermissions } = useUserPermissions();
membership: { currentProjectRole },
} = useUser();
const { currentProjectDetails, loader } = useProject(); const { currentProjectDetails, loader } = useProject();
const { setTrackElement } = useEventTracker(); const { setTrackElement } = useEventTracker();
const canUserCreatePage = const canUserCreatePage = allowPermissions(
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.PROJECT
);
return ( return (
<Header> <Header>

View file

@ -11,18 +11,19 @@ import { NotAuthorizedView } from "@/components/auth-screens";
import { AutoArchiveAutomation, AutoCloseAutomation } from "@/components/automation"; import { AutoArchiveAutomation, AutoCloseAutomation } from "@/components/automation";
import { PageHead } from "@/components/core"; import { PageHead } from "@/components/core";
// hooks // 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(() => { const AutomationSettingsPage = observer(() => {
// router // router
const { workspaceSlug, projectId } = useParams(); const { workspaceSlug, projectId } = useParams();
// store hooks // store hooks
const { const { workspaceUserInfo, allowPermissions } = useUserPermissions();
canPerformProjectAdminActions,
membership: { currentProjectRole },
} = useUser();
const { currentProjectDetails: projectDetails, updateProject } = useProject(); const { currentProjectDetails: projectDetails, updateProject } = useProject();
// derived values
const canPerformProjectAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT);
const handleChange = async (formData: Partial<IProject>) => { const handleChange = async (formData: Partial<IProject>) => {
if (!workspaceSlug || !projectId || !projectDetails) return; if (!workspaceSlug || !projectId || !projectDetails) return;
@ -38,7 +39,7 @@ const AutomationSettingsPage = observer(() => {
// derived values // derived values
const pageTitle = projectDetails?.name ? `${projectDetails?.name} - Automations` : undefined; const pageTitle = projectDetails?.name ? `${projectDetails?.name} - Automations` : undefined;
if (currentProjectRole && !canPerformProjectAdminActions) { if (workspaceUserInfo && !canPerformProjectAdminActions) {
return <NotAuthorizedView section="settings" isProjectView />; return <NotAuthorizedView section="settings" isProjectView />;
} }

View file

@ -7,22 +7,22 @@ import { NotAuthorizedView } from "@/components/auth-screens";
import { PageHead } from "@/components/core"; import { PageHead } from "@/components/core";
import { EstimateRoot } from "@/components/estimates"; import { EstimateRoot } from "@/components/estimates";
// hooks // 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 EstimatesSettingsPage = observer(() => {
const { workspaceSlug, projectId } = useParams(); const { workspaceSlug, projectId } = useParams();
const { // store
canPerformProjectAdminActions,
membership: { currentProjectRole },
} = useUser();
const { currentProjectDetails } = useProject(); const { currentProjectDetails } = useProject();
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
// derived values // derived values
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Estimates` : undefined; const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Estimates` : undefined;
const canPerformProjectAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT);
if (!workspaceSlug || !projectId) return <></>; if (!workspaceSlug || !projectId) return <></>;
if (currentProjectRole && !canPerformProjectAdminActions) { if (workspaceUserInfo && !canPerformProjectAdminActions) {
return <NotAuthorizedView section="settings" isProjectView />; return <NotAuthorizedView section="settings" isProjectView />;
} }

View file

@ -7,22 +7,22 @@ import { NotAuthorizedView } from "@/components/auth-screens";
import { PageHead } from "@/components/core"; import { PageHead } from "@/components/core";
import { ProjectFeaturesList } from "@/components/project"; import { ProjectFeaturesList } from "@/components/project";
// hooks // 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 FeaturesSettingsPage = observer(() => {
const { workspaceSlug, projectId } = useParams(); const { workspaceSlug, projectId } = useParams();
// store // store
const { const { workspaceUserInfo, allowPermissions } = useUserPermissions();
canPerformProjectAdminActions,
membership: { currentProjectRole },
} = useUser();
const { currentProjectDetails } = useProject(); const { currentProjectDetails } = useProject();
// derived values // derived values
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Features` : undefined; const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Features` : undefined;
const canPerformProjectAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT);
if (!workspaceSlug || !projectId) return null; if (!workspaceSlug || !projectId) return null;
if (currentProjectRole && !canPerformProjectAdminActions) { if (workspaceUserInfo && !canPerformProjectAdminActions) {
return <NotAuthorizedView section="settings" isProjectView />; return <NotAuthorizedView section="settings" isProjectView />;
} }

View file

@ -9,25 +9,21 @@ import { Breadcrumbs, CustomMenu, Header } from "@plane/ui";
// components // components
import { BreadcrumbLink, Logo } from "@/components/common"; import { BreadcrumbLink, Logo } from "@/components/common";
// constants // constants
import { EUserProjectRoles } from "@/constants/project";
// hooks // hooks
import { useProject, useUser } from "@/hooks/store"; import { useProject, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router"; import { useAppRouter } from "@/hooks/use-app-router";
// plane web constants // plane web constants
import { PROJECT_SETTINGS_LINKS } from "@/plane-web/constants/project"; import { PROJECT_SETTINGS_LINKS } from "@/plane-web/constants/project";
import { EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
export const ProjectSettingHeader: FC = observer(() => { export const ProjectSettingHeader: FC = observer(() => {
// router // router
const router = useAppRouter(); const router = useAppRouter();
const { workspaceSlug, projectId } = useParams(); const { workspaceSlug, projectId } = useParams();
// store hooks // store hooks
const { const { allowPermissions } = useUserPermissions();
membership: { currentProjectRole },
} = useUser();
const { currentProjectDetails, loader } = useProject(); const { currentProjectDetails, loader } = useProject();
const projectMemberInfo = currentProjectRole || EUserProjectRoles.GUEST;
return ( return (
<Header> <Header>
<Header.LeftItem> <Header.LeftItem>
@ -74,7 +70,12 @@ export const ProjectSettingHeader: FC = observer(() => {
> >
{PROJECT_SETTINGS_LINKS.map( {PROJECT_SETTINGS_LINKS.map(
(item) => (item) =>
projectMemberInfo >= item.access && ( allowPermissions(
item.access,
EUserPermissionsLevel.PROJECT,
workspaceSlug.toString(),
projectId.toString()
) && (
<CustomMenu.MenuItem <CustomMenu.MenuItem
key={item.key} key={item.key}
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}${item.href}`)} onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}${item.href}`)}

View file

@ -9,19 +9,24 @@ import { NotAuthorizedView } from "@/components/auth-screens";
import { PageHead } from "@/components/core"; import { PageHead } from "@/components/core";
import { ProjectSettingsLabelList } from "@/components/labels"; import { ProjectSettingsLabelList } from "@/components/labels";
// hooks // 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(() => { const LabelsSettingsPage = observer(() => {
// store hooks // store hooks
const { currentProjectDetails } = useProject(); const { currentProjectDetails } = useProject();
const { const { workspaceUserInfo, allowPermissions } = useUserPermissions();
canPerformProjectMemberActions,
membership: { currentProjectRole },
} = useUser();
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Labels` : undefined; const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Labels` : undefined;
const scrollableContainerRef = useRef<HTMLDivElement | null>(null); const scrollableContainerRef = useRef<HTMLDivElement | null>(null);
// derived values
const canPerformProjectMemberActions = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.PROJECT
);
// Enable Auto Scroll for Labels list // Enable Auto Scroll for Labels list
useEffect(() => { useEffect(() => {
const element = scrollableContainerRef.current; const element = scrollableContainerRef.current;
@ -35,7 +40,7 @@ const LabelsSettingsPage = observer(() => {
); );
}, [scrollableContainerRef?.current]); }, [scrollableContainerRef?.current]);
if (currentProjectRole && !canPerformProjectMemberActions) { if (workspaceUserInfo && !canPerformProjectMemberActions) {
return <NotAuthorizedView section="settings" isProjectView />; return <NotAuthorizedView section="settings" isProjectView />;
} }

View file

@ -6,19 +6,21 @@ import { NotAuthorizedView } from "@/components/auth-screens";
import { PageHead } from "@/components/core"; import { PageHead } from "@/components/core";
import { ProjectMemberList, ProjectSettingsMemberDefaults } from "@/components/project"; import { ProjectMemberList, ProjectSettingsMemberDefaults } from "@/components/project";
// hooks // 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(() => { const MembersSettingsPage = observer(() => {
// store // store
const { currentProjectDetails } = useProject(); const { currentProjectDetails } = useProject();
const { const { workspaceUserInfo, allowPermissions } = useUserPermissions();
canPerformProjectViewerActions,
membership: { currentProjectRole },
} = useUser();
// derived values // derived values
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Members` : undefined; const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Members` : undefined;
const canPerformProjectMemberActions = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.PROJECT
);
if (currentProjectRole && !canPerformProjectViewerActions) { if (workspaceUserInfo && !canPerformProjectMemberActions) {
return <NotAuthorizedView section="settings" isProjectView />; return <NotAuthorizedView section="settings" isProjectView />;
} }

View file

@ -15,7 +15,8 @@ import {
ProjectDetailsFormLoader, ProjectDetailsFormLoader,
} from "@/components/project"; } from "@/components/project";
// hooks // hooks
import { useProject } from "@/hooks/store"; import { useProject, useUserPermissions } from "@/hooks/store";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
const GeneralSettingsPage = observer(() => { const GeneralSettingsPage = observer(() => {
// states // states
@ -25,6 +26,8 @@ const GeneralSettingsPage = observer(() => {
const { workspaceSlug, projectId } = useParams(); const { workspaceSlug, projectId } = useParams();
// store hooks // store hooks
const { currentProjectDetails, fetchProjectDetails } = useProject(); const { currentProjectDetails, fetchProjectDetails } = useProject();
const { allowPermissions } = useUserPermissions();
// api call to fetch project details // api call to fetch project details
// TODO: removed this API if not necessary // TODO: removed this API if not necessary
const { isLoading } = useSWR( const { isLoading } = useSWR(
@ -32,7 +35,13 @@ const GeneralSettingsPage = observer(() => {
workspaceSlug && projectId ? () => fetchProjectDetails(workspaceSlug.toString(), projectId.toString()) : null workspaceSlug && projectId ? () => fetchProjectDetails(workspaceSlug.toString(), projectId.toString()) : null
); );
// derived values // 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 pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - General Settings` : undefined;
// const currentNetwork = NETWORK_CHOICES.find((n) => n.key === projectDetails?.network); // const currentNetwork = NETWORK_CHOICES.find((n) => n.key === projectDetails?.network);
// const selectedNetwork = NETWORK_CHOICES.find((n) => n.key === watch("network")); // const selectedNetwork = NETWORK_CHOICES.find((n) => n.key === watch("network"));
@ -69,7 +78,7 @@ const GeneralSettingsPage = observer(() => {
<ProjectDetailsFormLoader /> <ProjectDetailsFormLoader />
)} )}
{isAdmin && ( {isAdmin && currentProjectDetails && (
<> <>
<ArchiveProjectSelection <ArchiveProjectSelection
projectDetails={currentProjectDetails} projectDetails={currentProjectDetails}

View file

@ -8,22 +8,20 @@ import { useParams, usePathname } from "next/navigation";
import { Loader } from "@plane/ui"; import { Loader } from "@plane/ui";
// components // components
import { SidebarNavItem } from "@/components/sidebar"; import { SidebarNavItem } from "@/components/sidebar";
// constants
import { EUserProjectRoles } from "@/constants/project";
// hooks // hooks
import { useUser } from "@/hooks/store"; import { useUserPermissions } from "@/hooks/store";
// plane web constants // plane web constants
import { PROJECT_SETTINGS_LINKS } from "@/plane-web/constants/project"; import { PROJECT_SETTINGS_LINKS } from "@/plane-web/constants/project";
import { EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
export const ProjectSettingsSidebar = observer(() => { export const ProjectSettingsSidebar = observer(() => {
const { workspaceSlug, projectId } = useParams(); const { workspaceSlug, projectId } = useParams();
const pathname = usePathname(); const pathname = usePathname();
// mobx store // mobx store
const { const { allowPermissions, projectUserInfo } = useUserPermissions();
membership: { currentProjectRole },
} = useUser();
const projectMemberInfo = currentProjectRole || EUserProjectRoles.GUEST; // derived values
const currentProjectRole = projectUserInfo?.[workspaceSlug?.toString()]?.[projectId?.toString()]?.role;
if (!currentProjectRole) { if (!currentProjectRole) {
return ( return (
@ -47,7 +45,12 @@ export const ProjectSettingsSidebar = observer(() => {
<div className="flex w-full flex-col gap-1"> <div className="flex w-full flex-col gap-1">
{PROJECT_SETTINGS_LINKS.map( {PROJECT_SETTINGS_LINKS.map(
(link) => (link) =>
projectMemberInfo >= link.access && ( allowPermissions(
link.access,
EUserPermissionsLevel.PROJECT,
workspaceSlug.toString(),
projectId.toString()
) && (
<Link key={link.key} href={`/${workspaceSlug}/projects/${projectId}${link.href}`}> <Link key={link.key} href={`/${workspaceSlug}/projects/${projectId}${link.href}`}>
<SidebarNavItem <SidebarNavItem
key={link.key} key={link.key}

View file

@ -7,20 +7,24 @@ import { NotAuthorizedView } from "@/components/auth-screens";
import { PageHead } from "@/components/core"; import { PageHead } from "@/components/core";
import { ProjectStateRoot } from "@/components/project-states"; import { ProjectStateRoot } from "@/components/project-states";
// hook // hook
import { useProject, useUser } from "@/hooks/store"; import { useProject, useUserPermissions } from "@/hooks/store";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
const StatesSettingsPage = observer(() => { const StatesSettingsPage = observer(() => {
const { workspaceSlug, projectId } = useParams(); const { workspaceSlug, projectId } = useParams();
// store // store
const { currentProjectDetails } = useProject(); const { currentProjectDetails } = useProject();
const { const { workspaceUserInfo, allowPermissions } = useUserPermissions();
canPerformProjectMemberActions,
membership: { currentProjectRole },
} = useUser();
// derived values // derived values
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - States` : undefined; 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 <NotAuthorizedView section="settings" isProjectView />; return <NotAuthorizedView section="settings" isProjectView />;
} }

View file

@ -19,7 +19,6 @@ import {
EIssueLayoutTypes, EIssueLayoutTypes,
ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_DISPLAY_FILTERS_BY_LAYOUT,
} from "@/constants/issue"; } from "@/constants/issue";
import { EUserProjectRoles } from "@/constants/project";
import { EViewAccess } from "@/constants/views"; import { EViewAccess } from "@/constants/views";
// helpers // helpers
import { isIssueFilterActive } from "@/helpers/filter.helper"; import { isIssueFilterActive } from "@/helpers/filter.helper";
@ -35,8 +34,9 @@ import {
useProject, useProject,
useProjectState, useProjectState,
useProjectView, useProjectView,
useUser, useUserPermissions,
} from "@/hooks/store"; } from "@/hooks/store";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
export const ProjectViewIssuesHeader: React.FC = observer(() => { export const ProjectViewIssuesHeader: React.FC = observer(() => {
// router // router
@ -47,9 +47,8 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
} = useIssues(EIssuesStoreType.PROJECT_VIEW); } = useIssues(EIssuesStoreType.PROJECT_VIEW);
const { setTrackElement } = useEventTracker(); const { setTrackElement } = useEventTracker();
const { toggleCreateIssueModal } = useCommandPalette(); const { toggleCreateIssueModal } = useCommandPalette();
const { const { allowPermissions } = useUserPermissions();
membership: { currentProjectRole },
} = useUser();
const { currentProjectDetails, loader } = useProject(); const { currentProjectDetails, loader } = useProject();
const { projectViewIds, getViewById } = useProjectView(); const { projectViewIds, getViewById } = useProjectView();
const { projectStates } = useProjectState(); const { projectStates } = useProjectState();
@ -131,8 +130,10 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
const viewDetails = viewId ? getViewById(viewId.toString()) : null; const viewDetails = viewId ? getViewById(viewId.toString()) : null;
const canUserCreateIssue = const canUserCreateIssue = allowPermissions(
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.PROJECT
);
const publishLink = getPublishViewLink(viewDetails?.anchor); const publishLink = getPublishViewLink(viewDetails?.anchor);
return ( return (

View file

@ -16,7 +16,8 @@ import { APITokenSettingsLoader } from "@/components/ui";
import { EmptyStateType } from "@/constants/empty-state"; import { EmptyStateType } from "@/constants/empty-state";
import { API_TOKENS_LIST } from "@/constants/fetch-keys"; import { API_TOKENS_LIST } from "@/constants/fetch-keys";
// store hooks // store hooks
import { useUser, useWorkspace } from "@/hooks/store"; import { useUserPermissions, useWorkspace } from "@/hooks/store";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
// services // services
import { APITokenService } from "@/services/api_token.service"; import { APITokenService } from "@/services/api_token.service";
@ -28,11 +29,10 @@ const ApiTokensPage = observer(() => {
// router // router
const { workspaceSlug } = useParams(); const { workspaceSlug } = useParams();
// store hooks // store hooks
const {
canPerformWorkspaceAdminActions,
membership: { currentWorkspaceRole },
} = useUser();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
// derived values
const canPerformWorkspaceAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
const { data: tokens } = useSWR( const { data: tokens } = useSWR(
workspaceSlug && canPerformWorkspaceAdminActions ? API_TOKENS_LIST(workspaceSlug.toString()) : null, workspaceSlug && canPerformWorkspaceAdminActions ? API_TOKENS_LIST(workspaceSlug.toString()) : null,
@ -42,7 +42,7 @@ const ApiTokensPage = observer(() => {
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - API Tokens` : undefined; const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - API Tokens` : undefined;
if (currentWorkspaceRole && !canPerformWorkspaceAdminActions) { if (workspaceUserInfo && !canPerformWorkspaceAdminActions) {
return <NotAuthorizedView section="settings" />; return <NotAuthorizedView section="settings" />;
} }

View file

@ -5,21 +5,20 @@ import { observer } from "mobx-react";
import { NotAuthorizedView } from "@/components/auth-screens"; import { NotAuthorizedView } from "@/components/auth-screens";
import { PageHead } from "@/components/core"; import { PageHead } from "@/components/core";
// hooks // hooks
import { useUser, useWorkspace } from "@/hooks/store"; import { useUserPermissions, useWorkspace } from "@/hooks/store";
// plane web components // plane web components
import { BillingRoot } from "@/plane-web/components/workspace"; import { BillingRoot } from "@/plane-web/components/workspace";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
const BillingSettingsPage = observer(() => { const BillingSettingsPage = observer(() => {
// store hooks // store hooks
const { const { workspaceUserInfo, allowPermissions } = useUserPermissions();
canPerformWorkspaceAdminActions,
membership: { currentWorkspaceRole },
} = useUser();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
// derived values // derived values
const canPerformWorkspaceAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Billing & Plans` : undefined; const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Billing & Plans` : undefined;
if (currentWorkspaceRole && !canPerformWorkspaceAdminActions) { if (workspaceUserInfo && !canPerformWorkspaceAdminActions) {
return <NotAuthorizedView section="settings" />; return <NotAuthorizedView section="settings" />;
} }

View file

@ -8,22 +8,23 @@ import ExportGuide from "@/components/exporter/guide";
// helpers // helpers
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
// hooks // 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(() => { const ExportsPage = observer(() => {
// store hooks // store hooks
const { const { workspaceUserInfo, allowPermissions } = useUserPermissions();
canPerformWorkspaceViewerActions,
canPerformWorkspaceMemberActions,
membership: { currentWorkspaceRole },
} = useUser();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
// derived values // derived values
const canPerformWorkspaceMemberActions = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.WORKSPACE
);
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Exports` : undefined; const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Exports` : undefined;
// if user is not authorized to view this page // if user is not authorized to view this page
if (currentWorkspaceRole && !canPerformWorkspaceViewerActions) { if (workspaceUserInfo && !canPerformWorkspaceMemberActions) {
return <NotAuthorizedView section="settings" />; return <NotAuthorizedView section="settings" />;
} }

View file

@ -4,20 +4,17 @@ import { observer } from "mobx-react";
// components // components
import { PageHead } from "@/components/core"; import { PageHead } from "@/components/core";
import IntegrationGuide from "@/components/integration/guide"; import IntegrationGuide from "@/components/integration/guide";
// constants
import { EUserWorkspaceRoles } from "@/constants/workspace";
// hooks // 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(() => { const ImportsPage = observer(() => {
// store hooks // store hooks
const {
membership: { currentWorkspaceRole },
} = useUser();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { allowPermissions } = useUserPermissions();
// derived values // derived values
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN; const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Imports` : undefined; const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Imports` : undefined;
if (!isAdmin) if (!isAdmin)

View file

@ -8,9 +8,9 @@ import { SingleIntegrationCard } from "@/components/integration";
import { IntegrationAndImportExportBanner, IntegrationsSettingsLoader } from "@/components/ui"; import { IntegrationAndImportExportBanner, IntegrationsSettingsLoader } from "@/components/ui";
// constants // constants
import { APP_INTEGRATIONS } from "@/constants/fetch-keys"; import { APP_INTEGRATIONS } from "@/constants/fetch-keys";
import { EUserWorkspaceRoles } from "@/constants/workspace";
// hooks // hooks
import { useUser, useWorkspace } from "@/hooks/store"; import { useUserPermissions, useWorkspace } from "@/hooks/store";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
// services // services
import { IntegrationService } from "@/services/integrations"; import { IntegrationService } from "@/services/integrations";
@ -20,13 +20,11 @@ const WorkspaceIntegrationsPage = observer(() => {
// router // router
const { workspaceSlug } = useParams(); const { workspaceSlug } = useParams();
// store hooks // store hooks
const {
membership: { currentWorkspaceRole },
} = useUser();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { allowPermissions } = useUserPermissions();
// derived values // derived values
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN; const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Integrations` : undefined; const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Integrations` : undefined;
if (!isAdmin) if (!isAdmin)

View file

@ -18,7 +18,8 @@ import { MEMBER_INVITED } from "@/constants/event-tracker";
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
import { getUserRole } from "@/helpers/user.helper"; import { getUserRole } from "@/helpers/user.helper";
// hooks // 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(() => { const WorkspaceMembersSettingsPage = observer(() => {
// states // states
@ -27,18 +28,20 @@ const WorkspaceMembersSettingsPage = observer(() => {
// router // router
const { workspaceSlug } = useParams(); const { workspaceSlug } = useParams();
// store hooks // store hooks
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
const { captureEvent } = useEventTracker(); const { captureEvent } = useEventTracker();
const {
canPerformWorkspaceAdminActions,
canPerformWorkspaceViewerActions,
canPerformWorkspaceMemberActions,
membership: { currentWorkspaceRole },
} = useUser();
const { const {
workspace: { inviteMembersToWorkspace }, workspace: { inviteMembersToWorkspace },
} = useMember(); } = useMember();
const { currentWorkspace } = useWorkspace(); 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) => { const handleWorkspaceInvite = (data: IWorkspaceBulkInviteFormData) => {
if (!workspaceSlug) return; if (!workspaceSlug) return;
@ -49,7 +52,7 @@ const WorkspaceMembersSettingsPage = observer(() => {
emails: [ emails: [
...data.emails.map((email) => ({ ...data.emails.map((email) => ({
email: email.email, email: email.email,
role: getUserRole(email.role), role: getUserRole(email.role as unknown as EUserPermissions),
})), })),
], ],
project_id: undefined, project_id: undefined,
@ -67,7 +70,7 @@ const WorkspaceMembersSettingsPage = observer(() => {
emails: [ emails: [
...data.emails.map((email) => ({ ...data.emails.map((email) => ({
email: email.email, email: email.email,
role: getUserRole(email.role), role: getUserRole(email.role as unknown as EUserPermissions),
})), })),
], ],
project_id: undefined, project_id: undefined,
@ -86,7 +89,7 @@ const WorkspaceMembersSettingsPage = observer(() => {
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Members` : undefined; const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Members` : undefined;
// if user is not authorized to view this page // if user is not authorized to view this page
if (currentWorkspaceRole && !canPerformWorkspaceViewerActions) { if (workspaceUserInfo && !canPerformWorkspaceMemberActions) {
return <NotAuthorizedView section="settings" />; return <NotAuthorizedView section="settings" />;
} }

View file

@ -1,11 +1,10 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation"; import { useParams, usePathname } from "next/navigation";
// constants
import { EUserWorkspaceRoles } from "@/constants/workspace";
// hooks // hooks
import { useUser } from "@/hooks/store"; import { useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router"; import { useAppRouter } from "@/hooks/use-app-router";
// plane web constants // plane web constants
import { EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
import { WORKSPACE_SETTINGS_LINKS } from "@/plane-web/constants/workspace"; import { WORKSPACE_SETTINGS_LINKS } from "@/plane-web/constants/workspace";
// plane web helpers // plane web helpers
import { shouldRenderSettingLink } from "@/plane-web/helpers/workspace.helper"; import { shouldRenderSettingLink } from "@/plane-web/helpers/workspace.helper";
@ -15,19 +14,14 @@ export const MobileWorkspaceSettingsTabs = observer(() => {
const { workspaceSlug } = useParams(); const { workspaceSlug } = useParams();
const pathname = usePathname(); const pathname = usePathname();
// mobx store // mobx store
const { const { allowPermissions } = useUserPermissions();
membership: { currentWorkspaceRole },
} = useUser();
// derived values
const workspaceMemberInfo = currentWorkspaceRole || EUserWorkspaceRoles.GUEST;
return ( return (
<div className="flex-shrink-0 md:hidden sticky inset-0 flex overflow-x-auto bg-custom-background-100 z-10"> <div className="flex-shrink-0 md:hidden sticky inset-0 flex overflow-x-auto bg-custom-background-100 z-10">
{WORKSPACE_SETTINGS_LINKS.map( {WORKSPACE_SETTINGS_LINKS.map(
(item, index) => (item, index) =>
shouldRenderSettingLink(item.key) && shouldRenderSettingLink(item.key) &&
workspaceMemberInfo >= item.access && ( allowPermissions(item.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug.toString()) && (
<div <div
className={`${ className={`${
item.highlight(pathname, `/${workspaceSlug}`) item.highlight(pathname, `/${workspaceSlug}`)

View file

@ -6,11 +6,10 @@ import Link from "next/link";
import { useParams, usePathname } from "next/navigation"; import { useParams, usePathname } from "next/navigation";
// components // components
import { SidebarNavItem } from "@/components/sidebar"; import { SidebarNavItem } from "@/components/sidebar";
// constants
import { EUserWorkspaceRoles } from "@/constants/workspace";
// hooks // hooks
import { useUser } from "@/hooks/store"; import { useUserPermissions } from "@/hooks/store";
// plane web constants // plane web constants
import { EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
import { WORKSPACE_SETTINGS_LINKS } from "@/plane-web/constants/workspace"; import { WORKSPACE_SETTINGS_LINKS } from "@/plane-web/constants/workspace";
// plane web helpers // plane web helpers
import { shouldRenderSettingLink } from "@/plane-web/helpers/workspace.helper"; import { shouldRenderSettingLink } from "@/plane-web/helpers/workspace.helper";
@ -20,12 +19,7 @@ export const WorkspaceSettingsSidebar = observer(() => {
const { workspaceSlug } = useParams(); const { workspaceSlug } = useParams();
const pathname = usePathname(); const pathname = usePathname();
// mobx store // mobx store
const { const { allowPermissions } = useUserPermissions();
membership: { currentWorkspaceRole },
} = useUser();
// derived values
const workspaceMemberInfo = currentWorkspaceRole || EUserWorkspaceRoles.GUEST;
return ( return (
<div className="flex w-[280px] flex-col gap-6"> <div className="flex w-[280px] flex-col gap-6">
@ -35,7 +29,7 @@ export const WorkspaceSettingsSidebar = observer(() => {
{WORKSPACE_SETTINGS_LINKS.map( {WORKSPACE_SETTINGS_LINKS.map(
(link) => (link) =>
shouldRenderSettingLink(link.key) && shouldRenderSettingLink(link.key) &&
workspaceMemberInfo >= link.access && ( allowPermissions(link.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug.toString()) && (
<Link key={link.key} href={`/${workspaceSlug}${link.href}`}> <Link key={link.key} href={`/${workspaceSlug}${link.href}`}>
<SidebarNavItem <SidebarNavItem
key={link.key} key={link.key}

View file

@ -12,7 +12,8 @@ import { LogoSpinner } from "@/components/common";
import { PageHead } from "@/components/core"; import { PageHead } from "@/components/core";
import { DeleteWebhookModal, WebhookDeleteSection, WebhookForm } from "@/components/web-hooks"; import { DeleteWebhookModal, WebhookDeleteSection, WebhookForm } from "@/components/web-hooks";
// hooks // 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 WebhookDetailsPage = observer(() => { const WebhookDetailsPage = observer(() => {
// states // states
@ -20,18 +21,17 @@ const WebhookDetailsPage = observer(() => {
// router // router
const { workspaceSlug, webhookId } = useParams(); const { workspaceSlug, webhookId } = useParams();
// mobx store // mobx store
const {
membership: { currentWorkspaceRole },
} = useUser();
const { currentWebhook, fetchWebhookById, updateWebhook } = useWebhook(); const { currentWebhook, fetchWebhookById, updateWebhook } = useWebhook();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { allowPermissions } = useUserPermissions();
// TODO: fix this error // TODO: fix this error
// useEffect(() => { // useEffect(() => {
// if (isCreated !== "true") clearSecretKey(); // if (isCreated !== "true") clearSecretKey();
// }, [clearSecretKey, isCreated]); // }, [clearSecretKey, isCreated]);
const isAdmin = currentWorkspaceRole === 20; // derived values
const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Webhook` : undefined; const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Webhook` : undefined;
useSWR( useSWR(

View file

@ -15,7 +15,8 @@ import { WebhooksList, CreateWebhookModal } from "@/components/web-hooks";
// constants // constants
import { EmptyStateType } from "@/constants/empty-state"; import { EmptyStateType } from "@/constants/empty-state";
// hooks // 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(() => { const WebhooksListPage = observer(() => {
// states // states
@ -23,13 +24,13 @@ const WebhooksListPage = observer(() => {
// router // router
const { workspaceSlug } = useParams(); const { workspaceSlug } = useParams();
// mobx store // mobx store
const { const { workspaceUserInfo, allowPermissions } = useUserPermissions();
canPerformWorkspaceAdminActions,
membership: { currentWorkspaceRole },
} = useUser();
const { fetchWebhooks, webhooks, clearSecretKey, webhookSecretKey, createWebhook } = useWebhook(); const { fetchWebhooks, webhooks, clearSecretKey, webhookSecretKey, createWebhook } = useWebhook();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const canPerformWorkspaceAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
useSWR( useSWR(
workspaceSlug && canPerformWorkspaceAdminActions ? `WEBHOOKS_LIST_${workspaceSlug}` : null, workspaceSlug && canPerformWorkspaceAdminActions ? `WEBHOOKS_LIST_${workspaceSlug}` : null,
workspaceSlug && canPerformWorkspaceAdminActions ? () => fetchWebhooks(workspaceSlug.toString()) : null workspaceSlug && canPerformWorkspaceAdminActions ? () => fetchWebhooks(workspaceSlug.toString()) : null
@ -42,7 +43,7 @@ const WebhooksListPage = observer(() => {
if (!showCreateWebhookModal && webhookSecretKey) clearSecretKey(); if (!showCreateWebhookModal && webhookSecretKey) clearSecretKey();
}, [showCreateWebhookModal, webhookSecretKey, clearSecretKey]); }, [showCreateWebhookModal, webhookSecretKey, clearSecretKey]);
if (currentWorkspaceRole && !canPerformWorkspaceAdminActions) { if (workspaceUserInfo && !canPerformWorkspaceAdminActions) {
return <NotAuthorizedView section="settings" />; return <NotAuthorizedView section="settings" />;
} }

View file

@ -13,22 +13,29 @@ import {
import { SidebarFavoritesMenu } from "@/components/workspace/sidebar/favorites/favorites-menu"; import { SidebarFavoritesMenu } from "@/components/workspace/sidebar/favorites/favorites-menu";
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
// hooks // hooks
import { useAppTheme, useUser } from "@/hooks/store"; import { useAppTheme, useUserPermissions } from "@/hooks/store";
import useOutsideClickDetector from "@/hooks/use-outside-click-detector"; import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
// plane web components // plane web components
import useSize from "@/hooks/use-window-size"; import useSize from "@/hooks/use-window-size";
import { SidebarAppSwitcher } from "@/plane-web/components/sidebar"; import { SidebarAppSwitcher } from "@/plane-web/components/sidebar";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
export interface IAppSidebar {} export interface IAppSidebar {}
export const AppSidebar: FC<IAppSidebar> = observer(() => { export const AppSidebar: FC<IAppSidebar> = observer(() => {
// store hooks // store hooks
const { canPerformWorkspaceMemberActions } = useUser(); const { allowPermissions } = useUserPermissions();
const { toggleSidebar, sidebarCollapsed } = useAppTheme(); const { toggleSidebar, sidebarCollapsed } = useAppTheme();
const windowSize = useSize(); const windowSize = useSize();
// refs // refs
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
// derived values
const canPerformWorkspaceMemberActions = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.WORKSPACE
);
useOutsideClickDetector(ref, () => { useOutsideClickDetector(ref, () => {
if (sidebarCollapsed === false) { if (sidebarCollapsed === false) {
if (window.innerWidth < 768) { if (window.innerWidth < 768) {

View file

@ -27,6 +27,9 @@ import { useEventTracker, useUser, useUserProfile, useWorkspace } from "@/hooks/
import { useAppRouter } from "@/hooks/use-app-router"; import { useAppRouter } from "@/hooks/use-app-router";
// services // services
import { AuthenticationWrapper } from "@/lib/wrappers"; 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"; import { WorkspaceService } from "@/plane-web/services";
// images // images
import emptyInvitation from "@/public/empty-state/invitation.svg"; import emptyInvitation from "@/public/empty-state/invitation.svg";
@ -88,7 +91,7 @@ const UserInvitationsPage = observer(() => {
captureEvent(MEMBER_ACCEPTED, { captureEvent(MEMBER_ACCEPTED, {
member_id: invitation?.id, member_id: invitation?.id,
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain // 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, project_id: undefined,
accepted_from: "App", accepted_from: "App",
state: "SUCCESS", state: "SUCCESS",

View file

@ -1,14 +1,13 @@
import { useState } from "react"; import { useState } from "react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { IWorkspaceMember } from "@plane/types"; import { IWorkspaceMember } from "@plane/types";
import { EUserProjectRoles } from "@plane/types/src/enums";
import { AccountTypeColumn, NameColumn } from "@/components/project/settings/member-columns"; import { AccountTypeColumn, NameColumn } from "@/components/project/settings/member-columns";
import { EUserWorkspaceRoles } from "@/constants/workspace"; import { useUser, useUserPermissions } from "@/hooks/store";
import { useUser } from "@/hooks/store"; import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
export interface RowData { export interface RowData {
member: IWorkspaceMember; member: IWorkspaceMember;
role: EUserWorkspaceRoles; role: EUserPermissions;
} }
export const useProjectColumns = () => { export const useProjectColumns = () => {
@ -17,10 +16,12 @@ export const useProjectColumns = () => {
const { workspaceSlug, projectId } = useParams(); const { workspaceSlug, projectId } = useParams();
const { const { data: currentUser } = useUser();
membership: { currentProjectRole }, const { allowPermissions, projectUserInfo } = useUserPermissions();
data: currentUser,
} = useUser(); const currentProjectRole =
(projectUserInfo?.[workspaceSlug.toString()]?.[projectId.toString()]?.role as unknown as EUserPermissions) ??
EUserPermissions.GUEST;
const getFormattedDate = (dateStr: string) => { const getFormattedDate = (dateStr: string) => {
const date = new Date(dateStr); const date = new Date(dateStr);
@ -29,7 +30,13 @@ export const useProjectColumns = () => {
return date.toLocaleDateString("en-US", options); return date.toLocaleDateString("en-US", options);
}; };
// derived values // derived values
const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN; const isAdmin = allowPermissions(
[EUserPermissions.ADMIN],
EUserPermissionsLevel.PROJECT,
workspaceSlug.toString(),
projectId.toString()
);
const columns = [ const columns = [
{ {
key: "Full Name", key: "Full Name",

View file

@ -1,8 +1,8 @@
import { useState } from "react"; import { useState } from "react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { AccountTypeColumn, NameColumn, RowData } from "@/components/workspace/settings/member-columns"; import { AccountTypeColumn, NameColumn, RowData } from "@/components/workspace/settings/member-columns";
import { EUserWorkspaceRoles } from "@/constants/workspace"; import { useUser, useUserPermissions } from "@/hooks/store";
import { useUser } from "@/hooks/store"; import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
export const useMemberColumns = () => { export const useMemberColumns = () => {
// states // states
@ -10,10 +10,8 @@ export const useMemberColumns = () => {
const { workspaceSlug } = useParams(); const { workspaceSlug } = useParams();
const { const { data: currentUser } = useUser();
membership: { currentWorkspaceRole }, const { allowPermissions } = useUserPermissions();
data: currentUser,
} = useUser();
const getFormattedDate = (dateStr: string) => { const getFormattedDate = (dateStr: string) => {
const date = new Date(dateStr); const date = new Date(dateStr);
@ -22,7 +20,8 @@ export const useMemberColumns = () => {
return date.toLocaleDateString("en-US", options); return date.toLocaleDateString("en-US", options);
}; };
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN; // derived values
const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
const columns = [ const columns = [
{ {
@ -48,13 +47,7 @@ export const useMemberColumns = () => {
{ {
key: "Account Type", key: "Account Type",
content: "Account Type", content: "Account Type",
tdRender: (rowData: RowData) => ( tdRender: (rowData: RowData) => <AccountTypeColumn rowData={rowData} workspaceSlug={workspaceSlug as string} />,
<AccountTypeColumn
rowData={rowData}
currentWorkspaceRole={currentWorkspaceRole}
workspaceSlug={workspaceSlug as string}
/>
),
}, },
{ {

View file

@ -3,14 +3,14 @@ import { SettingIcon } from "@/components/icons/attachment";
// types // types
import { Props } from "@/components/icons/types"; import { Props } from "@/components/icons/types";
// constants // constants
import { EUserProjectRoles } from "@/constants/project"; import { EUserPermissions } from "../../user-permissions";
export const PROJECT_SETTINGS = { export const PROJECT_SETTINGS = {
general: { general: {
key: "general", key: "general",
label: "General", label: "General",
href: `/settings`, href: `/settings`,
access: EUserProjectRoles.GUEST, access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/`, highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/`,
Icon: SettingIcon, Icon: SettingIcon,
}, },
@ -18,7 +18,7 @@ export const PROJECT_SETTINGS = {
key: "members", key: "members",
label: "Members", label: "Members",
href: `/settings/members`, href: `/settings/members`,
access: EUserProjectRoles.VIEWER, access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/members/`, highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/members/`,
Icon: SettingIcon, Icon: SettingIcon,
}, },
@ -26,7 +26,7 @@ export const PROJECT_SETTINGS = {
key: "features", key: "features",
label: "Features", label: "Features",
href: `/settings/features`, href: `/settings/features`,
access: EUserProjectRoles.ADMIN, access: [EUserPermissions.ADMIN],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/features/`, highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/features/`,
Icon: SettingIcon, Icon: SettingIcon,
}, },
@ -34,7 +34,7 @@ export const PROJECT_SETTINGS = {
key: "states", key: "states",
label: "States", label: "States",
href: `/settings/states`, href: `/settings/states`,
access: EUserProjectRoles.MEMBER, access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/states/`, highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/states/`,
Icon: SettingIcon, Icon: SettingIcon,
}, },
@ -42,7 +42,7 @@ export const PROJECT_SETTINGS = {
key: "labels", key: "labels",
label: "Labels", label: "Labels",
href: `/settings/labels`, href: `/settings/labels`,
access: EUserProjectRoles.MEMBER, access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/labels/`, highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/labels/`,
Icon: SettingIcon, Icon: SettingIcon,
}, },
@ -50,7 +50,7 @@ export const PROJECT_SETTINGS = {
key: "estimates", key: "estimates",
label: "Estimates", label: "Estimates",
href: `/settings/estimates`, href: `/settings/estimates`,
access: EUserProjectRoles.ADMIN, access: [EUserPermissions.ADMIN],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/estimates/`, highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/estimates/`,
Icon: SettingIcon, Icon: SettingIcon,
}, },
@ -58,7 +58,7 @@ export const PROJECT_SETTINGS = {
key: "automations", key: "automations",
label: "Automations", label: "Automations",
href: `/settings/automations`, href: `/settings/automations`,
access: EUserProjectRoles.ADMIN, access: [EUserPermissions.ADMIN],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/automations/`, highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/automations/`,
Icon: SettingIcon, Icon: SettingIcon,
}, },
@ -68,7 +68,7 @@ export const PROJECT_SETTINGS_LINKS: {
key: string; key: string;
label: string; label: string;
href: string; href: string;
access: EUserProjectRoles; access: EUserPermissions[];
highlight: (pathname: string, baseUrl: string) => boolean; highlight: (pathname: string, baseUrl: string) => boolean;
Icon: React.FC<Props>; Icon: React.FC<Props>;
}[] = [ }[] = [

View file

@ -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<TUserAllowedPermissionsObject>;
};
project: {
[key: string]: Partial<TUserAllowedPermissionsObject>;
};
};
export const USER_ALLOWED_PERMISSIONS: TUserAllowedPermissions = {
workspace: {
dashboard: {
read: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
},
},
project: {},
};

View file

@ -1,15 +1,15 @@
// icons // icons
import { SettingIcon } from "@/components/icons/attachment"; import { SettingIcon } from "@/components/icons/attachment";
import { Props } from "@/components/icons/types"; import { Props } from "@/components/icons/types";
import { EUserPermissions } from "./user-permissions";
// constants // constants
import { EUserWorkspaceRoles } from "@/constants/workspace";
export const WORKSPACE_SETTINGS = { export const WORKSPACE_SETTINGS = {
general: { general: {
key: "general", key: "general",
label: "General", label: "General",
href: `/settings`, href: `/settings`,
access: EUserWorkspaceRoles.GUEST, access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/`, highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/`,
Icon: SettingIcon, Icon: SettingIcon,
}, },
@ -17,7 +17,7 @@ export const WORKSPACE_SETTINGS = {
key: "members", key: "members",
label: "Members", label: "Members",
href: `/settings/members`, href: `/settings/members`,
access: EUserWorkspaceRoles.VIEWER, access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/members/`, highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/members/`,
Icon: SettingIcon, Icon: SettingIcon,
}, },
@ -25,7 +25,7 @@ export const WORKSPACE_SETTINGS = {
key: "billing-and-plans", key: "billing-and-plans",
label: "Billing and plans", label: "Billing and plans",
href: `/settings/billing`, href: `/settings/billing`,
access: EUserWorkspaceRoles.ADMIN, access: [EUserPermissions.ADMIN],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/billing/`, highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/billing/`,
Icon: SettingIcon, Icon: SettingIcon,
}, },
@ -33,7 +33,7 @@ export const WORKSPACE_SETTINGS = {
key: "export", key: "export",
label: "Exports", label: "Exports",
href: `/settings/exports`, href: `/settings/exports`,
access: EUserWorkspaceRoles.VIEWER, access: [EUserPermissions.ADMIN],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/exports/`, highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/exports/`,
Icon: SettingIcon, Icon: SettingIcon,
}, },
@ -41,7 +41,7 @@ export const WORKSPACE_SETTINGS = {
key: "webhooks", key: "webhooks",
label: "Webhooks", label: "Webhooks",
href: `/settings/webhooks`, href: `/settings/webhooks`,
access: EUserWorkspaceRoles.ADMIN, access: [EUserPermissions.ADMIN],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/webhooks/`, highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/webhooks/`,
Icon: SettingIcon, Icon: SettingIcon,
}, },
@ -49,7 +49,7 @@ export const WORKSPACE_SETTINGS = {
key: "api-tokens", key: "api-tokens",
label: "API tokens", label: "API tokens",
href: `/settings/api-tokens`, href: `/settings/api-tokens`,
access: EUserWorkspaceRoles.ADMIN, access: [EUserPermissions.ADMIN],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/api-tokens/`, highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/api-tokens/`,
Icon: SettingIcon, Icon: SettingIcon,
}, },
@ -59,7 +59,7 @@ export const WORKSPACE_SETTINGS_LINKS: {
key: string; key: string;
label: string; label: string;
href: string; href: string;
access: EUserWorkspaceRoles; access: EUserPermissions[];
highlight: (pathname: string, baseUrl: string) => boolean; highlight: (pathname: string, baseUrl: string) => boolean;
Icon: React.FC<Props>; Icon: React.FC<Props>;
}[] = [ }[] = [

View file

@ -5,7 +5,7 @@ import { useParams } from "next/navigation";
// hooks // hooks
import { ClipboardList } from "lucide-react"; import { ClipboardList } from "lucide-react";
import { Button } from "@plane/ui"; import { Button } from "@plane/ui";
import { useProject, useUser } from "@/hooks/store"; import { useProject, useUserPermissions } from "@/hooks/store";
// ui // ui
// icons // icons
// images // images
@ -15,9 +15,7 @@ export const JoinProject: React.FC = () => {
// states // states
const [isJoiningProject, setIsJoiningProject] = useState(false); const [isJoiningProject, setIsJoiningProject] = useState(false);
// store hooks // store hooks
const { const { joinProject } = useUserPermissions();
membership: { joinProject },
} = useUser();
const { fetchProjects } = useProject(); const { fetchProjects } = useProject();
const { workspaceSlug, projectId } = useParams(); const { workspaceSlug, projectId } = useParams();
@ -27,7 +25,7 @@ export const JoinProject: React.FC = () => {
setIsJoiningProject(true); setIsJoiningProject(true);
joinProject(workspaceSlug.toString(), [projectId.toString()]) joinProject(workspaceSlug.toString(), projectId.toString())
.then(() => fetchProjects(workspaceSlug.toString())) .then(() => fetchProjects(workspaceSlug.toString()))
.finally(() => setIsJoiningProject(false)); .finally(() => setIsJoiningProject(false));
}; };

View file

@ -10,9 +10,10 @@ import { CustomSelect, Loader, ToggleSwitch } from "@plane/ui";
// component // component
import { SelectMonthModal } from "@/components/automation"; import { SelectMonthModal } from "@/components/automation";
// constants // constants
import { EUserProjectRoles, PROJECT_AUTOMATION_MONTHS } from "@/constants/project"; import { PROJECT_AUTOMATION_MONTHS } from "@/constants/project";
// hooks // hooks
import { useProject, useUser } from "@/hooks/store"; import { useProject, useUserPermissions } from "@/hooks/store";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
type Props = { type Props = {
handleChange: (formData: Partial<IProject>) => Promise<void>; handleChange: (formData: Partial<IProject>) => Promise<void>;
@ -25,12 +26,16 @@ export const AutoArchiveAutomation: React.FC<Props> = observer((props) => {
// states // states
const [monthModal, setmonthModal] = useState(false); const [monthModal, setmonthModal] = useState(false);
// store hooks // store hooks
const { const { allowPermissions } = useUserPermissions();
membership: { currentProjectRole },
} = useUser();
const { currentProjectDetails } = useProject(); const { currentProjectDetails } = useProject();
const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN; const isAdmin = allowPermissions(
[EUserPermissions.ADMIN],
EUserPermissionsLevel.PROJECT,
currentProjectDetails?.workspace_detail.slug,
currentProjectDetails?.id
);
return ( return (
<> <>

View file

@ -11,9 +11,10 @@ import { CustomSelect, CustomSearchSelect, ToggleSwitch, StateGroupIcon, DoubleC
// component // component
import { SelectMonthModal } from "@/components/automation"; import { SelectMonthModal } from "@/components/automation";
// constants // constants
import { EUserProjectRoles, PROJECT_AUTOMATION_MONTHS } from "@/constants/project"; import { PROJECT_AUTOMATION_MONTHS } from "@/constants/project";
// hooks // 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 = { type Props = {
handleChange: (formData: Partial<IProject>) => Promise<void>; handleChange: (formData: Partial<IProject>) => Promise<void>;
@ -24,11 +25,9 @@ export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
// states // states
const [monthModal, setmonthModal] = useState(false); const [monthModal, setmonthModal] = useState(false);
// store hooks // store hooks
const {
membership: { currentProjectRole },
} = useUser();
const { currentProjectDetails } = useProject(); const { currentProjectDetails } = useProject();
const { projectStates } = useProjectState(); const { projectStates } = useProjectState();
const { allowPermissions } = useUserPermissions();
// const stateGroups = projectStateStore.groupedProjectStates ?? undefined; // const stateGroups = projectStateStore.groupedProjectStates ?? undefined;
@ -57,7 +56,12 @@ export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
default_state: defaultState, default_state: defaultState,
}; };
const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN; const isAdmin = allowPermissions(
[EUserPermissions.ADMIN],
EUserPermissionsLevel.PROJECT,
currentProjectDetails?.workspace_detail?.slug,
currentProjectDetails?.id
);
return ( return (
<> <>

View file

@ -4,12 +4,12 @@ import { Command } from "cmdk";
// hooks // hooks
import Link from "next/link"; import Link from "next/link";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
// constants
import { EUserWorkspaceRoles } from "@/constants/workspace";
// hooks // hooks
import { useUser } from "@/hooks/store"; import { useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router"; import { useAppRouter } from "@/hooks/use-app-router";
// plane wev constants // plane wev constants
import { EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
import { WORKSPACE_SETTINGS_LINKS } from "@/plane-web/constants/workspace"; import { WORKSPACE_SETTINGS_LINKS } from "@/plane-web/constants/workspace";
// plane web helpers // plane web helpers
import { shouldRenderSettingLink } from "@/plane-web/helpers/workspace.helper"; import { shouldRenderSettingLink } from "@/plane-web/helpers/workspace.helper";
@ -25,11 +25,8 @@ export const CommandPaletteWorkspaceSettingsActions: React.FC<Props> = (props) =
// router params // router params
const { workspaceSlug } = useParams(); const { workspaceSlug } = useParams();
// mobx store // mobx store
const { const { allowPermissions } = useUserPermissions();
membership: { currentWorkspaceRole },
} = useUser();
// derived values // derived values
const workspaceMemberInfo = currentWorkspaceRole || EUserWorkspaceRoles.GUEST;
const redirect = (path: string) => { const redirect = (path: string) => {
closePalette(); closePalette();
@ -40,7 +37,7 @@ export const CommandPaletteWorkspaceSettingsActions: React.FC<Props> = (props) =
<> <>
{WORKSPACE_SETTINGS_LINKS.map( {WORKSPACE_SETTINGS_LINKS.map(
(setting) => (setting) =>
workspaceMemberInfo >= setting.access && allowPermissions(setting.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug.toString()) &&
shouldRenderSettingLink(setting.key) && ( shouldRenderSettingLink(setting.key) && (
<Command.Item <Command.Item
key={setting.key} key={setting.key}

View file

@ -20,10 +20,11 @@ import { ISSUE_DETAILS } from "@/constants/fetch-keys";
// helpers // helpers
import { copyTextToClipboard } from "@/helpers/string.helper"; import { copyTextToClipboard } from "@/helpers/string.helper";
// hooks // hooks
import { useEventTracker, useUser, useAppTheme, useCommandPalette } from "@/hooks/store"; import { useEventTracker, useUser, useAppTheme, useCommandPalette, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router"; import { useAppRouter } from "@/hooks/use-app-router";
import { useIssuesStore } from "@/hooks/use-issue-layout-store"; import { useIssuesStore } from "@/hooks/use-issue-layout-store";
import { usePlatformOS } from "@/hooks/use-platform-os"; import { usePlatformOS } from "@/hooks/use-platform-os";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
// services // services
import { IssueService } from "@/services/issue"; import { IssueService } from "@/services/issue";
@ -43,10 +44,10 @@ export const CommandPalette: FC = observer(() => {
const { platform } = usePlatformOS(); const { platform } = usePlatformOS();
const { const {
data: currentUser, data: currentUser,
canPerformProjectMemberActions, // canPerformProjectMemberActions,
canPerformWorkspaceMemberActions, // canPerformWorkspaceMemberActions,
canPerformAnyCreateAction, canPerformAnyCreateAction,
canPerformProjectAdminActions, // canPerformProjectAdminActions,
} = useUser(); } = useUser();
const { const {
issues: { removeIssue }, issues: { removeIssue },
@ -73,6 +74,7 @@ export const CommandPalette: FC = observer(() => {
toggleDeleteIssueModal, toggleDeleteIssueModal,
isAnyModalOpen, isAnyModalOpen,
} = useCommandPalette(); } = useCommandPalette();
const { allowPermissions } = useUserPermissions();
const { data: issueDetails } = useSWR( const { data: issueDetails } = useSWR(
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null, workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null,
@ -81,6 +83,17 @@ export const CommandPalette: FC = observer(() => {
: null : 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(() => { const copyIssueUrlToClipboard = useCallback(() => {
if (!issueId) return; if (!issueId) return;

View file

@ -17,16 +17,16 @@ import { DateRangeDropdown } from "@/components/dropdowns";
// constants // constants
import { CYCLE_STATUS } from "@/constants/cycle"; import { CYCLE_STATUS } from "@/constants/cycle";
import { CYCLE_UPDATED } from "@/constants/event-tracker"; import { CYCLE_UPDATED } from "@/constants/event-tracker";
import { EUserWorkspaceRoles } from "@/constants/workspace";
// helpers // helpers
import { findHowManyDaysLeft, getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; import { findHowManyDaysLeft, getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
import { copyUrlToClipboard } from "@/helpers/string.helper"; import { copyUrlToClipboard } from "@/helpers/string.helper";
// hooks // 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"; import { useAppRouter } from "@/hooks/use-app-router";
// plane web constants // plane web constants
import { EEstimateSystem } from "@/plane-web/constants/estimates"; import { EEstimateSystem } from "@/plane-web/constants/estimates";
// services // services
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
import { CycleService } from "@/services/cycle.service"; import { CycleService } from "@/services/cycle.service";
type Props = { type Props = {
@ -55,9 +55,8 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
// store hooks // store hooks
const { setTrackElement, captureCycleEvent } = useEventTracker(); const { setTrackElement, captureCycleEvent } = useEventTracker();
const { areEstimateEnabledByProjectId, currentActiveEstimateId, estimateById } = useProjectEstimates(); const { areEstimateEnabledByProjectId, currentActiveEstimateId, estimateById } = useProjectEstimates();
const { const { allowPermissions } = useUserPermissions();
membership: { currentProjectRole },
} = useUser();
const { getCycleById, updateCycleDetails, restoreCycle } = useCycle(); const { getCycleById, updateCycleDetails, restoreCycle } = useCycle();
const { getUserDetails } = useMember(); const { getUserDetails } = useMember();
// derived values // derived values
@ -236,7 +235,10 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
const daysLeft = findHowManyDaysLeft(cycleDetails.end_date); const daysLeft = findHowManyDaysLeft(cycleDetails.end_date);
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; const isEditingAllowed = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.PROJECT
);
return ( return (
<div className="relative"> <div className="relative">

View file

@ -4,9 +4,10 @@ import { TCycleFilters } from "@plane/types";
// hooks // hooks
import { Tag } from "@plane/ui"; import { Tag } from "@plane/ui";
import { AppliedDateFilters, AppliedStatusFilters } from "@/components/cycles"; import { AppliedDateFilters, AppliedStatusFilters } from "@/components/cycles";
import { EUserProjectRoles } from "@/constants/project";
import { replaceUnderscoreIfSnakeCase } from "@/helpers/string.helper"; 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 // components
// helpers // helpers
// types // types
@ -24,15 +25,15 @@ const DATE_FILTERS = ["start_date", "end_date"];
export const CycleAppliedFiltersList: React.FC<Props> = observer((props) => { export const CycleAppliedFiltersList: React.FC<Props> = observer((props) => {
const { appliedFilters, handleClearAllFilters, handleRemoveFilter, alwaysAllowEditing } = props; const { appliedFilters, handleClearAllFilters, handleRemoveFilter, alwaysAllowEditing } = props;
// store hooks // store hooks
const { const { allowPermissions } = useUserPermissions();
membership: { currentProjectRole },
} = useUser();
if (!appliedFilters) return null; if (!appliedFilters) return null;
if (Object.keys(appliedFilters).length === 0) 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 ( return (
<div className="flex flex-wrap items-stretch gap-2 bg-custom-background-100"> <div className="flex flex-wrap items-stretch gap-2 bg-custom-background-100">

View file

@ -14,14 +14,14 @@ import { CycleQuickActions } from "@/components/cycles";
// constants // constants
import { CYCLE_STATUS } from "@/constants/cycle"; import { CYCLE_STATUS } from "@/constants/cycle";
import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "@/constants/event-tracker"; import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "@/constants/event-tracker";
import { EUserWorkspaceRoles } from "@/constants/workspace";
// helpers // helpers
import { findHowManyDaysLeft, getDate, renderFormattedDate } from "@/helpers/date-time.helper"; import { findHowManyDaysLeft, getDate, renderFormattedDate } from "@/helpers/date-time.helper";
import { generateQueryParams } from "@/helpers/router.helper"; import { generateQueryParams } from "@/helpers/router.helper";
// hooks // 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 { useAppRouter } from "@/hooks/use-app-router";
import { usePlatformOS } from "@/hooks/use-platform-os"; import { usePlatformOS } from "@/hooks/use-platform-os";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
export interface ICyclesBoardCard { export interface ICyclesBoardCard {
workspaceSlug: string; workspaceSlug: string;
@ -39,9 +39,8 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = observer((props) => {
const pathname = usePathname(); const pathname = usePathname();
// store // store
const { captureEvent } = useEventTracker(); const { captureEvent } = useEventTracker();
const { const { allowPermissions } = useUserPermissions();
membership: { currentProjectRole },
} = useUser();
const { addCycleToFavorites, removeCycleFromFavorites, getCycleById } = useCycle(); const { addCycleToFavorites, removeCycleFromFavorites, getCycleById } = useCycle();
const { getUserDetails } = useMember(); const { getUserDetails } = useMember();
// computed // computed
@ -57,7 +56,10 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = observer((props) => {
const startDate = getDate(cycleDetails.start_date); const startDate = getDate(cycleDetails.start_date);
const isDateValid = cycleDetails.start_date || cycleDetails.end_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); const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);

View file

@ -15,13 +15,14 @@ import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
// constants // constants
import { CYCLE_STATUS } from "@/constants/cycle"; import { CYCLE_STATUS } from "@/constants/cycle";
import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "@/constants/event-tracker"; import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "@/constants/event-tracker";
import { EUserProjectRoles } from "@/constants/project";
// helpers // helpers
import { findHowManyDaysLeft, getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; import { findHowManyDaysLeft, getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
// hooks // 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 { usePlatformOS } from "@/hooks/use-platform-os";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
import { CycleService } from "@/services/cycle.service"; import { CycleService } from "@/services/cycle.service";
const cycleService = new CycleService(); const cycleService = new CycleService();
type Props = { type Props = {
@ -44,9 +45,8 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
// store hooks // store hooks
const { addCycleToFavorites, removeCycleFromFavorites, updateCycleDetails } = useCycle(); const { addCycleToFavorites, removeCycleFromFavorites, updateCycleDetails } = useCycle();
const { captureEvent } = useEventTracker(); const { captureEvent } = useEventTracker();
const { const { allowPermissions } = useUserPermissions();
membership: { currentProjectRole },
} = useUser();
const { getUserDetails } = useMember(); const { getUserDetails } = useMember();
// form // form
@ -56,7 +56,10 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
// derived values // derived values
const cycleStatus = cycleDetails.status ? (cycleDetails.status.toLocaleLowerCase() as TCycleGroups) : "draft"; 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 renderIcon = Boolean(cycleDetails.start_date) || Boolean(cycleDetails.end_date);
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus); const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
const daysLeft = findHowManyDaysLeft(cycleDetails.end_date) ?? 0; const daysLeft = findHowManyDaysLeft(cycleDetails.end_date) ?? 0;

View file

@ -9,14 +9,13 @@ import { ArchiveRestoreIcon, ExternalLink, LinkIcon, Pencil, Trash2 } from "luci
import { ArchiveIcon, ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui"; import { ArchiveIcon, ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui";
// components // components
import { ArchiveCycleModal, CycleCreateUpdateModal, CycleDeleteModal } from "@/components/cycles"; import { ArchiveCycleModal, CycleCreateUpdateModal, CycleDeleteModal } from "@/components/cycles";
// constants
import { EUserProjectRoles } from "@/constants/project";
// helpers // helpers
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
import { copyUrlToClipboard } from "@/helpers/string.helper"; import { copyUrlToClipboard } from "@/helpers/string.helper";
// hooks // hooks
import { useCycle, useEventTracker, useUser } from "@/hooks/store"; import { useCycle, useEventTracker, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router"; import { useAppRouter } from "@/hooks/use-app-router";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
type Props = { type Props = {
parentRef: React.RefObject<HTMLElement>; parentRef: React.RefObject<HTMLElement>;
@ -35,17 +34,19 @@ export const CycleQuickActions: React.FC<Props> = observer((props) => {
const [deleteModal, setDeleteModal] = useState(false); const [deleteModal, setDeleteModal] = useState(false);
// store hooks // store hooks
const { setTrackElement } = useEventTracker(); const { setTrackElement } = useEventTracker();
const { const { allowPermissions } = useUserPermissions();
membership: { currentWorkspaceAllProjectsRole },
} = useUser();
const { getCycleById, restoreCycle } = useCycle(); const { getCycleById, restoreCycle } = useCycle();
// derived values // derived values
const cycleDetails = getCycleById(cycleId); const cycleDetails = getCycleById(cycleId);
const isArchived = !!cycleDetails?.archived_at; const isArchived = !!cycleDetails?.archived_at;
const isCompleted = cycleDetails?.status?.toLowerCase() === "completed"; const isCompleted = cycleDetails?.status?.toLowerCase() === "completed";
// auth // auth
const isEditingAllowed = const isEditingAllowed = allowPermissions(
!!currentWorkspaceAllProjectsRole && currentWorkspaceAllProjectsRole[projectId] >= EUserProjectRoles.MEMBER; [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.PROJECT,
workspaceSlug,
projectId
);
const cycleLink = `${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`; const cycleLink = `${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`;
const handleCopyText = () => const handleCopyText = () =>

View file

@ -4,10 +4,9 @@ import { observer } from "mobx-react";
import Image from "next/image"; import Image from "next/image";
// ui // ui
import { Button } from "@plane/ui"; import { Button } from "@plane/ui";
// constants
import { EUserWorkspaceRoles } from "@/constants/workspace";
// hooks // 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 // assets
import ProjectEmptyStateImage from "@/public/empty-state/onboarding/dashboard-light.webp"; import ProjectEmptyStateImage from "@/public/empty-state/onboarding/dashboard-light.webp";
@ -15,11 +14,10 @@ export const DashboardProjectEmptyState = observer(() => {
// store hooks // store hooks
const { toggleCreateProjectModal } = useCommandPalette(); const { toggleCreateProjectModal } = useCommandPalette();
const { setTrackElement } = useEventTracker(); const { setTrackElement } = useEventTracker();
const { const { allowPermissions } = useUserPermissions();
membership: { currentWorkspaceRole },
} = useUser();
// derived values // derived values
const canCreateProject = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN; const canCreateProject = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
return ( return (
<div className="mx-auto flex h-full flex-col justify-center space-y-4 lg:w-3/5"> <div className="mx-auto flex h-full flex-col justify-center space-y-4 lg:w-3/5">

View file

@ -14,9 +14,9 @@ import { Logo } from "@/components/common";
import { WidgetLoader, WidgetProps } from "@/components/dashboard/widgets"; import { WidgetLoader, WidgetProps } from "@/components/dashboard/widgets";
// constants // constants
import { PROJECT_BACKGROUND_COLORS } from "@/constants/dashboard"; import { PROJECT_BACKGROUND_COLORS } from "@/constants/dashboard";
import { EUserWorkspaceRoles } from "@/constants/workspace";
// hooks // 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"; const WIDGET_KEY = "recent_projects";
@ -65,13 +65,14 @@ export const RecentProjectsWidget: React.FC<WidgetProps> = observer((props) => {
// store hooks // store hooks
const { toggleCreateProjectModal } = useCommandPalette(); const { toggleCreateProjectModal } = useCommandPalette();
const { setTrackElement } = useEventTracker(); const { setTrackElement } = useEventTracker();
const { const { allowPermissions } = useUserPermissions();
membership: { currentWorkspaceRole },
} = useUser();
const { fetchWidgetStats, getWidgetStats } = useDashboard(); const { fetchWidgetStats, getWidgetStats } = useDashboard();
// derived values // derived values
const widgetStats = getWidgetStats<TRecentProjectsWidgetResponse>(workspaceSlug, dashboardId, WIDGET_KEY); const widgetStats = getWidgetStats<TRecentProjectsWidgetResponse>(workspaceSlug, dashboardId, WIDGET_KEY);
const canCreateProject = currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; const canCreateProject = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.WORKSPACE
);
useEffect(() => { useEffect(() => {
fetchWidgetStats(workspaceSlug, dashboardId, { fetchWidgetStats(workspaceSlug, dashboardId, {

View file

@ -13,7 +13,8 @@ import { Button, TButtonVariant } from "@plane/ui";
import { EMPTY_STATE_DETAILS, EmptyStateType } from "@/constants/empty-state"; import { EMPTY_STATE_DETAILS, EmptyStateType } from "@/constants/empty-state";
// helpers // helpers
import { cn } from "@/helpers/common.helper"; 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"; import { ComicBoxButton } from "./comic-box-button";
export type EmptyStateProps = { export type EmptyStateProps = {
@ -37,9 +38,7 @@ export const EmptyState: React.FC<EmptyStateProps> = observer((props) => {
secondaryButtonOnClick, secondaryButtonOnClick,
} = props; } = props;
// store // store
const { const { allowPermissions } = useUserPermissions();
membership: { currentWorkspaceRole, currentProjectRole },
} = useUser();
// theme // theme
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme();
@ -53,10 +52,14 @@ export const EmptyState: React.FC<EmptyStateProps> = observer((props) => {
const resolvedEmptyStatePath = `${additionalPath && additionalPath !== "" ? `${path}${additionalPath}` : path}-${ const resolvedEmptyStatePath = `${additionalPath && additionalPath !== "" ? `${path}${additionalPath}` : path}-${
resolvedTheme === "light" ? "light" : "dark" resolvedTheme === "light" ? "light" : "dark"
}.webp`; }.webp`;
// current access type
const currentAccessType = accessType === "workspace" ? currentWorkspaceRole : currentProjectRole;
// permission // permission
const isEditingAllowed = currentAccessType && access && currentAccessType >= access; const isEditingAllowed =
access &&
accessType &&
allowPermissions(
access,
accessType === "workspace" ? EUserPermissionsLevel.WORKSPACE : EUserPermissionsLevel.PROJECT
);
const anyButton = primaryButton || secondaryButton; const anyButton = primaryButton || secondaryButton;
// primary button // primary button

View file

@ -17,10 +17,11 @@ import { ImportExportSettingsLoader } from "@/components/ui";
// constants // constants
import { EmptyStateType } from "@/constants/empty-state"; import { EmptyStateType } from "@/constants/empty-state";
import { EXPORT_SERVICES_LIST } from "@/constants/fetch-keys"; import { EXPORT_SERVICES_LIST } from "@/constants/fetch-keys";
import { EUserWorkspaceRoles, EXPORTERS_LIST } from "@/constants/workspace"; import { EXPORTERS_LIST } from "@/constants/workspace";
// hooks // hooks
import { useProject, useUser } from "@/hooks/store"; import { useProject, useUser, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router"; import { useAppRouter } from "@/hooks/use-app-router";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
// services // services
import { IntegrationService } from "@/services/integrations"; import { IntegrationService } from "@/services/integrations";
@ -37,11 +38,9 @@ const IntegrationGuide = observer(() => {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const provider = searchParams.get("provider"); const provider = searchParams.get("provider");
// store hooks // store hooks
const { const { data: currentUser, canPerformAnyCreateAction } = useUser();
data: currentUser, const { allowPermissions } = useUserPermissions();
canPerformAnyCreateAction,
membership: { currentWorkspaceRole },
} = useUser();
const { workspaceProjectIds } = useProject(); const { workspaceProjectIds } = useProject();
const { data: exporterServices } = useSWR( const { data: exporterServices } = useSWR(
@ -61,7 +60,7 @@ const IntegrationGuide = observer(() => {
}; };
const hasProjects = workspaceProjectIds && workspaceProjectIds.length > 0; const hasProjects = workspaceProjectIds && workspaceProjectIds.length > 0;
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN; const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {

View file

@ -27,15 +27,14 @@ import {
SelectDuplicateInboxIssueModal, SelectDuplicateInboxIssueModal,
} from "@/components/inbox"; } from "@/components/inbox";
import { IssueUpdateStatus } from "@/components/issues"; import { IssueUpdateStatus } from "@/components/issues";
// constants
import { EUserProjectRoles } from "@/constants/project";
// helpers // helpers
import { findHowManyDaysLeft } from "@/helpers/date-time.helper"; import { findHowManyDaysLeft } from "@/helpers/date-time.helper";
import { EInboxIssueStatus } from "@/helpers/inbox.helper"; import { EInboxIssueStatus } from "@/helpers/inbox.helper";
import { copyUrlToClipboard } from "@/helpers/string.helper"; import { copyUrlToClipboard } from "@/helpers/string.helper";
// hooks // hooks
import { useUser, useProjectInbox, useProject } from "@/hooks/store"; import { useUser, useProjectInbox, useProject, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router"; import { useAppRouter } from "@/hooks/use-app-router";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
// store types // store types
import type { IInboxIssueStore } from "@/store/inbox/inbox-issue.store"; import type { IInboxIssueStore } from "@/store/inbox/inbox-issue.store";
@ -70,23 +69,26 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
// store // store
const { currentTab, deleteInboxIssue, filteredInboxIssueIds } = useProjectInbox(); const { currentTab, deleteInboxIssue, filteredInboxIssueIds } = useProjectInbox();
const { data: currentUser } = useUser(); const { data: currentUser } = useUser();
const { const { allowPermissions } = useUserPermissions();
membership: { currentProjectRoleByProjectId },
} = useUser();
const router = useAppRouter(); const router = useAppRouter();
const { getProjectById } = useProject(); const { getProjectById } = useProject();
const issue = inboxIssue?.issue; const issue = inboxIssue?.issue;
// derived values // derived values
const currentProjectRole = currentProjectRoleByProjectId(projectId) || undefined; const isAllowed = allowPermissions(
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.PROJECT,
workspaceSlug,
projectId
);
const canMarkAsDuplicate = isAllowed && (inboxIssue?.status === 0 || inboxIssue?.status === -2); const canMarkAsDuplicate = isAllowed && (inboxIssue?.status === 0 || inboxIssue?.status === -2);
const canMarkAsAccepted = 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); const canMarkAsDeclined = isAllowed && (inboxIssue?.status === 0 || inboxIssue?.status === -2);
// can delete only if admin or is creator of the issue // can delete only if admin or is creator of the issue
const canDelete = 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; const isAcceptedOrDeclined = inboxIssue?.status ? [-1, 1, 2].includes(inboxIssue.status) : undefined;
// days left for snooze // days left for snooze
const numberOfDaysLeft = findHowManyDaysLeft(inboxIssue?.snoozed_till); const numberOfDaysLeft = findHowManyDaysLeft(inboxIssue?.snoozed_till);

View file

@ -4,11 +4,10 @@ import useSWR from "swr";
// components // components
import { ContentWrapper } from "@plane/ui"; import { ContentWrapper } from "@plane/ui";
import { InboxIssueActionsHeader, InboxIssueMainContent } from "@/components/inbox"; import { InboxIssueActionsHeader, InboxIssueMainContent } from "@/components/inbox";
// constants
import { EUserProjectRoles } from "@/constants/project";
// hooks // hooks
import { useProjectInbox, useUser } from "@/hooks/store"; import { useProjectInbox, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router"; import { useAppRouter } from "@/hooks/use-app-router";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
type TInboxContentRoot = { type TInboxContentRoot = {
workspaceSlug: string; workspaceSlug: string;
@ -37,9 +36,8 @@ export const InboxContentRoot: FC<TInboxContentRoot> = observer((props) => {
// hooks // hooks
const { currentTab, fetchInboxIssueById, getIssueInboxByIssueId, getIsIssueAvailable } = useProjectInbox(); const { currentTab, fetchInboxIssueById, getIssueInboxByIssueId, getIsIssueAvailable } = useProjectInbox();
const inboxIssue = getIssueInboxByIssueId(inboxIssueId); const inboxIssue = getIssueInboxByIssueId(inboxIssueId);
const { const { allowPermissions } = useUserPermissions();
membership: { currentProjectRole },
} = useUser();
// derived values // derived values
const isIssueAvailable = getIsIssueAvailable(inboxIssueId?.toString() || ""); const isIssueAvailable = getIsIssueAvailable(inboxIssueId?.toString() || "");
@ -63,7 +61,7 @@ export const InboxContentRoot: FC<TInboxContentRoot> = observer((props) => {
} }
); );
const isEditable = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; const isEditable = allowPermissions([EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.PROJECT);
if (!inboxIssue) return <></>; if (!inboxIssue) return <></>;

View file

@ -12,9 +12,10 @@ import { Button, Loader, Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
// constants // constants
import { WORKSPACE_INTEGRATIONS } from "@/constants/fetch-keys"; import { WORKSPACE_INTEGRATIONS } from "@/constants/fetch-keys";
// hooks // hooks
import { useUser, useInstance } from "@/hooks/store"; import { useInstance, useUserPermissions } from "@/hooks/store";
import useIntegrationPopup from "@/hooks/use-integration-popup"; import useIntegrationPopup from "@/hooks/use-integration-popup";
import { usePlatformOS } from "@/hooks/use-platform-os"; import { usePlatformOS } from "@/hooks/use-platform-os";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
// services // services
// icons // icons
import GithubLogo from "@/public/services/github.png"; import GithubLogo from "@/public/services/github.png";
@ -48,11 +49,9 @@ export const SingleIntegrationCard: React.FC<Props> = observer(({ integration })
const { workspaceSlug } = useParams(); const { workspaceSlug } = useParams();
// store hooks // store hooks
const { config } = useInstance(); const { config } = useInstance();
const { const { allowPermissions } = useUserPermissions();
membership: { currentWorkspaceRole },
} = useUser();
const isUserAdmin = currentWorkspaceRole === 20; const isUserAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
const { isMobile } = usePlatformOS(); const { isMobile } = usePlatformOS();
const { startAuth, isConnecting: isInstalling } = useIntegrationPopup({ const { startAuth, isConnecting: isInstalling } = useIntegrationPopup({
provider: integration.provider, provider: integration.provider,

View file

@ -8,8 +8,8 @@ import { AlertModalCore, TOAST_TYPE, setToast } from "@plane/ui";
// constants // constants
import { PROJECT_ERROR_MESSAGES } from "@/constants/project"; import { PROJECT_ERROR_MESSAGES } from "@/constants/project";
// hooks // 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 = { type Props = {
isOpen: boolean; isOpen: boolean;
handleClose: () => void; handleClose: () => void;
@ -26,7 +26,12 @@ export const DeleteIssueModal: React.FC<Props> = (props) => {
// store hooks // store hooks
const { issueMap } = useIssues(); const { issueMap } = useIssues();
const { getProjectById } = useProject(); 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(() => { useEffect(() => {
setIsDeleting(false); setIsDeleting(false);

View file

@ -10,15 +10,22 @@ import { ArchiveIssueModal, DeleteIssueModal, IssueSubscription } from "@/compon
// constants // constants
import { ISSUE_ARCHIVED, ISSUE_DELETED } from "@/constants/event-tracker"; import { ISSUE_ARCHIVED, ISSUE_DELETED } from "@/constants/event-tracker";
import { EIssuesStoreType } from "@/constants/issue"; import { EIssuesStoreType } from "@/constants/issue";
import { EUserProjectRoles } from "@/constants/project";
import { ARCHIVABLE_STATE_GROUPS } from "@/constants/state"; import { ARCHIVABLE_STATE_GROUPS } from "@/constants/state";
// helpers // helpers
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
import { copyTextToClipboard } from "@/helpers/string.helper"; import { copyTextToClipboard } from "@/helpers/string.helper";
// hooks // 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 { useAppRouter } from "@/hooks/use-app-router";
import { usePlatformOS } from "@/hooks/use-platform-os"; import { usePlatformOS } from "@/hooks/use-platform-os";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
type Props = { type Props = {
workspaceSlug: string; workspaceSlug: string;
@ -38,10 +45,8 @@ export const IssueDetailQuickActions: FC<Props> = observer((props) => {
const router = useAppRouter(); const router = useAppRouter();
// hooks // hooks
const { const { data: currentUser } = useUser();
data: currentUser, const { allowPermissions } = useUserPermissions();
membership: { currentProjectRole },
} = useUser();
const { isMobile } = usePlatformOS(); const { isMobile } = usePlatformOS();
const { getStateById } = useProjectState(); const { getStateById } = useProjectState();
const { const {
@ -149,8 +154,11 @@ export const IssueDetailQuickActions: FC<Props> = observer((props) => {
}; };
// auth // auth
const isEditable = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; const isEditable = allowPermissions([EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.PROJECT);
const canRestoreIssue = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; const canRestoreIssue = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.PROJECT
);
const isArchivingAllowed = !issue?.archived_at && isEditable; const isArchivingAllowed = !issue?.archived_at && isEditable;
const isInArchivableGroup = !!stateDetails && ARCHIVABLE_STATE_GROUPS.includes(stateDetails?.group); const isInArchivableGroup = !!stateDetails && ARCHIVABLE_STATE_GROUPS.includes(stateDetails?.group);

View file

@ -13,10 +13,10 @@ import { IssuePeekOverview } from "@/components/issues";
// constants // constants
import { ISSUE_UPDATED, ISSUE_DELETED, ISSUE_ARCHIVED } from "@/constants/event-tracker"; import { ISSUE_UPDATED, ISSUE_DELETED, ISSUE_ARCHIVED } from "@/constants/event-tracker";
import { EIssuesStoreType } from "@/constants/issue"; import { EIssuesStoreType } from "@/constants/issue";
import { EUserProjectRoles } from "@/constants/project";
// hooks // 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 { useAppRouter } from "@/hooks/use-app-router";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
// images // images
import emptyIssue from "@/public/empty-state/issue.svg"; import emptyIssue from "@/public/empty-state/issue.svg";
// local components // local components
@ -77,9 +77,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
issues: { removeIssue: removeArchivedIssue }, issues: { removeIssue: removeArchivedIssue },
} = useIssues(EIssuesStoreType.ARCHIVED); } = useIssues(EIssuesStoreType.ARCHIVED);
const { captureIssueEvent } = useEventTracker(); const { captureIssueEvent } = useEventTracker();
const { const { allowPermissions } = useUserPermissions();
membership: { currentProjectRole },
} = useUser();
const { issueDetailSidebarCollapsed } = useAppTheme(); const { issueDetailSidebarCollapsed } = useAppTheme();
const issueOperations: TIssueOperations = useMemo( const issueOperations: TIssueOperations = useMemo(
@ -332,7 +330,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
// issue details // issue details
const issue = getIssueById(issueId); const issue = getIssueById(issueId);
// checking if issue is editable, based on user role // 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 ( return (
<> <>

View file

@ -10,11 +10,11 @@ import { TOAST_TYPE, setToast } from "@plane/ui";
import { CalendarChart } from "@/components/issues"; import { CalendarChart } from "@/components/issues";
//constants //constants
import { EIssuesStoreType } from "@/constants/issue"; import { EIssuesStoreType } from "@/constants/issue";
import { EUserProjectRoles } from "@/constants/project";
// hooks // hooks
import { useIssues, useUser, useCalendarView } from "@/hooks/store"; import { useIssues, useCalendarView, useUserPermissions } from "@/hooks/store";
import { useIssueStoreType } from "@/hooks/use-issue-layout-store"; import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
import { useIssuesActions } from "@/hooks/use-issues-actions"; import { useIssuesActions } from "@/hooks/use-issues-actions";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
// types // types
import { IQuickActionProps } from "../list/list-view-types"; import { IQuickActionProps } from "../list/list-view-types";
import { handleDragDrop } from "./utils"; import { handleDragDrop } from "./utils";
@ -40,9 +40,7 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
// hooks // hooks
const storeType = useIssueStoreType() as CalendarStoreType; const storeType = useIssueStoreType() as CalendarStoreType;
const { const { allowPermissions } = useUserPermissions();
membership: { currentProjectRole },
} = useUser();
const { issues, issuesFilter, issueMap } = useIssues(storeType); const { issues, issuesFilter, issueMap } = useIssues(storeType);
const { const {
fetchIssues, fetchIssues,
@ -58,7 +56,10 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
const issueCalendarView = useCalendarView(); const issueCalendarView = useCalendarView();
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; const isEditingAllowed = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.PROJECT
);
const displayFilters = issuesFilter.issueFilters?.displayFilters; const displayFilters = issuesFilter.issueFilters?.displayFilters;

Some files were not shown because too many files have changed in this diff Show more