[WEB-2126] chore: guest and viewer role permission (#5347)
* chore: user store code refactor * chore: general unauthorized screen asset added * chore: workspace setting sidebar options updated for guest and viewer * chore: NotAuthorizedView component code updated * chore: project setting layout code refactor * chore: workspace setting members and exports page permission validation added * chore: workspace members and exports settings page improvement * chore: project invite modal updated * chore: workspace setting unauthorized access empty state * chore: workspace setting unauthorized access empty state * chore: project settings sidebar permission updated * fix: project settings user role permission updated * chore: app sidebar role permission validation updated * chore: app sidebar role permission validation * chore: disabled page empty state validation * chore: app sidebar add project improvement * chore: guest role changes * fix: user favorite * chore: changed pages permission * chore: guest role changes * fix: app sidebar project item permission * fix: project setting empty state flicker * fix: workspace setting empty state flicker * chore: granted notification permission to viewer * chore: project invite and edit validation updated * chore: favorite validation added for guest and viewer role * chore: create view validation updated * chore: views permission changes * chore: create view empty state validation updated * chore: created ENUM for permissions --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com> Co-authored-by: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com>
This commit is contained in:
parent
d60e988ca1
commit
0a1c656865
62 changed files with 957 additions and 590 deletions
|
|
@ -12,3 +12,4 @@ from .project import (
|
|||
ProjectMemberPermission,
|
||||
ProjectLitePermission,
|
||||
)
|
||||
from .base import allow_permission, ROLE
|
||||
61
apiserver/plane/app/permissions/base.py
Normal file
61
apiserver/plane/app/permissions/base.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
from plane.db.models import WorkspaceMember, ProjectMember
|
||||
from functools import wraps
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
|
||||
from enum import Enum
|
||||
|
||||
class ROLE(Enum):
|
||||
ADMIN = 20
|
||||
MEMBER = 15
|
||||
VIEWER = 10
|
||||
GUEST = 5
|
||||
|
||||
|
||||
def allow_permission(allowed_roles, level="PROJECT", creator=False, model=None):
|
||||
def decorator(view_func):
|
||||
@wraps(view_func)
|
||||
def _wrapped_view(instance, request, *args, **kwargs):
|
||||
|
||||
# Check for creator if required
|
||||
if creator and model:
|
||||
obj = model.objects.filter(
|
||||
id=kwargs["pk"], created_by=request.user
|
||||
).exists()
|
||||
if obj:
|
||||
return view_func(instance, request, *args, **kwargs)
|
||||
|
||||
# Convert allowed_roles to their values if they are enum members
|
||||
allowed_role_values = [
|
||||
role.value if isinstance(role, ROLE) else role
|
||||
for role in allowed_roles
|
||||
]
|
||||
|
||||
# Check role permissions
|
||||
if level == "WORKSPACE":
|
||||
if WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=kwargs["slug"],
|
||||
role__in=allowed_role_values,
|
||||
is_active=True,
|
||||
).exists():
|
||||
return view_func(instance, request, *args, **kwargs)
|
||||
else:
|
||||
if ProjectMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=kwargs["slug"],
|
||||
project_id=kwargs["project_id"],
|
||||
role__in=allowed_role_values,
|
||||
is_active=True,
|
||||
).exists():
|
||||
return view_func(instance, request, *args, **kwargs)
|
||||
|
||||
# Return permission denied if no conditions are met
|
||||
return Response(
|
||||
{"error": "You don't have the required permissions."},
|
||||
status=status.HTTP_401_UNAUTHORIZED,
|
||||
)
|
||||
|
||||
return _wrapped_view
|
||||
|
||||
return decorator
|
||||
|
|
@ -40,7 +40,7 @@ urlpatterns = [
|
|||
name="inbox-issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:issue_id>/",
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:pk>/",
|
||||
InboxIssueViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
|
|
|
|||
|
|
@ -7,22 +7,22 @@ from django.utils import timezone
|
|||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
from plane.app.permissions import WorkSpaceAdminPermission
|
||||
from plane.app.serializers import AnalyticViewSerializer
|
||||
|
||||
# Module imports
|
||||
from plane.app.views.base import BaseAPIView, BaseViewSet
|
||||
from plane.bgtasks.analytic_plot_export import analytic_export_task
|
||||
from plane.db.models import AnalyticView, Issue, Workspace
|
||||
from plane.utils.analytics_plot import build_graph_plot
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
|
||||
|
||||
class AnalyticsEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
WorkSpaceAdminPermission,
|
||||
]
|
||||
|
||||
@allow_permission(
|
||||
[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER], level="WORKSPACE"
|
||||
)
|
||||
def get(self, request, slug):
|
||||
x_axis = request.GET.get("x_axis", False)
|
||||
y_axis = request.GET.get("y_axis", False)
|
||||
|
|
@ -201,10 +201,10 @@ class AnalyticViewViewset(BaseViewSet):
|
|||
|
||||
|
||||
class SavedAnalyticEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
WorkSpaceAdminPermission,
|
||||
]
|
||||
|
||||
@allow_permission(
|
||||
[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER], level="WORKSPACE"
|
||||
)
|
||||
def get(self, request, slug, analytic_id):
|
||||
analytic_view = AnalyticView.objects.get(
|
||||
pk=analytic_id, workspace__slug=slug
|
||||
|
|
@ -234,10 +234,10 @@ class SavedAnalyticEndpoint(BaseAPIView):
|
|||
|
||||
|
||||
class ExportAnalyticsEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
WorkSpaceAdminPermission,
|
||||
]
|
||||
|
||||
@allow_permission(
|
||||
[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER], level="WORKSPACE"
|
||||
)
|
||||
def post(self, request, slug):
|
||||
x_axis = request.data.get("x_axis", False)
|
||||
y_axis = request.data.get("y_axis", False)
|
||||
|
|
@ -301,10 +301,10 @@ class ExportAnalyticsEndpoint(BaseAPIView):
|
|||
|
||||
|
||||
class DefaultAnalyticsEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
WorkSpaceAdminPermission,
|
||||
]
|
||||
|
||||
@allow_permission(
|
||||
[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST], level="WORKSPACE"
|
||||
)
|
||||
def get(self, request, slug):
|
||||
filters = issue_filters(request.GET, "GET")
|
||||
base_issues = Issue.issue_objects.filter(
|
||||
|
|
@ -380,12 +380,10 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
|
|||
.order_by("-count")
|
||||
)
|
||||
|
||||
open_estimate_sum = open_issues_queryset.aggregate(
|
||||
sum=Sum("point")
|
||||
)["sum"]
|
||||
total_estimate_sum = base_issues.aggregate(sum=Sum("point"))[
|
||||
open_estimate_sum = open_issues_queryset.aggregate(sum=Sum("point"))[
|
||||
"sum"
|
||||
]
|
||||
total_estimate_sum = base_issues.aggregate(sum=Sum("point"))["sum"]
|
||||
|
||||
return Response(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ from django.utils import timezone
|
|||
# Third party imports
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from plane.app.permissions import ProjectEntityPermission
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
from plane.db.models import Cycle, UserFavorite, Issue, Label, User, Project
|
||||
from plane.utils.analytics_plot import burndown_plot
|
||||
|
||||
|
|
@ -34,10 +34,6 @@ from .. import BaseAPIView
|
|||
|
||||
class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
favorite_subquery = UserFavorite.objects.filter(
|
||||
user=self.request.user,
|
||||
|
|
@ -292,6 +288,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
|||
.distinct()
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER])
|
||||
def get(self, request, slug, project_id, pk=None):
|
||||
if pk is None:
|
||||
queryset = (
|
||||
|
|
@ -596,6 +593,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
|||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def post(self, request, slug, project_id, cycle_id):
|
||||
cycle = Cycle.objects.get(
|
||||
pk=cycle_id, project_id=project_id, workspace__slug=slug
|
||||
|
|
@ -614,6 +612,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
|||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def delete(self, request, slug, project_id, cycle_id):
|
||||
cycle = Cycle.objects.get(
|
||||
pk=cycle_id, project_id=project_id, workspace__slug=slug
|
||||
|
|
|
|||
|
|
@ -29,8 +29,7 @@ from django.core.serializers.json import DjangoJSONEncoder
|
|||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from plane.app.permissions import (
|
||||
ProjectEntityPermission,
|
||||
ProjectLitePermission,
|
||||
allow_permission, ROLE
|
||||
)
|
||||
from plane.app.serializers import (
|
||||
CycleSerializer,
|
||||
|
|
@ -60,15 +59,6 @@ class CycleViewSet(BaseViewSet):
|
|||
serializer_class = CycleSerializer
|
||||
model = Cycle
|
||||
webhook_event = "cycle"
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
owned_by=self.request.user,
|
||||
)
|
||||
|
||||
def get_queryset(self):
|
||||
favorite_subquery = UserFavorite.objects.filter(
|
||||
|
|
@ -325,6 +315,7 @@ class CycleViewSet(BaseViewSet):
|
|||
.distinct()
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
|
||||
def list(self, request, slug, project_id):
|
||||
queryset = self.get_queryset().filter(archived_at__isnull=True)
|
||||
cycle_view = request.GET.get("cycle_view", "all")
|
||||
|
|
@ -611,6 +602,7 @@ class CycleViewSet(BaseViewSet):
|
|||
)
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def create(self, request, slug, project_id):
|
||||
if (
|
||||
request.data.get("start_date", None) is None
|
||||
|
|
@ -684,6 +676,7 @@ class CycleViewSet(BaseViewSet):
|
|||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def partial_update(self, request, slug, project_id, pk):
|
||||
queryset = self.get_queryset().filter(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
|
|
@ -771,6 +764,7 @@ class CycleViewSet(BaseViewSet):
|
|||
return Response(cycle, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER])
|
||||
def retrieve(self, request, slug, project_id, pk):
|
||||
queryset = (
|
||||
self.get_queryset().filter(archived_at__isnull=True).filter(pk=pk)
|
||||
|
|
@ -1039,6 +1033,7 @@ class CycleViewSet(BaseViewSet):
|
|||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN], creator=True, model=Cycle)
|
||||
def destroy(self, request, slug, project_id, pk):
|
||||
cycle = Cycle.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
|
|
@ -1097,10 +1092,8 @@ class CycleViewSet(BaseViewSet):
|
|||
|
||||
|
||||
class CycleDateCheckEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def post(self, request, slug, project_id):
|
||||
start_date = request.data.get("start_date", False)
|
||||
end_date = request.data.get("end_date", False)
|
||||
|
|
@ -1144,6 +1137,7 @@ class CycleFavoriteViewSet(BaseViewSet):
|
|||
.select_related("cycle", "cycle__owned_by")
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def create(self, request, slug, project_id):
|
||||
_ = UserFavorite.objects.create(
|
||||
project_id=project_id,
|
||||
|
|
@ -1153,6 +1147,7 @@ class CycleFavoriteViewSet(BaseViewSet):
|
|||
)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def destroy(self, request, slug, project_id, cycle_id):
|
||||
cycle_favorite = UserFavorite.objects.get(
|
||||
project=project_id,
|
||||
|
|
@ -1166,10 +1161,8 @@ class CycleFavoriteViewSet(BaseViewSet):
|
|||
|
||||
|
||||
class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def post(self, request, slug, project_id, cycle_id):
|
||||
new_cycle_id = request.data.get("new_cycle_id", False)
|
||||
|
||||
|
|
@ -1579,10 +1572,8 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
|||
|
||||
|
||||
class CycleUserPropertiesEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
ProjectLitePermission,
|
||||
]
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
|
||||
def patch(self, request, slug, project_id, cycle_id):
|
||||
cycle_properties = CycleUserProperties.objects.get(
|
||||
user=request.user,
|
||||
|
|
@ -1605,6 +1596,7 @@ class CycleUserPropertiesEndpoint(BaseAPIView):
|
|||
serializer = CycleUserPropertiesSerializer(cycle_properties)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
|
||||
def get(self, request, slug, project_id, cycle_id):
|
||||
cycle_properties, _ = CycleUserProperties.objects.get_or_create(
|
||||
user=request.user,
|
||||
|
|
|
|||
|
|
@ -3,12 +3,7 @@ import json
|
|||
|
||||
# Django imports
|
||||
from django.core import serializers
|
||||
from django.db.models import (
|
||||
F,
|
||||
Func,
|
||||
OuterRef,
|
||||
Q,
|
||||
)
|
||||
from django.db.models import F, Func, OuterRef, Q
|
||||
from django.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
|
|
@ -17,10 +12,6 @@ from django.views.decorators.gzip import gzip_page
|
|||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
from plane.app.permissions import (
|
||||
ProjectEntityPermission,
|
||||
)
|
||||
|
||||
# Module imports
|
||||
from .. import BaseViewSet
|
||||
from plane.app.serializers import (
|
||||
|
|
@ -45,6 +36,7 @@ from plane.utils.paginator import (
|
|||
GroupedOffsetPaginator,
|
||||
SubGroupedOffsetPaginator,
|
||||
)
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
|
||||
|
||||
class CycleIssueViewSet(BaseViewSet):
|
||||
|
|
@ -54,10 +46,6 @@ class CycleIssueViewSet(BaseViewSet):
|
|||
webhook_event = "cycle_issue"
|
||||
bulk = True
|
||||
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
filterset_fields = [
|
||||
"issue__labels__id",
|
||||
"issue__assignees__id",
|
||||
|
|
@ -92,6 +80,7 @@ class CycleIssueViewSet(BaseViewSet):
|
|||
)
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER])
|
||||
def list(self, request, slug, project_id, cycle_id):
|
||||
order_by_param = request.GET.get("order_by", "created_at")
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
|
|
@ -238,6 +227,7 @@ class CycleIssueViewSet(BaseViewSet):
|
|||
),
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def create(self, request, slug, project_id, cycle_id):
|
||||
issues = request.data.get("issues", [])
|
||||
|
||||
|
|
@ -333,6 +323,7 @@ class CycleIssueViewSet(BaseViewSet):
|
|||
)
|
||||
return Response({"message": "success"}, status=status.HTTP_201_CREATED)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def destroy(self, request, slug, project_id, cycle_id, issue_id):
|
||||
cycle_issue = CycleIssue.objects.filter(
|
||||
issue_id=issue_id,
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ from plane.db.models import (
|
|||
ProjectMember,
|
||||
User,
|
||||
Widget,
|
||||
WorkspaceMember,
|
||||
)
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
|
||||
|
|
@ -51,36 +52,61 @@ from .. import BaseAPIView
|
|||
|
||||
|
||||
def dashboard_overview_stats(self, request, slug):
|
||||
assigned_issues = Issue.issue_objects.filter(
|
||||
extra_filters = {}
|
||||
if WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role=5,
|
||||
is_active=True,
|
||||
).exists():
|
||||
extra_filters = {"created_by": request.user}
|
||||
|
||||
assigned_issues = (
|
||||
Issue.issue_objects.filter(
|
||||
project__project_projectmember__is_active=True,
|
||||
project__project_projectmember__member=request.user,
|
||||
workspace__slug=slug,
|
||||
assignees__in=[request.user],
|
||||
).count()
|
||||
)
|
||||
.filter(**extra_filters)
|
||||
.count()
|
||||
)
|
||||
|
||||
pending_issues_count = Issue.issue_objects.filter(
|
||||
pending_issues_count = (
|
||||
Issue.issue_objects.filter(
|
||||
~Q(state__group__in=["completed", "cancelled"]),
|
||||
target_date__lt=timezone.now().date(),
|
||||
project__project_projectmember__is_active=True,
|
||||
project__project_projectmember__member=request.user,
|
||||
workspace__slug=slug,
|
||||
assignees__in=[request.user],
|
||||
).count()
|
||||
)
|
||||
.filter(**extra_filters)
|
||||
.count()
|
||||
)
|
||||
|
||||
created_issues_count = Issue.issue_objects.filter(
|
||||
created_issues_count = (
|
||||
Issue.issue_objects.filter(
|
||||
workspace__slug=slug,
|
||||
project__project_projectmember__is_active=True,
|
||||
project__project_projectmember__member=request.user,
|
||||
created_by_id=request.user.id,
|
||||
).count()
|
||||
)
|
||||
.filter(**extra_filters)
|
||||
.count()
|
||||
)
|
||||
|
||||
completed_issues_count = Issue.issue_objects.filter(
|
||||
completed_issues_count = (
|
||||
Issue.issue_objects.filter(
|
||||
workspace__slug=slug,
|
||||
project__project_projectmember__is_active=True,
|
||||
project__project_projectmember__member=request.user,
|
||||
assignees__in=[request.user],
|
||||
state__group="completed",
|
||||
).count()
|
||||
)
|
||||
.filter(**extra_filters)
|
||||
.count()
|
||||
)
|
||||
|
||||
return Response(
|
||||
{
|
||||
|
|
@ -166,6 +192,14 @@ def dashboard_assigned_issues(self, request, slug):
|
|||
)
|
||||
)
|
||||
|
||||
if WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role=5,
|
||||
is_active=True,
|
||||
).exists():
|
||||
assigned_issues = assigned_issues.filter(created_by=request.user)
|
||||
|
||||
# Priority Ordering
|
||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||
assigned_issues = assigned_issues.annotate(
|
||||
|
|
@ -409,6 +443,16 @@ def dashboard_created_issues(self, request, slug):
|
|||
def dashboard_issues_by_state_groups(self, request, slug):
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
||||
extra_filters = {}
|
||||
|
||||
if WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role=5,
|
||||
is_active=True,
|
||||
).exists():
|
||||
extra_filters = {"created_by": request.user}
|
||||
|
||||
issues_by_state_groups = (
|
||||
Issue.issue_objects.filter(
|
||||
workspace__slug=slug,
|
||||
|
|
@ -416,7 +460,7 @@ def dashboard_issues_by_state_groups(self, request, slug):
|
|||
project__project_projectmember__member=request.user,
|
||||
assignees__in=[request.user],
|
||||
)
|
||||
.filter(**filters)
|
||||
.filter(**filters, **extra_filters)
|
||||
.values("state__group")
|
||||
.annotate(count=Count("id"))
|
||||
)
|
||||
|
|
@ -439,6 +483,15 @@ def dashboard_issues_by_state_groups(self, request, slug):
|
|||
def dashboard_issues_by_priority(self, request, slug):
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||
extra_filters = {}
|
||||
|
||||
if WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role=5,
|
||||
is_active=True,
|
||||
).exists():
|
||||
extra_filters = {"created_by": request.user}
|
||||
|
||||
issues_by_priority = (
|
||||
Issue.issue_objects.filter(
|
||||
|
|
@ -447,7 +500,7 @@ def dashboard_issues_by_priority(self, request, slug):
|
|||
project__project_projectmember__member=request.user,
|
||||
assignees__in=[request.user],
|
||||
)
|
||||
.filter(**filters)
|
||||
.filter(**filters, **extra_filters)
|
||||
.values("priority")
|
||||
.annotate(count=Count("id"))
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,11 @@ from rest_framework import status
|
|||
|
||||
# Module imports
|
||||
from ..base import BaseViewSet, BaseAPIView
|
||||
from plane.app.permissions import ProjectEntityPermission
|
||||
from plane.app.permissions import (
|
||||
ProjectEntityPermission,
|
||||
allow_permission,
|
||||
ROLE,
|
||||
)
|
||||
from plane.db.models import Project, Estimate, EstimatePoint, Issue
|
||||
from plane.app.serializers import (
|
||||
EstimateSerializer,
|
||||
|
|
@ -23,10 +27,8 @@ def generate_random_name(length=10):
|
|||
|
||||
|
||||
class ProjectEstimatePointEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER])
|
||||
def get(self, request, slug, project_id):
|
||||
project = Project.objects.get(workspace__slug=slug, pk=project_id)
|
||||
if project.estimate_id is not None:
|
||||
|
|
@ -189,10 +191,8 @@ class BulkEstimatePointEndpoint(BaseViewSet):
|
|||
|
||||
|
||||
class EstimatePointEndpoint(BaseViewSet):
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def create(self, request, slug, project_id, estimate_id):
|
||||
# TODO: add a key validation if the same key already exists
|
||||
if not request.data.get("key") or not request.data.get("value"):
|
||||
|
|
@ -211,6 +211,7 @@ class EstimatePointEndpoint(BaseViewSet):
|
|||
serializer = EstimatePointSerializer(estimate_point).data
|
||||
return Response(serializer, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def partial_update(
|
||||
self, request, slug, project_id, estimate_id, estimate_point_id
|
||||
):
|
||||
|
|
@ -231,6 +232,7 @@ class EstimatePointEndpoint(BaseViewSet):
|
|||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def destroy(
|
||||
self, request, slug, project_id, estimate_id, estimate_point_id
|
||||
):
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
from plane.app.permissions import WorkSpaceAdminPermission
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
from plane.app.serializers import ExporterHistorySerializer
|
||||
from plane.bgtasks.export_task import issue_export_task
|
||||
from plane.db.models import ExporterHistory, Project, Workspace
|
||||
|
|
@ -12,12 +12,10 @@ from .. import BaseAPIView
|
|||
|
||||
|
||||
class ExportIssuesEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
WorkSpaceAdminPermission,
|
||||
]
|
||||
model = ExporterHistory
|
||||
serializer_class = ExporterHistorySerializer
|
||||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
|
||||
def post(self, request, slug):
|
||||
# Get the workspace
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
|
@ -64,6 +62,7 @@ class ExportIssuesEndpoint(BaseAPIView):
|
|||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
|
||||
def get(self, request, slug):
|
||||
exporter_history = ExporterHistory.objects.filter(
|
||||
workspace__slug=slug,
|
||||
|
|
|
|||
12
apiserver/plane/app/views/external/base.py
vendored
12
apiserver/plane/app/views/external/base.py
vendored
|
|
@ -11,7 +11,7 @@ from rest_framework import status
|
|||
|
||||
# Module imports
|
||||
from ..base import BaseAPIView
|
||||
from plane.app.permissions import ProjectEntityPermission, WorkspaceEntityPermission
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
from plane.db.models import Workspace, Project
|
||||
from plane.app.serializers import (
|
||||
ProjectLiteSerializer,
|
||||
|
|
@ -21,10 +21,8 @@ from plane.license.utils.instance_value import get_configuration_value
|
|||
|
||||
|
||||
class GPTIntegrationEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def post(self, request, slug, project_id):
|
||||
OPENAI_API_KEY, GPT_ENGINE = get_configuration_value(
|
||||
[
|
||||
|
|
@ -84,10 +82,10 @@ class GPTIntegrationEndpoint(BaseAPIView):
|
|||
|
||||
|
||||
class WorkspaceGPTIntegrationEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
WorkspaceEntityPermission,
|
||||
]
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE"
|
||||
)
|
||||
def post(self, request, slug):
|
||||
OPENAI_API_KEY, GPT_ENGINE = get_configuration_value(
|
||||
[
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@ from rest_framework.response import Response
|
|||
|
||||
# Module imports
|
||||
from ..base import BaseViewSet
|
||||
from plane.app.permissions import ProjectBasePermission, ProjectLitePermission
|
||||
from plane.app.permissions import (
|
||||
allow_permission, ROLE
|
||||
)
|
||||
from plane.db.models import (
|
||||
Inbox,
|
||||
InboxIssue,
|
||||
|
|
@ -39,9 +41,6 @@ from plane.bgtasks.issue_activities_task import issue_activity
|
|||
|
||||
|
||||
class InboxViewSet(BaseViewSet):
|
||||
permission_classes = [
|
||||
ProjectBasePermission,
|
||||
]
|
||||
|
||||
serializer_class = InboxSerializer
|
||||
model = Inbox
|
||||
|
|
@ -63,6 +62,7 @@ class InboxViewSet(BaseViewSet):
|
|||
.select_related("workspace", "project")
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def list(self, request, slug, project_id):
|
||||
inbox = self.get_queryset().first()
|
||||
return Response(
|
||||
|
|
@ -70,9 +70,11 @@ class InboxViewSet(BaseViewSet):
|
|||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(project_id=self.kwargs.get("project_id"))
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def destroy(self, request, slug, project_id, pk):
|
||||
inbox = Inbox.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
|
|
@ -88,9 +90,6 @@ class InboxViewSet(BaseViewSet):
|
|||
|
||||
|
||||
class InboxIssueViewSet(BaseViewSet):
|
||||
permission_classes = [
|
||||
ProjectLitePermission,
|
||||
]
|
||||
|
||||
serializer_class = InboxIssueSerializer
|
||||
model = InboxIssue
|
||||
|
|
@ -168,6 +167,7 @@ class InboxIssueViewSet(BaseViewSet):
|
|||
)
|
||||
).distinct()
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
|
||||
def list(self, request, slug, project_id):
|
||||
inbox_id = Inbox.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
|
|
@ -201,6 +201,14 @@ class InboxIssueViewSet(BaseViewSet):
|
|||
if inbox_status:
|
||||
inbox_issue = inbox_issue.filter(status__in=inbox_status)
|
||||
|
||||
if ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
role=5,
|
||||
is_active=True,
|
||||
).exists():
|
||||
inbox_issue = inbox_issue.filter(created_by=request.user)
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(inbox_issue),
|
||||
|
|
@ -210,6 +218,7 @@ class InboxIssueViewSet(BaseViewSet):
|
|||
).data,
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def create(self, request, slug, project_id):
|
||||
if not request.data.get("issue", {}).get("name", False):
|
||||
return Response(
|
||||
|
|
@ -312,12 +321,13 @@ class InboxIssueViewSet(BaseViewSet):
|
|||
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
def partial_update(self, request, slug, project_id, issue_id):
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def partial_update(self, request, slug, project_id, pk):
|
||||
inbox_id = Inbox.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).first()
|
||||
inbox_issue = InboxIssue.objects.get(
|
||||
issue_id=issue_id,
|
||||
issue_id=pk,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
inbox_id=inbox_id,
|
||||
|
|
@ -458,7 +468,7 @@ class InboxIssueViewSet(BaseViewSet):
|
|||
request.data, cls=DjangoJSONEncoder
|
||||
),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue_id),
|
||||
issue_id=str(pk),
|
||||
project_id=str(project_id),
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
|
|
@ -493,7 +503,7 @@ class InboxIssueViewSet(BaseViewSet):
|
|||
)
|
||||
.get(
|
||||
inbox_id=inbox_id.id,
|
||||
issue_id=issue_id,
|
||||
issue_id=pk,
|
||||
project_id=project_id,
|
||||
)
|
||||
)
|
||||
|
|
@ -506,7 +516,12 @@ class InboxIssueViewSet(BaseViewSet):
|
|||
serializer = InboxIssueDetailSerializer(inbox_issue).data
|
||||
return Response(serializer, status=status.HTTP_200_OK)
|
||||
|
||||
def retrieve(self, request, slug, project_id, issue_id):
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER],
|
||||
creator=True,
|
||||
model=Issue,
|
||||
)
|
||||
def retrieve(self, request, slug, project_id, pk):
|
||||
inbox_id = Inbox.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).first()
|
||||
|
|
@ -534,9 +549,7 @@ class InboxIssueViewSet(BaseViewSet):
|
|||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
)
|
||||
.get(
|
||||
inbox_id=inbox_id.id, issue_id=issue_id, project_id=project_id
|
||||
)
|
||||
.get(inbox_id=inbox_id.id, issue_id=pk, project_id=project_id)
|
||||
)
|
||||
issue = InboxIssueDetailSerializer(inbox_issue).data
|
||||
return Response(
|
||||
|
|
@ -544,12 +557,13 @@ class InboxIssueViewSet(BaseViewSet):
|
|||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
def destroy(self, request, slug, project_id, issue_id):
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=Issue)
|
||||
def destroy(self, request, slug, project_id, pk):
|
||||
inbox_id = Inbox.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).first()
|
||||
inbox_issue = InboxIssue.objects.get(
|
||||
issue_id=issue_id,
|
||||
issue_id=pk,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
inbox_id=inbox_id,
|
||||
|
|
@ -559,21 +573,8 @@ class InboxIssueViewSet(BaseViewSet):
|
|||
if inbox_issue.status in [-2, -1, 0, 2]:
|
||||
# Delete the issue also
|
||||
issue = Issue.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, pk=issue_id
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
).first()
|
||||
if issue.created_by_id != request.user.id and (
|
||||
not ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role=20,
|
||||
project_id=project_id,
|
||||
is_active=True,
|
||||
).exists()
|
||||
):
|
||||
return Response(
|
||||
{"error": "Only admin or creator can delete the issue"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
issue.delete()
|
||||
|
||||
inbox_issue.delete()
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ from plane.app.serializers import (
|
|||
IssueActivitySerializer,
|
||||
IssueCommentSerializer,
|
||||
)
|
||||
from plane.app.permissions import ProjectEntityPermission
|
||||
from plane.app.permissions import ProjectEntityPermission, allow_permission, ROLE
|
||||
from plane.db.models import (
|
||||
IssueActivity,
|
||||
IssueComment,
|
||||
|
|
@ -33,6 +33,7 @@ class IssueActivityEndpoint(BaseAPIView):
|
|||
]
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST, ROLE.VIEWER])
|
||||
def get(self, request, slug, project_id, issue_id):
|
||||
filters = {}
|
||||
if request.GET.get("created_at__gt", None) is not None:
|
||||
|
|
|
|||
|
|
@ -46,15 +46,13 @@ from plane.utils.paginator import (
|
|||
GroupedOffsetPaginator,
|
||||
SubGroupedOffsetPaginator,
|
||||
)
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
|
||||
# Module imports
|
||||
from .. import BaseViewSet, BaseAPIView
|
||||
|
||||
|
||||
class IssueArchiveViewSet(BaseViewSet):
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
serializer_class = IssueFlatSerializer
|
||||
model = Issue
|
||||
|
||||
|
|
@ -98,6 +96,7 @@ class IssueArchiveViewSet(BaseViewSet):
|
|||
)
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER])
|
||||
def list(self, request, slug, project_id):
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
show_sub_issues = request.GET.get("show_sub_issues", "true")
|
||||
|
|
@ -213,6 +212,7 @@ class IssueArchiveViewSet(BaseViewSet):
|
|||
),
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER])
|
||||
def retrieve(self, request, slug, project_id, pk=None):
|
||||
issue = (
|
||||
self.get_queryset()
|
||||
|
|
@ -256,6 +256,7 @@ class IssueArchiveViewSet(BaseViewSet):
|
|||
serializer = IssueDetailSerializer(issue, expand=self.expand)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def archive(self, request, slug, project_id, pk=None):
|
||||
issue = Issue.issue_objects.get(
|
||||
workspace__slug=slug,
|
||||
|
|
@ -294,6 +295,7 @@ class IssueArchiveViewSet(BaseViewSet):
|
|||
{"archived_at": str(issue.archived_at)}, status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def unarchive(self, request, slug, project_id, pk=None):
|
||||
issue = Issue.objects.get(
|
||||
workspace__slug=slug,
|
||||
|
|
@ -325,6 +327,7 @@ class BulkArchiveIssuesEndpoint(BaseAPIView):
|
|||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def post(self, request, slug, project_id):
|
||||
issue_ids = request.data.get("issue_ids", [])
|
||||
|
||||
|
|
|
|||
|
|
@ -13,19 +13,16 @@ from rest_framework.parsers import MultiPartParser, FormParser
|
|||
# Module imports
|
||||
from .. import BaseAPIView
|
||||
from plane.app.serializers import IssueAttachmentSerializer
|
||||
from plane.app.permissions import ProjectEntityPermission
|
||||
from plane.db.models import IssueAttachment, ProjectMember
|
||||
from plane.db.models import IssueAttachment
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
|
||||
|
||||
class IssueAttachmentEndpoint(BaseAPIView):
|
||||
serializer_class = IssueAttachmentSerializer
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
model = IssueAttachment
|
||||
parser_classes = (MultiPartParser, FormParser)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def post(self, request, slug, project_id, issue_id):
|
||||
serializer = IssueAttachmentSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
|
|
@ -47,21 +44,9 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
|||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@allow_permission([ROLE.ADMIN], creator=True, model=IssueAttachment)
|
||||
def delete(self, request, slug, project_id, issue_id, pk):
|
||||
issue_attachment = IssueAttachment.objects.get(pk=pk)
|
||||
if issue_attachment.created_by_id != request.user.id and (
|
||||
not ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role=20,
|
||||
project_id=project_id,
|
||||
is_active=True,
|
||||
).exists()
|
||||
):
|
||||
return Response(
|
||||
{"error": "Only admin or creator can delete the attachment"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
issue_attachment.asset.delete(save=False)
|
||||
issue_attachment.delete()
|
||||
issue_activity.delay(
|
||||
|
|
@ -78,6 +63,7 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
|||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST, ROLE.VIEWER])
|
||||
def get(self, request, slug, project_id, issue_id):
|
||||
issue_attachments = IssueAttachment.objects.filter(
|
||||
issue_id=issue_id, workspace__slug=slug, project_id=project_id
|
||||
|
|
|
|||
|
|
@ -25,10 +25,7 @@ from rest_framework import status
|
|||
from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
from plane.app.permissions import (
|
||||
ProjectEntityPermission,
|
||||
ProjectLitePermission,
|
||||
)
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
from plane.app.serializers import (
|
||||
IssueCreateSerializer,
|
||||
IssueDetailSerializer,
|
||||
|
|
@ -60,14 +57,10 @@ from plane.utils.paginator import (
|
|||
from .. import BaseAPIView, BaseViewSet
|
||||
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||
|
||||
# Module imports
|
||||
|
||||
|
||||
class IssueListEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
|
||||
def get(self, request, slug, project_id):
|
||||
issue_ids = request.GET.get("issues", False)
|
||||
|
||||
|
|
@ -184,9 +177,6 @@ class IssueViewSet(BaseViewSet):
|
|||
|
||||
model = Issue
|
||||
webhook_event = "issue"
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
search_fields = [
|
||||
"name",
|
||||
|
|
@ -232,6 +222,7 @@ class IssueViewSet(BaseViewSet):
|
|||
).distinct()
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
|
||||
def list(self, request, slug, project_id):
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
order_by_param = request.GET.get("order_by", "-created_at")
|
||||
|
|
@ -256,6 +247,15 @@ class IssueViewSet(BaseViewSet):
|
|||
sub_group_by=sub_group_by,
|
||||
)
|
||||
|
||||
if ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
role=5,
|
||||
is_active=True,
|
||||
).exists():
|
||||
issue_queryset = issue_queryset.filter(created_by=request.user)
|
||||
|
||||
if group_by:
|
||||
if sub_group_by:
|
||||
if group_by == sub_group_by:
|
||||
|
|
@ -337,6 +337,7 @@ class IssueViewSet(BaseViewSet):
|
|||
),
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def create(self, request, slug, project_id):
|
||||
project = Project.objects.get(pk=project_id)
|
||||
|
||||
|
|
@ -411,6 +412,9 @@ class IssueViewSet(BaseViewSet):
|
|||
return Response(issue, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@allow_permission(
|
||||
[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER], creator=True, model=Issue
|
||||
)
|
||||
def retrieve(self, request, slug, project_id, pk=None):
|
||||
issue = (
|
||||
self.get_queryset()
|
||||
|
|
@ -483,6 +487,7 @@ class IssueViewSet(BaseViewSet):
|
|||
serializer = IssueDetailSerializer(issue, expand=self.expand)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def partial_update(self, request, slug, project_id, pk=None):
|
||||
issue = (
|
||||
self.get_queryset()
|
||||
|
|
@ -548,23 +553,11 @@ class IssueViewSet(BaseViewSet):
|
|||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@allow_permission([ROLE.ADMIN], creator=True, model=Issue)
|
||||
def destroy(self, request, slug, project_id, pk=None):
|
||||
issue = Issue.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
if issue.created_by_id != request.user.id and (
|
||||
not ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role=20,
|
||||
project_id=project_id,
|
||||
is_active=True,
|
||||
).exists()
|
||||
):
|
||||
return Response(
|
||||
{"error": "Only admin or creator can delete the issue"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
issue.delete()
|
||||
issue_activity.delay(
|
||||
|
|
@ -582,10 +575,8 @@ class IssueViewSet(BaseViewSet):
|
|||
|
||||
|
||||
class IssueUserDisplayPropertyEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
ProjectLitePermission,
|
||||
]
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST, ROLE.VIEWER])
|
||||
def patch(self, request, slug, project_id):
|
||||
issue_property = IssueUserProperty.objects.get(
|
||||
user=request.user,
|
||||
|
|
@ -605,6 +596,7 @@ class IssueUserDisplayPropertyEndpoint(BaseAPIView):
|
|||
serializer = IssueUserPropertySerializer(issue_property)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST, ROLE.VIEWER])
|
||||
def get(self, request, slug, project_id):
|
||||
issue_property, _ = IssueUserProperty.objects.get_or_create(
|
||||
user=request.user, project_id=project_id
|
||||
|
|
@ -614,22 +606,9 @@ class IssueUserDisplayPropertyEndpoint(BaseAPIView):
|
|||
|
||||
|
||||
class BulkDeleteIssuesEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
@allow_permission([ROLE.ADMIN])
|
||||
def delete(self, request, slug, project_id):
|
||||
if ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role__in=[15, 10, 5],
|
||||
project_id=project_id,
|
||||
is_active=True,
|
||||
).exists():
|
||||
return Response(
|
||||
{"error": "Only admin can perform this action"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
issue_ids = request.data.get("issue_ids", [])
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ from plane.app.serializers import (
|
|||
IssueCommentSerializer,
|
||||
CommentReactionSerializer,
|
||||
)
|
||||
from plane.app.permissions import ProjectLitePermission
|
||||
from plane.app.permissions import ProjectLitePermission, allow_permission, ROLE
|
||||
from plane.db.models import (
|
||||
IssueComment,
|
||||
ProjectMember,
|
||||
|
|
@ -29,9 +29,6 @@ class IssueCommentViewSet(BaseViewSet):
|
|||
serializer_class = IssueCommentSerializer
|
||||
model = IssueComment
|
||||
webhook_event = "issue_comment"
|
||||
permission_classes = [
|
||||
ProjectLitePermission,
|
||||
]
|
||||
|
||||
filterset_fields = [
|
||||
"issue__id",
|
||||
|
|
@ -66,6 +63,7 @@ class IssueCommentViewSet(BaseViewSet):
|
|||
.distinct()
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST, ROLE.VIEWER])
|
||||
def create(self, request, slug, project_id, issue_id):
|
||||
serializer = IssueCommentSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
|
|
@ -90,6 +88,11 @@ class IssueCommentViewSet(BaseViewSet):
|
|||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER],
|
||||
creator=True,
|
||||
model=IssueComment,
|
||||
)
|
||||
def partial_update(self, request, slug, project_id, issue_id, pk):
|
||||
issue_comment = IssueComment.objects.get(
|
||||
workspace__slug=slug,
|
||||
|
|
@ -121,6 +124,9 @@ class IssueCommentViewSet(BaseViewSet):
|
|||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN], creator=True, model=IssueComment
|
||||
)
|
||||
def destroy(self, request, slug, project_id, issue_id, pk):
|
||||
issue_comment = IssueComment.objects.get(
|
||||
workspace__slug=slug,
|
||||
|
|
|
|||
|
|
@ -11,9 +11,7 @@ from rest_framework import status
|
|||
# Module imports
|
||||
from .. import BaseViewSet, BaseAPIView
|
||||
from plane.app.serializers import LabelSerializer
|
||||
from plane.app.permissions import (
|
||||
ProjectMemberPermission,
|
||||
)
|
||||
from plane.app.permissions import allow_permission, ProjectBasePermission, ROLE
|
||||
from plane.db.models import (
|
||||
Project,
|
||||
Label,
|
||||
|
|
@ -25,7 +23,7 @@ class LabelViewSet(BaseViewSet):
|
|||
serializer_class = LabelSerializer
|
||||
model = Label
|
||||
permission_classes = [
|
||||
ProjectMemberPermission,
|
||||
ProjectBasePermission,
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
|
|
@ -45,6 +43,7 @@ class LabelViewSet(BaseViewSet):
|
|||
@invalidate_cache(
|
||||
path="/api/workspaces/:slug/labels/", url_params=True, user=False
|
||||
)
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def create(self, request, slug, project_id):
|
||||
try:
|
||||
serializer = LabelSerializer(data=request.data)
|
||||
|
|
@ -67,17 +66,20 @@ class LabelViewSet(BaseViewSet):
|
|||
@invalidate_cache(
|
||||
path="/api/workspaces/:slug/labels/", url_params=True, user=False
|
||||
)
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
return super().partial_update(request, *args, **kwargs)
|
||||
|
||||
@invalidate_cache(
|
||||
path="/api/workspaces/:slug/labels/", url_params=True, user=False
|
||||
)
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
return super().destroy(request, *args, **kwargs)
|
||||
|
||||
|
||||
class BulkCreateIssueLabelsEndpoint(BaseAPIView):
|
||||
@allow_permission([ROLE.ADMIN])
|
||||
def post(self, request, slug, project_id):
|
||||
label_data = request.data.get("label_data", [])
|
||||
project = Project.objects.get(pk=project_id)
|
||||
|
|
|
|||
|
|
@ -30,8 +30,10 @@ from rest_framework.response import Response
|
|||
# Module imports
|
||||
from plane.app.permissions import (
|
||||
ProjectEntityPermission,
|
||||
ProjectLitePermission,
|
||||
allow_permission,
|
||||
ROLE,
|
||||
)
|
||||
|
||||
from plane.app.serializers import (
|
||||
ModuleDetailSerializer,
|
||||
ModuleLinkSerializer,
|
||||
|
|
@ -48,7 +50,6 @@ from plane.db.models import (
|
|||
ModuleLink,
|
||||
ModuleUserProperties,
|
||||
Project,
|
||||
ProjectMember,
|
||||
)
|
||||
from plane.utils.analytics_plot import burndown_plot
|
||||
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||
|
|
@ -58,9 +59,6 @@ from .. import BaseAPIView, BaseViewSet
|
|||
|
||||
class ModuleViewSet(BaseViewSet):
|
||||
model = Module
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
webhook_event = "module"
|
||||
|
||||
def get_serializer_class(self):
|
||||
|
|
@ -318,6 +316,8 @@ class ModuleViewSet(BaseViewSet):
|
|||
.order_by("-is_favorite", "-created_at")
|
||||
)
|
||||
|
||||
allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER])
|
||||
|
||||
def create(self, request, slug, project_id):
|
||||
project = Project.objects.get(workspace__slug=slug, pk=project_id)
|
||||
serializer = ModuleWriteSerializer(
|
||||
|
|
@ -380,6 +380,8 @@ class ModuleViewSet(BaseViewSet):
|
|||
return Response(module, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
|
||||
|
||||
def list(self, request, slug, project_id):
|
||||
queryset = self.get_queryset().filter(archived_at__isnull=True)
|
||||
if self.fields:
|
||||
|
|
@ -427,6 +429,8 @@ class ModuleViewSet(BaseViewSet):
|
|||
)
|
||||
return Response(modules, status=status.HTTP_200_OK)
|
||||
|
||||
allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER])
|
||||
|
||||
def retrieve(self, request, slug, project_id, pk):
|
||||
queryset = (
|
||||
self.get_queryset()
|
||||
|
|
@ -671,6 +675,7 @@ class ModuleViewSet(BaseViewSet):
|
|||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def partial_update(self, request, slug, project_id, pk):
|
||||
module = self.get_queryset().filter(pk=pk)
|
||||
|
||||
|
|
@ -740,25 +745,12 @@ class ModuleViewSet(BaseViewSet):
|
|||
return Response(module, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@allow_permission([ROLE.ADMIN], creator=True, model=Module)
|
||||
def destroy(self, request, slug, project_id, pk):
|
||||
module = Module.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
|
||||
if module.created_by_id != request.user.id and (
|
||||
not ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role=20,
|
||||
project_id=project_id,
|
||||
is_active=True,
|
||||
).exists()
|
||||
):
|
||||
return Response(
|
||||
{"error": "Only admin or creator can delete the module"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
module_issues = list(
|
||||
ModuleIssue.objects.filter(module_id=pk).values_list(
|
||||
"issue", flat=True
|
||||
|
|
@ -859,10 +851,8 @@ class ModuleFavoriteViewSet(BaseViewSet):
|
|||
|
||||
|
||||
class ModuleUserPropertiesEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
ProjectLitePermission,
|
||||
]
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
|
||||
def patch(self, request, slug, project_id, module_id):
|
||||
module_properties = ModuleUserProperties.objects.get(
|
||||
user=request.user,
|
||||
|
|
@ -885,6 +875,7 @@ class ModuleUserPropertiesEndpoint(BaseAPIView):
|
|||
serializer = ModuleUserPropertiesSerializer(module_properties)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
|
||||
def get(self, request, slug, project_id, module_id):
|
||||
module_properties, _ = ModuleUserProperties.objects.get_or_create(
|
||||
user=request.user,
|
||||
|
|
|
|||
|
|
@ -17,9 +17,7 @@ from django.views.decorators.gzip import gzip_page
|
|||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
from plane.app.permissions import (
|
||||
ProjectEntityPermission,
|
||||
)
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
from plane.app.serializers import (
|
||||
ModuleIssueSerializer,
|
||||
)
|
||||
|
|
@ -58,10 +56,6 @@ class ModuleIssueViewSet(BaseViewSet):
|
|||
"issue__assignees__id",
|
||||
]
|
||||
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
Issue.issue_objects.filter(
|
||||
|
|
@ -97,6 +91,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
|||
).distinct()
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER])
|
||||
def list(self, request, slug, project_id, module_id):
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
issue_queryset = self.get_queryset().filter(**filters)
|
||||
|
|
@ -204,6 +199,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
|||
),
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
# create multiple issues inside a module
|
||||
def create_module_issues(self, request, slug, project_id, module_id):
|
||||
issues = request.data.get("issues", [])
|
||||
|
|
@ -245,6 +241,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
|||
]
|
||||
return Response({"message": "success"}, status=status.HTTP_201_CREATED)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
# add multiple module inside an issue and remove multiple modules from an issue
|
||||
def create_issue_modules(self, request, slug, project_id, issue_id):
|
||||
modules = request.data.get("modules", [])
|
||||
|
|
@ -307,6 +304,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
|||
|
||||
return Response({"message": "success"}, status=status.HTTP_201_CREATED)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def destroy(self, request, slug, project_id, module_id, issue_id):
|
||||
module_issue = ModuleIssue.objects.filter(
|
||||
workspace__slug=slug,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from plane.db.models import (
|
|||
WorkspaceMember,
|
||||
)
|
||||
from plane.utils.paginator import BasePaginator
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
|
||||
# Module imports
|
||||
from ..base import BaseAPIView, BaseViewSet
|
||||
|
|
@ -39,6 +40,10 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
|||
.select_related("workspace", "project," "triggered_by", "receiver")
|
||||
)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST],
|
||||
level="WORKSPACE",
|
||||
)
|
||||
def list(self, request, slug):
|
||||
# Get query parameters
|
||||
snoozed = request.GET.get("snoozed", "false")
|
||||
|
|
@ -168,6 +173,10 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
|||
serializer = NotificationSerializer(notifications, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST],
|
||||
level="WORKSPACE",
|
||||
)
|
||||
def partial_update(self, request, slug, pk):
|
||||
notification = Notification.objects.get(
|
||||
workspace__slug=slug, pk=pk, receiver=request.user
|
||||
|
|
@ -185,6 +194,9 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
|||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE"
|
||||
)
|
||||
def mark_read(self, request, slug, pk):
|
||||
notification = Notification.objects.get(
|
||||
receiver=request.user, workspace__slug=slug, pk=pk
|
||||
|
|
@ -194,6 +206,9 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
|||
serializer = NotificationSerializer(notification)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE"
|
||||
)
|
||||
def mark_unread(self, request, slug, pk):
|
||||
notification = Notification.objects.get(
|
||||
receiver=request.user, workspace__slug=slug, pk=pk
|
||||
|
|
@ -203,6 +218,9 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
|||
serializer = NotificationSerializer(notification)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE"
|
||||
)
|
||||
def archive(self, request, slug, pk):
|
||||
notification = Notification.objects.get(
|
||||
receiver=request.user, workspace__slug=slug, pk=pk
|
||||
|
|
@ -212,6 +230,9 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
|||
serializer = NotificationSerializer(notification)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE"
|
||||
)
|
||||
def unarchive(self, request, slug, pk):
|
||||
notification = Notification.objects.get(
|
||||
receiver=request.user, workspace__slug=slug, pk=pk
|
||||
|
|
@ -223,6 +244,11 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
|||
|
||||
|
||||
class UnreadNotificationEndpoint(BaseAPIView):
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST],
|
||||
level="WORKSPACE",
|
||||
)
|
||||
def get(self, request, slug):
|
||||
# Watching Issues Count
|
||||
unread_notifications_count = (
|
||||
|
|
@ -260,6 +286,10 @@ class UnreadNotificationEndpoint(BaseAPIView):
|
|||
|
||||
|
||||
class MarkAllReadNotificationViewSet(BaseViewSet):
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE"
|
||||
)
|
||||
def create(self, request, slug):
|
||||
snoozed = request.data.get("snoozed", False)
|
||||
archived = request.data.get("archived", False)
|
||||
|
|
@ -343,6 +373,9 @@ class UserNotificationPreferenceEndpoint(BaseAPIView):
|
|||
serializer_class = UserNotificationPreferenceSerializer
|
||||
|
||||
# request the object
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||
)
|
||||
def get(self, request):
|
||||
user_notification_preference = UserNotificationPreference.objects.get(
|
||||
user=request.user
|
||||
|
|
@ -353,6 +386,9 @@ class UserNotificationPreferenceEndpoint(BaseAPIView):
|
|||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
# update the object
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||
)
|
||||
def patch(self, request):
|
||||
user_notification_preference = UserNotificationPreference.objects.get(
|
||||
user=request.user
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ from rest_framework import status
|
|||
from rest_framework.response import Response
|
||||
|
||||
|
||||
from plane.app.permissions import ProjectEntityPermission
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
from plane.app.serializers import (
|
||||
PageLogSerializer,
|
||||
PageSerializer,
|
||||
|
|
@ -60,9 +60,6 @@ def unarchive_archive_page_and_descendants(page_id, archived_at):
|
|||
class PageViewSet(BaseViewSet):
|
||||
serializer_class = PageSerializer
|
||||
model = Page
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
search_fields = [
|
||||
"name",
|
||||
]
|
||||
|
|
@ -122,6 +119,7 @@ class PageViewSet(BaseViewSet):
|
|||
.distinct()
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def create(self, request, slug, project_id):
|
||||
serializer = PageSerializer(
|
||||
data=request.data,
|
||||
|
|
@ -143,6 +141,7 @@ class PageViewSet(BaseViewSet):
|
|||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def partial_update(self, request, slug, project_id, pk):
|
||||
try:
|
||||
page = Page.objects.get(
|
||||
|
|
@ -208,6 +207,7 @@ class PageViewSet(BaseViewSet):
|
|||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER])
|
||||
def retrieve(self, request, slug, project_id, pk=None):
|
||||
page = self.get_queryset().filter(pk=pk).first()
|
||||
if page is None:
|
||||
|
|
@ -226,6 +226,7 @@ class PageViewSet(BaseViewSet):
|
|||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def lock(self, request, slug, project_id, pk):
|
||||
page = Page.objects.filter(
|
||||
pk=pk, workspace__slug=slug, projects__id=project_id
|
||||
|
|
@ -235,6 +236,7 @@ class PageViewSet(BaseViewSet):
|
|||
page.save()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def unlock(self, request, slug, project_id, pk):
|
||||
page = Page.objects.filter(
|
||||
pk=pk, workspace__slug=slug, projects__id=project_id
|
||||
|
|
@ -245,6 +247,7 @@ class PageViewSet(BaseViewSet):
|
|||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def access(self, request, slug, project_id, pk):
|
||||
access = request.data.get("access", 0)
|
||||
page = Page.objects.filter(
|
||||
|
|
@ -267,11 +270,13 @@ class PageViewSet(BaseViewSet):
|
|||
page.save()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER])
|
||||
def list(self, request, slug, project_id):
|
||||
queryset = self.get_queryset()
|
||||
pages = PageSerializer(queryset, many=True).data
|
||||
return Response(pages, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def archive(self, request, slug, project_id, pk):
|
||||
page = Page.objects.get(
|
||||
pk=pk, workspace__slug=slug, projects__id=project_id
|
||||
|
|
@ -299,6 +304,7 @@ class PageViewSet(BaseViewSet):
|
|||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def unarchive(self, request, slug, project_id, pk):
|
||||
page = Page.objects.get(
|
||||
pk=pk, workspace__slug=slug, projects__id=project_id
|
||||
|
|
@ -328,6 +334,7 @@ class PageViewSet(BaseViewSet):
|
|||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@allow_permission([ROLE.ADMIN], creator=True, model=Page)
|
||||
def destroy(self, request, slug, project_id, pk):
|
||||
page = Page.objects.get(
|
||||
pk=pk, workspace__slug=slug, projects__id=project_id
|
||||
|
|
@ -370,12 +377,10 @@ class PageViewSet(BaseViewSet):
|
|||
|
||||
|
||||
class PageFavoriteViewSet(BaseViewSet):
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
model = UserFavorite
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def create(self, request, slug, project_id, pk):
|
||||
_ = UserFavorite.objects.create(
|
||||
project_id=project_id,
|
||||
|
|
@ -385,6 +390,7 @@ class PageFavoriteViewSet(BaseViewSet):
|
|||
)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def destroy(self, request, slug, project_id, pk):
|
||||
page_favorite = UserFavorite.objects.get(
|
||||
project=project_id,
|
||||
|
|
@ -398,9 +404,6 @@ class PageFavoriteViewSet(BaseViewSet):
|
|||
|
||||
|
||||
class PageLogEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
serializer_class = PageLogSerializer
|
||||
model = PageLog
|
||||
|
|
@ -440,9 +443,6 @@ class PageLogEndpoint(BaseAPIView):
|
|||
|
||||
|
||||
class SubPagesEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
def get(self, request, slug, project_id, page_id):
|
||||
|
|
@ -461,10 +461,8 @@ class SubPagesEndpoint(BaseAPIView):
|
|||
|
||||
|
||||
class PagesDescriptionViewSet(BaseViewSet):
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER])
|
||||
def retrieve(self, request, slug, project_id, pk):
|
||||
page = (
|
||||
Page.objects.filter(
|
||||
|
|
@ -489,6 +487,7 @@ class PagesDescriptionViewSet(BaseViewSet):
|
|||
)
|
||||
return response
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
|
||||
def partial_update(self, request, slug, project_id, pk):
|
||||
page = (
|
||||
Page.objects.filter(
|
||||
|
|
|
|||
|
|
@ -31,8 +31,9 @@ from plane.app.serializers import (
|
|||
)
|
||||
|
||||
from plane.app.permissions import (
|
||||
ProjectBasePermission,
|
||||
ProjectMemberPermission,
|
||||
allow_permission,
|
||||
ROLE,
|
||||
)
|
||||
from plane.db.models import (
|
||||
UserFavorite,
|
||||
|
|
@ -47,6 +48,7 @@ from plane.db.models import (
|
|||
ProjectMember,
|
||||
State,
|
||||
Workspace,
|
||||
WorkspaceMember,
|
||||
)
|
||||
from plane.utils.cache import cache_response
|
||||
from plane.bgtasks.webhook_task import model_activity
|
||||
|
|
@ -57,10 +59,6 @@ class ProjectViewSet(BaseViewSet):
|
|||
model = Project
|
||||
webhook_event = "project"
|
||||
|
||||
permission_classes = [
|
||||
ProjectBasePermission,
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
sort_order = ProjectMember.objects.filter(
|
||||
member=self.request.user,
|
||||
|
|
@ -155,6 +153,10 @@ class ProjectViewSet(BaseViewSet):
|
|||
.distinct()
|
||||
)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST],
|
||||
level="WORKSPACE",
|
||||
)
|
||||
def list(self, request, slug):
|
||||
fields = [
|
||||
field
|
||||
|
|
@ -173,11 +175,27 @@ class ProjectViewSet(BaseViewSet):
|
|||
projects, many=True
|
||||
).data,
|
||||
)
|
||||
|
||||
if WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=slug,
|
||||
is_active=True,
|
||||
role__in=[5, 10],
|
||||
).exists():
|
||||
projects = projects.filter(
|
||||
project_projectmember__member=self.request.user,
|
||||
project_projectmember__is_active=True,
|
||||
)
|
||||
|
||||
projects = ProjectListSerializer(
|
||||
projects, many=True, fields=fields if fields else None
|
||||
).data
|
||||
return Response(projects, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST],
|
||||
level="WORKSPACE",
|
||||
)
|
||||
def retrieve(self, request, slug, pk):
|
||||
project = (
|
||||
self.get_queryset()
|
||||
|
|
@ -249,6 +267,7 @@ class ProjectViewSet(BaseViewSet):
|
|||
serializer = ProjectListSerializer(project)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
|
||||
def create(self, request, slug):
|
||||
try:
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
|
@ -378,6 +397,7 @@ class ProjectViewSet(BaseViewSet):
|
|||
status=status.HTTP_410_GONE,
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
|
||||
def partial_update(self, request, slug, pk=None):
|
||||
try:
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
|
@ -459,10 +479,7 @@ class ProjectViewSet(BaseViewSet):
|
|||
|
||||
class ProjectArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
|
||||
permission_classes = [
|
||||
ProjectBasePermission,
|
||||
]
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def post(self, request, slug, project_id):
|
||||
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
||||
project.archived_at = timezone.now()
|
||||
|
|
@ -472,6 +489,7 @@ class ProjectArchiveUnarchiveEndpoint(BaseAPIView):
|
|||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def delete(self, request, slug, project_id):
|
||||
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
||||
project.archived_at = None
|
||||
|
|
@ -480,10 +498,7 @@ class ProjectArchiveUnarchiveEndpoint(BaseAPIView):
|
|||
|
||||
|
||||
class ProjectIdentifierEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
ProjectBasePermission,
|
||||
]
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
|
||||
def get(self, request, slug):
|
||||
name = request.GET.get("name", "").strip().upper()
|
||||
|
||||
|
|
@ -502,6 +517,7 @@ class ProjectIdentifierEndpoint(BaseAPIView):
|
|||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
|
||||
def delete(self, request, slug):
|
||||
name = request.data.get("name", "").strip().upper()
|
||||
|
||||
|
|
|
|||
|
|
@ -23,17 +23,16 @@ from plane.db.models import (
|
|||
Workspace,
|
||||
TeamMember,
|
||||
IssueUserProperty,
|
||||
WorkspaceMember,
|
||||
)
|
||||
from plane.bgtasks.project_add_user_email_task import project_add_user_email
|
||||
from plane.utils.host import base_host
|
||||
from plane.app.permissions.base import allow_permission, ROLE
|
||||
|
||||
|
||||
class ProjectMemberViewSet(BaseViewSet):
|
||||
serializer_class = ProjectMemberAdminSerializer
|
||||
model = ProjectMember
|
||||
permission_classes = [
|
||||
ProjectMemberPermission,
|
||||
]
|
||||
|
||||
def get_permissions(self):
|
||||
if self.action == "leave":
|
||||
|
|
@ -65,6 +64,7 @@ class ProjectMemberViewSet(BaseViewSet):
|
|||
.select_related("workspace", "workspace__owner")
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN])
|
||||
def create(self, request, slug, project_id):
|
||||
# Get the list of members to be added to the project and their roles i.e. the user_id and the role
|
||||
members = request.data.get("members", [])
|
||||
|
|
@ -88,6 +88,23 @@ class ProjectMemberViewSet(BaseViewSet):
|
|||
member.get("member_id"): member.get("role") for member in members
|
||||
}
|
||||
|
||||
# check the workspace role of the new user
|
||||
for member in member_roles:
|
||||
workspace_member_role = WorkspaceMember.objects.get(
|
||||
workspace__slug=slug,
|
||||
member=member,
|
||||
is_active=True,
|
||||
).role
|
||||
if workspace_member_role in [5, 10] and member_roles.get(
|
||||
member
|
||||
) in [15, 20]:
|
||||
return Response(
|
||||
{
|
||||
"error": "You cannot add a user with role higher than the workspace role"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Update roles in the members array based on the member_roles dictionary and set is_active to True
|
||||
for project_member in ProjectMember.objects.filter(
|
||||
project_id=project_id,
|
||||
|
|
@ -172,6 +189,7 @@ class ProjectMemberViewSet(BaseViewSet):
|
|||
# Return the serialized data
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
|
||||
def list(self, request, slug, project_id):
|
||||
# Get the list of project members for the project
|
||||
project_members = ProjectMember.objects.filter(
|
||||
|
|
@ -186,6 +204,7 @@ class ProjectMemberViewSet(BaseViewSet):
|
|||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission([ROLE.ADMIN])
|
||||
def partial_update(self, request, slug, project_id, pk):
|
||||
project_member = ProjectMember.objects.get(
|
||||
pk=pk,
|
||||
|
|
@ -205,6 +224,22 @@ class ProjectMemberViewSet(BaseViewSet):
|
|||
member=request.user,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
workspace_role = WorkspaceMember.objects.get(
|
||||
workspace__slug=slug,
|
||||
member=project_member.member,
|
||||
is_active=True,
|
||||
).role
|
||||
if workspace_role in [5, 10] and int(
|
||||
request.data.get("role", project_member.role)
|
||||
) in [15, 20]:
|
||||
return Response(
|
||||
{
|
||||
"error": "You cannot add a user with role higher than the workspace role"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if (
|
||||
"role" in request.data
|
||||
and int(request.data.get("role", project_member.role))
|
||||
|
|
@ -226,6 +261,7 @@ class ProjectMemberViewSet(BaseViewSet):
|
|||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def destroy(self, request, slug, project_id, pk):
|
||||
project_member = ProjectMember.objects.get(
|
||||
workspace__slug=slug,
|
||||
|
|
@ -262,6 +298,7 @@ class ProjectMemberViewSet(BaseViewSet):
|
|||
project_member.save()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
|
||||
def leave(self, request, slug, project_id):
|
||||
project_member = ProjectMember.objects.get(
|
||||
workspace__slug=slug,
|
||||
|
|
|
|||
|
|
@ -9,9 +9,7 @@ from rest_framework.response import Response
|
|||
|
||||
# Module imports
|
||||
from .base import BaseAPIView
|
||||
from plane.db.models import (
|
||||
Issue,
|
||||
)
|
||||
from plane.db.models import Issue, ProjectMember
|
||||
from plane.utils.issue_search import search_issues
|
||||
|
||||
|
||||
|
|
@ -76,6 +74,16 @@ class IssueSearchEndpoint(BaseAPIView):
|
|||
if target_date == "none":
|
||||
issues = issues.filter(target_date__isnull=True)
|
||||
|
||||
if ProjectMember.objects.filter(
|
||||
project_id=project_id,
|
||||
member=self.request.user,
|
||||
is_active=True,
|
||||
role=5
|
||||
).exists():
|
||||
issues = issues.filter(
|
||||
created_by=self.request.user
|
||||
)
|
||||
|
||||
return Response(
|
||||
issues.values(
|
||||
"name",
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ from django.db import transaction
|
|||
from rest_framework.response import Response
|
||||
|
||||
from plane.app.permissions import (
|
||||
ProjectEntityPermission,
|
||||
WorkspaceEntityPermission,
|
||||
allow_permission,
|
||||
ROLE,
|
||||
)
|
||||
from plane.app.serializers import (
|
||||
IssueViewSerializer,
|
||||
|
|
@ -58,9 +58,6 @@ from plane.db.models import (
|
|||
class WorkspaceViewViewSet(BaseViewSet):
|
||||
serializer_class = IssueViewSerializer
|
||||
model = IssueView
|
||||
permission_classes = [
|
||||
WorkspaceEntityPermission,
|
||||
]
|
||||
|
||||
def perform_create(self, serializer):
|
||||
workspace = Workspace.objects.get(slug=self.kwargs.get("slug"))
|
||||
|
|
@ -78,6 +75,32 @@ class WorkspaceViewViewSet(BaseViewSet):
|
|||
.distinct()
|
||||
)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST],
|
||||
level="WORKSPACE",
|
||||
)
|
||||
def list(self, request, slug):
|
||||
queryset = self.get_queryset()
|
||||
fields = [
|
||||
field
|
||||
for field in request.GET.get("fields", "").split(",")
|
||||
if field
|
||||
]
|
||||
if WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role=5,
|
||||
is_active=True,
|
||||
).exists():
|
||||
queryset = queryset.filter(owned_by=request.user)
|
||||
views = IssueViewSerializer(
|
||||
queryset, many=True, fields=fields if fields else None
|
||||
).data
|
||||
return Response(views, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[], level="WORKSPACE", creator=True, model=IssueView
|
||||
)
|
||||
def partial_update(self, request, slug, pk):
|
||||
with transaction.atomic():
|
||||
workspace_view = IssueView.objects.select_for_update().get(
|
||||
|
|
@ -111,6 +134,12 @@ class WorkspaceViewViewSet(BaseViewSet):
|
|||
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN],
|
||||
level="WORKSPACE",
|
||||
creator=True,
|
||||
model=IssueView,
|
||||
)
|
||||
def destroy(self, request, slug, pk):
|
||||
workspace_view = IssueView.objects.get(
|
||||
pk=pk,
|
||||
|
|
@ -157,10 +186,6 @@ class WorkspaceViewViewSet(BaseViewSet):
|
|||
|
||||
|
||||
class WorkspaceViewIssuesViewSet(BaseViewSet):
|
||||
permission_classes = [
|
||||
WorkspaceEntityPermission,
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
Issue.issue_objects.annotate(
|
||||
|
|
@ -232,6 +257,10 @@ class WorkspaceViewIssuesViewSet(BaseViewSet):
|
|||
)
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST],
|
||||
level="WORKSPACE",
|
||||
)
|
||||
def list(self, request, slug):
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
order_by_param = request.GET.get("order_by", "-created_at")
|
||||
|
|
@ -242,6 +271,16 @@ class WorkspaceViewIssuesViewSet(BaseViewSet):
|
|||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
)
|
||||
|
||||
if WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role=5,
|
||||
is_active=True,
|
||||
).exists():
|
||||
issue_queryset = issue_queryset.filter(
|
||||
created_by=request.user,
|
||||
)
|
||||
|
||||
# Issue queryset
|
||||
issue_queryset, order_by_param = order_issue_queryset(
|
||||
issue_queryset=issue_queryset,
|
||||
|
|
@ -348,9 +387,6 @@ class WorkspaceViewIssuesViewSet(BaseViewSet):
|
|||
class IssueViewViewSet(BaseViewSet):
|
||||
serializer_class = IssueViewSerializer
|
||||
model = IssueView
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(
|
||||
|
|
@ -384,8 +420,20 @@ class IssueViewViewSet(BaseViewSet):
|
|||
.distinct()
|
||||
)
|
||||
|
||||
allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST]
|
||||
)
|
||||
|
||||
def list(self, request, slug, project_id):
|
||||
queryset = self.get_queryset()
|
||||
if ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
role=5,
|
||||
is_active=True,
|
||||
).exists():
|
||||
queryset = queryset.filter(owned_by=request.user)
|
||||
fields = [
|
||||
field
|
||||
for field in request.GET.get("fields", "").split(",")
|
||||
|
|
@ -396,6 +444,8 @@ class IssueViewViewSet(BaseViewSet):
|
|||
).data
|
||||
return Response(views, status=status.HTTP_200_OK)
|
||||
|
||||
allow_permission(allowed_roles=[], creator=True, model=IssueView)
|
||||
|
||||
def partial_update(self, request, slug, project_id, pk):
|
||||
with transaction.atomic():
|
||||
issue_view = IssueView.objects.select_for_update().get(
|
||||
|
|
@ -428,6 +478,8 @@ class IssueViewViewSet(BaseViewSet):
|
|||
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=IssueView)
|
||||
|
||||
def destroy(self, request, slug, project_id, pk):
|
||||
project_view = IssueView.objects.get(
|
||||
pk=pk,
|
||||
|
|
@ -472,6 +524,8 @@ class IssueViewFavoriteViewSet(BaseViewSet):
|
|||
.select_related("view")
|
||||
)
|
||||
|
||||
allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
|
||||
def create(self, request, slug, project_id):
|
||||
_ = UserFavorite.objects.create(
|
||||
user=request.user,
|
||||
|
|
@ -481,6 +535,8 @@ class IssueViewFavoriteViewSet(BaseViewSet):
|
|||
)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
|
||||
def destroy(self, request, slug, project_id, view_id):
|
||||
view_favorite = UserFavorite.objects.get(
|
||||
project=project_id,
|
||||
|
|
|
|||
|
|
@ -9,15 +9,13 @@ from rest_framework.response import Response
|
|||
from plane.db.models import Webhook, WebhookLog, Workspace
|
||||
from plane.db.models.webhook import generate_token
|
||||
from ..base import BaseAPIView
|
||||
from plane.app.permissions import WorkspaceOwnerPermission
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
from plane.app.serializers import WebhookSerializer, WebhookLogSerializer
|
||||
|
||||
|
||||
class WebhookEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
WorkspaceOwnerPermission,
|
||||
]
|
||||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
|
||||
def post(self, request, slug):
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
try:
|
||||
|
|
@ -40,6 +38,7 @@ class WebhookEndpoint(BaseAPIView):
|
|||
)
|
||||
raise IntegrityError
|
||||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
|
||||
def get(self, request, slug, pk=None):
|
||||
if pk is None:
|
||||
webhooks = Webhook.objects.filter(workspace__slug=slug)
|
||||
|
|
@ -79,6 +78,7 @@ class WebhookEndpoint(BaseAPIView):
|
|||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
|
||||
def patch(self, request, slug, pk):
|
||||
webhook = Webhook.objects.get(workspace__slug=slug, pk=pk)
|
||||
serializer = WebhookSerializer(
|
||||
|
|
@ -104,6 +104,7 @@ class WebhookEndpoint(BaseAPIView):
|
|||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
|
||||
def delete(self, request, slug, pk):
|
||||
webhook = Webhook.objects.get(pk=pk, workspace__slug=slug)
|
||||
webhook.delete()
|
||||
|
|
@ -111,10 +112,8 @@ class WebhookEndpoint(BaseAPIView):
|
|||
|
||||
|
||||
class WebhookSecretRegenerateEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
WorkspaceOwnerPermission,
|
||||
]
|
||||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
|
||||
def post(self, request, slug, pk):
|
||||
webhook = Webhook.objects.get(workspace__slug=slug, pk=pk)
|
||||
webhook.secret_key = generate_token()
|
||||
|
|
@ -124,10 +123,8 @@ class WebhookSecretRegenerateEndpoint(BaseAPIView):
|
|||
|
||||
|
||||
class WebhookLogsEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
WorkspaceOwnerPermission,
|
||||
]
|
||||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
|
||||
def get(self, request, slug, webhook_id):
|
||||
webhook_logs = WebhookLog.objects.filter(
|
||||
workspace__slug=slug, webhook_id=webhook_id
|
||||
|
|
|
|||
|
|
@ -9,14 +9,14 @@ from django.db.models import Q
|
|||
from plane.app.views.base import BaseAPIView
|
||||
from plane.db.models import UserFavorite, Workspace
|
||||
from plane.app.serializers import UserFavoriteSerializer
|
||||
from plane.app.permissions import WorkspaceEntityPermission
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
|
||||
|
||||
class WorkspaceFavoriteEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
WorkspaceEntityPermission,
|
||||
]
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE"
|
||||
)
|
||||
def get(self, request, slug):
|
||||
# the second filter is to check if the user is a member of the project
|
||||
favorites = UserFavorite.objects.filter(
|
||||
|
|
@ -34,6 +34,9 @@ class WorkspaceFavoriteEndpoint(BaseAPIView):
|
|||
serializer = UserFavoriteSerializer(favorites, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE"
|
||||
)
|
||||
def post(self, request, slug):
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
serializer = UserFavoriteSerializer(data=request.data)
|
||||
|
|
@ -46,6 +49,9 @@ class WorkspaceFavoriteEndpoint(BaseAPIView):
|
|||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE"
|
||||
)
|
||||
def patch(self, request, slug, favorite_id):
|
||||
favorite = UserFavorite.objects.get(
|
||||
user=request.user, workspace__slug=slug, pk=favorite_id
|
||||
|
|
@ -58,6 +64,9 @@ class WorkspaceFavoriteEndpoint(BaseAPIView):
|
|||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE"
|
||||
)
|
||||
def delete(self, request, slug, favorite_id):
|
||||
favorite = UserFavorite.objects.get(
|
||||
user=request.user, workspace__slug=slug, pk=favorite_id
|
||||
|
|
@ -67,10 +76,10 @@ class WorkspaceFavoriteEndpoint(BaseAPIView):
|
|||
|
||||
|
||||
class WorkspaceFavoriteGroupEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
WorkspaceEntityPermission,
|
||||
]
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE"
|
||||
)
|
||||
def get(self, request, slug, favorite_id):
|
||||
favorites = UserFavorite.objects.filter(
|
||||
user=request.user,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ import { useParams, useSearchParams } from "next/navigation";
|
|||
import { TPageNavigationTabs } from "@plane/types";
|
||||
// components
|
||||
import { PageHead } from "@/components/core";
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
import { PagesListRoot, PagesListView } from "@/components/pages";
|
||||
// constants
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store";
|
||||
|
||||
|
|
@ -16,7 +19,7 @@ const ProjectPagesPage = observer(() => {
|
|||
const type = searchParams.get("type");
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
// store hooks
|
||||
const { getProjectById } = useProject();
|
||||
const { getProjectById, currentProjectDetails } = useProject();
|
||||
// derived values
|
||||
const project = projectId ? getProjectById(projectId.toString()) : undefined;
|
||||
const pageTitle = project?.name ? `${project?.name} - Pages` : undefined;
|
||||
|
|
@ -29,6 +32,17 @@ const ProjectPagesPage = observer(() => {
|
|||
};
|
||||
|
||||
if (!workspaceSlug || !projectId) return <></>;
|
||||
|
||||
// No access to cycle
|
||||
if (currentProjectDetails?.page_view === false)
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full w-full">
|
||||
<EmptyState
|
||||
type={EmptyStateType.DISABLED_PROJECT_PAGE}
|
||||
primaryButtonLink={`/${workspaceSlug}/projects/${projectId}/settings/features`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
|
|
|
|||
|
|
@ -7,10 +7,9 @@ import { IProject } from "@plane/types";
|
|||
// ui
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { AutoArchiveAutomation, AutoCloseAutomation } from "@/components/automation";
|
||||
import { PageHead } from "@/components/core";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
// hooks
|
||||
import { useProject, useUser } from "@/hooks/store";
|
||||
|
||||
|
|
@ -19,6 +18,7 @@ const AutomationSettingsPage = observer(() => {
|
|||
const { workspaceSlug, projectId } = useParams();
|
||||
// store hooks
|
||||
const {
|
||||
canPerformProjectAdminActions,
|
||||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
const { currentProjectDetails: projectDetails, updateProject } = useProject();
|
||||
|
|
@ -36,13 +36,16 @@ const AutomationSettingsPage = observer(() => {
|
|||
};
|
||||
|
||||
// derived values
|
||||
const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN;
|
||||
const pageTitle = projectDetails?.name ? `${projectDetails?.name} - Automations` : undefined;
|
||||
|
||||
if (currentProjectRole && !canPerformProjectAdminActions) {
|
||||
return <NotAuthorizedView section="settings" isProjectView />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<section className={`w-full overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "opacity-60"}`}>
|
||||
<section className={`w-full overflow-y-auto py-8 pr-9 ${canPerformProjectAdminActions ? "" : "opacity-60"}`}>
|
||||
<div className="flex items-center border-b border-custom-border-100 py-3.5">
|
||||
<h3 className="text-xl font-medium">Automations</h3>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,30 +3,40 @@
|
|||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// components
|
||||
import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { EstimateRoot } from "@/components/estimates";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
// hooks
|
||||
import { useUser, useProject } from "@/hooks/store";
|
||||
|
||||
const EstimatesSettingsPage = observer(() => {
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
const {
|
||||
canPerformProjectAdminActions,
|
||||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
const { currentProjectDetails } = useProject();
|
||||
|
||||
// derived values
|
||||
const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN;
|
||||
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Estimates` : undefined;
|
||||
|
||||
if (!workspaceSlug || !projectId) return <></>;
|
||||
|
||||
if (currentProjectRole && !canPerformProjectAdminActions) {
|
||||
return <NotAuthorizedView section="settings" isProjectView />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className={`w-full overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "pointer-events-none opacity-60"}`}>
|
||||
<EstimateRoot workspaceSlug={workspaceSlug?.toString()} projectId={projectId?.toString()} isAdmin={isAdmin} />
|
||||
<div
|
||||
className={`w-full overflow-y-auto py-8 pr-9 ${canPerformProjectAdminActions ? "" : "pointer-events-none opacity-60"}`}
|
||||
>
|
||||
<EstimateRoot
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
projectId={projectId?.toString()}
|
||||
isAdmin={canPerformProjectAdminActions}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,12 +2,10 @@
|
|||
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// components
|
||||
import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { ProjectFeaturesList } from "@/components/project";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
// hooks
|
||||
import { useProject, useUser } from "@/hooks/store";
|
||||
|
||||
|
|
@ -15,28 +13,27 @@ const FeaturesSettingsPage = observer(() => {
|
|||
const { workspaceSlug, projectId } = useParams();
|
||||
// store
|
||||
const {
|
||||
membership: { fetchUserProjectInfo },
|
||||
canPerformProjectAdminActions,
|
||||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
const { currentProjectDetails } = useProject();
|
||||
// fetch the project details
|
||||
const { data: memberDetails } = useSWR(
|
||||
workspaceSlug && projectId ? `PROJECT_MEMBERS_ME_${workspaceSlug}_${projectId}` : null,
|
||||
workspaceSlug && projectId ? () => fetchUserProjectInfo(workspaceSlug.toString(), projectId.toString()) : null
|
||||
);
|
||||
// derived values
|
||||
const isAdmin = memberDetails?.role === EUserProjectRoles.ADMIN;
|
||||
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Features` : undefined;
|
||||
|
||||
if (!workspaceSlug || !projectId) return null;
|
||||
|
||||
if (currentProjectRole && !canPerformProjectAdminActions) {
|
||||
return <NotAuthorizedView section="settings" isProjectView />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<section className={`w-full overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "opacity-60"}`}>
|
||||
<section className={`w-full overflow-y-auto py-8 pr-9 ${canPerformProjectAdminActions ? "" : "opacity-60"}`}>
|
||||
<ProjectFeaturesList
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
isAdmin={isAdmin}
|
||||
isAdmin={canPerformProjectAdminActions}
|
||||
/>
|
||||
</section>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export const ProjectSettingHeader: FC = observer(() => {
|
|||
} = useUser();
|
||||
const { currentProjectDetails, loader } = useProject();
|
||||
|
||||
if (currentProjectRole && currentProjectRole <= EUserProjectRoles.VIEWER) return null;
|
||||
const projectMemberInfo = currentProjectRole || EUserProjectRoles.GUEST;
|
||||
|
||||
return (
|
||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
|
||||
|
|
@ -70,14 +70,17 @@ export const ProjectSettingHeader: FC = observer(() => {
|
|||
placement="bottom-start"
|
||||
closeOnSelect
|
||||
>
|
||||
{PROJECT_SETTINGS_LINKS.map((item) => (
|
||||
{PROJECT_SETTINGS_LINKS.map(
|
||||
(item) =>
|
||||
projectMemberInfo >= item.access && (
|
||||
<CustomMenu.MenuItem
|
||||
key={item.key}
|
||||
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}${item.href}`)}
|
||||
>
|
||||
{item.label}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
)
|
||||
)}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,13 +5,19 @@ import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
|||
import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element";
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { ProjectSettingsLabelList } from "@/components/labels";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store";
|
||||
import { useProject, useUser } from "@/hooks/store";
|
||||
|
||||
const LabelsSettingsPage = observer(() => {
|
||||
// store hooks
|
||||
const { currentProjectDetails } = useProject();
|
||||
const {
|
||||
canPerformProjectMemberActions,
|
||||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Labels` : undefined;
|
||||
|
||||
const scrollableContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
|
@ -29,6 +35,10 @@ const LabelsSettingsPage = observer(() => {
|
|||
);
|
||||
}, [scrollableContainerRef?.current]);
|
||||
|
||||
if (currentProjectRole && !canPerformProjectMemberActions) {
|
||||
return <NotAuthorizedView section="settings" isProjectView />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
|
|
|
|||
|
|
@ -1,18 +1,8 @@
|
|||
"use client";
|
||||
|
||||
import { FC, ReactNode } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
// ui
|
||||
import { Button, LayersIcon } from "@plane/ui";
|
||||
// components
|
||||
import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { AppHeader, ContentWrapper } from "@/components/core";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
// hooks
|
||||
import { useUser } from "@/hooks/store";
|
||||
// local components
|
||||
import { ProjectSettingHeader } from "./header";
|
||||
import { ProjectSettingsSidebar } from "./sidebar";
|
||||
|
|
@ -21,33 +11,8 @@ export interface IProjectSettingLayout {
|
|||
children: ReactNode;
|
||||
}
|
||||
|
||||
const ProjectSettingLayout: FC<IProjectSettingLayout> = observer((props) => {
|
||||
const ProjectSettingLayout: FC<IProjectSettingLayout> = (props) => {
|
||||
const { children } = props;
|
||||
// router
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
// store hooks
|
||||
const {
|
||||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
|
||||
const restrictViewSettings = currentProjectRole && currentProjectRole <= EUserProjectRoles.VIEWER;
|
||||
|
||||
if (restrictViewSettings) {
|
||||
return (
|
||||
<NotAuthorizedView
|
||||
type="project"
|
||||
actionButton={
|
||||
//TODO: Create a new component called Button Link to handle such scenarios
|
||||
<Link href={`/${workspaceSlug}/projects/${projectId}/issues`}>
|
||||
<Button variant="primary" size="md" prependIcon={<LayersIcon />}>
|
||||
Go to issues
|
||||
</Button>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppHeader header={<ProjectSettingHeader />} />
|
||||
|
|
@ -63,6 +28,6 @@ const ProjectSettingLayout: FC<IProjectSettingLayout> = observer((props) => {
|
|||
</ContentWrapper>
|
||||
</>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export default ProjectSettingLayout;
|
||||
|
|
|
|||
|
|
@ -2,17 +2,26 @@
|
|||
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { ProjectMemberList, ProjectSettingsMemberDefaults } from "@/components/project";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store";
|
||||
import { useProject, useUser } from "@/hooks/store";
|
||||
|
||||
const MembersSettingsPage = observer(() => {
|
||||
// store
|
||||
const { currentProjectDetails } = useProject();
|
||||
const {
|
||||
canPerformProjectViewerActions,
|
||||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
// derived values
|
||||
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Members` : undefined;
|
||||
|
||||
if (currentProjectRole && !canPerformProjectViewerActions) {
|
||||
return <NotAuthorizedView section="settings" isProjectView />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
// ui
|
||||
|
|
@ -12,7 +13,7 @@ import { EUserProjectRoles, PROJECT_SETTINGS_LINKS } from "@/constants/project";
|
|||
// hooks
|
||||
import { useUser } from "@/hooks/store";
|
||||
|
||||
export const ProjectSettingsSidebar = () => {
|
||||
export const ProjectSettingsSidebar = observer(() => {
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
const pathname = usePathname();
|
||||
// mobx store
|
||||
|
|
@ -60,4 +61,4 @@ export const ProjectSettingsSidebar = () => {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,18 +3,27 @@
|
|||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// components
|
||||
import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { ProjectStateRoot } from "@/components/project-states";
|
||||
// hook
|
||||
import { useProject } from "@/hooks/store";
|
||||
import { useProject, useUser } from "@/hooks/store";
|
||||
|
||||
const StatesSettingsPage = observer(() => {
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
// store
|
||||
const { currentProjectDetails } = useProject();
|
||||
const {
|
||||
canPerformProjectMemberActions,
|
||||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
// derived values
|
||||
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - States` : undefined;
|
||||
|
||||
if (currentProjectRole && !canPerformProjectMemberActions) {
|
||||
return <NotAuthorizedView section="settings" isProjectView />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
|
|
|
|||
|
|
@ -12,21 +12,17 @@ import { BreadcrumbLink, Logo } from "@/components/common";
|
|||
import { ViewListHeader } from "@/components/views";
|
||||
import { ViewAppliedFiltersList } from "@/components/views/applied-filters";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
import { EViewAccess } from "@/constants/views";
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useCommandPalette, useProject, useProjectView, useUser } from "@/hooks/store";
|
||||
import { useCommandPalette, useProject, useProjectView } from "@/hooks/store";
|
||||
|
||||
export const ProjectViewsHeader = observer(() => {
|
||||
// router
|
||||
const { workspaceSlug } = useParams();
|
||||
// store hooks
|
||||
const { toggleCreateViewModal } = useCommandPalette();
|
||||
const {
|
||||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
const { currentProjectDetails, loader } = useProject();
|
||||
const { filters, updateFilters, clearAllFilters } = useProjectView();
|
||||
|
||||
|
|
@ -49,9 +45,6 @@ export const ProjectViewsHeader = observer(() => {
|
|||
|
||||
const isFiltersApplied = calculateTotalFilters(filters?.filters ?? {}) !== 0;
|
||||
|
||||
const canUserCreateView =
|
||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
|
||||
|
|
@ -83,13 +76,11 @@ export const ProjectViewsHeader = observer(() => {
|
|||
</div>
|
||||
<div className="flex flex-shrink-0 items-center gap-2">
|
||||
<ViewListHeader />
|
||||
{canUserCreateView && (
|
||||
<div>
|
||||
<Button variant="primary" size="sm" onClick={() => toggleCreateViewModal(true)}>
|
||||
Add view
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{isFiltersApplied && (
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@ import useSWR from "swr";
|
|||
import { Button } from "@plane/ui";
|
||||
// component
|
||||
import { ApiTokenListItem, CreateApiTokenModal } from "@/components/api-token";
|
||||
import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
import { APITokenSettingsLoader } from "@/components/ui";
|
||||
// constants
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
import { API_TOKENS_LIST } from "@/constants/fetch-keys";
|
||||
import { EUserWorkspaceRoles } from "@/constants/workspace";
|
||||
// store hooks
|
||||
import { useUser, useWorkspace } from "@/hooks/store";
|
||||
// services
|
||||
|
|
@ -29,27 +29,22 @@ const ApiTokensPage = observer(() => {
|
|||
const { workspaceSlug } = useParams();
|
||||
// store hooks
|
||||
const {
|
||||
canPerformWorkspaceAdminActions,
|
||||
membership: { currentWorkspaceRole },
|
||||
} = useUser();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
|
||||
|
||||
const { data: tokens } = useSWR(workspaceSlug && isAdmin ? API_TOKENS_LIST(workspaceSlug.toString()) : null, () =>
|
||||
workspaceSlug && isAdmin ? apiTokenService.getApiTokens(workspaceSlug.toString()) : null
|
||||
const { data: tokens } = useSWR(
|
||||
workspaceSlug && canPerformWorkspaceAdminActions ? API_TOKENS_LIST(workspaceSlug.toString()) : null,
|
||||
() =>
|
||||
workspaceSlug && canPerformWorkspaceAdminActions ? apiTokenService.getApiTokens(workspaceSlug.toString()) : null
|
||||
);
|
||||
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - API Tokens` : undefined;
|
||||
|
||||
if (!isAdmin)
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="mt-10 flex h-full w-full justify-center p-4">
|
||||
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
if (currentWorkspaceRole && !canPerformWorkspaceAdminActions) {
|
||||
return <NotAuthorizedView section="settings" />;
|
||||
}
|
||||
|
||||
if (!tokens) {
|
||||
return <APITokenSettingsLoader />;
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@
|
|||
|
||||
import { observer } from "mobx-react";
|
||||
// component
|
||||
import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { PageHead } from "@/components/core";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "@/constants/workspace";
|
||||
// hooks
|
||||
import { useUser, useWorkspace } from "@/hooks/store";
|
||||
// plane web components
|
||||
|
|
@ -13,22 +12,16 @@ import { BillingRoot } from "@/plane-web/components/workspace";
|
|||
const BillingSettingsPage = observer(() => {
|
||||
// store hooks
|
||||
const {
|
||||
canPerformWorkspaceAdminActions,
|
||||
membership: { currentWorkspaceRole },
|
||||
} = useUser();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
// derived values
|
||||
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Billing & Plans` : undefined;
|
||||
|
||||
if (!isAdmin)
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="mt-10 flex h-full w-full justify-center p-4">
|
||||
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
if (currentWorkspaceRole && !canPerformWorkspaceAdminActions) {
|
||||
return <NotAuthorizedView section="settings" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -2,39 +2,39 @@
|
|||
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { PageHead } from "@/components/core";
|
||||
import ExportGuide from "@/components/exporter/guide";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "@/constants/workspace";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useUser, useWorkspace } from "@/hooks/store";
|
||||
|
||||
const ExportsPage = observer(() => {
|
||||
// store hooks
|
||||
const {
|
||||
canPerformWorkspaceViewerActions,
|
||||
canPerformWorkspaceMemberActions,
|
||||
membership: { currentWorkspaceRole },
|
||||
} = useUser();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
// derived values
|
||||
const hasPageAccess =
|
||||
currentWorkspaceRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentWorkspaceRole);
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Exports` : undefined;
|
||||
|
||||
if (!hasPageAccess)
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="mt-10 flex h-full w-full justify-center p-4">
|
||||
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
// if user is not authorized to view this page
|
||||
if (currentWorkspaceRole && !canPerformWorkspaceViewerActions) {
|
||||
return <NotAuthorizedView section="settings" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="w-full overflow-y-auto md:pr-9 pr-4">
|
||||
<div
|
||||
className={cn("w-full overflow-y-auto md:pr-9 pr-4", {
|
||||
"opacity-60": !canPerformWorkspaceMemberActions,
|
||||
})}
|
||||
>
|
||||
<div className="flex items-center border-b border-custom-border-100 py-3.5">
|
||||
<h3 className="text-xl font-medium">Exports</h3>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,12 +9,13 @@ import { IWorkspaceBulkInviteFormData } from "@plane/types";
|
|||
// ui
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { SendWorkspaceInvitationModal, WorkspaceMembersList } from "@/components/workspace";
|
||||
// constants
|
||||
import { MEMBER_INVITED } from "@/constants/event-tracker";
|
||||
import { EUserWorkspaceRoles } from "@/constants/workspace";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { getUserRole } from "@/helpers/user.helper";
|
||||
// hooks
|
||||
import { useEventTracker, useMember, useUser, useWorkspace } from "@/hooks/store";
|
||||
|
|
@ -28,6 +29,9 @@ const WorkspaceMembersSettingsPage = observer(() => {
|
|||
// store hooks
|
||||
const { captureEvent } = useEventTracker();
|
||||
const {
|
||||
canPerformWorkspaceAdminActions,
|
||||
canPerformWorkspaceViewerActions,
|
||||
canPerformWorkspaceMemberActions,
|
||||
membership: { currentWorkspaceRole },
|
||||
} = useUser();
|
||||
const {
|
||||
|
|
@ -79,9 +83,13 @@ const WorkspaceMembersSettingsPage = observer(() => {
|
|||
};
|
||||
|
||||
// derived values
|
||||
const isAdmin = currentWorkspaceRole && [EUserWorkspaceRoles.ADMIN].includes(currentWorkspaceRole);
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Members` : undefined;
|
||||
|
||||
// if user is not authorized to view this page
|
||||
if (currentWorkspaceRole && !canPerformWorkspaceViewerActions) {
|
||||
return <NotAuthorizedView section="settings" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
|
|
@ -90,7 +98,11 @@ const WorkspaceMembersSettingsPage = observer(() => {
|
|||
onClose={() => setInviteModal(false)}
|
||||
onSubmit={handleWorkspaceInvite}
|
||||
/>
|
||||
<section className="w-full overflow-y-auto md:pr-9 pr-4">
|
||||
<section
|
||||
className={cn("w-full overflow-y-auto md:pr-9 pr-4", {
|
||||
"opacity-60": !canPerformWorkspaceMemberActions,
|
||||
})}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-4 py-3.5">
|
||||
<h4 className="text-xl font-medium">Members</h4>
|
||||
<div className="ml-auto flex items-center gap-1.5 rounded-md border border-custom-border-200 bg-custom-background-100 px-2.5 py-1.5">
|
||||
|
|
@ -103,13 +115,13 @@ const WorkspaceMembersSettingsPage = observer(() => {
|
|||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{isAdmin && (
|
||||
{canPerformWorkspaceAdminActions && (
|
||||
<Button variant="primary" size="sm" onClick={() => setInviteModal(true)}>
|
||||
Add member
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<WorkspaceMembersList searchQuery={searchQuery} isAdmin={isAdmin ?? false} />
|
||||
<WorkspaceMembersList searchQuery={searchQuery} isAdmin={canPerformWorkspaceAdminActions} />
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import useSWR from "swr";
|
|||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// components
|
||||
import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
import { WebhookSettingsLoader } from "@/components/ui";
|
||||
|
|
@ -23,16 +24,15 @@ const WebhooksListPage = observer(() => {
|
|||
const { workspaceSlug } = useParams();
|
||||
// mobx store
|
||||
const {
|
||||
canPerformWorkspaceAdminActions,
|
||||
membership: { currentWorkspaceRole },
|
||||
} = useUser();
|
||||
const { fetchWebhooks, webhooks, clearSecretKey, webhookSecretKey, createWebhook } = useWebhook();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const isAdmin = currentWorkspaceRole === 20;
|
||||
|
||||
useSWR(
|
||||
workspaceSlug && isAdmin ? `WEBHOOKS_LIST_${workspaceSlug}` : null,
|
||||
workspaceSlug && isAdmin ? () => fetchWebhooks(workspaceSlug.toString()) : null
|
||||
workspaceSlug && canPerformWorkspaceAdminActions ? `WEBHOOKS_LIST_${workspaceSlug}` : null,
|
||||
workspaceSlug && canPerformWorkspaceAdminActions ? () => fetchWebhooks(workspaceSlug.toString()) : null
|
||||
);
|
||||
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Webhooks` : undefined;
|
||||
|
|
@ -42,15 +42,9 @@ const WebhooksListPage = observer(() => {
|
|||
if (!showCreateWebhookModal && webhookSecretKey) clearSecretKey();
|
||||
}, [showCreateWebhookModal, webhookSecretKey, clearSecretKey]);
|
||||
|
||||
if (!isAdmin)
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="mt-10 flex h-full w-full justify-center p-4">
|
||||
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
if (currentWorkspaceRole && !canPerformWorkspaceAdminActions) {
|
||||
return <NotAuthorizedView section="settings" />;
|
||||
}
|
||||
|
||||
if (!webhooks) return <WebhookSettingsLoader />;
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
import { SidebarFavoritesMenu } from "@/components/workspace/sidebar/favorites/favorites-menu";
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useAppTheme } from "@/hooks/store";
|
||||
import { useAppTheme, useUser } from "@/hooks/store";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
// plane web components
|
||||
import useSize from "@/hooks/use-window-size";
|
||||
|
|
@ -23,6 +23,7 @@ export interface IAppSidebar {}
|
|||
|
||||
export const AppSidebar: FC<IAppSidebar> = observer(() => {
|
||||
// store hooks
|
||||
const { canPerformWorkspaceMemberActions } = useUser();
|
||||
const { toggleSidebar, sidebarCollapsed } = useAppTheme();
|
||||
const windowSize = useSize();
|
||||
// refs
|
||||
|
|
@ -85,7 +86,7 @@ export const AppSidebar: FC<IAppSidebar> = observer(() => {
|
|||
"opacity-0": !sidebarCollapsed,
|
||||
})}
|
||||
/>
|
||||
<SidebarFavoritesMenu />
|
||||
{canPerformWorkspaceMemberActions && <SidebarFavoritesMenu />}
|
||||
|
||||
<SidebarProjectsList />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,11 +14,10 @@ import { DisplayFiltersSelection, FiltersDropdown, FilterSelection } from "@/com
|
|||
import { CreateUpdateWorkspaceViewModal } from "@/components/workspace";
|
||||
// constants
|
||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||
import { EUserWorkspaceRoles } from "@/constants/workspace";
|
||||
// helpers
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useLabel, useMember, useUser, useIssues, useGlobalView } from "@/hooks/store";
|
||||
import { useLabel, useMember, useIssues, useGlobalView } from "@/hooks/store";
|
||||
|
||||
export const GlobalIssuesHeader = observer(() => {
|
||||
// states
|
||||
|
|
@ -30,9 +29,6 @@ export const GlobalIssuesHeader = observer(() => {
|
|||
issuesFilter: { filters, updateFilters },
|
||||
} = useIssues(EIssuesStoreType.GLOBAL);
|
||||
const { getViewDetailsById } = useGlobalView();
|
||||
const {
|
||||
membership: { currentWorkspaceRole },
|
||||
} = useUser();
|
||||
const { workspaceLabels } = useLabel();
|
||||
const {
|
||||
workspace: { workspaceMemberIds },
|
||||
|
|
@ -97,8 +93,6 @@ export const GlobalIssuesHeader = observer(() => {
|
|||
[workspaceSlug, updateFilters, globalViewId]
|
||||
);
|
||||
|
||||
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
const isLocked = viewDetails?.is_locked;
|
||||
|
||||
return (
|
||||
|
|
@ -142,11 +136,10 @@ export const GlobalIssuesHeader = observer(() => {
|
|||
</FiltersDropdown>
|
||||
</>
|
||||
)}
|
||||
{isAuthorizedUser && (
|
||||
|
||||
<Button variant="primary" size="sm" onClick={() => setCreateViewModal(true)}>
|
||||
Add view
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export const WORKSPACE_SETTINGS = {
|
|||
key: "members",
|
||||
label: "Members",
|
||||
href: `/settings/members`,
|
||||
access: EUserWorkspaceRoles.GUEST,
|
||||
access: EUserWorkspaceRoles.VIEWER,
|
||||
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/members/`,
|
||||
Icon: SettingIcon,
|
||||
},
|
||||
|
|
@ -33,7 +33,7 @@ export const WORKSPACE_SETTINGS = {
|
|||
key: "export",
|
||||
label: "Exports",
|
||||
href: `/settings/exports`,
|
||||
access: EUserWorkspaceRoles.MEMBER,
|
||||
access: EUserWorkspaceRoles.VIEWER,
|
||||
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/exports/`,
|
||||
Icon: SettingIcon,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,62 +1,33 @@
|
|||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
// hooks
|
||||
import { useUser } from "@/hooks/store";
|
||||
// layouts
|
||||
import DefaultLayout from "@/layouts/default-layout";
|
||||
// images
|
||||
import ProjectNotAuthorizedImg from "@/public/auth/project-not-authorized.svg";
|
||||
import Unauthorized from "@/public/auth/unauthorized.svg";
|
||||
import WorkspaceNotAuthorizedImg from "@/public/auth/workspace-not-authorized.svg";
|
||||
|
||||
type Props = {
|
||||
actionButton?: React.ReactNode;
|
||||
type: "project" | "workspace";
|
||||
section?: "settings" | "general";
|
||||
isProjectView?: boolean;
|
||||
};
|
||||
|
||||
export const NotAuthorizedView: React.FC<Props> = observer((props) => {
|
||||
const { actionButton, type } = props;
|
||||
// router
|
||||
const searchParams = useSearchParams();
|
||||
const next_path = searchParams.get("next_path");
|
||||
// hooks
|
||||
const { data: currentUser } = useUser();
|
||||
const { actionButton, section = "general", isProjectView = false } = props;
|
||||
|
||||
// assets
|
||||
const settingAsset = isProjectView ? ProjectNotAuthorizedImg : WorkspaceNotAuthorizedImg;
|
||||
const asset = section === "settings" ? settingAsset : Unauthorized;
|
||||
|
||||
return (
|
||||
<DefaultLayout>
|
||||
<div className="flex h-full w-full flex-col items-center justify-center gap-y-5 bg-custom-background-100 text-center">
|
||||
<div className="h-44 w-72">
|
||||
<Image
|
||||
src={type === "project" ? ProjectNotAuthorizedImg : WorkspaceNotAuthorizedImg}
|
||||
height="176"
|
||||
width="288"
|
||||
alt="ProjectSettingImg"
|
||||
/>
|
||||
<Image src={asset} height="176" width="288" alt="ProjectSettingImg" />
|
||||
</div>
|
||||
<h1 className="text-xl font-medium text-custom-text-100">Oops! You are not authorized to view this page</h1>
|
||||
|
||||
<div className="w-full max-w-md text-base text-custom-text-200">
|
||||
{currentUser ? (
|
||||
<p>
|
||||
You have signed in as {currentUser.email}. <br />
|
||||
<Link href={`/?next_path=${next_path}`}>
|
||||
<span className="font-medium text-custom-text-100">Sign in</span>
|
||||
</Link>{" "}
|
||||
with different account that has access to this page.
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
You need to{" "}
|
||||
<Link href={`/?next_path=${next_path}`}>
|
||||
<span className="font-medium text-custom-text-100">Sign in</span>
|
||||
</Link>{" "}
|
||||
with an account that has access to this page.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{actionButton}
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@ export const CommandPalette: FC = observer(() => {
|
|||
const { platform } = usePlatformOS();
|
||||
const {
|
||||
data: currentUser,
|
||||
canPerformProjectCreateActions,
|
||||
canPerformWorkspaceCreateActions,
|
||||
canPerformProjectMemberActions,
|
||||
canPerformWorkspaceMemberActions,
|
||||
canPerformAnyCreateAction,
|
||||
canPerformProjectAdminActions,
|
||||
} = useUser();
|
||||
|
|
@ -103,15 +103,15 @@ export const CommandPalette: FC = observer(() => {
|
|||
// auth
|
||||
const performProjectCreateActions = useCallback(
|
||||
(showToast: boolean = true) => {
|
||||
if (!canPerformProjectCreateActions && showToast)
|
||||
if (!canPerformProjectMemberActions && showToast)
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "You don't have permission to perform this action.",
|
||||
});
|
||||
|
||||
return canPerformProjectCreateActions;
|
||||
return canPerformProjectMemberActions;
|
||||
},
|
||||
[canPerformProjectCreateActions]
|
||||
[canPerformProjectMemberActions]
|
||||
);
|
||||
|
||||
const performProjectBulkDeleteActions = useCallback(
|
||||
|
|
@ -129,14 +129,14 @@ export const CommandPalette: FC = observer(() => {
|
|||
|
||||
const performWorkspaceCreateActions = useCallback(
|
||||
(showToast: boolean = true) => {
|
||||
if (!canPerformWorkspaceCreateActions && showToast)
|
||||
if (!canPerformWorkspaceMemberActions && showToast)
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "You don't have permission to perform this action.",
|
||||
});
|
||||
return canPerformWorkspaceCreateActions;
|
||||
return canPerformWorkspaceMemberActions;
|
||||
},
|
||||
[canPerformWorkspaceCreateActions]
|
||||
[canPerformWorkspaceMemberActions]
|
||||
);
|
||||
|
||||
const performAnyProjectCreateActions = useCallback(
|
||||
|
|
|
|||
|
|
@ -7,10 +7,12 @@ import { Earth, Info, Lock, Minus } from "lucide-react";
|
|||
import { Avatar, FavoriteStar, TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { PageQuickActions } from "@/components/pages/dropdowns";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
// helpers
|
||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
import { useMember, usePage } from "@/hooks/store";
|
||||
import { useMember, usePage, useProject } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
|
|
@ -25,10 +27,15 @@ export const BlockItemAction: FC<Props> = observer((props) => {
|
|||
// store hooks
|
||||
const page = usePage(pageId);
|
||||
const { getUserDetails } = useMember();
|
||||
const { getProjectById } = useProject();
|
||||
|
||||
// derived values
|
||||
const { access, created_at, is_favorite, owned_by, addToFavorites, removePageFromFavorites } = page;
|
||||
|
||||
// derived values
|
||||
const project = getProjectById(projectId);
|
||||
const isViewerOrGuest =
|
||||
project?.member_role && [EUserProjectRoles.VIEWER, EUserProjectRoles.GUEST].includes(project.member_role);
|
||||
const ownerDetails = owned_by ? getUserDetails(owned_by) : undefined;
|
||||
|
||||
// handlers
|
||||
|
|
@ -74,6 +81,7 @@ export const BlockItemAction: FC<Props> = observer((props) => {
|
|||
</Tooltip>
|
||||
|
||||
{/* favorite/unfavorite */}
|
||||
{!isViewerOrGuest && (
|
||||
<FavoriteStar
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
|
|
@ -82,6 +90,7 @@ export const BlockItemAction: FC<Props> = observer((props) => {
|
|||
}}
|
||||
selected={is_favorite}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* quick actions dropdown */}
|
||||
<PageQuickActions
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { Avatar, Button, CustomSelect, CustomSearchSelect, TOAST_TYPE, setToast
|
|||
// helpers
|
||||
import { PROJECT_MEMBER_ADDED } from "@/constants/event-tracker";
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
import { ROLE } from "@/constants/workspace";
|
||||
import { EUserWorkspaceRoles, ROLE } from "@/constants/workspace";
|
||||
import { useEventTracker, useMember, useUser } from "@/hooks/store";
|
||||
// constants
|
||||
|
||||
|
|
@ -56,6 +56,8 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
|
|||
// form info
|
||||
const {
|
||||
formState: { errors, isSubmitting },
|
||||
watch,
|
||||
setValue,
|
||||
reset,
|
||||
handleSubmit,
|
||||
control,
|
||||
|
|
@ -167,6 +169,19 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
|
|||
}[]
|
||||
| undefined;
|
||||
|
||||
const checkCurrentOptionWorkspaceRole = (value: string) => {
|
||||
const currentMemberWorkspaceRole = getWorkspaceMemberDetails(value)?.role;
|
||||
if (!value || !currentMemberWorkspaceRole) return ROLE;
|
||||
|
||||
const isGuestOrViewer = [EUserWorkspaceRoles.GUEST, EUserWorkspaceRoles.VIEWER].includes(
|
||||
currentMemberWorkspaceRole
|
||||
);
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(ROLE).filter(([key]) => !isGuestOrViewer || [5, 10].includes(parseInt(key)))
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||
|
|
@ -237,6 +252,14 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
|
|||
}
|
||||
onChange={(val: string) => {
|
||||
onChange(val);
|
||||
// Update the role to the workspace role when member ID changes
|
||||
const workspaceMemberDetails = getWorkspaceMemberDetails(val);
|
||||
const workspaceRole = workspaceMemberDetails?.role ?? 5;
|
||||
const newValue = ROLE[workspaceRole].toUpperCase();
|
||||
setValue(
|
||||
`members.${index}.role`,
|
||||
EUserProjectRoles[newValue as keyof typeof EUserProjectRoles]
|
||||
);
|
||||
}}
|
||||
options={options}
|
||||
optionsClassName="w-full"
|
||||
|
|
@ -271,7 +294,9 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
|
|||
input
|
||||
optionsClassName="w-full"
|
||||
>
|
||||
{Object.entries(ROLE).map(([key, label]) => {
|
||||
{Object.entries(
|
||||
checkCurrentOptionWorkspaceRole(watch(`members.${index}.member_id`))
|
||||
).map(([key, label]) => {
|
||||
if (parseInt(key) > (currentProjectRole ?? EUserProjectRoles.GUEST)) return null;
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ export const AccountTypeColumn: React.FC<AccountTypeProps> = observer((props) =>
|
|||
// store hooks
|
||||
const {
|
||||
project: { updateMember },
|
||||
workspace: { getWorkspaceMemberDetails },
|
||||
} = useMember();
|
||||
const { data: currentUser } = useUser();
|
||||
|
||||
|
|
@ -99,6 +100,19 @@ export const AccountTypeColumn: React.FC<AccountTypeProps> = observer((props) =>
|
|||
const isAdminRole = currentProjectRole === EUserProjectRoles.ADMIN;
|
||||
const isRoleNonEditable = isCurrentUser || !isAdminRole;
|
||||
|
||||
const checkCurrentOptionWorkspaceRole = (value: string) => {
|
||||
const currentMemberWorkspaceRole = getWorkspaceMemberDetails(value)?.role;
|
||||
if (!value || !currentMemberWorkspaceRole) return ROLE;
|
||||
|
||||
const isGuestOrViewer = [EUserWorkspaceRoles.GUEST, EUserWorkspaceRoles.VIEWER].includes(
|
||||
currentMemberWorkspaceRole
|
||||
);
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(ROLE).filter(([key]) => !isGuestOrViewer || [5, 10].includes(parseInt(key)))
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isRoleNonEditable ? (
|
||||
|
|
@ -140,11 +154,14 @@ export const AccountTypeColumn: React.FC<AccountTypeProps> = observer((props) =>
|
|||
optionsClassName="w-full"
|
||||
input
|
||||
>
|
||||
{Object.keys(ROLE).map((item) => (
|
||||
<CustomSelect.Option key={item} value={item as unknown as EUserProjectRoles}>
|
||||
{ROLE[item as unknown as keyof typeof ROLE]}
|
||||
{Object.entries(checkCurrentOptionWorkspaceRole(rowData.member.id)).map(([key, label]) => {
|
||||
if (parseInt(key) > (currentProjectRole ?? EUserProjectRoles.GUEST)) return null;
|
||||
return (
|
||||
<CustomSelect.Option key={key} value={key}>
|
||||
{label}
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</CustomSelect>
|
||||
)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ import { EUserProjectRoles } from "@/constants/project";
|
|||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useAppTheme, useEventTracker, useProject } from "@/hooks/store";
|
||||
import { useAppTheme, useEventTracker, useProject, useUser } from "@/hooks/store";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// constants
|
||||
|
|
@ -70,31 +70,37 @@ const navigation = (workspaceSlug: string, projectId: string) => [
|
|||
name: "Issues",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/issues`,
|
||||
Icon: LayersIcon,
|
||||
access: EUserProjectRoles.GUEST,
|
||||
},
|
||||
{
|
||||
name: "Cycles",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/cycles`,
|
||||
Icon: ContrastIcon,
|
||||
access: EUserProjectRoles.VIEWER,
|
||||
},
|
||||
{
|
||||
name: "Modules",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/modules`,
|
||||
Icon: DiceIcon,
|
||||
access: EUserProjectRoles.VIEWER,
|
||||
},
|
||||
{
|
||||
name: "Views",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/views`,
|
||||
Icon: Layers,
|
||||
access: EUserProjectRoles.GUEST,
|
||||
},
|
||||
{
|
||||
name: "Pages",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/pages`,
|
||||
Icon: FileText,
|
||||
access: EUserProjectRoles.VIEWER,
|
||||
},
|
||||
{
|
||||
name: "Intake",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/inbox`,
|
||||
Icon: Intake,
|
||||
access: EUserProjectRoles.GUEST,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -106,6 +112,9 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
|||
const { setTrackElement } = useEventTracker();
|
||||
const { addProjectToFavorites, removeProjectFromFavorites, getProjectById } = useProject();
|
||||
const { isMobile } = usePlatformOS();
|
||||
const {
|
||||
membership: { currentWorkspaceAllProjectsRole },
|
||||
} = useUser();
|
||||
// states
|
||||
const [leaveProjectModalOpen, setLeaveProjectModal] = useState(false);
|
||||
const [publishModalOpen, setPublishModal] = useState(false);
|
||||
|
|
@ -378,7 +387,10 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
|||
customButtonClassName="grid place-items-center"
|
||||
placement="bottom-start"
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={project.is_favorite ? handleRemoveFromFavorites : handleAddToFavorites}>
|
||||
{!isViewerOrGuest && (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={project.is_favorite ? handleRemoveFromFavorites : handleAddToFavorites}
|
||||
>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<Star
|
||||
className={cn("h-3.5 w-3.5 ", {
|
||||
|
|
@ -388,6 +400,7 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
|||
<span>{project.is_favorite ? "Remove from favorites" : "Add to favorites"}</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
|
||||
{/* publish project settings */}
|
||||
{isAdmin && (
|
||||
|
|
@ -400,6 +413,7 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
|||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
{!isViewerOrGuest && (
|
||||
<CustomMenu.MenuItem>
|
||||
<Link href={`/${workspaceSlug}/projects/${project?.id}/draft-issues/`}>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
|
|
@ -408,6 +422,7 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
|||
</div>
|
||||
</Link>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
<CustomMenu.MenuItem onClick={handleCopyText}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<LinkIcon className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||
|
|
@ -482,8 +497,12 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
|||
(item.name === "Intake" && !project.inbox_view)
|
||||
)
|
||||
return;
|
||||
|
||||
const currentRole = currentWorkspaceAllProjectsRole
|
||||
? currentWorkspaceAllProjectsRole[projectId]
|
||||
: undefined;
|
||||
return (
|
||||
<>
|
||||
{currentRole >= item.access && (
|
||||
<Tooltip
|
||||
key={item.name}
|
||||
isMobile={isMobile}
|
||||
|
|
@ -507,6 +526,8 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
|||
</SidebarNavItem>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</Disclosure.Panel>
|
||||
|
|
|
|||
|
|
@ -263,7 +263,6 @@ export const SidebarProjectsList: FC = observer(() => {
|
|||
toggleCreateProjectModal(true);
|
||||
}}
|
||||
>
|
||||
<Plus className="flex-shrink-0 size-4" />
|
||||
{!isCollapsed && "Add project"}
|
||||
</button>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -279,7 +279,7 @@ export const SIDEBAR_WORKSPACE_MENU_ITEMS: {
|
|||
key: "active-cycles",
|
||||
label: "Cycles",
|
||||
href: `/active-cycles`,
|
||||
access: EUserWorkspaceRoles.GUEST,
|
||||
access: EUserWorkspaceRoles.MEMBER,
|
||||
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/active-cycles/`,
|
||||
Icon: ContrastIcon,
|
||||
},
|
||||
|
|
@ -317,7 +317,7 @@ export const SIDEBAR_USER_MENU_ITEMS: {
|
|||
key: "your-work",
|
||||
label: "Your work",
|
||||
href: "/profile",
|
||||
access: EUserWorkspaceRoles.GUEST,
|
||||
access: EUserWorkspaceRoles.MEMBER,
|
||||
highlight: (pathname: string, baseUrl: string, options?: TLinkOptions) =>
|
||||
options?.userId ? pathname.includes(`${baseUrl}/profile/${options?.userId}`) : false,
|
||||
Icon: UserActivityIcon,
|
||||
|
|
|
|||
|
|
@ -490,7 +490,7 @@ const emptyStateDetails = {
|
|||
},
|
||||
},
|
||||
accessType: "project",
|
||||
access: EUserProjectRoles.MEMBER,
|
||||
access: EUserProjectRoles.GUEST,
|
||||
},
|
||||
// project pages
|
||||
[EmptyStateType.PROJECT_PAGE]: {
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export const PROJECT_SETTINGS_LINKS: {
|
|||
key: "general",
|
||||
label: "General",
|
||||
href: `/settings`,
|
||||
access: EUserProjectRoles.MEMBER,
|
||||
access: EUserProjectRoles.GUEST,
|
||||
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/`,
|
||||
Icon: SettingIcon,
|
||||
},
|
||||
|
|
@ -87,7 +87,7 @@ export const PROJECT_SETTINGS_LINKS: {
|
|||
key: "members",
|
||||
label: "Members",
|
||||
href: `/settings/members`,
|
||||
access: EUserProjectRoles.MEMBER,
|
||||
access: EUserProjectRoles.VIEWER,
|
||||
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/members/`,
|
||||
Icon: SettingIcon,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
import { useProject, usePage, useProjectView, useCycle, useModule } from "@/hooks/store";
|
||||
|
||||
export const useFavoriteItemDetails = (workspaceSlug: string, favorite: IFavorite) => {
|
||||
const favoriteItemId = favorite.entity_data.id;
|
||||
const favoriteItemId = favorite?.entity_data?.id;
|
||||
const favoriteItemLogoProps = favorite?.entity_data?.logo_props;
|
||||
const favoriteItemName = favorite?.entity_data.name || favorite?.name;
|
||||
const favoriteItemEntityType = favorite?.entity_type;
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export const WorkspaceAuthWrapper: FC<IWorkspaceAuthWrapper> = observer((props)
|
|||
// next themes
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const { membership, signOut, data: currentUser } = useUser();
|
||||
const { membership, signOut, data: currentUser, canPerformWorkspaceMemberActions } = useUser();
|
||||
const { fetchProjects } = useProject();
|
||||
const { fetchFavorite } = useFavorite();
|
||||
const {
|
||||
|
|
@ -72,8 +72,12 @@ export const WorkspaceAuthWrapper: FC<IWorkspaceAuthWrapper> = observer((props)
|
|||
);
|
||||
// fetch workspace favorite
|
||||
useSWR(
|
||||
workspaceSlug && currentWorkspace ? `WORKSPACE_FAVORITE_${workspaceSlug}` : null,
|
||||
workspaceSlug && currentWorkspace ? () => fetchFavorite(workspaceSlug.toString()) : null,
|
||||
workspaceSlug && currentWorkspace && canPerformWorkspaceMemberActions
|
||||
? `WORKSPACE_FAVORITE_${workspaceSlug}`
|
||||
: null,
|
||||
workspaceSlug && currentWorkspace && canPerformWorkspaceMemberActions
|
||||
? () => fetchFavorite(workspaceSlug.toString())
|
||||
: null,
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -42,9 +42,18 @@ export interface IUserStore {
|
|||
reset: () => void;
|
||||
signOut: () => Promise<void>;
|
||||
// computed
|
||||
canPerformProjectCreateActions: boolean;
|
||||
|
||||
// workspace level
|
||||
canPerformWorkspaceAdminActions: boolean;
|
||||
canPerformWorkspaceMemberActions: boolean;
|
||||
canPerformWorkspaceViewerActions: boolean;
|
||||
canPerformWorkspaceGuestActions: boolean;
|
||||
|
||||
// project level
|
||||
canPerformProjectAdminActions: boolean;
|
||||
canPerformWorkspaceCreateActions: boolean;
|
||||
canPerformProjectMemberActions: boolean;
|
||||
canPerformProjectViewerActions: boolean;
|
||||
canPerformProjectGuestActions: boolean;
|
||||
canPerformAnyCreateAction: boolean;
|
||||
projectsWithCreatePermissions: { [projectId: string]: number } | null;
|
||||
}
|
||||
|
|
@ -92,9 +101,16 @@ export class UserStore implements IUserStore {
|
|||
reset: action,
|
||||
signOut: action,
|
||||
// computed
|
||||
canPerformProjectCreateActions: computed,
|
||||
canPerformWorkspaceAdminActions: computed,
|
||||
canPerformWorkspaceMemberActions: computed,
|
||||
canPerformWorkspaceViewerActions: computed,
|
||||
canPerformWorkspaceGuestActions: computed,
|
||||
|
||||
canPerformProjectAdminActions: computed,
|
||||
canPerformWorkspaceCreateActions: computed,
|
||||
canPerformProjectMemberActions: computed,
|
||||
canPerformProjectViewerActions: computed,
|
||||
canPerformProjectGuestActions: computed,
|
||||
|
||||
canPerformAnyCreateAction: computed,
|
||||
projectsWithCreatePermissions: computed,
|
||||
});
|
||||
|
|
@ -273,15 +289,40 @@ export class UserStore implements IUserStore {
|
|||
}
|
||||
|
||||
/**
|
||||
* @description tells if user has project create actions permissions
|
||||
* @description returns true if user has workspace admin actions permissions
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get canPerformProjectCreateActions() {
|
||||
return !!this.membership.currentProjectRole && this.membership.currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
get canPerformWorkspaceAdminActions() {
|
||||
return !!this.membership.currentWorkspaceRole && this.membership.currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description tells if user has project admin actions permissions
|
||||
* @description returns true if user has workspace member actions permissions
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get canPerformWorkspaceMemberActions() {
|
||||
return !!this.membership.currentWorkspaceRole && this.membership.currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description returns true if user has workspace viewer actions permissions
|
||||
* @returns {boolean}
|
||||
*/
|
||||
|
||||
get canPerformWorkspaceViewerActions() {
|
||||
return !!this.membership.currentWorkspaceRole && this.membership.currentWorkspaceRole >= EUserWorkspaceRoles.VIEWER;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description returns true if user has workspace guest actions permissions
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get canPerformWorkspaceGuestActions() {
|
||||
return !!this.membership.currentWorkspaceRole && this.membership.currentWorkspaceRole >= EUserWorkspaceRoles.GUEST;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description returns true if user has project admin actions permissions
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get canPerformProjectAdminActions() {
|
||||
|
|
@ -289,10 +330,27 @@ export class UserStore implements IUserStore {
|
|||
}
|
||||
|
||||
/**
|
||||
* @description tells if user has workspace create actions permissions
|
||||
* @description returns true if user has project member actions permissions
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get canPerformWorkspaceCreateActions() {
|
||||
return !!this.membership.currentWorkspaceRole && this.membership.currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||
get canPerformProjectMemberActions() {
|
||||
return !!this.membership.currentProjectRole && this.membership.currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description returns true if user has project viewer actions permissions
|
||||
* @returns {boolean}
|
||||
*/
|
||||
|
||||
get canPerformProjectViewerActions() {
|
||||
return !!this.membership.currentProjectRole && this.membership.currentProjectRole >= EUserProjectRoles.VIEWER;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description returns true if user has project guest actions permissions
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get canPerformProjectGuestActions() {
|
||||
return !!this.membership.currentProjectRole && this.membership.currentProjectRole >= EUserProjectRoles.GUEST;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
44
web/public/auth/unauthorized.svg
Normal file
44
web/public/auth/unauthorized.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 23 KiB |
Loading…
Add table
Add a link
Reference in a new issue