fix: merge conflicts from preview
This commit is contained in:
commit
3729011cb0
283 changed files with 4895 additions and 5157 deletions
|
|
@ -86,6 +86,7 @@ export const GeneralConfigurationForm: FC<IGeneralConfigurationForm> = observer(
|
||||||
value={instanceAdmins[0]?.user_detail?.email ?? ""}
|
value={instanceAdmins[0]?.user_detail?.email ?? ""}
|
||||||
placeholder="Admin email"
|
placeholder="Admin email"
|
||||||
className="w-full cursor-not-allowed !text-custom-text-400"
|
className="w-full cursor-not-allowed !text-custom-text-400"
|
||||||
|
autoComplete="on"
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ export const HelpSection: FC = observer(() => {
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`absolute bottom-2 min-w-[10rem] ${
|
className={`absolute bottom-2 min-w-[10rem] z-[15] ${
|
||||||
isSidebarCollapsed ? "left-full" : "-left-[75px]"
|
isSidebarCollapsed ? "left-full" : "-left-[75px]"
|
||||||
} divide-y divide-custom-border-200 whitespace-nowrap rounded bg-custom-background-100 p-1 shadow-custom-shadow-xs`}
|
} divide-y divide-custom-border-200 whitespace-nowrap rounded bg-custom-background-100 p-1 shadow-custom-shadow-xs`}
|
||||||
ref={helpOptionsRef}
|
ref={helpOptionsRef}
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,7 @@ export const InstanceSetupForm: FC = (props) => {
|
||||||
placeholder="Wilber"
|
placeholder="Wilber"
|
||||||
value={formData.first_name}
|
value={formData.first_name}
|
||||||
onChange={(e) => handleFormChange("first_name", e.target.value)}
|
onChange={(e) => handleFormChange("first_name", e.target.value)}
|
||||||
|
autoComplete="on"
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -190,6 +191,7 @@ export const InstanceSetupForm: FC = (props) => {
|
||||||
placeholder="Wright"
|
placeholder="Wright"
|
||||||
value={formData.last_name}
|
value={formData.last_name}
|
||||||
onChange={(e) => handleFormChange("last_name", e.target.value)}
|
onChange={(e) => handleFormChange("last_name", e.target.value)}
|
||||||
|
autoComplete="on"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -208,6 +210,7 @@ export const InstanceSetupForm: FC = (props) => {
|
||||||
value={formData.email}
|
value={formData.email}
|
||||||
onChange={(e) => handleFormChange("email", e.target.value)}
|
onChange={(e) => handleFormChange("email", e.target.value)}
|
||||||
hasError={errorData.type && errorData.type === EErrorCodes.INVALID_EMAIL ? true : false}
|
hasError={errorData.type && errorData.type === EErrorCodes.INVALID_EMAIL ? true : false}
|
||||||
|
autoComplete="on"
|
||||||
/>
|
/>
|
||||||
{errorData.type && errorData.type === EErrorCodes.INVALID_EMAIL && errorData.message && (
|
{errorData.type && errorData.type === EErrorCodes.INVALID_EMAIL && errorData.message && (
|
||||||
<p className="px-1 text-xs text-red-500">{errorData.message}</p>
|
<p className="px-1 text-xs text-red-500">{errorData.message}</p>
|
||||||
|
|
@ -247,6 +250,7 @@ export const InstanceSetupForm: FC = (props) => {
|
||||||
hasError={errorData.type && errorData.type === EErrorCodes.INVALID_PASSWORD ? true : false}
|
hasError={errorData.type && errorData.type === EErrorCodes.INVALID_PASSWORD ? true : false}
|
||||||
onFocus={() => setIsPasswordInputFocused(true)}
|
onFocus={() => setIsPasswordInputFocused(true)}
|
||||||
onBlur={() => setIsPasswordInputFocused(false)}
|
onBlur={() => setIsPasswordInputFocused(false)}
|
||||||
|
autoComplete="on"
|
||||||
/>
|
/>
|
||||||
{showPassword.password ? (
|
{showPassword.password ? (
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,7 @@ export const InstanceSignInForm: FC = (props) => {
|
||||||
placeholder="name@company.com"
|
placeholder="name@company.com"
|
||||||
value={formData.email}
|
value={formData.email}
|
||||||
onChange={(e) => handleFormChange("email", e.target.value)}
|
onChange={(e) => handleFormChange("email", e.target.value)}
|
||||||
|
autoComplete="on"
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -145,6 +146,7 @@ export const InstanceSignInForm: FC = (props) => {
|
||||||
placeholder="Enter your password"
|
placeholder="Enter your password"
|
||||||
value={formData.password}
|
value={formData.password}
|
||||||
onChange={(e) => handleFormChange("password", e.target.value)}
|
onChange={(e) => handleFormChange("password", e.target.value)}
|
||||||
|
autoComplete="on"
|
||||||
/>
|
/>
|
||||||
{showPassword ? (
|
{showPassword ? (
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
"@types/lodash": "^4.17.0",
|
"@types/lodash": "^4.17.0",
|
||||||
"autoprefixer": "10.4.14",
|
"autoprefixer": "10.4.14",
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.7.4",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.356.0",
|
"lucide-react": "^0.356.0",
|
||||||
|
|
|
||||||
|
|
@ -52,4 +52,4 @@ SPACE_BASE_URL=
|
||||||
APP_BASE_URL=
|
APP_BASE_URL=
|
||||||
|
|
||||||
# Hard delete files after days
|
# Hard delete files after days
|
||||||
HARD_DELETE_AFTER_DAYS=
|
HARD_DELETE_AFTER_DAYS=60
|
||||||
|
|
|
||||||
|
|
@ -32,4 +32,3 @@ python manage.py create_bucket
|
||||||
python manage.py clear_cache
|
python manage.py clear_cache
|
||||||
|
|
||||||
python manage.py runserver 0.0.0.0:8000 --settings=plane.settings.local
|
python manage.py runserver 0.0.0.0:8000 --settings=plane.settings.local
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,3 +40,44 @@ class ApiKeyRateThrottle(SimpleRateThrottle):
|
||||||
request.META["X-RateLimit-Reset"] = reset_time
|
request.META["X-RateLimit-Reset"] = reset_time
|
||||||
|
|
||||||
return allowed
|
return allowed
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceTokenRateThrottle(SimpleRateThrottle):
|
||||||
|
scope = "service_token"
|
||||||
|
rate = "300/minute"
|
||||||
|
|
||||||
|
def get_cache_key(self, request, view):
|
||||||
|
# Retrieve the API key from the request header
|
||||||
|
api_key = request.headers.get("X-Api-Key")
|
||||||
|
if not api_key:
|
||||||
|
return None # Allow the request if there's no API key
|
||||||
|
|
||||||
|
# Use the API key as part of the cache key
|
||||||
|
return f"{self.scope}:{api_key}"
|
||||||
|
|
||||||
|
def allow_request(self, request, view):
|
||||||
|
allowed = super().allow_request(request, view)
|
||||||
|
|
||||||
|
if allowed:
|
||||||
|
now = self.timer()
|
||||||
|
# Calculate the remaining limit and reset time
|
||||||
|
history = self.cache.get(self.key, [])
|
||||||
|
|
||||||
|
# Remove old histories
|
||||||
|
while history and history[-1] <= now - self.duration:
|
||||||
|
history.pop()
|
||||||
|
|
||||||
|
# Calculate the requests
|
||||||
|
num_requests = len(history)
|
||||||
|
|
||||||
|
# Check available requests
|
||||||
|
available = self.num_requests - num_requests
|
||||||
|
|
||||||
|
# Unix timestamp for when the rate limit will reset
|
||||||
|
reset_time = int(now + self.duration)
|
||||||
|
|
||||||
|
# Add headers
|
||||||
|
request.META["X-RateLimit-Remaining"] = max(0, available)
|
||||||
|
request.META["X-RateLimit-Reset"] = reset_time
|
||||||
|
|
||||||
|
return allowed
|
||||||
|
|
@ -269,6 +269,7 @@ class LabelSerializer(BaseSerializer):
|
||||||
"updated_by",
|
"updated_by",
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
|
"deleted_at",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -430,4 +431,3 @@ class IssueExpandSerializer(BaseSerializer):
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ class StateSerializer(BaseSerializer):
|
||||||
"updated_at",
|
"updated_at",
|
||||||
"workspace",
|
"workspace",
|
||||||
"project",
|
"project",
|
||||||
|
"deleted_at",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.urls import resolve
|
from django.urls import resolve
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from plane.db.models.api import APIToken
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
@ -16,7 +17,7 @@ from rest_framework.views import APIView
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from plane.api.middleware.api_authentication import APIKeyAuthentication
|
from plane.api.middleware.api_authentication import APIKeyAuthentication
|
||||||
from plane.api.rate_limit import ApiKeyRateThrottle
|
from plane.api.rate_limit import ApiKeyRateThrottle, ServiceTokenRateThrottle
|
||||||
from plane.utils.exception_logger import log_exception
|
from plane.utils.exception_logger import log_exception
|
||||||
from plane.utils.paginator import BasePaginator
|
from plane.utils.paginator import BasePaginator
|
||||||
|
|
||||||
|
|
@ -44,15 +45,29 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
||||||
IsAuthenticated,
|
IsAuthenticated,
|
||||||
]
|
]
|
||||||
|
|
||||||
throttle_classes = [
|
|
||||||
ApiKeyRateThrottle,
|
|
||||||
]
|
|
||||||
|
|
||||||
def filter_queryset(self, queryset):
|
def filter_queryset(self, queryset):
|
||||||
for backend in list(self.filter_backends):
|
for backend in list(self.filter_backends):
|
||||||
queryset = backend().filter_queryset(self.request, queryset, self)
|
queryset = backend().filter_queryset(self.request, queryset, self)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
def get_throttles(self):
|
||||||
|
throttle_classes = []
|
||||||
|
api_key = self.request.headers.get("X-Api-Key")
|
||||||
|
|
||||||
|
if api_key:
|
||||||
|
service_token = APIToken.objects.filter(
|
||||||
|
token=api_key,
|
||||||
|
is_service=True,
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if service_token:
|
||||||
|
throttle_classes.append(ServiceTokenRateThrottle())
|
||||||
|
return throttle_classes
|
||||||
|
|
||||||
|
throttle_classes.append(ApiKeyRateThrottle())
|
||||||
|
|
||||||
|
return throttle_classes
|
||||||
|
|
||||||
def handle_exception(self, exc):
|
def handle_exception(self, exc):
|
||||||
"""
|
"""
|
||||||
Handle any exception that occurs, by returning an appropriate response,
|
Handle any exception that occurs, by returning an appropriate response,
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ from plane.api.serializers import (
|
||||||
CycleSerializer,
|
CycleSerializer,
|
||||||
)
|
)
|
||||||
from plane.app.permissions import ProjectEntityPermission
|
from plane.app.permissions import ProjectEntityPermission
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activities_task import issue_activity
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
Cycle,
|
Cycle,
|
||||||
CycleIssue,
|
CycleIssue,
|
||||||
|
|
@ -672,17 +672,6 @@ class CycleIssueAPIEndpoint(BaseAPIView):
|
||||||
workspace__slug=slug, project_id=project_id, pk=cycle_id
|
workspace__slug=slug, project_id=project_id, pk=cycle_id
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
|
||||||
cycle.end_date is not None
|
|
||||||
and cycle.end_date < timezone.now().date()
|
|
||||||
):
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"error": "The Cycle has already been completed so no new issues can be added"
|
|
||||||
},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get all CycleIssues already created
|
# Get all CycleIssues already created
|
||||||
cycle_issues = list(
|
cycle_issues = list(
|
||||||
CycleIssue.objects.filter(
|
CycleIssue.objects.filter(
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ from rest_framework.response import Response
|
||||||
# Module imports
|
# Module imports
|
||||||
from plane.api.serializers import InboxIssueSerializer, IssueSerializer
|
from plane.api.serializers import InboxIssueSerializer, IssueSerializer
|
||||||
from plane.app.permissions import ProjectLitePermission
|
from plane.app.permissions import ProjectLitePermission
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activities_task import issue_activity
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
Inbox,
|
Inbox,
|
||||||
InboxIssue,
|
InboxIssue,
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ from plane.app.permissions import (
|
||||||
ProjectLitePermission,
|
ProjectLitePermission,
|
||||||
ProjectMemberPermission,
|
ProjectMemberPermission,
|
||||||
)
|
)
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activities_task import issue_activity
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
Issue,
|
Issue,
|
||||||
IssueActivity,
|
IssueActivity,
|
||||||
|
|
@ -355,6 +355,124 @@ class IssueAPIEndpoint(BaseAPIView):
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def put(self, request, slug, project_id):
|
||||||
|
# Get the entities required for putting the issue, external_id and
|
||||||
|
# external_source are must to identify the issue here
|
||||||
|
project = Project.objects.get(pk=project_id)
|
||||||
|
external_id = request.data.get("external_id")
|
||||||
|
external_source = request.data.get("external_source")
|
||||||
|
|
||||||
|
# If the external_id and source are present, we need to find the exact
|
||||||
|
# issue that needs to be updated with the provided external_id and
|
||||||
|
# external_source
|
||||||
|
if external_id and external_source:
|
||||||
|
try:
|
||||||
|
issue = Issue.objects.get(
|
||||||
|
project_id=project_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
external_id=external_id,
|
||||||
|
external_source=external_source,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get the current instance of the issue in order to track
|
||||||
|
# changes and dispatch the issue activity
|
||||||
|
current_instance = json.dumps(
|
||||||
|
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get the requested data, encode it as django object and pass it
|
||||||
|
# to serializer to validation
|
||||||
|
requested_data = json.dumps(
|
||||||
|
self.request.data, cls=DjangoJSONEncoder
|
||||||
|
)
|
||||||
|
serializer = IssueSerializer(
|
||||||
|
issue,
|
||||||
|
data=request.data,
|
||||||
|
context={
|
||||||
|
"project_id": project_id,
|
||||||
|
"workspace_id": project.workspace_id,
|
||||||
|
},
|
||||||
|
partial=True,
|
||||||
|
)
|
||||||
|
if serializer.is_valid():
|
||||||
|
# If the serializer is valid, save the issue and dispatch
|
||||||
|
# the update issue activity worker event.
|
||||||
|
serializer.save()
|
||||||
|
issue_activity.delay(
|
||||||
|
type="issue.activity.updated",
|
||||||
|
requested_data=requested_data,
|
||||||
|
actor_id=str(request.user.id),
|
||||||
|
issue_id=str(issue.id),
|
||||||
|
project_id=str(project_id),
|
||||||
|
current_instance=current_instance,
|
||||||
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
return Response(
|
||||||
|
# If the serializer is not valid, respond with 400 bad
|
||||||
|
# request
|
||||||
|
serializer.errors,
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
except Issue.DoesNotExist:
|
||||||
|
# If the issue does not exist, a new record needs to be created
|
||||||
|
# for the requested data.
|
||||||
|
# Serialize the data with the context of the project and
|
||||||
|
# workspace
|
||||||
|
serializer = IssueSerializer(
|
||||||
|
data=request.data,
|
||||||
|
context={
|
||||||
|
"project_id": project_id,
|
||||||
|
"workspace_id": project.workspace_id,
|
||||||
|
"default_assignee_id": project.default_assignee_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# If the serializer is valid, save the issue and dispatch the
|
||||||
|
# issue activity worker event as created
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
# Refetch the issue
|
||||||
|
issue = Issue.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
pk=serializer.data["id"],
|
||||||
|
).first()
|
||||||
|
|
||||||
|
# If any of the created_at or created_by is present, update
|
||||||
|
# the issue with the provided data, else return with the
|
||||||
|
# default states given.
|
||||||
|
issue.created_at = request.data.get(
|
||||||
|
"created_at", timezone.now()
|
||||||
|
)
|
||||||
|
issue.created_by_id = request.data.get(
|
||||||
|
"created_by", request.user.id
|
||||||
|
)
|
||||||
|
issue.save(update_fields=["created_at", "created_by"])
|
||||||
|
|
||||||
|
issue_activity.delay(
|
||||||
|
type="issue.activity.created",
|
||||||
|
requested_data=json.dumps(
|
||||||
|
self.request.data, cls=DjangoJSONEncoder
|
||||||
|
),
|
||||||
|
actor_id=str(request.user.id),
|
||||||
|
issue_id=str(serializer.data.get("id", None)),
|
||||||
|
project_id=str(project_id),
|
||||||
|
current_instance=None,
|
||||||
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
serializer.data, status=status.HTTP_201_CREATED
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return Response(
|
||||||
|
{"error": "external_id and external_source are required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
def patch(self, request, slug, project_id, pk=None):
|
def patch(self, request, slug, project_id, pk=None):
|
||||||
issue = Issue.objects.get(
|
issue = Issue.objects.get(
|
||||||
workspace__slug=slug, project_id=project_id, pk=pk
|
workspace__slug=slug, project_id=project_id, pk=pk
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ from plane.api.serializers import (
|
||||||
ModuleSerializer,
|
ModuleSerializer,
|
||||||
)
|
)
|
||||||
from plane.app.permissions import ProjectEntityPermission
|
from plane.app.permissions import ProjectEntityPermission
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activities_task import issue_activity
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
Issue,
|
Issue,
|
||||||
IssueAttachment,
|
IssueAttachment,
|
||||||
|
|
@ -520,7 +520,6 @@ class ModuleIssueAPIEndpoint(BaseAPIView):
|
||||||
|
|
||||||
|
|
||||||
class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
|
class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
|
||||||
|
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
ProjectEntityPermission,
|
ProjectEntityPermission,
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -12,3 +12,4 @@ from .project import (
|
||||||
ProjectMemberPermission,
|
ProjectMemberPermission,
|
||||||
ProjectLitePermission,
|
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
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
from plane.app.views import ApiTokenEndpoint
|
from plane.app.views import ApiTokenEndpoint, ServiceApiTokenEndpoint
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# API Tokens
|
# API Tokens
|
||||||
|
|
@ -13,5 +13,10 @@ urlpatterns = [
|
||||||
ApiTokenEndpoint.as_view(),
|
ApiTokenEndpoint.as_view(),
|
||||||
name="api-tokens",
|
name="api-tokens",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/service-api-tokens/",
|
||||||
|
ServiceApiTokenEndpoint.as_view(),
|
||||||
|
name="service-api-tokens",
|
||||||
|
),
|
||||||
## End API Tokens
|
## End API Tokens
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ urlpatterns = [
|
||||||
name="inbox-issue",
|
name="inbox-issue",
|
||||||
),
|
),
|
||||||
path(
|
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(
|
InboxIssueViewSet.as_view(
|
||||||
{
|
{
|
||||||
"get": "retrieve",
|
"get": "retrieve",
|
||||||
|
|
|
||||||
|
|
@ -174,8 +174,10 @@ from .module.archive import (
|
||||||
ModuleArchiveUnarchiveEndpoint,
|
ModuleArchiveUnarchiveEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .api import ApiTokenEndpoint
|
from .api import (
|
||||||
|
ApiTokenEndpoint,
|
||||||
|
ServiceApiTokenEndpoint,
|
||||||
|
)
|
||||||
|
|
||||||
from .page.base import (
|
from .page.base import (
|
||||||
PageViewSet,
|
PageViewSet,
|
||||||
|
|
|
||||||
|
|
@ -7,22 +7,22 @@ from django.utils import timezone
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
# Module imports
|
||||||
from plane.app.permissions import WorkSpaceAdminPermission
|
from plane.app.permissions import WorkSpaceAdminPermission
|
||||||
from plane.app.serializers import AnalyticViewSerializer
|
from plane.app.serializers import AnalyticViewSerializer
|
||||||
|
|
||||||
# Module imports
|
|
||||||
from plane.app.views.base import BaseAPIView, BaseViewSet
|
from plane.app.views.base import BaseAPIView, BaseViewSet
|
||||||
from plane.bgtasks.analytic_plot_export import analytic_export_task
|
from plane.bgtasks.analytic_plot_export import analytic_export_task
|
||||||
from plane.db.models import AnalyticView, Issue, Workspace
|
from plane.db.models import AnalyticView, Issue, Workspace
|
||||||
from plane.utils.analytics_plot import build_graph_plot
|
from plane.utils.analytics_plot import build_graph_plot
|
||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
|
from plane.app.permissions import allow_permission, ROLE
|
||||||
|
|
||||||
|
|
||||||
class AnalyticsEndpoint(BaseAPIView):
|
class AnalyticsEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
|
||||||
WorkSpaceAdminPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
@allow_permission(
|
||||||
|
[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER], level="WORKSPACE"
|
||||||
|
)
|
||||||
def get(self, request, slug):
|
def get(self, request, slug):
|
||||||
x_axis = request.GET.get("x_axis", False)
|
x_axis = request.GET.get("x_axis", False)
|
||||||
y_axis = request.GET.get("y_axis", False)
|
y_axis = request.GET.get("y_axis", False)
|
||||||
|
|
@ -201,10 +201,10 @@ class AnalyticViewViewset(BaseViewSet):
|
||||||
|
|
||||||
|
|
||||||
class SavedAnalyticEndpoint(BaseAPIView):
|
class SavedAnalyticEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
|
||||||
WorkSpaceAdminPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
@allow_permission(
|
||||||
|
[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER], level="WORKSPACE"
|
||||||
|
)
|
||||||
def get(self, request, slug, analytic_id):
|
def get(self, request, slug, analytic_id):
|
||||||
analytic_view = AnalyticView.objects.get(
|
analytic_view = AnalyticView.objects.get(
|
||||||
pk=analytic_id, workspace__slug=slug
|
pk=analytic_id, workspace__slug=slug
|
||||||
|
|
@ -234,10 +234,10 @@ class SavedAnalyticEndpoint(BaseAPIView):
|
||||||
|
|
||||||
|
|
||||||
class ExportAnalyticsEndpoint(BaseAPIView):
|
class ExportAnalyticsEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
|
||||||
WorkSpaceAdminPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
@allow_permission(
|
||||||
|
[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER], level="WORKSPACE"
|
||||||
|
)
|
||||||
def post(self, request, slug):
|
def post(self, request, slug):
|
||||||
x_axis = request.data.get("x_axis", False)
|
x_axis = request.data.get("x_axis", False)
|
||||||
y_axis = request.data.get("y_axis", False)
|
y_axis = request.data.get("y_axis", False)
|
||||||
|
|
@ -301,10 +301,10 @@ class ExportAnalyticsEndpoint(BaseAPIView):
|
||||||
|
|
||||||
|
|
||||||
class DefaultAnalyticsEndpoint(BaseAPIView):
|
class DefaultAnalyticsEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
|
||||||
WorkSpaceAdminPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
@allow_permission(
|
||||||
|
[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST], level="WORKSPACE"
|
||||||
|
)
|
||||||
def get(self, request, slug):
|
def get(self, request, slug):
|
||||||
filters = issue_filters(request.GET, "GET")
|
filters = issue_filters(request.GET, "GET")
|
||||||
base_issues = Issue.issue_objects.filter(
|
base_issues = Issue.issue_objects.filter(
|
||||||
|
|
@ -380,12 +380,10 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
|
||||||
.order_by("-count")
|
.order_by("-count")
|
||||||
)
|
)
|
||||||
|
|
||||||
open_estimate_sum = open_issues_queryset.aggregate(
|
open_estimate_sum = open_issues_queryset.aggregate(sum=Sum("point"))[
|
||||||
sum=Sum("point")
|
|
||||||
)["sum"]
|
|
||||||
total_estimate_sum = base_issues.aggregate(sum=Sum("point"))[
|
|
||||||
"sum"
|
"sum"
|
||||||
]
|
]
|
||||||
|
total_estimate_sum = base_issues.aggregate(sum=Sum("point"))["sum"]
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ class ApiTokenEndpoint(BaseAPIView):
|
||||||
def get(self, request, slug, pk=None):
|
def get(self, request, slug, pk=None):
|
||||||
if pk is None:
|
if pk is None:
|
||||||
api_tokens = APIToken.objects.filter(
|
api_tokens = APIToken.objects.filter(
|
||||||
user=request.user, workspace__slug=slug
|
user=request.user, workspace__slug=slug, is_service=False
|
||||||
)
|
)
|
||||||
serializer = APITokenReadSerializer(api_tokens, many=True)
|
serializer = APITokenReadSerializer(api_tokens, many=True)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
@ -61,6 +61,7 @@ class ApiTokenEndpoint(BaseAPIView):
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
user=request.user,
|
user=request.user,
|
||||||
pk=pk,
|
pk=pk,
|
||||||
|
is_service=False,
|
||||||
)
|
)
|
||||||
api_token.delete()
|
api_token.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
@ -78,3 +79,44 @@ class ApiTokenEndpoint(BaseAPIView):
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceApiTokenEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
WorkspaceOwnerPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def post(self, request, slug):
|
||||||
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
|
|
||||||
|
api_token = APIToken.objects.filter(
|
||||||
|
workspace=workspace,
|
||||||
|
is_service=True,
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if api_token:
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"token": str(api_token.token),
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Check the user type
|
||||||
|
user_type = 1 if request.user.is_bot else 0
|
||||||
|
|
||||||
|
api_token = APIToken.objects.create(
|
||||||
|
label=str(uuid4().hex),
|
||||||
|
description="Service Token",
|
||||||
|
user=request.user,
|
||||||
|
workspace=workspace,
|
||||||
|
user_type=user_type,
|
||||||
|
is_service=True,
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"token": str(api_token.token),
|
||||||
|
},
|
||||||
|
status=status.HTTP_201_CREATED,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ from django.utils import timezone
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.response import Response
|
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.db.models import Cycle, UserFavorite, Issue, Label, User, Project
|
||||||
from plane.utils.analytics_plot import burndown_plot
|
from plane.utils.analytics_plot import burndown_plot
|
||||||
|
|
||||||
|
|
@ -34,10 +34,6 @@ from .. import BaseAPIView
|
||||||
|
|
||||||
class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||||
|
|
||||||
permission_classes = [
|
|
||||||
ProjectEntityPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
favorite_subquery = UserFavorite.objects.filter(
|
favorite_subquery = UserFavorite.objects.filter(
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
|
|
@ -292,6 +288,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER])
|
||||||
def get(self, request, slug, project_id, pk=None):
|
def get(self, request, slug, project_id, pk=None):
|
||||||
if pk is None:
|
if pk is None:
|
||||||
queryset = (
|
queryset = (
|
||||||
|
|
@ -596,6 +593,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def post(self, request, slug, project_id, cycle_id):
|
def post(self, request, slug, project_id, cycle_id):
|
||||||
cycle = Cycle.objects.get(
|
cycle = Cycle.objects.get(
|
||||||
pk=cycle_id, project_id=project_id, workspace__slug=slug
|
pk=cycle_id, project_id=project_id, workspace__slug=slug
|
||||||
|
|
@ -614,6 +612,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def delete(self, request, slug, project_id, cycle_id):
|
def delete(self, request, slug, project_id, cycle_id):
|
||||||
cycle = Cycle.objects.get(
|
cycle = Cycle.objects.get(
|
||||||
pk=cycle_id, project_id=project_id, workspace__slug=slug
|
pk=cycle_id, project_id=project_id, workspace__slug=slug
|
||||||
|
|
|
||||||
|
|
@ -29,15 +29,14 @@ from django.core.serializers.json import DjangoJSONEncoder
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from plane.app.permissions import (
|
from plane.app.permissions import (
|
||||||
ProjectEntityPermission,
|
allow_permission, ROLE
|
||||||
ProjectLitePermission,
|
|
||||||
)
|
)
|
||||||
from plane.app.serializers import (
|
from plane.app.serializers import (
|
||||||
CycleSerializer,
|
CycleSerializer,
|
||||||
CycleUserPropertiesSerializer,
|
CycleUserPropertiesSerializer,
|
||||||
CycleWriteSerializer,
|
CycleWriteSerializer,
|
||||||
)
|
)
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activities_task import issue_activity
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
Cycle,
|
Cycle,
|
||||||
CycleIssue,
|
CycleIssue,
|
||||||
|
|
@ -60,15 +59,6 @@ class CycleViewSet(BaseViewSet):
|
||||||
serializer_class = CycleSerializer
|
serializer_class = CycleSerializer
|
||||||
model = Cycle
|
model = Cycle
|
||||||
webhook_event = "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):
|
def get_queryset(self):
|
||||||
favorite_subquery = UserFavorite.objects.filter(
|
favorite_subquery = UserFavorite.objects.filter(
|
||||||
|
|
@ -325,6 +315,7 @@ class CycleViewSet(BaseViewSet):
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
|
||||||
def list(self, request, slug, project_id):
|
def list(self, request, slug, project_id):
|
||||||
queryset = self.get_queryset().filter(archived_at__isnull=True)
|
queryset = self.get_queryset().filter(archived_at__isnull=True)
|
||||||
cycle_view = request.GET.get("cycle_view", "all")
|
cycle_view = request.GET.get("cycle_view", "all")
|
||||||
|
|
@ -611,6 +602,7 @@ class CycleViewSet(BaseViewSet):
|
||||||
)
|
)
|
||||||
return Response(data, status=status.HTTP_200_OK)
|
return Response(data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def create(self, request, slug, project_id):
|
def create(self, request, slug, project_id):
|
||||||
if (
|
if (
|
||||||
request.data.get("start_date", None) is None
|
request.data.get("start_date", None) is None
|
||||||
|
|
@ -684,6 +676,7 @@ class CycleViewSet(BaseViewSet):
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def partial_update(self, request, slug, project_id, pk):
|
def partial_update(self, request, slug, project_id, pk):
|
||||||
queryset = self.get_queryset().filter(
|
queryset = self.get_queryset().filter(
|
||||||
workspace__slug=slug, project_id=project_id, pk=pk
|
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(cycle, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER])
|
||||||
def retrieve(self, request, slug, project_id, pk):
|
def retrieve(self, request, slug, project_id, pk):
|
||||||
queryset = (
|
queryset = (
|
||||||
self.get_queryset().filter(archived_at__isnull=True).filter(pk=pk)
|
self.get_queryset().filter(archived_at__isnull=True).filter(pk=pk)
|
||||||
|
|
@ -1039,6 +1033,7 @@ class CycleViewSet(BaseViewSet):
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN], creator=True, model=Cycle)
|
||||||
def destroy(self, request, slug, project_id, pk):
|
def destroy(self, request, slug, project_id, pk):
|
||||||
cycle = Cycle.objects.get(
|
cycle = Cycle.objects.get(
|
||||||
workspace__slug=slug, project_id=project_id, pk=pk
|
workspace__slug=slug, project_id=project_id, pk=pk
|
||||||
|
|
@ -1097,10 +1092,8 @@ class CycleViewSet(BaseViewSet):
|
||||||
|
|
||||||
|
|
||||||
class CycleDateCheckEndpoint(BaseAPIView):
|
class CycleDateCheckEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
|
||||||
ProjectEntityPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def post(self, request, slug, project_id):
|
def post(self, request, slug, project_id):
|
||||||
start_date = request.data.get("start_date", False)
|
start_date = request.data.get("start_date", False)
|
||||||
end_date = request.data.get("end_date", False)
|
end_date = request.data.get("end_date", False)
|
||||||
|
|
@ -1144,6 +1137,7 @@ class CycleFavoriteViewSet(BaseViewSet):
|
||||||
.select_related("cycle", "cycle__owned_by")
|
.select_related("cycle", "cycle__owned_by")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def create(self, request, slug, project_id):
|
def create(self, request, slug, project_id):
|
||||||
_ = UserFavorite.objects.create(
|
_ = UserFavorite.objects.create(
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
|
|
@ -1153,6 +1147,7 @@ class CycleFavoriteViewSet(BaseViewSet):
|
||||||
)
|
)
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def destroy(self, request, slug, project_id, cycle_id):
|
def destroy(self, request, slug, project_id, cycle_id):
|
||||||
cycle_favorite = UserFavorite.objects.get(
|
cycle_favorite = UserFavorite.objects.get(
|
||||||
project=project_id,
|
project=project_id,
|
||||||
|
|
@ -1166,10 +1161,8 @@ class CycleFavoriteViewSet(BaseViewSet):
|
||||||
|
|
||||||
|
|
||||||
class TransferCycleIssueEndpoint(BaseAPIView):
|
class TransferCycleIssueEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
|
||||||
ProjectEntityPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def post(self, request, slug, project_id, cycle_id):
|
def post(self, request, slug, project_id, cycle_id):
|
||||||
new_cycle_id = request.data.get("new_cycle_id", False)
|
new_cycle_id = request.data.get("new_cycle_id", False)
|
||||||
|
|
||||||
|
|
@ -1579,10 +1572,8 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
||||||
|
|
||||||
|
|
||||||
class CycleUserPropertiesEndpoint(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):
|
def patch(self, request, slug, project_id, cycle_id):
|
||||||
cycle_properties = CycleUserProperties.objects.get(
|
cycle_properties = CycleUserProperties.objects.get(
|
||||||
user=request.user,
|
user=request.user,
|
||||||
|
|
@ -1605,6 +1596,7 @@ class CycleUserPropertiesEndpoint(BaseAPIView):
|
||||||
serializer = CycleUserPropertiesSerializer(cycle_properties)
|
serializer = CycleUserPropertiesSerializer(cycle_properties)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
|
||||||
def get(self, request, slug, project_id, cycle_id):
|
def get(self, request, slug, project_id, cycle_id):
|
||||||
cycle_properties, _ = CycleUserProperties.objects.get_or_create(
|
cycle_properties, _ = CycleUserProperties.objects.get_or_create(
|
||||||
user=request.user,
|
user=request.user,
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,7 @@ import json
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.core import serializers
|
from django.core import serializers
|
||||||
from django.db.models import (
|
from django.db.models import F, Func, OuterRef, Q
|
||||||
F,
|
|
||||||
Func,
|
|
||||||
OuterRef,
|
|
||||||
Q,
|
|
||||||
)
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.decorators.gzip import gzip_page
|
from django.views.decorators.gzip import gzip_page
|
||||||
|
|
@ -17,16 +12,12 @@ from django.views.decorators.gzip import gzip_page
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from plane.app.permissions import (
|
|
||||||
ProjectEntityPermission,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .. import BaseViewSet
|
from .. import BaseViewSet
|
||||||
from plane.app.serializers import (
|
from plane.app.serializers import (
|
||||||
CycleIssueSerializer,
|
CycleIssueSerializer,
|
||||||
)
|
)
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activities_task import issue_activity
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
Cycle,
|
Cycle,
|
||||||
CycleIssue,
|
CycleIssue,
|
||||||
|
|
@ -45,6 +36,7 @@ from plane.utils.paginator import (
|
||||||
GroupedOffsetPaginator,
|
GroupedOffsetPaginator,
|
||||||
SubGroupedOffsetPaginator,
|
SubGroupedOffsetPaginator,
|
||||||
)
|
)
|
||||||
|
from plane.app.permissions import allow_permission, ROLE
|
||||||
|
|
||||||
|
|
||||||
class CycleIssueViewSet(BaseViewSet):
|
class CycleIssueViewSet(BaseViewSet):
|
||||||
|
|
@ -54,10 +46,6 @@ class CycleIssueViewSet(BaseViewSet):
|
||||||
webhook_event = "cycle_issue"
|
webhook_event = "cycle_issue"
|
||||||
bulk = True
|
bulk = True
|
||||||
|
|
||||||
permission_classes = [
|
|
||||||
ProjectEntityPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
filterset_fields = [
|
filterset_fields = [
|
||||||
"issue__labels__id",
|
"issue__labels__id",
|
||||||
"issue__assignees__id",
|
"issue__assignees__id",
|
||||||
|
|
@ -92,6 +80,7 @@ class CycleIssueViewSet(BaseViewSet):
|
||||||
)
|
)
|
||||||
|
|
||||||
@method_decorator(gzip_page)
|
@method_decorator(gzip_page)
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER])
|
||||||
def list(self, request, slug, project_id, cycle_id):
|
def list(self, request, slug, project_id, cycle_id):
|
||||||
order_by_param = request.GET.get("order_by", "created_at")
|
order_by_param = request.GET.get("order_by", "created_at")
|
||||||
filters = issue_filters(request.query_params, "GET")
|
filters = issue_filters(request.query_params, "GET")
|
||||||
|
|
@ -238,6 +227,7 @@ class CycleIssueViewSet(BaseViewSet):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def create(self, request, slug, project_id, cycle_id):
|
def create(self, request, slug, project_id, cycle_id):
|
||||||
issues = request.data.get("issues", [])
|
issues = request.data.get("issues", [])
|
||||||
|
|
||||||
|
|
@ -333,6 +323,7 @@ class CycleIssueViewSet(BaseViewSet):
|
||||||
)
|
)
|
||||||
return Response({"message": "success"}, status=status.HTTP_201_CREATED)
|
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):
|
def destroy(self, request, slug, project_id, cycle_id, issue_id):
|
||||||
cycle_issue = CycleIssue.objects.filter(
|
cycle_issue = CycleIssue.objects.filter(
|
||||||
issue_id=issue_id,
|
issue_id=issue_id,
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ from plane.db.models import (
|
||||||
ProjectMember,
|
ProjectMember,
|
||||||
User,
|
User,
|
||||||
Widget,
|
Widget,
|
||||||
|
WorkspaceMember,
|
||||||
)
|
)
|
||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
|
|
||||||
|
|
@ -51,36 +52,61 @@ from .. import BaseAPIView
|
||||||
|
|
||||||
|
|
||||||
def dashboard_overview_stats(self, request, slug):
|
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__is_active=True,
|
||||||
project__project_projectmember__member=request.user,
|
project__project_projectmember__member=request.user,
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
assignees__in=[request.user],
|
assignees__in=[request.user],
|
||||||
).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"]),
|
~Q(state__group__in=["completed", "cancelled"]),
|
||||||
target_date__lt=timezone.now().date(),
|
target_date__lt=timezone.now().date(),
|
||||||
project__project_projectmember__is_active=True,
|
project__project_projectmember__is_active=True,
|
||||||
project__project_projectmember__member=request.user,
|
project__project_projectmember__member=request.user,
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
assignees__in=[request.user],
|
assignees__in=[request.user],
|
||||||
).count()
|
)
|
||||||
|
.filter(**extra_filters)
|
||||||
|
.count()
|
||||||
|
)
|
||||||
|
|
||||||
created_issues_count = Issue.issue_objects.filter(
|
created_issues_count = (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
project__project_projectmember__is_active=True,
|
project__project_projectmember__is_active=True,
|
||||||
project__project_projectmember__member=request.user,
|
project__project_projectmember__member=request.user,
|
||||||
created_by_id=request.user.id,
|
created_by_id=request.user.id,
|
||||||
).count()
|
)
|
||||||
|
.filter(**extra_filters)
|
||||||
|
.count()
|
||||||
|
)
|
||||||
|
|
||||||
completed_issues_count = Issue.issue_objects.filter(
|
completed_issues_count = (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
project__project_projectmember__is_active=True,
|
project__project_projectmember__is_active=True,
|
||||||
project__project_projectmember__member=request.user,
|
project__project_projectmember__member=request.user,
|
||||||
assignees__in=[request.user],
|
assignees__in=[request.user],
|
||||||
state__group="completed",
|
state__group="completed",
|
||||||
).count()
|
)
|
||||||
|
.filter(**extra_filters)
|
||||||
|
.count()
|
||||||
|
)
|
||||||
|
|
||||||
return Response(
|
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 Ordering
|
||||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||||
assigned_issues = assigned_issues.annotate(
|
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):
|
def dashboard_issues_by_state_groups(self, request, slug):
|
||||||
filters = issue_filters(request.query_params, "GET")
|
filters = issue_filters(request.query_params, "GET")
|
||||||
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
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 = (
|
issues_by_state_groups = (
|
||||||
Issue.issue_objects.filter(
|
Issue.issue_objects.filter(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
|
|
@ -416,7 +460,7 @@ def dashboard_issues_by_state_groups(self, request, slug):
|
||||||
project__project_projectmember__member=request.user,
|
project__project_projectmember__member=request.user,
|
||||||
assignees__in=[request.user],
|
assignees__in=[request.user],
|
||||||
)
|
)
|
||||||
.filter(**filters)
|
.filter(**filters, **extra_filters)
|
||||||
.values("state__group")
|
.values("state__group")
|
||||||
.annotate(count=Count("id"))
|
.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):
|
def dashboard_issues_by_priority(self, request, slug):
|
||||||
filters = issue_filters(request.query_params, "GET")
|
filters = issue_filters(request.query_params, "GET")
|
||||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
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 = (
|
issues_by_priority = (
|
||||||
Issue.issue_objects.filter(
|
Issue.issue_objects.filter(
|
||||||
|
|
@ -447,7 +500,7 @@ def dashboard_issues_by_priority(self, request, slug):
|
||||||
project__project_projectmember__member=request.user,
|
project__project_projectmember__member=request.user,
|
||||||
assignees__in=[request.user],
|
assignees__in=[request.user],
|
||||||
)
|
)
|
||||||
.filter(**filters)
|
.filter(**filters, **extra_filters)
|
||||||
.values("priority")
|
.values("priority")
|
||||||
.annotate(count=Count("id"))
|
.annotate(count=Count("id"))
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,11 @@ from rest_framework import status
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from ..base import BaseViewSet, BaseAPIView
|
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.db.models import Project, Estimate, EstimatePoint, Issue
|
||||||
from plane.app.serializers import (
|
from plane.app.serializers import (
|
||||||
EstimateSerializer,
|
EstimateSerializer,
|
||||||
|
|
@ -23,10 +27,8 @@ def generate_random_name(length=10):
|
||||||
|
|
||||||
|
|
||||||
class ProjectEstimatePointEndpoint(BaseAPIView):
|
class ProjectEstimatePointEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
|
||||||
ProjectEntityPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER])
|
||||||
def get(self, request, slug, project_id):
|
def get(self, request, slug, project_id):
|
||||||
project = Project.objects.get(workspace__slug=slug, pk=project_id)
|
project = Project.objects.get(workspace__slug=slug, pk=project_id)
|
||||||
if project.estimate_id is not None:
|
if project.estimate_id is not None:
|
||||||
|
|
@ -189,10 +191,8 @@ class BulkEstimatePointEndpoint(BaseViewSet):
|
||||||
|
|
||||||
|
|
||||||
class EstimatePointEndpoint(BaseViewSet):
|
class EstimatePointEndpoint(BaseViewSet):
|
||||||
permission_classes = [
|
|
||||||
ProjectEntityPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def create(self, request, slug, project_id, estimate_id):
|
def create(self, request, slug, project_id, estimate_id):
|
||||||
# TODO: add a key validation if the same key already exists
|
# TODO: add a key validation if the same key already exists
|
||||||
if not request.data.get("key") or not request.data.get("value"):
|
if not request.data.get("key") or not request.data.get("value"):
|
||||||
|
|
@ -211,6 +211,7 @@ class EstimatePointEndpoint(BaseViewSet):
|
||||||
serializer = EstimatePointSerializer(estimate_point).data
|
serializer = EstimatePointSerializer(estimate_point).data
|
||||||
return Response(serializer, status=status.HTTP_200_OK)
|
return Response(serializer, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def partial_update(
|
def partial_update(
|
||||||
self, request, slug, project_id, estimate_id, estimate_point_id
|
self, request, slug, project_id, estimate_id, estimate_point_id
|
||||||
):
|
):
|
||||||
|
|
@ -231,6 +232,7 @@ class EstimatePointEndpoint(BaseViewSet):
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def destroy(
|
def destroy(
|
||||||
self, request, slug, project_id, estimate_id, estimate_point_id
|
self, request, slug, project_id, estimate_id, estimate_point_id
|
||||||
):
|
):
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.response import Response
|
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.app.serializers import ExporterHistorySerializer
|
||||||
from plane.bgtasks.export_task import issue_export_task
|
from plane.bgtasks.export_task import issue_export_task
|
||||||
from plane.db.models import ExporterHistory, Project, Workspace
|
from plane.db.models import ExporterHistory, Project, Workspace
|
||||||
|
|
@ -12,12 +12,10 @@ from .. import BaseAPIView
|
||||||
|
|
||||||
|
|
||||||
class ExportIssuesEndpoint(BaseAPIView):
|
class ExportIssuesEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
|
||||||
WorkSpaceAdminPermission,
|
|
||||||
]
|
|
||||||
model = ExporterHistory
|
model = ExporterHistory
|
||||||
serializer_class = ExporterHistorySerializer
|
serializer_class = ExporterHistorySerializer
|
||||||
|
|
||||||
|
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
|
||||||
def post(self, request, slug):
|
def post(self, request, slug):
|
||||||
# Get the workspace
|
# Get the workspace
|
||||||
workspace = Workspace.objects.get(slug=slug)
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
|
|
@ -64,6 +62,7 @@ class ExportIssuesEndpoint(BaseAPIView):
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
|
||||||
def get(self, request, slug):
|
def get(self, request, slug):
|
||||||
exporter_history = ExporterHistory.objects.filter(
|
exporter_history = ExporterHistory.objects.filter(
|
||||||
workspace__slug=slug,
|
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
|
# Module imports
|
||||||
from ..base import BaseAPIView
|
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.db.models import Workspace, Project
|
||||||
from plane.app.serializers import (
|
from plane.app.serializers import (
|
||||||
ProjectLiteSerializer,
|
ProjectLiteSerializer,
|
||||||
|
|
@ -21,10 +21,8 @@ from plane.license.utils.instance_value import get_configuration_value
|
||||||
|
|
||||||
|
|
||||||
class GPTIntegrationEndpoint(BaseAPIView):
|
class GPTIntegrationEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
|
||||||
ProjectEntityPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def post(self, request, slug, project_id):
|
def post(self, request, slug, project_id):
|
||||||
OPENAI_API_KEY, GPT_ENGINE = get_configuration_value(
|
OPENAI_API_KEY, GPT_ENGINE = get_configuration_value(
|
||||||
[
|
[
|
||||||
|
|
@ -84,10 +82,10 @@ class GPTIntegrationEndpoint(BaseAPIView):
|
||||||
|
|
||||||
|
|
||||||
class WorkspaceGPTIntegrationEndpoint(BaseAPIView):
|
class WorkspaceGPTIntegrationEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
|
||||||
WorkspaceEntityPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
@allow_permission(
|
||||||
|
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE"
|
||||||
|
)
|
||||||
def post(self, request, slug):
|
def post(self, request, slug):
|
||||||
OPENAI_API_KEY, GPT_ENGINE = get_configuration_value(
|
OPENAI_API_KEY, GPT_ENGINE = get_configuration_value(
|
||||||
[
|
[
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,9 @@ from rest_framework.response import Response
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from ..base import BaseViewSet
|
from ..base import BaseViewSet
|
||||||
from plane.app.permissions import ProjectBasePermission, ProjectLitePermission
|
from plane.app.permissions import (
|
||||||
|
allow_permission, ROLE
|
||||||
|
)
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
Inbox,
|
Inbox,
|
||||||
InboxIssue,
|
InboxIssue,
|
||||||
|
|
@ -35,13 +37,10 @@ from plane.app.serializers import (
|
||||||
InboxIssueDetailSerializer,
|
InboxIssueDetailSerializer,
|
||||||
)
|
)
|
||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activities_task import issue_activity
|
||||||
|
|
||||||
|
|
||||||
class InboxViewSet(BaseViewSet):
|
class InboxViewSet(BaseViewSet):
|
||||||
permission_classes = [
|
|
||||||
ProjectBasePermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
serializer_class = InboxSerializer
|
serializer_class = InboxSerializer
|
||||||
model = Inbox
|
model = Inbox
|
||||||
|
|
@ -63,6 +62,7 @@ class InboxViewSet(BaseViewSet):
|
||||||
.select_related("workspace", "project")
|
.select_related("workspace", "project")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def list(self, request, slug, project_id):
|
def list(self, request, slug, project_id):
|
||||||
inbox = self.get_queryset().first()
|
inbox = self.get_queryset().first()
|
||||||
return Response(
|
return Response(
|
||||||
|
|
@ -70,9 +70,11 @@ class InboxViewSet(BaseViewSet):
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
serializer.save(project_id=self.kwargs.get("project_id"))
|
serializer.save(project_id=self.kwargs.get("project_id"))
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def destroy(self, request, slug, project_id, pk):
|
def destroy(self, request, slug, project_id, pk):
|
||||||
inbox = Inbox.objects.filter(
|
inbox = Inbox.objects.filter(
|
||||||
workspace__slug=slug, project_id=project_id, pk=pk
|
workspace__slug=slug, project_id=project_id, pk=pk
|
||||||
|
|
@ -88,9 +90,6 @@ class InboxViewSet(BaseViewSet):
|
||||||
|
|
||||||
|
|
||||||
class InboxIssueViewSet(BaseViewSet):
|
class InboxIssueViewSet(BaseViewSet):
|
||||||
permission_classes = [
|
|
||||||
ProjectLitePermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
serializer_class = InboxIssueSerializer
|
serializer_class = InboxIssueSerializer
|
||||||
model = InboxIssue
|
model = InboxIssue
|
||||||
|
|
@ -160,13 +159,15 @@ class InboxIssueViewSet(BaseViewSet):
|
||||||
ArrayAgg(
|
ArrayAgg(
|
||||||
"issue_module__module_id",
|
"issue_module__module_id",
|
||||||
distinct=True,
|
distinct=True,
|
||||||
filter=~Q(issue_module__module_id__isnull=True),
|
filter=~Q(issue_module__module_id__isnull=True)
|
||||||
|
& Q(issue_module__module__archived_at__isnull=True),
|
||||||
),
|
),
|
||||||
Value([], output_field=ArrayField(UUIDField())),
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
|
||||||
def list(self, request, slug, project_id):
|
def list(self, request, slug, project_id):
|
||||||
inbox_id = Inbox.objects.filter(
|
inbox_id = Inbox.objects.filter(
|
||||||
workspace__slug=slug, project_id=project_id
|
workspace__slug=slug, project_id=project_id
|
||||||
|
|
@ -200,6 +201,14 @@ class InboxIssueViewSet(BaseViewSet):
|
||||||
if inbox_status:
|
if inbox_status:
|
||||||
inbox_issue = inbox_issue.filter(status__in=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(
|
return self.paginate(
|
||||||
request=request,
|
request=request,
|
||||||
queryset=(inbox_issue),
|
queryset=(inbox_issue),
|
||||||
|
|
@ -209,6 +218,7 @@ class InboxIssueViewSet(BaseViewSet):
|
||||||
).data,
|
).data,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||||
def create(self, request, slug, project_id):
|
def create(self, request, slug, project_id):
|
||||||
if not request.data.get("issue", {}).get("name", False):
|
if not request.data.get("issue", {}).get("name", False):
|
||||||
return Response(
|
return Response(
|
||||||
|
|
@ -311,12 +321,13 @@ class InboxIssueViewSet(BaseViewSet):
|
||||||
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
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(
|
inbox_id = Inbox.objects.filter(
|
||||||
workspace__slug=slug, project_id=project_id
|
workspace__slug=slug, project_id=project_id
|
||||||
).first()
|
).first()
|
||||||
inbox_issue = InboxIssue.objects.get(
|
inbox_issue = InboxIssue.objects.get(
|
||||||
issue_id=issue_id,
|
issue_id=pk,
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
inbox_id=inbox_id,
|
inbox_id=inbox_id,
|
||||||
|
|
@ -457,7 +468,7 @@ class InboxIssueViewSet(BaseViewSet):
|
||||||
request.data, cls=DjangoJSONEncoder
|
request.data, cls=DjangoJSONEncoder
|
||||||
),
|
),
|
||||||
actor_id=str(request.user.id),
|
actor_id=str(request.user.id),
|
||||||
issue_id=str(issue_id),
|
issue_id=str(pk),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=current_instance,
|
current_instance=current_instance,
|
||||||
epoch=int(timezone.now().timestamp()),
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
|
@ -492,7 +503,7 @@ class InboxIssueViewSet(BaseViewSet):
|
||||||
)
|
)
|
||||||
.get(
|
.get(
|
||||||
inbox_id=inbox_id.id,
|
inbox_id=inbox_id.id,
|
||||||
issue_id=issue_id,
|
issue_id=pk,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -505,7 +516,12 @@ class InboxIssueViewSet(BaseViewSet):
|
||||||
serializer = InboxIssueDetailSerializer(inbox_issue).data
|
serializer = InboxIssueDetailSerializer(inbox_issue).data
|
||||||
return Response(serializer, status=status.HTTP_200_OK)
|
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(
|
inbox_id = Inbox.objects.filter(
|
||||||
workspace__slug=slug, project_id=project_id
|
workspace__slug=slug, project_id=project_id
|
||||||
).first()
|
).first()
|
||||||
|
|
@ -533,9 +549,7 @@ class InboxIssueViewSet(BaseViewSet):
|
||||||
Value([], output_field=ArrayField(UUIDField())),
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.get(
|
.get(inbox_id=inbox_id.id, issue_id=pk, project_id=project_id)
|
||||||
inbox_id=inbox_id.id, issue_id=issue_id, project_id=project_id
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
issue = InboxIssueDetailSerializer(inbox_issue).data
|
issue = InboxIssueDetailSerializer(inbox_issue).data
|
||||||
return Response(
|
return Response(
|
||||||
|
|
@ -543,12 +557,13 @@ class InboxIssueViewSet(BaseViewSet):
|
||||||
status=status.HTTP_200_OK,
|
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(
|
inbox_id = Inbox.objects.filter(
|
||||||
workspace__slug=slug, project_id=project_id
|
workspace__slug=slug, project_id=project_id
|
||||||
).first()
|
).first()
|
||||||
inbox_issue = InboxIssue.objects.get(
|
inbox_issue = InboxIssue.objects.get(
|
||||||
issue_id=issue_id,
|
issue_id=pk,
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
inbox_id=inbox_id,
|
inbox_id=inbox_id,
|
||||||
|
|
@ -558,21 +573,8 @@ class InboxIssueViewSet(BaseViewSet):
|
||||||
if inbox_issue.status in [-2, -1, 0, 2]:
|
if inbox_issue.status in [-2, -1, 0, 2]:
|
||||||
# Delete the issue also
|
# Delete the issue also
|
||||||
issue = Issue.objects.filter(
|
issue = Issue.objects.filter(
|
||||||
workspace__slug=slug, project_id=project_id, pk=issue_id
|
workspace__slug=slug, project_id=project_id, pk=pk
|
||||||
).first()
|
).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()
|
issue.delete()
|
||||||
|
|
||||||
inbox_issue.delete()
|
inbox_issue.delete()
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ from plane.app.serializers import (
|
||||||
IssueActivitySerializer,
|
IssueActivitySerializer,
|
||||||
IssueCommentSerializer,
|
IssueCommentSerializer,
|
||||||
)
|
)
|
||||||
from plane.app.permissions import ProjectEntityPermission
|
from plane.app.permissions import ProjectEntityPermission, allow_permission, ROLE
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
IssueActivity,
|
IssueActivity,
|
||||||
IssueComment,
|
IssueComment,
|
||||||
|
|
@ -33,6 +33,7 @@ class IssueActivityEndpoint(BaseAPIView):
|
||||||
]
|
]
|
||||||
|
|
||||||
@method_decorator(gzip_page)
|
@method_decorator(gzip_page)
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST, ROLE.VIEWER])
|
||||||
def get(self, request, slug, project_id, issue_id):
|
def get(self, request, slug, project_id, issue_id):
|
||||||
filters = {}
|
filters = {}
|
||||||
if request.GET.get("created_at__gt", None) is not None:
|
if request.GET.get("created_at__gt", None) is not None:
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,9 @@ from plane.app.permissions import (
|
||||||
from plane.app.serializers import (
|
from plane.app.serializers import (
|
||||||
IssueFlatSerializer,
|
IssueFlatSerializer,
|
||||||
IssueSerializer,
|
IssueSerializer,
|
||||||
IssueDetailSerializer
|
IssueDetailSerializer,
|
||||||
)
|
)
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activities_task import issue_activity
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
Issue,
|
Issue,
|
||||||
IssueAttachment,
|
IssueAttachment,
|
||||||
|
|
@ -46,15 +46,13 @@ from plane.utils.paginator import (
|
||||||
GroupedOffsetPaginator,
|
GroupedOffsetPaginator,
|
||||||
SubGroupedOffsetPaginator,
|
SubGroupedOffsetPaginator,
|
||||||
)
|
)
|
||||||
|
from plane.app.permissions import allow_permission, ROLE
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .. import BaseViewSet, BaseAPIView
|
from .. import BaseViewSet, BaseAPIView
|
||||||
|
|
||||||
|
|
||||||
class IssueArchiveViewSet(BaseViewSet):
|
class IssueArchiveViewSet(BaseViewSet):
|
||||||
permission_classes = [
|
|
||||||
ProjectEntityPermission,
|
|
||||||
]
|
|
||||||
serializer_class = IssueFlatSerializer
|
serializer_class = IssueFlatSerializer
|
||||||
model = Issue
|
model = Issue
|
||||||
|
|
||||||
|
|
@ -98,6 +96,7 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||||
)
|
)
|
||||||
|
|
||||||
@method_decorator(gzip_page)
|
@method_decorator(gzip_page)
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER])
|
||||||
def list(self, request, slug, project_id):
|
def list(self, request, slug, project_id):
|
||||||
filters = issue_filters(request.query_params, "GET")
|
filters = issue_filters(request.query_params, "GET")
|
||||||
show_sub_issues = request.GET.get("show_sub_issues", "true")
|
show_sub_issues = request.GET.get("show_sub_issues", "true")
|
||||||
|
|
@ -213,6 +212,7 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER])
|
||||||
def retrieve(self, request, slug, project_id, pk=None):
|
def retrieve(self, request, slug, project_id, pk=None):
|
||||||
issue = (
|
issue = (
|
||||||
self.get_queryset()
|
self.get_queryset()
|
||||||
|
|
@ -256,6 +256,7 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||||
serializer = IssueDetailSerializer(issue, expand=self.expand)
|
serializer = IssueDetailSerializer(issue, expand=self.expand)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def archive(self, request, slug, project_id, pk=None):
|
def archive(self, request, slug, project_id, pk=None):
|
||||||
issue = Issue.issue_objects.get(
|
issue = Issue.issue_objects.get(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
|
|
@ -294,6 +295,7 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||||
{"archived_at": str(issue.archived_at)}, status=status.HTTP_200_OK
|
{"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):
|
def unarchive(self, request, slug, project_id, pk=None):
|
||||||
issue = Issue.objects.get(
|
issue = Issue.objects.get(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
|
|
@ -325,6 +327,7 @@ class BulkArchiveIssuesEndpoint(BaseAPIView):
|
||||||
ProjectEntityPermission,
|
ProjectEntityPermission,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def post(self, request, slug, project_id):
|
def post(self, request, slug, project_id):
|
||||||
issue_ids = request.data.get("issue_ids", [])
|
issue_ids = request.data.get("issue_ids", [])
|
||||||
|
|
||||||
|
|
@ -343,7 +346,7 @@ class BulkArchiveIssuesEndpoint(BaseAPIView):
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"error_code": 4091,
|
"error_code": 4091,
|
||||||
"error_message": "INVALID_ARCHIVE_STATE_GROUP"
|
"error_message": "INVALID_ARCHIVE_STATE_GROUP",
|
||||||
},
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -13,19 +13,17 @@ from rest_framework.parsers import MultiPartParser, FormParser
|
||||||
# Module imports
|
# Module imports
|
||||||
from .. import BaseAPIView
|
from .. import BaseAPIView
|
||||||
from plane.app.serializers import IssueAttachmentSerializer
|
from plane.app.serializers import IssueAttachmentSerializer
|
||||||
from plane.app.permissions import ProjectEntityPermission
|
from plane.db.models import IssueAttachment
|
||||||
from plane.db.models import IssueAttachment, ProjectMember
|
from plane.bgtasks.issue_activities_task import issue_activity
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.app.permissions import allow_permission, ROLE
|
||||||
|
|
||||||
|
|
||||||
class IssueAttachmentEndpoint(BaseAPIView):
|
class IssueAttachmentEndpoint(BaseAPIView):
|
||||||
serializer_class = IssueAttachmentSerializer
|
serializer_class = IssueAttachmentSerializer
|
||||||
permission_classes = [
|
|
||||||
ProjectEntityPermission,
|
|
||||||
]
|
|
||||||
model = IssueAttachment
|
model = IssueAttachment
|
||||||
parser_classes = (MultiPartParser, FormParser)
|
parser_classes = (MultiPartParser, FormParser)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||||
def post(self, request, slug, project_id, issue_id):
|
def post(self, request, slug, project_id, issue_id):
|
||||||
serializer = IssueAttachmentSerializer(data=request.data)
|
serializer = IssueAttachmentSerializer(data=request.data)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
|
|
@ -47,21 +45,9 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN], creator=True, model=IssueAttachment)
|
||||||
def delete(self, request, slug, project_id, issue_id, pk):
|
def delete(self, request, slug, project_id, issue_id, pk):
|
||||||
issue_attachment = IssueAttachment.objects.get(pk=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.asset.delete(save=False)
|
||||||
issue_attachment.delete()
|
issue_attachment.delete()
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
|
|
@ -78,6 +64,7 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
||||||
|
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST, ROLE.VIEWER])
|
||||||
def get(self, request, slug, project_id, issue_id):
|
def get(self, request, slug, project_id, issue_id):
|
||||||
issue_attachments = IssueAttachment.objects.filter(
|
issue_attachments = IssueAttachment.objects.filter(
|
||||||
issue_id=issue_id, workspace__slug=slug, project_id=project_id
|
issue_id=issue_id, workspace__slug=slug, project_id=project_id
|
||||||
|
|
|
||||||
|
|
@ -25,17 +25,14 @@ from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from plane.app.permissions import (
|
from plane.app.permissions import allow_permission, ROLE
|
||||||
ProjectEntityPermission,
|
|
||||||
ProjectLitePermission,
|
|
||||||
)
|
|
||||||
from plane.app.serializers import (
|
from plane.app.serializers import (
|
||||||
IssueCreateSerializer,
|
IssueCreateSerializer,
|
||||||
IssueDetailSerializer,
|
IssueDetailSerializer,
|
||||||
IssueUserPropertySerializer,
|
IssueUserPropertySerializer,
|
||||||
IssueSerializer,
|
IssueSerializer,
|
||||||
)
|
)
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activities_task import issue_activity
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
Issue,
|
Issue,
|
||||||
IssueAttachment,
|
IssueAttachment,
|
||||||
|
|
@ -60,15 +57,10 @@ from plane.utils.paginator import (
|
||||||
from .. import BaseAPIView, BaseViewSet
|
from .. import BaseAPIView, BaseViewSet
|
||||||
from plane.utils.user_timezone_converter import user_timezone_converter
|
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||||
|
|
||||||
# Module imports
|
|
||||||
|
|
||||||
|
|
||||||
class IssueListEndpoint(BaseAPIView):
|
class IssueListEndpoint(BaseAPIView):
|
||||||
|
|
||||||
permission_classes = [
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
|
||||||
ProjectEntityPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
def get(self, request, slug, project_id):
|
def get(self, request, slug, project_id):
|
||||||
issue_ids = request.GET.get("issues", False)
|
issue_ids = request.GET.get("issues", False)
|
||||||
|
|
||||||
|
|
@ -185,9 +177,6 @@ class IssueViewSet(BaseViewSet):
|
||||||
|
|
||||||
model = Issue
|
model = Issue
|
||||||
webhook_event = "issue"
|
webhook_event = "issue"
|
||||||
permission_classes = [
|
|
||||||
ProjectEntityPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
search_fields = [
|
search_fields = [
|
||||||
"name",
|
"name",
|
||||||
|
|
@ -233,6 +222,7 @@ class IssueViewSet(BaseViewSet):
|
||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
@method_decorator(gzip_page)
|
@method_decorator(gzip_page)
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
|
||||||
def list(self, request, slug, project_id):
|
def list(self, request, slug, project_id):
|
||||||
filters = issue_filters(request.query_params, "GET")
|
filters = issue_filters(request.query_params, "GET")
|
||||||
order_by_param = request.GET.get("order_by", "-created_at")
|
order_by_param = request.GET.get("order_by", "-created_at")
|
||||||
|
|
@ -257,6 +247,15 @@ class IssueViewSet(BaseViewSet):
|
||||||
sub_group_by=sub_group_by,
|
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 group_by:
|
||||||
if sub_group_by:
|
if sub_group_by:
|
||||||
if group_by == sub_group_by:
|
if group_by == sub_group_by:
|
||||||
|
|
@ -338,6 +337,7 @@ class IssueViewSet(BaseViewSet):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def create(self, request, slug, project_id):
|
def create(self, request, slug, project_id):
|
||||||
project = Project.objects.get(pk=project_id)
|
project = Project.objects.get(pk=project_id)
|
||||||
|
|
||||||
|
|
@ -412,6 +412,9 @@ class IssueViewSet(BaseViewSet):
|
||||||
return Response(issue, status=status.HTTP_201_CREATED)
|
return Response(issue, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
@allow_permission(
|
||||||
|
[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER], creator=True, model=Issue
|
||||||
|
)
|
||||||
def retrieve(self, request, slug, project_id, pk=None):
|
def retrieve(self, request, slug, project_id, pk=None):
|
||||||
issue = (
|
issue = (
|
||||||
self.get_queryset()
|
self.get_queryset()
|
||||||
|
|
@ -438,7 +441,8 @@ class IssueViewSet(BaseViewSet):
|
||||||
ArrayAgg(
|
ArrayAgg(
|
||||||
"issue_module__module_id",
|
"issue_module__module_id",
|
||||||
distinct=True,
|
distinct=True,
|
||||||
filter=~Q(issue_module__module_id__isnull=True),
|
filter=~Q(issue_module__module_id__isnull=True)
|
||||||
|
& Q(issue_module__module__archived_at__isnull=True),
|
||||||
),
|
),
|
||||||
Value([], output_field=ArrayField(UUIDField())),
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
),
|
),
|
||||||
|
|
@ -483,6 +487,7 @@ class IssueViewSet(BaseViewSet):
|
||||||
serializer = IssueDetailSerializer(issue, expand=self.expand)
|
serializer = IssueDetailSerializer(issue, expand=self.expand)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||||
def partial_update(self, request, slug, project_id, pk=None):
|
def partial_update(self, request, slug, project_id, pk=None):
|
||||||
issue = (
|
issue = (
|
||||||
self.get_queryset()
|
self.get_queryset()
|
||||||
|
|
@ -548,23 +553,11 @@ class IssueViewSet(BaseViewSet):
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
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):
|
def destroy(self, request, slug, project_id, pk=None):
|
||||||
issue = Issue.objects.get(
|
issue = Issue.objects.get(
|
||||||
workspace__slug=slug, project_id=project_id, pk=pk
|
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.delete()
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
|
|
@ -582,10 +575,8 @@ class IssueViewSet(BaseViewSet):
|
||||||
|
|
||||||
|
|
||||||
class IssueUserDisplayPropertyEndpoint(BaseAPIView):
|
class IssueUserDisplayPropertyEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
|
||||||
ProjectLitePermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST, ROLE.VIEWER])
|
||||||
def patch(self, request, slug, project_id):
|
def patch(self, request, slug, project_id):
|
||||||
issue_property = IssueUserProperty.objects.get(
|
issue_property = IssueUserProperty.objects.get(
|
||||||
user=request.user,
|
user=request.user,
|
||||||
|
|
@ -605,6 +596,7 @@ class IssueUserDisplayPropertyEndpoint(BaseAPIView):
|
||||||
serializer = IssueUserPropertySerializer(issue_property)
|
serializer = IssueUserPropertySerializer(issue_property)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST, ROLE.VIEWER])
|
||||||
def get(self, request, slug, project_id):
|
def get(self, request, slug, project_id):
|
||||||
issue_property, _ = IssueUserProperty.objects.get_or_create(
|
issue_property, _ = IssueUserProperty.objects.get_or_create(
|
||||||
user=request.user, project_id=project_id
|
user=request.user, project_id=project_id
|
||||||
|
|
@ -614,23 +606,9 @@ class IssueUserDisplayPropertyEndpoint(BaseAPIView):
|
||||||
|
|
||||||
|
|
||||||
class BulkDeleteIssuesEndpoint(BaseAPIView):
|
class BulkDeleteIssuesEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
|
||||||
ProjectEntityPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN])
|
||||||
def delete(self, request, slug, project_id):
|
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", [])
|
issue_ids = request.data.get("issue_ids", [])
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ from plane.db.models import (
|
||||||
IssueLabel,
|
IssueLabel,
|
||||||
IssueAssignee,
|
IssueAssignee,
|
||||||
)
|
)
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activities_task import issue_activity
|
||||||
|
|
||||||
|
|
||||||
class BulkIssueOperationsEndpoint(BaseAPIView):
|
class BulkIssueOperationsEndpoint(BaseAPIView):
|
||||||
|
|
@ -59,10 +59,16 @@ class BulkIssueOperationsEndpoint(BaseAPIView):
|
||||||
|
|
||||||
properties = request.data.get("properties", {})
|
properties = request.data.get("properties", {})
|
||||||
|
|
||||||
if properties.get("start_date", False) and properties.get("target_date", False):
|
if properties.get("start_date", False) and properties.get(
|
||||||
|
"target_date", False
|
||||||
|
):
|
||||||
if (
|
if (
|
||||||
datetime.strptime(properties.get("start_date"), "%Y-%m-%d").date()
|
datetime.strptime(
|
||||||
> datetime.strptime(properties.get("target_date"), "%Y-%m-%d").date()
|
properties.get("start_date"), "%Y-%m-%d"
|
||||||
|
).date()
|
||||||
|
> datetime.strptime(
|
||||||
|
properties.get("target_date"), "%Y-%m-%d"
|
||||||
|
).date()
|
||||||
):
|
):
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
|
|
@ -73,7 +79,6 @@ class BulkIssueOperationsEndpoint(BaseAPIView):
|
||||||
)
|
)
|
||||||
|
|
||||||
for issue in issues:
|
for issue in issues:
|
||||||
|
|
||||||
# Priority
|
# Priority
|
||||||
if properties.get("priority", False):
|
if properties.get("priority", False):
|
||||||
bulk_issue_activities.append(
|
bulk_issue_activities.append(
|
||||||
|
|
|
||||||
|
|
@ -16,22 +16,19 @@ from plane.app.serializers import (
|
||||||
IssueCommentSerializer,
|
IssueCommentSerializer,
|
||||||
CommentReactionSerializer,
|
CommentReactionSerializer,
|
||||||
)
|
)
|
||||||
from plane.app.permissions import ProjectLitePermission
|
from plane.app.permissions import ProjectLitePermission, allow_permission, ROLE
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
IssueComment,
|
IssueComment,
|
||||||
ProjectMember,
|
ProjectMember,
|
||||||
CommentReaction,
|
CommentReaction,
|
||||||
)
|
)
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activities_task import issue_activity
|
||||||
|
|
||||||
|
|
||||||
class IssueCommentViewSet(BaseViewSet):
|
class IssueCommentViewSet(BaseViewSet):
|
||||||
serializer_class = IssueCommentSerializer
|
serializer_class = IssueCommentSerializer
|
||||||
model = IssueComment
|
model = IssueComment
|
||||||
webhook_event = "issue_comment"
|
webhook_event = "issue_comment"
|
||||||
permission_classes = [
|
|
||||||
ProjectLitePermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
filterset_fields = [
|
filterset_fields = [
|
||||||
"issue__id",
|
"issue__id",
|
||||||
|
|
@ -66,6 +63,7 @@ class IssueCommentViewSet(BaseViewSet):
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST, ROLE.VIEWER])
|
||||||
def create(self, request, slug, project_id, issue_id):
|
def create(self, request, slug, project_id, issue_id):
|
||||||
serializer = IssueCommentSerializer(data=request.data)
|
serializer = IssueCommentSerializer(data=request.data)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
|
|
@ -90,6 +88,11 @@ class IssueCommentViewSet(BaseViewSet):
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
@allow_permission(
|
||||||
|
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER],
|
||||||
|
creator=True,
|
||||||
|
model=IssueComment,
|
||||||
|
)
|
||||||
def partial_update(self, request, slug, project_id, issue_id, pk):
|
def partial_update(self, request, slug, project_id, issue_id, pk):
|
||||||
issue_comment = IssueComment.objects.get(
|
issue_comment = IssueComment.objects.get(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
|
|
@ -121,6 +124,9 @@ class IssueCommentViewSet(BaseViewSet):
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
@allow_permission(
|
||||||
|
allowed_roles=[ROLE.ADMIN], creator=True, model=IssueComment
|
||||||
|
)
|
||||||
def destroy(self, request, slug, project_id, issue_id, pk):
|
def destroy(self, request, slug, project_id, issue_id, pk):
|
||||||
issue_comment = IssueComment.objects.get(
|
issue_comment = IssueComment.objects.get(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ from plane.app.serializers import (
|
||||||
IssueFlatSerializer,
|
IssueFlatSerializer,
|
||||||
IssueSerializer,
|
IssueSerializer,
|
||||||
)
|
)
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activities_task import issue_activity
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
Issue,
|
Issue,
|
||||||
IssueAttachment,
|
IssueAttachment,
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,7 @@ from rest_framework import status
|
||||||
# Module imports
|
# Module imports
|
||||||
from .. import BaseViewSet, BaseAPIView
|
from .. import BaseViewSet, BaseAPIView
|
||||||
from plane.app.serializers import LabelSerializer
|
from plane.app.serializers import LabelSerializer
|
||||||
from plane.app.permissions import (
|
from plane.app.permissions import allow_permission, ProjectBasePermission, ROLE
|
||||||
ProjectMemberPermission,
|
|
||||||
)
|
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
Project,
|
Project,
|
||||||
Label,
|
Label,
|
||||||
|
|
@ -25,7 +23,7 @@ class LabelViewSet(BaseViewSet):
|
||||||
serializer_class = LabelSerializer
|
serializer_class = LabelSerializer
|
||||||
model = Label
|
model = Label
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
ProjectMemberPermission,
|
ProjectBasePermission,
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
|
@ -45,6 +43,7 @@ class LabelViewSet(BaseViewSet):
|
||||||
@invalidate_cache(
|
@invalidate_cache(
|
||||||
path="/api/workspaces/:slug/labels/", url_params=True, user=False
|
path="/api/workspaces/:slug/labels/", url_params=True, user=False
|
||||||
)
|
)
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def create(self, request, slug, project_id):
|
def create(self, request, slug, project_id):
|
||||||
try:
|
try:
|
||||||
serializer = LabelSerializer(data=request.data)
|
serializer = LabelSerializer(data=request.data)
|
||||||
|
|
@ -67,17 +66,20 @@ class LabelViewSet(BaseViewSet):
|
||||||
@invalidate_cache(
|
@invalidate_cache(
|
||||||
path="/api/workspaces/:slug/labels/", url_params=True, user=False
|
path="/api/workspaces/:slug/labels/", url_params=True, user=False
|
||||||
)
|
)
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def partial_update(self, request, *args, **kwargs):
|
def partial_update(self, request, *args, **kwargs):
|
||||||
return super().partial_update(request, *args, **kwargs)
|
return super().partial_update(request, *args, **kwargs)
|
||||||
|
|
||||||
@invalidate_cache(
|
@invalidate_cache(
|
||||||
path="/api/workspaces/:slug/labels/", url_params=True, user=False
|
path="/api/workspaces/:slug/labels/", url_params=True, user=False
|
||||||
)
|
)
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def destroy(self, request, *args, **kwargs):
|
def destroy(self, request, *args, **kwargs):
|
||||||
return super().destroy(request, *args, **kwargs)
|
return super().destroy(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class BulkCreateIssueLabelsEndpoint(BaseAPIView):
|
class BulkCreateIssueLabelsEndpoint(BaseAPIView):
|
||||||
|
@allow_permission([ROLE.ADMIN])
|
||||||
def post(self, request, slug, project_id):
|
def post(self, request, slug, project_id):
|
||||||
label_data = request.data.get("label_data", [])
|
label_data = request.data.get("label_data", [])
|
||||||
project = Project.objects.get(pk=project_id)
|
project = Project.objects.get(pk=project_id)
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ from .. import BaseViewSet
|
||||||
from plane.app.serializers import IssueLinkSerializer
|
from plane.app.serializers import IssueLinkSerializer
|
||||||
from plane.app.permissions import ProjectEntityPermission
|
from plane.app.permissions import ProjectEntityPermission
|
||||||
from plane.db.models import IssueLink
|
from plane.db.models import IssueLink
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activities_task import issue_activity
|
||||||
|
|
||||||
|
|
||||||
class IssueLinkViewSet(BaseViewSet):
|
class IssueLinkViewSet(BaseViewSet):
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ from .. import BaseViewSet
|
||||||
from plane.app.serializers import IssueReactionSerializer
|
from plane.app.serializers import IssueReactionSerializer
|
||||||
from plane.app.permissions import ProjectLitePermission
|
from plane.app.permissions import ProjectLitePermission
|
||||||
from plane.db.models import IssueReaction
|
from plane.db.models import IssueReaction
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activities_task import issue_activity
|
||||||
|
|
||||||
|
|
||||||
class IssueReactionViewSet(BaseViewSet):
|
class IssueReactionViewSet(BaseViewSet):
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ from plane.db.models import (
|
||||||
IssueAttachment,
|
IssueAttachment,
|
||||||
IssueLink,
|
IssueLink,
|
||||||
)
|
)
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activities_task import issue_activity
|
||||||
|
|
||||||
|
|
||||||
class IssueRelationViewSet(BaseViewSet):
|
class IssueRelationViewSet(BaseViewSet):
|
||||||
|
|
@ -80,8 +80,7 @@ class IssueRelationViewSet(BaseViewSet):
|
||||||
).values_list("issue_id", flat=True)
|
).values_list("issue_id", flat=True)
|
||||||
|
|
||||||
queryset = (
|
queryset = (
|
||||||
Issue.issue_objects
|
Issue.issue_objects.filter(workspace__slug=slug)
|
||||||
.filter(workspace__slug=slug)
|
|
||||||
.select_related("workspace", "project", "state", "parent")
|
.select_related("workspace", "project", "state", "parent")
|
||||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ from plane.db.models import (
|
||||||
IssueLink,
|
IssueLink,
|
||||||
IssueAttachment,
|
IssueAttachment,
|
||||||
)
|
)
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activities_task import issue_activity
|
||||||
from plane.utils.user_timezone_converter import user_timezone_converter
|
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,10 @@ from rest_framework.response import Response
|
||||||
# Module imports
|
# Module imports
|
||||||
from plane.app.permissions import (
|
from plane.app.permissions import (
|
||||||
ProjectEntityPermission,
|
ProjectEntityPermission,
|
||||||
ProjectLitePermission,
|
allow_permission,
|
||||||
|
ROLE,
|
||||||
)
|
)
|
||||||
|
|
||||||
from plane.app.serializers import (
|
from plane.app.serializers import (
|
||||||
ModuleDetailSerializer,
|
ModuleDetailSerializer,
|
||||||
ModuleLinkSerializer,
|
ModuleLinkSerializer,
|
||||||
|
|
@ -39,7 +41,7 @@ from plane.app.serializers import (
|
||||||
ModuleUserPropertiesSerializer,
|
ModuleUserPropertiesSerializer,
|
||||||
ModuleWriteSerializer,
|
ModuleWriteSerializer,
|
||||||
)
|
)
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activities_task import issue_activity
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
Issue,
|
Issue,
|
||||||
Module,
|
Module,
|
||||||
|
|
@ -48,7 +50,6 @@ from plane.db.models import (
|
||||||
ModuleLink,
|
ModuleLink,
|
||||||
ModuleUserProperties,
|
ModuleUserProperties,
|
||||||
Project,
|
Project,
|
||||||
ProjectMember,
|
|
||||||
)
|
)
|
||||||
from plane.utils.analytics_plot import burndown_plot
|
from plane.utils.analytics_plot import burndown_plot
|
||||||
from plane.utils.user_timezone_converter import user_timezone_converter
|
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||||
|
|
@ -58,9 +59,6 @@ from .. import BaseAPIView, BaseViewSet
|
||||||
|
|
||||||
class ModuleViewSet(BaseViewSet):
|
class ModuleViewSet(BaseViewSet):
|
||||||
model = Module
|
model = Module
|
||||||
permission_classes = [
|
|
||||||
ProjectEntityPermission,
|
|
||||||
]
|
|
||||||
webhook_event = "module"
|
webhook_event = "module"
|
||||||
|
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
|
|
@ -318,6 +316,8 @@ class ModuleViewSet(BaseViewSet):
|
||||||
.order_by("-is_favorite", "-created_at")
|
.order_by("-is_favorite", "-created_at")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER])
|
||||||
|
|
||||||
def create(self, request, slug, project_id):
|
def create(self, request, slug, project_id):
|
||||||
project = Project.objects.get(workspace__slug=slug, pk=project_id)
|
project = Project.objects.get(workspace__slug=slug, pk=project_id)
|
||||||
serializer = ModuleWriteSerializer(
|
serializer = ModuleWriteSerializer(
|
||||||
|
|
@ -380,6 +380,8 @@ class ModuleViewSet(BaseViewSet):
|
||||||
return Response(module, status=status.HTTP_201_CREATED)
|
return Response(module, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
|
||||||
|
|
||||||
def list(self, request, slug, project_id):
|
def list(self, request, slug, project_id):
|
||||||
queryset = self.get_queryset().filter(archived_at__isnull=True)
|
queryset = self.get_queryset().filter(archived_at__isnull=True)
|
||||||
if self.fields:
|
if self.fields:
|
||||||
|
|
@ -427,6 +429,8 @@ class ModuleViewSet(BaseViewSet):
|
||||||
)
|
)
|
||||||
return Response(modules, status=status.HTTP_200_OK)
|
return Response(modules, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER])
|
||||||
|
|
||||||
def retrieve(self, request, slug, project_id, pk):
|
def retrieve(self, request, slug, project_id, pk):
|
||||||
queryset = (
|
queryset = (
|
||||||
self.get_queryset()
|
self.get_queryset()
|
||||||
|
|
@ -671,6 +675,7 @@ class ModuleViewSet(BaseViewSet):
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def partial_update(self, request, slug, project_id, pk):
|
def partial_update(self, request, slug, project_id, pk):
|
||||||
module = self.get_queryset().filter(pk=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(module, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN], creator=True, model=Module)
|
||||||
def destroy(self, request, slug, project_id, pk):
|
def destroy(self, request, slug, project_id, pk):
|
||||||
module = Module.objects.get(
|
module = Module.objects.get(
|
||||||
workspace__slug=slug, project_id=project_id, pk=pk
|
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(
|
module_issues = list(
|
||||||
ModuleIssue.objects.filter(module_id=pk).values_list(
|
ModuleIssue.objects.filter(module_id=pk).values_list(
|
||||||
"issue", flat=True
|
"issue", flat=True
|
||||||
|
|
@ -859,10 +851,8 @@ class ModuleFavoriteViewSet(BaseViewSet):
|
||||||
|
|
||||||
|
|
||||||
class ModuleUserPropertiesEndpoint(BaseAPIView):
|
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):
|
def patch(self, request, slug, project_id, module_id):
|
||||||
module_properties = ModuleUserProperties.objects.get(
|
module_properties = ModuleUserProperties.objects.get(
|
||||||
user=request.user,
|
user=request.user,
|
||||||
|
|
@ -885,6 +875,7 @@ class ModuleUserPropertiesEndpoint(BaseAPIView):
|
||||||
serializer = ModuleUserPropertiesSerializer(module_properties)
|
serializer = ModuleUserPropertiesSerializer(module_properties)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
|
||||||
def get(self, request, slug, project_id, module_id):
|
def get(self, request, slug, project_id, module_id):
|
||||||
module_properties, _ = ModuleUserProperties.objects.get_or_create(
|
module_properties, _ = ModuleUserProperties.objects.get_or_create(
|
||||||
user=request.user,
|
user=request.user,
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,11 @@ from django.views.decorators.gzip import gzip_page
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from plane.app.permissions import (
|
from plane.app.permissions import allow_permission, ROLE
|
||||||
ProjectEntityPermission,
|
|
||||||
)
|
|
||||||
from plane.app.serializers import (
|
from plane.app.serializers import (
|
||||||
ModuleIssueSerializer,
|
ModuleIssueSerializer,
|
||||||
)
|
)
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activities_task import issue_activity
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
Issue,
|
Issue,
|
||||||
IssueAttachment,
|
IssueAttachment,
|
||||||
|
|
@ -46,6 +44,7 @@ from plane.utils.paginator import (
|
||||||
# Module imports
|
# Module imports
|
||||||
from .. import BaseViewSet
|
from .. import BaseViewSet
|
||||||
|
|
||||||
|
|
||||||
class ModuleIssueViewSet(BaseViewSet):
|
class ModuleIssueViewSet(BaseViewSet):
|
||||||
serializer_class = ModuleIssueSerializer
|
serializer_class = ModuleIssueSerializer
|
||||||
model = ModuleIssue
|
model = ModuleIssue
|
||||||
|
|
@ -57,10 +56,6 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||||
"issue__assignees__id",
|
"issue__assignees__id",
|
||||||
]
|
]
|
||||||
|
|
||||||
permission_classes = [
|
|
||||||
ProjectEntityPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return (
|
return (
|
||||||
Issue.issue_objects.filter(
|
Issue.issue_objects.filter(
|
||||||
|
|
@ -96,6 +91,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
@method_decorator(gzip_page)
|
@method_decorator(gzip_page)
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER])
|
||||||
def list(self, request, slug, project_id, module_id):
|
def list(self, request, slug, project_id, module_id):
|
||||||
filters = issue_filters(request.query_params, "GET")
|
filters = issue_filters(request.query_params, "GET")
|
||||||
issue_queryset = self.get_queryset().filter(**filters)
|
issue_queryset = self.get_queryset().filter(**filters)
|
||||||
|
|
@ -203,6 +199,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
# create multiple issues inside a module
|
# create multiple issues inside a module
|
||||||
def create_module_issues(self, request, slug, project_id, module_id):
|
def create_module_issues(self, request, slug, project_id, module_id):
|
||||||
issues = request.data.get("issues", [])
|
issues = request.data.get("issues", [])
|
||||||
|
|
@ -244,6 +241,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||||
]
|
]
|
||||||
return Response({"message": "success"}, status=status.HTTP_201_CREATED)
|
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
|
# add multiple module inside an issue and remove multiple modules from an issue
|
||||||
def create_issue_modules(self, request, slug, project_id, issue_id):
|
def create_issue_modules(self, request, slug, project_id, issue_id):
|
||||||
modules = request.data.get("modules", [])
|
modules = request.data.get("modules", [])
|
||||||
|
|
@ -306,6 +304,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||||
|
|
||||||
return Response({"message": "success"}, status=status.HTTP_201_CREATED)
|
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):
|
def destroy(self, request, slug, project_id, module_id, issue_id):
|
||||||
module_issue = ModuleIssue.objects.filter(
|
module_issue = ModuleIssue.objects.filter(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ from plane.db.models import (
|
||||||
WorkspaceMember,
|
WorkspaceMember,
|
||||||
)
|
)
|
||||||
from plane.utils.paginator import BasePaginator
|
from plane.utils.paginator import BasePaginator
|
||||||
|
from plane.app.permissions import allow_permission, ROLE
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from ..base import BaseAPIView, BaseViewSet
|
from ..base import BaseAPIView, BaseViewSet
|
||||||
|
|
@ -39,6 +40,10 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||||
.select_related("workspace", "project," "triggered_by", "receiver")
|
.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):
|
def list(self, request, slug):
|
||||||
# Get query parameters
|
# Get query parameters
|
||||||
snoozed = request.GET.get("snoozed", "false")
|
snoozed = request.GET.get("snoozed", "false")
|
||||||
|
|
@ -168,6 +173,10 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||||
serializer = NotificationSerializer(notifications, many=True)
|
serializer = NotificationSerializer(notifications, many=True)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
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):
|
def partial_update(self, request, slug, pk):
|
||||||
notification = Notification.objects.get(
|
notification = Notification.objects.get(
|
||||||
workspace__slug=slug, pk=pk, receiver=request.user
|
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.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
@allow_permission(
|
||||||
|
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE"
|
||||||
|
)
|
||||||
def mark_read(self, request, slug, pk):
|
def mark_read(self, request, slug, pk):
|
||||||
notification = Notification.objects.get(
|
notification = Notification.objects.get(
|
||||||
receiver=request.user, workspace__slug=slug, pk=pk
|
receiver=request.user, workspace__slug=slug, pk=pk
|
||||||
|
|
@ -194,6 +206,9 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||||
serializer = NotificationSerializer(notification)
|
serializer = NotificationSerializer(notification)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
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):
|
def mark_unread(self, request, slug, pk):
|
||||||
notification = Notification.objects.get(
|
notification = Notification.objects.get(
|
||||||
receiver=request.user, workspace__slug=slug, pk=pk
|
receiver=request.user, workspace__slug=slug, pk=pk
|
||||||
|
|
@ -203,6 +218,9 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||||
serializer = NotificationSerializer(notification)
|
serializer = NotificationSerializer(notification)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
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):
|
def archive(self, request, slug, pk):
|
||||||
notification = Notification.objects.get(
|
notification = Notification.objects.get(
|
||||||
receiver=request.user, workspace__slug=slug, pk=pk
|
receiver=request.user, workspace__slug=slug, pk=pk
|
||||||
|
|
@ -212,6 +230,9 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||||
serializer = NotificationSerializer(notification)
|
serializer = NotificationSerializer(notification)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
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):
|
def unarchive(self, request, slug, pk):
|
||||||
notification = Notification.objects.get(
|
notification = Notification.objects.get(
|
||||||
receiver=request.user, workspace__slug=slug, pk=pk
|
receiver=request.user, workspace__slug=slug, pk=pk
|
||||||
|
|
@ -223,6 +244,11 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||||
|
|
||||||
|
|
||||||
class UnreadNotificationEndpoint(BaseAPIView):
|
class UnreadNotificationEndpoint(BaseAPIView):
|
||||||
|
|
||||||
|
@allow_permission(
|
||||||
|
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST],
|
||||||
|
level="WORKSPACE",
|
||||||
|
)
|
||||||
def get(self, request, slug):
|
def get(self, request, slug):
|
||||||
# Watching Issues Count
|
# Watching Issues Count
|
||||||
unread_notifications_count = (
|
unread_notifications_count = (
|
||||||
|
|
@ -260,6 +286,10 @@ class UnreadNotificationEndpoint(BaseAPIView):
|
||||||
|
|
||||||
|
|
||||||
class MarkAllReadNotificationViewSet(BaseViewSet):
|
class MarkAllReadNotificationViewSet(BaseViewSet):
|
||||||
|
|
||||||
|
@allow_permission(
|
||||||
|
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE"
|
||||||
|
)
|
||||||
def create(self, request, slug):
|
def create(self, request, slug):
|
||||||
snoozed = request.data.get("snoozed", False)
|
snoozed = request.data.get("snoozed", False)
|
||||||
archived = request.data.get("archived", False)
|
archived = request.data.get("archived", False)
|
||||||
|
|
@ -343,6 +373,9 @@ class UserNotificationPreferenceEndpoint(BaseAPIView):
|
||||||
serializer_class = UserNotificationPreferenceSerializer
|
serializer_class = UserNotificationPreferenceSerializer
|
||||||
|
|
||||||
# request the object
|
# request the object
|
||||||
|
@allow_permission(
|
||||||
|
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||||
|
)
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
user_notification_preference = UserNotificationPreference.objects.get(
|
user_notification_preference = UserNotificationPreference.objects.get(
|
||||||
user=request.user
|
user=request.user
|
||||||
|
|
@ -353,6 +386,9 @@ class UserNotificationPreferenceEndpoint(BaseAPIView):
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
# update the object
|
# update the object
|
||||||
|
@allow_permission(
|
||||||
|
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||||
|
)
|
||||||
def patch(self, request):
|
def patch(self, request):
|
||||||
user_notification_preference = UserNotificationPreference.objects.get(
|
user_notification_preference = UserNotificationPreference.objects.get(
|
||||||
user=request.user
|
user=request.user
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ from rest_framework import status
|
||||||
from rest_framework.response import Response
|
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 (
|
from plane.app.serializers import (
|
||||||
PageLogSerializer,
|
PageLogSerializer,
|
||||||
PageSerializer,
|
PageSerializer,
|
||||||
|
|
@ -60,9 +60,6 @@ def unarchive_archive_page_and_descendants(page_id, archived_at):
|
||||||
class PageViewSet(BaseViewSet):
|
class PageViewSet(BaseViewSet):
|
||||||
serializer_class = PageSerializer
|
serializer_class = PageSerializer
|
||||||
model = Page
|
model = Page
|
||||||
permission_classes = [
|
|
||||||
ProjectEntityPermission,
|
|
||||||
]
|
|
||||||
search_fields = [
|
search_fields = [
|
||||||
"name",
|
"name",
|
||||||
]
|
]
|
||||||
|
|
@ -122,6 +119,7 @@ class PageViewSet(BaseViewSet):
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def create(self, request, slug, project_id):
|
def create(self, request, slug, project_id):
|
||||||
serializer = PageSerializer(
|
serializer = PageSerializer(
|
||||||
data=request.data,
|
data=request.data,
|
||||||
|
|
@ -143,6 +141,7 @@ class PageViewSet(BaseViewSet):
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def partial_update(self, request, slug, project_id, pk):
|
def partial_update(self, request, slug, project_id, pk):
|
||||||
try:
|
try:
|
||||||
page = Page.objects.get(
|
page = Page.objects.get(
|
||||||
|
|
@ -208,6 +207,7 @@ class PageViewSet(BaseViewSet):
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER])
|
||||||
def retrieve(self, request, slug, project_id, pk=None):
|
def retrieve(self, request, slug, project_id, pk=None):
|
||||||
page = self.get_queryset().filter(pk=pk).first()
|
page = self.get_queryset().filter(pk=pk).first()
|
||||||
if page is None:
|
if page is None:
|
||||||
|
|
@ -226,6 +226,7 @@ class PageViewSet(BaseViewSet):
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def lock(self, request, slug, project_id, pk):
|
def lock(self, request, slug, project_id, pk):
|
||||||
page = Page.objects.filter(
|
page = Page.objects.filter(
|
||||||
pk=pk, workspace__slug=slug, projects__id=project_id
|
pk=pk, workspace__slug=slug, projects__id=project_id
|
||||||
|
|
@ -235,6 +236,7 @@ class PageViewSet(BaseViewSet):
|
||||||
page.save()
|
page.save()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def unlock(self, request, slug, project_id, pk):
|
def unlock(self, request, slug, project_id, pk):
|
||||||
page = Page.objects.filter(
|
page = Page.objects.filter(
|
||||||
pk=pk, workspace__slug=slug, projects__id=project_id
|
pk=pk, workspace__slug=slug, projects__id=project_id
|
||||||
|
|
@ -245,6 +247,7 @@ class PageViewSet(BaseViewSet):
|
||||||
|
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def access(self, request, slug, project_id, pk):
|
def access(self, request, slug, project_id, pk):
|
||||||
access = request.data.get("access", 0)
|
access = request.data.get("access", 0)
|
||||||
page = Page.objects.filter(
|
page = Page.objects.filter(
|
||||||
|
|
@ -267,11 +270,13 @@ class PageViewSet(BaseViewSet):
|
||||||
page.save()
|
page.save()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER])
|
||||||
def list(self, request, slug, project_id):
|
def list(self, request, slug, project_id):
|
||||||
queryset = self.get_queryset()
|
queryset = self.get_queryset()
|
||||||
pages = PageSerializer(queryset, many=True).data
|
pages = PageSerializer(queryset, many=True).data
|
||||||
return Response(pages, status=status.HTTP_200_OK)
|
return Response(pages, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def archive(self, request, slug, project_id, pk):
|
def archive(self, request, slug, project_id, pk):
|
||||||
page = Page.objects.get(
|
page = Page.objects.get(
|
||||||
pk=pk, workspace__slug=slug, projects__id=project_id
|
pk=pk, workspace__slug=slug, projects__id=project_id
|
||||||
|
|
@ -299,6 +304,7 @@ class PageViewSet(BaseViewSet):
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def unarchive(self, request, slug, project_id, pk):
|
def unarchive(self, request, slug, project_id, pk):
|
||||||
page = Page.objects.get(
|
page = Page.objects.get(
|
||||||
pk=pk, workspace__slug=slug, projects__id=project_id
|
pk=pk, workspace__slug=slug, projects__id=project_id
|
||||||
|
|
@ -328,6 +334,7 @@ class PageViewSet(BaseViewSet):
|
||||||
|
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN], creator=True, model=Page)
|
||||||
def destroy(self, request, slug, project_id, pk):
|
def destroy(self, request, slug, project_id, pk):
|
||||||
page = Page.objects.get(
|
page = Page.objects.get(
|
||||||
pk=pk, workspace__slug=slug, projects__id=project_id
|
pk=pk, workspace__slug=slug, projects__id=project_id
|
||||||
|
|
@ -370,12 +377,10 @@ class PageViewSet(BaseViewSet):
|
||||||
|
|
||||||
|
|
||||||
class PageFavoriteViewSet(BaseViewSet):
|
class PageFavoriteViewSet(BaseViewSet):
|
||||||
permission_classes = [
|
|
||||||
ProjectEntityPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
model = UserFavorite
|
model = UserFavorite
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def create(self, request, slug, project_id, pk):
|
def create(self, request, slug, project_id, pk):
|
||||||
_ = UserFavorite.objects.create(
|
_ = UserFavorite.objects.create(
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
|
|
@ -385,6 +390,7 @@ class PageFavoriteViewSet(BaseViewSet):
|
||||||
)
|
)
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def destroy(self, request, slug, project_id, pk):
|
def destroy(self, request, slug, project_id, pk):
|
||||||
page_favorite = UserFavorite.objects.get(
|
page_favorite = UserFavorite.objects.get(
|
||||||
project=project_id,
|
project=project_id,
|
||||||
|
|
@ -398,9 +404,6 @@ class PageFavoriteViewSet(BaseViewSet):
|
||||||
|
|
||||||
|
|
||||||
class PageLogEndpoint(BaseAPIView):
|
class PageLogEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
|
||||||
ProjectEntityPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
serializer_class = PageLogSerializer
|
serializer_class = PageLogSerializer
|
||||||
model = PageLog
|
model = PageLog
|
||||||
|
|
@ -440,9 +443,6 @@ class PageLogEndpoint(BaseAPIView):
|
||||||
|
|
||||||
|
|
||||||
class SubPagesEndpoint(BaseAPIView):
|
class SubPagesEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
|
||||||
ProjectEntityPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
@method_decorator(gzip_page)
|
@method_decorator(gzip_page)
|
||||||
def get(self, request, slug, project_id, page_id):
|
def get(self, request, slug, project_id, page_id):
|
||||||
|
|
@ -461,10 +461,8 @@ class SubPagesEndpoint(BaseAPIView):
|
||||||
|
|
||||||
|
|
||||||
class PagesDescriptionViewSet(BaseViewSet):
|
class PagesDescriptionViewSet(BaseViewSet):
|
||||||
permission_classes = [
|
|
||||||
ProjectEntityPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER])
|
||||||
def retrieve(self, request, slug, project_id, pk):
|
def retrieve(self, request, slug, project_id, pk):
|
||||||
page = (
|
page = (
|
||||||
Page.objects.filter(
|
Page.objects.filter(
|
||||||
|
|
@ -489,6 +487,7 @@ class PagesDescriptionViewSet(BaseViewSet):
|
||||||
)
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
|
||||||
def partial_update(self, request, slug, project_id, pk):
|
def partial_update(self, request, slug, project_id, pk):
|
||||||
page = (
|
page = (
|
||||||
Page.objects.filter(
|
Page.objects.filter(
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,9 @@ from plane.app.serializers import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from plane.app.permissions import (
|
from plane.app.permissions import (
|
||||||
ProjectBasePermission,
|
|
||||||
ProjectMemberPermission,
|
ProjectMemberPermission,
|
||||||
|
allow_permission,
|
||||||
|
ROLE,
|
||||||
)
|
)
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
UserFavorite,
|
UserFavorite,
|
||||||
|
|
@ -47,6 +48,7 @@ from plane.db.models import (
|
||||||
ProjectMember,
|
ProjectMember,
|
||||||
State,
|
State,
|
||||||
Workspace,
|
Workspace,
|
||||||
|
WorkspaceMember,
|
||||||
)
|
)
|
||||||
from plane.utils.cache import cache_response
|
from plane.utils.cache import cache_response
|
||||||
from plane.bgtasks.webhook_task import model_activity
|
from plane.bgtasks.webhook_task import model_activity
|
||||||
|
|
@ -57,10 +59,6 @@ class ProjectViewSet(BaseViewSet):
|
||||||
model = Project
|
model = Project
|
||||||
webhook_event = "project"
|
webhook_event = "project"
|
||||||
|
|
||||||
permission_classes = [
|
|
||||||
ProjectBasePermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
sort_order = ProjectMember.objects.filter(
|
sort_order = ProjectMember.objects.filter(
|
||||||
member=self.request.user,
|
member=self.request.user,
|
||||||
|
|
@ -155,6 +153,10 @@ class ProjectViewSet(BaseViewSet):
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@allow_permission(
|
||||||
|
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST],
|
||||||
|
level="WORKSPACE",
|
||||||
|
)
|
||||||
def list(self, request, slug):
|
def list(self, request, slug):
|
||||||
fields = [
|
fields = [
|
||||||
field
|
field
|
||||||
|
|
@ -173,11 +175,27 @@ class ProjectViewSet(BaseViewSet):
|
||||||
projects, many=True
|
projects, many=True
|
||||||
).data,
|
).data,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if WorkspaceMember.objects.filter(
|
||||||
|
member=request.user,
|
||||||
|
workspace__slug=slug,
|
||||||
|
is_active=True,
|
||||||
|
role__in=[5, 10],
|
||||||
|
).exists():
|
||||||
|
projects = projects.filter(
|
||||||
|
project_projectmember__member=self.request.user,
|
||||||
|
project_projectmember__is_active=True,
|
||||||
|
)
|
||||||
|
|
||||||
projects = ProjectListSerializer(
|
projects = ProjectListSerializer(
|
||||||
projects, many=True, fields=fields if fields else None
|
projects, many=True, fields=fields if fields else None
|
||||||
).data
|
).data
|
||||||
return Response(projects, status=status.HTTP_200_OK)
|
return Response(projects, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
@allow_permission(
|
||||||
|
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST],
|
||||||
|
level="WORKSPACE",
|
||||||
|
)
|
||||||
def retrieve(self, request, slug, pk):
|
def retrieve(self, request, slug, pk):
|
||||||
project = (
|
project = (
|
||||||
self.get_queryset()
|
self.get_queryset()
|
||||||
|
|
@ -249,6 +267,7 @@ class ProjectViewSet(BaseViewSet):
|
||||||
serializer = ProjectListSerializer(project)
|
serializer = ProjectListSerializer(project)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
|
||||||
def create(self, request, slug):
|
def create(self, request, slug):
|
||||||
try:
|
try:
|
||||||
workspace = Workspace.objects.get(slug=slug)
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
|
|
@ -378,6 +397,7 @@ class ProjectViewSet(BaseViewSet):
|
||||||
status=status.HTTP_410_GONE,
|
status=status.HTTP_410_GONE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
|
||||||
def partial_update(self, request, slug, pk=None):
|
def partial_update(self, request, slug, pk=None):
|
||||||
try:
|
try:
|
||||||
workspace = Workspace.objects.get(slug=slug)
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
|
|
@ -459,10 +479,7 @@ class ProjectViewSet(BaseViewSet):
|
||||||
|
|
||||||
class ProjectArchiveUnarchiveEndpoint(BaseAPIView):
|
class ProjectArchiveUnarchiveEndpoint(BaseAPIView):
|
||||||
|
|
||||||
permission_classes = [
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
ProjectBasePermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
def post(self, request, slug, project_id):
|
def post(self, request, slug, project_id):
|
||||||
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
||||||
project.archived_at = timezone.now()
|
project.archived_at = timezone.now()
|
||||||
|
|
@ -472,6 +489,7 @@ class ProjectArchiveUnarchiveEndpoint(BaseAPIView):
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def delete(self, request, slug, project_id):
|
def delete(self, request, slug, project_id):
|
||||||
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
||||||
project.archived_at = None
|
project.archived_at = None
|
||||||
|
|
@ -480,10 +498,7 @@ class ProjectArchiveUnarchiveEndpoint(BaseAPIView):
|
||||||
|
|
||||||
|
|
||||||
class ProjectIdentifierEndpoint(BaseAPIView):
|
class ProjectIdentifierEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
|
||||||
ProjectBasePermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
def get(self, request, slug):
|
def get(self, request, slug):
|
||||||
name = request.GET.get("name", "").strip().upper()
|
name = request.GET.get("name", "").strip().upper()
|
||||||
|
|
||||||
|
|
@ -502,6 +517,7 @@ class ProjectIdentifierEndpoint(BaseAPIView):
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
|
||||||
def delete(self, request, slug):
|
def delete(self, request, slug):
|
||||||
name = request.data.get("name", "").strip().upper()
|
name = request.data.get("name", "").strip().upper()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,17 +23,16 @@ from plane.db.models import (
|
||||||
Workspace,
|
Workspace,
|
||||||
TeamMember,
|
TeamMember,
|
||||||
IssueUserProperty,
|
IssueUserProperty,
|
||||||
|
WorkspaceMember,
|
||||||
)
|
)
|
||||||
from plane.bgtasks.project_add_user_email_task import project_add_user_email
|
from plane.bgtasks.project_add_user_email_task import project_add_user_email
|
||||||
from plane.utils.host import base_host
|
from plane.utils.host import base_host
|
||||||
|
from plane.app.permissions.base import allow_permission, ROLE
|
||||||
|
|
||||||
|
|
||||||
class ProjectMemberViewSet(BaseViewSet):
|
class ProjectMemberViewSet(BaseViewSet):
|
||||||
serializer_class = ProjectMemberAdminSerializer
|
serializer_class = ProjectMemberAdminSerializer
|
||||||
model = ProjectMember
|
model = ProjectMember
|
||||||
permission_classes = [
|
|
||||||
ProjectMemberPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_permissions(self):
|
def get_permissions(self):
|
||||||
if self.action == "leave":
|
if self.action == "leave":
|
||||||
|
|
@ -65,6 +64,7 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||||
.select_related("workspace", "workspace__owner")
|
.select_related("workspace", "workspace__owner")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN])
|
||||||
def create(self, request, slug, project_id):
|
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
|
# 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", [])
|
members = request.data.get("members", [])
|
||||||
|
|
@ -88,6 +88,23 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||||
member.get("member_id"): member.get("role") for member in members
|
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
|
# 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(
|
for project_member in ProjectMember.objects.filter(
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
|
|
@ -172,6 +189,7 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||||
# Return the serialized data
|
# Return the serialized data
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
|
||||||
def list(self, request, slug, project_id):
|
def list(self, request, slug, project_id):
|
||||||
# Get the list of project members for the project
|
# Get the list of project members for the project
|
||||||
project_members = ProjectMember.objects.filter(
|
project_members = ProjectMember.objects.filter(
|
||||||
|
|
@ -186,6 +204,7 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||||
)
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN])
|
||||||
def partial_update(self, request, slug, project_id, pk):
|
def partial_update(self, request, slug, project_id, pk):
|
||||||
project_member = ProjectMember.objects.get(
|
project_member = ProjectMember.objects.get(
|
||||||
pk=pk,
|
pk=pk,
|
||||||
|
|
@ -205,6 +224,22 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||||
member=request.user,
|
member=request.user,
|
||||||
is_active=True,
|
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 (
|
if (
|
||||||
"role" in request.data
|
"role" in request.data
|
||||||
and int(request.data.get("role", project_member.role))
|
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.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def destroy(self, request, slug, project_id, pk):
|
def destroy(self, request, slug, project_id, pk):
|
||||||
project_member = ProjectMember.objects.get(
|
project_member = ProjectMember.objects.get(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
|
|
@ -262,6 +298,7 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||||
project_member.save()
|
project_member.save()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST])
|
||||||
def leave(self, request, slug, project_id):
|
def leave(self, request, slug, project_id):
|
||||||
project_member = ProjectMember.objects.get(
|
project_member = ProjectMember.objects.get(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,7 @@ from rest_framework.response import Response
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .base import BaseAPIView
|
from .base import BaseAPIView
|
||||||
from plane.db.models import (
|
from plane.db.models import Issue, ProjectMember
|
||||||
Issue,
|
|
||||||
)
|
|
||||||
from plane.utils.issue_search import search_issues
|
from plane.utils.issue_search import search_issues
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -76,6 +74,16 @@ class IssueSearchEndpoint(BaseAPIView):
|
||||||
if target_date == "none":
|
if target_date == "none":
|
||||||
issues = issues.filter(target_date__isnull=True)
|
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(
|
return Response(
|
||||||
issues.values(
|
issues.values(
|
||||||
"name",
|
"name",
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,8 @@ from django.db import transaction
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from plane.app.permissions import (
|
from plane.app.permissions import (
|
||||||
ProjectEntityPermission,
|
allow_permission,
|
||||||
WorkspaceEntityPermission,
|
ROLE,
|
||||||
)
|
)
|
||||||
from plane.app.serializers import (
|
from plane.app.serializers import (
|
||||||
IssueViewSerializer,
|
IssueViewSerializer,
|
||||||
|
|
@ -58,9 +58,6 @@ from plane.db.models import (
|
||||||
class WorkspaceViewViewSet(BaseViewSet):
|
class WorkspaceViewViewSet(BaseViewSet):
|
||||||
serializer_class = IssueViewSerializer
|
serializer_class = IssueViewSerializer
|
||||||
model = IssueView
|
model = IssueView
|
||||||
permission_classes = [
|
|
||||||
WorkspaceEntityPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
workspace = Workspace.objects.get(slug=self.kwargs.get("slug"))
|
workspace = Workspace.objects.get(slug=self.kwargs.get("slug"))
|
||||||
|
|
@ -78,6 +75,32 @@ class WorkspaceViewViewSet(BaseViewSet):
|
||||||
.distinct()
|
.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):
|
def partial_update(self, request, slug, pk):
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
workspace_view = IssueView.objects.select_for_update().get(
|
workspace_view = IssueView.objects.select_for_update().get(
|
||||||
|
|
@ -111,6 +134,12 @@ class WorkspaceViewViewSet(BaseViewSet):
|
||||||
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
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):
|
def destroy(self, request, slug, pk):
|
||||||
workspace_view = IssueView.objects.get(
|
workspace_view = IssueView.objects.get(
|
||||||
pk=pk,
|
pk=pk,
|
||||||
|
|
@ -157,10 +186,6 @@ class WorkspaceViewViewSet(BaseViewSet):
|
||||||
|
|
||||||
|
|
||||||
class WorkspaceViewIssuesViewSet(BaseViewSet):
|
class WorkspaceViewIssuesViewSet(BaseViewSet):
|
||||||
permission_classes = [
|
|
||||||
WorkspaceEntityPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return (
|
return (
|
||||||
Issue.issue_objects.annotate(
|
Issue.issue_objects.annotate(
|
||||||
|
|
@ -223,7 +248,8 @@ class WorkspaceViewIssuesViewSet(BaseViewSet):
|
||||||
ArrayAgg(
|
ArrayAgg(
|
||||||
"issue_module__module_id",
|
"issue_module__module_id",
|
||||||
distinct=True,
|
distinct=True,
|
||||||
filter=~Q(issue_module__module_id__isnull=True),
|
filter=~Q(issue_module__module_id__isnull=True)
|
||||||
|
& Q(issue_module__module__archived_at__isnull=True),
|
||||||
),
|
),
|
||||||
Value([], output_field=ArrayField(UUIDField())),
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
),
|
),
|
||||||
|
|
@ -231,6 +257,10 @@ class WorkspaceViewIssuesViewSet(BaseViewSet):
|
||||||
)
|
)
|
||||||
|
|
||||||
@method_decorator(gzip_page)
|
@method_decorator(gzip_page)
|
||||||
|
@allow_permission(
|
||||||
|
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST],
|
||||||
|
level="WORKSPACE",
|
||||||
|
)
|
||||||
def list(self, request, slug):
|
def list(self, request, slug):
|
||||||
filters = issue_filters(request.query_params, "GET")
|
filters = issue_filters(request.query_params, "GET")
|
||||||
order_by_param = request.GET.get("order_by", "-created_at")
|
order_by_param = request.GET.get("order_by", "-created_at")
|
||||||
|
|
@ -241,6 +271,16 @@ class WorkspaceViewIssuesViewSet(BaseViewSet):
|
||||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
.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
|
||||||
issue_queryset, order_by_param = order_issue_queryset(
|
issue_queryset, order_by_param = order_issue_queryset(
|
||||||
issue_queryset=issue_queryset,
|
issue_queryset=issue_queryset,
|
||||||
|
|
@ -347,9 +387,6 @@ class WorkspaceViewIssuesViewSet(BaseViewSet):
|
||||||
class IssueViewViewSet(BaseViewSet):
|
class IssueViewViewSet(BaseViewSet):
|
||||||
serializer_class = IssueViewSerializer
|
serializer_class = IssueViewSerializer
|
||||||
model = IssueView
|
model = IssueView
|
||||||
permission_classes = [
|
|
||||||
ProjectEntityPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
serializer.save(
|
serializer.save(
|
||||||
|
|
@ -383,8 +420,20 @@ class IssueViewViewSet(BaseViewSet):
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
allow_permission(
|
||||||
|
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST]
|
||||||
|
)
|
||||||
|
|
||||||
def list(self, request, slug, project_id):
|
def list(self, request, slug, project_id):
|
||||||
queryset = self.get_queryset()
|
queryset = self.get_queryset()
|
||||||
|
if ProjectMember.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
member=request.user,
|
||||||
|
role=5,
|
||||||
|
is_active=True,
|
||||||
|
).exists():
|
||||||
|
queryset = queryset.filter(owned_by=request.user)
|
||||||
fields = [
|
fields = [
|
||||||
field
|
field
|
||||||
for field in request.GET.get("fields", "").split(",")
|
for field in request.GET.get("fields", "").split(",")
|
||||||
|
|
@ -395,6 +444,8 @@ class IssueViewViewSet(BaseViewSet):
|
||||||
).data
|
).data
|
||||||
return Response(views, status=status.HTTP_200_OK)
|
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):
|
def partial_update(self, request, slug, project_id, pk):
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
issue_view = IssueView.objects.select_for_update().get(
|
issue_view = IssueView.objects.select_for_update().get(
|
||||||
|
|
@ -427,6 +478,8 @@ class IssueViewViewSet(BaseViewSet):
|
||||||
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
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):
|
def destroy(self, request, slug, project_id, pk):
|
||||||
project_view = IssueView.objects.get(
|
project_view = IssueView.objects.get(
|
||||||
pk=pk,
|
pk=pk,
|
||||||
|
|
@ -471,6 +524,8 @@ class IssueViewFavoriteViewSet(BaseViewSet):
|
||||||
.select_related("view")
|
.select_related("view")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
|
|
||||||
def create(self, request, slug, project_id):
|
def create(self, request, slug, project_id):
|
||||||
_ = UserFavorite.objects.create(
|
_ = UserFavorite.objects.create(
|
||||||
user=request.user,
|
user=request.user,
|
||||||
|
|
@ -480,6 +535,8 @@ class IssueViewFavoriteViewSet(BaseViewSet):
|
||||||
)
|
)
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, view_id):
|
def destroy(self, request, slug, project_id, view_id):
|
||||||
view_favorite = UserFavorite.objects.get(
|
view_favorite = UserFavorite.objects.get(
|
||||||
project=project_id,
|
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 import Webhook, WebhookLog, Workspace
|
||||||
from plane.db.models.webhook import generate_token
|
from plane.db.models.webhook import generate_token
|
||||||
from ..base import BaseAPIView
|
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
|
from plane.app.serializers import WebhookSerializer, WebhookLogSerializer
|
||||||
|
|
||||||
|
|
||||||
class WebhookEndpoint(BaseAPIView):
|
class WebhookEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
|
||||||
WorkspaceOwnerPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
|
||||||
def post(self, request, slug):
|
def post(self, request, slug):
|
||||||
workspace = Workspace.objects.get(slug=slug)
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
try:
|
try:
|
||||||
|
|
@ -40,6 +38,7 @@ class WebhookEndpoint(BaseAPIView):
|
||||||
)
|
)
|
||||||
raise IntegrityError
|
raise IntegrityError
|
||||||
|
|
||||||
|
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
|
||||||
def get(self, request, slug, pk=None):
|
def get(self, request, slug, pk=None):
|
||||||
if pk is None:
|
if pk is None:
|
||||||
webhooks = Webhook.objects.filter(workspace__slug=slug)
|
webhooks = Webhook.objects.filter(workspace__slug=slug)
|
||||||
|
|
@ -79,6 +78,7 @@ class WebhookEndpoint(BaseAPIView):
|
||||||
)
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
|
||||||
def patch(self, request, slug, pk):
|
def patch(self, request, slug, pk):
|
||||||
webhook = Webhook.objects.get(workspace__slug=slug, pk=pk)
|
webhook = Webhook.objects.get(workspace__slug=slug, pk=pk)
|
||||||
serializer = WebhookSerializer(
|
serializer = WebhookSerializer(
|
||||||
|
|
@ -104,6 +104,7 @@ class WebhookEndpoint(BaseAPIView):
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
|
||||||
def delete(self, request, slug, pk):
|
def delete(self, request, slug, pk):
|
||||||
webhook = Webhook.objects.get(pk=pk, workspace__slug=slug)
|
webhook = Webhook.objects.get(pk=pk, workspace__slug=slug)
|
||||||
webhook.delete()
|
webhook.delete()
|
||||||
|
|
@ -111,10 +112,8 @@ class WebhookEndpoint(BaseAPIView):
|
||||||
|
|
||||||
|
|
||||||
class WebhookSecretRegenerateEndpoint(BaseAPIView):
|
class WebhookSecretRegenerateEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
|
||||||
WorkspaceOwnerPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
|
||||||
def post(self, request, slug, pk):
|
def post(self, request, slug, pk):
|
||||||
webhook = Webhook.objects.get(workspace__slug=slug, pk=pk)
|
webhook = Webhook.objects.get(workspace__slug=slug, pk=pk)
|
||||||
webhook.secret_key = generate_token()
|
webhook.secret_key = generate_token()
|
||||||
|
|
@ -124,10 +123,8 @@ class WebhookSecretRegenerateEndpoint(BaseAPIView):
|
||||||
|
|
||||||
|
|
||||||
class WebhookLogsEndpoint(BaseAPIView):
|
class WebhookLogsEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
|
||||||
WorkspaceOwnerPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
|
||||||
def get(self, request, slug, webhook_id):
|
def get(self, request, slug, webhook_id):
|
||||||
webhook_logs = WebhookLog.objects.filter(
|
webhook_logs = WebhookLog.objects.filter(
|
||||||
workspace__slug=slug, webhook_id=webhook_id
|
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.app.views.base import BaseAPIView
|
||||||
from plane.db.models import UserFavorite, Workspace
|
from plane.db.models import UserFavorite, Workspace
|
||||||
from plane.app.serializers import UserFavoriteSerializer
|
from plane.app.serializers import UserFavoriteSerializer
|
||||||
from plane.app.permissions import WorkspaceEntityPermission
|
from plane.app.permissions import allow_permission, ROLE
|
||||||
|
|
||||||
|
|
||||||
class WorkspaceFavoriteEndpoint(BaseAPIView):
|
class WorkspaceFavoriteEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
|
||||||
WorkspaceEntityPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
@allow_permission(
|
||||||
|
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE"
|
||||||
|
)
|
||||||
def get(self, request, slug):
|
def get(self, request, slug):
|
||||||
# the second filter is to check if the user is a member of the project
|
# the second filter is to check if the user is a member of the project
|
||||||
favorites = UserFavorite.objects.filter(
|
favorites = UserFavorite.objects.filter(
|
||||||
|
|
@ -34,6 +34,9 @@ class WorkspaceFavoriteEndpoint(BaseAPIView):
|
||||||
serializer = UserFavoriteSerializer(favorites, many=True)
|
serializer = UserFavoriteSerializer(favorites, many=True)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
@allow_permission(
|
||||||
|
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE"
|
||||||
|
)
|
||||||
def post(self, request, slug):
|
def post(self, request, slug):
|
||||||
workspace = Workspace.objects.get(slug=slug)
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
serializer = UserFavoriteSerializer(data=request.data)
|
serializer = UserFavoriteSerializer(data=request.data)
|
||||||
|
|
@ -46,6 +49,9 @@ class WorkspaceFavoriteEndpoint(BaseAPIView):
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
@allow_permission(
|
||||||
|
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE"
|
||||||
|
)
|
||||||
def patch(self, request, slug, favorite_id):
|
def patch(self, request, slug, favorite_id):
|
||||||
favorite = UserFavorite.objects.get(
|
favorite = UserFavorite.objects.get(
|
||||||
user=request.user, workspace__slug=slug, pk=favorite_id
|
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.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
@allow_permission(
|
||||||
|
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE"
|
||||||
|
)
|
||||||
def delete(self, request, slug, favorite_id):
|
def delete(self, request, slug, favorite_id):
|
||||||
favorite = UserFavorite.objects.get(
|
favorite = UserFavorite.objects.get(
|
||||||
user=request.user, workspace__slug=slug, pk=favorite_id
|
user=request.user, workspace__slug=slug, pk=favorite_id
|
||||||
|
|
@ -67,10 +76,10 @@ class WorkspaceFavoriteEndpoint(BaseAPIView):
|
||||||
|
|
||||||
|
|
||||||
class WorkspaceFavoriteGroupEndpoint(BaseAPIView):
|
class WorkspaceFavoriteGroupEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
|
||||||
WorkspaceEntityPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
@allow_permission(
|
||||||
|
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE"
|
||||||
|
)
|
||||||
def get(self, request, slug, favorite_id):
|
def get(self, request, slug, favorite_id):
|
||||||
favorites = UserFavorite.objects.filter(
|
favorites = UserFavorite.objects.filter(
|
||||||
user=request.user,
|
user=request.user,
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ from plane.authentication.adapter.error import (
|
||||||
AuthenticationException,
|
AuthenticationException,
|
||||||
AUTHENTICATION_ERROR_CODES,
|
AUTHENTICATION_ERROR_CODES,
|
||||||
)
|
)
|
||||||
|
from plane.authentication.rate_limit import AuthenticationThrottle
|
||||||
|
|
||||||
|
|
||||||
class MagicGenerateEndpoint(APIView):
|
class MagicGenerateEndpoint(APIView):
|
||||||
|
|
@ -37,6 +38,10 @@ class MagicGenerateEndpoint(APIView):
|
||||||
AllowAny,
|
AllowAny,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
throttle_classes = [
|
||||||
|
AuthenticationThrottle,
|
||||||
|
]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
# Check if instance is configured
|
# Check if instance is configured
|
||||||
instance = Instance.objects.first()
|
instance = Instance.objects.first()
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ from django.db.models import Q
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activities_task import issue_activity
|
||||||
from plane.db.models import Issue, Project, State
|
from plane.db.models import Issue, Project, State
|
||||||
from plane.utils.exception_logger import log_exception
|
from plane.utils.exception_logger import log_exception
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,23 @@ from django.core.management import BaseCommand
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = "Clear Cache before starting the server to remove stale values"
|
help = "Clear Cache before starting the server to remove stale values"
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
# Positional argument
|
||||||
|
parser.add_argument(
|
||||||
|
"--key", type=str, nargs="?", help="Key to clear cache"
|
||||||
|
)
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
try:
|
try:
|
||||||
|
if options["key"]:
|
||||||
|
cache.delete(options["key"])
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(
|
||||||
|
f"Cache Cleared for key: {options['key']}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
cache.clear()
|
cache.clear()
|
||||||
self.stdout.write(self.style.SUCCESS("Cache Cleared"))
|
self.stdout.write(self.style.SUCCESS("Cache Cleared"))
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,203 @@
|
||||||
|
# Generated by Django 4.2.11 on 2024-08-13 16:21
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("db", "0073_alter_commentreaction_unique_together_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="deployboard",
|
||||||
|
name="is_activity_enabled",
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="fileasset",
|
||||||
|
name="is_archived",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="userfavorite",
|
||||||
|
name="sequence",
|
||||||
|
field=models.FloatField(default=65535),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="ProjectIssueType",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"created_at",
|
||||||
|
models.DateTimeField(
|
||||||
|
auto_now_add=True, verbose_name="Created At"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"updated_at",
|
||||||
|
models.DateTimeField(
|
||||||
|
auto_now=True, verbose_name="Last Modified At"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"deleted_at",
|
||||||
|
models.DateTimeField(
|
||||||
|
blank=True, null=True, verbose_name="Deleted At"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.UUIDField(
|
||||||
|
db_index=True,
|
||||||
|
default=uuid.uuid4,
|
||||||
|
editable=False,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
unique=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("level", models.PositiveIntegerField(default=0)),
|
||||||
|
("is_default", models.BooleanField(default=False)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Project Issue Type",
|
||||||
|
"verbose_name_plural": "Project Issue Types",
|
||||||
|
"db_table": "project_issue_types",
|
||||||
|
"ordering": ("project", "issue_type"),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="issuetype",
|
||||||
|
options={
|
||||||
|
"verbose_name": "Issue Type",
|
||||||
|
"verbose_name_plural": "Issue Types",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.RemoveConstraint(
|
||||||
|
model_name="issuetype",
|
||||||
|
name="issue_type_unique_name_project_when_deleted_at_null",
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name="issuetype",
|
||||||
|
unique_together=set(),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="issuetype",
|
||||||
|
name="workspace",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="issue_types",
|
||||||
|
to="db.workspace",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name="issuetype",
|
||||||
|
unique_together={("workspace", "name", "deleted_at")},
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="issuetype",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
condition=models.Q(("deleted_at__isnull", True)),
|
||||||
|
fields=("name", "workspace"),
|
||||||
|
name="issue_type_unique_name_workspace_when_deleted_at_null",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="projectissuetype",
|
||||||
|
name="created_by",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="%(class)s_created_by",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
verbose_name="Created By",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="projectissuetype",
|
||||||
|
name="issue_type",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="project_issue_types",
|
||||||
|
to="db.issuetype",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="projectissuetype",
|
||||||
|
name="project",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="project_%(class)s",
|
||||||
|
to="db.project",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="projectissuetype",
|
||||||
|
name="updated_by",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="%(class)s_updated_by",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
verbose_name="Last Modified By",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="projectissuetype",
|
||||||
|
name="workspace",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="workspace_%(class)s",
|
||||||
|
to="db.workspace",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="issuetype",
|
||||||
|
name="is_default",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="issuetype",
|
||||||
|
name="project",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="issuetype",
|
||||||
|
name="sort_order",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="issuetype",
|
||||||
|
name="weight",
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="projectissuetype",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
condition=models.Q(("deleted_at__isnull", True)),
|
||||||
|
fields=("project", "issue_type"),
|
||||||
|
name="project_issue_type_unique_project_issue_type_when_deleted_at_null",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name="projectissuetype",
|
||||||
|
unique_together={("project", "issue_type", "deleted_at")},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="issuetype",
|
||||||
|
name="is_default",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="issuetype",
|
||||||
|
name="level",
|
||||||
|
field=models.PositiveIntegerField(default=0),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name="issuetype",
|
||||||
|
unique_together=set(),
|
||||||
|
),
|
||||||
|
migrations.RemoveConstraint(
|
||||||
|
model_name="issuetype",
|
||||||
|
name="issue_type_unique_name_workspace_when_deleted_at_null",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -42,6 +42,7 @@ class FileAsset(BaseModel):
|
||||||
related_name="assets",
|
related_name="assets",
|
||||||
)
|
)
|
||||||
is_deleted = models.BooleanField(default=False)
|
is_deleted = models.BooleanField(default=False)
|
||||||
|
is_archived = models.BooleanField(default=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "File Asset"
|
verbose_name = "File Asset"
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ class DeployBoard(WorkspaceBaseModel):
|
||||||
)
|
)
|
||||||
is_votes_enabled = models.BooleanField(default=False)
|
is_votes_enabled = models.BooleanField(default=False)
|
||||||
view_props = models.JSONField(default=dict)
|
view_props = models.JSONField(default=dict)
|
||||||
|
is_activity_enabled = models.BooleanField(default=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""Return name of the deploy board"""
|
"""Return name of the deploy board"""
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ class UserFavorite(WorkspaceBaseModel):
|
||||||
entity_identifier = models.UUIDField(null=True, blank=True)
|
entity_identifier = models.UUIDField(null=True, blank=True)
|
||||||
name = models.CharField(max_length=255, blank=True, null=True)
|
name = models.CharField(max_length=255, blank=True, null=True)
|
||||||
is_folder = models.BooleanField(default=False)
|
is_folder = models.BooleanField(default=False)
|
||||||
sequence = models.IntegerField(default=65535)
|
sequence = models.FloatField(default=65535)
|
||||||
parent = models.ForeignKey(
|
parent = models.ForeignKey(
|
||||||
"self",
|
"self",
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
|
|
@ -31,7 +31,12 @@ class UserFavorite(WorkspaceBaseModel):
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ["entity_type", "user", "entity_identifier", "deleted_at"]
|
unique_together = [
|
||||||
|
"entity_type",
|
||||||
|
"user",
|
||||||
|
"entity_identifier",
|
||||||
|
"deleted_at",
|
||||||
|
]
|
||||||
constraints = [
|
constraints = [
|
||||||
models.UniqueConstraint(
|
models.UniqueConstraint(
|
||||||
fields=["entity_type", "entity_identifier", "user"],
|
fields=["entity_type", "entity_identifier", "user"],
|
||||||
|
|
|
||||||
|
|
@ -3,43 +3,54 @@ from django.db import models
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .workspace import WorkspaceBaseModel
|
from .project import ProjectBaseModel
|
||||||
|
from .base import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class IssueType(WorkspaceBaseModel):
|
class IssueType(BaseModel):
|
||||||
|
workspace = models.ForeignKey(
|
||||||
|
"db.Workspace",
|
||||||
|
related_name="issue_types",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
description = models.TextField(blank=True)
|
description = models.TextField(blank=True)
|
||||||
logo_props = models.JSONField(default=dict)
|
logo_props = models.JSONField(default=dict)
|
||||||
sort_order = models.FloatField(default=65535)
|
|
||||||
is_default = models.BooleanField(default=False)
|
is_default = models.BooleanField(default=False)
|
||||||
weight = models.PositiveIntegerField(default=0)
|
|
||||||
is_active = models.BooleanField(default=True)
|
is_active = models.BooleanField(default=True)
|
||||||
|
level = models.PositiveIntegerField(default=0)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ["project", "name", "deleted_at"]
|
|
||||||
constraints = [
|
|
||||||
models.UniqueConstraint(
|
|
||||||
fields=["name", "project"],
|
|
||||||
condition=Q(deleted_at__isnull=True),
|
|
||||||
name="issue_type_unique_name_project_when_deleted_at_null",
|
|
||||||
)
|
|
||||||
]
|
|
||||||
verbose_name = "Issue Type"
|
verbose_name = "Issue Type"
|
||||||
verbose_name_plural = "Issue Types"
|
verbose_name_plural = "Issue Types"
|
||||||
db_table = "issue_types"
|
db_table = "issue_types"
|
||||||
ordering = ("sort_order",)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
# If we are adding a new issue type, we need to set the sort order
|
class ProjectIssueType(ProjectBaseModel):
|
||||||
if self._state.adding:
|
issue_type = models.ForeignKey(
|
||||||
# Get the largest sort order for the project
|
"db.IssueType",
|
||||||
largest_sort_order = IssueType.objects.filter(
|
related_name="project_issue_types",
|
||||||
project=self.project
|
on_delete=models.CASCADE,
|
||||||
).aggregate(largest=models.Max("sort_order"))["largest"]
|
)
|
||||||
# If there are issue types, set the sort order to the largest + 10000
|
level = models.PositiveIntegerField(default=0)
|
||||||
if largest_sort_order is not None:
|
is_default = models.BooleanField(default=False)
|
||||||
self.sort_order = largest_sort_order + 10000
|
|
||||||
super(IssueType, self).save(*args, **kwargs)
|
class Meta:
|
||||||
|
unique_together = ["project", "issue_type", "deleted_at"]
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["project", "issue_type"],
|
||||||
|
condition=Q(deleted_at__isnull=True),
|
||||||
|
name="project_issue_type_unique_project_issue_type_when_deleted_at_null",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
verbose_name = "Project Issue Type"
|
||||||
|
verbose_name_plural = "Project Issue Types"
|
||||||
|
db_table = "project_issue_types"
|
||||||
|
ordering = ("project", "issue_type")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.project} - {self.issue_type}"
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ class InstanceSerializer(BaseSerializer):
|
||||||
model = Instance
|
model = Instance
|
||||||
exclude = [
|
exclude = [
|
||||||
"license_key",
|
"license_key",
|
||||||
|
"user_count"
|
||||||
]
|
]
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
"id",
|
"id",
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ class InstanceEndpoint(BaseAPIView):
|
||||||
data["is_activated"] = True
|
data["is_activated"] = True
|
||||||
# Get all the configuration
|
# Get all the configuration
|
||||||
(
|
(
|
||||||
|
ENABLE_SIGNUP,
|
||||||
IS_GOOGLE_ENABLED,
|
IS_GOOGLE_ENABLED,
|
||||||
IS_GITHUB_ENABLED,
|
IS_GITHUB_ENABLED,
|
||||||
GITHUB_APP_NAME,
|
GITHUB_APP_NAME,
|
||||||
|
|
@ -70,6 +71,10 @@ class InstanceEndpoint(BaseAPIView):
|
||||||
INTERCOM_APP_ID,
|
INTERCOM_APP_ID,
|
||||||
) = get_configuration_value(
|
) = get_configuration_value(
|
||||||
[
|
[
|
||||||
|
{
|
||||||
|
"key": "ENABLE_SIGNUP",
|
||||||
|
"default": os.environ.get("ENABLE_SIGNUP", "0"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"key": "IS_GOOGLE_ENABLED",
|
"key": "IS_GOOGLE_ENABLED",
|
||||||
"default": os.environ.get("IS_GOOGLE_ENABLED", "0"),
|
"default": os.environ.get("IS_GOOGLE_ENABLED", "0"),
|
||||||
|
|
@ -132,6 +137,7 @@ class InstanceEndpoint(BaseAPIView):
|
||||||
|
|
||||||
data = {}
|
data = {}
|
||||||
# Authentication
|
# Authentication
|
||||||
|
data["enable_signup"] = ENABLE_SIGNUP == "1"
|
||||||
data["is_google_enabled"] = IS_GOOGLE_ENABLED == "1"
|
data["is_google_enabled"] = IS_GOOGLE_ENABLED == "1"
|
||||||
data["is_github_enabled"] = IS_GITHUB_ENABLED == "1"
|
data["is_github_enabled"] = IS_GITHUB_ENABLED == "1"
|
||||||
data["is_gitlab_enabled"] = IS_GITLAB_ENABLED == "1"
|
data["is_gitlab_enabled"] = IS_GITLAB_ENABLED == "1"
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ from plane.app.serializers import (
|
||||||
IssueStateInboxSerializer,
|
IssueStateInboxSerializer,
|
||||||
)
|
)
|
||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activities_task import issue_activity
|
||||||
|
|
||||||
|
|
||||||
class InboxIssuePublicViewSet(BaseViewSet):
|
class InboxIssuePublicViewSet(BaseViewSet):
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ from django.db.models import (
|
||||||
JSONField,
|
JSONField,
|
||||||
Value,
|
Value,
|
||||||
OuterRef,
|
OuterRef,
|
||||||
Func
|
Func,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Third Party imports
|
# Third Party imports
|
||||||
|
|
@ -61,7 +61,7 @@ from plane.db.models import (
|
||||||
ProjectPublicMember,
|
ProjectPublicMember,
|
||||||
IssueAttachment,
|
IssueAttachment,
|
||||||
)
|
)
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activities_task import issue_activity
|
||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,14 +27,11 @@ class ProjectStatesEndpoint(BaseAPIView):
|
||||||
status=status.HTTP_404_NOT_FOUND,
|
status=status.HTTP_404_NOT_FOUND,
|
||||||
)
|
)
|
||||||
|
|
||||||
states = (
|
states = State.objects.filter(
|
||||||
State.objects.filter(
|
|
||||||
~Q(name="Triage"),
|
~Q(name="Triage"),
|
||||||
workspace__slug=deploy_board.workspace.slug,
|
workspace__slug=deploy_board.workspace.slug,
|
||||||
project_id=deploy_board.project_id,
|
project_id=deploy_board.project_id,
|
||||||
)
|
).values("name", "group", "color", "id", "sequence")
|
||||||
.values("name", "group", "color", "id")
|
|
||||||
)
|
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
states,
|
states,
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ from plane.db.models import (
|
||||||
|
|
||||||
|
|
||||||
def issue_queryset_grouper(queryset, group_by, sub_group_by):
|
def issue_queryset_grouper(queryset, group_by, sub_group_by):
|
||||||
|
|
||||||
FIELD_MAPPER = {
|
FIELD_MAPPER = {
|
||||||
"label_ids": "labels__id",
|
"label_ids": "labels__id",
|
||||||
"assignee_ids": "assignees__id",
|
"assignee_ids": "assignees__id",
|
||||||
|
|
@ -30,7 +29,10 @@ def issue_queryset_grouper(queryset, group_by, sub_group_by):
|
||||||
"label_ids": ("labels__id", ~Q(labels__id__isnull=True)),
|
"label_ids": ("labels__id", ~Q(labels__id__isnull=True)),
|
||||||
"module_ids": (
|
"module_ids": (
|
||||||
"issue_module__module_id",
|
"issue_module__module_id",
|
||||||
~Q(issue_module__module_id__isnull=True),
|
(
|
||||||
|
~Q(issue_module__module_id__isnull=True)
|
||||||
|
& Q(issue_module__module__archived_at__isnull=True)
|
||||||
|
),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
default_annotations = {
|
default_annotations = {
|
||||||
|
|
@ -51,7 +53,6 @@ def issue_queryset_grouper(queryset, group_by, sub_group_by):
|
||||||
|
|
||||||
|
|
||||||
def issue_on_results(issues, group_by, sub_group_by):
|
def issue_on_results(issues, group_by, sub_group_by):
|
||||||
|
|
||||||
FIELD_MAPPER = {
|
FIELD_MAPPER = {
|
||||||
"labels__id": "label_ids",
|
"labels__id": "label_ids",
|
||||||
"assignees__id": "assignee_ids",
|
"assignees__id": "assignee_ids",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# base requirements
|
# base requirements
|
||||||
|
|
||||||
# django
|
# django
|
||||||
Django==4.2.14
|
Django==4.2.15
|
||||||
# rest framework
|
# rest framework
|
||||||
djangorestframework==3.15.2
|
djangorestframework==3.15.2
|
||||||
# postgres
|
# postgres
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,20 @@ export DOCKERHUB_USER=makeplane
|
||||||
export PULL_POLICY=${PULL_POLICY:-if_not_present}
|
export PULL_POLICY=${PULL_POLICY:-if_not_present}
|
||||||
|
|
||||||
CPU_ARCH=$(uname -m)
|
CPU_ARCH=$(uname -m)
|
||||||
|
OS_NAME=$(uname)
|
||||||
|
UPPER_CPU_ARCH=$(tr '[:lower:]' '[:upper:]' <<< "$CPU_ARCH")
|
||||||
|
|
||||||
mkdir -p $PLANE_INSTALL_DIR/archive
|
mkdir -p $PLANE_INSTALL_DIR/archive
|
||||||
DOCKER_FILE_PATH=$PLANE_INSTALL_DIR/docker-compose.yaml
|
DOCKER_FILE_PATH=$PLANE_INSTALL_DIR/docker-compose.yaml
|
||||||
DOCKER_ENV_PATH=$PLANE_INSTALL_DIR/plane.env
|
DOCKER_ENV_PATH=$PLANE_INSTALL_DIR/plane.env
|
||||||
|
|
||||||
|
SED_PREFIX=()
|
||||||
|
if [ "$OS_NAME" == "Darwin" ]; then
|
||||||
|
SED_PREFIX=("-i" "")
|
||||||
|
else
|
||||||
|
SED_PREFIX=("-i")
|
||||||
|
fi
|
||||||
|
|
||||||
function print_header() {
|
function print_header() {
|
||||||
clear
|
clear
|
||||||
|
|
||||||
|
|
@ -51,12 +60,12 @@ function spinner() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function initialize(){
|
function initialize(){
|
||||||
printf "Please wait while we check the availability of Docker images for the selected release ($APP_RELEASE) with ${CPU_ARCH^^} support." >&2
|
printf "Please wait while we check the availability of Docker images for the selected release ($APP_RELEASE) with ${UPPER_CPU_ARCH} support." >&2
|
||||||
|
|
||||||
if [ "$CUSTOM_BUILD" == "true" ]; then
|
if [ "$CUSTOM_BUILD" == "true" ]; then
|
||||||
echo "" >&2
|
echo "" >&2
|
||||||
echo "" >&2
|
echo "" >&2
|
||||||
echo "${CPU_ARCH^^} images are not available for selected release ($APP_RELEASE)." >&2
|
echo "${UPPER_CPU_ARCH} images are not available for selected release ($APP_RELEASE)." >&2
|
||||||
echo "build"
|
echo "build"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
@ -78,7 +87,7 @@ function initialize(){
|
||||||
else
|
else
|
||||||
echo "" >&2
|
echo "" >&2
|
||||||
echo "" >&2
|
echo "" >&2
|
||||||
echo "${CPU_ARCH^^} images are not available for selected release ($APP_RELEASE)." >&2
|
echo "${UPPER_CPU_ARCH} images are not available for selected release ($APP_RELEASE)." >&2
|
||||||
echo "" >&2
|
echo "" >&2
|
||||||
echo "build"
|
echo "build"
|
||||||
return 1
|
return 1
|
||||||
|
|
@ -122,7 +131,7 @@ function updateEnvFile() {
|
||||||
return
|
return
|
||||||
else
|
else
|
||||||
# if key exists, update the value
|
# if key exists, update the value
|
||||||
sed -i "s/^$key=.*/$key=$value/g" "$file"
|
sed "${SED_PREFIX[@]}" "s/^$key=.*/$key=$value/g" "$file"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo "File not found: $file"
|
echo "File not found: $file"
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
|
import { Extensions } from "@tiptap/core";
|
||||||
import { SlashCommand } from "@/extensions";
|
import { SlashCommand } from "@/extensions";
|
||||||
// hooks
|
// hooks
|
||||||
import { TFileHandler } from "@/hooks/use-editor";
|
import { TFileHandler } from "@/hooks/use-editor";
|
||||||
// plane editor types
|
// plane editor types
|
||||||
import { TIssueEmbedConfig } from "@/plane-editor/types";
|
import { TIssueEmbedConfig } from "@/plane-editor/types";
|
||||||
|
// types
|
||||||
|
import { TExtensions } from "@/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
disabledExtensions?: TExtensions[];
|
||||||
fileHandler: TFileHandler;
|
fileHandler: TFileHandler;
|
||||||
issueEmbedConfig: TIssueEmbedConfig | undefined;
|
issueEmbedConfig: TIssueEmbedConfig | undefined;
|
||||||
};
|
};
|
||||||
|
|
@ -12,7 +16,7 @@ type Props = {
|
||||||
export const DocumentEditorAdditionalExtensions = (props: Props) => {
|
export const DocumentEditorAdditionalExtensions = (props: Props) => {
|
||||||
const { fileHandler } = props;
|
const { fileHandler } = props;
|
||||||
|
|
||||||
const extensions = [SlashCommand(fileHandler.upload)];
|
const extensions: Extensions = [SlashCommand(fileHandler.upload)];
|
||||||
|
|
||||||
return extensions;
|
return extensions;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,30 @@
|
||||||
import React, { useState } from "react";
|
import React from "react";
|
||||||
// components
|
// components
|
||||||
import { PageRenderer } from "@/components/editors";
|
import { PageRenderer } from "@/components/editors";
|
||||||
|
// constants
|
||||||
|
import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config";
|
||||||
// helpers
|
// helpers
|
||||||
import { getEditorClassNames } from "@/helpers/common";
|
import { getEditorClassNames } from "@/helpers/common";
|
||||||
// hooks
|
// hooks
|
||||||
import { useDocumentEditor } from "@/hooks/use-document-editor";
|
import { useDocumentEditor } from "@/hooks/use-document-editor";
|
||||||
import { TFileHandler } from "@/hooks/use-editor";
|
|
||||||
// plane editor types
|
// plane editor types
|
||||||
import { TEmbedConfig } from "@/plane-editor/types";
|
import { TEmbedConfig } from "@/plane-editor/types";
|
||||||
// types
|
// types
|
||||||
import { EditorRefApi, IMentionHighlight, IMentionSuggestion } from "@/types";
|
import {
|
||||||
|
EditorRefApi,
|
||||||
|
IMentionHighlight,
|
||||||
|
IMentionSuggestion,
|
||||||
|
TAIHandler,
|
||||||
|
TDisplayConfig,
|
||||||
|
TExtensions,
|
||||||
|
TFileHandler,
|
||||||
|
} from "@/types";
|
||||||
|
|
||||||
interface IDocumentEditor {
|
interface IDocumentEditor {
|
||||||
|
aiHandler?: TAIHandler;
|
||||||
containerClassName?: string;
|
containerClassName?: string;
|
||||||
|
disabledExtensions?: TExtensions[];
|
||||||
|
displayConfig?: TDisplayConfig;
|
||||||
editorClassName?: string;
|
editorClassName?: string;
|
||||||
embedHandler: TEmbedConfig;
|
embedHandler: TEmbedConfig;
|
||||||
fileHandler: TFileHandler;
|
fileHandler: TFileHandler;
|
||||||
|
|
@ -31,7 +43,10 @@ interface IDocumentEditor {
|
||||||
|
|
||||||
const DocumentEditor = (props: IDocumentEditor) => {
|
const DocumentEditor = (props: IDocumentEditor) => {
|
||||||
const {
|
const {
|
||||||
|
aiHandler,
|
||||||
containerClassName,
|
containerClassName,
|
||||||
|
disabledExtensions,
|
||||||
|
displayConfig = DEFAULT_DISPLAY_CONFIG,
|
||||||
editorClassName = "",
|
editorClassName = "",
|
||||||
embedHandler,
|
embedHandler,
|
||||||
fileHandler,
|
fileHandler,
|
||||||
|
|
@ -44,16 +59,10 @@ const DocumentEditor = (props: IDocumentEditor) => {
|
||||||
tabIndex,
|
tabIndex,
|
||||||
value,
|
value,
|
||||||
} = props;
|
} = props;
|
||||||
// states
|
|
||||||
const [hideDragHandleOnMouseLeave, setHideDragHandleOnMouseLeave] = useState<() => void>(() => {});
|
|
||||||
// this essentially sets the hideDragHandle function from the DragAndDrop extension as the Plugin
|
|
||||||
// loads such that we can invoke it from react when the cursor leaves the container
|
|
||||||
const setHideDragHandleFunction = (hideDragHandlerFromDragDrop: () => void) => {
|
|
||||||
setHideDragHandleOnMouseLeave(() => hideDragHandlerFromDragDrop);
|
|
||||||
};
|
|
||||||
|
|
||||||
// use document editor
|
// use document editor
|
||||||
const { editor, isIndexedDbSynced } = useDocumentEditor({
|
const { editor, isIndexedDbSynced } = useDocumentEditor({
|
||||||
|
disabledExtensions,
|
||||||
id,
|
id,
|
||||||
editorClassName,
|
editorClassName,
|
||||||
embedHandler,
|
embedHandler,
|
||||||
|
|
@ -64,7 +73,6 @@ const DocumentEditor = (props: IDocumentEditor) => {
|
||||||
forwardedRef,
|
forwardedRef,
|
||||||
mentionHandler,
|
mentionHandler,
|
||||||
placeholder,
|
placeholder,
|
||||||
setHideDragHandleFunction,
|
|
||||||
tabIndex,
|
tabIndex,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -78,9 +86,10 @@ const DocumentEditor = (props: IDocumentEditor) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageRenderer
|
<PageRenderer
|
||||||
|
displayConfig={displayConfig}
|
||||||
|
aiHandler={aiHandler}
|
||||||
editor={editor}
|
editor={editor}
|
||||||
editorContainerClassName={editorContainerClassNames}
|
editorContainerClassName={editorContainerClassNames}
|
||||||
hideDragHandle={hideDragHandleOnMouseLeave}
|
|
||||||
id={id}
|
id={id}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -15,18 +15,21 @@ import { Editor, ReactRenderer } from "@tiptap/react";
|
||||||
// components
|
// components
|
||||||
import { EditorContainer, EditorContentWrapper } from "@/components/editors";
|
import { EditorContainer, EditorContentWrapper } from "@/components/editors";
|
||||||
import { LinkView, LinkViewProps } from "@/components/links";
|
import { LinkView, LinkViewProps } from "@/components/links";
|
||||||
import { BlockMenu } from "@/components/menus";
|
import { AIFeaturesMenu, BlockMenu } from "@/components/menus";
|
||||||
|
// types
|
||||||
|
import { TAIHandler, TDisplayConfig } from "@/types";
|
||||||
|
|
||||||
type IPageRenderer = {
|
type IPageRenderer = {
|
||||||
|
aiHandler?: TAIHandler;
|
||||||
|
displayConfig: TDisplayConfig;
|
||||||
editor: Editor;
|
editor: Editor;
|
||||||
editorContainerClassName: string;
|
editorContainerClassName: string;
|
||||||
hideDragHandle?: () => void;
|
|
||||||
id: string;
|
id: string;
|
||||||
tabIndex?: number;
|
tabIndex?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PageRenderer = (props: IPageRenderer) => {
|
export const PageRenderer = (props: IPageRenderer) => {
|
||||||
const { editor, editorContainerClassName, hideDragHandle, id, tabIndex } = props;
|
const { aiHandler, displayConfig, editor, editorContainerClassName, id, tabIndex } = props;
|
||||||
// states
|
// states
|
||||||
const [linkViewProps, setLinkViewProps] = useState<LinkViewProps>();
|
const [linkViewProps, setLinkViewProps] = useState<LinkViewProps>();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
@ -130,13 +133,18 @@ export const PageRenderer = (props: IPageRenderer) => {
|
||||||
<>
|
<>
|
||||||
<div className="frame-renderer flex-grow w-full -mx-5" onMouseOver={handleLinkHover}>
|
<div className="frame-renderer flex-grow w-full -mx-5" onMouseOver={handleLinkHover}>
|
||||||
<EditorContainer
|
<EditorContainer
|
||||||
|
displayConfig={displayConfig}
|
||||||
editor={editor}
|
editor={editor}
|
||||||
editorContainerClassName={editorContainerClassName}
|
editorContainerClassName={editorContainerClassName}
|
||||||
hideDragHandle={hideDragHandle}
|
|
||||||
id={id}
|
id={id}
|
||||||
>
|
>
|
||||||
<EditorContentWrapper editor={editor} id={id} tabIndex={tabIndex} />
|
<EditorContentWrapper editor={editor} id={id} tabIndex={tabIndex} />
|
||||||
{editor && editor.isEditable && <BlockMenu editor={editor} />}
|
{editor.isEditable && (
|
||||||
|
<>
|
||||||
|
<BlockMenu editor={editor} />
|
||||||
|
<AIFeaturesMenu menu={aiHandler?.menu} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</EditorContainer>
|
</EditorContainer>
|
||||||
</div>
|
</div>
|
||||||
{isOpen && linkViewProps && coordinates && (
|
{isOpen && linkViewProps && coordinates && (
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { forwardRef, MutableRefObject } from "react";
|
import { forwardRef, MutableRefObject } from "react";
|
||||||
// components
|
// components
|
||||||
import { PageRenderer } from "@/components/editors";
|
import { PageRenderer } from "@/components/editors";
|
||||||
|
// constants
|
||||||
|
import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config";
|
||||||
// extensions
|
// extensions
|
||||||
import { IssueWidget } from "@/extensions";
|
import { IssueWidget } from "@/extensions";
|
||||||
// helpers
|
// helpers
|
||||||
|
|
@ -10,12 +12,13 @@ import { useReadOnlyEditor } from "@/hooks/use-read-only-editor";
|
||||||
// plane web types
|
// plane web types
|
||||||
import { TEmbedConfig } from "@/plane-editor/types";
|
import { TEmbedConfig } from "@/plane-editor/types";
|
||||||
// types
|
// types
|
||||||
import { EditorReadOnlyRefApi, IMentionHighlight } from "@/types";
|
import { EditorReadOnlyRefApi, IMentionHighlight, TDisplayConfig } from "@/types";
|
||||||
|
|
||||||
interface IDocumentReadOnlyEditor {
|
interface IDocumentReadOnlyEditor {
|
||||||
id: string;
|
id: string;
|
||||||
initialValue: string;
|
initialValue: string;
|
||||||
containerClassName: string;
|
containerClassName: string;
|
||||||
|
displayConfig?: TDisplayConfig;
|
||||||
editorClassName?: string;
|
editorClassName?: string;
|
||||||
embedHandler: TEmbedConfig;
|
embedHandler: TEmbedConfig;
|
||||||
tabIndex?: number;
|
tabIndex?: number;
|
||||||
|
|
@ -29,6 +32,7 @@ interface IDocumentReadOnlyEditor {
|
||||||
const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => {
|
const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => {
|
||||||
const {
|
const {
|
||||||
containerClassName,
|
containerClassName,
|
||||||
|
displayConfig = DEFAULT_DISPLAY_CONFIG,
|
||||||
editorClassName = "",
|
editorClassName = "",
|
||||||
embedHandler,
|
embedHandler,
|
||||||
id,
|
id,
|
||||||
|
|
@ -39,17 +43,17 @@ const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => {
|
||||||
mentionHandler,
|
mentionHandler,
|
||||||
} = props;
|
} = props;
|
||||||
const editor = useReadOnlyEditor({
|
const editor = useReadOnlyEditor({
|
||||||
initialValue,
|
|
||||||
editorClassName,
|
editorClassName,
|
||||||
mentionHandler,
|
|
||||||
forwardedRef,
|
|
||||||
handleEditorReady,
|
|
||||||
extensions: [
|
extensions: [
|
||||||
embedHandler?.issue &&
|
embedHandler?.issue &&
|
||||||
IssueWidget({
|
IssueWidget({
|
||||||
widgetCallback: embedHandler?.issue.widgetCallback,
|
widgetCallback: embedHandler?.issue.widgetCallback,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
forwardedRef,
|
||||||
|
handleEditorReady,
|
||||||
|
initialValue,
|
||||||
|
mentionHandler,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!editor) {
|
if (!editor) {
|
||||||
|
|
@ -61,7 +65,13 @@ const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageRenderer editor={editor} editorContainerClassName={editorContainerClassName} id={id} tabIndex={tabIndex} />
|
<PageRenderer
|
||||||
|
displayConfig={displayConfig}
|
||||||
|
editor={editor}
|
||||||
|
editorContainerClassName={editorContainerClassName}
|
||||||
|
id={id}
|
||||||
|
tabIndex={tabIndex}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,22 @@
|
||||||
import { FC, ReactNode } from "react";
|
import { FC, ReactNode } from "react";
|
||||||
import { Editor } from "@tiptap/react";
|
import { Editor } from "@tiptap/react";
|
||||||
|
// constants
|
||||||
|
import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config";
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "@/helpers/common";
|
import { cn } from "@/helpers/common";
|
||||||
|
// types
|
||||||
|
import { TDisplayConfig } from "@/types";
|
||||||
|
|
||||||
interface EditorContainerProps {
|
interface EditorContainerProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
displayConfig: TDisplayConfig;
|
||||||
editor: Editor | null;
|
editor: Editor | null;
|
||||||
editorContainerClassName: string;
|
editorContainerClassName: string;
|
||||||
hideDragHandle?: () => void;
|
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EditorContainer: FC<EditorContainerProps> = (props) => {
|
export const EditorContainer: FC<EditorContainerProps> = (props) => {
|
||||||
const { children, editor, editorContainerClassName, hideDragHandle, id } = props;
|
const { children, displayConfig, editor, editorContainerClassName, id } = props;
|
||||||
|
|
||||||
const handleContainerClick = () => {
|
const handleContainerClick = () => {
|
||||||
if (!editor) return;
|
if (!editor) return;
|
||||||
|
|
@ -53,16 +57,25 @@ export const EditorContainer: FC<EditorContainerProps> = (props) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleContainerMouseLeave = () => {
|
||||||
|
const dragHandleElement = document.querySelector("#editor-side-menu");
|
||||||
|
if (!dragHandleElement?.classList.contains("side-menu-hidden")) {
|
||||||
|
dragHandleElement?.classList.add("side-menu-hidden");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id={`editor-container-${id}`}
|
id={`editor-container-${id}`}
|
||||||
onClick={handleContainerClick}
|
onClick={handleContainerClick}
|
||||||
onMouseLeave={hideDragHandle}
|
onMouseLeave={handleContainerMouseLeave}
|
||||||
className={cn(
|
className={cn(
|
||||||
"cursor-text relative",
|
"editor-container cursor-text relative",
|
||||||
{
|
{
|
||||||
"active-editor": editor?.isFocused && editor?.isEditable,
|
"active-editor": editor?.isFocused && editor?.isEditable,
|
||||||
},
|
},
|
||||||
|
displayConfig.fontSize ?? DEFAULT_DISPLAY_CONFIG.fontSize,
|
||||||
|
displayConfig.fontStyle ?? DEFAULT_DISPLAY_CONFIG.fontStyle,
|
||||||
editorContainerClassName
|
editorContainerClassName
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { Editor, Extension } from "@tiptap/core";
|
import { Editor, Extension } from "@tiptap/core";
|
||||||
// components
|
// components
|
||||||
import { EditorContainer } from "@/components/editors";
|
import { EditorContainer } from "@/components/editors";
|
||||||
|
// constants
|
||||||
|
import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config";
|
||||||
// hooks
|
// hooks
|
||||||
import { getEditorClassNames } from "@/helpers/common";
|
import { getEditorClassNames } from "@/helpers/common";
|
||||||
import { useEditor } from "@/hooks/use-editor";
|
import { useEditor } from "@/hooks/use-editor";
|
||||||
|
|
@ -11,16 +13,15 @@ import { EditorContentWrapper } from "./editor-content";
|
||||||
type Props = IEditorProps & {
|
type Props = IEditorProps & {
|
||||||
children?: (editor: Editor) => React.ReactNode;
|
children?: (editor: Editor) => React.ReactNode;
|
||||||
extensions: Extension<any, any>[];
|
extensions: Extension<any, any>[];
|
||||||
hideDragHandleOnMouseLeave: () => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EditorWrapper: React.FC<Props> = (props) => {
|
export const EditorWrapper: React.FC<Props> = (props) => {
|
||||||
const {
|
const {
|
||||||
children,
|
children,
|
||||||
containerClassName,
|
containerClassName,
|
||||||
|
displayConfig = DEFAULT_DISPLAY_CONFIG,
|
||||||
editorClassName = "",
|
editorClassName = "",
|
||||||
extensions,
|
extensions,
|
||||||
hideDragHandleOnMouseLeave,
|
|
||||||
id,
|
id,
|
||||||
initialValue,
|
initialValue,
|
||||||
fileHandler,
|
fileHandler,
|
||||||
|
|
@ -57,10 +58,10 @@ export const EditorWrapper: React.FC<Props> = (props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditorContainer
|
<EditorContainer
|
||||||
|
displayConfig={displayConfig}
|
||||||
editor={editor}
|
editor={editor}
|
||||||
editorContainerClassName={editorContainerClassName}
|
editorContainerClassName={editorContainerClassName}
|
||||||
id={id}
|
id={id}
|
||||||
hideDragHandle={hideDragHandleOnMouseLeave}
|
|
||||||
>
|
>
|
||||||
{children?.(editor)}
|
{children?.(editor)}
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ const LiteTextEditor = (props: ILiteTextEditor) => {
|
||||||
|
|
||||||
const extensions = [EnterKeyExtension(onEnterKeyPress)];
|
const extensions = [EnterKeyExtension(onEnterKeyPress)];
|
||||||
|
|
||||||
return <EditorWrapper {...props} extensions={extensions} hideDragHandleOnMouseLeave={() => {}} />;
|
return <EditorWrapper {...props} extensions={extensions} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const LiteTextEditorWithRef = forwardRef<EditorRefApi, ILiteTextEditor>((props, ref) => (
|
const LiteTextEditorWithRef = forwardRef<EditorRefApi, ILiteTextEditor>((props, ref) => (
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
// components
|
// components
|
||||||
import { EditorContainer, EditorContentWrapper } from "@/components/editors";
|
import { EditorContainer, EditorContentWrapper } from "@/components/editors";
|
||||||
|
// constants
|
||||||
|
import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config";
|
||||||
// helpers
|
// helpers
|
||||||
import { getEditorClassNames } from "@/helpers/common";
|
import { getEditorClassNames } from "@/helpers/common";
|
||||||
// hooks
|
// hooks
|
||||||
|
|
@ -8,12 +10,20 @@ import { useReadOnlyEditor } from "@/hooks/use-read-only-editor";
|
||||||
import { IReadOnlyEditorProps } from "@/types";
|
import { IReadOnlyEditorProps } from "@/types";
|
||||||
|
|
||||||
export const ReadOnlyEditorWrapper = (props: IReadOnlyEditorProps) => {
|
export const ReadOnlyEditorWrapper = (props: IReadOnlyEditorProps) => {
|
||||||
const { containerClassName, editorClassName = "", id, initialValue, forwardedRef, mentionHandler } = props;
|
const {
|
||||||
|
containerClassName,
|
||||||
|
displayConfig = DEFAULT_DISPLAY_CONFIG,
|
||||||
|
editorClassName = "",
|
||||||
|
id,
|
||||||
|
initialValue,
|
||||||
|
forwardedRef,
|
||||||
|
mentionHandler,
|
||||||
|
} = props;
|
||||||
|
|
||||||
const editor = useReadOnlyEditor({
|
const editor = useReadOnlyEditor({
|
||||||
initialValue,
|
|
||||||
editorClassName,
|
editorClassName,
|
||||||
forwardedRef,
|
forwardedRef,
|
||||||
|
initialValue,
|
||||||
mentionHandler,
|
mentionHandler,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -24,7 +34,12 @@ export const ReadOnlyEditorWrapper = (props: IReadOnlyEditorProps) => {
|
||||||
if (!editor) return null;
|
if (!editor) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditorContainer editor={editor} editorContainerClassName={editorContainerClassName} id={id}>
|
<EditorContainer
|
||||||
|
displayConfig={displayConfig}
|
||||||
|
editor={editor}
|
||||||
|
editorContainerClassName={editorContainerClassName}
|
||||||
|
id={id}
|
||||||
|
>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<EditorContentWrapper editor={editor} id={id} />
|
<EditorContentWrapper editor={editor} id={id} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,30 @@
|
||||||
import { forwardRef, useCallback, useState } from "react";
|
import { forwardRef, useCallback } from "react";
|
||||||
// components
|
// components
|
||||||
import { EditorWrapper } from "@/components/editors";
|
import { EditorWrapper } from "@/components/editors";
|
||||||
import { EditorBubbleMenu } from "@/components/menus";
|
import { EditorBubbleMenu } from "@/components/menus";
|
||||||
// extensions
|
// extensions
|
||||||
import { DragAndDrop, SlashCommand } from "@/extensions";
|
import { SideMenuExtension, SlashCommand } from "@/extensions";
|
||||||
// types
|
// types
|
||||||
import { EditorRefApi, IRichTextEditor } from "@/types";
|
import { EditorRefApi, IRichTextEditor } from "@/types";
|
||||||
|
|
||||||
const RichTextEditor = (props: IRichTextEditor) => {
|
const RichTextEditor = (props: IRichTextEditor) => {
|
||||||
const { dragDropEnabled, fileHandler } = props;
|
const { dragDropEnabled, fileHandler } = props;
|
||||||
// states
|
|
||||||
const [hideDragHandleOnMouseLeave, setHideDragHandleOnMouseLeave] = useState<() => void>(() => {});
|
|
||||||
|
|
||||||
// this essentially sets the hideDragHandle function from the DragAndDrop extension as the Plugin
|
|
||||||
// loads such that we can invoke it from react when the cursor leaves the container
|
|
||||||
const setHideDragHandleFunction = (hideDragHandlerFromDragDrop: () => void) => {
|
|
||||||
setHideDragHandleOnMouseLeave(() => hideDragHandlerFromDragDrop);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getExtensions = useCallback(() => {
|
const getExtensions = useCallback(() => {
|
||||||
const extensions = [
|
const extensions = [SlashCommand(fileHandler.upload)];
|
||||||
SlashCommand(fileHandler.upload),
|
|
||||||
// TODO; add the extension conditionally for forms that don't require it
|
|
||||||
// EnterKeyExtension(onEnterKeyPress),
|
|
||||||
];
|
|
||||||
|
|
||||||
if (dragDropEnabled) extensions.push(DragAndDrop(setHideDragHandleFunction));
|
extensions.push(
|
||||||
|
SideMenuExtension({
|
||||||
|
aiEnabled: false,
|
||||||
|
dragDropEnabled: !!dragDropEnabled,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return extensions;
|
return extensions;
|
||||||
}, [dragDropEnabled, fileHandler.upload]);
|
}, [dragDropEnabled, fileHandler.upload]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditorWrapper {...props} extensions={getExtensions()} hideDragHandleOnMouseLeave={hideDragHandleOnMouseLeave}>
|
<EditorWrapper {...props} extensions={getExtensions()}>
|
||||||
{(editor) => <>{editor && <EditorBubbleMenu editor={editor} />}</>}
|
{(editor) => <>{editor && <EditorBubbleMenu editor={editor} />}</>}
|
||||||
</EditorWrapper>
|
</EditorWrapper>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
95
packages/editor/src/core/components/menus/ai-menu.tsx
Normal file
95
packages/editor/src/core/components/menus/ai-menu.tsx
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import tippy, { Instance } from "tippy.js";
|
||||||
|
// helpers
|
||||||
|
import { cn } from "@/helpers/common";
|
||||||
|
// types
|
||||||
|
import { TAIHandler } from "@/types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
menu: TAIHandler["menu"];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AIFeaturesMenu: React.FC<Props> = (props) => {
|
||||||
|
const { menu } = props;
|
||||||
|
// states
|
||||||
|
const [isPopupVisible, setIsPopupVisible] = useState(false);
|
||||||
|
// refs
|
||||||
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
|
const popup = useRef<Instance | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!menuRef.current) return;
|
||||||
|
|
||||||
|
menuRef.current.remove();
|
||||||
|
menuRef.current.style.visibility = "visible";
|
||||||
|
|
||||||
|
// @ts-expect-error - tippy types are incorrect
|
||||||
|
popup.current = tippy(document.body, {
|
||||||
|
getReferenceClientRect: null,
|
||||||
|
content: menuRef.current,
|
||||||
|
appendTo: () => document.querySelector(".frame-renderer"),
|
||||||
|
trigger: "manual",
|
||||||
|
interactive: true,
|
||||||
|
arrow: false,
|
||||||
|
placement: "bottom-start",
|
||||||
|
animation: "shift-away",
|
||||||
|
hideOnClick: true,
|
||||||
|
onShown: () => menuRef.current?.focus(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
popup.current?.destroy();
|
||||||
|
popup.current = null;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const hidePopup = useCallback(() => {
|
||||||
|
popup.current?.hide();
|
||||||
|
setIsPopupVisible(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickAIHandle = (e: MouseEvent) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
if (target.matches("#ai-handle") || menuRef.current?.contains(e.target as Node)) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!isPopupVisible) {
|
||||||
|
popup.current?.setProps({
|
||||||
|
getReferenceClientRect: () => target.getBoundingClientRect(),
|
||||||
|
});
|
||||||
|
popup.current?.show();
|
||||||
|
setIsPopupVisible(true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hidePopup();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("click", handleClickAIHandle);
|
||||||
|
document.addEventListener("contextmenu", handleClickAIHandle);
|
||||||
|
document.addEventListener("keydown", hidePopup);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("click", handleClickAIHandle);
|
||||||
|
document.removeEventListener("contextmenu", handleClickAIHandle);
|
||||||
|
document.removeEventListener("keydown", hidePopup);
|
||||||
|
};
|
||||||
|
}, [hidePopup, isPopupVisible]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("opacity-0 pointer-events-none fixed inset-0 size-full z-10 transition-opacity", {
|
||||||
|
"opacity-100 pointer-events-auto": isPopupVisible,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div ref={menuRef} className="z-10">
|
||||||
|
{menu?.({
|
||||||
|
onClose: hidePopup,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -14,7 +14,7 @@ export const BlockMenu = (props: BlockMenuProps) => {
|
||||||
|
|
||||||
const handleClickDragHandle = useCallback((event: MouseEvent) => {
|
const handleClickDragHandle = useCallback((event: MouseEvent) => {
|
||||||
const target = event.target as HTMLElement;
|
const target = event.target as HTMLElement;
|
||||||
if (target.matches(".drag-handle-dots") || target.matches(".drag-handle-dot")) {
|
if (target.matches("#drag-handle")) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
popup.current?.setProps({
|
popup.current?.setProps({
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
export * from "./bubble-menu";
|
export * from "./bubble-menu";
|
||||||
|
export * from "./ai-menu";
|
||||||
export * from "./block-menu";
|
export * from "./block-menu";
|
||||||
export * from "./menu-items";
|
export * from "./menu-items";
|
||||||
|
|
|
||||||
7
packages/editor/src/core/constants/config.ts
Normal file
7
packages/editor/src/core/constants/config.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
// types
|
||||||
|
import { TDisplayConfig } from "@/types";
|
||||||
|
|
||||||
|
export const DEFAULT_DISPLAY_CONFIG: TDisplayConfig = {
|
||||||
|
fontSize: "large-font",
|
||||||
|
fontStyle: "sans-serif",
|
||||||
|
};
|
||||||
|
|
@ -56,7 +56,7 @@ export const CodeBlockComponent: React.FC<CodeBlockComponentProps> = ({ node })
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<pre className="bg-custom-background-90 text-custom-text-100 rounded-lg p-4 my-2">
|
<pre className="bg-custom-background-90 text-custom-text-100 rounded-lg p-4 my-2">
|
||||||
<NodeViewContent as="code" className="whitespace-[pre-wrap]" />
|
<NodeViewContent as="code" className="whitespace-pre-wrap" />
|
||||||
</pre>
|
</pre>
|
||||||
</NodeViewWrapper>
|
</NodeViewWrapper>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Extension } from "@tiptap/core";
|
import { Extension } from "@tiptap/core";
|
||||||
|
|
||||||
export const EnterKeyExtension = (onEnterKeyPress?: (descriptionHTML: string) => void) =>
|
export const EnterKeyExtension = (onEnterKeyPress?: () => void) =>
|
||||||
Extension.create({
|
Extension.create({
|
||||||
name: "enterKey",
|
name: "enterKey",
|
||||||
|
|
||||||
|
|
@ -8,7 +8,9 @@ export const EnterKeyExtension = (onEnterKeyPress?: (descriptionHTML: string) =>
|
||||||
return {
|
return {
|
||||||
Enter: () => {
|
Enter: () => {
|
||||||
if (!this.editor.storage.mentionsOpen) {
|
if (!this.editor.storage.mentionsOpen) {
|
||||||
onEnterKeyPress?.(this.editor.getHTML());
|
if (onEnterKeyPress) {
|
||||||
|
onEnterKeyPress();
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ export * from "./typography";
|
||||||
export * from "./core-without-props";
|
export * from "./core-without-props";
|
||||||
export * from "./document-without-props";
|
export * from "./document-without-props";
|
||||||
export * from "./custom-code-inline";
|
export * from "./custom-code-inline";
|
||||||
export * from "./drag-drop";
|
|
||||||
export * from "./drop";
|
export * from "./drop";
|
||||||
export * from "./enter-key-extension";
|
export * from "./enter-key-extension";
|
||||||
export * from "./extensions";
|
export * from "./extensions";
|
||||||
|
|
@ -18,4 +17,5 @@ export * from "./horizontal-rule";
|
||||||
export * from "./keymap";
|
export * from "./keymap";
|
||||||
export * from "./quote";
|
export * from "./quote";
|
||||||
export * from "./read-only-extensions";
|
export * from "./read-only-extensions";
|
||||||
|
export * from "./side-menu";
|
||||||
export * from "./slash-commands";
|
export * from "./slash-commands";
|
||||||
|
|
|
||||||
205
packages/editor/src/core/extensions/side-menu.tsx
Normal file
205
packages/editor/src/core/extensions/side-menu.tsx
Normal file
|
|
@ -0,0 +1,205 @@
|
||||||
|
import { Extension } from "@tiptap/core";
|
||||||
|
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
||||||
|
import { EditorView } from "@tiptap/pm/view";
|
||||||
|
// plugins
|
||||||
|
import { AIHandlePlugin } from "@/plugins/ai-handle";
|
||||||
|
import { DragHandlePlugin } from "@/plugins/drag-handle";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
aiEnabled: boolean;
|
||||||
|
dragDropEnabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SideMenuPluginProps = {
|
||||||
|
dragHandleWidth: number;
|
||||||
|
handlesConfig: {
|
||||||
|
ai: boolean;
|
||||||
|
dragDrop: boolean;
|
||||||
|
};
|
||||||
|
scrollThreshold: {
|
||||||
|
up: number;
|
||||||
|
down: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SideMenuHandleOptions = {
|
||||||
|
view: (view: EditorView, sideMenu: HTMLDivElement | null) => void;
|
||||||
|
domEvents?: {
|
||||||
|
[key: string]: (...args: any) => void;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SideMenuExtension = (props: Props) => {
|
||||||
|
const { aiEnabled, dragDropEnabled } = props;
|
||||||
|
|
||||||
|
return Extension.create({
|
||||||
|
name: "editorSideMenu",
|
||||||
|
addProseMirrorPlugins() {
|
||||||
|
return [
|
||||||
|
SideMenu({
|
||||||
|
dragHandleWidth: 24,
|
||||||
|
handlesConfig: {
|
||||||
|
ai: aiEnabled,
|
||||||
|
dragDrop: dragDropEnabled,
|
||||||
|
},
|
||||||
|
scrollThreshold: { up: 300, down: 100 },
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const absoluteRect = (node: Element) => {
|
||||||
|
const data = node.getBoundingClientRect();
|
||||||
|
|
||||||
|
return {
|
||||||
|
top: data.top,
|
||||||
|
left: data.left,
|
||||||
|
width: data.width,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const nodeDOMAtCoords = (coords: { x: number; y: number }) => {
|
||||||
|
const elements = document.elementsFromPoint(coords.x, coords.y);
|
||||||
|
const generalSelectors = [
|
||||||
|
"li",
|
||||||
|
"p:not(:first-child)",
|
||||||
|
".code-block",
|
||||||
|
"blockquote",
|
||||||
|
"img",
|
||||||
|
"h1, h2, h3, h4, h5, h6",
|
||||||
|
"[data-type=horizontalRule]",
|
||||||
|
".table-wrapper",
|
||||||
|
".issue-embed",
|
||||||
|
].join(", ");
|
||||||
|
|
||||||
|
for (const elem of elements) {
|
||||||
|
if (elem.matches("p:first-child") && elem.parentElement?.matches(".ProseMirror")) {
|
||||||
|
return elem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the element is a <p> tag that is the first child of a td or th
|
||||||
|
if (
|
||||||
|
(elem.matches("td > p:first-child") || elem.matches("th > p:first-child")) &&
|
||||||
|
elem?.textContent?.trim() !== ""
|
||||||
|
) {
|
||||||
|
return elem; // Return only if p tag is not empty in td or th
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply general selector
|
||||||
|
if (elem.matches(generalSelectors)) {
|
||||||
|
return elem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SideMenu = (options: SideMenuPluginProps) => {
|
||||||
|
const { handlesConfig } = options;
|
||||||
|
const editorSideMenu: HTMLDivElement | null = document.createElement("div");
|
||||||
|
editorSideMenu.id = "editor-side-menu";
|
||||||
|
// side menu view actions
|
||||||
|
const hideSideMenu = () => {
|
||||||
|
if (!editorSideMenu?.classList.contains("side-menu-hidden")) editorSideMenu?.classList.add("side-menu-hidden");
|
||||||
|
};
|
||||||
|
const showSideMenu = () => editorSideMenu?.classList.remove("side-menu-hidden");
|
||||||
|
// side menu elements
|
||||||
|
const { view: dragHandleView, domEvents: dragHandleDOMEvents } = DragHandlePlugin(options);
|
||||||
|
const { view: aiHandleView, domEvents: aiHandleDOMEvents } = AIHandlePlugin(options);
|
||||||
|
|
||||||
|
return new Plugin({
|
||||||
|
key: new PluginKey("sideMenu"),
|
||||||
|
view: (view) => {
|
||||||
|
hideSideMenu();
|
||||||
|
view?.dom.parentElement?.appendChild(editorSideMenu);
|
||||||
|
// side menu elements' initialization
|
||||||
|
if (handlesConfig.ai) {
|
||||||
|
aiHandleView(view, editorSideMenu);
|
||||||
|
}
|
||||||
|
if (handlesConfig.dragDrop) {
|
||||||
|
dragHandleView(view, editorSideMenu);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
destroy: () => hideSideMenu(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
handleDOMEvents: {
|
||||||
|
mousemove: (view, event) => {
|
||||||
|
if (!view.editable) return;
|
||||||
|
|
||||||
|
const node = nodeDOMAtCoords({
|
||||||
|
x: event.clientX + 50 + options.dragHandleWidth,
|
||||||
|
y: event.clientY,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!(node instanceof Element) || node.matches("ul, ol")) {
|
||||||
|
hideSideMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const compStyle = window.getComputedStyle(node);
|
||||||
|
const lineHeight = parseInt(compStyle.lineHeight, 10);
|
||||||
|
const paddingTop = parseInt(compStyle.paddingTop, 10);
|
||||||
|
|
||||||
|
const rect = absoluteRect(node);
|
||||||
|
|
||||||
|
rect.top += (lineHeight - 20) / 2;
|
||||||
|
rect.top += paddingTop;
|
||||||
|
|
||||||
|
if (node.parentElement?.parentElement?.matches("td") || node.parentElement?.parentElement?.matches("th")) {
|
||||||
|
if (node.matches("ul:not([data-type=taskList]) li, ol li")) {
|
||||||
|
rect.left -= 5;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Li markers
|
||||||
|
if (node.matches("ul:not([data-type=taskList]) li, ol li")) {
|
||||||
|
rect.left -= 18;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.matches(".table-wrapper")) {
|
||||||
|
rect.top += 8;
|
||||||
|
rect.left -= 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.parentElement?.matches("td") || node.parentElement?.matches("th")) {
|
||||||
|
rect.left += 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
rect.width = options.dragHandleWidth;
|
||||||
|
|
||||||
|
if (!editorSideMenu) return;
|
||||||
|
|
||||||
|
editorSideMenu.style.left = `${rect.left - rect.width}px`;
|
||||||
|
editorSideMenu.style.top = `${rect.top}px`;
|
||||||
|
showSideMenu();
|
||||||
|
if (handlesConfig.dragDrop) {
|
||||||
|
dragHandleDOMEvents?.mousemove();
|
||||||
|
}
|
||||||
|
if (handlesConfig.ai) {
|
||||||
|
aiHandleDOMEvents?.mousemove?.();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
keydown: () => hideSideMenu(),
|
||||||
|
mousewheel: () => hideSideMenu(),
|
||||||
|
dragenter: (view) => {
|
||||||
|
if (handlesConfig.dragDrop) {
|
||||||
|
dragHandleDOMEvents?.dragenter?.(view);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
drop: (view, event) => {
|
||||||
|
if (handlesConfig.dragDrop) {
|
||||||
|
dragHandleDOMEvents?.drop?.(view, event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dragend: (view) => {
|
||||||
|
if (handlesConfig.dragDrop) {
|
||||||
|
dragHandleDOMEvents?.dragend?.(view);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
@ -3,9 +3,9 @@ import Collaboration from "@tiptap/extension-collaboration";
|
||||||
import { EditorProps } from "@tiptap/pm/view";
|
import { EditorProps } from "@tiptap/pm/view";
|
||||||
import * as Y from "yjs";
|
import * as Y from "yjs";
|
||||||
// extensions
|
// extensions
|
||||||
import { DragAndDrop, IssueWidget } from "@/extensions";
|
import { IssueWidget, SideMenuExtension } from "@/extensions";
|
||||||
// hooks
|
// hooks
|
||||||
import { TFileHandler, useEditor } from "@/hooks/use-editor";
|
import { useEditor } from "@/hooks/use-editor";
|
||||||
// plane editor extensions
|
// plane editor extensions
|
||||||
import { DocumentEditorAdditionalExtensions } from "@/plane-editor/extensions";
|
import { DocumentEditorAdditionalExtensions } from "@/plane-editor/extensions";
|
||||||
// plane editor provider
|
// plane editor provider
|
||||||
|
|
@ -13,9 +13,10 @@ import { CollaborationProvider } from "@/plane-editor/providers";
|
||||||
// plane editor types
|
// plane editor types
|
||||||
import { TEmbedConfig } from "@/plane-editor/types";
|
import { TEmbedConfig } from "@/plane-editor/types";
|
||||||
// types
|
// types
|
||||||
import { EditorRefApi, IMentionHighlight, IMentionSuggestion } from "@/types";
|
import { EditorRefApi, IMentionHighlight, IMentionSuggestion, TExtensions, TFileHandler } from "@/types";
|
||||||
|
|
||||||
type DocumentEditorProps = {
|
type DocumentEditorProps = {
|
||||||
|
disabledExtensions?: TExtensions[];
|
||||||
editorClassName: string;
|
editorClassName: string;
|
||||||
editorProps?: EditorProps;
|
editorProps?: EditorProps;
|
||||||
embedHandler?: TEmbedConfig;
|
embedHandler?: TEmbedConfig;
|
||||||
|
|
@ -29,13 +30,13 @@ type DocumentEditorProps = {
|
||||||
};
|
};
|
||||||
onChange: (updates: Uint8Array) => void;
|
onChange: (updates: Uint8Array) => void;
|
||||||
placeholder?: string | ((isFocused: boolean, value: string) => string);
|
placeholder?: string | ((isFocused: boolean, value: string) => string);
|
||||||
setHideDragHandleFunction: (hideDragHandlerFromDragDrop: () => void) => void;
|
|
||||||
tabIndex?: number;
|
tabIndex?: number;
|
||||||
value: Uint8Array;
|
value: Uint8Array;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useDocumentEditor = (props: DocumentEditorProps) => {
|
export const useDocumentEditor = (props: DocumentEditorProps) => {
|
||||||
const {
|
const {
|
||||||
|
disabledExtensions,
|
||||||
editorClassName,
|
editorClassName,
|
||||||
editorProps = {},
|
editorProps = {},
|
||||||
embedHandler,
|
embedHandler,
|
||||||
|
|
@ -46,7 +47,6 @@ export const useDocumentEditor = (props: DocumentEditorProps) => {
|
||||||
mentionHandler,
|
mentionHandler,
|
||||||
onChange,
|
onChange,
|
||||||
placeholder,
|
placeholder,
|
||||||
setHideDragHandleFunction,
|
|
||||||
tabIndex,
|
tabIndex,
|
||||||
value,
|
value,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
@ -93,7 +93,10 @@ export const useDocumentEditor = (props: DocumentEditorProps) => {
|
||||||
forwardedRef,
|
forwardedRef,
|
||||||
mentionHandler,
|
mentionHandler,
|
||||||
extensions: [
|
extensions: [
|
||||||
DragAndDrop(setHideDragHandleFunction),
|
SideMenuExtension({
|
||||||
|
aiEnabled: !disabledExtensions?.includes("ai"),
|
||||||
|
dragDropEnabled: true,
|
||||||
|
}),
|
||||||
embedHandler?.issue &&
|
embedHandler?.issue &&
|
||||||
IssueWidget({
|
IssueWidget({
|
||||||
widgetCallback: embedHandler.issue.widgetCallback,
|
widgetCallback: embedHandler.issue.widgetCallback,
|
||||||
|
|
@ -102,6 +105,7 @@ export const useDocumentEditor = (props: DocumentEditorProps) => {
|
||||||
document: provider.document,
|
document: provider.document,
|
||||||
}),
|
}),
|
||||||
...DocumentEditorAdditionalExtensions({
|
...DocumentEditorAdditionalExtensions({
|
||||||
|
disabledExtensions,
|
||||||
fileHandler,
|
fileHandler,
|
||||||
issueEmbedConfig: embedHandler?.issue,
|
issueEmbedConfig: embedHandler?.issue,
|
||||||
}),
|
}),
|
||||||
|
|
@ -111,5 +115,8 @@ export const useDocumentEditor = (props: DocumentEditorProps) => {
|
||||||
tabIndex,
|
tabIndex,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { editor, isIndexedDbSynced };
|
return {
|
||||||
|
editor,
|
||||||
|
isIndexedDbSynced,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { useImperativeHandle, useRef, MutableRefObject, useState, useEffect } from "react";
|
import { useImperativeHandle, useRef, MutableRefObject, useState, useEffect } from "react";
|
||||||
|
import { DOMSerializer } from "@tiptap/pm/model";
|
||||||
import { Selection } from "@tiptap/pm/state";
|
import { Selection } from "@tiptap/pm/state";
|
||||||
import { EditorProps } from "@tiptap/pm/view";
|
import { EditorProps } from "@tiptap/pm/view";
|
||||||
import { useEditor as useCustomEditor, Editor } from "@tiptap/react";
|
import { useEditor as useTiptapEditor, Editor } from "@tiptap/react";
|
||||||
// components
|
// components
|
||||||
import { getEditorMenuItems } from "@/components/menus";
|
import { getEditorMenuItems } from "@/components/menus";
|
||||||
// extensions
|
// extensions
|
||||||
|
|
@ -14,22 +15,7 @@ import { CollaborationProvider } from "@/plane-editor/providers";
|
||||||
// props
|
// props
|
||||||
import { CoreEditorProps } from "@/props";
|
import { CoreEditorProps } from "@/props";
|
||||||
// types
|
// types
|
||||||
import {
|
import { EditorRefApi, IMentionHighlight, IMentionSuggestion, TEditorCommands, TFileHandler } from "@/types";
|
||||||
DeleteImage,
|
|
||||||
EditorRefApi,
|
|
||||||
IMentionHighlight,
|
|
||||||
IMentionSuggestion,
|
|
||||||
RestoreImage,
|
|
||||||
TEditorCommands,
|
|
||||||
UploadImage,
|
|
||||||
} from "@/types";
|
|
||||||
|
|
||||||
export type TFileHandler = {
|
|
||||||
cancel: () => void;
|
|
||||||
delete: DeleteImage;
|
|
||||||
upload: UploadImage;
|
|
||||||
restore: RestoreImage;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface CustomEditorProps {
|
export interface CustomEditorProps {
|
||||||
editorClassName: string;
|
editorClassName: string;
|
||||||
|
|
@ -54,7 +40,8 @@ export interface CustomEditorProps {
|
||||||
value?: string | null | undefined;
|
value?: string | null | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useEditor = ({
|
export const useEditor = (props: CustomEditorProps) => {
|
||||||
|
const {
|
||||||
editorClassName,
|
editorClassName,
|
||||||
editorProps = {},
|
editorProps = {},
|
||||||
enableHistory,
|
enableHistory,
|
||||||
|
|
@ -70,10 +57,13 @@ export const useEditor = ({
|
||||||
provider,
|
provider,
|
||||||
tabIndex,
|
tabIndex,
|
||||||
value,
|
value,
|
||||||
}: CustomEditorProps) => {
|
} = props;
|
||||||
const editor = useCustomEditor({
|
|
||||||
|
const editor = useTiptapEditor({
|
||||||
editorProps: {
|
editorProps: {
|
||||||
...CoreEditorProps(editorClassName),
|
...CoreEditorProps({
|
||||||
|
editorClassName,
|
||||||
|
}),
|
||||||
...editorProps,
|
...editorProps,
|
||||||
},
|
},
|
||||||
extensions: [
|
extensions: [
|
||||||
|
|
@ -95,18 +85,10 @@ export const useEditor = ({
|
||||||
...extensions,
|
...extensions,
|
||||||
],
|
],
|
||||||
content: typeof initialValue === "string" && initialValue.trim() !== "" ? initialValue : "<p></p>",
|
content: typeof initialValue === "string" && initialValue.trim() !== "" ? initialValue : "<p></p>",
|
||||||
onCreate: async () => {
|
onCreate: () => handleEditorReady?.(true),
|
||||||
handleEditorReady?.(true);
|
onTransaction: ({ editor }) => setSavedSelection(editor.state.selection),
|
||||||
},
|
onUpdate: ({ editor }) => onChange?.(editor.getJSON(), editor.getHTML()),
|
||||||
onTransaction: async ({ editor }) => {
|
onDestroy: () => handleEditorReady?.(false),
|
||||||
setSavedSelection(editor.state.selection);
|
|
||||||
},
|
|
||||||
onUpdate: async ({ editor }) => {
|
|
||||||
onChange?.(editor.getJSON(), editor.getHTML());
|
|
||||||
},
|
|
||||||
onDestroy: async () => {
|
|
||||||
handleEditorReady?.(false);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const editorRef: MutableRefObject<Editor | null> = useRef(null);
|
const editorRef: MutableRefObject<Editor | null> = useRef(null);
|
||||||
|
|
@ -232,6 +214,41 @@ export const useEditor = ({
|
||||||
console.error("An error occurred while setting focus at position:", error);
|
console.error("An error occurred while setting focus at position:", error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
getSelectedText: () => {
|
||||||
|
if (!editorRef.current) return null;
|
||||||
|
|
||||||
|
const { state } = editorRef.current;
|
||||||
|
const { from, to, empty } = state.selection;
|
||||||
|
|
||||||
|
if (empty) return null;
|
||||||
|
|
||||||
|
const nodesArray: string[] = [];
|
||||||
|
state.doc.nodesBetween(from, to, (node, pos, parent) => {
|
||||||
|
if (parent === state.doc && editorRef.current) {
|
||||||
|
const serializer = DOMSerializer.fromSchema(editorRef.current?.schema);
|
||||||
|
const dom = serializer.serializeNode(node);
|
||||||
|
const tempDiv = document.createElement("div");
|
||||||
|
tempDiv.appendChild(dom);
|
||||||
|
nodesArray.push(tempDiv.innerHTML);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const selection = nodesArray.join("");
|
||||||
|
console.log(selection);
|
||||||
|
return selection;
|
||||||
|
},
|
||||||
|
insertText: (contentHTML, insertOnNextLine) => {
|
||||||
|
if (!editor) return;
|
||||||
|
// get selection
|
||||||
|
const { from, to, empty } = editor.state.selection;
|
||||||
|
if (empty) return;
|
||||||
|
if (insertOnNextLine) {
|
||||||
|
// move cursor to the end of the selection and insert a new line
|
||||||
|
editor.chain().focus().setTextSelection(to).insertContent("<br />").insertContent(contentHTML).run();
|
||||||
|
} else {
|
||||||
|
// replace selected text with the content provided
|
||||||
|
editor.chain().focus().deleteRange({ from, to }).insertContent(contentHTML).run();
|
||||||
|
}
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
[editorRef, savedSelection, fileHandler.upload]
|
[editorRef, savedSelection, fileHandler.upload]
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,9 @@ export const useReadOnlyEditor = ({
|
||||||
editable: false,
|
editable: false,
|
||||||
content: typeof initialValue === "string" && initialValue.trim() !== "" ? initialValue : "<p></p>",
|
content: typeof initialValue === "string" && initialValue.trim() !== "" ? initialValue : "<p></p>",
|
||||||
editorProps: {
|
editorProps: {
|
||||||
...CoreReadOnlyEditorProps(editorClassName),
|
...CoreReadOnlyEditorProps({
|
||||||
|
editorClassName,
|
||||||
|
}),
|
||||||
...editorProps,
|
...editorProps,
|
||||||
},
|
},
|
||||||
onCreate: async () => {
|
onCreate: async () => {
|
||||||
|
|
|
||||||
153
packages/editor/src/core/plugins/ai-handle.ts
Normal file
153
packages/editor/src/core/plugins/ai-handle.ts
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
import { NodeSelection } from "@tiptap/pm/state";
|
||||||
|
import { EditorView } from "@tiptap/pm/view";
|
||||||
|
// extensions
|
||||||
|
import { SideMenuHandleOptions, SideMenuPluginProps } from "@/extensions";
|
||||||
|
|
||||||
|
const sparklesIcon =
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-sparkles"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/><path d="M4 17v2"/><path d="M5 18H3"/></svg>';
|
||||||
|
|
||||||
|
const nodeDOMAtCoords = (coords: { x: number; y: number }) => {
|
||||||
|
const elements = document.elementsFromPoint(coords.x, coords.y);
|
||||||
|
const generalSelectors = [
|
||||||
|
"li",
|
||||||
|
"p:not(:first-child)",
|
||||||
|
".code-block",
|
||||||
|
"blockquote",
|
||||||
|
"img",
|
||||||
|
"h1, h2, h3, h4, h5, h6",
|
||||||
|
"[data-type=horizontalRule]",
|
||||||
|
".table-wrapper",
|
||||||
|
".issue-embed",
|
||||||
|
].join(", ");
|
||||||
|
|
||||||
|
for (const elem of elements) {
|
||||||
|
if (elem.matches("p:first-child") && elem.parentElement?.matches(".ProseMirror")) {
|
||||||
|
return elem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the element is a <p> tag that is the first child of a td or th
|
||||||
|
if (
|
||||||
|
(elem.matches("td > p:first-child") || elem.matches("th > p:first-child")) &&
|
||||||
|
elem?.textContent?.trim() !== ""
|
||||||
|
) {
|
||||||
|
return elem; // Return only if p tag is not empty in td or th
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply general selector
|
||||||
|
if (elem.matches(generalSelectors)) {
|
||||||
|
return elem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const nodePosAtDOM = (node: Element, view: EditorView, options: SideMenuPluginProps) => {
|
||||||
|
const boundingRect = node.getBoundingClientRect();
|
||||||
|
|
||||||
|
return view.posAtCoords({
|
||||||
|
left: boundingRect.left + 50 + options.dragHandleWidth,
|
||||||
|
top: boundingRect.top + 1,
|
||||||
|
})?.inside;
|
||||||
|
};
|
||||||
|
|
||||||
|
const nodePosAtDOMForBlockQuotes = (node: Element, view: EditorView) => {
|
||||||
|
const boundingRect = node.getBoundingClientRect();
|
||||||
|
|
||||||
|
return view.posAtCoords({
|
||||||
|
left: boundingRect.left + 1,
|
||||||
|
top: boundingRect.top + 1,
|
||||||
|
})?.inside;
|
||||||
|
};
|
||||||
|
|
||||||
|
const calcNodePos = (pos: number, view: EditorView, node: Element) => {
|
||||||
|
const maxPos = view.state.doc.content.size;
|
||||||
|
const safePos = Math.max(0, Math.min(pos, maxPos));
|
||||||
|
const $pos = view.state.doc.resolve(safePos);
|
||||||
|
|
||||||
|
if ($pos.depth > 1) {
|
||||||
|
if (node.matches("ul li, ol li")) {
|
||||||
|
// only for nested lists
|
||||||
|
const newPos = $pos.before($pos.depth);
|
||||||
|
return Math.max(0, Math.min(newPos, maxPos));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return safePos;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AIHandlePlugin = (options: SideMenuPluginProps): SideMenuHandleOptions => {
|
||||||
|
let aiHandleElement: HTMLButtonElement | null = null;
|
||||||
|
|
||||||
|
const handleClick = (event: MouseEvent, view: EditorView) => {
|
||||||
|
view.focus();
|
||||||
|
|
||||||
|
const node = nodeDOMAtCoords({
|
||||||
|
x: event.clientX + 50 + options.dragHandleWidth,
|
||||||
|
y: event.clientY,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!(node instanceof Element)) return;
|
||||||
|
|
||||||
|
if (node.matches("blockquote")) {
|
||||||
|
let nodePosForBlockQuotes = nodePosAtDOMForBlockQuotes(node, view);
|
||||||
|
if (nodePosForBlockQuotes === null || nodePosForBlockQuotes === undefined) return;
|
||||||
|
|
||||||
|
const docSize = view.state.doc.content.size;
|
||||||
|
nodePosForBlockQuotes = Math.max(0, Math.min(nodePosForBlockQuotes, docSize));
|
||||||
|
|
||||||
|
if (nodePosForBlockQuotes >= 0 && nodePosForBlockQuotes <= docSize) {
|
||||||
|
// TODO FIX ERROR
|
||||||
|
const nodeSelection = NodeSelection.create(view.state.doc, nodePosForBlockQuotes);
|
||||||
|
view.dispatch(view.state.tr.setSelection(nodeSelection));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let nodePos = nodePosAtDOM(node, view, options);
|
||||||
|
|
||||||
|
if (nodePos === null || nodePos === undefined) return;
|
||||||
|
|
||||||
|
// Adjust the nodePos to point to the start of the node, ensuring NodeSelection can be applied
|
||||||
|
nodePos = calcNodePos(nodePos, view, node);
|
||||||
|
|
||||||
|
// TODO FIX ERROR
|
||||||
|
// Use NodeSelection to select the node at the calculated position
|
||||||
|
const nodeSelection = NodeSelection.create(view.state.doc, nodePos);
|
||||||
|
|
||||||
|
// Dispatch the transaction to update the selection
|
||||||
|
view.dispatch(view.state.tr.setSelection(nodeSelection));
|
||||||
|
};
|
||||||
|
|
||||||
|
const view = (view: EditorView, sideMenu: HTMLDivElement | null) => {
|
||||||
|
// create handle element
|
||||||
|
const className =
|
||||||
|
"grid place-items-center font-medium size-5 aspect-square text-xs text-custom-text-300 hover:bg-custom-background-80 rounded-sm opacity-100 !outline-none z-[5] transition-[background-color,_opacity] duration-200 ease-linear";
|
||||||
|
aiHandleElement = document.createElement("button");
|
||||||
|
aiHandleElement.type = "button";
|
||||||
|
aiHandleElement.id = "ai-handle";
|
||||||
|
aiHandleElement.classList.value = className;
|
||||||
|
const iconElement = document.createElement("span");
|
||||||
|
iconElement.classList.value = "pointer-events-none";
|
||||||
|
iconElement.innerHTML = sparklesIcon;
|
||||||
|
aiHandleElement.appendChild(iconElement);
|
||||||
|
// bind events
|
||||||
|
aiHandleElement.addEventListener("click", (e) => handleClick(e, view));
|
||||||
|
|
||||||
|
sideMenu?.appendChild(aiHandleElement);
|
||||||
|
|
||||||
|
return {
|
||||||
|
// destroy the handle element on un-initialize
|
||||||
|
destroy: () => {
|
||||||
|
aiHandleElement?.remove();
|
||||||
|
aiHandleElement = null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const domEvents = {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
view,
|
||||||
|
domEvents,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -1,68 +1,35 @@
|
||||||
import { Extension } from "@tiptap/core";
|
|
||||||
import { Fragment, Slice, Node } from "@tiptap/pm/model";
|
import { Fragment, Slice, Node } from "@tiptap/pm/model";
|
||||||
import { NodeSelection, Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
|
import { NodeSelection, TextSelection } from "@tiptap/pm/state";
|
||||||
// @ts-expect-error __serializeForClipboard's is not exported
|
// @ts-expect-error __serializeForClipboard's is not exported
|
||||||
import { __serializeForClipboard, EditorView } from "@tiptap/pm/view";
|
import { __serializeForClipboard, EditorView } from "@tiptap/pm/view";
|
||||||
|
// extensions
|
||||||
|
import { SideMenuHandleOptions, SideMenuPluginProps } from "@/extensions";
|
||||||
|
|
||||||
export interface DragHandleOptions {
|
const verticalEllipsisIcon =
|
||||||
dragHandleWidth: number;
|
'<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-ellipsis-vertical"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg>';
|
||||||
setHideDragHandle?: (hideDragHandlerFromDragDrop: () => void) => void;
|
|
||||||
scrollThreshold: {
|
|
||||||
up: number;
|
|
||||||
down: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DragAndDrop = (setHideDragHandle?: (hideDragHandlerFromDragDrop: () => void) => void) =>
|
|
||||||
Extension.create({
|
|
||||||
name: "dragAndDrop",
|
|
||||||
|
|
||||||
addProseMirrorPlugins() {
|
|
||||||
return [
|
|
||||||
DragHandle({
|
|
||||||
dragHandleWidth: 24,
|
|
||||||
scrollThreshold: { up: 300, down: 100 },
|
|
||||||
setHideDragHandle,
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const createDragHandleElement = (): HTMLElement => {
|
const createDragHandleElement = (): HTMLElement => {
|
||||||
const dragHandleElement = document.createElement("button");
|
const dragHandleElement = document.createElement("button");
|
||||||
dragHandleElement.type = "button";
|
dragHandleElement.type = "button";
|
||||||
|
dragHandleElement.id = "drag-handle";
|
||||||
dragHandleElement.draggable = true;
|
dragHandleElement.draggable = true;
|
||||||
dragHandleElement.dataset.dragHandle = "";
|
dragHandleElement.dataset.dragHandle = "";
|
||||||
dragHandleElement.classList.add("drag-handle");
|
dragHandleElement.classList.value =
|
||||||
|
"hidden sm:flex items-center size-5 aspect-square rounded-sm cursor-grab outline-none hover:bg-custom-background-80 active:bg-custom-background-80 active:cursor-grabbing transition-[background-color,_opacity] duration-200 ease-linear";
|
||||||
|
|
||||||
const dragHandleContainer = document.createElement("span");
|
const iconElement1 = document.createElement("span");
|
||||||
dragHandleContainer.classList.add("drag-handle-container");
|
iconElement1.classList.value = "pointer-events-none text-custom-text-300";
|
||||||
dragHandleElement.appendChild(dragHandleContainer);
|
iconElement1.innerHTML = verticalEllipsisIcon;
|
||||||
|
const iconElement2 = document.createElement("span");
|
||||||
|
iconElement2.classList.value = "pointer-events-none text-custom-text-300 -ml-2.5";
|
||||||
|
iconElement2.innerHTML = verticalEllipsisIcon;
|
||||||
|
|
||||||
const dotsContainer = document.createElement("span");
|
dragHandleElement.appendChild(iconElement1);
|
||||||
dotsContainer.classList.add("drag-handle-dots");
|
dragHandleElement.appendChild(iconElement2);
|
||||||
|
|
||||||
for (let i = 0; i < 6; i++) {
|
|
||||||
const spanElement = document.createElement("span");
|
|
||||||
spanElement.classList.add("drag-handle-dot");
|
|
||||||
dotsContainer.appendChild(spanElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
dragHandleContainer.appendChild(dotsContainer);
|
|
||||||
|
|
||||||
return dragHandleElement;
|
return dragHandleElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
const absoluteRect = (node: Element) => {
|
|
||||||
const data = node.getBoundingClientRect();
|
|
||||||
|
|
||||||
return {
|
|
||||||
top: data.top,
|
|
||||||
left: data.left,
|
|
||||||
width: data.width,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const nodeDOMAtCoords = (coords: { x: number; y: number }) => {
|
const nodeDOMAtCoords = (coords: { x: number; y: number }) => {
|
||||||
const elements = document.elementsFromPoint(coords.x, coords.y);
|
const elements = document.elementsFromPoint(coords.x, coords.y);
|
||||||
const generalSelectors = [
|
const generalSelectors = [
|
||||||
|
|
@ -98,7 +65,7 @@ const nodeDOMAtCoords = (coords: { x: number; y: number }) => {
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const nodePosAtDOM = (node: Element, view: EditorView, options: DragHandleOptions) => {
|
const nodePosAtDOM = (node: Element, view: EditorView, options: SideMenuPluginProps) => {
|
||||||
const boundingRect = node.getBoundingClientRect();
|
const boundingRect = node.getBoundingClientRect();
|
||||||
|
|
||||||
return view.posAtCoords({
|
return view.posAtCoords({
|
||||||
|
|
@ -132,7 +99,7 @@ const calcNodePos = (pos: number, view: EditorView, node: Element) => {
|
||||||
return safePos;
|
return safePos;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DragHandle = (options: DragHandleOptions) => {
|
export const DragHandlePlugin = (options: SideMenuPluginProps): SideMenuHandleOptions => {
|
||||||
let listType = "";
|
let listType = "";
|
||||||
const handleDragStart = (event: DragEvent, view: EditorView) => {
|
const handleDragStart = (event: DragEvent, view: EditorView) => {
|
||||||
view.focus();
|
view.focus();
|
||||||
|
|
@ -222,15 +189,15 @@ const DragHandle = (options: DragHandleOptions) => {
|
||||||
if (!(node instanceof Element)) return;
|
if (!(node instanceof Element)) return;
|
||||||
|
|
||||||
if (node.matches("blockquote")) {
|
if (node.matches("blockquote")) {
|
||||||
let nodePosForBlockquotes = nodePosAtDOMForBlockQuotes(node, view);
|
let nodePosForBlockQuotes = nodePosAtDOMForBlockQuotes(node, view);
|
||||||
if (nodePosForBlockquotes === null || nodePosForBlockquotes === undefined) return;
|
if (nodePosForBlockQuotes === null || nodePosForBlockQuotes === undefined) return;
|
||||||
|
|
||||||
const docSize = view.state.doc.content.size;
|
const docSize = view.state.doc.content.size;
|
||||||
nodePosForBlockquotes = Math.max(0, Math.min(nodePosForBlockquotes, docSize));
|
nodePosForBlockQuotes = Math.max(0, Math.min(nodePosForBlockQuotes, docSize));
|
||||||
|
|
||||||
if (nodePosForBlockquotes >= 0 && nodePosForBlockquotes <= docSize) {
|
if (nodePosForBlockQuotes >= 0 && nodePosForBlockQuotes <= docSize) {
|
||||||
// TODO FIX ERROR
|
// TODO FIX ERROR
|
||||||
const nodeSelection = NodeSelection.create(view.state.doc, nodePosForBlockquotes);
|
const nodeSelection = NodeSelection.create(view.state.doc, nodePosForBlockQuotes);
|
||||||
view.dispatch(view.state.tr.setSelection(nodeSelection));
|
view.dispatch(view.state.tr.setSelection(nodeSelection));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|
@ -253,14 +220,13 @@ const DragHandle = (options: DragHandleOptions) => {
|
||||||
|
|
||||||
let dragHandleElement: HTMLElement | null = null;
|
let dragHandleElement: HTMLElement | null = null;
|
||||||
// drag handle view actions
|
// drag handle view actions
|
||||||
const hideDragHandle = () => dragHandleElement?.classList.add("drag-handle-hidden");
|
|
||||||
const showDragHandle = () => dragHandleElement?.classList.remove("drag-handle-hidden");
|
const showDragHandle = () => dragHandleElement?.classList.remove("drag-handle-hidden");
|
||||||
|
const hideDragHandle = () => {
|
||||||
|
if (!dragHandleElement?.classList.contains("drag-handle-hidden"))
|
||||||
|
dragHandleElement?.classList.add("drag-handle-hidden");
|
||||||
|
};
|
||||||
|
|
||||||
options.setHideDragHandle?.(hideDragHandle);
|
const view = (view: EditorView, sideMenu: HTMLDivElement | null) => {
|
||||||
|
|
||||||
return new Plugin({
|
|
||||||
key: new PluginKey("dragHandle"),
|
|
||||||
view: (view) => {
|
|
||||||
dragHandleElement = createDragHandleElement();
|
dragHandleElement = createDragHandleElement();
|
||||||
dragHandleElement.addEventListener("dragstart", (e) => handleDragStart(e, view));
|
dragHandleElement.addEventListener("dragstart", (e) => handleDragStart(e, view));
|
||||||
dragHandleElement.addEventListener("click", (e) => handleClick(e, view));
|
dragHandleElement.addEventListener("click", (e) => handleClick(e, view));
|
||||||
|
|
@ -279,7 +245,7 @@ const DragHandle = (options: DragHandleOptions) => {
|
||||||
|
|
||||||
hideDragHandle();
|
hideDragHandle();
|
||||||
|
|
||||||
view?.dom?.parentElement?.appendChild(dragHandleElement);
|
sideMenu?.appendChild(dragHandleElement);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
destroy: () => {
|
destroy: () => {
|
||||||
|
|
@ -287,70 +253,14 @@ const DragHandle = (options: DragHandleOptions) => {
|
||||||
dragHandleElement = null;
|
dragHandleElement = null;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
};
|
||||||
props: {
|
const domEvents = {
|
||||||
handleDOMEvents: {
|
mousemove: () => showDragHandle(),
|
||||||
mousemove: (view, event) => {
|
dragenter: (view: EditorView) => {
|
||||||
if (!view.editable) return;
|
|
||||||
|
|
||||||
const node = nodeDOMAtCoords({
|
|
||||||
x: event.clientX + 50 + options.dragHandleWidth,
|
|
||||||
y: event.clientY,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!(node instanceof Element) || node.matches("ul, ol")) {
|
|
||||||
hideDragHandle();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const compStyle = window.getComputedStyle(node);
|
|
||||||
const lineHeight = parseInt(compStyle.lineHeight, 10);
|
|
||||||
const paddingTop = parseInt(compStyle.paddingTop, 10);
|
|
||||||
|
|
||||||
const rect = absoluteRect(node);
|
|
||||||
|
|
||||||
rect.top += (lineHeight - 20) / 2;
|
|
||||||
rect.top += paddingTop;
|
|
||||||
|
|
||||||
if (node.parentElement?.parentElement?.matches("td") || node.parentElement?.parentElement?.matches("th")) {
|
|
||||||
if (node.matches("ul:not([data-type=taskList]) li, ol li")) {
|
|
||||||
rect.left -= 5;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Li markers
|
|
||||||
if (node.matches("ul:not([data-type=taskList]) li, ol li")) {
|
|
||||||
rect.left -= 18;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.matches(".table-wrapper")) {
|
|
||||||
rect.top += 8;
|
|
||||||
rect.left -= 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.parentElement?.matches("td") || node.parentElement?.matches("th")) {
|
|
||||||
rect.left += 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
rect.width = options.dragHandleWidth;
|
|
||||||
|
|
||||||
if (!dragHandleElement) return;
|
|
||||||
|
|
||||||
dragHandleElement.style.left = `${rect.left - rect.width}px`;
|
|
||||||
dragHandleElement.style.top = `${rect.top}px`;
|
|
||||||
showDragHandle();
|
|
||||||
},
|
|
||||||
keydown: () => {
|
|
||||||
hideDragHandle();
|
|
||||||
},
|
|
||||||
mousewheel: () => {
|
|
||||||
hideDragHandle();
|
|
||||||
},
|
|
||||||
dragenter: (view) => {
|
|
||||||
view.dom.classList.add("dragging");
|
view.dom.classList.add("dragging");
|
||||||
hideDragHandle();
|
hideDragHandle();
|
||||||
},
|
},
|
||||||
drop: (view, event) => {
|
drop: (view: EditorView, event: DragEvent) => {
|
||||||
view.dom.classList.remove("dragging");
|
view.dom.classList.remove("dragging");
|
||||||
hideDragHandle();
|
hideDragHandle();
|
||||||
let droppedNode: Node | null = null;
|
let droppedNode: Node | null = null;
|
||||||
|
|
@ -395,10 +305,13 @@ const DragHandle = (options: DragHandleOptions) => {
|
||||||
view.dragging = { slice, move: event.ctrlKey };
|
view.dragging = { slice, move: event.ctrlKey };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dragend: (view) => {
|
dragend: (view: EditorView) => {
|
||||||
view.dom.classList.remove("dragging");
|
view.dom.classList.remove("dragging");
|
||||||
},
|
},
|
||||||
},
|
};
|
||||||
},
|
|
||||||
});
|
return {
|
||||||
|
view,
|
||||||
|
domEvents,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -2,7 +2,13 @@ import { EditorProps } from "@tiptap/pm/view";
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "@/helpers/common";
|
import { cn } from "@/helpers/common";
|
||||||
|
|
||||||
export function CoreEditorProps(editorClassName: string): EditorProps {
|
export type TCoreEditorProps = {
|
||||||
|
editorClassName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CoreEditorProps = (props: TCoreEditorProps): EditorProps => {
|
||||||
|
const { editorClassName } = props;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
attributes: {
|
attributes: {
|
||||||
class: cn(
|
class: cn(
|
||||||
|
|
@ -25,4 +31,4 @@ export function CoreEditorProps(editorClassName: string): EditorProps {
|
||||||
return html.replace(/<img.*?>/g, "");
|
return html.replace(/<img.*?>/g, "");
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,18 @@
|
||||||
import { EditorProps } from "@tiptap/pm/view";
|
import { EditorProps } from "@tiptap/pm/view";
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "@/helpers/common";
|
import { cn } from "@/helpers/common";
|
||||||
|
// props
|
||||||
|
import { TCoreEditorProps } from "@/props";
|
||||||
|
|
||||||
export const CoreReadOnlyEditorProps = (editorClassName: string): EditorProps => ({
|
export const CoreReadOnlyEditorProps = (props: TCoreEditorProps): EditorProps => {
|
||||||
|
const { editorClassName } = props;
|
||||||
|
|
||||||
|
return {
|
||||||
attributes: {
|
attributes: {
|
||||||
class: cn(
|
class: cn(
|
||||||
"prose prose-brand max-w-full prose-headings:font-display font-default focus:outline-none",
|
"prose prose-brand max-w-full prose-headings:font-display font-default focus:outline-none",
|
||||||
editorClassName
|
editorClassName
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
};
|
||||||
|
|
|
||||||
7
packages/editor/src/core/types/ai.ts
Normal file
7
packages/editor/src/core/types/ai.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
type TMenuProps = {
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAIHandler = {
|
||||||
|
menu?: (props: TMenuProps) => React.ReactNode;
|
||||||
|
};
|
||||||
17
packages/editor/src/core/types/config.ts
Normal file
17
packages/editor/src/core/types/config.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { DeleteImage, RestoreImage, UploadImage } from "@/types";
|
||||||
|
|
||||||
|
export type TFileHandler = {
|
||||||
|
cancel: () => void;
|
||||||
|
delete: DeleteImage;
|
||||||
|
upload: UploadImage;
|
||||||
|
restore: RestoreImage;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TEditorFontStyle = "sans-serif" | "serif" | "monospace";
|
||||||
|
|
||||||
|
export type TEditorFontSize = "small-font" | "large-font";
|
||||||
|
|
||||||
|
export type TDisplayConfig = {
|
||||||
|
fontStyle?: TEditorFontStyle;
|
||||||
|
fontSize?: TEditorFontSize;
|
||||||
|
};
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
// helpers
|
// helpers
|
||||||
import { IMarking } from "@/helpers/scroll-to-node";
|
import { IMarking } from "@/helpers/scroll-to-node";
|
||||||
// hooks
|
|
||||||
import { TFileHandler } from "@/hooks/use-editor";
|
|
||||||
// types
|
// types
|
||||||
import { IMentionHighlight, IMentionSuggestion, TEditorCommands } from "@/types";
|
import { IMentionHighlight, IMentionSuggestion, TDisplayConfig, TEditorCommands, TFileHandler } from "@/types";
|
||||||
|
|
||||||
export type EditorReadOnlyRefApi = {
|
export type EditorReadOnlyRefApi = {
|
||||||
getMarkDown: () => string;
|
getMarkDown: () => string;
|
||||||
|
|
@ -22,10 +20,13 @@ export interface EditorRefApi extends EditorReadOnlyRefApi {
|
||||||
isEditorReadyToDiscard: () => boolean;
|
isEditorReadyToDiscard: () => boolean;
|
||||||
setSynced: () => void;
|
setSynced: () => void;
|
||||||
hasUnsyncedChanges: () => boolean;
|
hasUnsyncedChanges: () => boolean;
|
||||||
|
getSelectedText: () => string | null;
|
||||||
|
insertText: (contentHTML: string, insertOnNextLine?: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IEditorProps {
|
export interface IEditorProps {
|
||||||
containerClassName?: string;
|
containerClassName?: string;
|
||||||
|
displayConfig?: TDisplayConfig;
|
||||||
editorClassName?: string;
|
editorClassName?: string;
|
||||||
fileHandler: TFileHandler;
|
fileHandler: TFileHandler;
|
||||||
forwardedRef?: React.MutableRefObject<EditorRefApi | null>;
|
forwardedRef?: React.MutableRefObject<EditorRefApi | null>;
|
||||||
|
|
@ -36,7 +37,7 @@ export interface IEditorProps {
|
||||||
suggestions?: () => Promise<IMentionSuggestion[]>;
|
suggestions?: () => Promise<IMentionSuggestion[]>;
|
||||||
};
|
};
|
||||||
onChange?: (json: object, html: string) => void;
|
onChange?: (json: object, html: string) => void;
|
||||||
onEnterKeyPress?: (descriptionHTML: string) => void;
|
onEnterKeyPress?: (e?: any) => void;
|
||||||
placeholder?: string | ((isFocused: boolean, value: string) => string);
|
placeholder?: string | ((isFocused: boolean, value: string) => string);
|
||||||
tabIndex?: number;
|
tabIndex?: number;
|
||||||
value?: string | null;
|
value?: string | null;
|
||||||
|
|
@ -50,6 +51,7 @@ export interface IRichTextEditor extends IEditorProps {
|
||||||
|
|
||||||
export interface IReadOnlyEditorProps {
|
export interface IReadOnlyEditorProps {
|
||||||
containerClassName?: string;
|
containerClassName?: string;
|
||||||
|
displayConfig?: TDisplayConfig;
|
||||||
editorClassName?: string;
|
editorClassName?: string;
|
||||||
forwardedRef?: React.MutableRefObject<EditorReadOnlyRefApi | null>;
|
forwardedRef?: React.MutableRefObject<EditorReadOnlyRefApi | null>;
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
1
packages/editor/src/core/types/extensions.ts
Normal file
1
packages/editor/src/core/types/extensions.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export type TExtensions = "ai" | "issue-embed";
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
|
export * from "./ai";
|
||||||
|
export * from "./config";
|
||||||
export * from "./editor";
|
export * from "./editor";
|
||||||
export * from "./embed";
|
export * from "./embed";
|
||||||
|
export * from "./extensions";
|
||||||
export * from "./image";
|
export * from "./image";
|
||||||
export * from "./mention-suggestion";
|
export * from "./mention-suggestion";
|
||||||
export * from "./slash-commands-suggestion";
|
export * from "./slash-commands-suggestion";
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,8 @@ export type TEditorCommands =
|
||||||
| "code"
|
| "code"
|
||||||
| "table"
|
| "table"
|
||||||
| "image"
|
| "image"
|
||||||
| "divider";
|
| "divider"
|
||||||
|
| "issue-embed";
|
||||||
|
|
||||||
export type CommandProps = {
|
export type CommandProps = {
|
||||||
editor: Editor;
|
editor: Editor;
|
||||||
|
|
|
||||||
|
|
@ -34,5 +34,5 @@ export { type IMarking, useEditorMarkings } from "@/hooks/use-editor-markings";
|
||||||
export { useReadOnlyEditor } from "@/hooks/use-read-only-editor";
|
export { useReadOnlyEditor } from "@/hooks/use-read-only-editor";
|
||||||
|
|
||||||
// types
|
// types
|
||||||
export type { CustomEditorProps, TFileHandler } from "@/hooks/use-editor";
|
export type { CustomEditorProps } from "@/hooks/use-editor";
|
||||||
export * from "@/types";
|
export * from "@/types";
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue