Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 707570ca7a | |||
| c76af7d7d6 | |||
| 1dcea9bcc8 | |||
| da957e06b6 | |||
| a0b9596cb4 | |||
| f71e8a3a0f | |||
| 002fb4547b | |||
| c1b1ba35c1 | |||
| 4566d6e80c | |||
| e8d359e625 | |||
| 351eba8d61 | |||
| 1e27e37b51 | |||
| 7df2e9cf11 | |||
| c6e3f1b932 |
@@ -2,7 +2,7 @@ name: Bug report
|
||||
description: Create a bug report to help us improve Plane
|
||||
title: "[bug]: "
|
||||
labels: [🐛bug]
|
||||
assignees: [vihar, pushya22]
|
||||
assignees: [srinivaspendem, pushya22]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
@@ -2,7 +2,7 @@ name: Feature request
|
||||
description: Suggest a feature to improve Plane
|
||||
title: "[feature]: "
|
||||
labels: [✨feature]
|
||||
assignees: [vihar, pushya22]
|
||||
assignees: [srinivaspendem, pushya22]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
@@ -31,7 +31,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
gh_branch_name: ${{ steps.set_env_variables.outputs.TARGET_BRANCH }}
|
||||
flat_branch_name: ${{ steps.set_env_variables.outputs.FLAT_BRANCH_NAME }}
|
||||
gh_buildx_driver: ${{ steps.set_env_variables.outputs.BUILDX_DRIVER }}
|
||||
gh_buildx_version: ${{ steps.set_env_variables.outputs.BUILDX_VERSION }}
|
||||
gh_buildx_platforms: ${{ steps.set_env_variables.outputs.BUILDX_PLATFORMS }}
|
||||
@@ -57,7 +56,7 @@ jobs:
|
||||
echo "BUILDX_PLATFORMS=linux/amd64" >> $GITHUB_OUTPUT
|
||||
echo "BUILDX_ENDPOINT=" >> $GITHUB_OUTPUT
|
||||
|
||||
if [ "${{ github.event_name}}" == "workflow_dispatch" ] && [ "${{ github.event.inputs.base_tag_name }}" != "" ]; then
|
||||
if [ "${{ github.event.inputs.base_tag_name }}" != "" ]; then
|
||||
echo "AIO_BASE_TAG=${{ github.event.inputs.base_tag_name }}" >> $GITHUB_OUTPUT
|
||||
elif [ "${{ env.TARGET_BRANCH }}" == "preview" ]; then
|
||||
echo "AIO_BASE_TAG=preview" >> $GITHUB_OUTPUT
|
||||
@@ -79,9 +78,6 @@ jobs:
|
||||
echo "DO_SLIM_BUILD=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
FLAT_BRANCH_NAME=$(echo "${{ env.TARGET_BRANCH }}" | sed 's/[^a-zA-Z0-9]/-/g')
|
||||
echo "FLAT_BRANCH_NAME=$FLAT_BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
- id: checkout_files
|
||||
name: Checkout Files
|
||||
uses: actions/checkout@v4
|
||||
@@ -93,7 +89,7 @@ jobs:
|
||||
env:
|
||||
BUILD_TYPE: full
|
||||
AIO_BASE_TAG: ${{ needs.branch_build_setup.outputs.aio_base_tag }}
|
||||
AIO_IMAGE_TAGS: makeplane/plane-aio:full-${{ needs.branch_build_setup.outputs.flat_branch_name }}
|
||||
AIO_IMAGE_TAGS: makeplane/plane-aio:full-${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
BUILDX_DRIVER: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
|
||||
BUILDX_VERSION: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
|
||||
@@ -136,7 +132,7 @@ jobs:
|
||||
tags: ${{ env.AIO_IMAGE_TAGS }}
|
||||
push: true
|
||||
build-args: |
|
||||
BASE_TAG=${{ env.AIO_BASE_TAG }}
|
||||
BUILD_TAG=${{ env.AIO_BASE_TAG }}
|
||||
BUILD_TYPE=${{env.BUILD_TYPE}}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
@@ -153,7 +149,7 @@ jobs:
|
||||
env:
|
||||
BUILD_TYPE: slim
|
||||
AIO_BASE_TAG: ${{ needs.branch_build_setup.outputs.aio_base_tag }}
|
||||
AIO_IMAGE_TAGS: makeplane/plane-aio:slim-${{ needs.branch_build_setup.outputs.flat_branch_name }}
|
||||
AIO_IMAGE_TAGS: makeplane/plane-aio:slim-${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
BUILDX_DRIVER: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
|
||||
BUILDX_VERSION: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
|
||||
@@ -196,7 +192,7 @@ jobs:
|
||||
tags: ${{ env.AIO_IMAGE_TAGS }}
|
||||
push: true
|
||||
build-args: |
|
||||
BASE_TAG=${{ env.AIO_BASE_TAG }}
|
||||
BUILD_TAG=${{ env.AIO_BASE_TAG }}
|
||||
BUILD_TYPE=${{env.BUILD_TYPE}}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
+62
-23
@@ -1,5 +1,6 @@
|
||||
# Environment Variables
|
||||
|
||||
|
||||
Environment variables are distributed in various files. Please refer them carefully.
|
||||
|
||||
## {PROJECT_FOLDER}/.env
|
||||
@@ -8,13 +9,17 @@ File is available in the project root folder
|
||||
|
||||
```
|
||||
# Database Settings
|
||||
POSTGRES_USER="plane"
|
||||
POSTGRES_PASSWORD="plane"
|
||||
POSTGRES_DB="plane"
|
||||
PGDATA="/var/lib/postgresql/data"
|
||||
PGUSER="plane"
|
||||
PGPASSWORD="plane"
|
||||
PGHOST="plane-db"
|
||||
PGDATABASE="plane"
|
||||
DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}
|
||||
|
||||
# Redis Settings
|
||||
REDIS_HOST="plane-redis"
|
||||
REDIS_PORT="6379"
|
||||
REDIS_URL="redis://${REDIS_HOST}:6379/"
|
||||
|
||||
# AWS Settings
|
||||
AWS_REGION=""
|
||||
AWS_ACCESS_KEY_ID="access-key"
|
||||
@@ -24,39 +29,63 @@ AWS_S3_ENDPOINT_URL="http://plane-minio:9000"
|
||||
AWS_S3_BUCKET_NAME="uploads"
|
||||
# Maximum file upload limit
|
||||
FILE_SIZE_LIMIT=5242880
|
||||
|
||||
# GPT settings
|
||||
OPENAI_API_BASE="https://api.openai.com/v1" # deprecated
|
||||
OPENAI_API_KEY="sk-" # deprecated
|
||||
GPT_ENGINE="gpt-3.5-turbo" # deprecated
|
||||
# Settings related to Docker
|
||||
DOCKERIZED=1 # deprecated
|
||||
|
||||
# set to 1 If using the pre-configured minio setup
|
||||
USE_MINIO=1
|
||||
|
||||
# Nginx Configuration
|
||||
NGINX_PORT=80
|
||||
```
|
||||
|
||||
|
||||
|
||||
## {PROJECT_FOLDER}/web/.env.example
|
||||
|
||||
|
||||
|
||||
```
|
||||
# Public boards deploy URL
|
||||
NEXT_PUBLIC_DEPLOY_URL="http://localhost/spaces"
|
||||
```
|
||||
|
||||
## {PROJECT_FOLDER}/apiserver/.env
|
||||
|
||||
|
||||
|
||||
```
|
||||
# Backend
|
||||
# Debug value for api server use it as 0 for production use
|
||||
DEBUG=0
|
||||
CORS_ALLOWED_ORIGINS="http://localhost"
|
||||
|
||||
# Error logs
|
||||
SENTRY_DSN=""
|
||||
SENTRY_ENVIRONMENT="development"
|
||||
|
||||
# Database Settings
|
||||
POSTGRES_USER="plane"
|
||||
POSTGRES_PASSWORD="plane"
|
||||
POSTGRES_HOST="plane-db"
|
||||
POSTGRES_DB="plane"
|
||||
POSTGRES_PORT=5432
|
||||
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
|
||||
PGUSER="plane"
|
||||
PGPASSWORD="plane"
|
||||
PGHOST="plane-db"
|
||||
PGDATABASE="plane"
|
||||
DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}
|
||||
|
||||
# Redis Settings
|
||||
REDIS_HOST="plane-redis"
|
||||
REDIS_PORT="6379"
|
||||
REDIS_URL="redis://${REDIS_HOST}:6379/"
|
||||
|
||||
# Email Settings
|
||||
EMAIL_HOST=""
|
||||
EMAIL_HOST_USER=""
|
||||
EMAIL_HOST_PASSWORD=""
|
||||
EMAIL_PORT=587
|
||||
EMAIL_FROM="Team Plane <team@mailer.plane.so>"
|
||||
EMAIL_USE_TLS="1"
|
||||
EMAIL_USE_SSL="0"
|
||||
|
||||
# AWS Settings
|
||||
AWS_REGION=""
|
||||
AWS_ACCESS_KEY_ID="access-key"
|
||||
@@ -66,25 +95,35 @@ AWS_S3_ENDPOINT_URL="http://plane-minio:9000"
|
||||
AWS_S3_BUCKET_NAME="uploads"
|
||||
# Maximum file upload limit
|
||||
FILE_SIZE_LIMIT=5242880
|
||||
|
||||
# GPT settings
|
||||
OPENAI_API_BASE="https://api.openai.com/v1" # deprecated
|
||||
OPENAI_API_KEY="sk-" # deprecated
|
||||
GPT_ENGINE="gpt-3.5-turbo" # deprecated
|
||||
|
||||
# Settings related to Docker
|
||||
DOCKERIZED=1 # deprecated
|
||||
DOCKERIZED=1 # Deprecated
|
||||
|
||||
# Github
|
||||
GITHUB_CLIENT_SECRET="" # For fetching release notes
|
||||
|
||||
# set to 1 If using the pre-configured minio setup
|
||||
USE_MINIO=1
|
||||
|
||||
# Nginx Configuration
|
||||
NGINX_PORT=80
|
||||
# Email redirections and minio domain settings
|
||||
|
||||
|
||||
# SignUps
|
||||
ENABLE_SIGNUP="1"
|
||||
|
||||
# Email Redirection URL
|
||||
WEB_URL="http://localhost"
|
||||
# Gunicorn Workers
|
||||
GUNICORN_WORKERS=2
|
||||
# Base URLs
|
||||
ADMIN_BASE_URL=
|
||||
SPACE_BASE_URL=
|
||||
APP_BASE_URL=
|
||||
SECRET_KEY="gxoytl7dmnc1y37zahah820z5iq3iozu38cnfjtu3yaau9cd9z"
|
||||
```
|
||||
|
||||
## Updates
|
||||
|
||||
- The environment variable NEXT_PUBLIC_API_BASE_URL has been removed from both the web and space projects.
|
||||
- The naming convention for containers and images has been updated.
|
||||
- The plane-worker image will no longer be maintained, as it has been merged with plane-backend.
|
||||
- The Tiptap pro-extension dependency has been removed, eliminating the need for Tiptap API keys.
|
||||
|
||||
+1
-2
@@ -14,7 +14,6 @@
|
||||
"@headlessui/react": "^1.7.19",
|
||||
"@plane/types": "*",
|
||||
"@plane/ui": "*",
|
||||
"@plane/constants": "*",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/lodash": "^4.17.0",
|
||||
"autoprefixer": "10.4.14",
|
||||
@@ -47,4 +46,4 @@
|
||||
"tsconfig": "*",
|
||||
"typescript": "^5.4.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,9 @@ class CycleSerializer(BaseSerializer):
|
||||
started_issues = serializers.IntegerField(read_only=True)
|
||||
unstarted_issues = serializers.IntegerField(read_only=True)
|
||||
backlog_issues = serializers.IntegerField(read_only=True)
|
||||
total_estimates = serializers.FloatField(read_only=True)
|
||||
completed_estimates = serializers.FloatField(read_only=True)
|
||||
started_estimates = serializers.FloatField(read_only=True)
|
||||
total_estimates = serializers.IntegerField(read_only=True)
|
||||
completed_estimates = serializers.IntegerField(read_only=True)
|
||||
started_estimates = serializers.IntegerField(read_only=True)
|
||||
|
||||
def validate(self, data):
|
||||
if (
|
||||
|
||||
@@ -55,6 +55,7 @@ class IssueSerializer(BaseSerializer):
|
||||
"project",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
exclude = [
|
||||
@@ -130,10 +131,7 @@ class IssueSerializer(BaseSerializer):
|
||||
workspace_id = self.context["workspace_id"]
|
||||
default_assignee_id = self.context["default_assignee_id"]
|
||||
|
||||
issue = Issue.objects.create(
|
||||
**validated_data,
|
||||
project_id=project_id,
|
||||
)
|
||||
issue = Issue.objects.create(**validated_data, project_id=project_id)
|
||||
|
||||
# Issue Audit Users
|
||||
created_by_id = issue.created_by_id
|
||||
@@ -314,14 +312,10 @@ class IssueLinkSerializer(BaseSerializer):
|
||||
return IssueLink.objects.create(**validated_data)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
if (
|
||||
IssueLink.objects.filter(
|
||||
url=validated_data.get("url"),
|
||||
issue_id=instance.issue_id,
|
||||
)
|
||||
.exclude(pk=instance.id)
|
||||
.exists()
|
||||
):
|
||||
if IssueLink.objects.filter(
|
||||
url=validated_data.get("url"),
|
||||
issue_id=instance.issue_id,
|
||||
).exclude(pk=instance.id).exists():
|
||||
raise serializers.ValidationError(
|
||||
{"error": "URL already exists for this Issue"}
|
||||
)
|
||||
|
||||
@@ -4,7 +4,6 @@ from .issue import urlpatterns as issue_patterns
|
||||
from .cycle import urlpatterns as cycle_patterns
|
||||
from .module import urlpatterns as module_patterns
|
||||
from .inbox import urlpatterns as inbox_patterns
|
||||
from .member import urlpatterns as member_patterns
|
||||
|
||||
urlpatterns = [
|
||||
*project_patterns,
|
||||
@@ -13,5 +12,4 @@ urlpatterns = [
|
||||
*cycle_patterns,
|
||||
*module_patterns,
|
||||
*inbox_patterns,
|
||||
*member_patterns,
|
||||
]
|
||||
|
||||
@@ -7,7 +7,6 @@ from plane.api.views import (
|
||||
IssueCommentAPIEndpoint,
|
||||
IssueActivityAPIEndpoint,
|
||||
WorkspaceIssueAPIEndpoint,
|
||||
IssueAttachmentEndpoint,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
@@ -66,9 +65,4 @@ urlpatterns = [
|
||||
IssueActivityAPIEndpoint.as_view(),
|
||||
name="activity",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-attachments/",
|
||||
IssueAttachmentEndpoint.as_view(),
|
||||
name="attachment",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
from plane.api.views import (
|
||||
WorkspaceMemberAPIEndpoint,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/members/",
|
||||
WorkspaceMemberAPIEndpoint.as_view(),
|
||||
name="users",
|
||||
),
|
||||
]
|
||||
@@ -9,7 +9,6 @@ from .issue import (
|
||||
IssueLinkAPIEndpoint,
|
||||
IssueCommentAPIEndpoint,
|
||||
IssueActivityAPIEndpoint,
|
||||
IssueAttachmentEndpoint,
|
||||
)
|
||||
|
||||
from .cycle import (
|
||||
@@ -25,6 +24,4 @@ from .module import (
|
||||
ModuleArchiveUnarchiveAPIEndpoint,
|
||||
)
|
||||
|
||||
from .member import WorkspaceMemberAPIEndpoint
|
||||
|
||||
from .inbox import InboxIssueAPIEndpoint
|
||||
|
||||
@@ -34,7 +34,6 @@ from plane.db.models import (
|
||||
Project,
|
||||
IssueAttachment,
|
||||
IssueLink,
|
||||
ProjectMember,
|
||||
)
|
||||
from plane.utils.analytics_plot import burndown_plot
|
||||
|
||||
@@ -364,28 +363,14 @@ class CycleAPIEndpoint(BaseAPIView):
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def delete(self, request, slug, project_id, pk):
|
||||
cycle = Cycle.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
if cycle.owned_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 cycle"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
cycle_issues = list(
|
||||
CycleIssue.objects.filter(
|
||||
cycle_id=self.kwargs.get("pk")
|
||||
).values_list("issue", flat=True)
|
||||
)
|
||||
cycle = Cycle.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
|
||||
issue_activity.delay(
|
||||
type="cycle.activity.deleted",
|
||||
@@ -408,6 +393,7 @@ class CycleAPIEndpoint(BaseAPIView):
|
||||
|
||||
|
||||
class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
|
||||
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
@@ -661,6 +647,17 @@ class CycleIssueAPIEndpoint(BaseAPIView):
|
||||
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,
|
||||
)
|
||||
|
||||
issues = Issue.objects.filter(
|
||||
pk__in=issues, workspace__slug=slug, project_id=project_id
|
||||
).values_list("id", flat=True)
|
||||
@@ -1143,38 +1140,12 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
updated_cycles = []
|
||||
update_cycle_issue_activity = []
|
||||
for cycle_issue in cycle_issues:
|
||||
cycle_issue.cycle_id = new_cycle_id
|
||||
updated_cycles.append(cycle_issue)
|
||||
update_cycle_issue_activity.append(
|
||||
{
|
||||
"old_cycle_id": str(cycle_id),
|
||||
"new_cycle_id": str(new_cycle_id),
|
||||
"issue_id": str(cycle_issue.issue_id),
|
||||
}
|
||||
)
|
||||
|
||||
cycle_issues = CycleIssue.objects.bulk_update(
|
||||
updated_cycles, ["cycle_id"], batch_size=100
|
||||
)
|
||||
|
||||
# Capture Issue Activity
|
||||
issue_activity.delay(
|
||||
type="cycle.activity.created",
|
||||
requested_data=json.dumps({"cycles_list": []}),
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=None,
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=json.dumps(
|
||||
{
|
||||
"updated_cycle_issues": update_cycle_issue_activity,
|
||||
"created_cycle_issues": "[]",
|
||||
}
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
return Response({"message": "Success"}, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -3,11 +3,8 @@ import json
|
||||
|
||||
# Django improts
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
from django.db.models import Q, Value, UUIDField
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
|
||||
# Third party imports
|
||||
from rest_framework import status
|
||||
@@ -227,27 +224,8 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
||||
issue_data = request.data.pop("issue", False)
|
||||
|
||||
if bool(issue_data):
|
||||
issue = Issue.objects.annotate(
|
||||
label_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=~Q(labels__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
assignee_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=~Q(assignees__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
).get(
|
||||
pk=issue_id,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
issue = Issue.objects.get(
|
||||
pk=issue_id, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
# Only allow guests and viewers to edit name and description
|
||||
if project_member.role <= 10:
|
||||
@@ -390,26 +368,29 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
||||
inbox_id=inbox.id,
|
||||
)
|
||||
|
||||
# Get the project member
|
||||
project_member = ProjectMember.objects.get(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
# Check the inbox issue created
|
||||
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(
|
||||
request.user.id
|
||||
):
|
||||
return Response(
|
||||
{"error": "You cannot delete inbox issue"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Check the issue status
|
||||
if inbox_issue.status in [-2, -1, 0, 2]:
|
||||
# Delete the issue also
|
||||
issue = Issue.objects.filter(
|
||||
Issue.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, pk=issue_id
|
||||
).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()
|
||||
).delete()
|
||||
|
||||
inbox_issue.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -22,11 +22,9 @@ from django.utils import timezone
|
||||
# Third party imports
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.parsers import MultiPartParser, FormParser
|
||||
|
||||
# Module imports
|
||||
from plane.api.serializers import (
|
||||
IssueAttachmentSerializer,
|
||||
IssueActivitySerializer,
|
||||
IssueCommentSerializer,
|
||||
IssueLinkSerializer,
|
||||
@@ -309,14 +307,6 @@ class IssueAPIEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
serializer.save()
|
||||
# Refetch the issue
|
||||
issue = Issue.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
pk=serializer.data["id"],
|
||||
).first()
|
||||
issue.created_at = request.data.get("created_at")
|
||||
issue.save(update_fields=["created_at"])
|
||||
|
||||
# Track the issue
|
||||
issue_activity.delay(
|
||||
@@ -389,19 +379,6 @@ class IssueAPIEndpoint(BaseAPIView):
|
||||
issue = Issue.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
if issue.created_by_id != request.user.id and (
|
||||
not ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role=20,
|
||||
project_id=project_id,
|
||||
is_active=True,
|
||||
).exists()
|
||||
):
|
||||
return Response(
|
||||
{"error": "Only admin or creator can delete the issue"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
current_instance = json.dumps(
|
||||
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
@@ -897,83 +874,3 @@ class IssueActivityAPIEndpoint(BaseAPIView):
|
||||
expand=self.expand,
|
||||
).data,
|
||||
)
|
||||
|
||||
|
||||
class IssueAttachmentEndpoint(BaseAPIView):
|
||||
serializer_class = IssueAttachmentSerializer
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
model = IssueAttachment
|
||||
parser_classes = (MultiPartParser, FormParser)
|
||||
|
||||
def post(self, request, slug, project_id, issue_id):
|
||||
serializer = IssueAttachmentSerializer(data=request.data)
|
||||
if (
|
||||
request.data.get("external_id")
|
||||
and request.data.get("external_source")
|
||||
and IssueAttachment.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
issue_id=issue_id,
|
||||
external_source=request.data.get("external_source"),
|
||||
external_id=request.data.get("external_id"),
|
||||
).exists()
|
||||
):
|
||||
issue_attachment = IssueAttachment.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
external_id=request.data.get("external_id"),
|
||||
external_source=request.data.get("external_source"),
|
||||
).first()
|
||||
return Response(
|
||||
{
|
||||
"error": "Issue attachment with the same external id and external source already exists",
|
||||
"id": str(issue_attachment.id),
|
||||
},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save(project_id=project_id, issue_id=issue_id)
|
||||
issue_activity.delay(
|
||||
type="attachment.activity.created",
|
||||
requested_data=None,
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=str(self.kwargs.get("issue_id", None)),
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=json.dumps(
|
||||
serializer.data,
|
||||
cls=DjangoJSONEncoder,
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def delete(self, request, slug, project_id, issue_id, pk):
|
||||
issue_attachment = IssueAttachment.objects.get(pk=pk)
|
||||
issue_attachment.asset.delete(save=False)
|
||||
issue_attachment.delete()
|
||||
issue_activity.delay(
|
||||
type="attachment.activity.deleted",
|
||||
requested_data=None,
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=str(self.kwargs.get("issue_id", None)),
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def get(self, request, slug, project_id, issue_id):
|
||||
issue_attachments = IssueAttachment.objects.filter(
|
||||
issue_id=issue_id, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
serializer = IssueAttachmentSerializer(issue_attachments, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
# Python imports
|
||||
import uuid
|
||||
|
||||
# Django imports
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.core.validators import validate_email
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
# Third Party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
|
||||
# Module imports
|
||||
from .base import BaseAPIView
|
||||
from plane.api.serializers import UserLiteSerializer
|
||||
from plane.db.models import (
|
||||
User,
|
||||
Workspace,
|
||||
Project,
|
||||
WorkspaceMember,
|
||||
ProjectMember,
|
||||
)
|
||||
|
||||
|
||||
# API endpoint to get and insert users inside the workspace
|
||||
class WorkspaceMemberAPIEndpoint(BaseAPIView):
|
||||
# Get all the users that are present inside the workspace
|
||||
def get(self, request, slug):
|
||||
# Check if the workspace exists
|
||||
if not Workspace.objects.filter(slug=slug).exists():
|
||||
return Response(
|
||||
{"error": "Provided workspace does not exist"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Get the workspace members that are present inside the workspace
|
||||
workspace_members = WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug
|
||||
)
|
||||
|
||||
# Get all the users that are present inside the workspace
|
||||
users = UserLiteSerializer(
|
||||
User.objects.filter(
|
||||
id__in=workspace_members.values_list("member_id", flat=True)
|
||||
),
|
||||
many=True,
|
||||
).data
|
||||
|
||||
return Response(users, status=status.HTTP_200_OK)
|
||||
|
||||
# Insert a new user inside the workspace, and assign the user to the project
|
||||
def post(self, request, slug):
|
||||
# Check if user with email already exists, and send bad request if it's
|
||||
# not present, check for workspace and valid project mandat
|
||||
# ------------------- Validation -------------------
|
||||
if (
|
||||
request.data.get("email") is None
|
||||
or request.data.get("display_name") is None
|
||||
or request.data.get("project_id") is None
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error": "Expected email, display_name, workspace_slug, project_id, one or more of the fields are missing."
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
email = request.data.get("email")
|
||||
|
||||
try:
|
||||
validate_email(email)
|
||||
except ValidationError:
|
||||
return Response(
|
||||
{"error": "Invalid email provided"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
workspace = Workspace.objects.filter(slug=slug).first()
|
||||
project = Project.objects.filter(
|
||||
pk=request.data.get("project_id")
|
||||
).first()
|
||||
|
||||
if not all([workspace, project]):
|
||||
return Response(
|
||||
{"error": "Provided workspace or project does not exist"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Check if user exists
|
||||
user = User.objects.filter(email=email).first()
|
||||
workspace_member = None
|
||||
project_member = None
|
||||
|
||||
if user:
|
||||
# Check if user is part of the workspace
|
||||
workspace_member = WorkspaceMember.objects.filter(
|
||||
workspace=workspace, member=user
|
||||
).first()
|
||||
if workspace_member:
|
||||
# Check if user is part of the project
|
||||
project_member = ProjectMember.objects.filter(
|
||||
project=project, member=user
|
||||
).first()
|
||||
if project_member:
|
||||
return Response(
|
||||
{
|
||||
"error": "User is already part of the workspace and project"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# If user does not exist, create the user
|
||||
if not user:
|
||||
user = User.objects.create(
|
||||
email=email,
|
||||
display_name=request.data.get("display_name"),
|
||||
first_name=request.data.get("first_name", ""),
|
||||
last_name=request.data.get("last_name", ""),
|
||||
username=uuid.uuid4().hex,
|
||||
password=make_password(uuid.uuid4().hex),
|
||||
is_password_autoset=True,
|
||||
is_active=False,
|
||||
)
|
||||
user.save()
|
||||
|
||||
# Create a workspace member for the user if not already a member
|
||||
if not workspace_member:
|
||||
workspace_member = WorkspaceMember.objects.create(
|
||||
workspace=workspace,
|
||||
member=user,
|
||||
role=request.data.get("role", 10),
|
||||
)
|
||||
workspace_member.save()
|
||||
|
||||
# Create a project member for the user if not already a member
|
||||
if not project_member:
|
||||
project_member = ProjectMember.objects.create(
|
||||
project=project,
|
||||
member=user,
|
||||
role=request.data.get("role", 10),
|
||||
)
|
||||
project_member.save()
|
||||
|
||||
# Serialize the user and return the response
|
||||
user_data = UserLiteSerializer(user).data
|
||||
|
||||
return Response(user_data, status=status.HTTP_201_CREATED)
|
||||
@@ -27,7 +27,6 @@ from plane.db.models import (
|
||||
ModuleIssue,
|
||||
ModuleLink,
|
||||
Project,
|
||||
ProjectMember,
|
||||
)
|
||||
|
||||
from .base import BaseAPIView
|
||||
@@ -266,20 +265,6 @@ class ModuleAPIEndpoint(BaseAPIView):
|
||||
module = Module.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
if module.created_by_id != request.user.id and (
|
||||
not ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role=20,
|
||||
project_id=project_id,
|
||||
is_active=True,
|
||||
).exists()
|
||||
):
|
||||
return Response(
|
||||
{"error": "Only admin or creator can delete the module"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
module_issues = list(
|
||||
ModuleIssue.objects.filter(module_id=pk).values_list(
|
||||
"issue", flat=True
|
||||
|
||||
@@ -19,7 +19,7 @@ from plane.app.permissions import ProjectBasePermission
|
||||
from plane.db.models import (
|
||||
Cycle,
|
||||
Inbox,
|
||||
IssueUserProperty,
|
||||
IssueProperty,
|
||||
Module,
|
||||
Project,
|
||||
DeployBoard,
|
||||
@@ -165,7 +165,7 @@ class ProjectAPIEndpoint(BaseAPIView):
|
||||
role=20,
|
||||
)
|
||||
# Also create the issue property for the user
|
||||
_ = IssueUserProperty.objects.create(
|
||||
_ = IssueProperty.objects.create(
|
||||
project_id=serializer.data["id"],
|
||||
user=request.user,
|
||||
)
|
||||
@@ -179,7 +179,7 @@ class ProjectAPIEndpoint(BaseAPIView):
|
||||
role=20,
|
||||
)
|
||||
# Also create the issue property for the user
|
||||
IssueUserProperty.objects.create(
|
||||
IssueProperty.objects.create(
|
||||
project_id=serializer.data["id"],
|
||||
user_id=serializer.data["project_lead"],
|
||||
)
|
||||
@@ -240,7 +240,6 @@ class ProjectAPIEndpoint(BaseAPIView):
|
||||
.filter(pk=serializer.data["id"])
|
||||
.first()
|
||||
)
|
||||
|
||||
# Model activity
|
||||
model_activity.delay(
|
||||
model_name="project",
|
||||
|
||||
@@ -50,7 +50,7 @@ from .issue import (
|
||||
IssueCreateSerializer,
|
||||
IssueActivitySerializer,
|
||||
IssueCommentSerializer,
|
||||
IssueUserPropertySerializer,
|
||||
IssuePropertySerializer,
|
||||
IssueAssigneeSerializer,
|
||||
LabelSerializer,
|
||||
IssueSerializer,
|
||||
@@ -91,7 +91,6 @@ from .page import (
|
||||
PageLogSerializer,
|
||||
SubPageSerializer,
|
||||
PageDetailSerializer,
|
||||
PageVersionSerializer,
|
||||
)
|
||||
|
||||
from .estimate import (
|
||||
|
||||
@@ -17,7 +17,7 @@ from plane.db.models import (
|
||||
Issue,
|
||||
IssueActivity,
|
||||
IssueComment,
|
||||
IssueUserProperty,
|
||||
IssueProperty,
|
||||
IssueAssignee,
|
||||
IssueSubscriber,
|
||||
IssueLabel,
|
||||
@@ -135,11 +135,7 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
workspace_id = self.context["workspace_id"]
|
||||
default_assignee_id = self.context["default_assignee_id"]
|
||||
|
||||
# Create Issue
|
||||
issue = Issue.objects.create(
|
||||
**validated_data,
|
||||
project_id=project_id,
|
||||
)
|
||||
issue = Issue.objects.create(**validated_data, project_id=project_id)
|
||||
|
||||
# Issue Audit Users
|
||||
created_by_id = issue.created_by_id
|
||||
@@ -252,9 +248,9 @@ class IssueActivitySerializer(BaseSerializer):
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class IssueUserPropertySerializer(BaseSerializer):
|
||||
class IssuePropertySerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = IssueUserProperty
|
||||
model = IssueProperty
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"user",
|
||||
@@ -463,14 +459,10 @@ class IssueLinkSerializer(BaseSerializer):
|
||||
return IssueLink.objects.create(**validated_data)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
if (
|
||||
IssueLink.objects.filter(
|
||||
url=validated_data.get("url"),
|
||||
issue_id=instance.issue_id,
|
||||
)
|
||||
.exclude(pk=instance.id)
|
||||
.exists()
|
||||
):
|
||||
if IssueLink.objects.filter(
|
||||
url=validated_data.get("url"),
|
||||
issue_id=instance.issue_id,
|
||||
).exclude(pk=instance.id).exists():
|
||||
raise serializers.ValidationError(
|
||||
{"error": "URL already exists for this Issue"}
|
||||
)
|
||||
@@ -517,7 +509,7 @@ class IssueAttachmentLiteSerializer(DynamicBaseSerializer):
|
||||
"attributes",
|
||||
"issue_id",
|
||||
"updated_at",
|
||||
"updated_by",
|
||||
"updated_by_id",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ class NotificationSerializer(BaseSerializer):
|
||||
read_only=True, source="triggered_by"
|
||||
)
|
||||
is_inbox_issue = serializers.BooleanField(read_only=True)
|
||||
is_mentioned_notification = serializers.BooleanField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Notification
|
||||
|
||||
@@ -10,7 +10,6 @@ from plane.db.models import (
|
||||
Label,
|
||||
ProjectPage,
|
||||
Project,
|
||||
PageVersion,
|
||||
)
|
||||
|
||||
|
||||
@@ -162,13 +161,3 @@ class PageLogSerializer(BaseSerializer):
|
||||
"workspace",
|
||||
"page",
|
||||
]
|
||||
|
||||
|
||||
class PageVersionSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = PageVersion
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"page",
|
||||
]
|
||||
|
||||
@@ -23,7 +23,7 @@ class IssueViewSerializer(DynamicBaseSerializer):
|
||||
]
|
||||
|
||||
def create(self, validated_data):
|
||||
query_params = validated_data.get("filters", {})
|
||||
query_params = validated_data.get("query_data", {})
|
||||
if bool(query_params):
|
||||
validated_data["query"] = issue_filters(query_params, "POST")
|
||||
else:
|
||||
@@ -31,7 +31,7 @@ class IssueViewSerializer(DynamicBaseSerializer):
|
||||
return IssueView.objects.create(**validated_data)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
query_params = validated_data.get("filters", {})
|
||||
query_params = validated_data.get("query_data", {})
|
||||
if bool(query_params):
|
||||
validated_data["query"] = issue_filters(query_params, "POST")
|
||||
else:
|
||||
|
||||
@@ -233,13 +233,13 @@ urlpatterns = [
|
||||
name="project-issue-comment-reactions",
|
||||
),
|
||||
## End Comment Reactions
|
||||
## IssueUserProperty
|
||||
## IssueProperty
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/user-properties/",
|
||||
IssueUserDisplayPropertyEndpoint.as_view(),
|
||||
name="project-issue-display-properties",
|
||||
),
|
||||
## IssueUserProperty End
|
||||
## IssueProperty End
|
||||
## Issue Archives
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-issues/",
|
||||
|
||||
@@ -7,7 +7,6 @@ from plane.app.views import (
|
||||
PageLogEndpoint,
|
||||
SubPagesEndpoint,
|
||||
PagesDescriptionViewSet,
|
||||
PageVersionEndpoint,
|
||||
)
|
||||
|
||||
|
||||
@@ -66,16 +65,6 @@ urlpatterns = [
|
||||
),
|
||||
name="project-pages-lock-unlock",
|
||||
),
|
||||
# private and public page
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/access/",
|
||||
PageViewSet.as_view(
|
||||
{
|
||||
"post": "access",
|
||||
}
|
||||
),
|
||||
name="project-pages-access",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/transactions/",
|
||||
PageLogEndpoint.as_view(),
|
||||
@@ -101,14 +90,4 @@ urlpatterns = [
|
||||
),
|
||||
name="page-description",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/versions/",
|
||||
PageVersionEndpoint.as_view(),
|
||||
name="page-versions",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/versions/<uuid:pk>/",
|
||||
PageVersionEndpoint.as_view(),
|
||||
name="page-versions",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -179,7 +179,6 @@ from .page.base import (
|
||||
SubPagesEndpoint,
|
||||
PagesDescriptionViewSet,
|
||||
)
|
||||
from .page.version import PageVersionEndpoint
|
||||
|
||||
from .search.base import GlobalSearchEndpoint
|
||||
from .search.issue import IssueSearchEndpoint
|
||||
|
||||
@@ -47,7 +47,6 @@ from plane.db.models import (
|
||||
Label,
|
||||
User,
|
||||
Project,
|
||||
ProjectMember,
|
||||
)
|
||||
from plane.utils.analytics_plot import burndown_plot
|
||||
|
||||
@@ -466,14 +465,14 @@ class CycleViewSet(BaseViewSet):
|
||||
}
|
||||
|
||||
if data[0]["start_date"] and data[0]["end_date"]:
|
||||
data[0]["estimate_distribution"][
|
||||
"completion_chart"
|
||||
] = burndown_plot(
|
||||
queryset=queryset.first(),
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
plot_type="points",
|
||||
cycle_id=data[0]["id"],
|
||||
data[0]["estimate_distribution"]["completion_chart"] = (
|
||||
burndown_plot(
|
||||
queryset=queryset.first(),
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
plot_type="points",
|
||||
cycle_id=data[0]["id"],
|
||||
)
|
||||
)
|
||||
|
||||
assignee_distribution = (
|
||||
@@ -646,8 +645,6 @@ class CycleViewSet(BaseViewSet):
|
||||
"progress_snapshot",
|
||||
"logo_props",
|
||||
# meta fields
|
||||
"completed_estimate_points",
|
||||
"total_estimate_points",
|
||||
"is_favorite",
|
||||
"cancelled_issues",
|
||||
"total_issues",
|
||||
@@ -657,7 +654,6 @@ class CycleViewSet(BaseViewSet):
|
||||
"backlog_issues",
|
||||
"assignee_ids",
|
||||
"status",
|
||||
"created_by",
|
||||
)
|
||||
.first()
|
||||
)
|
||||
@@ -743,8 +739,6 @@ class CycleViewSet(BaseViewSet):
|
||||
"progress_snapshot",
|
||||
"logo_props",
|
||||
# meta fields
|
||||
"completed_estimate_points",
|
||||
"total_estimate_points",
|
||||
"is_favorite",
|
||||
"total_issues",
|
||||
"cancelled_issues",
|
||||
@@ -754,7 +748,6 @@ class CycleViewSet(BaseViewSet):
|
||||
"backlog_issues",
|
||||
"assignee_ids",
|
||||
"status",
|
||||
"created_by",
|
||||
).first()
|
||||
|
||||
# Send the model activity
|
||||
@@ -807,8 +800,6 @@ class CycleViewSet(BaseViewSet):
|
||||
"sub_issues",
|
||||
"logo_props",
|
||||
# meta fields
|
||||
"completed_estimate_points",
|
||||
"total_estimate_points",
|
||||
"is_favorite",
|
||||
"total_issues",
|
||||
"cancelled_issues",
|
||||
@@ -1040,28 +1031,14 @@ class CycleViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
def destroy(self, request, slug, project_id, pk):
|
||||
cycle = Cycle.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
if cycle.owned_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 owner can delete the cycle"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
cycle_issues = list(
|
||||
CycleIssue.objects.filter(
|
||||
cycle_id=self.kwargs.get("pk")
|
||||
).values_list("issue", flat=True)
|
||||
)
|
||||
cycle = Cycle.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
|
||||
issue_activity.delay(
|
||||
type="cycle.activity.deleted",
|
||||
@@ -1530,40 +1507,14 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
updated_cycles = []
|
||||
update_cycle_issue_activity = []
|
||||
for cycle_issue in cycle_issues:
|
||||
cycle_issue.cycle_id = new_cycle_id
|
||||
updated_cycles.append(cycle_issue)
|
||||
update_cycle_issue_activity.append(
|
||||
{
|
||||
"old_cycle_id": str(cycle_id),
|
||||
"new_cycle_id": str(new_cycle_id),
|
||||
"issue_id": str(cycle_issue.issue_id),
|
||||
}
|
||||
)
|
||||
|
||||
cycle_issues = CycleIssue.objects.bulk_update(
|
||||
updated_cycles, ["cycle_id"], batch_size=100
|
||||
)
|
||||
|
||||
# Capture Issue Activity
|
||||
issue_activity.delay(
|
||||
type="cycle.activity.created",
|
||||
requested_data=json.dumps({"cycles_list": []}),
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=None,
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=json.dumps(
|
||||
{
|
||||
"updated_cycle_issues": update_cycle_issue_activity,
|
||||
"created_cycle_issues": "[]",
|
||||
}
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
return Response({"message": "Success"}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
|
||||
@@ -542,7 +542,6 @@ def dashboard_recent_collaborators(self, request, slug):
|
||||
project__project_projectmember__member=request.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
project__archived_at__isnull=True,
|
||||
is_active=True,
|
||||
)
|
||||
.annotate(
|
||||
num_activities=Coalesce(
|
||||
|
||||
@@ -41,7 +41,6 @@ class ExportIssuesEndpoint(BaseAPIView):
|
||||
project=project_ids,
|
||||
initiated_by=request.user,
|
||||
provider=provider,
|
||||
type="issue_exports",
|
||||
)
|
||||
|
||||
issue_export_task.delay(
|
||||
@@ -66,8 +65,7 @@ class ExportIssuesEndpoint(BaseAPIView):
|
||||
|
||||
def get(self, request, slug):
|
||||
exporter_history = ExporterHistory.objects.filter(
|
||||
workspace__slug=slug,
|
||||
type="issue_exports",
|
||||
workspace__slug=slug
|
||||
).select_related("workspace", "initiated_by")
|
||||
|
||||
if request.GET.get("per_page", False) and request.GET.get(
|
||||
|
||||
@@ -553,27 +553,28 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
project_id=project_id,
|
||||
inbox_id=inbox_id,
|
||||
)
|
||||
# Get the project member
|
||||
project_member = ProjectMember.objects.get(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(
|
||||
request.user.id
|
||||
):
|
||||
return Response(
|
||||
{"error": "You cannot delete inbox issue"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Check the issue status
|
||||
if inbox_issue.status in [-2, -1, 0, 2]:
|
||||
# Delete the issue also
|
||||
issue = Issue.objects.filter(
|
||||
Issue.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, pk=issue_id
|
||||
).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()
|
||||
).delete()
|
||||
|
||||
inbox_issue.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -32,7 +32,7 @@ from plane.app.permissions import (
|
||||
from plane.app.serializers import (
|
||||
IssueCreateSerializer,
|
||||
IssueDetailSerializer,
|
||||
IssueUserPropertySerializer,
|
||||
IssuePropertySerializer,
|
||||
IssueSerializer,
|
||||
)
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
@@ -40,11 +40,10 @@ from plane.db.models import (
|
||||
Issue,
|
||||
IssueAttachment,
|
||||
IssueLink,
|
||||
IssueUserProperty,
|
||||
IssueProperty,
|
||||
IssueReaction,
|
||||
IssueSubscriber,
|
||||
Project,
|
||||
ProjectMember,
|
||||
)
|
||||
from plane.utils.grouper import (
|
||||
issue_group_values,
|
||||
@@ -482,38 +481,7 @@ class IssueViewSet(BaseViewSet):
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def partial_update(self, request, slug, project_id, pk=None):
|
||||
issue = (
|
||||
self.get_queryset()
|
||||
.annotate(
|
||||
label_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=~Q(labels__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
assignee_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
module_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=~Q(issue_module__module_id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
)
|
||||
.filter(pk=pk)
|
||||
.first()
|
||||
)
|
||||
issue = self.get_queryset().filter(pk=pk).first()
|
||||
|
||||
if not issue:
|
||||
return Response(
|
||||
@@ -550,20 +518,6 @@ class IssueViewSet(BaseViewSet):
|
||||
issue = Issue.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
if issue.created_by_id != request.user.id and (
|
||||
not ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role=20,
|
||||
project_id=project_id,
|
||||
is_active=True,
|
||||
).exists()
|
||||
):
|
||||
return Response(
|
||||
{"error": "Only admin or creator can delete the issue"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
issue.delete()
|
||||
issue_activity.delay(
|
||||
type="issue.activity.deleted",
|
||||
@@ -585,7 +539,7 @@ class IssueUserDisplayPropertyEndpoint(BaseAPIView):
|
||||
]
|
||||
|
||||
def patch(self, request, slug, project_id):
|
||||
issue_property = IssueUserProperty.objects.get(
|
||||
issue_property = IssueProperty.objects.get(
|
||||
user=request.user,
|
||||
project_id=project_id,
|
||||
)
|
||||
@@ -600,14 +554,14 @@ class IssueUserDisplayPropertyEndpoint(BaseAPIView):
|
||||
"display_properties", issue_property.display_properties
|
||||
)
|
||||
issue_property.save()
|
||||
serializer = IssueUserPropertySerializer(issue_property)
|
||||
serializer = IssuePropertySerializer(issue_property)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
def get(self, request, slug, project_id):
|
||||
issue_property, _ = IssueUserProperty.objects.get_or_create(
|
||||
issue_property, _ = IssueProperty.objects.get_or_create(
|
||||
user=request.user, project_id=project_id
|
||||
)
|
||||
serializer = IssueUserPropertySerializer(issue_property)
|
||||
serializer = IssuePropertySerializer(issue_property)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@@ -617,19 +571,6 @@ class BulkDeleteIssuesEndpoint(BaseAPIView):
|
||||
]
|
||||
|
||||
def delete(self, request, slug, project_id):
|
||||
if ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role=20,
|
||||
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", [])
|
||||
|
||||
if not len(issue_ids):
|
||||
|
||||
@@ -40,7 +40,6 @@ from plane.db.models import (
|
||||
IssueReaction,
|
||||
IssueSubscriber,
|
||||
Project,
|
||||
ProjectMember,
|
||||
)
|
||||
from plane.utils.grouper import (
|
||||
issue_group_values,
|
||||
@@ -381,19 +380,6 @@ class IssueDraftViewSet(BaseViewSet):
|
||||
issue = Issue.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
if issue.created_by_id != request.user.id and (
|
||||
not ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role=20,
|
||||
project_id=project_id,
|
||||
is_active=True,
|
||||
).exists()
|
||||
):
|
||||
return Response(
|
||||
{"error": "Only admin or creator can delete the issue"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
issue.delete()
|
||||
issue_activity.delay(
|
||||
type="issue_draft.activity.deleted",
|
||||
|
||||
@@ -3,11 +3,8 @@ import json
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
from django.db.models import Q, OuterRef, F, Func, UUIDField, Value, CharField
|
||||
from django.db.models import Q
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
|
||||
# Third Party imports
|
||||
from rest_framework.response import Response
|
||||
@@ -23,9 +20,6 @@ from plane.app.permissions import ProjectEntityPermission
|
||||
from plane.db.models import (
|
||||
Project,
|
||||
IssueRelation,
|
||||
Issue,
|
||||
IssueAttachment,
|
||||
IssueLink,
|
||||
)
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
|
||||
@@ -37,6 +31,24 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(issue_id=self.kwargs.get("issue_id"))
|
||||
.filter(
|
||||
project__project_projectmember__member=self.request.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
project__archived_at__isnull=True,
|
||||
)
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("issue")
|
||||
.distinct()
|
||||
)
|
||||
|
||||
def list(self, request, slug, project_id, issue_id):
|
||||
issue_relations = (
|
||||
IssueRelation.objects.filter(
|
||||
@@ -49,146 +61,56 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
.order_by("-created_at")
|
||||
.distinct()
|
||||
)
|
||||
# get all blocking issues
|
||||
|
||||
blocking_issues = issue_relations.filter(
|
||||
relation_type="blocked_by", related_issue_id=issue_id
|
||||
).values_list("issue_id", flat=True)
|
||||
|
||||
# get all blocked by issues
|
||||
)
|
||||
blocked_by_issues = issue_relations.filter(
|
||||
relation_type="blocked_by", issue_id=issue_id
|
||||
).values_list("related_issue_id", flat=True)
|
||||
|
||||
# get all duplicate issues
|
||||
)
|
||||
duplicate_issues = issue_relations.filter(
|
||||
issue_id=issue_id, relation_type="duplicate"
|
||||
).values_list("related_issue_id", flat=True)
|
||||
|
||||
# get all relates to issues
|
||||
)
|
||||
duplicate_issues_related = issue_relations.filter(
|
||||
related_issue_id=issue_id, relation_type="duplicate"
|
||||
).values_list("issue_id", flat=True)
|
||||
|
||||
# get all relates to issues
|
||||
)
|
||||
relates_to_issues = issue_relations.filter(
|
||||
issue_id=issue_id, relation_type="relates_to"
|
||||
).values_list("related_issue_id", flat=True)
|
||||
|
||||
# get all relates to issues
|
||||
)
|
||||
relates_to_issues_related = issue_relations.filter(
|
||||
related_issue_id=issue_id, relation_type="relates_to"
|
||||
).values_list("issue_id", flat=True)
|
||||
)
|
||||
|
||||
queryset = (
|
||||
Issue.issue_objects
|
||||
.filter(workspace__slug=slug)
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=IssueAttachment.objects.filter(
|
||||
issue=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(
|
||||
parent=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
label_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=~Q(labels__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
assignee_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
)
|
||||
).distinct()
|
||||
blocked_by_issues_serialized = IssueRelationSerializer(
|
||||
blocked_by_issues, many=True
|
||||
).data
|
||||
duplicate_issues_serialized = IssueRelationSerializer(
|
||||
duplicate_issues, many=True
|
||||
).data
|
||||
relates_to_issues_serialized = IssueRelationSerializer(
|
||||
relates_to_issues, many=True
|
||||
).data
|
||||
|
||||
# Fields
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"state_id",
|
||||
"sort_order",
|
||||
"priority",
|
||||
"sequence_id",
|
||||
"project_id",
|
||||
"label_ids",
|
||||
"assignee_ids",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"relation_type",
|
||||
]
|
||||
# revere relation for blocked by issues
|
||||
blocking_issues_serialized = RelatedIssueSerializer(
|
||||
blocking_issues, many=True
|
||||
).data
|
||||
# reverse relation for duplicate issues
|
||||
duplicate_issues_related_serialized = RelatedIssueSerializer(
|
||||
duplicate_issues_related, many=True
|
||||
).data
|
||||
# reverse relation for related issues
|
||||
relates_to_issues_related_serialized = RelatedIssueSerializer(
|
||||
relates_to_issues_related, many=True
|
||||
).data
|
||||
|
||||
response_data = {
|
||||
"blocking": queryset.filter(pk__in=blocking_issues)
|
||||
.annotate(
|
||||
relation_type=Value("blocking", output_field=CharField())
|
||||
)
|
||||
.values(*fields),
|
||||
"blocked_by": queryset.filter(pk__in=blocked_by_issues)
|
||||
.annotate(
|
||||
relation_type=Value("blocked_by", output_field=CharField())
|
||||
)
|
||||
.values(*fields),
|
||||
"duplicate": queryset.filter(pk__in=duplicate_issues)
|
||||
.annotate(
|
||||
relation_type=Value(
|
||||
"duplicate",
|
||||
output_field=CharField(),
|
||||
)
|
||||
)
|
||||
.values(*fields)
|
||||
| queryset.filter(pk__in=duplicate_issues_related)
|
||||
.annotate(
|
||||
relation_type=Value(
|
||||
"duplicate",
|
||||
output_field=CharField(),
|
||||
)
|
||||
)
|
||||
.values(*fields),
|
||||
"relates_to": queryset.filter(pk__in=relates_to_issues)
|
||||
.annotate(
|
||||
relation_type=Value(
|
||||
"relates_to",
|
||||
output_field=CharField(),
|
||||
)
|
||||
)
|
||||
.values(*fields)
|
||||
| queryset.filter(pk__in=relates_to_issues_related)
|
||||
.annotate(
|
||||
relation_type=Value(
|
||||
"relates_to",
|
||||
output_field=CharField(),
|
||||
)
|
||||
)
|
||||
.values(*fields),
|
||||
"blocking": blocking_issues_serialized,
|
||||
"blocked_by": blocked_by_issues_serialized,
|
||||
"duplicate": duplicate_issues_serialized
|
||||
+ duplicate_issues_related_serialized,
|
||||
"relates_to": relates_to_issues_serialized
|
||||
+ relates_to_issues_related_serialized,
|
||||
}
|
||||
|
||||
return Response(response_data, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -48,7 +48,6 @@ from plane.db.models import (
|
||||
ModuleLink,
|
||||
ModuleUserProperties,
|
||||
Project,
|
||||
ProjectMember,
|
||||
)
|
||||
from plane.utils.analytics_plot import burndown_plot
|
||||
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||
@@ -738,21 +737,6 @@ class ModuleViewSet(BaseViewSet):
|
||||
module = Module.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
|
||||
if module.created_by_id != request.user.id and (
|
||||
not ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role=20,
|
||||
project_id=project_id,
|
||||
is_active=True,
|
||||
).exists()
|
||||
):
|
||||
return Response(
|
||||
{"error": "Only admin or creator can delete the module"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
module_issues = list(
|
||||
ModuleIssue.objects.filter(module_id=pk).values_list(
|
||||
"issue", flat=True
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Django imports
|
||||
from django.db.models import Exists, OuterRef, Q, Case, When, BooleanField
|
||||
from django.db.models import Exists, OuterRef, Q
|
||||
from django.utils import timezone
|
||||
|
||||
# Third party imports
|
||||
@@ -45,7 +45,6 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||
archived = request.GET.get("archived", "false")
|
||||
read = request.GET.get("read", None)
|
||||
type = request.GET.get("type", "all")
|
||||
mentioned = request.GET.get("mentioned", False)
|
||||
q_filters = Q()
|
||||
|
||||
inbox_issue = Issue.objects.filter(
|
||||
@@ -60,13 +59,6 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||
)
|
||||
.filter(entity_name="issue")
|
||||
.annotate(is_inbox_issue=Exists(inbox_issue))
|
||||
.annotate(
|
||||
is_mentioned_notification=Case(
|
||||
When(sender__icontains="mentioned", then=True),
|
||||
default=False,
|
||||
output_field=BooleanField(),
|
||||
)
|
||||
)
|
||||
.select_related("workspace", "project", "triggered_by", "receiver")
|
||||
.order_by("snoozed_till", "-created_at")
|
||||
)
|
||||
@@ -94,13 +86,6 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||
if read == "true":
|
||||
notifications = notifications.filter(read_at__isnull=False)
|
||||
|
||||
if mentioned:
|
||||
notifications = notifications.filter(sender__icontains="mentioned")
|
||||
else:
|
||||
notifications = notifications.exclude(
|
||||
sender__icontains="mentioned"
|
||||
)
|
||||
|
||||
type = type.split(",")
|
||||
# Subscribed issues
|
||||
if "subscribed" in type:
|
||||
@@ -225,35 +210,19 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||
class UnreadNotificationEndpoint(BaseAPIView):
|
||||
def get(self, request, slug):
|
||||
# Watching Issues Count
|
||||
unread_notifications_count = (
|
||||
Notification.objects.filter(
|
||||
workspace__slug=slug,
|
||||
receiver_id=request.user.id,
|
||||
read_at__isnull=True,
|
||||
archived_at__isnull=True,
|
||||
snoozed_till__isnull=True,
|
||||
)
|
||||
.exclude(sender__icontains="mentioned")
|
||||
.count()
|
||||
)
|
||||
|
||||
mention_notifications_count = Notification.objects.filter(
|
||||
unread_notifications_count = Notification.objects.filter(
|
||||
workspace__slug=slug,
|
||||
receiver_id=request.user.id,
|
||||
read_at__isnull=True,
|
||||
archived_at__isnull=True,
|
||||
snoozed_till__isnull=True,
|
||||
sender__icontains="mentioned",
|
||||
).count()
|
||||
|
||||
return Response(
|
||||
{
|
||||
"total_unread_notifications_count": int(
|
||||
unread_notifications_count
|
||||
),
|
||||
"mention_unread_notifications_count": int(
|
||||
mention_notifications_count
|
||||
),
|
||||
)
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@@ -38,7 +38,6 @@ from plane.db.models import (
|
||||
from ..base import BaseAPIView, BaseViewSet
|
||||
|
||||
from plane.bgtasks.page_transaction_task import page_transaction
|
||||
from plane.bgtasks.page_version_task import page_version
|
||||
|
||||
|
||||
def unarchive_archive_page_and_descendants(page_id, archived_at):
|
||||
@@ -216,14 +215,8 @@ class PageViewSet(BaseViewSet):
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
else:
|
||||
issue_ids = PageLog.objects.filter(
|
||||
page_id=pk, entity_name="issue"
|
||||
).values_list("entity_identifier", flat=True)
|
||||
data = PageDetailSerializer(page).data
|
||||
data["issue_ids"] = issue_ids
|
||||
return Response(
|
||||
data,
|
||||
status=status.HTTP_200_OK,
|
||||
PageDetailSerializer(page).data, status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
def lock(self, request, slug, project_id, pk):
|
||||
@@ -245,28 +238,6 @@ class PageViewSet(BaseViewSet):
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def access(self, request, slug, project_id, pk):
|
||||
access = request.data.get("access", 0)
|
||||
page = Page.objects.filter(
|
||||
pk=pk, workspace__slug=slug, projects__id=project_id
|
||||
).first()
|
||||
|
||||
# Only update access if the page owner is the requesting user
|
||||
if (
|
||||
page.access != request.data.get("access", page.access)
|
||||
and page.owned_by_id != request.user.id
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error": "Access cannot be updated since this page is owned by someone else"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
page.access = access
|
||||
page.save()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def list(self, request, slug, project_id):
|
||||
queryset = self.get_queryset()
|
||||
pages = PageSerializer(queryset, many=True).data
|
||||
@@ -333,20 +304,6 @@ class PageViewSet(BaseViewSet):
|
||||
pk=pk, workspace__slug=slug, projects__id=project_id
|
||||
)
|
||||
|
||||
if not page.owned_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 owner can delete the page"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
# only the owner and admin can delete the page
|
||||
if (
|
||||
ProjectMember.objects.filter(
|
||||
@@ -506,12 +463,6 @@ class PagesDescriptionViewSet(BaseViewSet):
|
||||
.first()
|
||||
)
|
||||
|
||||
if page is None:
|
||||
return Response(
|
||||
{"error": "Page not found"},
|
||||
status=404,
|
||||
)
|
||||
|
||||
if page.is_locked:
|
||||
return Response(
|
||||
{"error": "Page is locked"},
|
||||
@@ -524,38 +475,16 @@ class PagesDescriptionViewSet(BaseViewSet):
|
||||
status=472,
|
||||
)
|
||||
|
||||
# Serialize the existing instance
|
||||
existing_instance = json.dumps(
|
||||
{
|
||||
"description_html": page.description_html,
|
||||
},
|
||||
cls=DjangoJSONEncoder,
|
||||
)
|
||||
|
||||
# Get the base64 data from the request
|
||||
base64_data = request.data.get("description_binary")
|
||||
|
||||
# If base64 data is provided
|
||||
if base64_data:
|
||||
# Decode the base64 data to bytes
|
||||
new_binary_data = base64.b64decode(base64_data)
|
||||
# capture the page transaction
|
||||
if request.data.get("description_html"):
|
||||
page_transaction.delay(
|
||||
new_value=request.data,
|
||||
old_value=existing_instance,
|
||||
page_id=pk,
|
||||
)
|
||||
|
||||
# Store the updated binary data
|
||||
page.description_binary = new_binary_data
|
||||
page.description_html = request.data.get("description_html")
|
||||
page.save()
|
||||
# Return a success response
|
||||
page_version.delay(
|
||||
page_id=page.id,
|
||||
existing_instance=existing_instance,
|
||||
user_id=request.user.id,
|
||||
)
|
||||
return Response({"message": "Updated successfully"})
|
||||
else:
|
||||
return Response({"error": "No binary data provided"})
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
# Third party imports
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import PageVersion
|
||||
from ..base import BaseAPIView
|
||||
from plane.app.permissions import ProjectEntityPermission
|
||||
from plane.app.serializers import PageVersionSerializer
|
||||
|
||||
|
||||
class PageVersionEndpoint(BaseAPIView):
|
||||
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
def get(self, request, slug, project_id, page_id, pk=None):
|
||||
# Check if pk is provided
|
||||
if pk:
|
||||
# Return a single page version
|
||||
page_version = PageVersion.objects.get(
|
||||
workspace__slug=slug,
|
||||
page_id=page_id,
|
||||
pk=pk,
|
||||
)
|
||||
# Serialize the page version
|
||||
serializer = PageVersionSerializer(page_version)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
# Return all page versions
|
||||
page_versions = PageVersion.objects.filter(
|
||||
workspace__slug=slug,
|
||||
page_id=page_id,
|
||||
)
|
||||
# Serialize the page versions
|
||||
serializer = PageVersionSerializer(page_versions, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
@@ -39,7 +39,7 @@ from plane.db.models import (
|
||||
Cycle,
|
||||
Inbox,
|
||||
DeployBoard,
|
||||
IssueUserProperty,
|
||||
IssueProperty,
|
||||
Issue,
|
||||
Module,
|
||||
Project,
|
||||
@@ -266,7 +266,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
role=20,
|
||||
)
|
||||
# Also create the issue property for the user
|
||||
_ = IssueUserProperty.objects.create(
|
||||
_ = IssueProperty.objects.create(
|
||||
project_id=serializer.data["id"],
|
||||
user=request.user,
|
||||
)
|
||||
@@ -280,7 +280,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
role=20,
|
||||
)
|
||||
# Also create the issue property for the user
|
||||
IssueUserProperty.objects.create(
|
||||
IssueProperty.objects.create(
|
||||
project_id=serializer.data["id"],
|
||||
user_id=serializer.data["project_lead"],
|
||||
)
|
||||
@@ -342,7 +342,6 @@ class ProjectViewSet(BaseViewSet):
|
||||
.first()
|
||||
)
|
||||
|
||||
# Create the model activity
|
||||
model_activity.delay(
|
||||
model_name="project",
|
||||
model_id=str(project.id),
|
||||
|
||||
@@ -25,7 +25,7 @@ from plane.db.models import (
|
||||
ProjectMemberInvite,
|
||||
User,
|
||||
WorkspaceMember,
|
||||
IssueUserProperty,
|
||||
IssueProperty,
|
||||
)
|
||||
|
||||
|
||||
@@ -179,9 +179,9 @@ class UserProjectInvitationsViewset(BaseViewSet):
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
IssueUserProperty.objects.bulk_create(
|
||||
IssueProperty.objects.bulk_create(
|
||||
[
|
||||
IssueUserProperty(
|
||||
IssueProperty(
|
||||
project_id=project_id,
|
||||
user=request.user,
|
||||
workspace=workspace,
|
||||
|
||||
@@ -22,7 +22,7 @@ from plane.db.models import (
|
||||
ProjectMember,
|
||||
Workspace,
|
||||
TeamMember,
|
||||
IssueUserProperty,
|
||||
IssueProperty,
|
||||
)
|
||||
from plane.bgtasks.project_add_user_email_task import project_add_user_email
|
||||
from plane.utils.host import base_host
|
||||
@@ -136,7 +136,7 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
)
|
||||
# Create a new issue property
|
||||
bulk_issue_props.append(
|
||||
IssueUserProperty(
|
||||
IssueProperty(
|
||||
user_id=member.get("member_id"),
|
||||
project_id=project_id,
|
||||
workspace_id=project.workspace_id,
|
||||
@@ -150,7 +150,7 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
_ = IssueUserProperty.objects.bulk_create(
|
||||
_ = IssueProperty.objects.bulk_create(
|
||||
bulk_issue_props, batch_size=10, ignore_conflicts=True
|
||||
)
|
||||
|
||||
@@ -323,7 +323,7 @@ class AddTeamToProjectEndpoint(BaseAPIView):
|
||||
)
|
||||
)
|
||||
issue_props.append(
|
||||
IssueUserProperty(
|
||||
IssueProperty(
|
||||
project_id=project_id,
|
||||
user_id=member,
|
||||
workspace=workspace,
|
||||
@@ -335,7 +335,7 @@ class AddTeamToProjectEndpoint(BaseAPIView):
|
||||
project_members, batch_size=10, ignore_conflicts=True
|
||||
)
|
||||
|
||||
_ = IssueUserProperty.objects.bulk_create(
|
||||
_ = IssueProperty.objects.bulk_create(
|
||||
issue_props, batch_size=10, ignore_conflicts=True
|
||||
)
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Python imports
|
||||
import re
|
||||
|
||||
# Django imports
|
||||
from django.db.models import Q
|
||||
@@ -10,7 +11,13 @@ from rest_framework.response import Response
|
||||
# Module imports
|
||||
from .base import BaseAPIView
|
||||
from plane.db.models import (
|
||||
Workspace,
|
||||
Project,
|
||||
Issue,
|
||||
Cycle,
|
||||
Module,
|
||||
Page,
|
||||
IssueView,
|
||||
)
|
||||
from plane.utils.issue_search import search_issues
|
||||
|
||||
|
||||
@@ -37,9 +37,6 @@ from plane.utils.paginator import BasePaginator
|
||||
from plane.authentication.utils.host import user_ip
|
||||
from plane.bgtasks.user_deactivation_email_task import user_deactivation_email
|
||||
from plane.utils.host import base_host
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.cache import cache_control
|
||||
from django.views.decorators.vary import vary_on_cookie
|
||||
|
||||
|
||||
class UserEndpoint(BaseViewSet):
|
||||
@@ -50,8 +47,6 @@ class UserEndpoint(BaseViewSet):
|
||||
return self.request.user
|
||||
|
||||
@cache_response(60 * 60)
|
||||
@method_decorator(cache_control(private=True, max_age=12))
|
||||
@method_decorator(vary_on_cookie)
|
||||
def retrieve(self, request):
|
||||
serialized_data = UserMeSerializer(request.user).data
|
||||
return Response(
|
||||
@@ -60,8 +55,6 @@ class UserEndpoint(BaseViewSet):
|
||||
)
|
||||
|
||||
@cache_response(60 * 60)
|
||||
@method_decorator(cache_control(private=True, max_age=12))
|
||||
@method_decorator(vary_on_cookie)
|
||||
def retrieve_user_settings(self, request):
|
||||
serialized_data = UserMeSettingsSerializer(request.user).data
|
||||
return Response(serialized_data, status=status.HTTP_200_OK)
|
||||
@@ -86,9 +79,6 @@ class UserEndpoint(BaseViewSet):
|
||||
return super().partial_update(request, *args, **kwargs)
|
||||
|
||||
@invalidate_cache(path="/api/users/me/")
|
||||
@invalidate_cache(
|
||||
path="/api/users/me/workspaces/", multiple=True, user=False
|
||||
)
|
||||
def deactivate(self, request):
|
||||
# Check all workspace user is active
|
||||
user = self.get_object()
|
||||
@@ -298,8 +288,6 @@ class AccountEndpoint(BaseAPIView):
|
||||
|
||||
|
||||
class ProfileEndpoint(BaseAPIView):
|
||||
@method_decorator(cache_control(private=True, max_age=12))
|
||||
@method_decorator(vary_on_cookie)
|
||||
def get(self, request):
|
||||
profile = Profile.objects.get(user=request.user)
|
||||
serializer = ProfileSerializer(profile)
|
||||
|
||||
@@ -116,20 +116,6 @@ class WorkspaceViewViewSet(BaseViewSet):
|
||||
pk=pk,
|
||||
workspace__slug=slug,
|
||||
)
|
||||
if not (
|
||||
WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role=20,
|
||||
is_active=True,
|
||||
).exists()
|
||||
and workspace_view.owned_by_id != request.user.id
|
||||
):
|
||||
return Response(
|
||||
{"error": "You do not have permission to delete this view"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
workspace_member = WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
@@ -426,16 +412,14 @@ class IssueViewViewSet(BaseViewSet):
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
)
|
||||
if (
|
||||
ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
role=20,
|
||||
is_active=True,
|
||||
).exists()
|
||||
or project_view.owned_by_id == request.user.id
|
||||
):
|
||||
project_member = ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
role=20,
|
||||
is_active=True,
|
||||
)
|
||||
if project_member.exists() or project_view.owned_by == request.user:
|
||||
project_view.delete()
|
||||
else:
|
||||
return Response(
|
||||
|
||||
@@ -44,9 +44,6 @@ from plane.db.models import (
|
||||
WorkspaceTheme,
|
||||
)
|
||||
from plane.utils.cache import cache_response, invalidate_cache
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.cache import cache_control
|
||||
from django.views.decorators.vary import vary_on_cookie
|
||||
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
|
||||
|
||||
|
||||
@@ -175,8 +172,6 @@ class UserWorkSpacesEndpoint(BaseAPIView):
|
||||
]
|
||||
|
||||
@cache_response(60 * 60 * 2)
|
||||
@method_decorator(cache_control(private=True, max_age=12))
|
||||
@method_decorator(vary_on_cookie)
|
||||
def get(self, request):
|
||||
fields = [
|
||||
field
|
||||
|
||||
@@ -3,7 +3,7 @@ from plane.db.models import Profile, Workspace, WorkspaceMemberInvite
|
||||
|
||||
def get_redirection_path(user):
|
||||
# Handle redirections
|
||||
profile, _ = Profile.objects.get_or_create(user=user)
|
||||
profile = Profile.objects.get(user=user)
|
||||
|
||||
# Redirect to onboarding if the user is not onboarded yet
|
||||
if not profile.is_onboarded:
|
||||
|
||||
@@ -120,7 +120,7 @@ class MagicSignInEndpoint(View):
|
||||
callback=post_user_auth_workflow,
|
||||
)
|
||||
user = provider.authenticate()
|
||||
profile, _ = Profile.objects.get_or_create(user=user)
|
||||
profile = Profile.objects.get(user=user)
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_app=True)
|
||||
if user.is_password_autoset and profile.is_onboarded:
|
||||
|
||||
@@ -582,18 +582,17 @@ def create_issue_activity(
|
||||
issue_activities,
|
||||
epoch,
|
||||
):
|
||||
issue = Issue.objects.get(pk=issue_id)
|
||||
issue_activity = IssueActivity.objects.create(
|
||||
issue_id=issue_id,
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
comment="created the issue",
|
||||
verb="created",
|
||||
actor_id=actor_id,
|
||||
epoch=epoch,
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
comment="created the issue",
|
||||
verb="created",
|
||||
actor_id=actor_id,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
issue_activity.created_at = issue.created_at
|
||||
issue_activity.save(update_fields=["created_at"])
|
||||
requested_data = (
|
||||
json.loads(requested_data) if requested_data is not None else None
|
||||
)
|
||||
@@ -1392,7 +1391,6 @@ def create_issue_relation_activity(
|
||||
workspace_id=workspace_id,
|
||||
comment=f"added {requested_data.get('relation_type')} relation",
|
||||
old_identifier=related_issue,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
issue = Issue.objects.get(pk=issue_id)
|
||||
@@ -1718,16 +1716,12 @@ def issue_activity(
|
||||
event=(
|
||||
"issue_comment"
|
||||
if activity.field == "comment"
|
||||
else "inbox_issue"
|
||||
if inbox
|
||||
else "issue"
|
||||
else "inbox_issue" if inbox else "issue"
|
||||
),
|
||||
event_id=(
|
||||
activity.issue_comment_id
|
||||
if activity.field == "comment"
|
||||
else inbox
|
||||
if inbox
|
||||
else activity.issue_id
|
||||
else inbox if inbox else activity.issue_id
|
||||
),
|
||||
verb=activity.verb,
|
||||
field=(
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
# Python imports
|
||||
import json
|
||||
|
||||
# Third party imports
|
||||
from celery import shared_task
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import Page, PageVersion
|
||||
|
||||
|
||||
@shared_task
|
||||
def page_version(
|
||||
page_id,
|
||||
existing_instance,
|
||||
user_id,
|
||||
):
|
||||
# Get the page
|
||||
page = Page.objects.get(id=page_id)
|
||||
|
||||
# Get the current instance
|
||||
current_instance = (
|
||||
json.loads(existing_instance) if existing_instance is not None else {}
|
||||
)
|
||||
|
||||
# Create a version if description_html is updated
|
||||
if current_instance.get("description_html") != page.description_html:
|
||||
# Create a new page version
|
||||
PageVersion.objects.create(
|
||||
page_id=page_id,
|
||||
workspace_id=page.workspace_id,
|
||||
description_html=page.description_html,
|
||||
description_binary=page.description_binary,
|
||||
owned_by_id=user_id,
|
||||
last_saved_at=page.updated_at,
|
||||
)
|
||||
|
||||
# If page versions are greater than 20 delete the oldest one
|
||||
if PageVersion.objects.filter(page_id=page_id).count() > 20:
|
||||
# Delete the old page version
|
||||
PageVersion.objects.filter(page_id=page_id).order_by(
|
||||
"last_saved_at"
|
||||
).first().delete()
|
||||
|
||||
return
|
||||
@@ -161,7 +161,7 @@ class Migration(migrations.Migration):
|
||||
options={
|
||||
"verbose_name": "Workspace User Property",
|
||||
"verbose_name_plural": "Workspace User Property",
|
||||
"db_table": "workspace_user_properties",
|
||||
"db_table": "Workspace_user_properties",
|
||||
"ordering": ("-created_at",),
|
||||
"unique_together": {("workspace", "user")},
|
||||
},
|
||||
|
||||
-106
@@ -1,106 +0,0 @@
|
||||
# Generated by Django 4.2.11 on 2024-07-10 13:59
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0069_alter_account_provider_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='apitoken',
|
||||
name='is_service',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='exporterhistory',
|
||||
name='filters',
|
||||
field=models.JSONField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='exporterhistory',
|
||||
name='name',
|
||||
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Exporter Name'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='exporterhistory',
|
||||
name='type',
|
||||
field=models.CharField(choices=[('issue_exports', 'Issue Exports'), ('issue_worklogs', 'Issue Worklogs')], default='issue_exports', max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='is_time_tracking_enabled',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='start_date',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='target_date',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PageVersion',
|
||||
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')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('last_saved_at', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('description_binary', models.BinaryField(null=True)),
|
||||
('description_html', models.TextField(blank=True, default='<p></p>')),
|
||||
('description_stripped', models.TextField(blank=True, null=True)),
|
||||
('description_json', models.JSONField(blank=True, default=dict)),
|
||||
('created_by', 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')),
|
||||
('owned_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='page_versions', to=settings.AUTH_USER_MODEL)),
|
||||
('page', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='page_versions', to='db.page')),
|
||||
('updated_by', 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')),
|
||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='page_versions', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Page Version',
|
||||
'verbose_name_plural': 'Page Versions',
|
||||
'db_table': 'page_versions',
|
||||
'ordering': ('-created_at',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='IssueType',
|
||||
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')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('logo_props', models.JSONField(default=dict)),
|
||||
('sort_order', models.FloatField(default=65535)),
|
||||
('is_default', models.BooleanField(default=True)),
|
||||
('weight', models.PositiveIntegerField(default=0)),
|
||||
('created_by', 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')),
|
||||
('project', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')),
|
||||
('updated_by', 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')),
|
||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Issue Type',
|
||||
'verbose_name_plural': 'Issue Types',
|
||||
'db_table': 'issue_types',
|
||||
'ordering': ('sort_order',),
|
||||
'unique_together': {('project', 'name')},
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issue',
|
||||
name='type',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issue_type', to='db.issuetype'),
|
||||
),
|
||||
]
|
||||
@@ -1,44 +0,0 @@
|
||||
# Generated by Django 4.2.11 on 2024-07-15 06:07
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("db", "0070_apitoken_is_service_exporterhistory_filters_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name="IssueProperty",
|
||||
new_name="IssueUserProperty",
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="issueuserproperty",
|
||||
options={
|
||||
"ordering": ("-created_at",),
|
||||
"verbose_name": "Issue User Property",
|
||||
"verbose_name_plural": "Issue User Properties",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelTable(
|
||||
name="issueuserproperty",
|
||||
table="issue_user_properties",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="issuetype",
|
||||
name="is_active",
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="project",
|
||||
name="is_issue_type_enabled",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="issuetype",
|
||||
name="is_default",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -1,145 +0,0 @@
|
||||
# Generated by Django 4.2.14 on 2024-07-22 13:22
|
||||
from django.db import migrations, models
|
||||
from django.conf import settings
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("db", "0071_rename_issueproperty_issueuserproperty_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="issueattachment",
|
||||
name="external_id",
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="issueattachment",
|
||||
name="external_source",
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="UserRecentVisit",
|
||||
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"
|
||||
),
|
||||
),
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
db_index=True,
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
unique=True,
|
||||
),
|
||||
),
|
||||
("entity_identifier", models.UUIDField(null=True)),
|
||||
(
|
||||
"entity_name",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("VIEW", "View"),
|
||||
("PAGE", "Page"),
|
||||
("ISSUE", "Issue"),
|
||||
("CYCLE", "Cycle"),
|
||||
("MODULE", "Module"),
|
||||
("PROJECT", "Project"),
|
||||
],
|
||||
max_length=30,
|
||||
),
|
||||
),
|
||||
("visited_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"created_by",
|
||||
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",
|
||||
),
|
||||
),
|
||||
(
|
||||
"project",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="project_%(class)s",
|
||||
to="db.project",
|
||||
),
|
||||
),
|
||||
(
|
||||
"updated_by",
|
||||
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",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="user_recent_visit",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"workspace",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="workspace_%(class)s",
|
||||
to="db.workspace",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "User Recent Visit",
|
||||
"verbose_name_plural": "User Recent Visits",
|
||||
"db_table": "user_recent_visits",
|
||||
"ordering": ("-created_at",),
|
||||
},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="project",
|
||||
name="start_date",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="project",
|
||||
name="target_date",
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="issuesequence",
|
||||
name="sequence",
|
||||
field=models.PositiveBigIntegerField(db_index=True, default=1),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="project",
|
||||
name="identifier",
|
||||
field=models.CharField(
|
||||
db_index=True, max_length=12, verbose_name="Project Identifier"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="projectidentifier",
|
||||
name="name",
|
||||
field=models.CharField(db_index=True, max_length=12),
|
||||
),
|
||||
]
|
||||
@@ -29,7 +29,7 @@ from .issue import (
|
||||
IssueLabel,
|
||||
IssueLink,
|
||||
IssueMention,
|
||||
IssueUserProperty,
|
||||
IssueProperty,
|
||||
IssueReaction,
|
||||
IssueRelation,
|
||||
IssueSequence,
|
||||
@@ -50,14 +50,7 @@ from .notification import (
|
||||
Notification,
|
||||
UserNotificationPreference,
|
||||
)
|
||||
from .page import (
|
||||
Page,
|
||||
PageFavorite,
|
||||
PageLabel,
|
||||
PageLog,
|
||||
ProjectPage,
|
||||
PageVersion,
|
||||
)
|
||||
from .page import Page, PageFavorite, PageLabel, PageLog, ProjectPage
|
||||
from .project import (
|
||||
Project,
|
||||
ProjectBaseModel,
|
||||
@@ -108,7 +101,3 @@ from .webhook import Webhook, WebhookLog
|
||||
from .dashboard import Dashboard, DashboardWidget, Widget
|
||||
|
||||
from .favorite import UserFavorite
|
||||
|
||||
from .issue_type import IssueType
|
||||
|
||||
from .recent_visit import UserRecentVisit
|
||||
@@ -44,7 +44,6 @@ class APIToken(BaseModel):
|
||||
null=True,
|
||||
)
|
||||
expired_at = models.DateTimeField(blank=True, null=True)
|
||||
is_service = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "API Token"
|
||||
|
||||
@@ -18,17 +18,6 @@ def generate_token():
|
||||
|
||||
|
||||
class ExporterHistory(BaseModel):
|
||||
name = models.CharField(
|
||||
max_length=255, verbose_name="Exporter Name", null=True, blank=True
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
default="issue_exports",
|
||||
choices=(
|
||||
("issue_exports", "Issue Exports"),
|
||||
("issue_worklogs", "Issue Worklogs"),
|
||||
),
|
||||
)
|
||||
workspace = models.ForeignKey(
|
||||
"db.WorkSpace",
|
||||
on_delete=models.CASCADE,
|
||||
@@ -66,7 +55,6 @@ class ExporterHistory(BaseModel):
|
||||
on_delete=models.CASCADE,
|
||||
related_name="workspace_exporters",
|
||||
)
|
||||
filters = models.JSONField(blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Exporter"
|
||||
|
||||
@@ -6,7 +6,9 @@ from django.conf import settings
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models, transaction
|
||||
from django.db import models
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from django.utils import timezone
|
||||
|
||||
# Module imports
|
||||
@@ -162,13 +164,6 @@ class Issue(ProjectBaseModel):
|
||||
is_draft = models.BooleanField(default=False)
|
||||
external_source = models.CharField(max_length=255, null=True, blank=True)
|
||||
external_id = models.CharField(max_length=255, blank=True, null=True)
|
||||
type = models.ForeignKey(
|
||||
"db.IssueType",
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="issue_type",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
objects = models.Manager()
|
||||
issue_objects = IssueManager()
|
||||
@@ -180,6 +175,7 @@ class Issue(ProjectBaseModel):
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# This means that the model isn't saved to the database yet
|
||||
if self.state is None:
|
||||
try:
|
||||
from plane.db.models import State
|
||||
@@ -189,6 +185,7 @@ class Issue(ProjectBaseModel):
|
||||
project=self.project,
|
||||
default=True,
|
||||
).first()
|
||||
# if there is no default state assign any random state
|
||||
if default_state is None:
|
||||
random_state = State.objects.filter(
|
||||
~models.Q(is_triage=True), project=self.project
|
||||
@@ -202,6 +199,7 @@ class Issue(ProjectBaseModel):
|
||||
try:
|
||||
from plane.db.models import State
|
||||
|
||||
# Check if the current issue state group is completed or not
|
||||
if self.state.group == "completed":
|
||||
self.completed_at = timezone.now()
|
||||
else:
|
||||
@@ -210,44 +208,30 @@ class Issue(ProjectBaseModel):
|
||||
pass
|
||||
|
||||
if self._state.adding:
|
||||
with transaction.atomic():
|
||||
last_sequence = (
|
||||
IssueSequence.objects.filter(project=self.project)
|
||||
.select_for_update()
|
||||
.aggregate(largest=models.Max("sequence"))["largest"]
|
||||
)
|
||||
self.sequence_id = last_sequence + 1 if last_sequence else 1
|
||||
# Strip the html tags using html parser
|
||||
self.description_stripped = (
|
||||
None
|
||||
if (
|
||||
self.description_html == ""
|
||||
or self.description_html is None
|
||||
)
|
||||
else strip_tags(self.description_html)
|
||||
)
|
||||
largest_sort_order = Issue.objects.filter(
|
||||
project=self.project, state=self.state
|
||||
).aggregate(largest=models.Max("sort_order"))["largest"]
|
||||
if largest_sort_order is not None:
|
||||
self.sort_order = largest_sort_order + 10000
|
||||
# Get the maximum display_id value from the database
|
||||
last_id = IssueSequence.objects.filter(
|
||||
project=self.project
|
||||
).aggregate(largest=models.Max("sequence"))["largest"]
|
||||
# aggregate can return None! Check it first.
|
||||
# If it isn't none, just use the last ID specified (which should be the greatest) and add one to it
|
||||
if last_id:
|
||||
self.sequence_id = last_id + 1
|
||||
else:
|
||||
self.sequence_id = 1
|
||||
|
||||
super(Issue, self).save(*args, **kwargs)
|
||||
largest_sort_order = Issue.objects.filter(
|
||||
project=self.project, state=self.state
|
||||
).aggregate(largest=models.Max("sort_order"))["largest"]
|
||||
if largest_sort_order is not None:
|
||||
self.sort_order = largest_sort_order + 10000
|
||||
|
||||
IssueSequence.objects.create(
|
||||
issue=self, sequence=self.sequence_id, project=self.project
|
||||
)
|
||||
else:
|
||||
# Strip the html tags using html parser
|
||||
self.description_stripped = (
|
||||
None
|
||||
if (
|
||||
self.description_html == ""
|
||||
or self.description_html is None
|
||||
)
|
||||
else strip_tags(self.description_html)
|
||||
)
|
||||
super(Issue, self).save(*args, **kwargs)
|
||||
# Strip the html tags using html parser
|
||||
self.description_stripped = (
|
||||
None
|
||||
if (self.description_html == "" or self.description_html is None)
|
||||
else strip_tags(self.description_html)
|
||||
)
|
||||
super(Issue, self).save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
"""Return name of the issue"""
|
||||
@@ -384,8 +368,6 @@ class IssueAttachment(ProjectBaseModel):
|
||||
issue = models.ForeignKey(
|
||||
"db.Issue", on_delete=models.CASCADE, related_name="issue_attachment"
|
||||
)
|
||||
external_source = models.CharField(max_length=255, null=True, blank=True)
|
||||
external_id = models.CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Issue Attachment"
|
||||
@@ -493,7 +475,7 @@ class IssueComment(ProjectBaseModel):
|
||||
return str(self.issue)
|
||||
|
||||
|
||||
class IssueUserProperty(ProjectBaseModel):
|
||||
class IssueProperty(ProjectBaseModel):
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
@@ -506,9 +488,9 @@ class IssueUserProperty(ProjectBaseModel):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Issue User Property"
|
||||
verbose_name_plural = "Issue User Properties"
|
||||
db_table = "issue_user_properties"
|
||||
verbose_name = "Issue Property"
|
||||
verbose_name_plural = "Issue Properties"
|
||||
db_table = "issue_properties"
|
||||
ordering = ("-created_at",)
|
||||
unique_together = ["user", "project"]
|
||||
|
||||
@@ -578,9 +560,9 @@ class IssueSequence(ProjectBaseModel):
|
||||
Issue,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="issue_sequence",
|
||||
null=True, # This is set to null because we want to keep the sequence even if the issue is deleted
|
||||
null=True,
|
||||
)
|
||||
sequence = models.PositiveBigIntegerField(default=1, db_index=True)
|
||||
sequence = models.PositiveBigIntegerField(default=1)
|
||||
deleted = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
@@ -686,3 +668,14 @@ class IssueVote(ProjectBaseModel):
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.issue.name} {self.actor.email}"
|
||||
|
||||
|
||||
# TODO: Find a better method to save the model
|
||||
@receiver(post_save, sender=Issue)
|
||||
def create_issue_sequence(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
IssueSequence.objects.create(
|
||||
issue=instance,
|
||||
sequence=instance.sequence_id,
|
||||
project=instance.project,
|
||||
)
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
# Django imports
|
||||
from django.db import models
|
||||
|
||||
# Module imports
|
||||
from .workspace import WorkspaceBaseModel
|
||||
|
||||
|
||||
class IssueType(WorkspaceBaseModel):
|
||||
name = models.CharField(max_length=255)
|
||||
description = models.TextField(blank=True)
|
||||
logo_props = models.JSONField(default=dict)
|
||||
sort_order = models.FloatField(default=65535)
|
||||
is_default = models.BooleanField(default=False)
|
||||
weight = models.PositiveIntegerField(default=0)
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["project", "name"]
|
||||
verbose_name = "Issue Type"
|
||||
verbose_name_plural = "Issue Types"
|
||||
db_table = "issue_types"
|
||||
ordering = ("sort_order",)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# If we are adding a new issue type, we need to set the sort order
|
||||
if self._state.adding:
|
||||
# Get the largest sort order for the project
|
||||
largest_sort_order = IssueType.objects.filter(
|
||||
project=self.project
|
||||
).aggregate(largest=models.Max("sort_order"))["largest"]
|
||||
# If there are issue types, set the sort order to the largest + 10000
|
||||
if largest_sort_order is not None:
|
||||
self.sort_order = largest_sort_order + 10000
|
||||
super(IssueType, self).save(*args, **kwargs)
|
||||
@@ -1,7 +1,6 @@
|
||||
import uuid
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
|
||||
# Django imports
|
||||
from django.db import models
|
||||
@@ -67,15 +66,6 @@ class Page(BaseModel):
|
||||
"""Return owner email and page name"""
|
||||
return f"{self.owned_by.email} <{self.name}>"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Strip the html tags using html parser
|
||||
self.description_stripped = (
|
||||
None
|
||||
if (self.description_html == "" or self.description_html is None)
|
||||
else strip_tags(self.description_html)
|
||||
)
|
||||
super(Page, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
class PageLog(BaseModel):
|
||||
TYPE_CHOICES = (
|
||||
@@ -103,9 +93,7 @@ class PageLog(BaseModel):
|
||||
verbose_name="Transaction Type",
|
||||
)
|
||||
workspace = models.ForeignKey(
|
||||
"db.Workspace",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="workspace_page_log",
|
||||
"db.Workspace", on_delete=models.CASCADE, related_name="workspace_page_log"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -259,41 +247,3 @@ class TeamPage(BaseModel):
|
||||
verbose_name_plural = "Team Pages"
|
||||
db_table = "team_pages"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
|
||||
class PageVersion(BaseModel):
|
||||
workspace = models.ForeignKey(
|
||||
"db.Workspace",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="page_versions",
|
||||
)
|
||||
page = models.ForeignKey(
|
||||
"db.Page",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="page_versions",
|
||||
)
|
||||
last_saved_at = models.DateTimeField(default=timezone.now)
|
||||
owned_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="page_versions",
|
||||
)
|
||||
description_binary = models.BinaryField(null=True)
|
||||
description_html = models.TextField(blank=True, default="<p></p>")
|
||||
description_stripped = models.TextField(blank=True, null=True)
|
||||
description_json = models.JSONField(default=dict, blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Page Version"
|
||||
verbose_name_plural = "Page Versions"
|
||||
db_table = "page_versions"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Strip the html tags using html parser
|
||||
self.description_stripped = (
|
||||
None
|
||||
if (self.description_html == "" or self.description_html is None)
|
||||
else strip_tags(self.description_html)
|
||||
)
|
||||
super(PageVersion, self).save(*args, **kwargs)
|
||||
|
||||
@@ -72,7 +72,6 @@ class Project(BaseModel):
|
||||
identifier = models.CharField(
|
||||
max_length=12,
|
||||
verbose_name="Project Identifier",
|
||||
db_index=True,
|
||||
)
|
||||
default_assignee = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
@@ -95,8 +94,6 @@ class Project(BaseModel):
|
||||
issue_views_view = models.BooleanField(default=True)
|
||||
page_view = models.BooleanField(default=True)
|
||||
inbox_view = models.BooleanField(default=False)
|
||||
is_time_tracking_enabled = models.BooleanField(default=False)
|
||||
is_issue_type_enabled = models.BooleanField(default=False)
|
||||
cover_image = models.URLField(blank=True, null=True, max_length=800)
|
||||
estimate = models.ForeignKey(
|
||||
"db.Estimate",
|
||||
@@ -220,7 +217,7 @@ class ProjectIdentifier(AuditModel):
|
||||
project = models.OneToOneField(
|
||||
Project, on_delete=models.CASCADE, related_name="project_identifier"
|
||||
)
|
||||
name = models.CharField(max_length=12, db_index=True)
|
||||
name = models.CharField(max_length=12)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["name", "workspace"]
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
# Django imports
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
|
||||
# Module imports
|
||||
from .workspace import WorkspaceBaseModel
|
||||
|
||||
|
||||
class EntityNameEnum(models.TextChoices):
|
||||
VIEW = "VIEW", "View"
|
||||
PAGE = "PAGE", "Page"
|
||||
ISSUE = "ISSUE", "Issue"
|
||||
CYCLE = "CYCLE", "Cycle"
|
||||
MODULE = "MODULE", "Module"
|
||||
PROJECT = "PROJECT", "Project"
|
||||
|
||||
|
||||
class UserRecentVisit(WorkspaceBaseModel):
|
||||
entity_identifier = models.UUIDField(null=True)
|
||||
entity_name = models.CharField(
|
||||
max_length=30,
|
||||
choices=EntityNameEnum.choices,
|
||||
)
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="user_recent_visit",
|
||||
)
|
||||
visited_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "User Recent Visit"
|
||||
verbose_name_plural = "User Recent Visits"
|
||||
db_table = "user_recent_visits"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.entity_name} {self.user.email}"
|
||||
@@ -6,7 +6,6 @@ from django.db import models
|
||||
from .base import BaseModel
|
||||
from .project import ProjectBaseModel
|
||||
from .workspace import WorkspaceBaseModel
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
|
||||
|
||||
def get_default_filters():
|
||||
@@ -117,26 +116,6 @@ class IssueView(WorkspaceBaseModel):
|
||||
db_table = "issue_views"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
query_params = self.filters
|
||||
self.query = (
|
||||
issue_filters(query_params, "POST") if query_params else {}
|
||||
)
|
||||
|
||||
if self._state.adding:
|
||||
if self.project:
|
||||
largest_sort_order = IssueView.objects.filter(
|
||||
project=self.project
|
||||
).aggregate(largest=models.Max("sort_order"))["largest"]
|
||||
else:
|
||||
largest_sort_order = IssueView.objects.filter(
|
||||
workspace=self.workspace, project__isnull=True
|
||||
).aggregate(largest=models.Max("sort_order"))["largest"]
|
||||
if largest_sort_order is not None:
|
||||
self.sort_order = largest_sort_order + 10000
|
||||
|
||||
super(IssueView, self).save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
"""Return name of the View"""
|
||||
return f"{self.name} <{self.project.name}>"
|
||||
|
||||
@@ -23,8 +23,6 @@ from plane.license.utils.instance_value import (
|
||||
get_configuration_value,
|
||||
)
|
||||
from plane.utils.cache import cache_response, invalidate_cache
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.cache import cache_control
|
||||
|
||||
|
||||
class InstanceEndpoint(BaseAPIView):
|
||||
@@ -38,7 +36,6 @@ class InstanceEndpoint(BaseAPIView):
|
||||
]
|
||||
|
||||
@cache_response(60 * 60 * 2, user=False)
|
||||
@method_decorator(cache_control(private=True, max_age=12))
|
||||
def get(self, request):
|
||||
instance = Instance.objects.first()
|
||||
|
||||
|
||||
@@ -294,7 +294,7 @@ if bool(os.environ.get("SENTRY_DSN", False)) and os.environ.get(
|
||||
send_default_pii=True,
|
||||
environment=os.environ.get("SENTRY_ENVIRONMENT", "development"),
|
||||
profiles_sample_rate=float(
|
||||
os.environ.get("SENTRY_PROFILE_SAMPLE_RATE", 0)
|
||||
os.environ.get("SENTRY_PROFILE_SAMPLE_RATE", 0.5)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
from .user import UserLiteSerializer
|
||||
|
||||
from .issue import (
|
||||
LabelLiteSerializer,
|
||||
StateLiteSerializer,
|
||||
IssuePublicSerializer,
|
||||
)
|
||||
from .issue import LabelLiteSerializer, StateLiteSerializer
|
||||
|
||||
from .state import StateSerializer, StateLiteSerializer
|
||||
|
||||
@@ -188,16 +188,11 @@ class IssueAttachmentSerializer(BaseSerializer):
|
||||
|
||||
|
||||
class IssueReactionSerializer(BaseSerializer):
|
||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||
|
||||
class Meta:
|
||||
model = IssueReaction
|
||||
fields = [
|
||||
"issue",
|
||||
"reaction",
|
||||
"workspace",
|
||||
"project",
|
||||
"actor",
|
||||
]
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
@@ -459,6 +454,20 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class IssueReactionSerializer(BaseSerializer):
|
||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||
|
||||
class Meta:
|
||||
model = IssueReaction
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
"issue",
|
||||
"actor",
|
||||
]
|
||||
|
||||
|
||||
class CommentReactionSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = CommentReaction
|
||||
@@ -467,6 +476,7 @@ class CommentReactionSerializer(BaseSerializer):
|
||||
|
||||
|
||||
class IssueVoteSerializer(BaseSerializer):
|
||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||
|
||||
class Meta:
|
||||
model = IssueVote
|
||||
@@ -476,45 +486,35 @@ class IssueVoteSerializer(BaseSerializer):
|
||||
"workspace",
|
||||
"project",
|
||||
"actor",
|
||||
"actor_detail",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
class IssuePublicSerializer(BaseSerializer):
|
||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||
state_detail = StateLiteSerializer(read_only=True, source="state")
|
||||
reactions = IssueReactionSerializer(
|
||||
read_only=True, many=True, source="issue_reactions"
|
||||
)
|
||||
votes = IssueVoteSerializer(read_only=True, many=True)
|
||||
module_ids = serializers.ListField(
|
||||
child=serializers.UUIDField(),
|
||||
required=False,
|
||||
)
|
||||
label_ids = serializers.ListField(
|
||||
child=serializers.UUIDField(),
|
||||
required=False,
|
||||
)
|
||||
assignee_ids = serializers.ListField(
|
||||
child=serializers.UUIDField(),
|
||||
required=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Issue
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"description_html",
|
||||
"sequence_id",
|
||||
"state",
|
||||
"state_detail",
|
||||
"project",
|
||||
"project_detail",
|
||||
"workspace",
|
||||
"priority",
|
||||
"target_date",
|
||||
"reactions",
|
||||
"votes",
|
||||
"module_ids",
|
||||
"created_by",
|
||||
"label_ids",
|
||||
"assignee_ids",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ from django.urls import path
|
||||
|
||||
from plane.space.views import (
|
||||
InboxIssuePublicViewSet,
|
||||
IssueVotePublicViewSet,
|
||||
WorkspaceProjectDeployBoardEndpoint,
|
||||
)
|
||||
|
||||
@@ -29,6 +30,17 @@ urlpatterns = [
|
||||
),
|
||||
name="inbox-issue",
|
||||
),
|
||||
path(
|
||||
"anchor/<str:anchor>/issues/<uuid:issue_id>/votes/",
|
||||
IssueVotePublicViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="issue-vote-project-board",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/project-boards/",
|
||||
WorkspaceProjectDeployBoardEndpoint.as_view(),
|
||||
|
||||
@@ -6,7 +6,6 @@ from plane.space.views import (
|
||||
IssueCommentPublicViewSet,
|
||||
IssueReactionPublicViewSet,
|
||||
CommentReactionPublicViewSet,
|
||||
IssueVotePublicViewSet,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
@@ -74,15 +73,4 @@ urlpatterns = [
|
||||
),
|
||||
name="comment-reactions-project-board",
|
||||
),
|
||||
path(
|
||||
"anchor/<str:anchor>/issues/<uuid:issue_id>/votes/",
|
||||
IssueVotePublicViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="issue-vote-project-board",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -5,11 +5,6 @@ from plane.space.views import (
|
||||
ProjectDeployBoardPublicSettingsEndpoint,
|
||||
ProjectIssuesPublicEndpoint,
|
||||
WorkspaceProjectAnchorEndpoint,
|
||||
ProjectCyclesEndpoint,
|
||||
ProjectModulesEndpoint,
|
||||
ProjectStatesEndpoint,
|
||||
ProjectLabelsEndpoint,
|
||||
ProjectMembersEndpoint,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
@@ -28,29 +23,4 @@ urlpatterns = [
|
||||
WorkspaceProjectAnchorEndpoint.as_view(),
|
||||
name="project-deploy-board",
|
||||
),
|
||||
path(
|
||||
"anchor/<str:anchor>/cycles/",
|
||||
ProjectCyclesEndpoint.as_view(),
|
||||
name="project-cycles",
|
||||
),
|
||||
path(
|
||||
"anchor/<str:anchor>/modules/",
|
||||
ProjectModulesEndpoint.as_view(),
|
||||
name="project-modules",
|
||||
),
|
||||
path(
|
||||
"anchor/<str:anchor>/states/",
|
||||
ProjectStatesEndpoint.as_view(),
|
||||
name="project-states",
|
||||
),
|
||||
path(
|
||||
"anchor/<str:anchor>/labels/",
|
||||
ProjectLabelsEndpoint.as_view(),
|
||||
name="project-labels",
|
||||
),
|
||||
path(
|
||||
"anchor/<str:anchor>/members/",
|
||||
ProjectMembersEndpoint.as_view(),
|
||||
name="project-members",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,248 +0,0 @@
|
||||
# Django imports
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db.models import Q, UUIDField, Value, F, Case, When, JSONField
|
||||
from django.db.models.functions import Coalesce, JSONObject
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import (
|
||||
Cycle,
|
||||
Issue,
|
||||
Label,
|
||||
Module,
|
||||
Project,
|
||||
ProjectMember,
|
||||
State,
|
||||
WorkspaceMember,
|
||||
)
|
||||
|
||||
def issue_queryset_grouper(queryset, group_by, sub_group_by):
|
||||
|
||||
FIELD_MAPPER = {
|
||||
"label_ids": "labels__id",
|
||||
"assignee_ids": "assignees__id",
|
||||
"module_ids": "issue_module__module_id",
|
||||
}
|
||||
|
||||
annotations_map = {
|
||||
"assignee_ids": ("assignees__id", ~Q(assignees__id__isnull=True)),
|
||||
"label_ids": ("labels__id", ~Q(labels__id__isnull=True)),
|
||||
"module_ids": (
|
||||
"issue_module__module_id",
|
||||
~Q(issue_module__module_id__isnull=True),
|
||||
),
|
||||
}
|
||||
default_annotations = {
|
||||
key: Coalesce(
|
||||
ArrayAgg(
|
||||
field,
|
||||
distinct=True,
|
||||
filter=condition,
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
)
|
||||
for key, (field, condition) in annotations_map.items()
|
||||
if FIELD_MAPPER.get(key) != group_by
|
||||
or FIELD_MAPPER.get(key) != sub_group_by
|
||||
}
|
||||
|
||||
return queryset.annotate(**default_annotations)
|
||||
|
||||
|
||||
def issue_on_results(issues, group_by, sub_group_by):
|
||||
|
||||
FIELD_MAPPER = {
|
||||
"labels__id": "label_ids",
|
||||
"assignees__id": "assignee_ids",
|
||||
"issue_module__module_id": "module_ids",
|
||||
}
|
||||
|
||||
original_list = ["assignee_ids", "label_ids", "module_ids"]
|
||||
|
||||
required_fields = [
|
||||
"id",
|
||||
"name",
|
||||
"state_id",
|
||||
"sort_order",
|
||||
"estimate_point",
|
||||
"priority",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"sequence_id",
|
||||
"project_id",
|
||||
"parent_id",
|
||||
"cycle_id",
|
||||
"created_by",
|
||||
"state__group",
|
||||
]
|
||||
|
||||
if group_by in FIELD_MAPPER:
|
||||
original_list.remove(FIELD_MAPPER[group_by])
|
||||
original_list.append(group_by)
|
||||
|
||||
if sub_group_by in FIELD_MAPPER:
|
||||
original_list.remove(FIELD_MAPPER[sub_group_by])
|
||||
original_list.append(sub_group_by)
|
||||
|
||||
required_fields.extend(original_list)
|
||||
|
||||
issues = issues.annotate(
|
||||
vote_items=ArrayAgg(
|
||||
Case(
|
||||
When(
|
||||
votes__isnull=False,
|
||||
then=JSONObject(
|
||||
vote=F("votes__vote"),
|
||||
actor_details=JSONObject(
|
||||
id=F("votes__actor__id"),
|
||||
first_name=F("votes__actor__first_name"),
|
||||
last_name=F("votes__actor__last_name"),
|
||||
avatar=F("votes__actor__avatar"),
|
||||
display_name=F("votes__actor__display_name"),
|
||||
)
|
||||
),
|
||||
),
|
||||
default=None,
|
||||
output_field=JSONField(),
|
||||
),
|
||||
filter=Case(
|
||||
When(votes__isnull=False, then=True),
|
||||
default=False,
|
||||
output_field=JSONField(),
|
||||
),
|
||||
distinct=True,
|
||||
),
|
||||
reaction_items=ArrayAgg(
|
||||
Case(
|
||||
When(
|
||||
issue_reactions__isnull=False,
|
||||
then=JSONObject(
|
||||
reaction=F("issue_reactions__reaction"),
|
||||
actor_details=JSONObject(
|
||||
id=F("issue_reactions__actor__id"),
|
||||
first_name=F("issue_reactions__actor__first_name"),
|
||||
last_name=F("issue_reactions__actor__last_name"),
|
||||
avatar=F("issue_reactions__actor__avatar"),
|
||||
display_name=F("issue_reactions__actor__display_name"),
|
||||
),
|
||||
),
|
||||
),
|
||||
default=None,
|
||||
output_field=JSONField(),
|
||||
),
|
||||
filter=Case(
|
||||
When(issue_reactions__isnull=False, then=True),
|
||||
default=False,
|
||||
output_field=JSONField(),
|
||||
),
|
||||
distinct=True,
|
||||
),
|
||||
).values(*required_fields, "vote_items", "reaction_items")
|
||||
|
||||
return issues
|
||||
|
||||
|
||||
def issue_group_values(field, slug, project_id=None, filters=dict):
|
||||
if field == "state_id":
|
||||
queryset = State.objects.filter(
|
||||
is_triage=False,
|
||||
workspace__slug=slug,
|
||||
).values_list("id", flat=True)
|
||||
if project_id:
|
||||
return list(queryset.filter(project_id=project_id))
|
||||
else:
|
||||
return list(queryset)
|
||||
if field == "labels__id":
|
||||
queryset = Label.objects.filter(workspace__slug=slug).values_list(
|
||||
"id", flat=True
|
||||
)
|
||||
if project_id:
|
||||
return list(queryset.filter(project_id=project_id)) + ["None"]
|
||||
else:
|
||||
return list(queryset) + ["None"]
|
||||
if field == "assignees__id":
|
||||
if project_id:
|
||||
return ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
is_active=True,
|
||||
).values_list("member_id", flat=True)
|
||||
else:
|
||||
return list(
|
||||
WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug, is_active=True
|
||||
).values_list("member_id", flat=True)
|
||||
)
|
||||
if field == "issue_module__module_id":
|
||||
queryset = Module.objects.filter(
|
||||
workspace__slug=slug,
|
||||
).values_list("id", flat=True)
|
||||
if project_id:
|
||||
return list(queryset.filter(project_id=project_id)) + ["None"]
|
||||
else:
|
||||
return list(queryset) + ["None"]
|
||||
if field == "cycle_id":
|
||||
queryset = Cycle.objects.filter(
|
||||
workspace__slug=slug,
|
||||
).values_list("id", flat=True)
|
||||
if project_id:
|
||||
return list(queryset.filter(project_id=project_id)) + ["None"]
|
||||
else:
|
||||
return list(queryset) + ["None"]
|
||||
if field == "project_id":
|
||||
queryset = Project.objects.filter(workspace__slug=slug).values_list(
|
||||
"id", flat=True
|
||||
)
|
||||
return list(queryset)
|
||||
if field == "priority":
|
||||
return [
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"urgent",
|
||||
"none",
|
||||
]
|
||||
if field == "state__group":
|
||||
return [
|
||||
"backlog",
|
||||
"unstarted",
|
||||
"started",
|
||||
"completed",
|
||||
"cancelled",
|
||||
]
|
||||
if field == "target_date":
|
||||
queryset = (
|
||||
Issue.issue_objects.filter(workspace__slug=slug)
|
||||
.filter(**filters)
|
||||
.values_list("target_date", flat=True)
|
||||
.distinct()
|
||||
)
|
||||
if project_id:
|
||||
return list(queryset.filter(project_id=project_id))
|
||||
else:
|
||||
return list(queryset)
|
||||
if field == "start_date":
|
||||
queryset = (
|
||||
Issue.issue_objects.filter(workspace__slug=slug)
|
||||
.filter(**filters)
|
||||
.values_list("start_date", flat=True)
|
||||
.distinct()
|
||||
)
|
||||
if project_id:
|
||||
return list(queryset.filter(project_id=project_id))
|
||||
else:
|
||||
return list(queryset)
|
||||
|
||||
if field == "created_by":
|
||||
queryset = (
|
||||
Issue.issue_objects.filter(workspace__slug=slug)
|
||||
.filter(**filters)
|
||||
.values_list("created_by", flat=True)
|
||||
.distinct()
|
||||
)
|
||||
if project_id:
|
||||
return list(queryset.filter(project_id=project_id))
|
||||
else:
|
||||
return list(queryset)
|
||||
|
||||
return []
|
||||
@@ -2,7 +2,6 @@ from .project import (
|
||||
ProjectDeployBoardPublicSettingsEndpoint,
|
||||
WorkspaceProjectDeployBoardEndpoint,
|
||||
WorkspaceProjectAnchorEndpoint,
|
||||
ProjectMembersEndpoint,
|
||||
)
|
||||
|
||||
from .issue import (
|
||||
@@ -15,11 +14,3 @@ from .issue import (
|
||||
)
|
||||
|
||||
from .inbox import InboxIssuePublicViewSet
|
||||
|
||||
from .cycle import ProjectCyclesEndpoint
|
||||
|
||||
from .module import ProjectModulesEndpoint
|
||||
|
||||
from .state import ProjectStatesEndpoint
|
||||
|
||||
from .label import ProjectLabelsEndpoint
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
# Third Party imports
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
from .base import BaseAPIView
|
||||
from plane.db.models import (
|
||||
DeployBoard,
|
||||
Cycle,
|
||||
)
|
||||
|
||||
|
||||
class ProjectCyclesEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
|
||||
def get(self, request, anchor):
|
||||
deploy_board = DeployBoard.objects.filter(anchor=anchor).first()
|
||||
if not deploy_board:
|
||||
return Response(
|
||||
{"error": "Invalid anchor"},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
cycles = Cycle.objects.filter(
|
||||
workspace__slug=deploy_board.workspace.slug,
|
||||
project_id=deploy_board.project_id,
|
||||
).values("id", "name")
|
||||
|
||||
return Response(
|
||||
cycles,
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
@@ -1,51 +1,34 @@
|
||||
# Python imports
|
||||
import json
|
||||
|
||||
# Django imports
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db.models.functions import Coalesce, JSONObject
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.utils import timezone
|
||||
from django.db.models import (
|
||||
Exists,
|
||||
F,
|
||||
Func,
|
||||
OuterRef,
|
||||
Q,
|
||||
Prefetch,
|
||||
UUIDField,
|
||||
Case,
|
||||
When,
|
||||
JSONField,
|
||||
CharField,
|
||||
IntegerField,
|
||||
Value,
|
||||
OuterRef,
|
||||
Func
|
||||
Max,
|
||||
)
|
||||
|
||||
# Third Party imports
|
||||
from rest_framework.response import Response
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
|
||||
# Third Party imports
|
||||
from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
from .base import BaseAPIView, BaseViewSet
|
||||
|
||||
# fetch the space app grouper function separately
|
||||
from plane.space.utils.grouper import (
|
||||
issue_group_values,
|
||||
issue_on_results,
|
||||
issue_queryset_grouper,
|
||||
)
|
||||
|
||||
|
||||
from plane.utils.order_queryset import order_issue_queryset
|
||||
from plane.utils.paginator import (
|
||||
GroupedOffsetPaginator,
|
||||
SubGroupedOffsetPaginator,
|
||||
)
|
||||
from plane.app.serializers import (
|
||||
CommentReactionSerializer,
|
||||
IssueCommentSerializer,
|
||||
IssuePublicSerializer,
|
||||
IssueReactionSerializer,
|
||||
IssueVoteSerializer,
|
||||
)
|
||||
@@ -53,183 +36,21 @@ from plane.db.models import (
|
||||
Issue,
|
||||
IssueComment,
|
||||
IssueLink,
|
||||
IssueReaction,
|
||||
IssueAttachment,
|
||||
ProjectMember,
|
||||
IssueReaction,
|
||||
CommentReaction,
|
||||
DeployBoard,
|
||||
IssueVote,
|
||||
ProjectPublicMember,
|
||||
IssueAttachment,
|
||||
State,
|
||||
Label,
|
||||
)
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
|
||||
|
||||
class ProjectIssuesPublicEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
|
||||
def get(self, request, anchor):
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
order_by_param = request.GET.get("order_by", "-created_at")
|
||||
|
||||
deploy_board = DeployBoard.objects.filter(
|
||||
anchor=anchor, entity_name="project"
|
||||
).first()
|
||||
if not deploy_board:
|
||||
return Response(
|
||||
{"error": "Project is not published"},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
project_id = deploy_board.entity_identifier
|
||||
slug = deploy_board.workspace.slug
|
||||
|
||||
issue_queryset = (
|
||||
Issue.issue_objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_reactions",
|
||||
queryset=IssueReaction.objects.select_related("actor"),
|
||||
)
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"votes",
|
||||
queryset=IssueVote.objects.select_related("actor"),
|
||||
)
|
||||
)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=IssueAttachment.objects.filter(
|
||||
issue=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(
|
||||
parent=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
).distinct()
|
||||
|
||||
issue_queryset = issue_queryset.filter(**filters)
|
||||
|
||||
# Issue queryset
|
||||
issue_queryset, order_by_param = order_issue_queryset(
|
||||
issue_queryset=issue_queryset,
|
||||
order_by_param=order_by_param,
|
||||
)
|
||||
|
||||
# Group by
|
||||
group_by = request.GET.get("group_by", False)
|
||||
sub_group_by = request.GET.get("sub_group_by", False)
|
||||
|
||||
# issue queryset
|
||||
issue_queryset = issue_queryset_grouper(
|
||||
queryset=issue_queryset,
|
||||
group_by=group_by,
|
||||
sub_group_by=sub_group_by,
|
||||
)
|
||||
|
||||
if group_by:
|
||||
if sub_group_by:
|
||||
if group_by == sub_group_by:
|
||||
return Response(
|
||||
{
|
||||
"error": "Group by and sub group by cannot have same parameters"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
else:
|
||||
return self.paginate(
|
||||
request=request,
|
||||
order_by=order_by_param,
|
||||
queryset=issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by,
|
||||
issues=issues,
|
||||
sub_group_by=sub_group_by,
|
||||
),
|
||||
paginator_cls=SubGroupedOffsetPaginator,
|
||||
group_by_fields=issue_group_values(
|
||||
field=group_by,
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
filters=filters,
|
||||
),
|
||||
sub_group_by_fields=issue_group_values(
|
||||
field=sub_group_by,
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
filters=filters,
|
||||
),
|
||||
group_by_field_name=group_by,
|
||||
sub_group_by_field_name=sub_group_by,
|
||||
count_filter=Q(
|
||||
Q(issue_inbox__status=1)
|
||||
| Q(issue_inbox__status=-1)
|
||||
| Q(issue_inbox__status=2)
|
||||
| Q(issue_inbox__isnull=True),
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
else:
|
||||
# Group paginate
|
||||
return self.paginate(
|
||||
request=request,
|
||||
order_by=order_by_param,
|
||||
queryset=issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by,
|
||||
issues=issues,
|
||||
sub_group_by=sub_group_by,
|
||||
),
|
||||
paginator_cls=GroupedOffsetPaginator,
|
||||
group_by_fields=issue_group_values(
|
||||
field=group_by,
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
filters=filters,
|
||||
),
|
||||
group_by_field_name=group_by,
|
||||
count_filter=Q(
|
||||
Q(issue_inbox__status=1)
|
||||
| Q(issue_inbox__status=-1)
|
||||
| Q(issue_inbox__status=2)
|
||||
| Q(issue_inbox__isnull=True),
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
else:
|
||||
return self.paginate(
|
||||
order_by=order_by_param,
|
||||
request=request,
|
||||
queryset=issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by,
|
||||
issues=issues,
|
||||
sub_group_by=sub_group_by,
|
||||
),
|
||||
)
|
||||
# Module imports
|
||||
from .base import BaseAPIView, BaseViewSet
|
||||
|
||||
|
||||
class IssueCommentPublicViewSet(BaseViewSet):
|
||||
@@ -682,50 +503,67 @@ class IssueRetrievePublicEndpoint(BaseAPIView):
|
||||
]
|
||||
|
||||
def get(self, request, anchor, issue_id):
|
||||
deploy_board = DeployBoard.objects.get(anchor=anchor)
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
anchor=anchor, entity_name="project"
|
||||
)
|
||||
issue = Issue.objects.get(
|
||||
workspace_id=project_deploy_board.workspace_id,
|
||||
project_id=project_deploy_board.project_id,
|
||||
pk=issue_id,
|
||||
)
|
||||
serializer = IssuePublicSerializer(issue)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class ProjectIssuesPublicEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
|
||||
def get(self, request, anchor):
|
||||
deploy_board = DeployBoard.objects.filter(
|
||||
anchor=anchor, entity_name="project"
|
||||
).first()
|
||||
if not deploy_board:
|
||||
return Response(
|
||||
{"error": "Project is not published"},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
|
||||
# Custom ordering for priority and state
|
||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||
state_order = [
|
||||
"backlog",
|
||||
"unstarted",
|
||||
"started",
|
||||
"completed",
|
||||
"cancelled",
|
||||
]
|
||||
|
||||
order_by_param = request.GET.get("order_by", "-created_at")
|
||||
|
||||
project_id = deploy_board.entity_identifier
|
||||
slug = deploy_board.workspace.slug
|
||||
|
||||
issue_queryset = (
|
||||
Issue.issue_objects.filter(
|
||||
pk=issue_id,
|
||||
workspace__slug=deploy_board.workspace.slug,
|
||||
project_id=deploy_board.project_id,
|
||||
)
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(
|
||||
label_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=~Q(labels__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
assignee_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
module_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=~Q(issue_module__module_id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
Issue.issue_objects.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(
|
||||
parent=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.filter(project_id=project_id)
|
||||
.filter(workspace__slug=slug)
|
||||
.select_related("project", "workspace", "state", "parent")
|
||||
.prefetch_related("assignees", "labels")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_reactions",
|
||||
queryset=IssueReaction.objects.select_related(
|
||||
"issue", "actor"
|
||||
),
|
||||
queryset=IssueReaction.objects.select_related("actor"),
|
||||
)
|
||||
)
|
||||
.prefetch_related(
|
||||
@@ -734,91 +572,124 @@ class IssueRetrievePublicEndpoint(BaseAPIView):
|
||||
queryset=IssueVote.objects.select_related("actor"),
|
||||
)
|
||||
)
|
||||
.filter(**filters)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(module_id=F("issue_module__module_id"))
|
||||
.annotate(
|
||||
vote_items=ArrayAgg(
|
||||
Case(
|
||||
When(
|
||||
votes__isnull=False,
|
||||
then=JSONObject(
|
||||
vote=F("votes__vote"),
|
||||
actor_details=JSONObject(
|
||||
id=F("votes__actor__id"),
|
||||
first_name=F("votes__actor__first_name"),
|
||||
last_name=F("votes__actor__last_name"),
|
||||
avatar=F("votes__actor__avatar"),
|
||||
display_name=F(
|
||||
"votes__actor__display_name"
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
default=None,
|
||||
output_field=JSONField(),
|
||||
),
|
||||
filter=Case(
|
||||
When(votes__isnull=False, then=True),
|
||||
default=False,
|
||||
output_field=JSONField(),
|
||||
),
|
||||
distinct=True,
|
||||
),
|
||||
reaction_items=ArrayAgg(
|
||||
Case(
|
||||
When(
|
||||
issue_reactions__isnull=False,
|
||||
then=JSONObject(
|
||||
reaction=F("issue_reactions__reaction"),
|
||||
actor_details=JSONObject(
|
||||
id=F("issue_reactions__actor__id"),
|
||||
first_name=F(
|
||||
"issue_reactions__actor__first_name"
|
||||
),
|
||||
last_name=F(
|
||||
"issue_reactions__actor__last_name"
|
||||
),
|
||||
avatar=F("issue_reactions__actor__avatar"),
|
||||
display_name=F(
|
||||
"issue_reactions__actor__display_name"
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
default=None,
|
||||
output_field=JSONField(),
|
||||
),
|
||||
filter=Case(
|
||||
When(issue_reactions__isnull=False, then=True),
|
||||
default=False,
|
||||
output_field=JSONField(),
|
||||
),
|
||||
distinct=True,
|
||||
),
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.values(
|
||||
"id",
|
||||
"name",
|
||||
"state_id",
|
||||
"sort_order",
|
||||
"description",
|
||||
"description_html",
|
||||
"description_stripped",
|
||||
"description_binary",
|
||||
"module_ids",
|
||||
"label_ids",
|
||||
"assignee_ids",
|
||||
"estimate_point",
|
||||
"priority",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"sequence_id",
|
||||
"project_id",
|
||||
"parent_id",
|
||||
"cycle_id",
|
||||
"created_by",
|
||||
"state__group",
|
||||
"vote_items",
|
||||
"reaction_items",
|
||||
.annotate(
|
||||
attachment_count=IssueAttachment.objects.filter(
|
||||
issue=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
).first()
|
||||
)
|
||||
|
||||
return Response(issue_queryset, status=status.HTTP_200_OK)
|
||||
# Priority Ordering
|
||||
if order_by_param == "priority" or order_by_param == "-priority":
|
||||
priority_order = (
|
||||
priority_order
|
||||
if order_by_param == "priority"
|
||||
else priority_order[::-1]
|
||||
)
|
||||
issue_queryset = issue_queryset.annotate(
|
||||
priority_order=Case(
|
||||
*[
|
||||
When(priority=p, then=Value(i))
|
||||
for i, p in enumerate(priority_order)
|
||||
],
|
||||
output_field=CharField(),
|
||||
)
|
||||
).order_by("priority_order")
|
||||
|
||||
# State Ordering
|
||||
elif order_by_param in [
|
||||
"state__name",
|
||||
"state__group",
|
||||
"-state__name",
|
||||
"-state__group",
|
||||
]:
|
||||
state_order = (
|
||||
state_order
|
||||
if order_by_param in ["state__name", "state__group"]
|
||||
else state_order[::-1]
|
||||
)
|
||||
issue_queryset = issue_queryset.annotate(
|
||||
state_order=Case(
|
||||
*[
|
||||
When(state__group=state_group, then=Value(i))
|
||||
for i, state_group in enumerate(state_order)
|
||||
],
|
||||
default=Value(len(state_order)),
|
||||
output_field=CharField(),
|
||||
)
|
||||
).order_by("state_order")
|
||||
# assignee and label ordering
|
||||
elif order_by_param in [
|
||||
"labels__name",
|
||||
"-labels__name",
|
||||
"assignees__first_name",
|
||||
"-assignees__first_name",
|
||||
]:
|
||||
issue_queryset = issue_queryset.annotate(
|
||||
max_values=Max(
|
||||
order_by_param[1::]
|
||||
if order_by_param.startswith("-")
|
||||
else order_by_param
|
||||
)
|
||||
).order_by(
|
||||
"-max_values"
|
||||
if order_by_param.startswith("-")
|
||||
else "max_values"
|
||||
)
|
||||
else:
|
||||
issue_queryset = issue_queryset.order_by(order_by_param)
|
||||
|
||||
issues = IssuePublicSerializer(issue_queryset, many=True).data
|
||||
|
||||
state_group_order = [
|
||||
"backlog",
|
||||
"unstarted",
|
||||
"started",
|
||||
"completed",
|
||||
"cancelled",
|
||||
]
|
||||
|
||||
states = (
|
||||
State.objects.filter(
|
||||
~Q(name="Triage"),
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
.annotate(
|
||||
custom_order=Case(
|
||||
*[
|
||||
When(group=value, then=Value(index))
|
||||
for index, value in enumerate(state_group_order)
|
||||
],
|
||||
default=Value(len(state_group_order)),
|
||||
output_field=IntegerField(),
|
||||
),
|
||||
)
|
||||
.values("name", "group", "color", "id")
|
||||
.order_by("custom_order", "sequence")
|
||||
)
|
||||
|
||||
labels = Label.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).values("id", "name", "color", "parent")
|
||||
|
||||
return Response(
|
||||
{
|
||||
"issues": issues,
|
||||
"states": states,
|
||||
"labels": labels,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
# Third Party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import AllowAny
|
||||
|
||||
# Module imports
|
||||
from .base import BaseAPIView
|
||||
from plane.db.models import (
|
||||
DeployBoard,
|
||||
Label,
|
||||
)
|
||||
|
||||
|
||||
class ProjectLabelsEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
|
||||
def get(self, request, anchor):
|
||||
deploy_board = DeployBoard.objects.filter(anchor=anchor).first()
|
||||
if not deploy_board:
|
||||
return Response(
|
||||
{"error": "Invalid anchor"},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
labels = Label.objects.filter(
|
||||
workspace__slug=deploy_board.workspace.slug,
|
||||
project_id=deploy_board.project_id,
|
||||
).values("id", "name", "color", "parent")
|
||||
|
||||
return Response(
|
||||
labels,
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
@@ -1,35 +0,0 @@
|
||||
# Third Party imports
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
from .base import BaseAPIView
|
||||
from plane.db.models import (
|
||||
DeployBoard,
|
||||
Module,
|
||||
)
|
||||
|
||||
|
||||
class ProjectModulesEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
|
||||
def get(self, request, anchor):
|
||||
deploy_board = DeployBoard.objects.filter(anchor=anchor).first()
|
||||
if not deploy_board:
|
||||
return Response(
|
||||
{"error": "Invalid anchor"},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
modules = Module.objects.filter(
|
||||
workspace__slug=deploy_board.workspace.slug,
|
||||
project_id=deploy_board.project_id,
|
||||
).values("id", "name")
|
||||
|
||||
return Response(
|
||||
modules,
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
@@ -12,7 +12,10 @@ from rest_framework.permissions import AllowAny
|
||||
# Module imports
|
||||
from .base import BaseAPIView
|
||||
from plane.app.serializers import DeployBoardSerializer
|
||||
from plane.db.models import Project, DeployBoard, ProjectMember
|
||||
from plane.db.models import (
|
||||
Project,
|
||||
DeployBoard,
|
||||
)
|
||||
|
||||
|
||||
class ProjectDeployBoardPublicSettingsEndpoint(BaseAPIView):
|
||||
@@ -73,27 +76,3 @@ class WorkspaceProjectAnchorEndpoint(BaseAPIView):
|
||||
)
|
||||
serializer = DeployBoardSerializer(project_deploy_board)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class ProjectMembersEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
|
||||
def get(self, request, anchor):
|
||||
deploy_board = DeployBoard.objects.filter(anchor=anchor).first()
|
||||
|
||||
members = ProjectMember.objects.filter(
|
||||
project=deploy_board.project,
|
||||
workspace=deploy_board.workspace,
|
||||
is_active=True,
|
||||
).values(
|
||||
"id",
|
||||
"member",
|
||||
"member__first_name",
|
||||
"member__last_name",
|
||||
"member__display_name",
|
||||
"project",
|
||||
"workspace",
|
||||
)
|
||||
return Response(members, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
# Django imports
|
||||
from django.db.models import Q
|
||||
|
||||
# Third Party imports
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
from .base import BaseAPIView
|
||||
from plane.db.models import (
|
||||
DeployBoard,
|
||||
State,
|
||||
)
|
||||
|
||||
|
||||
class ProjectStatesEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
|
||||
def get(self, request, anchor):
|
||||
deploy_board = DeployBoard.objects.filter(anchor=anchor).first()
|
||||
if not deploy_board:
|
||||
return Response(
|
||||
{"error": "Invalid anchor"},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
states = (
|
||||
State.objects.filter(
|
||||
~Q(name="Triage"),
|
||||
workspace__slug=deploy_board.workspace.slug,
|
||||
project_id=deploy_board.project_id,
|
||||
)
|
||||
.values("name", "group", "color", "id")
|
||||
)
|
||||
|
||||
return Response(
|
||||
states,
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
@@ -12,7 +12,7 @@ from django.db.models import (
|
||||
Sum,
|
||||
Value,
|
||||
When,
|
||||
FloatField,
|
||||
IntegerField,
|
||||
)
|
||||
from django.db.models.functions import (
|
||||
Coalesce,
|
||||
@@ -98,7 +98,7 @@ def build_graph_plot(queryset, x_axis, y_axis, segment=None):
|
||||
# Estimate
|
||||
else:
|
||||
queryset = queryset.annotate(
|
||||
estimate=Sum(Cast("estimate_point__value", FloatField()))
|
||||
estimate=Sum(Cast("estimate_point__value", IntegerField()))
|
||||
).order_by(x_axis)
|
||||
queryset = (
|
||||
queryset.annotate(segment=F(segment)) if segment else queryset
|
||||
|
||||
@@ -23,42 +23,42 @@ def filter_valid_uuids(uuid_list):
|
||||
|
||||
# Get the 2_weeks, 3_months
|
||||
def string_date_filter(
|
||||
issue_filter, duration, subsequent, term, date_filter, offset
|
||||
filter, duration, subsequent, term, date_filter, offset
|
||||
):
|
||||
now = timezone.now().date()
|
||||
if term == "months":
|
||||
if subsequent == "after":
|
||||
if offset == "fromnow":
|
||||
issue_filter[f"{date_filter}__gte"] = now + timedelta(
|
||||
filter[f"{date_filter}__gte"] = now + timedelta(
|
||||
days=duration * 30
|
||||
)
|
||||
else:
|
||||
issue_filter[f"{date_filter}__gte"] = now - timedelta(
|
||||
filter[f"{date_filter}__gte"] = now - timedelta(
|
||||
days=duration * 30
|
||||
)
|
||||
else:
|
||||
if offset == "fromnow":
|
||||
issue_filter[f"{date_filter}__lte"] = now + timedelta(
|
||||
filter[f"{date_filter}__lte"] = now + timedelta(
|
||||
days=duration * 30
|
||||
)
|
||||
else:
|
||||
issue_filter[f"{date_filter}__lte"] = now - timedelta(
|
||||
filter[f"{date_filter}__lte"] = now - timedelta(
|
||||
days=duration * 30
|
||||
)
|
||||
if term == "weeks":
|
||||
if subsequent == "after":
|
||||
if offset == "fromnow":
|
||||
issue_filter[f"{date_filter}__gte"] = now + timedelta(weeks=duration)
|
||||
filter[f"{date_filter}__gte"] = now + timedelta(weeks=duration)
|
||||
else:
|
||||
issue_filter[f"{date_filter}__gte"] = now - timedelta(weeks=duration)
|
||||
filter[f"{date_filter}__gte"] = now - timedelta(weeks=duration)
|
||||
else:
|
||||
if offset == "fromnow":
|
||||
issue_filter[f"{date_filter}__lte"] = now + timedelta(weeks=duration)
|
||||
filter[f"{date_filter}__lte"] = now + timedelta(weeks=duration)
|
||||
else:
|
||||
issue_filter[f"{date_filter}__lte"] = now - timedelta(weeks=duration)
|
||||
filter[f"{date_filter}__lte"] = now - timedelta(weeks=duration)
|
||||
|
||||
|
||||
def date_filter(issue_filter, date_term, queries):
|
||||
def date_filter(filter, date_term, queries):
|
||||
"""
|
||||
Handle all date filters
|
||||
"""
|
||||
@@ -71,7 +71,7 @@ def date_filter(issue_filter, date_term, queries):
|
||||
if len(date_query) == 3:
|
||||
digit, term = date_query[0].split("_")
|
||||
string_date_filter(
|
||||
issue_filter=issue_filter,
|
||||
filter=filter,
|
||||
duration=int(digit),
|
||||
subsequent=date_query[1],
|
||||
term=term,
|
||||
@@ -80,32 +80,32 @@ def date_filter(issue_filter, date_term, queries):
|
||||
)
|
||||
else:
|
||||
if "after" in date_query:
|
||||
issue_filter[f"{date_term}__gte"] = date_query[0]
|
||||
filter[f"{date_term}__gte"] = date_query[0]
|
||||
else:
|
||||
issue_filter[f"{date_term}__lte"] = date_query[0]
|
||||
filter[f"{date_term}__lte"] = date_query[0]
|
||||
else:
|
||||
issue_filter[f"{date_term}__contains"] = date_query[0]
|
||||
filter[f"{date_term}__contains"] = date_query[0]
|
||||
|
||||
|
||||
def filter_state(params, issue_filter, method, prefix=""):
|
||||
def filter_state(params, filter, method, prefix=""):
|
||||
if method == "GET":
|
||||
states = [
|
||||
item for item in params.get("state").split(",") if item != "null"
|
||||
]
|
||||
states = filter_valid_uuids(states)
|
||||
if len(states) and "" not in states:
|
||||
issue_filter[f"{prefix}state__in"] = states
|
||||
filter[f"{prefix}state__in"] = states
|
||||
else:
|
||||
if (
|
||||
params.get("state", None)
|
||||
and len(params.get("state"))
|
||||
and params.get("state") != "null"
|
||||
):
|
||||
issue_filter[f"{prefix}state__in"] = params.get("state")
|
||||
return issue_filter
|
||||
filter[f"{prefix}state__in"] = params.get("state")
|
||||
return filter
|
||||
|
||||
|
||||
def filter_state_group(params, issue_filter, method, prefix=""):
|
||||
def filter_state_group(params, filter, method, prefix=""):
|
||||
if method == "GET":
|
||||
state_group = [
|
||||
item
|
||||
@@ -113,18 +113,18 @@ def filter_state_group(params, issue_filter, method, prefix=""):
|
||||
if item != "null"
|
||||
]
|
||||
if len(state_group) and "" not in state_group:
|
||||
issue_filter[f"{prefix}state__group__in"] = state_group
|
||||
filter[f"{prefix}state__group__in"] = state_group
|
||||
else:
|
||||
if (
|
||||
params.get("state_group", None)
|
||||
and len(params.get("state_group"))
|
||||
and params.get("state_group") != "null"
|
||||
):
|
||||
issue_filter[f"{prefix}state__group__in"] = params.get("state_group")
|
||||
return issue_filter
|
||||
filter[f"{prefix}state__group__in"] = params.get("state_group")
|
||||
return filter
|
||||
|
||||
|
||||
def filter_estimate_point(params, issue_filter, method, prefix=""):
|
||||
def filter_estimate_point(params, filter, method, prefix=""):
|
||||
if method == "GET":
|
||||
estimate_points = [
|
||||
item
|
||||
@@ -132,20 +132,20 @@ def filter_estimate_point(params, issue_filter, method, prefix=""):
|
||||
if item != "null"
|
||||
]
|
||||
if len(estimate_points) and "" not in estimate_points:
|
||||
issue_filter[f"{prefix}estimate_point__in"] = estimate_points
|
||||
filter[f"{prefix}estimate_point__in"] = estimate_points
|
||||
else:
|
||||
if (
|
||||
params.get("estimate_point", None)
|
||||
and len(params.get("estimate_point"))
|
||||
and params.get("estimate_point") != "null"
|
||||
):
|
||||
issue_filter[f"{prefix}estimate_point__in"] = params.get(
|
||||
filter[f"{prefix}estimate_point__in"] = params.get(
|
||||
"estimate_point"
|
||||
)
|
||||
return issue_filter
|
||||
return filter
|
||||
|
||||
|
||||
def filter_priority(params, issue_filter, method, prefix=""):
|
||||
def filter_priority(params, filter, method, prefix=""):
|
||||
if method == "GET":
|
||||
priorities = [
|
||||
item
|
||||
@@ -153,58 +153,51 @@ def filter_priority(params, issue_filter, method, prefix=""):
|
||||
if item != "null"
|
||||
]
|
||||
if len(priorities) and "" not in priorities:
|
||||
issue_filter[f"{prefix}priority__in"] = priorities
|
||||
else:
|
||||
if (
|
||||
params.get("priority", None)
|
||||
and len(params.get("priority"))
|
||||
and params.get("priority") != "null"
|
||||
):
|
||||
issue_filter[f"{prefix}priority__in"] = params.get("priority")
|
||||
return issue_filter
|
||||
filter[f"{prefix}priority__in"] = priorities
|
||||
return filter
|
||||
|
||||
|
||||
def filter_parent(params, issue_filter, method, prefix=""):
|
||||
def filter_parent(params, filter, method, prefix=""):
|
||||
if method == "GET":
|
||||
parents = [
|
||||
item for item in params.get("parent").split(",") if item != "null"
|
||||
]
|
||||
if "None" in parents:
|
||||
issue_filter[f"{prefix}parent__isnull"] = True
|
||||
filter[f"{prefix}parent__isnull"] = True
|
||||
parents = filter_valid_uuids(parents)
|
||||
if len(parents) and "" not in parents:
|
||||
issue_filter[f"{prefix}parent__in"] = parents
|
||||
filter[f"{prefix}parent__in"] = parents
|
||||
else:
|
||||
if (
|
||||
params.get("parent", None)
|
||||
and len(params.get("parent"))
|
||||
and params.get("parent") != "null"
|
||||
):
|
||||
issue_filter[f"{prefix}parent__in"] = params.get("parent")
|
||||
return issue_filter
|
||||
filter[f"{prefix}parent__in"] = params.get("parent")
|
||||
return filter
|
||||
|
||||
|
||||
def filter_labels(params, issue_filter, method, prefix=""):
|
||||
def filter_labels(params, filter, method, prefix=""):
|
||||
if method == "GET":
|
||||
labels = [
|
||||
item for item in params.get("labels").split(",") if item != "null"
|
||||
]
|
||||
if "None" in labels:
|
||||
issue_filter[f"{prefix}labels__isnull"] = True
|
||||
filter[f"{prefix}labels__isnull"] = True
|
||||
labels = filter_valid_uuids(labels)
|
||||
if len(labels) and "" not in labels:
|
||||
issue_filter[f"{prefix}labels__in"] = labels
|
||||
filter[f"{prefix}labels__in"] = labels
|
||||
else:
|
||||
if (
|
||||
params.get("labels", None)
|
||||
and len(params.get("labels"))
|
||||
and params.get("labels") != "null"
|
||||
):
|
||||
issue_filter[f"{prefix}labels__in"] = params.get("labels")
|
||||
return issue_filter
|
||||
filter[f"{prefix}labels__in"] = params.get("labels")
|
||||
return filter
|
||||
|
||||
|
||||
def filter_assignees(params, issue_filter, method, prefix=""):
|
||||
def filter_assignees(params, filter, method, prefix=""):
|
||||
if method == "GET":
|
||||
assignees = [
|
||||
item
|
||||
@@ -212,21 +205,21 @@ def filter_assignees(params, issue_filter, method, prefix=""):
|
||||
if item != "null"
|
||||
]
|
||||
if "None" in assignees:
|
||||
issue_filter[f"{prefix}assignees__isnull"] = True
|
||||
filter[f"{prefix}assignees__isnull"] = True
|
||||
assignees = filter_valid_uuids(assignees)
|
||||
if len(assignees) and "" not in assignees:
|
||||
issue_filter[f"{prefix}assignees__in"] = assignees
|
||||
filter[f"{prefix}assignees__in"] = assignees
|
||||
else:
|
||||
if (
|
||||
params.get("assignees", None)
|
||||
and len(params.get("assignees"))
|
||||
and params.get("assignees") != "null"
|
||||
):
|
||||
issue_filter[f"{prefix}assignees__in"] = params.get("assignees")
|
||||
return issue_filter
|
||||
filter[f"{prefix}assignees__in"] = params.get("assignees")
|
||||
return filter
|
||||
|
||||
|
||||
def filter_mentions(params, issue_filter, method, prefix=""):
|
||||
def filter_mentions(params, filter, method, prefix=""):
|
||||
if method == "GET":
|
||||
mentions = [
|
||||
item
|
||||
@@ -235,20 +228,20 @@ def filter_mentions(params, issue_filter, method, prefix=""):
|
||||
]
|
||||
mentions = filter_valid_uuids(mentions)
|
||||
if len(mentions) and "" not in mentions:
|
||||
issue_filter[f"{prefix}issue_mention__mention__id__in"] = mentions
|
||||
filter[f"{prefix}issue_mention__mention__id__in"] = mentions
|
||||
else:
|
||||
if (
|
||||
params.get("mentions", None)
|
||||
and len(params.get("mentions"))
|
||||
and params.get("mentions") != "null"
|
||||
):
|
||||
issue_filter[f"{prefix}issue_mention__mention__id__in"] = params.get(
|
||||
filter[f"{prefix}issue_mention__mention__id__in"] = params.get(
|
||||
"mentions"
|
||||
)
|
||||
return issue_filter
|
||||
return filter
|
||||
|
||||
|
||||
def filter_created_by(params, issue_filter, method, prefix=""):
|
||||
def filter_created_by(params, filter, method, prefix=""):
|
||||
if method == "GET":
|
||||
created_bys = [
|
||||
item
|
||||
@@ -256,100 +249,100 @@ def filter_created_by(params, issue_filter, method, prefix=""):
|
||||
if item != "null"
|
||||
]
|
||||
if "None" in created_bys:
|
||||
issue_filter[f"{prefix}created_by__isnull"] = True
|
||||
filter[f"{prefix}created_by__isnull"] = True
|
||||
created_bys = filter_valid_uuids(created_bys)
|
||||
if len(created_bys) and "" not in created_bys:
|
||||
issue_filter[f"{prefix}created_by__in"] = created_bys
|
||||
filter[f"{prefix}created_by__in"] = created_bys
|
||||
else:
|
||||
if (
|
||||
params.get("created_by", None)
|
||||
and len(params.get("created_by"))
|
||||
and params.get("created_by") != "null"
|
||||
):
|
||||
issue_filter[f"{prefix}created_by__in"] = params.get("created_by")
|
||||
return issue_filter
|
||||
filter[f"{prefix}created_by__in"] = params.get("created_by")
|
||||
return filter
|
||||
|
||||
|
||||
def filter_name(params, issue_filter, method, prefix=""):
|
||||
def filter_name(params, filter, method, prefix=""):
|
||||
if params.get("name", "") != "":
|
||||
issue_filter[f"{prefix}name__icontains"] = params.get("name")
|
||||
return issue_filter
|
||||
filter[f"{prefix}name__icontains"] = params.get("name")
|
||||
return filter
|
||||
|
||||
|
||||
def filter_created_at(params, issue_filter, method, prefix=""):
|
||||
def filter_created_at(params, filter, method, prefix=""):
|
||||
if method == "GET":
|
||||
created_ats = params.get("created_at").split(",")
|
||||
if len(created_ats) and "" not in created_ats:
|
||||
date_filter(
|
||||
issue_filter=issue_filter,
|
||||
filter=filter,
|
||||
date_term=f"{prefix}created_at__date",
|
||||
queries=created_ats,
|
||||
)
|
||||
else:
|
||||
if params.get("created_at", None) and len(params.get("created_at")):
|
||||
date_filter(
|
||||
issue_filter=issue_filter,
|
||||
filter=filter,
|
||||
date_term=f"{prefix}created_at__date",
|
||||
queries=params.get("created_at", []),
|
||||
)
|
||||
return issue_filter
|
||||
return filter
|
||||
|
||||
|
||||
def filter_updated_at(params, issue_filter, method, prefix=""):
|
||||
def filter_updated_at(params, filter, method, prefix=""):
|
||||
if method == "GET":
|
||||
updated_ats = params.get("updated_at").split(",")
|
||||
if len(updated_ats) and "" not in updated_ats:
|
||||
date_filter(
|
||||
issue_filter=issue_filter,
|
||||
filter=filter,
|
||||
date_term=f"{prefix}created_at__date",
|
||||
queries=updated_ats,
|
||||
)
|
||||
else:
|
||||
if params.get("updated_at", None) and len(params.get("updated_at")):
|
||||
date_filter(
|
||||
issue_filter=issue_filter,
|
||||
filter=filter,
|
||||
date_term=f"{prefix}created_at__date",
|
||||
queries=params.get("updated_at", []),
|
||||
)
|
||||
return issue_filter
|
||||
return filter
|
||||
|
||||
|
||||
def filter_start_date(params, issue_filter, method, prefix=""):
|
||||
def filter_start_date(params, filter, method, prefix=""):
|
||||
if method == "GET":
|
||||
start_dates = params.get("start_date").split(",")
|
||||
if len(start_dates) and "" not in start_dates:
|
||||
date_filter(
|
||||
issue_filter=issue_filter,
|
||||
filter=filter,
|
||||
date_term=f"{prefix}start_date",
|
||||
queries=start_dates,
|
||||
)
|
||||
else:
|
||||
if params.get("start_date", None) and len(params.get("start_date")):
|
||||
issue_filter[f"{prefix}start_date"] = params.get("start_date")
|
||||
return issue_filter
|
||||
filter[f"{prefix}start_date"] = params.get("start_date")
|
||||
return filter
|
||||
|
||||
|
||||
def filter_target_date(params, issue_filter, method, prefix=""):
|
||||
def filter_target_date(params, filter, method, prefix=""):
|
||||
if method == "GET":
|
||||
target_dates = params.get("target_date").split(",")
|
||||
if len(target_dates) and "" not in target_dates:
|
||||
date_filter(
|
||||
issue_filter=issue_filter,
|
||||
filter=filter,
|
||||
date_term=f"{prefix}target_date",
|
||||
queries=target_dates,
|
||||
)
|
||||
else:
|
||||
if params.get("target_date", None) and len(params.get("target_date")):
|
||||
issue_filter[f"{prefix}target_date"] = params.get("target_date")
|
||||
return issue_filter
|
||||
filter[f"{prefix}target_date"] = params.get("target_date")
|
||||
return filter
|
||||
|
||||
|
||||
def filter_completed_at(params, issue_filter, method, prefix=""):
|
||||
def filter_completed_at(params, filter, method, prefix=""):
|
||||
if method == "GET":
|
||||
completed_ats = params.get("completed_at").split(",")
|
||||
if len(completed_ats) and "" not in completed_ats:
|
||||
date_filter(
|
||||
issue_filter=issue_filter,
|
||||
filter=filter,
|
||||
date_term=f"{prefix}completed_at__date",
|
||||
queries=completed_ats,
|
||||
)
|
||||
@@ -358,14 +351,14 @@ def filter_completed_at(params, issue_filter, method, prefix=""):
|
||||
params.get("completed_at")
|
||||
):
|
||||
date_filter(
|
||||
issue_filter=issue_filter,
|
||||
filter=filter,
|
||||
date_term=f"{prefix}completed_at__date",
|
||||
queries=params.get("completed_at", []),
|
||||
)
|
||||
return issue_filter
|
||||
return filter
|
||||
|
||||
|
||||
def filter_issue_state_type(params, issue_filter, method, prefix=""):
|
||||
def filter_issue_state_type(params, filter, method, prefix=""):
|
||||
type = params.get("type", "all")
|
||||
group = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
||||
if type == "backlog":
|
||||
@@ -373,71 +366,71 @@ def filter_issue_state_type(params, issue_filter, method, prefix=""):
|
||||
if type == "active":
|
||||
group = ["unstarted", "started"]
|
||||
|
||||
issue_filter[f"{prefix}state__group__in"] = group
|
||||
return issue_filter
|
||||
filter[f"{prefix}state__group__in"] = group
|
||||
return filter
|
||||
|
||||
|
||||
def filter_project(params, issue_filter, method, prefix=""):
|
||||
def filter_project(params, filter, method, prefix=""):
|
||||
if method == "GET":
|
||||
projects = [
|
||||
item for item in params.get("project").split(",") if item != "null"
|
||||
]
|
||||
projects = filter_valid_uuids(projects)
|
||||
if len(projects) and "" not in projects:
|
||||
issue_filter[f"{prefix}project__in"] = projects
|
||||
filter[f"{prefix}project__in"] = projects
|
||||
else:
|
||||
if (
|
||||
params.get("project", None)
|
||||
and len(params.get("project"))
|
||||
and params.get("project") != "null"
|
||||
):
|
||||
issue_filter[f"{prefix}project__in"] = params.get("project")
|
||||
return issue_filter
|
||||
filter[f"{prefix}project__in"] = params.get("project")
|
||||
return filter
|
||||
|
||||
|
||||
def filter_cycle(params, issue_filter, method, prefix=""):
|
||||
def filter_cycle(params, filter, method, prefix=""):
|
||||
if method == "GET":
|
||||
cycles = [
|
||||
item for item in params.get("cycle").split(",") if item != "null"
|
||||
]
|
||||
if "None" in cycles:
|
||||
issue_filter[f"{prefix}issue_cycle__cycle_id__isnull"] = True
|
||||
filter[f"{prefix}issue_cycle__cycle_id__isnull"] = True
|
||||
cycles = filter_valid_uuids(cycles)
|
||||
if len(cycles) and "" not in cycles:
|
||||
issue_filter[f"{prefix}issue_cycle__cycle_id__in"] = cycles
|
||||
filter[f"{prefix}issue_cycle__cycle_id__in"] = cycles
|
||||
else:
|
||||
if (
|
||||
params.get("cycle", None)
|
||||
and len(params.get("cycle"))
|
||||
and params.get("cycle") != "null"
|
||||
):
|
||||
issue_filter[f"{prefix}issue_cycle__cycle_id__in"] = params.get("cycle")
|
||||
return issue_filter
|
||||
filter[f"{prefix}issue_cycle__cycle_id__in"] = params.get("cycle")
|
||||
return filter
|
||||
|
||||
|
||||
def filter_module(params, issue_filter, method, prefix=""):
|
||||
def filter_module(params, filter, method, prefix=""):
|
||||
if method == "GET":
|
||||
modules = [
|
||||
item for item in params.get("module").split(",") if item != "null"
|
||||
]
|
||||
if "None" in modules:
|
||||
issue_filter[f"{prefix}issue_module__module_id__isnull"] = True
|
||||
filter[f"{prefix}issue_module__module_id__isnull"] = True
|
||||
modules = filter_valid_uuids(modules)
|
||||
if len(modules) and "" not in modules:
|
||||
issue_filter[f"{prefix}issue_module__module_id__in"] = modules
|
||||
filter[f"{prefix}issue_module__module_id__in"] = modules
|
||||
else:
|
||||
if (
|
||||
params.get("module", None)
|
||||
and len(params.get("module"))
|
||||
and params.get("module") != "null"
|
||||
):
|
||||
issue_filter[f"{prefix}issue_module__module_id__in"] = params.get(
|
||||
filter[f"{prefix}issue_module__module_id__in"] = params.get(
|
||||
"module"
|
||||
)
|
||||
return issue_filter
|
||||
return filter
|
||||
|
||||
|
||||
def filter_inbox_status(params, issue_filter, method, prefix=""):
|
||||
def filter_inbox_status(params, filter, method, prefix=""):
|
||||
if method == "GET":
|
||||
status = [
|
||||
item
|
||||
@@ -445,32 +438,32 @@ def filter_inbox_status(params, issue_filter, method, prefix=""):
|
||||
if item != "null"
|
||||
]
|
||||
if len(status) and "" not in status:
|
||||
issue_filter[f"{prefix}issue_inbox__status__in"] = status
|
||||
filter[f"{prefix}issue_inbox__status__in"] = status
|
||||
else:
|
||||
if (
|
||||
params.get("inbox_status", None)
|
||||
and len(params.get("inbox_status"))
|
||||
and params.get("inbox_status") != "null"
|
||||
):
|
||||
issue_filter[f"{prefix}issue_inbox__status__in"] = params.get(
|
||||
filter[f"{prefix}issue_inbox__status__in"] = params.get(
|
||||
"inbox_status"
|
||||
)
|
||||
return issue_filter
|
||||
return filter
|
||||
|
||||
|
||||
def filter_sub_issue_toggle(params, issue_filter, method, prefix=""):
|
||||
def filter_sub_issue_toggle(params, filter, method, prefix=""):
|
||||
if method == "GET":
|
||||
sub_issue = params.get("sub_issue", "false")
|
||||
if sub_issue == "false":
|
||||
issue_filter[f"{prefix}parent__isnull"] = True
|
||||
filter[f"{prefix}parent__isnull"] = True
|
||||
else:
|
||||
sub_issue = params.get("sub_issue", "false")
|
||||
if sub_issue == "false":
|
||||
issue_filter[f"{prefix}parent__isnull"] = True
|
||||
return issue_filter
|
||||
filter[f"{prefix}parent__isnull"] = True
|
||||
return filter
|
||||
|
||||
|
||||
def filter_subscribed_issues(params, issue_filter, method, prefix=""):
|
||||
def filter_subscribed_issues(params, filter, method, prefix=""):
|
||||
if method == "GET":
|
||||
subscribers = [
|
||||
item
|
||||
@@ -479,7 +472,7 @@ def filter_subscribed_issues(params, issue_filter, method, prefix=""):
|
||||
]
|
||||
subscribers = filter_valid_uuids(subscribers)
|
||||
if len(subscribers) and "" not in subscribers:
|
||||
issue_filter[f"{prefix}issue_subscribers__subscriber_id__in"] = (
|
||||
filter[f"{prefix}issue_subscribers__subscriber_id__in"] = (
|
||||
subscribers
|
||||
)
|
||||
else:
|
||||
@@ -488,44 +481,22 @@ def filter_subscribed_issues(params, issue_filter, method, prefix=""):
|
||||
and len(params.get("subscriber"))
|
||||
and params.get("subscriber") != "null"
|
||||
):
|
||||
issue_filter[f"{prefix}issue_subscribers__subscriber_id__in"] = (
|
||||
filter[f"{prefix}issue_subscribers__subscriber_id__in"] = (
|
||||
params.get("subscriber")
|
||||
)
|
||||
return issue_filter
|
||||
return filter
|
||||
|
||||
|
||||
def filter_start_target_date_issues(params, issue_filter, method, prefix=""):
|
||||
def filter_start_target_date_issues(params, filter, method, prefix=""):
|
||||
start_target_date = params.get("start_target_date", "false")
|
||||
if start_target_date == "true":
|
||||
issue_filter[f"{prefix}target_date__isnull"] = False
|
||||
issue_filter[f"{prefix}start_date__isnull"] = False
|
||||
return issue_filter
|
||||
|
||||
|
||||
def filter_logged_by(params, issue_filter, method, prefix=""):
|
||||
if method == "GET":
|
||||
logged_bys = [
|
||||
item
|
||||
for item in params.get("logged_by").split(",")
|
||||
if item != "null"
|
||||
]
|
||||
if "None" in logged_bys:
|
||||
issue_filter[f"{prefix}logged_by__isnull"] = True
|
||||
logged_bys = filter_valid_uuids(logged_bys)
|
||||
if len(logged_bys) and "" not in logged_bys:
|
||||
issue_filter[f"{prefix}logged_by__in"] = logged_bys
|
||||
else:
|
||||
if (
|
||||
params.get("logged_by", None)
|
||||
and len(params.get("logged_by"))
|
||||
and params.get("logged_by") != "null"
|
||||
):
|
||||
issue_filter[f"{prefix}logged_by__in"] = params.get("logged_by")
|
||||
return issue_filter
|
||||
filter[f"{prefix}target_date__isnull"] = False
|
||||
filter[f"{prefix}start_date__isnull"] = False
|
||||
return filter
|
||||
|
||||
|
||||
def issue_filters(query_params, method, prefix=""):
|
||||
issue_filter = {}
|
||||
filter = {}
|
||||
|
||||
ISSUE_FILTER = {
|
||||
"state": filter_state,
|
||||
@@ -537,7 +508,6 @@ def issue_filters(query_params, method, prefix=""):
|
||||
"assignees": filter_assignees,
|
||||
"mentions": filter_mentions,
|
||||
"created_by": filter_created_by,
|
||||
"logged_by": filter_logged_by,
|
||||
"name": filter_name,
|
||||
"created_at": filter_created_at,
|
||||
"updated_at": filter_updated_at,
|
||||
@@ -557,5 +527,5 @@ def issue_filters(query_params, method, prefix=""):
|
||||
for key, value in ISSUE_FILTER.items():
|
||||
if key in query_params:
|
||||
func = value
|
||||
func(query_params, issue_filter, method, prefix)
|
||||
return issue_filter
|
||||
func(query_params, filter, method, prefix)
|
||||
return filter
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# base requirements
|
||||
|
||||
# django
|
||||
Django==4.2.14
|
||||
Django==4.2.11
|
||||
# rest framework
|
||||
djangorestframework==3.15.2
|
||||
# postgres
|
||||
@@ -26,7 +26,7 @@ django-filter==24.2
|
||||
# json model
|
||||
jsonmodels==2.7.0
|
||||
# sentry
|
||||
sentry-sdk==2.8.0
|
||||
sentry-sdk==2.0.1
|
||||
# storage
|
||||
django-storages==1.14.2
|
||||
# user management
|
||||
|
||||
+22
-58
@@ -10,7 +10,7 @@ Let's get started!
|
||||
|
||||
<details>
|
||||
<summary>Option 1 - Using Cloud Server</summary>
|
||||
<p>Best way to start is to create EC2 machine on AWS. It must have minimum of 2vCPU and 4GB RAM.</p>
|
||||
<p>Best way to start is to create EC2 maching on AWS. It must of minimum t3.medium/t3a/medium</p>
|
||||
<p>Run the below command to install docker engine.</p>
|
||||
|
||||
`curl -fsSL https://get.docker.com | sh -`
|
||||
@@ -67,6 +67,23 @@ curl -fsSL -o setup.sh https://raw.githubusercontent.com/makeplane/plane/master/
|
||||
chmod +x setup.sh
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Downloading Preview Release</summary>
|
||||
|
||||
```
|
||||
mkdir plane-selfhost
|
||||
|
||||
cd plane-selfhost
|
||||
|
||||
export RELEASE=preview
|
||||
|
||||
curl -fsSL https://raw.githubusercontent.com/makeplane/plane/$BRANCH/deploy/selfhost/install.sh | sed 's@BRANCH=master@BRANCH='"$RELEASE"'@' > setup.sh
|
||||
|
||||
chmod +x setup.sh
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### Proceed with setup
|
||||
@@ -97,7 +114,7 @@ This will create a create a folder `plane-app` or `plane-app-preview` (in case o
|
||||
- `docker-compose.yaml`
|
||||
- `plane.env`
|
||||
|
||||
Again the `options [1-8]` will be popped up and this time hit `8` to exit.
|
||||
Again the `options [1-7]` will be popped up and this time hit `7` to exit.
|
||||
|
||||
---
|
||||
|
||||
@@ -219,7 +236,7 @@ Select a Action you want to perform:
|
||||
Action [2]: 5
|
||||
```
|
||||
|
||||
By choosing this, it will stop the services and then will download the latest `docker-compose.yaml` and `plane.env`.
|
||||
By choosing this, it will stop the services and then will download the latest `docker-compose.yaml` and `variables-upgrade.env`. Here system will not replace `.env` with the new one.
|
||||
|
||||
You must expect the below message
|
||||
|
||||
@@ -227,7 +244,7 @@ You must expect the below message
|
||||
|
||||
Once done, choose `8` to exit from prompt.
|
||||
|
||||
> It is very important for you to validate the `plane.env` for the new changes.
|
||||
> It is very important for you to compare the 2 files `variables-upgrade.env` and `.env`. Copy the newly added variable from downloaded file to `.env` and set the expected values.
|
||||
|
||||
Once done with making changes in `plane.env` file, jump on to `Start Server`
|
||||
|
||||
@@ -355,60 +372,8 @@ Backup completed successfully. Backup files are stored in /....../plane-app/back
|
||||
|
||||
---
|
||||
|
||||
### Restore Data
|
||||
|
||||
When you want to restore the previously backed-up data, follow the instructions below.
|
||||
|
||||
1. Make sure that Plane-CE is installed, started, and then stopped. This ensures that the Docker volumes are created.
|
||||
|
||||
1. Download the restore script using the command below. We suggest downloading it in the same folder as `setup.sh`.
|
||||
|
||||
```bash
|
||||
curl -fsSL -o restore.sh https://raw.githubusercontent.com/makeplane/plane/master/deploy/selfhost/restore.sh
|
||||
chmod +x restore.sh
|
||||
```
|
||||
|
||||
1. Execute the command below to restore your data.
|
||||
|
||||
```bash
|
||||
./restore.sh <path to backup folder containing *.tar.gz files>
|
||||
```
|
||||
|
||||
As an example, for a backup folder `/opt/plane-selfhost/plane-app/backup/20240722-0914`, expect the response below:
|
||||
|
||||
```bash
|
||||
--------------------------------------------
|
||||
____ _ /////////
|
||||
| _ \| | __ _ _ __ ___ /////////
|
||||
| |_) | |/ _` | '_ \ / _ \ ///// /////
|
||||
| __/| | (_| | | | | __/ ///// /////
|
||||
|_| |_|\__,_|_| |_|\___| ////
|
||||
////
|
||||
--------------------------------------------
|
||||
Project management tool from the future
|
||||
--------------------------------------------
|
||||
Found /opt/plane-selfhost/plane-app/backup/20240722-0914/pgdata.tar.gz
|
||||
.....Restoring plane-app_pgdata
|
||||
.....Successfully restored volume plane-app_pgdata from pgdata.tar.gz
|
||||
|
||||
Found /opt/plane-selfhost/plane-app/backup/20240722-0914/redisdata.tar.gz
|
||||
.....Restoring plane-app_redisdata
|
||||
.....Successfully restored volume plane-app_redisdata from redisdata.tar.gz
|
||||
|
||||
Found /opt/plane-selfhost/plane-app/backup/20240722-0914/uploads.tar.gz
|
||||
.....Restoring plane-app_uploads
|
||||
.....Successfully restored volume plane-app_uploads from uploads.tar.gz
|
||||
|
||||
|
||||
Restore completed successfully.
|
||||
```
|
||||
|
||||
1. Start the Plane instance using `./setup.sh start`.
|
||||
|
||||
---
|
||||
|
||||
<details>
|
||||
<summary><h2>Upgrading from v0.13.2 to v0.14.x</h2></summary>
|
||||
## Upgrading from v0.13.2 to v0.14.x
|
||||
|
||||
This is one time activity for users who are upgrading from v0.13.2 to v0.14.0
|
||||
|
||||
@@ -480,4 +445,3 @@ In case the suffixes are wrong or the mentioned volumes are not found, you will
|
||||
In case of successful migration, it will be a silent exit without error.
|
||||
|
||||
Now its time to restart v0.14.0 setup.
|
||||
</details>
|
||||
@@ -1,3 +1,5 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: ${DOCKERHUB_USER:-local}/plane-frontend:${APP_RELEASE:-latest}
|
||||
|
||||
@@ -44,7 +44,7 @@ services:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-frontend:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: if_not_present
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
restart: unless-stopped
|
||||
command: node web/server.js web
|
||||
deploy:
|
||||
@@ -57,7 +57,7 @@ services:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-space:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: if_not_present
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
restart: unless-stopped
|
||||
command: node space/server.js space
|
||||
deploy:
|
||||
@@ -71,7 +71,7 @@ services:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-admin:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: if_not_present
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
restart: unless-stopped
|
||||
command: node admin/server.js admin
|
||||
deploy:
|
||||
@@ -84,7 +84,7 @@ services:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: if_not_present
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
restart: unless-stopped
|
||||
command: ./bin/docker-entrypoint-api.sh
|
||||
deploy:
|
||||
@@ -99,7 +99,7 @@ services:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: if_not_present
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
restart: unless-stopped
|
||||
command: ./bin/docker-entrypoint-worker.sh
|
||||
volumes:
|
||||
@@ -113,7 +113,7 @@ services:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: if_not_present
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
restart: unless-stopped
|
||||
command: ./bin/docker-entrypoint-beat.sh
|
||||
volumes:
|
||||
@@ -127,7 +127,7 @@ services:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: if_not_present
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
restart: "no"
|
||||
command: ./bin/docker-entrypoint-migrator.sh
|
||||
volumes:
|
||||
@@ -167,8 +167,7 @@ services:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-proxy:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
ports:
|
||||
- ${NGINX_PORT}:80
|
||||
depends_on:
|
||||
|
||||
+129
-247
@@ -1,18 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
BRANCH=${BRANCH:-master}
|
||||
BRANCH=master
|
||||
SCRIPT_DIR=$PWD
|
||||
SERVICE_FOLDER=plane-app
|
||||
PLANE_INSTALL_DIR=$PWD/$SERVICE_FOLDER
|
||||
export APP_RELEASE="stable"
|
||||
export APP_RELEASE=$BRANCH
|
||||
export DOCKERHUB_USER=makeplane
|
||||
export PULL_POLICY=${PULL_POLICY:-if_not_present}
|
||||
export PULL_POLICY=always
|
||||
USE_GLOBAL_IMAGES=1
|
||||
|
||||
CPU_ARCH=$(uname -m)
|
||||
|
||||
mkdir -p $PLANE_INSTALL_DIR/archive
|
||||
DOCKER_FILE_PATH=$PLANE_INSTALL_DIR/docker-compose.yaml
|
||||
DOCKER_ENV_PATH=$PLANE_INSTALL_DIR/plane.env
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
GREEN='\033[0;32m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
function print_header() {
|
||||
clear
|
||||
@@ -31,191 +31,65 @@ Project management tool from the future
|
||||
EOF
|
||||
}
|
||||
|
||||
function spinner() {
|
||||
local pid=$1
|
||||
local delay=.5
|
||||
local spinstr='|/-\'
|
||||
|
||||
if ! ps -p "$pid" > /dev/null; then
|
||||
echo "Invalid PID: $pid"
|
||||
return 1
|
||||
fi
|
||||
while ps -p "$pid" > /dev/null; do
|
||||
local temp=${spinstr#?}
|
||||
printf " [%c] " "$spinstr" >&2
|
||||
local spinstr=$temp${spinstr%"$temp"}
|
||||
sleep $delay
|
||||
printf "\b\b\b\b\b\b" >&2
|
||||
done
|
||||
printf " \b\b\b\b" >&2
|
||||
}
|
||||
|
||||
function initialize(){
|
||||
printf "Please wait while we check the availability of Docker images for the selected release ($APP_RELEASE) with ${CPU_ARCH^^} support." >&2
|
||||
|
||||
if [ "$CUSTOM_BUILD" == "true" ]; then
|
||||
function buildLocalImage() {
|
||||
if [ "$1" == "--force-build" ]; then
|
||||
DO_BUILD="1"
|
||||
elif [ "$1" == "--skip-build" ]; then
|
||||
DO_BUILD="2"
|
||||
else
|
||||
printf "\n" >&2
|
||||
printf "${YELLOW}You are on ${CPU_ARCH} cpu architecture. ${NC}\n" >&2
|
||||
printf "${YELLOW}Since the prebuilt ${CPU_ARCH} compatible docker images are not available for, we will be running the docker build on this system. ${NC} \n" >&2
|
||||
printf "${YELLOW}This might take ${YELLOW}5-30 min based on your system's hardware configuration. \n ${NC} \n" >&2
|
||||
printf "\n" >&2
|
||||
printf "${GREEN}Select an option to proceed: ${NC}\n" >&2
|
||||
printf " 1) Build Fresh Images \n" >&2
|
||||
printf " 2) Skip Building Images \n" >&2
|
||||
printf " 3) Exit \n" >&2
|
||||
printf "\n" >&2
|
||||
read -p "Select Option [1]: " DO_BUILD
|
||||
until [[ -z "$DO_BUILD" || "$DO_BUILD" =~ ^[1-3]$ ]]; do
|
||||
echo "$DO_BUILD: invalid selection." >&2
|
||||
read -p "Select Option [1]: " DO_BUILD
|
||||
done
|
||||
echo "" >&2
|
||||
echo "" >&2
|
||||
echo "${CPU_ARCH^^} images are not available for selected release ($APP_RELEASE)." >&2
|
||||
echo "build"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local IMAGE_NAME=makeplane/plane-proxy
|
||||
local IMAGE_TAG=${APP_RELEASE}
|
||||
docker manifest inspect "${IMAGE_NAME}:${IMAGE_TAG}" | grep -q "\"architecture\": \"${CPU_ARCH}\"" &
|
||||
local pid=$!
|
||||
spinner "$pid"
|
||||
|
||||
echo "" >&2
|
||||
if [ "$DO_BUILD" == "1" ] || [ "$DO_BUILD" == "" ];
|
||||
then
|
||||
REPO=https://github.com/makeplane/plane.git
|
||||
CURR_DIR=$PWD
|
||||
PLANE_TEMP_CODE_DIR=$(mktemp -d)
|
||||
git clone $REPO $PLANE_TEMP_CODE_DIR --branch $BRANCH --single-branch
|
||||
|
||||
wait "$pid"
|
||||
cp $PLANE_TEMP_CODE_DIR/deploy/selfhost/build.yml $PLANE_TEMP_CODE_DIR/build.yml
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Plane supports ${CPU_ARCH}" >&2
|
||||
echo "available"
|
||||
return 0
|
||||
else
|
||||
echo "" >&2
|
||||
echo "" >&2
|
||||
echo "${CPU_ARCH^^} images are not available for selected release ($APP_RELEASE)." >&2
|
||||
echo "" >&2
|
||||
echo "build"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
function getEnvValue() {
|
||||
local key=$1
|
||||
local file=$2
|
||||
|
||||
if [ -z "$key" ] || [ -z "$file" ]; then
|
||||
echo "Invalid arguments supplied"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -f "$file" ]; then
|
||||
grep -q "^$key=" "$file"
|
||||
if [ $? -eq 0 ]; then
|
||||
local value
|
||||
value=$(grep "^$key=" "$file" | cut -d'=' -f2)
|
||||
echo "$value"
|
||||
else
|
||||
echo ""
|
||||
cd $PLANE_TEMP_CODE_DIR
|
||||
if [ "$BRANCH" == "master" ];
|
||||
then
|
||||
export APP_RELEASE=stable
|
||||
fi
|
||||
fi
|
||||
}
|
||||
function updateEnvFile() {
|
||||
local key=$1
|
||||
local value=$2
|
||||
local file=$3
|
||||
|
||||
if [ -z "$key" ] || [ -z "$value" ] || [ -z "$file" ]; then
|
||||
echo "Invalid arguments supplied"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -f "$file" ]; then
|
||||
# check if key exists in the file
|
||||
grep -q "^$key=" "$file"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "$key=$value" >> "$file"
|
||||
return
|
||||
else
|
||||
# if key exists, update the value
|
||||
sed -i "s/^$key=.*/$key=$value/g" "$file"
|
||||
fi
|
||||
/bin/bash -c "$COMPOSE_CMD -f build.yml build --no-cache" >&2
|
||||
# cd $CURR_DIR
|
||||
# rm -rf $PLANE_TEMP_CODE_DIR
|
||||
echo "build_completed"
|
||||
elif [ "$DO_BUILD" == "2" ];
|
||||
then
|
||||
printf "${YELLOW}Build action skipped by you in lieu of using existing images. ${NC} \n" >&2
|
||||
echo "build_skipped"
|
||||
elif [ "$DO_BUILD" == "3" ];
|
||||
then
|
||||
echo "build_exited"
|
||||
else
|
||||
echo "File not found: $file"
|
||||
exit 1
|
||||
printf "INVALID OPTION SUPPLIED" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
function updateCustomVariables(){
|
||||
echo "Updating custom variables..." >&2
|
||||
updateEnvFile "DOCKERHUB_USER" "$DOCKERHUB_USER" "$DOCKER_ENV_PATH"
|
||||
updateEnvFile "APP_RELEASE" "$APP_RELEASE" "$DOCKER_ENV_PATH"
|
||||
updateEnvFile "PULL_POLICY" "$PULL_POLICY" "$DOCKER_ENV_PATH"
|
||||
updateEnvFile "CUSTOM_BUILD" "$CUSTOM_BUILD" "$DOCKER_ENV_PATH"
|
||||
echo "Custom variables updated successfully" >&2
|
||||
}
|
||||
|
||||
function syncEnvFile(){
|
||||
echo "Syncing environment variables..." >&2
|
||||
if [ -f "$PLANE_INSTALL_DIR/plane.env.bak" ]; then
|
||||
updateCustomVariables
|
||||
|
||||
# READ keys of plane.env and update the values from plane.env.bak
|
||||
while IFS= read -r line
|
||||
do
|
||||
# ignore is the line is empty or starts with #
|
||||
if [ -z "$line" ] || [[ $line == \#* ]]; then
|
||||
continue
|
||||
fi
|
||||
key=$(echo "$line" | cut -d'=' -f1)
|
||||
value=$(getEnvValue "$key" "$PLANE_INSTALL_DIR/plane.env.bak")
|
||||
if [ -n "$value" ]; then
|
||||
updateEnvFile "$key" "$value" "$DOCKER_ENV_PATH"
|
||||
fi
|
||||
done < "$DOCKER_ENV_PATH"
|
||||
fi
|
||||
echo "Environment variables synced successfully" >&2
|
||||
}
|
||||
|
||||
function buildYourOwnImage(){
|
||||
echo "Building images locally..."
|
||||
|
||||
export DOCKERHUB_USER="myplane"
|
||||
export APP_RELEASE="local"
|
||||
export PULL_POLICY="never"
|
||||
CUSTOM_BUILD="true"
|
||||
|
||||
# checkout the code to ~/tmp/plane folder and build the images
|
||||
local PLANE_TEMP_CODE_DIR=~/tmp/plane
|
||||
rm -rf $PLANE_TEMP_CODE_DIR
|
||||
mkdir -p $PLANE_TEMP_CODE_DIR
|
||||
REPO=https://github.com/makeplane/plane.git
|
||||
git clone "$REPO" "$PLANE_TEMP_CODE_DIR" --branch "$BRANCH" --single-branch --depth 1
|
||||
|
||||
cp "$PLANE_TEMP_CODE_DIR/deploy/selfhost/build.yml" "$PLANE_TEMP_CODE_DIR/build.yml"
|
||||
|
||||
cd "$PLANE_TEMP_CODE_DIR" || exit
|
||||
|
||||
/bin/bash -c "$COMPOSE_CMD -f build.yml build --no-cache" >&2
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Build failed. Exiting..."
|
||||
exit 1
|
||||
fi
|
||||
echo "Build completed successfully"
|
||||
echo ""
|
||||
echo "You can now start the services by running the command: ./setup.sh start"
|
||||
echo ""
|
||||
}
|
||||
|
||||
function install() {
|
||||
echo "Begin Installing Plane"
|
||||
echo ""
|
||||
|
||||
local build_image=$(initialize)
|
||||
|
||||
if [ "$build_image" == "build" ]; then
|
||||
# ask for confirmation to continue building the images
|
||||
echo "Do you want to continue with building the Docker images locally?"
|
||||
read -p "Continue? [y/N]: " confirm
|
||||
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
|
||||
echo "Exiting..."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$build_image" == "build" ]; then
|
||||
download "true"
|
||||
else
|
||||
download "false"
|
||||
fi
|
||||
echo "Installing Plane.........."
|
||||
download
|
||||
}
|
||||
|
||||
function download() {
|
||||
local LOCAL_BUILD=$1
|
||||
cd $SCRIPT_DIR
|
||||
TS=$(date +%s)
|
||||
if [ -f "$PLANE_INSTALL_DIR/docker-compose.yaml" ]
|
||||
@@ -228,48 +102,44 @@ function download() {
|
||||
|
||||
if [ -f "$DOCKER_ENV_PATH" ];
|
||||
then
|
||||
cp "$DOCKER_ENV_PATH" "$PLANE_INSTALL_DIR/archive/$TS.env"
|
||||
cp "$DOCKER_ENV_PATH" "$PLANE_INSTALL_DIR/plane.env.bak"
|
||||
cp $DOCKER_ENV_PATH $PLANE_INSTALL_DIR/archive/$TS.env
|
||||
else
|
||||
mv $PLANE_INSTALL_DIR/variables-upgrade.env $DOCKER_ENV_PATH
|
||||
fi
|
||||
|
||||
mv $PLANE_INSTALL_DIR/variables-upgrade.env $DOCKER_ENV_PATH
|
||||
if [ "$BRANCH" != "master" ];
|
||||
then
|
||||
cp $PLANE_INSTALL_DIR/docker-compose.yaml $PLANE_INSTALL_DIR/temp.yaml
|
||||
sed -e 's@${APP_RELEASE:-stable}@'"$BRANCH"'@g' \
|
||||
$PLANE_INSTALL_DIR/temp.yaml > $PLANE_INSTALL_DIR/docker-compose.yaml
|
||||
|
||||
syncEnvFile
|
||||
rm $PLANE_INSTALL_DIR/temp.yaml
|
||||
fi
|
||||
|
||||
if [ "$LOCAL_BUILD" == "true" ]; then
|
||||
export DOCKERHUB_USER="myplane"
|
||||
export APP_RELEASE="local"
|
||||
export PULL_POLICY="never"
|
||||
CUSTOM_BUILD="true"
|
||||
if [ $USE_GLOBAL_IMAGES == 0 ]; then
|
||||
local res=$(buildLocalImage)
|
||||
# echo $res
|
||||
|
||||
buildYourOwnImage
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo ""
|
||||
echo "Build failed. Exiting..."
|
||||
exit 1
|
||||
if [ "$res" == "build_exited" ];
|
||||
then
|
||||
echo
|
||||
echo "Install action cancelled by you. Exiting now."
|
||||
echo
|
||||
exit 0
|
||||
fi
|
||||
updateCustomVariables
|
||||
else
|
||||
CUSTOM_BUILD="false"
|
||||
updateCustomVariables
|
||||
/bin/bash -c "$COMPOSE_CMD -f $DOCKER_FILE_PATH --env-file=$DOCKER_ENV_PATH pull --policy always"
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo ""
|
||||
echo "Failed to pull the images. Exiting..."
|
||||
exit 1
|
||||
fi
|
||||
/bin/bash -c "$COMPOSE_CMD -f $DOCKER_FILE_PATH --env-file=$DOCKER_ENV_PATH pull"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Most recent version of Plane is now available for you to use"
|
||||
echo "Most recent Stable version is now available for you to use"
|
||||
echo ""
|
||||
echo "In case of 'Upgrade', please check the 'plane.env 'file for any new variables and update them accordingly"
|
||||
echo "In case of Upgrade, your new setting file is availabe as 'variables-upgrade.env'. Please compare and set the required values in 'plane.env 'file."
|
||||
echo ""
|
||||
|
||||
}
|
||||
function startServices() {
|
||||
/bin/bash -c "$COMPOSE_CMD -f $DOCKER_FILE_PATH --env-file=$DOCKER_ENV_PATH up -d --pull if_not_present --quiet-pull"
|
||||
/bin/bash -c "$COMPOSE_CMD -f $DOCKER_FILE_PATH --env-file=$DOCKER_ENV_PATH up -d --quiet-pull"
|
||||
|
||||
local migrator_container_id=$(docker container ls -aq -f "name=$SERVICE_FOLDER-migrator")
|
||||
if [ -n "$migrator_container_id" ]; then
|
||||
@@ -331,7 +201,7 @@ function upgrade() {
|
||||
|
||||
echo
|
||||
echo "***** DOWNLOADING STABLE VERSION ****"
|
||||
install
|
||||
download
|
||||
|
||||
echo "***** PLEASE VALIDATE AND START SERVICES ****"
|
||||
}
|
||||
@@ -412,6 +282,7 @@ function viewLogs(){
|
||||
echo "INVALID SERVICE NAME SUPPLIED"
|
||||
fi
|
||||
}
|
||||
|
||||
function backupSingleVolume() {
|
||||
backupFolder=$1
|
||||
selectedVolume=$2
|
||||
@@ -428,6 +299,7 @@ function backupSingleVolume() {
|
||||
-v "$backupFolder":/backup \
|
||||
busybox sh -c 'tar -czf "/backup/${TAR_NAME}.tar.gz" /${TAR_NAME}'
|
||||
}
|
||||
|
||||
function backupData() {
|
||||
local datetime=$(date +"%Y%m%d-%H%M")
|
||||
local BACKUP_FOLDER=$PLANE_INSTALL_DIR/backup/$datetime
|
||||
@@ -457,7 +329,7 @@ function askForAction() {
|
||||
then
|
||||
echo
|
||||
echo "Select a Action you want to perform:"
|
||||
echo " 1) Install"
|
||||
echo " 1) Install (${CPU_ARCH})"
|
||||
echo " 2) Start"
|
||||
echo " 3) Stop"
|
||||
echo " 4) Restart"
|
||||
@@ -479,31 +351,31 @@ function askForAction() {
|
||||
echo
|
||||
fi
|
||||
|
||||
if [ "$ACTION" == "1" ] || [ "$DEFAULT_ACTION" == "install" ];
|
||||
if [ "$ACTION" == "1" ] || [ "$DEFAULT_ACTION" == "install" ]
|
||||
then
|
||||
install
|
||||
# askForAction
|
||||
elif [ "$ACTION" == "2" ] || [ "$DEFAULT_ACTION" == "start" ];
|
||||
askForAction
|
||||
elif [ "$ACTION" == "2" ] || [ "$DEFAULT_ACTION" == "start" ]
|
||||
then
|
||||
startServices
|
||||
# askForAction
|
||||
elif [ "$ACTION" == "3" ] || [ "$DEFAULT_ACTION" == "stop" ];
|
||||
elif [ "$ACTION" == "3" ] || [ "$DEFAULT_ACTION" == "stop" ]
|
||||
then
|
||||
stopServices
|
||||
# askForAction
|
||||
elif [ "$ACTION" == "4" ] || [ "$DEFAULT_ACTION" == "restart" ];
|
||||
elif [ "$ACTION" == "4" ] || [ "$DEFAULT_ACTION" == "restart" ]
|
||||
then
|
||||
restartServices
|
||||
# askForAction
|
||||
elif [ "$ACTION" == "5" ] || [ "$DEFAULT_ACTION" == "upgrade" ];
|
||||
elif [ "$ACTION" == "5" ] || [ "$DEFAULT_ACTION" == "upgrade" ]
|
||||
then
|
||||
upgrade
|
||||
# askForAction
|
||||
elif [ "$ACTION" == "6" ] || [ "$DEFAULT_ACTION" == "logs" ];
|
||||
then
|
||||
viewLogs "$@"
|
||||
askForAction
|
||||
elif [ "$ACTION" == "7" ] || [ "$DEFAULT_ACTION" == "backup" ];
|
||||
elif [ "$ACTION" == "6" ] || [ "$DEFAULT_ACTION" == "logs" ]
|
||||
then
|
||||
viewLogs $@
|
||||
askForAction
|
||||
elif [ "$ACTION" == "7" ] || [ "$DEFAULT_ACTION" == "backup" ]
|
||||
then
|
||||
backupData
|
||||
elif [ "$ACTION" == "8" ]
|
||||
@@ -522,38 +394,48 @@ else
|
||||
COMPOSE_CMD="docker compose"
|
||||
fi
|
||||
|
||||
if [ "$CPU_ARCH" == "x86_64" ] || [ "$CPU_ARCH" == "amd64" ]; then
|
||||
CPU_ARCH="amd64"
|
||||
elif [ "$CPU_ARCH" == "aarch64" ] || [ "$CPU_ARCH" == "arm64" ]; then
|
||||
CPU_ARCH="arm64"
|
||||
# CPU ARCHITECHTURE BASED SETTINGS
|
||||
CPU_ARCH=$(uname -m)
|
||||
if [[ $FORCE_CPU == "amd64" || $CPU_ARCH == "amd64" || $CPU_ARCH == "x86_64" || ( $BRANCH == "master" && ( $CPU_ARCH == "arm64" || $CPU_ARCH == "aarch64" ) ) ]];
|
||||
then
|
||||
USE_GLOBAL_IMAGES=1
|
||||
DOCKERHUB_USER=makeplane
|
||||
PULL_POLICY=always
|
||||
else
|
||||
USE_GLOBAL_IMAGES=0
|
||||
DOCKERHUB_USER=myplane
|
||||
PULL_POLICY=never
|
||||
fi
|
||||
|
||||
if [ -f "$DOCKER_ENV_PATH" ]; then
|
||||
DOCKERHUB_USER=$(getEnvValue "DOCKERHUB_USER" "$DOCKER_ENV_PATH")
|
||||
APP_RELEASE=$(getEnvValue "APP_RELEASE" "$DOCKER_ENV_PATH")
|
||||
PULL_POLICY=$(getEnvValue "PULL_POLICY" "$DOCKER_ENV_PATH")
|
||||
CUSTOM_BUILD=$(getEnvValue "CUSTOM_BUILD" "$DOCKER_ENV_PATH")
|
||||
if [ "$BRANCH" == "master" ];
|
||||
then
|
||||
export APP_RELEASE=stable
|
||||
fi
|
||||
|
||||
if [ -z "$DOCKERHUB_USER" ]; then
|
||||
DOCKERHUB_USER=makeplane
|
||||
updateEnvFile "DOCKERHUB_USER" "$DOCKERHUB_USER" "$DOCKER_ENV_PATH"
|
||||
fi
|
||||
# REMOVE SPECIAL CHARACTERS FROM BRANCH NAME
|
||||
if [ "$BRANCH" != "master" ];
|
||||
then
|
||||
SERVICE_FOLDER=plane-app-$(echo $BRANCH | sed -r 's@(\/|" "|\.)@-@g')
|
||||
PLANE_INSTALL_DIR=$PWD/$SERVICE_FOLDER
|
||||
fi
|
||||
mkdir -p $PLANE_INSTALL_DIR/archive
|
||||
|
||||
if [ -z "$APP_RELEASE" ]; then
|
||||
APP_RELEASE=stable
|
||||
updateEnvFile "APP_RELEASE" "$APP_RELEASE" "$DOCKER_ENV_PATH"
|
||||
fi
|
||||
DOCKER_FILE_PATH=$PLANE_INSTALL_DIR/docker-compose.yaml
|
||||
DOCKER_ENV_PATH=$PLANE_INSTALL_DIR/plane.env
|
||||
|
||||
if [ -z "$PULL_POLICY" ]; then
|
||||
PULL_POLICY=if_not_present
|
||||
updateEnvFile "PULL_POLICY" "$PULL_POLICY" "$DOCKER_ENV_PATH"
|
||||
fi
|
||||
|
||||
if [ -z "$CUSTOM_BUILD" ]; then
|
||||
CUSTOM_BUILD=false
|
||||
updateEnvFile "CUSTOM_BUILD" "$CUSTOM_BUILD" "$DOCKER_ENV_PATH"
|
||||
# BACKWARD COMPATIBILITY
|
||||
OLD_DOCKER_ENV_PATH=$PLANE_INSTALL_DIR/.env
|
||||
if [ -f "$OLD_DOCKER_ENV_PATH" ];
|
||||
then
|
||||
mv "$OLD_DOCKER_ENV_PATH" "$DOCKER_ENV_PATH"
|
||||
OS_NAME=$(uname)
|
||||
if [ "$OS_NAME" == "Darwin" ];
|
||||
then
|
||||
sed -i '' -e 's@APP_RELEASE=latest@APP_RELEASE=stable@' "$DOCKER_ENV_PATH"
|
||||
else
|
||||
sed -i -e 's@APP_RELEASE=latest@APP_RELEASE=stable@' "$DOCKER_ENV_PATH"
|
||||
fi
|
||||
fi
|
||||
|
||||
print_header
|
||||
askForAction "$@"
|
||||
askForAction $@
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
function print_header() {
|
||||
clear
|
||||
|
||||
cat <<"EOF"
|
||||
--------------------------------------------
|
||||
____ _ /////////
|
||||
| _ \| | __ _ _ __ ___ /////////
|
||||
| |_) | |/ _` | '_ \ / _ \ ///// /////
|
||||
| __/| | (_| | | | | __/ ///// /////
|
||||
|_| |_|\__,_|_| |_|\___| ////
|
||||
////
|
||||
--------------------------------------------
|
||||
Project management tool from the future
|
||||
--------------------------------------------
|
||||
EOF
|
||||
}
|
||||
|
||||
function restoreSingleVolume() {
|
||||
selectedVolume=$1
|
||||
backupFolder=$2
|
||||
restoreFile=$3
|
||||
|
||||
docker volume rm "$selectedVolume" > /dev/null 2>&1
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Failed to remove volume $selectedVolume"
|
||||
echo ""
|
||||
return 1
|
||||
fi
|
||||
|
||||
docker volume create "$selectedVolume" > /dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Failed to create volume $selectedVolume"
|
||||
echo ""
|
||||
return 1
|
||||
fi
|
||||
|
||||
docker run --rm \
|
||||
-e TAR_NAME="$restoreFile" \
|
||||
-v "$selectedVolume":"/vol" \
|
||||
-v "$backupFolder":/backup \
|
||||
busybox sh -c 'mkdir -p /restore && tar -xzf "/backup/${TAR_NAME}.tar.gz" -C /restore && mv /restore/${TAR_NAME}/* /vol'
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Failed to restore volume ${selectedVolume} from ${restoreFile}.tar.gz"
|
||||
echo ""
|
||||
return 1
|
||||
fi
|
||||
echo ".....Successfully restored volume $selectedVolume from ${restoreFile}.tar.gz"
|
||||
echo ""
|
||||
}
|
||||
|
||||
function restoreData() {
|
||||
print_header
|
||||
local BACKUP_FOLDER=${1:-$PWD}
|
||||
|
||||
local dockerServiceStatus
|
||||
dockerServiceStatus=$($COMPOSE_CMD ls --filter name=plane-app --format=json | jq -r .[0].Status)
|
||||
local dockerServicePrefix
|
||||
dockerServicePrefix="running"
|
||||
|
||||
if [[ $dockerServiceStatus == $dockerServicePrefix* ]]; then
|
||||
echo "Plane App is running. Please STOP the Plane App before restoring data."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local volumes
|
||||
volumes=$(docker volume ls -f "name=plane-app" --format "{{.Name}}" | grep -E "_pgdata|_redisdata|_uploads")
|
||||
# Check if there are any matching volumes
|
||||
if [ -z "$volumes" ]; then
|
||||
echo ".....No volumes found starting with 'plane-app'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
for BACKUP_FILE in $BACKUP_FOLDER/*.tar.gz; do
|
||||
if [ -e "$BACKUP_FILE" ]; then
|
||||
|
||||
local restoreFileName
|
||||
restoreFileName=$(basename "$BACKUP_FILE")
|
||||
restoreFileName="${restoreFileName%.tar.gz}"
|
||||
|
||||
local restoreVolName
|
||||
restoreVolName="plane-app_${restoreFileName}"
|
||||
echo "Found $BACKUP_FILE"
|
||||
|
||||
local docVol
|
||||
docVol=$(docker volume ls -f "name=$restoreVolName" --format "{{.Name}}" | grep -E "_pgdata|_redisdata|_uploads")
|
||||
|
||||
if [ -z "$docVol" ]; then
|
||||
echo "Skipping: No volume found with name $restoreVolName"
|
||||
else
|
||||
echo ".....Restoring $docVol"
|
||||
restoreSingleVolume "$docVol" "$BACKUP_FOLDER" "$restoreFileName"
|
||||
fi
|
||||
else
|
||||
echo "No .tar.gz files found in the current directory."
|
||||
echo ""
|
||||
echo "Please provide the path to the backup file."
|
||||
echo ""
|
||||
echo "Usage: ./restore.sh /path/to/backup"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Restore completed successfully."
|
||||
echo ""
|
||||
}
|
||||
|
||||
# if docker-compose is installed
|
||||
if command -v docker-compose &> /dev/null
|
||||
then
|
||||
COMPOSE_CMD="docker-compose"
|
||||
else
|
||||
COMPOSE_CMD="docker compose"
|
||||
fi
|
||||
|
||||
restoreData "$@"
|
||||
@@ -1,4 +1,3 @@
|
||||
APP_DOMAIN=localhost
|
||||
APP_RELEASE=stable
|
||||
|
||||
WEB_REPLICAS=1
|
||||
@@ -7,11 +6,11 @@ ADMIN_REPLICAS=1
|
||||
API_REPLICAS=1
|
||||
|
||||
NGINX_PORT=80
|
||||
WEB_URL=http://${APP_DOMAIN}
|
||||
WEB_URL=http://localhost
|
||||
DEBUG=0
|
||||
SENTRY_DSN=
|
||||
SENTRY_ENVIRONMENT=production
|
||||
CORS_ALLOWED_ORIGINS=http://${APP_DOMAIN}
|
||||
CORS_ALLOWED_ORIGINS=http://localhost
|
||||
|
||||
#DB SETTINGS
|
||||
PGHOST=plane-db
|
||||
@@ -47,5 +46,4 @@ FILE_SIZE_LIMIT=5242880
|
||||
GUNICORN_WORKERS=1
|
||||
|
||||
# UNCOMMENT `DOCKER_PLATFORM` IF YOU ARE ON `ARM64` AND DOCKER IMAGE IS NOT AVAILABLE FOR RESPECTIVE `APP_RELEASE`
|
||||
# DOCKER_PLATFORM=linux/amd64
|
||||
|
||||
# DOCKER_PLATFORM=linux/amd64
|
||||
+1
-1
@@ -34,7 +34,7 @@
|
||||
"prettier": "latest",
|
||||
"prettier-plugin-tailwindcss": "^0.5.4",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"turbo": "^2.0.9"
|
||||
"turbo": "^2.0.6"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "18.2.48"
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export * from "./auth";
|
||||
export * from "./issue";
|
||||
@@ -1,27 +0,0 @@
|
||||
export const ALL_ISSUES = "All Issues";
|
||||
|
||||
export enum EIssueGroupByToServerOptions {
|
||||
"state" = "state_id",
|
||||
"priority" = "priority",
|
||||
"labels" = "labels__id",
|
||||
"state_detail.group" = "state__group",
|
||||
"assignees" = "assignees__id",
|
||||
"cycle" = "cycle_id",
|
||||
"module" = "issue_module__module_id",
|
||||
"target_date" = "target_date",
|
||||
"project" = "project_id",
|
||||
"created_by" = "created_by",
|
||||
}
|
||||
|
||||
export enum EServerGroupByToFilterOptions {
|
||||
"state_id" = "state",
|
||||
"priority" = "priority",
|
||||
"labels__id" = "labels",
|
||||
"state__group" = "state_group",
|
||||
"assignees__id" = "assignees",
|
||||
"cycle_id" = "cycle",
|
||||
"issue_module__module_id" = "module",
|
||||
"target_date" = "target_date",
|
||||
"project_id" = "project",
|
||||
"created_by" = "created_by",
|
||||
}
|
||||
@@ -34,7 +34,6 @@ export const EditorWrapper: React.FC<Props> = (props) => {
|
||||
|
||||
const editor = useEditor({
|
||||
editorClassName,
|
||||
enableHistory: true,
|
||||
extensions,
|
||||
fileHandler,
|
||||
forwardedRef,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Selection } from "@tiptap/pm/state";
|
||||
import { Editor } from "@tiptap/react";
|
||||
import {
|
||||
BoldIcon,
|
||||
Heading1,
|
||||
@@ -21,6 +19,8 @@ import {
|
||||
CaseSensitive,
|
||||
LucideIcon,
|
||||
} from "lucide-react";
|
||||
import { Selection } from "@tiptap/pm/state";
|
||||
import { Editor } from "@tiptap/react";
|
||||
// helpers
|
||||
import {
|
||||
insertImageCommand,
|
||||
@@ -43,151 +43,168 @@ import {
|
||||
toggleUnderline,
|
||||
} from "@/helpers/editor-commands";
|
||||
// types
|
||||
import { TEditorCommands, UploadImage } from "@/types";
|
||||
import { UploadImage } from "@/types";
|
||||
|
||||
export interface EditorMenuItem {
|
||||
key: TEditorCommands;
|
||||
key: string;
|
||||
name: string;
|
||||
isActive: () => boolean;
|
||||
command: () => void;
|
||||
icon: LucideIcon;
|
||||
}
|
||||
|
||||
export const TextItem = (editor: Editor): EditorMenuItem => ({
|
||||
key: "text",
|
||||
name: "Text",
|
||||
isActive: () => editor.isActive("paragraph"),
|
||||
command: () => setText(editor),
|
||||
icon: CaseSensitive,
|
||||
});
|
||||
export const TextItem = (editor: Editor) =>
|
||||
({
|
||||
key: "text",
|
||||
name: "Text",
|
||||
isActive: () => editor.isActive("paragraph"),
|
||||
command: () => setText(editor),
|
||||
icon: CaseSensitive,
|
||||
}) as const satisfies EditorMenuItem;
|
||||
|
||||
export const HeadingOneItem = (editor: Editor): EditorMenuItem => ({
|
||||
key: "h1",
|
||||
name: "Heading 1",
|
||||
isActive: () => editor.isActive("heading", { level: 1 }),
|
||||
command: () => toggleHeadingOne(editor),
|
||||
icon: Heading1,
|
||||
});
|
||||
export const HeadingOneItem = (editor: Editor) =>
|
||||
({
|
||||
key: "h1",
|
||||
name: "Heading 1",
|
||||
isActive: () => editor.isActive("heading", { level: 1 }),
|
||||
command: () => toggleHeadingOne(editor),
|
||||
icon: Heading1,
|
||||
}) as const satisfies EditorMenuItem;
|
||||
|
||||
export const HeadingTwoItem = (editor: Editor): EditorMenuItem => ({
|
||||
key: "h2",
|
||||
name: "Heading 2",
|
||||
isActive: () => editor.isActive("heading", { level: 2 }),
|
||||
command: () => toggleHeadingTwo(editor),
|
||||
icon: Heading2,
|
||||
});
|
||||
export const HeadingTwoItem = (editor: Editor) =>
|
||||
({
|
||||
key: "h2",
|
||||
name: "Heading 2",
|
||||
isActive: () => editor.isActive("heading", { level: 2 }),
|
||||
command: () => toggleHeadingTwo(editor),
|
||||
icon: Heading2,
|
||||
}) as const satisfies EditorMenuItem;
|
||||
|
||||
export const HeadingThreeItem = (editor: Editor): EditorMenuItem => ({
|
||||
key: "h3",
|
||||
name: "Heading 3",
|
||||
isActive: () => editor.isActive("heading", { level: 3 }),
|
||||
command: () => toggleHeadingThree(editor),
|
||||
icon: Heading3,
|
||||
});
|
||||
export const HeadingThreeItem = (editor: Editor) =>
|
||||
({
|
||||
key: "h3",
|
||||
name: "Heading 3",
|
||||
isActive: () => editor.isActive("heading", { level: 3 }),
|
||||
command: () => toggleHeadingThree(editor),
|
||||
icon: Heading3,
|
||||
}) as const satisfies EditorMenuItem;
|
||||
|
||||
export const HeadingFourItem = (editor: Editor): EditorMenuItem => ({
|
||||
key: "h4",
|
||||
name: "Heading 4",
|
||||
isActive: () => editor.isActive("heading", { level: 4 }),
|
||||
command: () => toggleHeadingFour(editor),
|
||||
icon: Heading4,
|
||||
});
|
||||
export const HeadingFourItem = (editor: Editor) =>
|
||||
({
|
||||
key: "h4",
|
||||
name: "Heading 4",
|
||||
isActive: () => editor.isActive("heading", { level: 4 }),
|
||||
command: () => toggleHeadingFour(editor),
|
||||
icon: Heading4,
|
||||
}) as const satisfies EditorMenuItem;
|
||||
|
||||
export const HeadingFiveItem = (editor: Editor): EditorMenuItem => ({
|
||||
key: "h5",
|
||||
name: "Heading 5",
|
||||
isActive: () => editor.isActive("heading", { level: 5 }),
|
||||
command: () => toggleHeadingFive(editor),
|
||||
icon: Heading5,
|
||||
});
|
||||
export const HeadingFiveItem = (editor: Editor) =>
|
||||
({
|
||||
key: "h5",
|
||||
name: "Heading 5",
|
||||
isActive: () => editor.isActive("heading", { level: 5 }),
|
||||
command: () => toggleHeadingFive(editor),
|
||||
icon: Heading5,
|
||||
}) as const satisfies EditorMenuItem;
|
||||
|
||||
export const HeadingSixItem = (editor: Editor): EditorMenuItem => ({
|
||||
key: "h6",
|
||||
name: "Heading 6",
|
||||
isActive: () => editor.isActive("heading", { level: 6 }),
|
||||
command: () => toggleHeadingSix(editor),
|
||||
icon: Heading6,
|
||||
});
|
||||
export const HeadingSixItem = (editor: Editor) =>
|
||||
({
|
||||
key: "h6",
|
||||
name: "Heading 6",
|
||||
isActive: () => editor.isActive("heading", { level: 6 }),
|
||||
command: () => toggleHeadingSix(editor),
|
||||
icon: Heading6,
|
||||
}) as const satisfies EditorMenuItem;
|
||||
|
||||
export const BoldItem = (editor: Editor): EditorMenuItem => ({
|
||||
key: "bold",
|
||||
name: "Bold",
|
||||
isActive: () => editor?.isActive("bold"),
|
||||
command: () => toggleBold(editor),
|
||||
icon: BoldIcon,
|
||||
});
|
||||
export const BoldItem = (editor: Editor) =>
|
||||
({
|
||||
key: "bold",
|
||||
name: "Bold",
|
||||
isActive: () => editor?.isActive("bold"),
|
||||
command: () => toggleBold(editor),
|
||||
icon: BoldIcon,
|
||||
}) as const satisfies EditorMenuItem;
|
||||
|
||||
export const ItalicItem = (editor: Editor): EditorMenuItem => ({
|
||||
key: "italic",
|
||||
name: "Italic",
|
||||
isActive: () => editor?.isActive("italic"),
|
||||
command: () => toggleItalic(editor),
|
||||
icon: ItalicIcon,
|
||||
});
|
||||
export const ItalicItem = (editor: Editor) =>
|
||||
({
|
||||
key: "italic",
|
||||
name: "Italic",
|
||||
isActive: () => editor?.isActive("italic"),
|
||||
command: () => toggleItalic(editor),
|
||||
icon: ItalicIcon,
|
||||
}) as const satisfies EditorMenuItem;
|
||||
|
||||
export const UnderLineItem = (editor: Editor): EditorMenuItem => ({
|
||||
key: "underline",
|
||||
name: "Underline",
|
||||
isActive: () => editor?.isActive("underline"),
|
||||
command: () => toggleUnderline(editor),
|
||||
icon: UnderlineIcon,
|
||||
});
|
||||
export const UnderLineItem = (editor: Editor) =>
|
||||
({
|
||||
key: "underline",
|
||||
name: "Underline",
|
||||
isActive: () => editor?.isActive("underline"),
|
||||
command: () => toggleUnderline(editor),
|
||||
icon: UnderlineIcon,
|
||||
}) as const satisfies EditorMenuItem;
|
||||
|
||||
export const StrikeThroughItem = (editor: Editor): EditorMenuItem => ({
|
||||
key: "strikethrough",
|
||||
name: "Strikethrough",
|
||||
isActive: () => editor?.isActive("strike"),
|
||||
command: () => toggleStrike(editor),
|
||||
icon: StrikethroughIcon,
|
||||
});
|
||||
export const StrikeThroughItem = (editor: Editor) =>
|
||||
({
|
||||
key: "strikethrough",
|
||||
name: "Strikethrough",
|
||||
isActive: () => editor?.isActive("strike"),
|
||||
command: () => toggleStrike(editor),
|
||||
icon: StrikethroughIcon,
|
||||
}) as const satisfies EditorMenuItem;
|
||||
|
||||
export const BulletListItem = (editor: Editor): EditorMenuItem => ({
|
||||
key: "bulleted-list",
|
||||
name: "Bulleted list",
|
||||
isActive: () => editor?.isActive("bulletList"),
|
||||
command: () => toggleBulletList(editor),
|
||||
icon: ListIcon,
|
||||
});
|
||||
export const BulletListItem = (editor: Editor) =>
|
||||
({
|
||||
key: "bulleted-list",
|
||||
name: "Bulleted list",
|
||||
isActive: () => editor?.isActive("bulletList"),
|
||||
command: () => toggleBulletList(editor),
|
||||
icon: ListIcon,
|
||||
}) as const satisfies EditorMenuItem;
|
||||
|
||||
export const NumberedListItem = (editor: Editor): EditorMenuItem => ({
|
||||
key: "numbered-list",
|
||||
name: "Numbered list",
|
||||
isActive: () => editor?.isActive("orderedList"),
|
||||
command: () => toggleOrderedList(editor),
|
||||
icon: ListOrderedIcon,
|
||||
});
|
||||
export const NumberedListItem = (editor: Editor) =>
|
||||
({
|
||||
key: "numbered-list",
|
||||
name: "Numbered list",
|
||||
isActive: () => editor?.isActive("orderedList"),
|
||||
command: () => toggleOrderedList(editor),
|
||||
icon: ListOrderedIcon,
|
||||
}) as const satisfies EditorMenuItem;
|
||||
|
||||
export const TodoListItem = (editor: Editor): EditorMenuItem => ({
|
||||
key: "to-do-list",
|
||||
name: "To-do list",
|
||||
isActive: () => editor.isActive("taskItem"),
|
||||
command: () => toggleTaskList(editor),
|
||||
icon: CheckSquare,
|
||||
});
|
||||
export const TodoListItem = (editor: Editor) =>
|
||||
({
|
||||
key: "to-do-list",
|
||||
name: "To-do list",
|
||||
isActive: () => editor.isActive("taskItem"),
|
||||
command: () => toggleTaskList(editor),
|
||||
icon: CheckSquare,
|
||||
}) as const satisfies EditorMenuItem;
|
||||
|
||||
export const QuoteItem = (editor: Editor): EditorMenuItem => ({
|
||||
key: "quote",
|
||||
name: "Quote",
|
||||
isActive: () => editor?.isActive("blockquote"),
|
||||
command: () => toggleBlockquote(editor),
|
||||
icon: QuoteIcon,
|
||||
});
|
||||
export const QuoteItem = (editor: Editor) =>
|
||||
({
|
||||
key: "quote",
|
||||
name: "Quote",
|
||||
isActive: () => editor?.isActive("blockquote"),
|
||||
command: () => toggleBlockquote(editor),
|
||||
icon: QuoteIcon,
|
||||
}) as const satisfies EditorMenuItem;
|
||||
|
||||
export const CodeItem = (editor: Editor): EditorMenuItem => ({
|
||||
key: "code",
|
||||
name: "Code",
|
||||
isActive: () => editor?.isActive("code") || editor?.isActive("codeBlock"),
|
||||
command: () => toggleCodeBlock(editor),
|
||||
icon: CodeIcon,
|
||||
});
|
||||
export const CodeItem = (editor: Editor) =>
|
||||
({
|
||||
key: "code",
|
||||
name: "Code",
|
||||
isActive: () => editor?.isActive("code") || editor?.isActive("codeBlock"),
|
||||
command: () => toggleCodeBlock(editor),
|
||||
icon: CodeIcon,
|
||||
}) as const satisfies EditorMenuItem;
|
||||
|
||||
export const TableItem = (editor: Editor): EditorMenuItem => ({
|
||||
key: "table",
|
||||
name: "Table",
|
||||
isActive: () => editor?.isActive("table"),
|
||||
command: () => insertTableCommand(editor),
|
||||
icon: TableIcon,
|
||||
});
|
||||
export const TableItem = (editor: Editor) =>
|
||||
({
|
||||
key: "table",
|
||||
name: "Table",
|
||||
isActive: () => editor?.isActive("table"),
|
||||
command: () => insertTableCommand(editor),
|
||||
icon: TableIcon,
|
||||
}) as const satisfies EditorMenuItem;
|
||||
|
||||
export const ImageItem = (editor: Editor, uploadFile: UploadImage) =>
|
||||
({
|
||||
@@ -223,3 +240,6 @@ export function getEditorMenuItems(editor: Editor | null, uploadFile: UploadImag
|
||||
ImageItem(editor, uploadFile),
|
||||
];
|
||||
}
|
||||
|
||||
export type EditorMenuItemNames =
|
||||
ReturnType<typeof getEditorMenuItems> extends (infer U)[] ? (U extends { key: infer N } ? N : never) : never;
|
||||
|
||||
@@ -33,7 +33,7 @@ export const CustomCodeInlineExtension = Mark.create<CodeOptions>({
|
||||
return {
|
||||
HTMLAttributes: {
|
||||
class:
|
||||
"rounded bg-custom-background-80 px-[6px] py-[1.5px] font-mono font-medium text-orange-500 border-[0.5px] border-custom-border-200",
|
||||
"rounded bg-custom-background-80 px-1 py-[2px] font-mono font-medium text-orange-500 border-[0.5px] border-custom-border-200",
|
||||
spellcheck: "false",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -40,9 +40,9 @@ export const CodeBlockComponent: React.FC<CodeBlockComponentProps> = ({ node })
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"group/button hidden group-hover/code:flex items-center justify-center absolute top-2 right-2 z-10 size-8 rounded-md bg-custom-background-80 border border-custom-border-200 transition duration-150 ease-in-out backdrop-blur-sm",
|
||||
"group/button hidden group-hover/code:flex items-center justify-center absolute top-2 right-2 z-10 size-8 rounded-md bg-custom-background-80 border border-custom-border-200 transition duration-150 ease-in-out",
|
||||
{
|
||||
"bg-green-500/30 hover:bg-green-500/30 active:bg-green-500/30": copied,
|
||||
"bg-green-500/10 hover:bg-green-500/10 active:bg-green-500/10": copied,
|
||||
}
|
||||
)}
|
||||
onClick={copyToClipboard}
|
||||
@@ -55,7 +55,7 @@ export const CodeBlockComponent: React.FC<CodeBlockComponentProps> = ({ node })
|
||||
</button>
|
||||
</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-8 pl-9 pr-4">
|
||||
<NodeViewContent as="code" className="whitespace-[pre-wrap]" />
|
||||
</pre>
|
||||
</NodeViewWrapper>
|
||||
|
||||
@@ -30,25 +30,23 @@ import { isValidHttpUrl } from "@/helpers/common";
|
||||
import { DeleteImage, IMentionHighlight, IMentionSuggestion, RestoreImage, UploadImage } from "@/types";
|
||||
|
||||
type TArguments = {
|
||||
enableHistory: boolean;
|
||||
mentionConfig: {
|
||||
mentionSuggestions?: () => Promise<IMentionSuggestion[]>;
|
||||
mentionHighlights?: () => Promise<IMentionHighlight[]>;
|
||||
};
|
||||
fileConfig: {
|
||||
deleteFile: DeleteImage;
|
||||
restoreFile: RestoreImage;
|
||||
cancelUploadImage?: () => void;
|
||||
uploadFile: UploadImage;
|
||||
};
|
||||
mentionConfig: {
|
||||
mentionSuggestions?: () => Promise<IMentionSuggestion[]>;
|
||||
mentionHighlights?: () => Promise<IMentionHighlight[]>;
|
||||
};
|
||||
placeholder?: string | ((isFocused: boolean, value: string) => string);
|
||||
tabIndex?: number;
|
||||
};
|
||||
|
||||
export const CoreEditorExtensions = ({
|
||||
enableHistory,
|
||||
fileConfig: { deleteFile, restoreFile, cancelUploadImage, uploadFile },
|
||||
mentionConfig,
|
||||
fileConfig: { deleteFile, restoreFile, cancelUploadImage, uploadFile },
|
||||
placeholder,
|
||||
tabIndex,
|
||||
}: TArguments) => [
|
||||
@@ -72,11 +70,11 @@ export const CoreEditorExtensions = ({
|
||||
codeBlock: false,
|
||||
horizontalRule: false,
|
||||
blockquote: false,
|
||||
history: false,
|
||||
dropcursor: {
|
||||
color: "rgba(var(--color-text-100))",
|
||||
width: 1,
|
||||
},
|
||||
...(enableHistory ? {} : { history: false }),
|
||||
}),
|
||||
CustomQuoteExtension,
|
||||
DropHandlerExtension(uploadFile),
|
||||
|
||||
@@ -146,11 +146,10 @@ export const MentionList = forwardRef((props: MentionListProps, ref) => {
|
||||
<div className="text-center text-custom-text-400">Loading...</div>
|
||||
) : items.length ? (
|
||||
items.map((item, index) => (
|
||||
<button
|
||||
<div
|
||||
key={item.id}
|
||||
type="button"
|
||||
className={cn(
|
||||
"w-full text-left flex cursor-pointer items-center gap-2 rounded px-1 py-1.5 hover:bg-custom-background-80 text-custom-text-200",
|
||||
"flex cursor-pointer items-center gap-2 rounded px-1 py-1.5 hover:bg-custom-background-80 text-custom-text-200",
|
||||
{
|
||||
"bg-custom-background-80": index === selectedIndex,
|
||||
}
|
||||
@@ -159,7 +158,7 @@ export const MentionList = forwardRef((props: MentionListProps, ref) => {
|
||||
>
|
||||
<Avatar name={item?.title} src={item?.avatar} />
|
||||
<span className="flex-grow truncate">{item.title}</span>
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center text-custom-text-400">No results</div>
|
||||
|
||||
@@ -9,9 +9,6 @@ import {
|
||||
Heading1,
|
||||
Heading2,
|
||||
Heading3,
|
||||
Heading4,
|
||||
Heading5,
|
||||
Heading6,
|
||||
ImageIcon,
|
||||
List,
|
||||
ListOrdered,
|
||||
@@ -32,9 +29,6 @@ import {
|
||||
toggleHeadingOne,
|
||||
toggleHeadingTwo,
|
||||
toggleHeadingThree,
|
||||
toggleHeadingFour,
|
||||
toggleHeadingFive,
|
||||
toggleHeadingSix,
|
||||
} from "@/helpers/editor-commands";
|
||||
// types
|
||||
import { CommandProps, ISlashCommandItem, UploadImage } from "@/types";
|
||||
@@ -97,7 +91,7 @@ const getSuggestionItems =
|
||||
title: "Text",
|
||||
description: "Just start typing with plain text.",
|
||||
searchTerms: ["p", "paragraph"],
|
||||
icon: <CaseSensitive className="size-3.5" />,
|
||||
icon: <CaseSensitive className="h-3.5 w-3.5" />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
if (range) {
|
||||
editor.chain().focus().deleteRange(range).clearNodes().run();
|
||||
@@ -106,91 +100,61 @@ const getSuggestionItems =
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "h1",
|
||||
key: "heading_1",
|
||||
title: "Heading 1",
|
||||
description: "Big section heading.",
|
||||
searchTerms: ["title", "big", "large"],
|
||||
icon: <Heading1 className="size-3.5" />,
|
||||
icon: <Heading1 className="h-3.5 w-3.5" />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
toggleHeadingOne(editor, range);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "h2",
|
||||
key: "heading_2",
|
||||
title: "Heading 2",
|
||||
description: "Medium section heading.",
|
||||
searchTerms: ["subtitle", "medium"],
|
||||
icon: <Heading2 className="size-3.5" />,
|
||||
icon: <Heading2 className="h-3.5 w-3.5" />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
toggleHeadingTwo(editor, range);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "h3",
|
||||
key: "heading_3",
|
||||
title: "Heading 3",
|
||||
description: "Small section heading.",
|
||||
searchTerms: ["subtitle", "small"],
|
||||
icon: <Heading3 className="size-3.5" />,
|
||||
icon: <Heading3 className="h-3.5 w-3.5" />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
toggleHeadingThree(editor, range);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "h4",
|
||||
title: "Heading 4",
|
||||
description: "Small section heading.",
|
||||
searchTerms: ["subtitle", "small"],
|
||||
icon: <Heading4 className="size-3.5" />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
toggleHeadingFour(editor, range);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "h5",
|
||||
title: "Heading 5",
|
||||
description: "Small section heading.",
|
||||
searchTerms: ["subtitle", "small"],
|
||||
icon: <Heading5 className="size-3.5" />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
toggleHeadingFive(editor, range);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "h6",
|
||||
title: "Heading 6",
|
||||
description: "Small section heading.",
|
||||
searchTerms: ["subtitle", "small"],
|
||||
icon: <Heading6 className="size-3.5" />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
toggleHeadingSix(editor, range);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "to-do-list",
|
||||
key: "todo_list",
|
||||
title: "To do",
|
||||
description: "Track tasks with a to-do list.",
|
||||
searchTerms: ["todo", "task", "list", "check", "checkbox"],
|
||||
icon: <ListTodo className="size-3.5" />,
|
||||
icon: <ListTodo className="h-3.5 w-3.5" />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
toggleTaskList(editor, range);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "bulleted-list",
|
||||
key: "bullet_list",
|
||||
title: "Bullet list",
|
||||
description: "Create a simple bullet list.",
|
||||
searchTerms: ["unordered", "point"],
|
||||
icon: <List className="size-3.5" />,
|
||||
icon: <List className="h-3.5 w-3.5" />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
toggleBulletList(editor, range);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "numbered-list",
|
||||
key: "numbered_list",
|
||||
title: "Numbered list",
|
||||
description: "Create a list with numbering.",
|
||||
searchTerms: ["ordered"],
|
||||
icon: <ListOrdered className="size-3.5" />,
|
||||
icon: <ListOrdered className="h-3.5 w-3.5" />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
toggleOrderedList(editor, range);
|
||||
},
|
||||
@@ -200,25 +164,25 @@ const getSuggestionItems =
|
||||
title: "Table",
|
||||
description: "Create a table",
|
||||
searchTerms: ["table", "cell", "db", "data", "tabular"],
|
||||
icon: <Table className="size-3.5" />,
|
||||
icon: <Table className="h-3.5 w-3.5" />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
insertTableCommand(editor, range);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "quote",
|
||||
key: "quote_block",
|
||||
title: "Quote",
|
||||
description: "Capture a quote.",
|
||||
searchTerms: ["blockquote"],
|
||||
icon: <Quote className="size-3.5" />,
|
||||
icon: <Quote className="h-3.5 w-3.5" />,
|
||||
command: ({ editor, range }: CommandProps) => toggleBlockquote(editor, range),
|
||||
},
|
||||
{
|
||||
key: "code",
|
||||
key: "code_block",
|
||||
title: "Code",
|
||||
description: "Capture a code snippet.",
|
||||
searchTerms: ["codeblock"],
|
||||
icon: <Code2 className="size-3.5" />,
|
||||
icon: <Code2 className="h-3.5 w-3.5" />,
|
||||
command: ({ editor, range }: CommandProps) => editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
|
||||
},
|
||||
{
|
||||
@@ -226,7 +190,7 @@ const getSuggestionItems =
|
||||
title: "Image",
|
||||
description: "Upload an image from your computer.",
|
||||
searchTerms: ["img", "photo", "picture", "media"],
|
||||
icon: <ImageIcon className="size-3.5" />,
|
||||
icon: <ImageIcon className="h-3.5 w-3.5" />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
insertImageCommand(editor, uploadFile, null, range);
|
||||
},
|
||||
@@ -236,7 +200,7 @@ const getSuggestionItems =
|
||||
title: "Divider",
|
||||
description: "Visually divide blocks.",
|
||||
searchTerms: ["line", "divider", "horizontal", "rule", "separate"],
|
||||
icon: <MinusSquare className="size-3.5" />,
|
||||
icon: <MinusSquare className="h-3.5 w-3.5" />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
editor.chain().focus().deleteRange(range).setHorizontalRule().run();
|
||||
},
|
||||
|
||||
@@ -87,7 +87,6 @@ export const useDocumentEditor = (props: DocumentEditorProps) => {
|
||||
id,
|
||||
editorProps,
|
||||
editorClassName,
|
||||
enableHistory: false,
|
||||
fileHandler,
|
||||
handleEditorReady,
|
||||
forwardedRef,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Selection } from "@tiptap/pm/state";
|
||||
import { EditorProps } from "@tiptap/pm/view";
|
||||
import { useEditor as useCustomEditor, Editor } from "@tiptap/react";
|
||||
// components
|
||||
import { getEditorMenuItems } from "@/components/menus";
|
||||
import { EditorMenuItemNames, getEditorMenuItems } from "@/components/menus";
|
||||
// extensions
|
||||
import { CoreEditorExtensions } from "@/extensions";
|
||||
// helpers
|
||||
@@ -14,15 +14,7 @@ import { CollaborationProvider } from "@/plane-editor/providers";
|
||||
// props
|
||||
import { CoreEditorProps } from "@/props";
|
||||
// types
|
||||
import {
|
||||
DeleteImage,
|
||||
EditorRefApi,
|
||||
IMentionHighlight,
|
||||
IMentionSuggestion,
|
||||
RestoreImage,
|
||||
TEditorCommands,
|
||||
UploadImage,
|
||||
} from "@/types";
|
||||
import { DeleteImage, EditorRefApi, IMentionHighlight, IMentionSuggestion, RestoreImage, UploadImage } from "@/types";
|
||||
|
||||
export type TFileHandler = {
|
||||
cancel: () => void;
|
||||
@@ -32,44 +24,42 @@ export type TFileHandler = {
|
||||
};
|
||||
|
||||
export interface CustomEditorProps {
|
||||
editorClassName: string;
|
||||
editorProps?: EditorProps;
|
||||
enableHistory: boolean;
|
||||
extensions?: any;
|
||||
fileHandler: TFileHandler;
|
||||
forwardedRef?: MutableRefObject<EditorRefApi | null>;
|
||||
handleEditorReady?: (value: boolean) => void;
|
||||
id?: string;
|
||||
fileHandler: TFileHandler;
|
||||
initialValue?: string;
|
||||
editorClassName: string;
|
||||
// undefined when prop is not passed, null if intentionally passed to stop
|
||||
// swr syncing
|
||||
value?: string | null | undefined;
|
||||
provider?: CollaborationProvider;
|
||||
onChange?: (json: object, html: string) => void;
|
||||
extensions?: any;
|
||||
editorProps?: EditorProps;
|
||||
forwardedRef?: MutableRefObject<EditorRefApi | null>;
|
||||
mentionHandler: {
|
||||
highlights: () => Promise<IMentionHighlight[]>;
|
||||
suggestions?: () => Promise<IMentionSuggestion[]>;
|
||||
};
|
||||
onChange?: (json: object, html: string) => void;
|
||||
handleEditorReady?: (value: boolean) => void;
|
||||
placeholder?: string | ((isFocused: boolean, value: string) => string);
|
||||
provider?: CollaborationProvider;
|
||||
tabIndex?: number;
|
||||
// undefined when prop is not passed, null if intentionally passed to stop
|
||||
// swr syncing
|
||||
value?: string | null | undefined;
|
||||
}
|
||||
|
||||
export const useEditor = ({
|
||||
editorClassName,
|
||||
id = "",
|
||||
editorProps = {},
|
||||
enableHistory,
|
||||
initialValue,
|
||||
editorClassName,
|
||||
value,
|
||||
extensions = [],
|
||||
fileHandler,
|
||||
forwardedRef,
|
||||
handleEditorReady,
|
||||
id = "",
|
||||
initialValue,
|
||||
mentionHandler,
|
||||
onChange,
|
||||
placeholder,
|
||||
provider,
|
||||
forwardedRef,
|
||||
tabIndex,
|
||||
value,
|
||||
handleEditorReady,
|
||||
provider,
|
||||
mentionHandler,
|
||||
placeholder,
|
||||
}: CustomEditorProps) => {
|
||||
const editor = useCustomEditor({
|
||||
editorProps: {
|
||||
@@ -78,17 +68,16 @@ export const useEditor = ({
|
||||
},
|
||||
extensions: [
|
||||
...CoreEditorExtensions({
|
||||
enableHistory,
|
||||
mentionConfig: {
|
||||
mentionSuggestions: mentionHandler.suggestions ?? (() => Promise.resolve<IMentionSuggestion[]>([])),
|
||||
mentionHighlights: mentionHandler.highlights ?? [],
|
||||
},
|
||||
fileConfig: {
|
||||
uploadFile: fileHandler.upload,
|
||||
deleteFile: fileHandler.delete,
|
||||
restoreFile: fileHandler.restore,
|
||||
cancelUploadImage: fileHandler.cancel,
|
||||
},
|
||||
mentionConfig: {
|
||||
mentionSuggestions: mentionHandler.suggestions ?? (() => Promise.resolve<IMentionSuggestion[]>([])),
|
||||
mentionHighlights: mentionHandler.highlights ?? [],
|
||||
},
|
||||
placeholder,
|
||||
tabIndex,
|
||||
}),
|
||||
@@ -155,12 +144,12 @@ export const useEditor = ({
|
||||
insertContentAtSavedSelection(editorRef, content, savedSelection);
|
||||
}
|
||||
},
|
||||
executeMenuItemCommand: (itemKey: TEditorCommands) => {
|
||||
executeMenuItemCommand: (itemName: EditorMenuItemNames) => {
|
||||
const editorItems = getEditorMenuItems(editorRef.current, fileHandler.upload);
|
||||
|
||||
const getEditorMenuItem = (itemKey: TEditorCommands) => editorItems.find((item) => item.key === itemKey);
|
||||
const getEditorMenuItem = (itemName: EditorMenuItemNames) => editorItems.find((item) => item.key === itemName);
|
||||
|
||||
const item = getEditorMenuItem(itemKey);
|
||||
const item = getEditorMenuItem(itemName);
|
||||
if (item) {
|
||||
if (item.key === "image") {
|
||||
item.command(savedSelectionRef.current);
|
||||
@@ -168,13 +157,13 @@ export const useEditor = ({
|
||||
item.command();
|
||||
}
|
||||
} else {
|
||||
console.warn(`No command found for item: ${itemKey}`);
|
||||
console.warn(`No command found for item: ${itemName}`);
|
||||
}
|
||||
},
|
||||
isMenuItemActive: (itemName: TEditorCommands): boolean => {
|
||||
isMenuItemActive: (itemName: EditorMenuItemNames): boolean => {
|
||||
const editorItems = getEditorMenuItems(editorRef.current, fileHandler.upload);
|
||||
|
||||
const getEditorMenuItem = (itemName: TEditorCommands) => editorItems.find((item) => item.key === itemName);
|
||||
const getEditorMenuItem = (itemName: EditorMenuItemNames) => editorItems.find((item) => item.key === itemName);
|
||||
const item = getEditorMenuItem(itemName);
|
||||
return item ? item.isActive() : false;
|
||||
},
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// components
|
||||
import { EditorMenuItemNames } from "@/components/menus";
|
||||
// helpers
|
||||
import { IMarking } from "@/helpers/scroll-to-node";
|
||||
// hooks
|
||||
import { TFileHandler } from "@/hooks/use-editor";
|
||||
// types
|
||||
import { IMentionHighlight, IMentionSuggestion, TEditorCommands } from "@/types";
|
||||
import { IMentionHighlight, IMentionSuggestion } from "@/types";
|
||||
|
||||
export type EditorReadOnlyRefApi = {
|
||||
getMarkDown: () => string;
|
||||
@@ -15,8 +17,8 @@ export type EditorReadOnlyRefApi = {
|
||||
|
||||
export interface EditorRefApi extends EditorReadOnlyRefApi {
|
||||
setEditorValueAtCursorPosition: (content: string) => void;
|
||||
executeMenuItemCommand: (itemKey: TEditorCommands) => void;
|
||||
isMenuItemActive: (itemKey: TEditorCommands) => boolean;
|
||||
executeMenuItemCommand: (itemName: EditorMenuItemNames) => void;
|
||||
isMenuItemActive: (itemName: EditorMenuItemNames) => boolean;
|
||||
onStateChange: (callback: () => void) => () => void;
|
||||
setFocusAtPosition: (position: number) => void;
|
||||
isEditorReadyToDiscard: () => boolean;
|
||||
|
||||
@@ -3,4 +3,3 @@ export * from "./embed";
|
||||
export * from "./image";
|
||||
export * from "./mention-suggestion";
|
||||
export * from "./slash-commands-suggestion";
|
||||
export * from "@/plane-editor/types";
|
||||
|
||||
@@ -1,34 +1,13 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Editor, Range } from "@tiptap/core";
|
||||
|
||||
export type TEditorCommands =
|
||||
| "text"
|
||||
| "h1"
|
||||
| "h2"
|
||||
| "h3"
|
||||
| "h4"
|
||||
| "h5"
|
||||
| "h6"
|
||||
| "bold"
|
||||
| "italic"
|
||||
| "underline"
|
||||
| "strikethrough"
|
||||
| "bulleted-list"
|
||||
| "numbered-list"
|
||||
| "to-do-list"
|
||||
| "quote"
|
||||
| "code"
|
||||
| "table"
|
||||
| "image"
|
||||
| "divider";
|
||||
|
||||
export type CommandProps = {
|
||||
editor: Editor;
|
||||
range: Range;
|
||||
};
|
||||
|
||||
export type ISlashCommandItem = {
|
||||
key: TEditorCommands;
|
||||
key: string;
|
||||
title: string;
|
||||
description: string;
|
||||
searchTerms: string[];
|
||||
|
||||
@@ -270,7 +270,7 @@ module.exports = {
|
||||
"--tw-prose-headings": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-lead": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-links": convertToRGB("--color-primary-100"),
|
||||
"--tw-prose-bold": "inherit",
|
||||
"--tw-prose-bold": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-counters": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-bullets": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-hr": convertToRGB("--color-text-100"),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user