Compare commits
65
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c809538853 | ||
|
|
14ec8215e7 | ||
|
|
34820eec7a | ||
|
|
93e6c3b6e0 | ||
|
|
8f8a97589d | ||
|
|
3a5c77e8a4 | ||
|
|
79fbcaa2b2 | ||
|
|
76983a57e9 | ||
|
|
e9b1151702 | ||
|
|
f4f5e5a0d3 | ||
|
|
f55c135052 | ||
|
|
8924e303da | ||
|
|
c89fe9a313 | ||
|
|
b381331b75 | ||
|
|
ee76cb1dc7 | ||
|
|
daaa04c6ea | ||
|
|
67f2e2fdb2 | ||
|
|
18df1530c1 | ||
|
|
dd3df20319 | ||
|
|
569b592711 | ||
|
|
f75df83ca1 | ||
|
|
8415df4cf3 | ||
|
|
3c684ecab7 | ||
|
|
0b01d3e88d | ||
|
|
889393e1d1 | ||
|
|
6fa45d8723 | ||
|
|
88533933b4 | ||
|
|
fffa8648bb | ||
|
|
1f8f6d1b26 | ||
|
|
cce7bddbcc | ||
|
|
518327e380 | ||
|
|
6bb534dabc | ||
|
|
dc2e293058 | ||
|
|
1adfb4dbe4 | ||
|
|
f2af5f0653 | ||
|
|
e3143ff00b | ||
|
|
7b82d1c62f | ||
|
|
3c2aec2776 | ||
|
|
35e58e9ec7 | ||
|
|
ba9d9fd5eb | ||
|
|
040ee4b256 | ||
|
|
f48bc5a876 | ||
|
|
10e9122c1d | ||
|
|
d5cbe3283b | ||
|
|
ae931f8172 | ||
|
|
a8c6483c60 | ||
|
|
9c761a614f | ||
|
|
adf88a0f13 | ||
|
|
5d2983d027 | ||
|
|
8339daa3ee | ||
|
|
4a9e09a54a | ||
|
|
2c609670c8 | ||
|
|
dfcba4dfc1 | ||
|
|
d0e68cdcfb | ||
|
|
43103a1445 | ||
|
|
1c155f6cbe | ||
|
|
1707f4f282 | ||
|
|
c2c2ad0d7a | ||
|
|
1bf8f82ccb | ||
|
|
3bdd91e577 | ||
|
|
1f9c7a4b67 | ||
|
|
d1828c9496 | ||
|
|
3f87d8b99d | ||
|
|
aba6e603a3 | ||
|
|
b4f2176ffa |
@@ -50,3 +50,6 @@ GUNICORN_WORKERS=2
|
||||
ADMIN_BASE_URL=
|
||||
SPACE_BASE_URL=
|
||||
APP_BASE_URL=
|
||||
|
||||
# Hard delete files after days
|
||||
HARD_DELETE_AFTER_DAYS=
|
||||
@@ -40,6 +40,7 @@ class CycleSerializer(BaseSerializer):
|
||||
"workspace",
|
||||
"project",
|
||||
"owned_by",
|
||||
"deleted_at",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -53,7 +53,6 @@ class IssueSerializer(BaseSerializer):
|
||||
"id",
|
||||
"workspace",
|
||||
"project",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"updated_at",
|
||||
]
|
||||
@@ -338,9 +337,7 @@ class IssueAttachmentSerializer(BaseSerializer):
|
||||
"workspace",
|
||||
"project",
|
||||
"issue",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
@@ -433,3 +430,4 @@ class IssueExpandSerializer(BaseSerializer):
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ class ModuleSerializer(BaseSerializer):
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"deleted_at",
|
||||
]
|
||||
|
||||
def to_representation(self, instance):
|
||||
|
||||
@@ -31,6 +31,7 @@ class ProjectSerializer(BaseSerializer):
|
||||
"updated_at",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"deleted_at",
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
from django.urls import path
|
||||
|
||||
from plane.api.views import (
|
||||
WorkspaceMemberAPIEndpoint,
|
||||
ProjectMemberAPIEndpoint,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/members/",
|
||||
WorkspaceMemberAPIEndpoint.as_view(),
|
||||
"workspaces/<str:slug>/projects/<str:project_id>/members/",
|
||||
ProjectMemberAPIEndpoint.as_view(),
|
||||
name="users",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -25,6 +25,7 @@ from .module import (
|
||||
ModuleArchiveUnarchiveAPIEndpoint,
|
||||
)
|
||||
|
||||
from .member import WorkspaceMemberAPIEndpoint
|
||||
from .member import ProjectMemberAPIEndpoint
|
||||
|
||||
from .inbox import InboxIssueAPIEndpoint
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ from plane.db.models import (
|
||||
Project,
|
||||
IssueAttachment,
|
||||
IssueLink,
|
||||
ProjectMember,
|
||||
)
|
||||
from plane.utils.analytics_plot import burndown_plot
|
||||
|
||||
@@ -363,14 +364,28 @@ 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",
|
||||
@@ -389,6 +404,10 @@ class CycleAPIEndpoint(BaseAPIView):
|
||||
)
|
||||
# Delete the cycle
|
||||
cycle.delete()
|
||||
# Delete the cycle issues
|
||||
CycleIssue.objects.filter(
|
||||
cycle_id=self.kwargs.get("pk"),
|
||||
).delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@@ -646,61 +665,74 @@ class CycleIssueAPIEndpoint(BaseAPIView):
|
||||
workspace__slug=slug, project_id=project_id, pk=cycle_id
|
||||
)
|
||||
|
||||
issues = Issue.objects.filter(
|
||||
pk__in=issues, workspace__slug=slug, project_id=project_id
|
||||
).values_list("id", flat=True)
|
||||
if (
|
||||
cycle.end_date is not None
|
||||
and cycle.end_date < timezone.now().date()
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error": "The Cycle has already been completed so no new issues can be added"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Get all CycleIssues already created
|
||||
cycle_issues = list(CycleIssue.objects.filter(issue_id__in=issues))
|
||||
update_cycle_issue_activity = []
|
||||
record_to_create = []
|
||||
records_to_update = []
|
||||
|
||||
for issue in issues:
|
||||
cycle_issue = [
|
||||
cycle_issue
|
||||
for cycle_issue in cycle_issues
|
||||
if str(cycle_issue.issue_id) in issues
|
||||
]
|
||||
# Update only when cycle changes
|
||||
if len(cycle_issue):
|
||||
if cycle_issue[0].cycle_id != cycle_id:
|
||||
update_cycle_issue_activity.append(
|
||||
{
|
||||
"old_cycle_id": str(cycle_issue[0].cycle_id),
|
||||
"new_cycle_id": str(cycle_id),
|
||||
"issue_id": str(cycle_issue[0].issue_id),
|
||||
}
|
||||
)
|
||||
cycle_issue[0].cycle_id = cycle_id
|
||||
records_to_update.append(cycle_issue[0])
|
||||
else:
|
||||
record_to_create.append(
|
||||
CycleIssue(
|
||||
project_id=project_id,
|
||||
workspace=cycle.workspace,
|
||||
created_by=request.user,
|
||||
updated_by=request.user,
|
||||
cycle=cycle,
|
||||
issue_id=issue,
|
||||
)
|
||||
)
|
||||
|
||||
CycleIssue.objects.bulk_create(
|
||||
record_to_create,
|
||||
batch_size=10,
|
||||
ignore_conflicts=True,
|
||||
cycle_issues = list(
|
||||
CycleIssue.objects.filter(
|
||||
~Q(cycle_id=cycle_id), issue_id__in=issues
|
||||
)
|
||||
)
|
||||
CycleIssue.objects.bulk_update(
|
||||
records_to_update,
|
||||
["cycle"],
|
||||
|
||||
existing_issues = [
|
||||
str(cycle_issue.issue_id)
|
||||
for cycle_issue in cycle_issues
|
||||
if str(cycle_issue.issue_id) in issues
|
||||
]
|
||||
new_issues = list(set(issues) - set(existing_issues))
|
||||
|
||||
# New issues to create
|
||||
created_records = CycleIssue.objects.bulk_create(
|
||||
[
|
||||
CycleIssue(
|
||||
project_id=project_id,
|
||||
workspace_id=cycle.workspace_id,
|
||||
cycle_id=cycle_id,
|
||||
issue_id=issue,
|
||||
)
|
||||
for issue in new_issues
|
||||
],
|
||||
ignore_conflicts=True,
|
||||
batch_size=10,
|
||||
)
|
||||
|
||||
# Updated Issues
|
||||
updated_records = []
|
||||
update_cycle_issue_activity = []
|
||||
# Iterate over each cycle_issue in cycle_issues
|
||||
for cycle_issue in cycle_issues:
|
||||
old_cycle_id = cycle_issue.cycle_id
|
||||
# Update the cycle_issue's cycle_id
|
||||
cycle_issue.cycle_id = cycle_id
|
||||
# Add the modified cycle_issue to the records_to_update list
|
||||
updated_records.append(cycle_issue)
|
||||
# Record the update activity
|
||||
update_cycle_issue_activity.append(
|
||||
{
|
||||
"old_cycle_id": str(old_cycle_id),
|
||||
"new_cycle_id": str(cycle_id),
|
||||
"issue_id": str(cycle_issue.issue_id),
|
||||
}
|
||||
)
|
||||
|
||||
# Update the cycle issues
|
||||
CycleIssue.objects.bulk_update(
|
||||
updated_records, ["cycle_id"], batch_size=100
|
||||
)
|
||||
|
||||
# Capture Issue Activity
|
||||
issue_activity.delay(
|
||||
type="cycle.activity.created",
|
||||
requested_data=json.dumps({"cycles_list": str(issues)}),
|
||||
requested_data=json.dumps({"cycles_list": issues}),
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=None,
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
@@ -708,13 +740,14 @@ class CycleIssueAPIEndpoint(BaseAPIView):
|
||||
{
|
||||
"updated_cycle_issues": update_cycle_issue_activity,
|
||||
"created_cycle_issues": serializers.serialize(
|
||||
"json", record_to_create
|
||||
"json", created_records
|
||||
),
|
||||
}
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
# Return all Cycle Issues
|
||||
return Response(
|
||||
CycleIssueSerializer(self.get_queryset(), many=True).data,
|
||||
|
||||
@@ -390,29 +390,26 @@ 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.objects.filter(
|
||||
issue = Issue.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, pk=issue_id
|
||||
).delete()
|
||||
).first()
|
||||
if issue.created_by_id != request.user.id and (
|
||||
not ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role=20,
|
||||
project_id=project_id,
|
||||
is_active=True,
|
||||
).exists()
|
||||
):
|
||||
return Response(
|
||||
{"error": "Only admin or creator can delete the issue"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
issue.delete()
|
||||
|
||||
inbox_issue.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -151,6 +151,25 @@ class IssueAPIEndpoint(BaseAPIView):
|
||||
).distinct()
|
||||
|
||||
def get(self, request, slug, project_id, pk=None):
|
||||
external_id = request.GET.get("external_id")
|
||||
external_source = request.GET.get("external_source")
|
||||
|
||||
if external_id and external_source:
|
||||
issue = Issue.objects.get(
|
||||
external_id=external_id,
|
||||
external_source=external_source,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
return Response(
|
||||
IssueSerializer(
|
||||
issue,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
).data,
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
if pk:
|
||||
issue = Issue.issue_objects.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(
|
||||
@@ -310,10 +329,16 @@ 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"])
|
||||
|
||||
issue = Issue.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
pk=serializer.data["id"],
|
||||
).first()
|
||||
issue.created_at = request.data.get("created_at", timezone.now())
|
||||
issue.created_by_id = request.data.get(
|
||||
"created_by", request.user.id
|
||||
)
|
||||
issue.save(update_fields=["created_at", "created_by"])
|
||||
|
||||
# Track the issue
|
||||
issue_activity.delay(
|
||||
@@ -386,6 +411,19 @@ 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
|
||||
)
|
||||
@@ -594,14 +632,20 @@ class IssueLinkAPIEndpoint(BaseAPIView):
|
||||
project_id=project_id,
|
||||
issue_id=issue_id,
|
||||
)
|
||||
|
||||
link = IssueLink.objects.get(pk=serializer.data["id"])
|
||||
link.created_by_id = request.data.get(
|
||||
"created_by", request.user.id
|
||||
)
|
||||
link.save(update_fields=["created_by"])
|
||||
issue_activity.delay(
|
||||
type="link.activity.created",
|
||||
requested_data=json.dumps(
|
||||
serializer.data, cls=DjangoJSONEncoder
|
||||
),
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=str(self.kwargs.get("issue_id")),
|
||||
project_id=str(self.kwargs.get("project_id")),
|
||||
actor_id=str(link.created_by_id),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
)
|
||||
@@ -755,12 +799,24 @@ class IssueCommentAPIEndpoint(BaseAPIView):
|
||||
issue_id=issue_id,
|
||||
actor=request.user,
|
||||
)
|
||||
issue_comment = IssueComment.objects.get(
|
||||
pk=serializer.data.get("id")
|
||||
)
|
||||
# Update the created_at and the created_by and save the comment
|
||||
issue_comment.created_at = request.data.get(
|
||||
"created_at", timezone.now()
|
||||
)
|
||||
issue_comment.created_by_id = request.data.get(
|
||||
"created_by", request.user.id
|
||||
)
|
||||
issue_comment.save(update_fields=["created_at", "created_by"])
|
||||
|
||||
issue_activity.delay(
|
||||
type="comment.activity.created",
|
||||
requested_data=json.dumps(
|
||||
serializer.data, cls=DjangoJSONEncoder
|
||||
),
|
||||
actor_id=str(self.request.user.id),
|
||||
actor_id=str(issue_comment.created_by_id),
|
||||
issue_id=str(self.kwargs.get("issue_id")),
|
||||
project_id=str(self.kwargs.get("project_id")),
|
||||
current_instance=None,
|
||||
|
||||
@@ -21,11 +21,19 @@ from plane.db.models import (
|
||||
ProjectMember,
|
||||
)
|
||||
|
||||
from plane.app.permissions import (
|
||||
ProjectMemberPermission,
|
||||
)
|
||||
|
||||
|
||||
# API endpoint to get and insert users inside the workspace
|
||||
class WorkspaceMemberAPIEndpoint(BaseAPIView):
|
||||
class ProjectMemberAPIEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
ProjectMemberPermission,
|
||||
]
|
||||
|
||||
# Get all the users that are present inside the workspace
|
||||
def get(self, request, slug):
|
||||
def get(self, request, slug, project_id):
|
||||
# Check if the workspace exists
|
||||
if not Workspace.objects.filter(slug=slug).exists():
|
||||
return Response(
|
||||
@@ -34,14 +42,14 @@ class WorkspaceMemberAPIEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
# Get the workspace members that are present inside the workspace
|
||||
workspace_members = WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug
|
||||
)
|
||||
project_members = ProjectMember.objects.filter(
|
||||
project_id=project_id, workspace__slug=slug
|
||||
).values_list("member_id", flat=True)
|
||||
|
||||
# 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)
|
||||
id__in=project_members,
|
||||
),
|
||||
many=True,
|
||||
).data
|
||||
@@ -49,14 +57,13 @@ class WorkspaceMemberAPIEndpoint(BaseAPIView):
|
||||
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):
|
||||
def post(self, request, slug, project_id):
|
||||
# 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(
|
||||
{
|
||||
@@ -76,9 +83,7 @@ class WorkspaceMemberAPIEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
workspace = Workspace.objects.filter(slug=slug).first()
|
||||
project = Project.objects.filter(
|
||||
pk=request.data.get("project_id")
|
||||
).first()
|
||||
project = Project.objects.filter(pk=project_id).first()
|
||||
|
||||
if not all([workspace, project]):
|
||||
return Response(
|
||||
@@ -145,3 +150,4 @@ class WorkspaceMemberAPIEndpoint(BaseAPIView):
|
||||
user_data = UserLiteSerializer(user).data
|
||||
|
||||
return Response(user_data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ from plane.db.models import (
|
||||
ModuleIssue,
|
||||
ModuleLink,
|
||||
Project,
|
||||
ProjectMember,
|
||||
)
|
||||
|
||||
from .base import BaseAPIView
|
||||
@@ -265,6 +266,20 @@ 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
|
||||
@@ -286,6 +301,10 @@ class ModuleAPIEndpoint(BaseAPIView):
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
)
|
||||
module.delete()
|
||||
# Delete the module issues
|
||||
ModuleIssue.objects.filter(
|
||||
module=pk,
|
||||
).delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
|
||||
@@ -121,3 +121,5 @@ from .exporter import ExporterHistorySerializer
|
||||
from .webhook import WebhookSerializer, WebhookLogSerializer
|
||||
|
||||
from .dashboard import DashboardSerializer, WidgetSerializer
|
||||
|
||||
from .favorite import UserFavoriteSerializer
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from plane.db.models import (
|
||||
UserFavorite,
|
||||
Cycle,
|
||||
Module,
|
||||
Issue,
|
||||
IssueView,
|
||||
Page,
|
||||
Project,
|
||||
)
|
||||
|
||||
|
||||
class ProjectFavoriteLiteSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Project
|
||||
fields = ["id", "name", "logo_props"]
|
||||
|
||||
|
||||
class PageFavoriteLiteSerializer(serializers.ModelSerializer):
|
||||
project_id = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Page
|
||||
fields = ["id", "name", "logo_props", "project_id"]
|
||||
|
||||
def get_project_id(self, obj):
|
||||
project = (
|
||||
obj.projects.first()
|
||||
) # This gets the first project related to the Page
|
||||
return project.id if project else None
|
||||
|
||||
|
||||
class CycleFavoriteLiteSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Cycle
|
||||
fields = ["id", "name", "logo_props", "project_id"]
|
||||
|
||||
|
||||
class ModuleFavoriteLiteSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Module
|
||||
fields = ["id", "name", "logo_props", "project_id"]
|
||||
|
||||
|
||||
class ViewFavoriteSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = IssueView
|
||||
fields = ["id", "name", "logo_props", "project_id"]
|
||||
|
||||
|
||||
def get_entity_model_and_serializer(entity_type):
|
||||
entity_map = {
|
||||
"cycle": (Cycle, CycleFavoriteLiteSerializer),
|
||||
"issue": (Issue, None),
|
||||
"module": (Module, ModuleFavoriteLiteSerializer),
|
||||
"view": (IssueView, ViewFavoriteSerializer),
|
||||
"page": (Page, PageFavoriteLiteSerializer),
|
||||
"project": (Project, ProjectFavoriteLiteSerializer),
|
||||
"folder": (None, None),
|
||||
}
|
||||
return entity_map.get(entity_type, (None, None))
|
||||
|
||||
|
||||
class UserFavoriteSerializer(serializers.ModelSerializer):
|
||||
entity_data = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = UserFavorite
|
||||
fields = [
|
||||
"id",
|
||||
"entity_type",
|
||||
"entity_identifier",
|
||||
"entity_data",
|
||||
"name",
|
||||
"is_folder",
|
||||
"sequence",
|
||||
"parent",
|
||||
"workspace_id",
|
||||
"project_id",
|
||||
]
|
||||
read_only_fields = ["workspace", "created_by", "updated_by"]
|
||||
|
||||
def get_entity_data(self, obj):
|
||||
entity_type = obj.entity_type
|
||||
entity_identifier = obj.entity_identifier
|
||||
|
||||
entity_model, entity_serializer = get_entity_model_and_serializer(
|
||||
entity_type
|
||||
)
|
||||
if entity_model and entity_serializer:
|
||||
try:
|
||||
entity = entity_model.objects.get(pk=entity_identifier)
|
||||
return entity_serializer(entity).data
|
||||
except entity_model.DoesNotExist:
|
||||
return None
|
||||
return None
|
||||
@@ -533,6 +533,7 @@ class IssueReactionSerializer(BaseSerializer):
|
||||
"project",
|
||||
"issue",
|
||||
"actor",
|
||||
"deleted_at"
|
||||
]
|
||||
|
||||
|
||||
@@ -551,7 +552,7 @@ class CommentReactionSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = CommentReaction
|
||||
fields = "__all__"
|
||||
read_only_fields = ["workspace", "project", "comment", "actor"]
|
||||
read_only_fields = ["workspace", "project", "comment", "actor", "deleted_at"]
|
||||
|
||||
|
||||
class IssueVoteSerializer(BaseSerializer):
|
||||
|
||||
@@ -39,6 +39,7 @@ class ModuleWriteSerializer(BaseSerializer):
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"archived_at",
|
||||
"deleted_at",
|
||||
]
|
||||
|
||||
def to_representation(self, instance):
|
||||
|
||||
@@ -28,6 +28,7 @@ class ProjectSerializer(BaseSerializer):
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"deleted_at",
|
||||
]
|
||||
|
||||
def create(self, validated_data):
|
||||
|
||||
@@ -25,6 +25,8 @@ from plane.app.views import (
|
||||
ExportWorkspaceUserActivityEndpoint,
|
||||
WorkspaceModulesEndpoint,
|
||||
WorkspaceCyclesEndpoint,
|
||||
WorkspaceFavoriteEndpoint,
|
||||
WorkspaceFavoriteGroupEndpoint,
|
||||
)
|
||||
|
||||
|
||||
@@ -237,4 +239,19 @@ urlpatterns = [
|
||||
WorkspaceCyclesEndpoint.as_view(),
|
||||
name="workspace-cycles",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/user-favorites/",
|
||||
WorkspaceFavoriteEndpoint.as_view(),
|
||||
name="workspace-user-favorites",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/user-favorites/<uuid:favorite_id>/",
|
||||
WorkspaceFavoriteEndpoint.as_view(),
|
||||
name="workspace-user-favorites",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/user-favorites/<uuid:favorite_id>/group/",
|
||||
WorkspaceFavoriteGroupEndpoint.as_view(),
|
||||
name="workspace-user-favorites-groups",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -40,6 +40,11 @@ from .workspace.base import (
|
||||
ExportWorkspaceUserActivityEndpoint,
|
||||
)
|
||||
|
||||
from .workspace.favorite import (
|
||||
WorkspaceFavoriteEndpoint,
|
||||
WorkspaceFavoriteGroupEndpoint,
|
||||
)
|
||||
|
||||
from .workspace.member import (
|
||||
WorkSpaceMemberViewSet,
|
||||
TeamMemberViewSet,
|
||||
|
||||
@@ -14,21 +14,18 @@ from django.db.models import (
|
||||
UUIDField,
|
||||
Value,
|
||||
When,
|
||||
Subquery,
|
||||
Sum,
|
||||
FloatField,
|
||||
)
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.db.models.functions import Coalesce, Cast
|
||||
from django.utils import timezone
|
||||
|
||||
# Third party imports
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from plane.app.permissions import ProjectEntityPermission
|
||||
from plane.db.models import (
|
||||
Cycle,
|
||||
UserFavorite,
|
||||
Issue,
|
||||
Label,
|
||||
User,
|
||||
)
|
||||
from plane.db.models import Cycle, UserFavorite, Issue, Label, User, Project
|
||||
from plane.utils.analytics_plot import burndown_plot
|
||||
|
||||
# Module imports
|
||||
@@ -49,6 +46,89 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
)
|
||||
backlog_estimate_point = (
|
||||
Issue.issue_objects.filter(
|
||||
estimate_point__estimate__type="points",
|
||||
state__group="backlog",
|
||||
issue_cycle__cycle_id=OuterRef("pk"),
|
||||
)
|
||||
.values("issue_cycle__cycle_id")
|
||||
.annotate(
|
||||
backlog_estimate_point=Sum(
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.values("backlog_estimate_point")[:1]
|
||||
)
|
||||
unstarted_estimate_point = (
|
||||
Issue.issue_objects.filter(
|
||||
estimate_point__estimate__type="points",
|
||||
state__group="unstarted",
|
||||
issue_cycle__cycle_id=OuterRef("pk"),
|
||||
)
|
||||
.values("issue_cycle__cycle_id")
|
||||
.annotate(
|
||||
unstarted_estimate_point=Sum(
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.values("unstarted_estimate_point")[:1]
|
||||
)
|
||||
started_estimate_point = (
|
||||
Issue.issue_objects.filter(
|
||||
estimate_point__estimate__type="points",
|
||||
state__group="started",
|
||||
issue_cycle__cycle_id=OuterRef("pk"),
|
||||
)
|
||||
.values("issue_cycle__cycle_id")
|
||||
.annotate(
|
||||
started_estimate_point=Sum(
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.values("started_estimate_point")[:1]
|
||||
)
|
||||
cancelled_estimate_point = (
|
||||
Issue.issue_objects.filter(
|
||||
estimate_point__estimate__type="points",
|
||||
state__group="cancelled",
|
||||
issue_cycle__cycle_id=OuterRef("pk"),
|
||||
)
|
||||
.values("issue_cycle__cycle_id")
|
||||
.annotate(
|
||||
cancelled_estimate_point=Sum(
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.values("cancelled_estimate_point")[:1]
|
||||
)
|
||||
completed_estimate_point = (
|
||||
Issue.issue_objects.filter(
|
||||
estimate_point__estimate__type="points",
|
||||
state__group="completed",
|
||||
issue_cycle__cycle_id=OuterRef("pk"),
|
||||
)
|
||||
.values("issue_cycle__cycle_id")
|
||||
.annotate(
|
||||
completed_estimate_points=Sum(
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.values("completed_estimate_points")[:1]
|
||||
)
|
||||
total_estimate_point = (
|
||||
Issue.issue_objects.filter(
|
||||
estimate_point__estimate__type="points",
|
||||
issue_cycle__cycle_id=OuterRef("pk"),
|
||||
)
|
||||
.values("issue_cycle__cycle_id")
|
||||
.annotate(
|
||||
total_estimate_points=Sum(
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.values("total_estimate_points")[:1]
|
||||
)
|
||||
return (
|
||||
Cycle.objects.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
@@ -172,6 +252,42 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
backlog_estimate_points=Coalesce(
|
||||
Subquery(backlog_estimate_point),
|
||||
Value(0, output_field=FloatField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
unstarted_estimate_points=Coalesce(
|
||||
Subquery(unstarted_estimate_point),
|
||||
Value(0, output_field=FloatField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
started_estimate_points=Coalesce(
|
||||
Subquery(started_estimate_point),
|
||||
Value(0, output_field=FloatField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
cancelled_estimate_points=Coalesce(
|
||||
Subquery(cancelled_estimate_point),
|
||||
Value(0, output_field=FloatField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
completed_estimate_points=Coalesce(
|
||||
Subquery(completed_estimate_point),
|
||||
Value(0, output_field=FloatField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
total_estimate_points=Coalesce(
|
||||
Subquery(total_estimate_point),
|
||||
Value(0, output_field=FloatField()),
|
||||
),
|
||||
)
|
||||
.order_by("-is_favorite", "name")
|
||||
.distinct()
|
||||
)
|
||||
@@ -179,17 +295,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
def get(self, request, slug, project_id, pk=None):
|
||||
if pk is None:
|
||||
queryset = (
|
||||
self.get_queryset()
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"issue_cycle",
|
||||
filter=Q(
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.values(
|
||||
self.get_queryset().values(
|
||||
# necessary fields
|
||||
"id",
|
||||
"workspace_id",
|
||||
@@ -255,7 +361,10 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
"external_id",
|
||||
"progress_snapshot",
|
||||
"sub_issues",
|
||||
"logo_props",
|
||||
# meta fields
|
||||
"completed_estimate_points",
|
||||
"total_estimate_points",
|
||||
"is_favorite",
|
||||
"total_issues",
|
||||
"cancelled_issues",
|
||||
@@ -265,17 +374,114 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
"backlog_issues",
|
||||
"assignee_ids",
|
||||
"status",
|
||||
"created_by",
|
||||
"archived_at",
|
||||
)
|
||||
.first()
|
||||
)
|
||||
queryset = queryset.first()
|
||||
|
||||
if data is None:
|
||||
return Response(
|
||||
{"error": "Cycle does not exist"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
estimate_type = Project.objects.filter(
|
||||
workspace__slug=slug,
|
||||
pk=project_id,
|
||||
estimate__isnull=False,
|
||||
estimate__type="points",
|
||||
).exists()
|
||||
|
||||
data["estimate_distribution"] = {}
|
||||
if estimate_type:
|
||||
assignee_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
issue_cycle__cycle_id=pk,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
.annotate(display_name=F("assignees__display_name"))
|
||||
.annotate(assignee_id=F("assignees__id"))
|
||||
.annotate(avatar=F("assignees__avatar"))
|
||||
.values("display_name", "assignee_id", "avatar")
|
||||
.annotate(
|
||||
total_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
filter=Q(
|
||||
completed_at__isnull=False,
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
pending_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
filter=Q(
|
||||
completed_at__isnull=True,
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.order_by("display_name")
|
||||
)
|
||||
|
||||
label_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
issue_cycle__cycle_id=pk,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
.annotate(label_name=F("labels__name"))
|
||||
.annotate(color=F("labels__color"))
|
||||
.annotate(label_id=F("labels__id"))
|
||||
.values("label_name", "color", "label_id")
|
||||
.annotate(
|
||||
total_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
filter=Q(
|
||||
completed_at__isnull=False,
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
pending_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
filter=Q(
|
||||
completed_at__isnull=True,
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.order_by("label_name")
|
||||
)
|
||||
data["estimate_distribution"] = {
|
||||
"assignees": assignee_distribution,
|
||||
"labels": label_distribution,
|
||||
"completion_chart": {},
|
||||
}
|
||||
|
||||
if data["start_date"] and data["end_date"]:
|
||||
data["estimate_distribution"]["completion_chart"] = (
|
||||
burndown_plot(
|
||||
queryset=queryset,
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
plot_type="points",
|
||||
cycle_id=pk,
|
||||
)
|
||||
)
|
||||
|
||||
# Assignee Distribution
|
||||
assignee_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
@@ -298,7 +504,10 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"id",
|
||||
filter=Q(archived_at__isnull=True, is_draft=False),
|
||||
filter=Q(
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
@@ -338,7 +547,10 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"id",
|
||||
filter=Q(archived_at__isnull=True, is_draft=False),
|
||||
filter=Q(
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
|
||||
@@ -47,6 +47,7 @@ from plane.db.models import (
|
||||
Label,
|
||||
User,
|
||||
Project,
|
||||
ProjectMember,
|
||||
)
|
||||
from plane.utils.analytics_plot import burndown_plot
|
||||
|
||||
@@ -384,7 +385,7 @@ class CycleViewSet(BaseViewSet):
|
||||
data[0]["estimate_distribution"] = {}
|
||||
if estimate_type:
|
||||
assignee_distribution = (
|
||||
Issue.objects.filter(
|
||||
Issue.issue_objects.filter(
|
||||
issue_cycle__cycle_id=data[0]["id"],
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
@@ -422,7 +423,7 @@ class CycleViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
label_distribution = (
|
||||
Issue.objects.filter(
|
||||
Issue.issue_objects.filter(
|
||||
issue_cycle__cycle_id=data[0]["id"],
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
@@ -476,7 +477,7 @@ class CycleViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
assignee_distribution = (
|
||||
Issue.objects.filter(
|
||||
Issue.issue_objects.filter(
|
||||
issue_cycle__cycle_id=data[0]["id"],
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
@@ -518,7 +519,7 @@ class CycleViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
label_distribution = (
|
||||
Issue.objects.filter(
|
||||
Issue.issue_objects.filter(
|
||||
issue_cycle__cycle_id=data[0]["id"],
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
@@ -833,7 +834,7 @@ class CycleViewSet(BaseViewSet):
|
||||
data["estimate_distribution"] = {}
|
||||
if estimate_type:
|
||||
assignee_distribution = (
|
||||
Issue.objects.filter(
|
||||
Issue.issue_objects.filter(
|
||||
issue_cycle__cycle_id=pk,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
@@ -871,7 +872,7 @@ class CycleViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
label_distribution = (
|
||||
Issue.objects.filter(
|
||||
Issue.issue_objects.filter(
|
||||
issue_cycle__cycle_id=pk,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
@@ -926,7 +927,7 @@ class CycleViewSet(BaseViewSet):
|
||||
|
||||
# Assignee Distribution
|
||||
assignee_distribution = (
|
||||
Issue.objects.filter(
|
||||
Issue.issue_objects.filter(
|
||||
issue_cycle__cycle_id=pk,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
@@ -977,7 +978,7 @@ class CycleViewSet(BaseViewSet):
|
||||
|
||||
# Label Distribution
|
||||
label_distribution = (
|
||||
Issue.objects.filter(
|
||||
Issue.issue_objects.filter(
|
||||
issue_cycle__cycle_id=pk,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
@@ -1039,14 +1040,28 @@ 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",
|
||||
@@ -1067,6 +1082,10 @@ class CycleViewSet(BaseViewSet):
|
||||
)
|
||||
# Delete the cycle
|
||||
cycle.delete()
|
||||
# Delete the cycle issues
|
||||
CycleIssue.objects.filter(
|
||||
cycle_id=self.kwargs.get("pk"),
|
||||
).delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@@ -1135,7 +1154,7 @@ class CycleFavoriteViewSet(BaseViewSet):
|
||||
workspace__slug=slug,
|
||||
entity_identifier=cycle_id,
|
||||
)
|
||||
cycle_favorite.delete()
|
||||
cycle_favorite.delete(soft=False)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ from rest_framework.response import Response
|
||||
from plane.app.permissions import (
|
||||
ProjectEntityPermission,
|
||||
)
|
||||
|
||||
# Module imports
|
||||
from .. import BaseViewSet
|
||||
from plane.app.serializers import (
|
||||
@@ -45,7 +46,6 @@ from plane.utils.paginator import (
|
||||
SubGroupedOffsetPaginator,
|
||||
)
|
||||
|
||||
# Module imports
|
||||
|
||||
class CycleIssueViewSet(BaseViewSet):
|
||||
serializer_class = CycleIssueSerializer
|
||||
@@ -334,7 +334,7 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
return Response({"message": "success"}, status=status.HTTP_201_CREATED)
|
||||
|
||||
def destroy(self, request, slug, project_id, cycle_id, issue_id):
|
||||
cycle_issue = CycleIssue.objects.get(
|
||||
cycle_issue = CycleIssue.objects.filter(
|
||||
issue_id=issue_id,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
|
||||
@@ -553,28 +553,27 @@ 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.objects.filter(
|
||||
issue = Issue.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, pk=issue_id
|
||||
).delete()
|
||||
).first()
|
||||
if issue.created_by_id != request.user.id and (
|
||||
not ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role=20,
|
||||
project_id=project_id,
|
||||
is_active=True,
|
||||
).exists()
|
||||
):
|
||||
return Response(
|
||||
{"error": "Only admin or creator can delete the issue"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
issue.delete()
|
||||
|
||||
inbox_issue.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -66,6 +66,7 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.filter(deleted_at__isnull=True)
|
||||
.filter(archived_at__isnull=False)
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
|
||||
@@ -14,7 +14,7 @@ from rest_framework.parsers import MultiPartParser, FormParser
|
||||
from .. import BaseAPIView
|
||||
from plane.app.serializers import IssueAttachmentSerializer
|
||||
from plane.app.permissions import ProjectEntityPermission
|
||||
from plane.db.models import IssueAttachment
|
||||
from plane.db.models import IssueAttachment, ProjectMember
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
|
||||
|
||||
@@ -49,6 +49,19 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
||||
|
||||
def delete(self, request, slug, project_id, issue_id, pk):
|
||||
issue_attachment = IssueAttachment.objects.get(pk=pk)
|
||||
if issue_attachment.created_by_id != request.user.id and (
|
||||
not ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role=20,
|
||||
project_id=project_id,
|
||||
is_active=True,
|
||||
).exists()
|
||||
):
|
||||
return Response(
|
||||
{"error": "Only admin or creator can delete the attachment"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
issue_attachment.asset.delete(save=False)
|
||||
issue_attachment.delete()
|
||||
issue_activity.delay(
|
||||
|
||||
@@ -44,6 +44,7 @@ from plane.db.models import (
|
||||
IssueReaction,
|
||||
IssueSubscriber,
|
||||
Project,
|
||||
ProjectMember,
|
||||
)
|
||||
from plane.utils.grouper import (
|
||||
issue_group_values,
|
||||
@@ -165,6 +166,7 @@ class IssueListEndpoint(BaseAPIView):
|
||||
"link_count",
|
||||
"is_draft",
|
||||
"archived_at",
|
||||
"deleted_at",
|
||||
)
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
issues = user_timezone_converter(
|
||||
@@ -399,6 +401,7 @@ class IssueViewSet(BaseViewSet):
|
||||
"link_count",
|
||||
"is_draft",
|
||||
"archived_at",
|
||||
"deleted_at",
|
||||
)
|
||||
.first()
|
||||
)
|
||||
@@ -549,6 +552,20 @@ 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",
|
||||
@@ -602,6 +619,19 @@ class BulkDeleteIssuesEndpoint(BaseAPIView):
|
||||
]
|
||||
|
||||
def delete(self, request, slug, project_id):
|
||||
if ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role__in=[15, 10, 5],
|
||||
project_id=project_id,
|
||||
is_active=True,
|
||||
).exists():
|
||||
|
||||
return Response(
|
||||
{"error": "Only admin can perform this action"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
issue_ids = request.data.get("issue_ids", [])
|
||||
|
||||
if not len(issue_ids):
|
||||
|
||||
@@ -40,6 +40,7 @@ from plane.db.models import (
|
||||
IssueReaction,
|
||||
IssueSubscriber,
|
||||
Project,
|
||||
ProjectMember,
|
||||
)
|
||||
from plane.utils.grouper import (
|
||||
issue_group_values,
|
||||
@@ -67,6 +68,7 @@ class IssueDraftViewSet(BaseViewSet):
|
||||
Issue.objects.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(is_draft=True)
|
||||
.filter(deleted_at__isnull=True)
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
@@ -380,6 +382,19 @@ 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",
|
||||
|
||||
@@ -12,7 +12,8 @@ from django.db.models import (
|
||||
Subquery,
|
||||
UUIDField,
|
||||
Value,
|
||||
Sum
|
||||
Sum,
|
||||
FloatField,
|
||||
)
|
||||
from django.db.models.functions import Coalesce, Cast
|
||||
from django.utils import timezone
|
||||
@@ -44,8 +45,8 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
def get_queryset(self):
|
||||
favorite_subquery = UserFavorite.objects.filter(
|
||||
user=self.request.user,
|
||||
entity_identifier=OuterRef("pk"),
|
||||
entity_type="module",
|
||||
entity_identifier=OuterRef("pk"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
)
|
||||
@@ -102,8 +103,93 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
.annotate(cnt=Count("pk"))
|
||||
.values("cnt")
|
||||
)
|
||||
completed_estimate_point = (
|
||||
Issue.issue_objects.filter(
|
||||
estimate_point__estimate__type="points",
|
||||
state__group="completed",
|
||||
issue_module__module_id=OuterRef("pk"),
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
completed_estimate_points=Sum(
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.values("completed_estimate_points")[:1]
|
||||
)
|
||||
|
||||
total_estimate_point = (
|
||||
Issue.issue_objects.filter(
|
||||
estimate_point__estimate__type="points",
|
||||
issue_module__module_id=OuterRef("pk"),
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
total_estimate_points=Sum(
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.values("total_estimate_points")[:1]
|
||||
)
|
||||
backlog_estimate_point = (
|
||||
Issue.issue_objects.filter(
|
||||
estimate_point__estimate__type="points",
|
||||
state__group="backlog",
|
||||
issue_module__module_id=OuterRef("pk"),
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
backlog_estimate_point=Sum(
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.values("backlog_estimate_point")[:1]
|
||||
)
|
||||
unstarted_estimate_point = (
|
||||
Issue.issue_objects.filter(
|
||||
estimate_point__estimate__type="points",
|
||||
state__group="unstarted",
|
||||
issue_module__module_id=OuterRef("pk"),
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
unstarted_estimate_point=Sum(
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.values("unstarted_estimate_point")[:1]
|
||||
)
|
||||
started_estimate_point = (
|
||||
Issue.issue_objects.filter(
|
||||
estimate_point__estimate__type="points",
|
||||
state__group="started",
|
||||
issue_module__module_id=OuterRef("pk"),
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
started_estimate_point=Sum(
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.values("started_estimate_point")[:1]
|
||||
)
|
||||
cancelled_estimate_point = (
|
||||
Issue.issue_objects.filter(
|
||||
estimate_point__estimate__type="points",
|
||||
state__group="cancelled",
|
||||
issue_module__module_id=OuterRef("pk"),
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
cancelled_estimate_point=Sum(
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.values("cancelled_estimate_point")[:1]
|
||||
)
|
||||
return (
|
||||
Module.objects.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(archived_at__isnull=False)
|
||||
.annotate(is_favorite=Exists(favorite_subquery))
|
||||
.select_related("workspace", "project", "lead")
|
||||
@@ -152,6 +238,42 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
Value(0, output_field=IntegerField()),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
backlog_estimate_points=Coalesce(
|
||||
Subquery(backlog_estimate_point),
|
||||
Value(0, output_field=FloatField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
unstarted_estimate_points=Coalesce(
|
||||
Subquery(unstarted_estimate_point),
|
||||
Value(0, output_field=FloatField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
started_estimate_points=Coalesce(
|
||||
Subquery(started_estimate_point),
|
||||
Value(0, output_field=FloatField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
cancelled_estimate_points=Coalesce(
|
||||
Subquery(cancelled_estimate_point),
|
||||
Value(0, output_field=FloatField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
completed_estimate_points=Coalesce(
|
||||
Subquery(completed_estimate_point),
|
||||
Value(0, output_field=FloatField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
total_estimate_points=Coalesce(
|
||||
Subquery(total_estimate_point),
|
||||
Value(0, output_field=FloatField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
member_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
@@ -232,7 +354,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
data["estimate_distribution"] = {}
|
||||
|
||||
if estimate_type:
|
||||
label_distribution = (
|
||||
assignee_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
issue_module__module_id=pk,
|
||||
workspace__slug=slug,
|
||||
@@ -252,12 +374,12 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
)
|
||||
.annotate(
|
||||
total_estimates=Sum(
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
Cast("estimate_point__value", FloatField())
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
filter=Q(
|
||||
completed_at__isnull=False,
|
||||
archived_at__isnull=True,
|
||||
@@ -267,7 +389,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
)
|
||||
.annotate(
|
||||
pending_estimates=Sum(
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
filter=Q(
|
||||
completed_at__isnull=True,
|
||||
archived_at__isnull=True,
|
||||
@@ -278,7 +400,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
.order_by("first_name", "last_name")
|
||||
)
|
||||
|
||||
assignee_distribution = (
|
||||
label_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
issue_module__module_id=pk,
|
||||
workspace__slug=slug,
|
||||
@@ -290,12 +412,12 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
.values("label_name", "color", "label_id")
|
||||
.annotate(
|
||||
total_estimates=Sum(
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
Cast("estimate_point__value", FloatField())
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
filter=Q(
|
||||
completed_at__isnull=False,
|
||||
archived_at__isnull=True,
|
||||
@@ -305,7 +427,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
)
|
||||
.annotate(
|
||||
pending_estimates=Sum(
|
||||
Cast("estimate_point__value", IntegerField()),
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
filter=Q(
|
||||
completed_at__isnull=True,
|
||||
archived_at__isnull=True,
|
||||
@@ -315,8 +437,8 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
)
|
||||
.order_by("label_name")
|
||||
)
|
||||
data["estimate_distribution"]["assignee"] = assignee_distribution
|
||||
data["estimate_distribution"]["label"] = label_distribution
|
||||
data["estimate_distribution"]["assignees"] = assignee_distribution
|
||||
data["estimate_distribution"]["labels"] = label_distribution
|
||||
|
||||
if modules and modules.start_date and modules.target_date:
|
||||
data["estimate_distribution"]["completion_chart"] = (
|
||||
@@ -328,6 +450,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
module_id=pk,
|
||||
)
|
||||
)
|
||||
|
||||
assignee_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
issue_module__module_id=pk,
|
||||
@@ -353,7 +476,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
completed_issues=Count(
|
||||
@@ -425,8 +548,6 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
"labels": label_distribution,
|
||||
"completion_chart": {},
|
||||
}
|
||||
|
||||
# Fetch the modules
|
||||
if modules and modules.start_date and modules.target_date:
|
||||
data["distribution"]["completion_chart"] = burndown_plot(
|
||||
queryset=modules,
|
||||
|
||||
@@ -48,6 +48,7 @@ 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
|
||||
@@ -554,7 +555,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
assignee_distribution = (
|
||||
Issue.objects.filter(
|
||||
Issue.issue_objects.filter(
|
||||
issue_module__module_id=pk,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
@@ -604,7 +605,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
label_distribution = (
|
||||
Issue.objects.filter(
|
||||
Issue.issue_objects.filter(
|
||||
issue_module__module_id=pk,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
@@ -737,6 +738,21 @@ 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
|
||||
@@ -757,6 +773,10 @@ class ModuleViewSet(BaseViewSet):
|
||||
for issue in module_issues
|
||||
]
|
||||
module.delete()
|
||||
# Delete the module issues
|
||||
ModuleIssue.objects.filter(
|
||||
module=pk,
|
||||
).delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@@ -820,7 +840,7 @@ class ModuleFavoriteViewSet(BaseViewSet):
|
||||
entity_type="module",
|
||||
entity_identifier=module_id,
|
||||
)
|
||||
module_favorite.delete()
|
||||
module_favorite.delete(soft=False)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
|
||||
@@ -250,7 +250,6 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
removed_modules = request.data.get("removed_modules", [])
|
||||
project = Project.objects.get(pk=project_id)
|
||||
|
||||
|
||||
if modules:
|
||||
_ = ModuleIssue.objects.bulk_create(
|
||||
[
|
||||
@@ -284,7 +283,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
]
|
||||
|
||||
for module_id in removed_modules:
|
||||
module_issue = ModuleIssue.objects.get(
|
||||
module_issue = ModuleIssue.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
module_id=module_id,
|
||||
@@ -297,7 +296,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
issue_id=str(issue_id),
|
||||
project_id=str(project_id),
|
||||
current_instance=json.dumps(
|
||||
{"module_name": module_issue.module.name}
|
||||
{"module_name": module_issue.first().module.name}
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
@@ -308,7 +307,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
return Response({"message": "success"}, status=status.HTTP_201_CREATED)
|
||||
|
||||
def destroy(self, request, slug, project_id, module_id, issue_id):
|
||||
module_issue = ModuleIssue.objects.get(
|
||||
module_issue = ModuleIssue.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
module_id=module_id,
|
||||
@@ -321,7 +320,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
issue_id=str(issue_id),
|
||||
project_id=str(project_id),
|
||||
current_instance=json.dumps(
|
||||
{"module_name": module_issue.module.name}
|
||||
{"module_name": module_issue.first().module.name}
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
|
||||
@@ -333,27 +333,26 @@ class PageViewSet(BaseViewSet):
|
||||
pk=pk, workspace__slug=slug, projects__id=project_id
|
||||
)
|
||||
|
||||
# only the owner and admin can delete the page
|
||||
if (
|
||||
ProjectMember.objects.filter(
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
is_active=True,
|
||||
role__gt=20,
|
||||
).exists()
|
||||
or request.user.id != page.owned_by_id
|
||||
):
|
||||
return Response(
|
||||
{"error": "Only the owner and admin can delete the page"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if page.archived_at is None:
|
||||
return Response(
|
||||
{"error": "The page should be archived before deleting"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if 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,
|
||||
)
|
||||
|
||||
# remove parent from all the children
|
||||
_ = Page.objects.filter(
|
||||
parent_id=pk, projects__id=project_id, workspace__slug=slug
|
||||
@@ -387,7 +386,7 @@ class PageFavoriteViewSet(BaseViewSet):
|
||||
entity_identifier=pk,
|
||||
entity_type="page",
|
||||
)
|
||||
page_favorite.delete()
|
||||
page_favorite.delete(soft=False)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
|
||||
@@ -599,7 +599,7 @@ class ProjectFavoritesViewSet(BaseViewSet):
|
||||
user=request.user,
|
||||
workspace__slug=slug,
|
||||
)
|
||||
project_favorite.delete()
|
||||
project_favorite.delete(soft=False)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
|
||||
@@ -52,8 +52,14 @@ class IssueSearchEndpoint(BaseAPIView):
|
||||
issue = Issue.issue_objects.get(pk=issue_id)
|
||||
issues = issues.filter(
|
||||
~Q(pk=issue_id),
|
||||
~Q(issue_related__issue=issue),
|
||||
~Q(issue_relation__related_issue=issue),
|
||||
~Q(
|
||||
issue_related__issue=issue,
|
||||
issue_related__deleted_at__isnull=True,
|
||||
),
|
||||
~Q(
|
||||
issue_relation__related_issue=issue,
|
||||
issue_related__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
if sub_issue == "true" and issue_id:
|
||||
issue = Issue.issue_objects.get(pk=issue_id)
|
||||
|
||||
@@ -116,6 +116,20 @@ 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,
|
||||
@@ -412,14 +426,16 @@ class IssueViewViewSet(BaseViewSet):
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
)
|
||||
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:
|
||||
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_view.delete()
|
||||
else:
|
||||
return Response(
|
||||
@@ -458,5 +474,5 @@ class IssueViewFavoriteViewSet(BaseViewSet):
|
||||
entity_type="view",
|
||||
entity_identifier=view_id,
|
||||
)
|
||||
view_favorite.delete()
|
||||
view_favorite.delete(soft=False)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# Third party modules
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
# Django modules
|
||||
from django.db.models import Q
|
||||
|
||||
# Module imports
|
||||
from plane.app.views.base import BaseAPIView
|
||||
from plane.db.models import UserFavorite, Workspace
|
||||
from plane.app.serializers import UserFavoriteSerializer
|
||||
from plane.app.permissions import WorkspaceEntityPermission
|
||||
|
||||
|
||||
class WorkspaceFavoriteEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
WorkspaceEntityPermission,
|
||||
]
|
||||
|
||||
def get(self, request, slug):
|
||||
# the second filter is to check if the user is a member of the project
|
||||
favorites = UserFavorite.objects.filter(
|
||||
user=request.user,
|
||||
workspace__slug=slug,
|
||||
parent__isnull=True,
|
||||
).filter(
|
||||
Q(project__isnull=True)
|
||||
| (
|
||||
Q(project__isnull=False)
|
||||
& Q(project__project_projectmember__member=request.user)
|
||||
& Q(project__project_projectmember__is_active=True)
|
||||
)
|
||||
)
|
||||
serializer = UserFavoriteSerializer(favorites, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def post(self, request, slug):
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
serializer = UserFavoriteSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
serializer.save(
|
||||
user_id=request.user.id,
|
||||
workspace=workspace,
|
||||
project_id=request.data.get("project_id", None),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def patch(self, request, slug, favorite_id):
|
||||
favorite = UserFavorite.objects.get(
|
||||
user=request.user, workspace__slug=slug, pk=favorite_id
|
||||
)
|
||||
serializer = UserFavoriteSerializer(
|
||||
favorite, data=request.data, partial=True
|
||||
)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def delete(self, request, slug, favorite_id):
|
||||
favorite = UserFavorite.objects.get(
|
||||
user=request.user, workspace__slug=slug, pk=favorite_id
|
||||
)
|
||||
favorite.delete(soft=False)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class WorkspaceFavoriteGroupEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
WorkspaceEntityPermission,
|
||||
]
|
||||
|
||||
def get(self, request, slug, favorite_id):
|
||||
favorites = UserFavorite.objects.filter(
|
||||
user=request.user,
|
||||
workspace__slug=slug,
|
||||
parent_id=favorite_id,
|
||||
).filter(
|
||||
Q(project__isnull=True)
|
||||
| (
|
||||
Q(project__isnull=False)
|
||||
& Q(project__project_projectmember__member=request.user)
|
||||
& Q(project__project_projectmember__is_active=True)
|
||||
)
|
||||
)
|
||||
serializer = UserFavoriteSerializer(favorites, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
@@ -100,10 +100,8 @@ class OauthAdapter(Adapter):
|
||||
account, created = Account.objects.update_or_create(
|
||||
user=user,
|
||||
provider=self.provider,
|
||||
provider_account_id=self.user_data.get("user").get("provider_id"),
|
||||
defaults={
|
||||
"provider_account_id": self.user_data.get("user").get(
|
||||
"provider_id"
|
||||
),
|
||||
"access_token": self.token_data.get("access_token"),
|
||||
"refresh_token": self.token_data.get("refresh_token", None),
|
||||
"access_token_expired_at": self.token_data.get(
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
# Third party imports
|
||||
from celery import shared_task
|
||||
|
||||
|
||||
@shared_task
|
||||
def soft_delete_related_objects(
|
||||
app_label, model_name, instance_pk, using=None
|
||||
):
|
||||
model_class = apps.get_model(app_label, model_name)
|
||||
instance = model_class.all_objects.get(pk=instance_pk)
|
||||
related_fields = instance._meta.get_fields()
|
||||
for field in related_fields:
|
||||
if field.one_to_many or field.one_to_one:
|
||||
try:
|
||||
if field.one_to_many:
|
||||
related_objects = getattr(instance, field.name).all()
|
||||
elif field.one_to_one:
|
||||
related_object = getattr(instance, field.name)
|
||||
related_objects = (
|
||||
[related_object] if related_object is not None else []
|
||||
)
|
||||
for obj in related_objects:
|
||||
if obj:
|
||||
obj.deleted_at = timezone.now()
|
||||
obj.save(using=using)
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
|
||||
|
||||
# @shared_task
|
||||
def restore_related_objects(app_label, model_name, instance_pk, using=None):
|
||||
pass
|
||||
|
||||
|
||||
@shared_task
|
||||
def hard_delete():
|
||||
|
||||
from plane.db.models import (
|
||||
Workspace,
|
||||
Project,
|
||||
Cycle,
|
||||
Module,
|
||||
Issue,
|
||||
Page,
|
||||
IssueView,
|
||||
Label,
|
||||
State,
|
||||
IssueActivity,
|
||||
IssueComment,
|
||||
IssueLink,
|
||||
IssueReaction,
|
||||
UserFavorite,
|
||||
ModuleIssue,
|
||||
CycleIssue,
|
||||
Estimate,
|
||||
EstimatePoint,
|
||||
)
|
||||
|
||||
days = settings.HARD_DELETE_AFTER_DAYS
|
||||
# check delete workspace
|
||||
_ = Workspace.all_objects.filter(
|
||||
deleted_at__lt=timezone.now() - timezone.timedelta(days=days)
|
||||
).delete()
|
||||
|
||||
# check delete project
|
||||
_ = Project.all_objects.filter(
|
||||
deleted_at__lt=timezone.now() - timezone.timedelta(days=days)
|
||||
).delete()
|
||||
|
||||
# check delete cycle
|
||||
_ = Cycle.all_objects.filter(
|
||||
deleted_at__lt=timezone.now() - timezone.timedelta(days=days)
|
||||
).delete()
|
||||
|
||||
# check delete module
|
||||
_ = Module.all_objects.filter(
|
||||
deleted_at__lt=timezone.now() - timezone.timedelta(days=days)
|
||||
).delete()
|
||||
|
||||
# check delete issue
|
||||
_ = Issue.all_objects.filter(
|
||||
deleted_at__lt=timezone.now() - timezone.timedelta(days=days)
|
||||
).delete()
|
||||
|
||||
# check delete page
|
||||
_ = Page.all_objects.filter(
|
||||
deleted_at__lt=timezone.now() - timezone.timedelta(days=days)
|
||||
).delete()
|
||||
|
||||
# check delete view
|
||||
_ = IssueView.all_objects.filter(
|
||||
deleted_at__lt=timezone.now() - timezone.timedelta(days=days)
|
||||
).delete()
|
||||
|
||||
# check delete label
|
||||
_ = Label.all_objects.filter(
|
||||
deleted_at__lt=timezone.now() - timezone.timedelta(days=days)
|
||||
).delete()
|
||||
|
||||
# check delete state
|
||||
_ = State.all_objects.filter(
|
||||
deleted_at__lt=timezone.now() - timezone.timedelta(days=days)
|
||||
).delete()
|
||||
|
||||
_ = IssueActivity.all_objects.filter(
|
||||
deleted_at__lt=timezone.now() - timezone.timedelta(days=days)
|
||||
).delete()
|
||||
|
||||
_ = IssueComment.all_objects.filter(
|
||||
deleted_at__lt=timezone.now() - timezone.timedelta(days=days)
|
||||
).delete()
|
||||
|
||||
_ = IssueLink.all_objects.filter(
|
||||
deleted_at__lt=timezone.now() - timezone.timedelta(days=days)
|
||||
).delete()
|
||||
|
||||
_ = IssueReaction.all_objects.filter(
|
||||
deleted_at__lt=timezone.now() - timezone.timedelta(days=days)
|
||||
).delete()
|
||||
|
||||
_ = UserFavorite.all_objects.filter(
|
||||
deleted_at__lt=timezone.now() - timezone.timedelta(days=days)
|
||||
).delete()
|
||||
|
||||
_ = ModuleIssue.all_objects.filter(
|
||||
deleted_at__lt=timezone.now() - timezone.timedelta(days=days)
|
||||
).delete()
|
||||
|
||||
_ = CycleIssue.all_objects.filter(
|
||||
deleted_at__lt=timezone.now() - timezone.timedelta(days=days)
|
||||
).delete()
|
||||
|
||||
_ = Estimate.all_objects.filter(
|
||||
deleted_at__lt=timezone.now() - timezone.timedelta(days=days)
|
||||
).delete()
|
||||
|
||||
_ = EstimatePoint.all_objects.filter(
|
||||
deleted_at__lt=timezone.now() - timezone.timedelta(days=days)
|
||||
).delete()
|
||||
|
||||
# at last, check for every thing which ever is left and delete it
|
||||
# Get all Django models
|
||||
all_models = apps.get_models()
|
||||
|
||||
# Iterate through all models
|
||||
for model in all_models:
|
||||
# Check if the model has a 'deleted_at' field
|
||||
if hasattr(model, "deleted_at"):
|
||||
# Get all instances where 'deleted_at' is greater than 30 days ago
|
||||
_ = model.all_objects.filter(
|
||||
deleted_at__lt=timezone.now()
|
||||
- timezone.timedelta(days=days)
|
||||
).delete()
|
||||
|
||||
return
|
||||
@@ -593,7 +593,8 @@ def create_issue_activity(
|
||||
epoch=epoch,
|
||||
)
|
||||
issue_activity.created_at = issue.created_at
|
||||
issue_activity.save(update_fields=["created_at"])
|
||||
issue_activity.actor_id = issue.created_by_id
|
||||
issue_activity.save(update_fields=["created_at", "actor_id"])
|
||||
requested_data = (
|
||||
json.loads(requested_data) if requested_data is not None else None
|
||||
)
|
||||
@@ -671,6 +672,7 @@ def delete_issue_activity(
|
||||
IssueActivity(
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
issue_id=issue_id,
|
||||
comment="deleted the issue",
|
||||
verb="deleted",
|
||||
actor_id=actor_id,
|
||||
@@ -878,7 +880,6 @@ def delete_cycle_issue_activity(
|
||||
cycle_name = requested_data.get("cycle_name", "")
|
||||
cycle = Cycle.objects.filter(pk=cycle_id).first()
|
||||
issues = requested_data.get("issues")
|
||||
|
||||
for issue in issues:
|
||||
current_issue = Issue.objects.filter(pk=issue).first()
|
||||
if issue:
|
||||
|
||||
@@ -221,7 +221,6 @@ def notifications(
|
||||
else None
|
||||
)
|
||||
if type not in [
|
||||
"issue.activity.deleted",
|
||||
"cycle.activity.created",
|
||||
"cycle.activity.deleted",
|
||||
"module.activity.created",
|
||||
|
||||
@@ -36,6 +36,10 @@ app.conf.beat_schedule = {
|
||||
"task": "plane.bgtasks.api_logs_task.delete_api_logs",
|
||||
"schedule": crontab(hour=0, minute=0),
|
||||
},
|
||||
"check-every-day-to-delete-hard-delete": {
|
||||
"task": "plane.bgtasks.deletion_task.hard_delete",
|
||||
"schedule": crontab(hour=0, minute=0),
|
||||
},
|
||||
}
|
||||
|
||||
# Load task modules from all registered Django app configs.
|
||||
|
||||
+1130
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,9 @@
|
||||
# Python imports
|
||||
|
||||
# Django imports
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
|
||||
# Module imports
|
||||
from plane.bgtasks.deletion_task import soft_delete_related_objects
|
||||
|
||||
|
||||
class TimeAuditModel(models.Model):
|
||||
@@ -41,7 +43,45 @@ class UserAuditModel(models.Model):
|
||||
abstract = True
|
||||
|
||||
|
||||
class AuditModel(TimeAuditModel, UserAuditModel):
|
||||
class SoftDeletionManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().filter(deleted_at__isnull=True)
|
||||
|
||||
|
||||
class SoftDeleteModel(models.Model):
|
||||
"""To soft delete records"""
|
||||
|
||||
deleted_at = models.DateTimeField(
|
||||
verbose_name="Deleted At",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
objects = SoftDeletionManager()
|
||||
all_objects = models.Manager()
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def delete(self, using=None, soft=True, *args, **kwargs):
|
||||
if soft:
|
||||
# Soft delete the current instance
|
||||
self.deleted_at = timezone.now()
|
||||
self.save(using=using)
|
||||
|
||||
soft_delete_related_objects.delay(
|
||||
self._meta.app_label,
|
||||
self._meta.model_name,
|
||||
self.pk,
|
||||
using=using,
|
||||
)
|
||||
|
||||
else:
|
||||
# Perform hard delete if soft deletion is not enabled
|
||||
return super().delete(using=using, *args, **kwargs)
|
||||
|
||||
|
||||
class AuditModel(TimeAuditModel, UserAuditModel, SoftDeleteModel):
|
||||
"""To path when the record was created and last modified"""
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -116,6 +116,7 @@ class CycleIssue(ProjectBaseModel):
|
||||
return f"{self.cycle}"
|
||||
|
||||
|
||||
# DEPRECATED TODO: - Remove in next release
|
||||
class CycleFavorite(ProjectBaseModel):
|
||||
"""_summary_
|
||||
CycleFavorite (model): To store all the cycle favorite of the user
|
||||
@@ -160,7 +161,14 @@ class CycleUserProperties(ProjectBaseModel):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["cycle", "user"]
|
||||
unique_together = ["cycle", "user", "deleted_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["cycle", "user"],
|
||||
condition=models.Q(deleted_at__isnull=True),
|
||||
name="cycle_user_properties_unique_cycle_user_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
verbose_name = "Cycle User Property"
|
||||
verbose_name_plural = "Cycle User Properties"
|
||||
db_table = "cycle_user_properties"
|
||||
|
||||
@@ -88,7 +88,14 @@ class DashboardWidget(BaseModel):
|
||||
return f"{self.dashboard.name} {self.widget.key}"
|
||||
|
||||
class Meta:
|
||||
unique_together = ("widget", "dashboard")
|
||||
unique_together = ("widget", "dashboard", "deleted_at")
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["widget", "dashboard"],
|
||||
condition=models.Q(deleted_at__isnull=True),
|
||||
name="dashboard_widget_unique_widget_dashboard_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
verbose_name = "Dashboard Widget"
|
||||
verbose_name_plural = "Dashboard Widgets"
|
||||
db_table = "dashboard_widgets"
|
||||
|
||||
@@ -46,7 +46,14 @@ class DeployBoard(WorkspaceBaseModel):
|
||||
return f"{self.entity_identifier} <{self.entity_name}>"
|
||||
|
||||
class Meta:
|
||||
unique_together = ["entity_name", "entity_identifier"]
|
||||
unique_together = ["entity_name", "entity_identifier", "deleted_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["entity_name", "entity_identifier"],
|
||||
condition=models.Q(deleted_at__isnull=True),
|
||||
name="deploy_board_unique_entity_name_entity_identifier_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
verbose_name = "Deploy Board"
|
||||
verbose_name_plural = "Deploy Boards"
|
||||
db_table = "deploy_boards"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Django imports
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
|
||||
# Module imports
|
||||
from .project import ProjectBaseModel
|
||||
@@ -19,7 +20,14 @@ class Estimate(ProjectBaseModel):
|
||||
return f"{self.name} <{self.project.name}>"
|
||||
|
||||
class Meta:
|
||||
unique_together = ["name", "project"]
|
||||
unique_together = ["name", "project", "deleted_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["name", "project"],
|
||||
condition=Q(deleted_at__isnull=True),
|
||||
name="estimate_unique_name_project_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
verbose_name = "Estimate"
|
||||
verbose_name_plural = "Estimates"
|
||||
db_table = "estimates"
|
||||
|
||||
@@ -31,7 +31,14 @@ class UserFavorite(WorkspaceBaseModel):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["entity_type", "user", "entity_identifier"]
|
||||
unique_together = ["entity_type", "user", "entity_identifier", "deleted_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["entity_type", "entity_identifier", "user"],
|
||||
condition=models.Q(deleted_at__isnull=True),
|
||||
name="user_favorite_unique_entity_type_entity_identifier_user_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
verbose_name = "User Favorite"
|
||||
verbose_name_plural = "User Favorites"
|
||||
db_table = "user_favorites"
|
||||
@@ -39,9 +46,14 @@ class UserFavorite(WorkspaceBaseModel):
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self._state.adding:
|
||||
largest_sequence = UserFavorite.objects.filter(
|
||||
workspace=self.project.workspace
|
||||
).aggregate(largest=models.Max("sequence"))["largest"]
|
||||
if self.project:
|
||||
largest_sequence = UserFavorite.objects.filter(
|
||||
workspace=self.project.workspace
|
||||
).aggregate(largest=models.Max("sequence"))["largest"]
|
||||
else:
|
||||
largest_sequence = UserFavorite.objects.filter(
|
||||
workspace=self.workspace,
|
||||
).aggregate(largest=models.Max("sequence"))["largest"]
|
||||
if largest_sequence is not None:
|
||||
self.sequence = largest_sequence + 10000
|
||||
|
||||
|
||||
@@ -19,7 +19,14 @@ class Inbox(ProjectBaseModel):
|
||||
return f"{self.name} <{self.project.name}>"
|
||||
|
||||
class Meta:
|
||||
unique_together = ["name", "project"]
|
||||
unique_together = ["name", "project", "deleted_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["name", "project"],
|
||||
condition=models.Q(deleted_at__isnull=True),
|
||||
name="inbox_unique_name_project_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
verbose_name = "Inbox"
|
||||
verbose_name_plural = "Inboxes"
|
||||
db_table = "inboxes"
|
||||
|
||||
@@ -8,6 +8,7 @@ from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models, transaction
|
||||
from django.utils import timezone
|
||||
from django.db.models import Q
|
||||
|
||||
# Module imports
|
||||
from plane.utils.html_processor import strip_tags
|
||||
@@ -89,6 +90,7 @@ class IssueManager(models.Manager):
|
||||
| models.Q(issue_inbox__status=2)
|
||||
| models.Q(issue_inbox__isnull=True)
|
||||
)
|
||||
.filter(deleted_at__isnull=True)
|
||||
.filter(state__is_triage=False)
|
||||
.exclude(archived_at__isnull=False)
|
||||
.exclude(project__archived_at__isnull=False)
|
||||
@@ -293,7 +295,14 @@ class IssueRelation(ProjectBaseModel):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["issue", "related_issue"]
|
||||
unique_together = ["issue", "related_issue", "deleted_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["issue", "related_issue"],
|
||||
condition=Q(deleted_at__isnull=True),
|
||||
name="issue_relation_unique_issue_related_issue_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
verbose_name = "Issue Relation"
|
||||
verbose_name_plural = "Issue Relations"
|
||||
db_table = "issue_relations"
|
||||
@@ -314,7 +323,14 @@ class IssueMention(ProjectBaseModel):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["issue", "mention"]
|
||||
unique_together = ["issue", "mention", "deleted_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["issue", "mention"],
|
||||
condition=Q(deleted_at__isnull=True),
|
||||
name="issue_mention_unique_issue_mention_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
verbose_name = "Issue Mention"
|
||||
verbose_name_plural = "Issue Mentions"
|
||||
db_table = "issue_mentions"
|
||||
@@ -335,7 +351,14 @@ class IssueAssignee(ProjectBaseModel):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["issue", "assignee"]
|
||||
unique_together = ["issue", "assignee", "deleted_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["issue", "assignee"],
|
||||
condition=Q(deleted_at__isnull=True),
|
||||
name="issue_assignee_unique_issue_assignee_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
verbose_name = "Issue Assignee"
|
||||
verbose_name_plural = "Issue Assignees"
|
||||
db_table = "issue_assignees"
|
||||
@@ -510,7 +533,14 @@ class IssueUserProperty(ProjectBaseModel):
|
||||
verbose_name_plural = "Issue User Properties"
|
||||
db_table = "issue_user_properties"
|
||||
ordering = ("-created_at",)
|
||||
unique_together = ["user", "project"]
|
||||
unique_together = ["user", "project", "deleted_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["user", "project"],
|
||||
condition=Q(deleted_at__isnull=True),
|
||||
name="issue_user_property_unique_user_project_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
"""Return properties status of the issue"""
|
||||
@@ -533,7 +563,14 @@ class Label(ProjectBaseModel):
|
||||
external_id = models.CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["name", "project"]
|
||||
unique_together = ["name", "project", "deleted_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["name", "project"],
|
||||
condition=Q(deleted_at__isnull=True),
|
||||
name="label_unique_name_project_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
verbose_name = "Label"
|
||||
verbose_name_plural = "Labels"
|
||||
db_table = "labels"
|
||||
@@ -601,7 +638,14 @@ class IssueSubscriber(ProjectBaseModel):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["issue", "subscriber"]
|
||||
unique_together = ["issue", "subscriber", "deleted_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["issue", "subscriber"],
|
||||
condition=models.Q(deleted_at__isnull=True),
|
||||
name="issue_subscriber_unique_issue_subscriber_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
verbose_name = "Issue Subscriber"
|
||||
verbose_name_plural = "Issue Subscribers"
|
||||
db_table = "issue_subscribers"
|
||||
@@ -623,7 +667,14 @@ class IssueReaction(ProjectBaseModel):
|
||||
reaction = models.CharField(max_length=20)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["issue", "actor", "reaction"]
|
||||
unique_together = ["issue", "actor", "reaction", "deleted_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["issue", "actor", "reaction"],
|
||||
condition=models.Q(deleted_at__isnull=True),
|
||||
name="issue_reaction_unique_issue_actor_reaction_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
verbose_name = "Issue Reaction"
|
||||
verbose_name_plural = "Issue Reactions"
|
||||
db_table = "issue_reactions"
|
||||
@@ -647,7 +698,14 @@ class CommentReaction(ProjectBaseModel):
|
||||
reaction = models.CharField(max_length=20)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["comment", "actor", "reaction"]
|
||||
unique_together = ["comment", "actor", "reaction", "deleted_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["comment", "actor", "reaction"],
|
||||
condition=models.Q(deleted_at__isnull=True),
|
||||
name="comment_reaction_unique_comment_actor_reaction_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
verbose_name = "Comment Reaction"
|
||||
verbose_name_plural = "Comment Reactions"
|
||||
db_table = "comment_reactions"
|
||||
@@ -678,6 +736,14 @@ class IssueVote(ProjectBaseModel):
|
||||
unique_together = [
|
||||
"issue",
|
||||
"actor",
|
||||
"deleted_at",
|
||||
]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["issue", "actor"],
|
||||
condition=models.Q(deleted_at__isnull=True),
|
||||
name="issue_vote_unique_issue_actor_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
verbose_name = "Issue Vote"
|
||||
verbose_name_plural = "Issue Votes"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Django imports
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
|
||||
# Module imports
|
||||
from .workspace import WorkspaceBaseModel
|
||||
@@ -15,7 +16,14 @@ class IssueType(WorkspaceBaseModel):
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["project", "name"]
|
||||
unique_together = ["project", "name", "deleted_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["name", "project"],
|
||||
condition=Q(deleted_at__isnull=True),
|
||||
name="issue_type_unique_name_project_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
verbose_name = "Issue Type"
|
||||
verbose_name_plural = "Issue Types"
|
||||
db_table = "issue_types"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
|
||||
# Module imports
|
||||
from .project import ProjectBaseModel
|
||||
@@ -96,7 +97,14 @@ class Module(ProjectBaseModel):
|
||||
logo_props = models.JSONField(default=dict)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["name", "project"]
|
||||
unique_together = ["name", "project", "deleted_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=['name', 'project'],
|
||||
condition=Q(deleted_at__isnull=True),
|
||||
name='module_unique_name_project_when_deleted_at_null'
|
||||
)
|
||||
]
|
||||
verbose_name = "Module"
|
||||
verbose_name_plural = "Modules"
|
||||
db_table = "modules"
|
||||
@@ -122,7 +130,14 @@ class ModuleMember(ProjectBaseModel):
|
||||
member = models.ForeignKey("db.User", on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["module", "member"]
|
||||
unique_together = ["module", "member", "deleted_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["module", "member"],
|
||||
condition=models.Q(deleted_at__isnull=True),
|
||||
name="module_member_unique_module_member_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
verbose_name = "Module Member"
|
||||
verbose_name_plural = "Module Members"
|
||||
db_table = "module_members"
|
||||
@@ -141,7 +156,14 @@ class ModuleIssue(ProjectBaseModel):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["issue", "module"]
|
||||
unique_together = ["issue", "module", "deleted_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["issue", "module"],
|
||||
condition=models.Q(deleted_at__isnull=True),
|
||||
name="module_issue_unique_issue_module_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
verbose_name = "Module Issue"
|
||||
verbose_name_plural = "Module Issues"
|
||||
db_table = "module_issues"
|
||||
@@ -169,6 +191,7 @@ class ModuleLink(ProjectBaseModel):
|
||||
return f"{self.module.name} {self.url}"
|
||||
|
||||
|
||||
# DEPRECATED TODO: - Remove in next release
|
||||
class ModuleFavorite(ProjectBaseModel):
|
||||
"""_summary_
|
||||
ModuleFavorite (model): To store all the module favorite of the user
|
||||
@@ -213,7 +236,14 @@ class ModuleUserProperties(ProjectBaseModel):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["module", "user"]
|
||||
unique_together = ["module", "user", "deleted_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["module", "user"],
|
||||
condition=models.Q(deleted_at__isnull=True),
|
||||
name="module_user_properties_unique_module_user_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
verbose_name = "Module User Property"
|
||||
verbose_name_plural = "Module User Property"
|
||||
db_table = "module_user_properties"
|
||||
|
||||
@@ -119,6 +119,7 @@ class PageLog(BaseModel):
|
||||
return f"{self.page.name} {self.entity_name}"
|
||||
|
||||
|
||||
# DEPRECATED TODO: - Remove in next release
|
||||
class PageBlock(ProjectBaseModel):
|
||||
page = models.ForeignKey(
|
||||
"db.Page", on_delete=models.CASCADE, related_name="blocks"
|
||||
@@ -175,6 +176,7 @@ class PageBlock(ProjectBaseModel):
|
||||
return f"{self.page.name} <{self.name}>"
|
||||
|
||||
|
||||
# DEPRECATED TODO: - Remove in next release
|
||||
class PageFavorite(ProjectBaseModel):
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
@@ -232,7 +234,14 @@ class ProjectPage(BaseModel):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["project", "page"]
|
||||
unique_together = ["project", "page", "deleted_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["project", "page"],
|
||||
condition=models.Q(deleted_at__isnull=True),
|
||||
name="project_page_unique_project_page_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
verbose_name = "Project Page"
|
||||
verbose_name_plural = "Project Pages"
|
||||
db_table = "project_pages"
|
||||
@@ -254,7 +263,14 @@ class TeamPage(BaseModel):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["team", "page"]
|
||||
unique_together = ["team", "page", "deleted_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["team", "page"],
|
||||
condition=models.Q(deleted_at__isnull=True),
|
||||
name="team_page_unique_team_page_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
verbose_name = "Team Page"
|
||||
verbose_name_plural = "Team Pages"
|
||||
db_table = "team_pages"
|
||||
|
||||
@@ -5,6 +5,7 @@ from uuid import uuid4
|
||||
from django.conf import settings
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
|
||||
# Modeule imports
|
||||
from plane.db.mixins import AuditModel
|
||||
@@ -124,7 +125,22 @@ class Project(BaseModel):
|
||||
return f"{self.name} <{self.workspace.name}>"
|
||||
|
||||
class Meta:
|
||||
unique_together = [["identifier", "workspace"], ["name", "workspace"]]
|
||||
unique_together = [
|
||||
["identifier", "workspace", "deleted_at"],
|
||||
["name", "workspace", "deleted_at"],
|
||||
]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["identifier", "workspace"],
|
||||
condition=Q(deleted_at__isnull=True),
|
||||
name="project_unique_identifier_workspace_when_deleted_at_null",
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
fields=["name", "workspace"],
|
||||
condition=Q(deleted_at__isnull=True),
|
||||
name="project_unique_name_workspace_when_deleted_at_null",
|
||||
),
|
||||
]
|
||||
verbose_name = "Project"
|
||||
verbose_name_plural = "Projects"
|
||||
db_table = "projects"
|
||||
@@ -198,7 +214,14 @@ class ProjectMember(ProjectBaseModel):
|
||||
super(ProjectMember, self).save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["project", "member"]
|
||||
unique_together = ["project", "member", "deleted_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["project", "member"],
|
||||
condition=Q(deleted_at__isnull=True),
|
||||
name="project_member_unique_project_member_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
verbose_name = "Project Member"
|
||||
verbose_name_plural = "Project Members"
|
||||
db_table = "project_members"
|
||||
@@ -223,13 +246,21 @@ class ProjectIdentifier(AuditModel):
|
||||
name = models.CharField(max_length=12, db_index=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["name", "workspace"]
|
||||
unique_together = ["name", "workspace", "deleted_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["name", "workspace"],
|
||||
condition=Q(deleted_at__isnull=True),
|
||||
name="unique_name_workspace_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
verbose_name = "Project Identifier"
|
||||
verbose_name_plural = "Project Identifiers"
|
||||
db_table = "project_identifiers"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
|
||||
# DEPRECATED TODO: - Remove in next release
|
||||
class ProjectFavorite(ProjectBaseModel):
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
@@ -300,7 +331,14 @@ class ProjectPublicMember(ProjectBaseModel):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["project", "member"]
|
||||
unique_together = ["project", "member", "deleted_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["project", "member"],
|
||||
condition=models.Q(deleted_at__isnull=True),
|
||||
name="project_public_member_unique_project_member_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
verbose_name = "Project Public Member"
|
||||
verbose_name_plural = "Project Public Members"
|
||||
db_table = "project_public_members"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Django imports
|
||||
from django.db import models
|
||||
from django.template.defaultfilters import slugify
|
||||
from django.db.models import Q
|
||||
|
||||
# Module imports
|
||||
from .project import ProjectBaseModel
|
||||
@@ -36,7 +37,14 @@ class State(ProjectBaseModel):
|
||||
return f"{self.name} <{self.project.name}>"
|
||||
|
||||
class Meta:
|
||||
unique_together = ["name", "project"]
|
||||
unique_together = ["name", "project", "deleted_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["name", "project"],
|
||||
condition=Q(deleted_at__isnull=True),
|
||||
name="state_unique_name_project_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
verbose_name = "State"
|
||||
verbose_name_plural = "States"
|
||||
db_table = "states"
|
||||
|
||||
@@ -52,7 +52,6 @@ def get_default_display_properties():
|
||||
"updated_on": True,
|
||||
}
|
||||
|
||||
|
||||
# DEPRECATED TODO: - Remove in next release
|
||||
class GlobalView(BaseModel):
|
||||
workspace = models.ForeignKey(
|
||||
@@ -142,6 +141,7 @@ class IssueView(WorkspaceBaseModel):
|
||||
return f"{self.name} <{self.project.name}>"
|
||||
|
||||
|
||||
# DEPRECATED TODO: - Remove in next release
|
||||
class IssueViewFavorite(ProjectBaseModel):
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
|
||||
@@ -185,7 +185,14 @@ class WorkspaceMember(BaseModel):
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["workspace", "member"]
|
||||
unique_together = ["workspace", "member", "deleted_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["workspace", "member"],
|
||||
condition=models.Q(deleted_at__isnull=True),
|
||||
name="workspace_member_unique_workspace_member_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
verbose_name = "Workspace Member"
|
||||
verbose_name_plural = "Workspace Members"
|
||||
db_table = "workspace_members"
|
||||
@@ -210,7 +217,14 @@ class WorkspaceMemberInvite(BaseModel):
|
||||
role = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, default=10)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["email", "workspace"]
|
||||
unique_together = ["email", "workspace", "deleted_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["email", "workspace"],
|
||||
condition=models.Q(deleted_at__isnull=True),
|
||||
name="workspace_member_invite_unique_email_workspace_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
verbose_name = "Workspace Member Invite"
|
||||
verbose_name_plural = "Workspace Member Invites"
|
||||
db_table = "workspace_member_invites"
|
||||
@@ -240,7 +254,14 @@ class Team(BaseModel):
|
||||
return f"{self.name} <{self.workspace.name}>"
|
||||
|
||||
class Meta:
|
||||
unique_together = ["name", "workspace"]
|
||||
unique_together = ["name", "workspace", "deleted_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["name", "workspace"],
|
||||
condition=models.Q(deleted_at__isnull=True),
|
||||
name="team_unique_name_workspace_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
verbose_name = "Team"
|
||||
verbose_name_plural = "Teams"
|
||||
db_table = "teams"
|
||||
@@ -264,7 +285,14 @@ class TeamMember(BaseModel):
|
||||
return self.team.name
|
||||
|
||||
class Meta:
|
||||
unique_together = ["team", "member"]
|
||||
unique_together = ["team", "member", "deleted_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["team", "member"],
|
||||
condition=models.Q(deleted_at__isnull=True),
|
||||
name="team_member_unique_team_member_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
verbose_name = "Team Member"
|
||||
verbose_name_plural = "Team Members"
|
||||
db_table = "team_members"
|
||||
@@ -287,7 +315,14 @@ class WorkspaceTheme(BaseModel):
|
||||
return str(self.name) + str(self.actor.email)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["workspace", "name"]
|
||||
unique_together = ["workspace", "name", "deleted_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["workspace", "name"],
|
||||
condition=models.Q(deleted_at__isnull=True),
|
||||
name="workspace_theme_unique_workspace_name_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
verbose_name = "Workspace Theme"
|
||||
verbose_name_plural = "Workspace Themes"
|
||||
db_table = "workspace_themes"
|
||||
@@ -312,7 +347,14 @@ class WorkspaceUserProperties(BaseModel):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["workspace", "user"]
|
||||
unique_together = ["workspace", "user", "deleted_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["workspace", "user"],
|
||||
condition=models.Q(deleted_at__isnull=True),
|
||||
name="workspace_user_properties_unique_workspace_user_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
verbose_name = "Workspace User Property"
|
||||
verbose_name_plural = "Workspace User Property"
|
||||
db_table = "workspace_user_properties"
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 4.2.11 on 2024-07-26 11:31
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('license', '0003_alter_changelog_title_alter_changelog_version_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='changelog',
|
||||
name='deleted_at',
|
||||
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='instance',
|
||||
name='deleted_at',
|
||||
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='instanceadmin',
|
||||
name='deleted_at',
|
||||
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='instanceconfiguration',
|
||||
name='deleted_at',
|
||||
field=models.DateTimeField(blank=True, null=True, verbose_name='Deleted At'),
|
||||
),
|
||||
]
|
||||
@@ -355,3 +355,5 @@ CSRF_FAILURE_VIEW = "plane.authentication.views.common.csrf_failure"
|
||||
ADMIN_BASE_URL = os.environ.get("ADMIN_BASE_URL", None)
|
||||
SPACE_BASE_URL = os.environ.get("SPACE_BASE_URL", None)
|
||||
APP_BASE_URL = os.environ.get("APP_BASE_URL")
|
||||
|
||||
HARD_DELETE_AFTER_DAYS = int(os.environ.get("HARD_DELETE_AFTER_DAYS", 60))
|
||||
|
||||
@@ -69,7 +69,7 @@ class WorkspaceProjectAnchorEndpoint(BaseAPIView):
|
||||
|
||||
def get(self, request, slug, project_id):
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
workspace__slug=slug, project_id=project_id, entity_name="project"
|
||||
)
|
||||
serializer = DeployBoardSerializer(project_deploy_board)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -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 maching on AWS. It must of minimum t3.medium/t3a/medium</p>
|
||||
<p>Best way to start is to create EC2 machine on AWS. It must have minimum of 2vCPU and 4GB RAM.</p>
|
||||
<p>Run the below command to install docker engine.</p>
|
||||
|
||||
`curl -fsSL https://get.docker.com | sh -`
|
||||
@@ -67,23 +67,6 @@ 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
|
||||
@@ -114,7 +97,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-7]` will be popped up and this time hit `7` to exit.
|
||||
Again the `options [1-8]` will be popped up and this time hit `8` to exit.
|
||||
|
||||
---
|
||||
|
||||
@@ -236,7 +219,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 `variables-upgrade.env`. Here system will not replace `.env` with the new one.
|
||||
By choosing this, it will stop the services and then will download the latest `docker-compose.yaml` and `plane.env`.
|
||||
|
||||
You must expect the below message
|
||||
|
||||
@@ -244,7 +227,7 @@ You must expect the below message
|
||||
|
||||
Once done, choose `8` to exit from prompt.
|
||||
|
||||
> 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.
|
||||
> It is very important for you to validate the `plane.env` for the new changes.
|
||||
|
||||
Once done with making changes in `plane.env` file, jump on to `Start Server`
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
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: ${PULL_POLICY:-always}
|
||||
pull_policy: if_not_present
|
||||
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: ${PULL_POLICY:-always}
|
||||
pull_policy: if_not_present
|
||||
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: ${PULL_POLICY:-always}
|
||||
pull_policy: if_not_present
|
||||
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: ${PULL_POLICY:-always}
|
||||
pull_policy: if_not_present
|
||||
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: ${PULL_POLICY:-always}
|
||||
pull_policy: if_not_present
|
||||
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: ${PULL_POLICY:-always}
|
||||
pull_policy: if_not_present
|
||||
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: ${PULL_POLICY:-always}
|
||||
pull_policy: if_not_present
|
||||
restart: "no"
|
||||
command: ./bin/docker-entrypoint-migrator.sh
|
||||
volumes:
|
||||
@@ -167,7 +167,8 @@ services:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-proxy:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- ${NGINX_PORT}:80
|
||||
depends_on:
|
||||
|
||||
+249
-131
@@ -1,18 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
BRANCH=master
|
||||
BRANCH=${BRANCH:-master}
|
||||
SCRIPT_DIR=$PWD
|
||||
SERVICE_FOLDER=plane-app
|
||||
PLANE_INSTALL_DIR=$PWD/$SERVICE_FOLDER
|
||||
export APP_RELEASE=$BRANCH
|
||||
export APP_RELEASE="stable"
|
||||
export DOCKERHUB_USER=makeplane
|
||||
export PULL_POLICY=always
|
||||
USE_GLOBAL_IMAGES=1
|
||||
export PULL_POLICY=${PULL_POLICY:-if_not_present}
|
||||
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
GREEN='\033[0;32m'
|
||||
NC='\033[0m' # No Color
|
||||
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
|
||||
|
||||
function print_header() {
|
||||
clear
|
||||
@@ -31,65 +31,191 @@ Project management tool from the future
|
||||
EOF
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
echo "" >&2
|
||||
echo "" >&2
|
||||
echo "${CPU_ARCH^^} images are not available for selected release ($APP_RELEASE)." >&2
|
||||
echo "build"
|
||||
return 1
|
||||
fi
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
cp $PLANE_TEMP_CODE_DIR/deploy/selfhost/build.yml $PLANE_TEMP_CODE_DIR/build.yml
|
||||
wait "$pid"
|
||||
|
||||
cd $PLANE_TEMP_CODE_DIR
|
||||
if [ "$BRANCH" == "master" ];
|
||||
then
|
||||
export APP_RELEASE=stable
|
||||
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"
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Plane supports ${CPU_ARCH}" >&2
|
||||
echo "available"
|
||||
return 0
|
||||
else
|
||||
printf "INVALID OPTION SUPPLIED" >&2
|
||||
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 install() {
|
||||
echo "Installing Plane.........."
|
||||
download
|
||||
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 ""
|
||||
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
|
||||
else
|
||||
echo "File not found: $file"
|
||||
exit 1
|
||||
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
|
||||
}
|
||||
|
||||
function download() {
|
||||
local LOCAL_BUILD=$1
|
||||
cd $SCRIPT_DIR
|
||||
TS=$(date +%s)
|
||||
if [ -f "$PLANE_INSTALL_DIR/docker-compose.yaml" ]
|
||||
@@ -102,44 +228,48 @@ function download() {
|
||||
|
||||
if [ -f "$DOCKER_ENV_PATH" ];
|
||||
then
|
||||
cp $DOCKER_ENV_PATH $PLANE_INSTALL_DIR/archive/$TS.env
|
||||
else
|
||||
mv $PLANE_INSTALL_DIR/variables-upgrade.env $DOCKER_ENV_PATH
|
||||
cp "$DOCKER_ENV_PATH" "$PLANE_INSTALL_DIR/archive/$TS.env"
|
||||
cp "$DOCKER_ENV_PATH" "$PLANE_INSTALL_DIR/plane.env.bak"
|
||||
fi
|
||||
|
||||
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
|
||||
mv $PLANE_INSTALL_DIR/variables-upgrade.env $DOCKER_ENV_PATH
|
||||
|
||||
rm $PLANE_INSTALL_DIR/temp.yaml
|
||||
fi
|
||||
syncEnvFile
|
||||
|
||||
if [ $USE_GLOBAL_IMAGES == 0 ]; then
|
||||
local res=$(buildLocalImage)
|
||||
# echo $res
|
||||
if [ "$LOCAL_BUILD" == "true" ]; then
|
||||
export DOCKERHUB_USER="myplane"
|
||||
export APP_RELEASE="local"
|
||||
export PULL_POLICY="never"
|
||||
CUSTOM_BUILD="true"
|
||||
|
||||
if [ "$res" == "build_exited" ];
|
||||
then
|
||||
echo
|
||||
echo "Install action cancelled by you. Exiting now."
|
||||
echo
|
||||
exit 0
|
||||
buildYourOwnImage
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo ""
|
||||
echo "Build failed. Exiting..."
|
||||
exit 1
|
||||
fi
|
||||
updateCustomVariables
|
||||
else
|
||||
/bin/bash -c "$COMPOSE_CMD -f $DOCKER_FILE_PATH --env-file=$DOCKER_ENV_PATH pull"
|
||||
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
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Most recent Stable version is now available for you to use"
|
||||
echo "Most recent version of Plane is now available for you to use"
|
||||
echo ""
|
||||
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 "In case of 'Upgrade', please check the 'plane.env 'file for any new variables and update them accordingly"
|
||||
echo ""
|
||||
|
||||
}
|
||||
function startServices() {
|
||||
/bin/bash -c "$COMPOSE_CMD -f $DOCKER_FILE_PATH --env-file=$DOCKER_ENV_PATH up -d --quiet-pull"
|
||||
/bin/bash -c "$COMPOSE_CMD -f $DOCKER_FILE_PATH --env-file=$DOCKER_ENV_PATH up -d --pull if_not_present --quiet-pull"
|
||||
|
||||
local migrator_container_id=$(docker container ls -aq -f "name=$SERVICE_FOLDER-migrator")
|
||||
if [ -n "$migrator_container_id" ]; then
|
||||
@@ -201,7 +331,7 @@ function upgrade() {
|
||||
|
||||
echo
|
||||
echo "***** DOWNLOADING STABLE VERSION ****"
|
||||
download
|
||||
install
|
||||
|
||||
echo "***** PLEASE VALIDATE AND START SERVICES ****"
|
||||
}
|
||||
@@ -282,7 +412,6 @@ function viewLogs(){
|
||||
echo "INVALID SERVICE NAME SUPPLIED"
|
||||
fi
|
||||
}
|
||||
|
||||
function backupSingleVolume() {
|
||||
backupFolder=$1
|
||||
selectedVolume=$2
|
||||
@@ -299,7 +428,6 @@ 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
|
||||
@@ -329,7 +457,7 @@ function askForAction() {
|
||||
then
|
||||
echo
|
||||
echo "Select a Action you want to perform:"
|
||||
echo " 1) Install (${CPU_ARCH})"
|
||||
echo " 1) Install"
|
||||
echo " 2) Start"
|
||||
echo " 3) Stop"
|
||||
echo " 4) Restart"
|
||||
@@ -351,31 +479,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" ]
|
||||
# askForAction
|
||||
elif [ "$ACTION" == "6" ] || [ "$DEFAULT_ACTION" == "logs" ];
|
||||
then
|
||||
viewLogs $@
|
||||
viewLogs "$@"
|
||||
askForAction
|
||||
elif [ "$ACTION" == "7" ] || [ "$DEFAULT_ACTION" == "backup" ]
|
||||
elif [ "$ACTION" == "7" ] || [ "$DEFAULT_ACTION" == "backup" ];
|
||||
then
|
||||
backupData
|
||||
elif [ "$ACTION" == "8" ]
|
||||
@@ -394,48 +522,38 @@ else
|
||||
COMPOSE_CMD="docker compose"
|
||||
fi
|
||||
|
||||
# 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
|
||||
if [ "$CPU_ARCH" == "x86_64" ] || [ "$CPU_ARCH" == "amd64" ]; then
|
||||
CPU_ARCH="amd64"
|
||||
elif [ "$CPU_ARCH" == "aarch64" ] || [ "$CPU_ARCH" == "arm64" ]; then
|
||||
CPU_ARCH="arm64"
|
||||
fi
|
||||
|
||||
if [ "$BRANCH" == "master" ];
|
||||
then
|
||||
export APP_RELEASE=stable
|
||||
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")
|
||||
|
||||
# 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 "$DOCKERHUB_USER" ]; then
|
||||
DOCKERHUB_USER=makeplane
|
||||
updateEnvFile "DOCKERHUB_USER" "$DOCKERHUB_USER" "$DOCKER_ENV_PATH"
|
||||
fi
|
||||
|
||||
DOCKER_FILE_PATH=$PLANE_INSTALL_DIR/docker-compose.yaml
|
||||
DOCKER_ENV_PATH=$PLANE_INSTALL_DIR/plane.env
|
||||
if [ -z "$APP_RELEASE" ]; then
|
||||
APP_RELEASE=stable
|
||||
updateEnvFile "APP_RELEASE" "$APP_RELEASE" "$DOCKER_ENV_PATH"
|
||||
fi
|
||||
|
||||
# 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"
|
||||
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"
|
||||
fi
|
||||
fi
|
||||
|
||||
print_header
|
||||
askForAction $@
|
||||
askForAction "$@"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
APP_DOMAIN=localhost
|
||||
APP_RELEASE=stable
|
||||
|
||||
WEB_REPLICAS=1
|
||||
@@ -6,11 +7,11 @@ ADMIN_REPLICAS=1
|
||||
API_REPLICAS=1
|
||||
|
||||
NGINX_PORT=80
|
||||
WEB_URL=http://localhost
|
||||
WEB_URL=http://${APP_DOMAIN}
|
||||
DEBUG=0
|
||||
SENTRY_DSN=
|
||||
SENTRY_ENVIRONMENT=production
|
||||
CORS_ALLOWED_ORIGINS=http://localhost
|
||||
CORS_ALLOWED_ORIGINS=http://${APP_DOMAIN}
|
||||
|
||||
#DB SETTINGS
|
||||
PGHOST=plane-db
|
||||
@@ -46,4 +47,5 @@ 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
|
||||
|
||||
|
||||
@@ -78,10 +78,11 @@ const DocumentEditor = (props: IDocumentEditor) => {
|
||||
|
||||
return (
|
||||
<PageRenderer
|
||||
tabIndex={tabIndex}
|
||||
editor={editor}
|
||||
editorContainerClassName={editorContainerClassNames}
|
||||
hideDragHandle={hideDragHandleOnMouseLeave}
|
||||
id={id}
|
||||
tabIndex={tabIndex}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -21,11 +21,12 @@ type IPageRenderer = {
|
||||
editor: Editor;
|
||||
editorContainerClassName: string;
|
||||
hideDragHandle?: () => void;
|
||||
id: string;
|
||||
tabIndex?: number;
|
||||
};
|
||||
|
||||
export const PageRenderer = (props: IPageRenderer) => {
|
||||
const { tabIndex, editor, hideDragHandle, editorContainerClassName } = props;
|
||||
const { editor, editorContainerClassName, hideDragHandle, id, tabIndex } = props;
|
||||
// states
|
||||
const [linkViewProps, setLinkViewProps] = useState<LinkViewProps>();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@@ -130,10 +131,11 @@ export const PageRenderer = (props: IPageRenderer) => {
|
||||
<div className="frame-renderer flex-grow w-full -mx-5" onMouseOver={handleLinkHover}>
|
||||
<EditorContainer
|
||||
editor={editor}
|
||||
hideDragHandle={hideDragHandle}
|
||||
editorContainerClassName={editorContainerClassName}
|
||||
hideDragHandle={hideDragHandle}
|
||||
id={id}
|
||||
>
|
||||
<EditorContentWrapper tabIndex={tabIndex} editor={editor} />
|
||||
<EditorContentWrapper editor={editor} id={id} tabIndex={tabIndex} />
|
||||
{editor && editor.isEditable && <BlockMenu editor={editor} />}
|
||||
</EditorContainer>
|
||||
</div>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { TEmbedConfig } from "@/plane-editor/types";
|
||||
import { EditorReadOnlyRefApi, IMentionHighlight } from "@/types";
|
||||
|
||||
interface IDocumentReadOnlyEditor {
|
||||
id: string;
|
||||
initialValue: string;
|
||||
containerClassName: string;
|
||||
editorClassName?: string;
|
||||
@@ -30,6 +31,7 @@ const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => {
|
||||
containerClassName,
|
||||
editorClassName = "",
|
||||
embedHandler,
|
||||
id,
|
||||
initialValue,
|
||||
forwardedRef,
|
||||
tabIndex,
|
||||
@@ -58,7 +60,9 @@ const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => {
|
||||
containerClassName,
|
||||
});
|
||||
|
||||
return <PageRenderer tabIndex={tabIndex} editor={editor} editorContainerClassName={editorContainerClassName} />;
|
||||
return (
|
||||
<PageRenderer editor={editor} editorContainerClassName={editorContainerClassName} id={id} tabIndex={tabIndex} />
|
||||
);
|
||||
};
|
||||
|
||||
const DocumentReadOnlyEditorWithRef = forwardRef<EditorReadOnlyRefApi, IDocumentReadOnlyEditor>((props, ref) => (
|
||||
|
||||
@@ -4,14 +4,15 @@ import { Editor } from "@tiptap/react";
|
||||
import { cn } from "@/helpers/common";
|
||||
|
||||
interface EditorContainerProps {
|
||||
children: ReactNode;
|
||||
editor: Editor | null;
|
||||
editorContainerClassName: string;
|
||||
children: ReactNode;
|
||||
hideDragHandle?: () => void;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export const EditorContainer: FC<EditorContainerProps> = (props) => {
|
||||
const { editor, editorContainerClassName, hideDragHandle, children } = props;
|
||||
const { children, editor, editorContainerClassName, hideDragHandle, id } = props;
|
||||
|
||||
const handleContainerClick = () => {
|
||||
if (!editor) return;
|
||||
@@ -54,7 +55,7 @@ export const EditorContainer: FC<EditorContainerProps> = (props) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
id="editor-container"
|
||||
id={`editor-container-${id}`}
|
||||
onClick={handleContainerClick}
|
||||
onMouseLeave={hideDragHandle}
|
||||
className={cn(
|
||||
|
||||
@@ -4,18 +4,19 @@ import { Editor, EditorContent } from "@tiptap/react";
|
||||
import { ImageResizer } from "@/extensions/image";
|
||||
|
||||
interface EditorContentProps {
|
||||
editor: Editor | null;
|
||||
children?: ReactNode;
|
||||
editor: Editor | null;
|
||||
id: string;
|
||||
tabIndex?: number;
|
||||
}
|
||||
|
||||
export const EditorContentWrapper: FC<EditorContentProps> = (props) => {
|
||||
const { editor, tabIndex, children } = props;
|
||||
const { editor, children, id, tabIndex } = props;
|
||||
|
||||
return (
|
||||
<div tabIndex={tabIndex} onFocus={() => editor?.chain().focus(undefined, { scrollIntoView: false }).run()}>
|
||||
<EditorContent editor={editor} />
|
||||
{editor?.isActive("image") && editor?.isEditable && <ImageResizer editor={editor} />}
|
||||
{editor?.isActive("image") && editor?.isEditable && <ImageResizer editor={editor} id={id} />}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -21,7 +21,7 @@ export const EditorWrapper: React.FC<Props> = (props) => {
|
||||
editorClassName = "",
|
||||
extensions,
|
||||
hideDragHandleOnMouseLeave,
|
||||
id = "",
|
||||
id,
|
||||
initialValue,
|
||||
fileHandler,
|
||||
forwardedRef,
|
||||
@@ -57,13 +57,14 @@ export const EditorWrapper: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<EditorContainer
|
||||
hideDragHandle={hideDragHandleOnMouseLeave}
|
||||
editor={editor}
|
||||
editorContainerClassName={editorContainerClassName}
|
||||
id={id}
|
||||
hideDragHandle={hideDragHandleOnMouseLeave}
|
||||
>
|
||||
{children?.(editor)}
|
||||
<div className="flex flex-col">
|
||||
<EditorContentWrapper tabIndex={tabIndex} editor={editor} />
|
||||
<EditorContentWrapper editor={editor} id={id} tabIndex={tabIndex} />
|
||||
</div>
|
||||
</EditorContainer>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useReadOnlyEditor } from "@/hooks/use-read-only-editor";
|
||||
import { IReadOnlyEditorProps } from "@/types";
|
||||
|
||||
export const ReadOnlyEditorWrapper = (props: IReadOnlyEditorProps) => {
|
||||
const { containerClassName, editorClassName = "", initialValue, forwardedRef, mentionHandler } = props;
|
||||
const { containerClassName, editorClassName = "", id, initialValue, forwardedRef, mentionHandler } = props;
|
||||
|
||||
const editor = useReadOnlyEditor({
|
||||
initialValue,
|
||||
@@ -24,9 +24,9 @@ export const ReadOnlyEditorWrapper = (props: IReadOnlyEditorProps) => {
|
||||
if (!editor) return null;
|
||||
|
||||
return (
|
||||
<EditorContainer editor={editor} editorContainerClassName={editorContainerClassName}>
|
||||
<EditorContainer editor={editor} editorContainerClassName={editorContainerClassName} id={id}>
|
||||
<div className="flex flex-col">
|
||||
<EditorContentWrapper editor={editor} />
|
||||
<EditorContentWrapper editor={editor} id={id} />
|
||||
</div>
|
||||
</EditorContainer>
|
||||
);
|
||||
|
||||
@@ -17,6 +17,7 @@ export const EnterKeyExtension = (onEnterKeyPress?: (descriptionHTML: string) =>
|
||||
editor.commands.first(({ commands }) => [
|
||||
() => commands.newlineInCode(),
|
||||
() => commands.splitListItem("listItem"),
|
||||
() => commands.splitListItem("taskItem"),
|
||||
() => commands.createParagraphNear(),
|
||||
() => commands.liftEmptyBlock(),
|
||||
() => commands.splitBlock(),
|
||||
|
||||
@@ -2,75 +2,84 @@ import { useState } from "react";
|
||||
import { Editor } from "@tiptap/react";
|
||||
import Moveable from "react-moveable";
|
||||
|
||||
export const ImageResizer = ({ editor }: { editor: Editor }) => {
|
||||
const updateMediaSize = () => {
|
||||
const imageInfo = document.querySelector(".ProseMirror-selectednode") as HTMLImageElement;
|
||||
if (imageInfo) {
|
||||
const selection = editor.state.selection;
|
||||
type Props = {
|
||||
editor: Editor;
|
||||
id: string;
|
||||
};
|
||||
|
||||
// Use the style width/height if available, otherwise fall back to the element's natural width/height
|
||||
const width = imageInfo.style.width
|
||||
? Number(imageInfo.style.width.replace("px", ""))
|
||||
: imageInfo.getAttribute("width");
|
||||
const height = imageInfo.style.height
|
||||
? Number(imageInfo.style.height.replace("px", ""))
|
||||
: imageInfo.getAttribute("height");
|
||||
|
||||
editor.commands.setImage({
|
||||
src: imageInfo.src,
|
||||
width: width,
|
||||
height: height,
|
||||
} as any);
|
||||
editor.commands.setNodeSelection(selection.from);
|
||||
}
|
||||
};
|
||||
const getImageElement = (editorId: string): HTMLImageElement | null =>
|
||||
document.querySelector(`#editor-container-${editorId}.active-editor .ProseMirror-selectednode`);
|
||||
|
||||
export const ImageResizer = (props: Props) => {
|
||||
const { editor, id } = props;
|
||||
// states
|
||||
const [aspectRatio, setAspectRatio] = useState(1);
|
||||
|
||||
const updateMediaSize = () => {
|
||||
const imageElement = getImageElement(id);
|
||||
|
||||
if (!imageElement) return;
|
||||
|
||||
const selection = editor.state.selection;
|
||||
|
||||
// Use the style width/height if available, otherwise fall back to the element's natural width/height
|
||||
const width = imageElement.style.width
|
||||
? Number(imageElement.style.width.replace("px", ""))
|
||||
: imageElement.getAttribute("width");
|
||||
const height = imageElement.style.height
|
||||
? Number(imageElement.style.height.replace("px", ""))
|
||||
: imageElement.getAttribute("height");
|
||||
|
||||
editor.commands.setImage({
|
||||
src: imageElement.src,
|
||||
width: width,
|
||||
height: height,
|
||||
} as any);
|
||||
editor.commands.setNodeSelection(selection.from);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Moveable
|
||||
target={document.querySelector(".ProseMirror-selectednode") as HTMLElement}
|
||||
container={null}
|
||||
origin={false}
|
||||
edge={false}
|
||||
throttleDrag={0}
|
||||
keepRatio
|
||||
resizable
|
||||
throttleResize={0}
|
||||
onResizeStart={() => {
|
||||
const imageInfo = document.querySelector(".ProseMirror-selectednode") as HTMLImageElement;
|
||||
if (imageInfo) {
|
||||
const originalWidth = Number(imageInfo.width);
|
||||
const originalHeight = Number(imageInfo.height);
|
||||
setAspectRatio(originalWidth / originalHeight);
|
||||
<Moveable
|
||||
target={getImageElement(id)}
|
||||
container={null}
|
||||
origin={false}
|
||||
edge={false}
|
||||
throttleDrag={0}
|
||||
keepRatio
|
||||
resizable
|
||||
throttleResize={0}
|
||||
onResizeStart={() => {
|
||||
const imageElement = getImageElement(id);
|
||||
if (imageElement) {
|
||||
const originalWidth = Number(imageElement.width);
|
||||
const originalHeight = Number(imageElement.height);
|
||||
setAspectRatio(originalWidth / originalHeight);
|
||||
}
|
||||
}}
|
||||
onResize={({ target, width, height, delta }) => {
|
||||
if (delta[0] || delta[1]) {
|
||||
let newWidth, newHeight;
|
||||
if (delta[0]) {
|
||||
// Width change detected
|
||||
newWidth = Math.max(width, 100);
|
||||
newHeight = newWidth / aspectRatio;
|
||||
} else if (delta[1]) {
|
||||
// Height change detected
|
||||
newHeight = Math.max(height, 100);
|
||||
newWidth = newHeight * aspectRatio;
|
||||
}
|
||||
}}
|
||||
onResize={({ target, width, height, delta }) => {
|
||||
if (delta[0] || delta[1]) {
|
||||
let newWidth, newHeight;
|
||||
if (delta[0]) {
|
||||
// Width change detected
|
||||
newWidth = Math.max(width, 100);
|
||||
newHeight = newWidth / aspectRatio;
|
||||
} else if (delta[1]) {
|
||||
// Height change detected
|
||||
newHeight = Math.max(height, 100);
|
||||
newWidth = newHeight * aspectRatio;
|
||||
}
|
||||
target.style.width = `${newWidth}px`;
|
||||
target.style.height = `${newHeight}px`;
|
||||
}
|
||||
}}
|
||||
onResizeEnd={() => {
|
||||
updateMediaSize();
|
||||
}}
|
||||
scalable
|
||||
renderDirections={["se"]}
|
||||
onScale={({ target, transform }) => {
|
||||
target.style.transform = transform;
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
target.style.width = `${newWidth}px`;
|
||||
target.style.height = `${newHeight}px`;
|
||||
}
|
||||
}}
|
||||
onResizeEnd={() => {
|
||||
updateMediaSize();
|
||||
}}
|
||||
scalable
|
||||
renderDirections={["se"]}
|
||||
onScale={({ target, transform }) => {
|
||||
target.style.transform = transform;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -91,7 +91,8 @@ export const CustomMention = ({
|
||||
// @ts-expect-error - Tippy types are incorrect
|
||||
popup = tippy("body", {
|
||||
getReferenceClientRect: props.clientRect,
|
||||
appendTo: () => document.querySelector(".active-editor") ?? document.querySelector("#editor-container"),
|
||||
appendTo: () =>
|
||||
document.querySelector(".active-editor") ?? document.querySelector('[id^="editor-container"]'),
|
||||
content: component.element,
|
||||
showOnCreate: true,
|
||||
interactive: true,
|
||||
|
||||
@@ -374,7 +374,8 @@ const renderItems = () => {
|
||||
editor: props.editor,
|
||||
});
|
||||
|
||||
const tippyContainer = document.querySelector(".active-editor") ?? document.querySelector("#editor-container");
|
||||
const tippyContainer =
|
||||
document.querySelector(".active-editor") ?? document.querySelector('[id^="editor-container"]');
|
||||
|
||||
// @ts-expect-error Tippy overloads are messed up
|
||||
popup = tippy("body", {
|
||||
|
||||
@@ -29,7 +29,7 @@ export interface IEditorProps {
|
||||
editorClassName?: string;
|
||||
fileHandler: TFileHandler;
|
||||
forwardedRef?: React.MutableRefObject<EditorRefApi | null>;
|
||||
id?: string;
|
||||
id: string;
|
||||
initialValue: string;
|
||||
mentionHandler: {
|
||||
highlights: () => Promise<IMentionHighlight[]>;
|
||||
@@ -52,6 +52,7 @@ export interface IReadOnlyEditorProps {
|
||||
containerClassName?: string;
|
||||
editorClassName?: string;
|
||||
forwardedRef?: React.MutableRefObject<EditorReadOnlyRefApi | null>;
|
||||
id: string;
|
||||
initialValue: string;
|
||||
mentionHandler: {
|
||||
highlights: () => Promise<IMentionHighlight[]>;
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
export type IFavorite = {
|
||||
id: string;
|
||||
name: string;
|
||||
entity_type: string;
|
||||
entity_data: {
|
||||
name: string;
|
||||
};
|
||||
is_folder: boolean;
|
||||
sort_order: number;
|
||||
parent: string | null;
|
||||
entity_identifier?: string | null;
|
||||
children: IFavorite[];
|
||||
project_id: string | null;
|
||||
sequence: number;
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./favorite";
|
||||
Vendored
+1
@@ -28,3 +28,4 @@ export * from "./common";
|
||||
export * from "./pragmatic";
|
||||
export * from "./publish";
|
||||
export * from "./workspace-notifications";
|
||||
export * from "./favorite";
|
||||
|
||||
Vendored
+1
-1
@@ -42,7 +42,7 @@ export type TBaseIssue = {
|
||||
export type TIssue = TBaseIssue & {
|
||||
description_html?: string;
|
||||
is_subscribed?: boolean;
|
||||
parent?: partial<TIssue>;
|
||||
parent?: Partial<TBaseIssue>;
|
||||
issue_reactions?: TIssueReaction[];
|
||||
issue_attachment?: TIssueAttachment[];
|
||||
issue_link?: TIssueLink[];
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { ISvgIcons } from "./type";
|
||||
|
||||
export const FavoriteFolderIcon: React.FC<ISvgIcons> = ({ className = "text-current", color = "#a3a3a3", ...rest }) => (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
stroke={color}
|
||||
className={`${className} stroke-2`}
|
||||
{...rest}
|
||||
>
|
||||
<path
|
||||
d="M7.33325 13.3334H2.66659C2.31296 13.3334 1.97382 13.1929 1.72378 12.9429C1.47373 12.6928 1.33325 12.3537 1.33325 12.0001V3.3334C1.33325 2.97978 1.47373 2.64064 1.72378 2.39059C1.97382 2.14054 2.31296 2.00006 2.66659 2.00006H5.26659C5.48958 1.99788 5.70955 2.05166 5.90638 2.15648C6.10322 2.2613 6.27061 2.41381 6.39325 2.60006L6.93325 3.40006C7.05466 3.58442 7.21994 3.73574 7.41425 3.84047C7.60857 3.94519 7.82585 4.00003 8.04658 4.00006H13.3333C13.6869 4.00006 14.026 4.14054 14.2761 4.39059C14.5261 4.64064 14.6666 4.97978 14.6666 5.3334V6.3334"
|
||||
// stroke="#60646C"
|
||||
stroke-width="1.25"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M12.1373 8L13.0038 9.75535L14.9414 10.0386L13.5394 11.4041L13.8702 13.3333L12.1373 12.422L10.4044 13.3333L10.7353 11.4041L9.33325 10.0386L11.2709 9.75535L12.1373 8Z"
|
||||
stroke-width="1.25"
|
||||
// stroke="#60646C"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -24,3 +24,4 @@ export * from "./info-icon";
|
||||
export * from "./dropdown-icon";
|
||||
export * from "./intake";
|
||||
export * from "./user-activity-icon";
|
||||
export * from "./favorite-folder-icon";
|
||||
|
||||
@@ -6,6 +6,7 @@ import useSWR from "swr";
|
||||
// components
|
||||
import { LogoSpinner } from "@/components/common";
|
||||
import { IssuesNavbarRoot } from "@/components/issues";
|
||||
import { SomethingWentWrongError } from "@/components/issues/issue-layouts/error";
|
||||
// hooks
|
||||
import { useIssueFilter, usePublish, usePublishList } from "@/hooks/store";
|
||||
// assets
|
||||
@@ -27,7 +28,7 @@ const IssuesLayout = observer((props: Props) => {
|
||||
const publishSettings = usePublish(anchor);
|
||||
const { updateLayoutOptions } = useIssueFilter();
|
||||
// fetch publish settings
|
||||
useSWR(
|
||||
const { error } = useSWR(
|
||||
anchor ? `PUBLISH_SETTINGS_${anchor}` : null,
|
||||
anchor
|
||||
? async () => {
|
||||
@@ -45,7 +46,9 @@ const IssuesLayout = observer((props: Props) => {
|
||||
: null
|
||||
);
|
||||
|
||||
if (!publishSettings) return <LogoSpinner />;
|
||||
if (!publishSettings && !error) return <LogoSpinner />;
|
||||
|
||||
if (error) return <SomethingWentWrongError />;
|
||||
|
||||
return (
|
||||
<div className="relative flex h-screen min-h-[500px] w-screen flex-col overflow-hidden">
|
||||
|
||||
@@ -5,6 +5,7 @@ import Image from "next/image";
|
||||
import useSWR from "swr";
|
||||
// components
|
||||
import { LogoSpinner } from "@/components/common";
|
||||
import { SomethingWentWrongError } from "@/components/issues/issue-layouts/error";
|
||||
// hooks
|
||||
import { usePublish, usePublishList } from "@/hooks/store";
|
||||
// Plane web
|
||||
@@ -28,25 +29,22 @@ const IssuesLayout = observer((props: Props) => {
|
||||
const { fetchPublishSettings } = usePublishList();
|
||||
const { viewData, fetchViewDetails } = useView();
|
||||
const publishSettings = usePublish(anchor);
|
||||
// fetch publish settings
|
||||
useSWR(
|
||||
anchor ? `PUBLISH_SETTINGS_${anchor}` : null,
|
||||
|
||||
// fetch publish settings && view details
|
||||
const { error } = useSWR(
|
||||
anchor ? `PUBLISHED_VIEW_SETTINGS_${anchor}` : null,
|
||||
anchor
|
||||
? async () => {
|
||||
await fetchPublishSettings(anchor);
|
||||
}
|
||||
: null
|
||||
);
|
||||
// fetch view data
|
||||
useSWR(
|
||||
anchor ? `VIEW_DETAILS_${anchor}` : null,
|
||||
anchor
|
||||
? async () => {
|
||||
await fetchViewDetails(anchor);
|
||||
const promises = [];
|
||||
promises.push(fetchPublishSettings(anchor));
|
||||
promises.push(fetchViewDetails(anchor));
|
||||
await Promise.all(promises);
|
||||
}
|
||||
: null
|
||||
);
|
||||
|
||||
if (error) return <SomethingWentWrongError />;
|
||||
|
||||
if (!publishSettings || !viewData) return <LogoSpinner />;
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import Image from "next/image";
|
||||
// assets
|
||||
import SomethingWentWrongImage from "public/something-went-wrong.svg";
|
||||
|
||||
export const SomethingWentWrongError = () => (
|
||||
<div className="grid h-full w-full place-items-center p-6">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto grid h-52 w-52 place-items-center rounded-full bg-custom-background-80">
|
||||
<div className="grid h-32 w-32 place-items-center">
|
||||
<Image src={SomethingWentWrongImage} alt="Oops! Something went wrong" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="mt-12 text-3xl font-semibold">Oops! Something went wrong.</h1>
|
||||
<p className="mt-4 text-custom-text-300">The public board does not exist. Please check the URL.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,45 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
//plane
|
||||
import { cn } from "@plane/editor";
|
||||
// components
|
||||
import { IssueEmojiReactions, IssueVotes } from "@/components/issues/reactions";
|
||||
// hooks
|
||||
import { usePublish } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
issueId: string;
|
||||
};
|
||||
export const BlockReactions = observer((props: Props) => {
|
||||
const { issueId } = props;
|
||||
const { anchor } = useParams();
|
||||
const { canVote, canReact } = usePublish(anchor.toString());
|
||||
|
||||
// if the user cannot vote or react then return empty
|
||||
if (!canVote && !canReact) return <></>;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-wrap border-t-[1px] outline-transparent w-full border-t-custom-border-200 bg-custom-background-90 rounded-b"
|
||||
)}
|
||||
>
|
||||
<div className="py-2 px-3 flex flex-wrap items-center gap-2">
|
||||
{canVote && (
|
||||
<div
|
||||
className={cn(`flex items-center gap-2 pr-1`, {
|
||||
"after:h-6 after:ml-1 after:w-[1px] after:bg-custom-border-200": canReact,
|
||||
})}
|
||||
>
|
||||
<IssueVotes anchor={anchor.toString()} issueIdFromProps={issueId} size="sm" />
|
||||
</div>
|
||||
)}
|
||||
{canReact && (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<IssueEmojiReactions anchor={anchor.toString()} issueIdFromProps={issueId} size="sm" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -14,10 +14,11 @@ import { WithDisplayPropertiesHOC } from "@/components/issues/issue-layouts/with
|
||||
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
||||
// hooks
|
||||
import { useIssueDetails, usePublish } from "@/hooks/store";
|
||||
|
||||
//
|
||||
import { IIssue } from "@/types/issue";
|
||||
import { IssueProperties } from "../properties/all-properties";
|
||||
import { getIssueBlockId } from "../utils";
|
||||
import { BlockReactions } from "./block-reactions";
|
||||
|
||||
interface IssueBlockProps {
|
||||
issueId: string;
|
||||
@@ -83,17 +84,22 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = observer((props) => {
|
||||
|
||||
return (
|
||||
<div className={cn("group/kanban-block relative p-1.5")}>
|
||||
<Link
|
||||
id={getIssueBlockId(issueId, groupId, subGroupId)}
|
||||
<div
|
||||
className={cn(
|
||||
"block rounded border-[1px] outline-[0.5px] outline-transparent w-full border-custom-border-200 bg-custom-background-100 text-sm transition-all hover:border-custom-border-400",
|
||||
"relative block rounded border-[1px] outline-[0.5px] outline-transparent w-full border-custom-border-200 bg-custom-background-100 text-sm transition-all hover:border-custom-border-400",
|
||||
{ "border border-custom-primary-70 hover:border-custom-primary-70": getIsIssuePeeked(issue.id) }
|
||||
)}
|
||||
href={`?${queryParam}`}
|
||||
onClick={handleIssuePeekOverview}
|
||||
>
|
||||
<KanbanIssueDetailsBlock issue={issue} displayProperties={displayProperties} />
|
||||
</Link>
|
||||
<Link
|
||||
id={getIssueBlockId(issueId, groupId, subGroupId)}
|
||||
className="w-full"
|
||||
href={`?${queryParam}`}
|
||||
onClick={handleIssuePeekOverview}
|
||||
>
|
||||
<KanbanIssueDetailsBlock issue={issue} displayProperties={displayProperties} />
|
||||
</Link>
|
||||
<BlockReactions issueId={issueId} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -64,22 +64,15 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
if (!groupList) return null;
|
||||
|
||||
const visibilityGroupBy = (_list: IGroupByColumn): { showGroup: boolean; showIssues: boolean } => {
|
||||
if (subGroupBy) {
|
||||
const groupVisibility = {
|
||||
showGroup: true,
|
||||
showIssues: true,
|
||||
};
|
||||
if (!showEmptyGroup) {
|
||||
groupVisibility.showGroup = (getGroupIssueCount(_list.id, undefined, false) ?? 0) > 0;
|
||||
}
|
||||
return groupVisibility;
|
||||
} else {
|
||||
const groupVisibility = {
|
||||
showGroup: true,
|
||||
showIssues: true,
|
||||
};
|
||||
return groupVisibility;
|
||||
const groupVisibility = {
|
||||
showGroup: true,
|
||||
showIssues: true,
|
||||
};
|
||||
|
||||
if (!showEmptyGroup) {
|
||||
groupVisibility.showGroup = (getGroupIssueCount(_list.id, undefined, false) ?? 0) > 0;
|
||||
}
|
||||
return groupVisibility;
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { FC, useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Image from "next/image";
|
||||
import useSWR from "swr";
|
||||
// components
|
||||
import { IssueKanbanLayoutRoot, IssuesListLayoutRoot } from "@/components/issues";
|
||||
@@ -13,7 +12,7 @@ import { useIssue, useIssueDetails, useIssueFilter } from "@/hooks/store";
|
||||
// store
|
||||
import { PublishStore } from "@/store/publish/publish.store";
|
||||
// assets
|
||||
import SomethingWentWrongImage from "public/something-went-wrong.svg";
|
||||
import { SomethingWentWrongError } from "./error";
|
||||
|
||||
type Props = {
|
||||
peekId: string | undefined;
|
||||
@@ -24,7 +23,7 @@ export const IssuesLayoutsRoot: FC<Props> = observer((props) => {
|
||||
const { peekId, publishSettings } = props;
|
||||
// store hooks
|
||||
const { getIssueFilters } = useIssueFilter();
|
||||
const { loader, groupedIssueIds, fetchPublicIssues } = useIssue();
|
||||
const { fetchPublicIssues } = useIssue();
|
||||
const issueDetailStore = useIssueDetails();
|
||||
// derived values
|
||||
const { anchor } = publishSettings;
|
||||
@@ -48,46 +47,27 @@ export const IssuesLayoutsRoot: FC<Props> = observer((props) => {
|
||||
|
||||
if (!anchor) return null;
|
||||
|
||||
if (error) return <SomethingWentWrongError />;
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full overflow-hidden">
|
||||
{peekId && <IssuePeekOverview anchor={anchor} peekId={peekId} />}
|
||||
{activeLayout && (
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
{/* applied filters */}
|
||||
<IssueAppliedFilters anchor={anchor} />
|
||||
|
||||
{loader && !groupedIssueIds ? (
|
||||
<div className="py-10 text-center text-sm text-custom-text-100">Loading...</div>
|
||||
) : (
|
||||
<>
|
||||
{error ? (
|
||||
<div className="grid h-full w-full place-items-center p-6">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto grid h-52 w-52 place-items-center rounded-full bg-custom-background-80">
|
||||
<div className="grid h-32 w-32 place-items-center">
|
||||
<Image src={SomethingWentWrongImage} alt="Oops! Something went wrong" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="mt-12 text-3xl font-semibold">Oops! Something went wrong.</h1>
|
||||
<p className="mt-4 text-custom-text-300">The public board does not exist. Please check the URL.</p>
|
||||
</div>
|
||||
{activeLayout === "list" && (
|
||||
<div className="relative h-full w-full overflow-y-auto">
|
||||
<IssuesListLayoutRoot anchor={anchor} />
|
||||
</div>
|
||||
) : (
|
||||
activeLayout && (
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
{/* applied filters */}
|
||||
<IssueAppliedFilters anchor={anchor} />
|
||||
|
||||
{activeLayout === "list" && (
|
||||
<div className="relative h-full w-full overflow-y-auto">
|
||||
<IssuesListLayoutRoot anchor={anchor} />
|
||||
</div>
|
||||
)}
|
||||
{activeLayout === "kanban" && (
|
||||
<div className="relative mx-auto h-full w-full p-5">
|
||||
<IssueKanbanLayoutRoot anchor={anchor} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
{activeLayout === "kanban" && (
|
||||
<div className="relative mx-auto h-full w-full p-5">
|
||||
<IssueKanbanLayoutRoot anchor={anchor} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -72,6 +72,7 @@ export const AddComment: React.FC<Props> = observer((props) => {
|
||||
workspaceId={workspaceID?.toString() ?? ""}
|
||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
ref={editorRef}
|
||||
id="peek-overview-add-comment"
|
||||
initialValue={
|
||||
!value || value === "" || (typeof value === "object" && Object.keys(value).length === 0)
|
||||
? watch("comment_html")
|
||||
|
||||
@@ -105,6 +105,7 @@ export const CommentCard: React.FC<Props> = observer((props) => {
|
||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
onEnterKeyPress={() => handleSubmit(handleCommentUpdate)()}
|
||||
ref={editorRef}
|
||||
id={comment.id}
|
||||
initialValue={value}
|
||||
value={null}
|
||||
onChange={(comment_json, comment_html) => onChange(comment_html)}
|
||||
@@ -132,7 +133,7 @@ export const CommentCard: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
</form>
|
||||
<div className={`${isEditing ? "hidden" : ""}`}>
|
||||
<LiteTextReadOnlyEditor ref={showEditorRef} initialValue={comment.comment_html} />
|
||||
<LiteTextReadOnlyEditor ref={showEditorRef} id={comment.id} initialValue={comment.comment_html} />
|
||||
<CommentReactions anchor={anchor} commentId={comment.id} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,5 +7,3 @@ export * from "./issue-properties";
|
||||
export * from "./layout";
|
||||
export * from "./side-peek-view";
|
||||
export * from "./issue-reaction";
|
||||
export * from "./issue-vote-reactions";
|
||||
export * from "./issue-emoji-reactions";
|
||||
|
||||
@@ -26,6 +26,7 @@ export const PeekOverviewIssueDetails: React.FC<Props> = observer((props) => {
|
||||
<h4 className="break-words text-2xl font-medium">{issueDetails.name}</h4>
|
||||
{description !== "" && description !== "<p></p>" && (
|
||||
<RichTextReadOnlyEditor
|
||||
id={issueDetails.id}
|
||||
initialValue={
|
||||
!description ||
|
||||
description === "" ||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { IssueEmojiReactions, IssueVotes } from "@/components/issues/peek-overview";
|
||||
import { IssueEmojiReactions, IssueVotes } from "@/components/issues/reactions";
|
||||
// hooks
|
||||
import { usePublish } from "@/hooks/store";
|
||||
import useIsInIframe from "@/hooks/use-is-in-iframe";
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./issue-emoji-reactions";
|
||||
export * from "./issue-vote-reactions";
|
||||
+46
-45
@@ -13,10 +13,12 @@ import { useIssueDetails, useUser } from "@/hooks/store";
|
||||
|
||||
type IssueEmojiReactionsProps = {
|
||||
anchor: string;
|
||||
issueIdFromProps?: string;
|
||||
size?: "md" | "sm";
|
||||
};
|
||||
|
||||
export const IssueEmojiReactions: React.FC<IssueEmojiReactionsProps> = observer((props) => {
|
||||
const { anchor } = props;
|
||||
const { anchor, issueIdFromProps, size = "md" } = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
const pathName = usePathname();
|
||||
@@ -31,7 +33,7 @@ export const IssueEmojiReactions: React.FC<IssueEmojiReactionsProps> = observer(
|
||||
const issueDetailsStore = useIssueDetails();
|
||||
const { data: user } = useUser();
|
||||
|
||||
const issueId = issueDetailsStore.peekId;
|
||||
const issueId = issueIdFromProps ?? issueDetailsStore.peekId;
|
||||
const reactions = issueDetailsStore.details[issueId ?? ""]?.reaction_items ?? [];
|
||||
const groupedReactions = groupReactions(reactions, "reaction");
|
||||
|
||||
@@ -55,6 +57,7 @@ export const IssueEmojiReactions: React.FC<IssueEmojiReactionsProps> = observer(
|
||||
|
||||
// derived values
|
||||
const { queryParam } = queryParamGenerator({ peekId, board, state, priority, labels });
|
||||
const reactionDimensions = size === "sm" ? "h-6 px-2 py-1" : "h-full px-2 py-1";
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -64,54 +67,52 @@ export const IssueEmojiReactions: React.FC<IssueEmojiReactionsProps> = observer(
|
||||
else router.push(`/?next_path=${pathName}?${queryParam}`);
|
||||
}}
|
||||
selected={userReactions?.map((r) => r.reaction)}
|
||||
size="md"
|
||||
size={size}
|
||||
/>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{Object.keys(groupedReactions || {}).map((reaction) => {
|
||||
const reactions = groupedReactions?.[reaction] ?? [];
|
||||
const REACTIONS_LIMIT = 1000;
|
||||
{Object.keys(groupedReactions || {}).map((reaction) => {
|
||||
const reactions = groupedReactions?.[reaction] ?? [];
|
||||
const REACTIONS_LIMIT = 1000;
|
||||
|
||||
if (reactions.length > 0)
|
||||
return (
|
||||
<Tooltip
|
||||
key={reaction}
|
||||
tooltipContent={
|
||||
<div>
|
||||
{reactions
|
||||
?.map((r) => r?.actor_details?.display_name)
|
||||
?.splice(0, REACTIONS_LIMIT)
|
||||
?.join(", ")}
|
||||
{reactions.length > REACTIONS_LIMIT && " and " + (reactions.length - REACTIONS_LIMIT) + " more"}
|
||||
</div>
|
||||
}
|
||||
if (reactions.length > 0)
|
||||
return (
|
||||
<Tooltip
|
||||
key={reaction}
|
||||
tooltipContent={
|
||||
<div>
|
||||
{reactions
|
||||
?.map((r) => r?.actor_details?.display_name)
|
||||
?.splice(0, REACTIONS_LIMIT)
|
||||
?.join(", ")}
|
||||
{reactions.length > REACTIONS_LIMIT && " and " + (reactions.length - REACTIONS_LIMIT) + " more"}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (user) handleReactionClick(reaction);
|
||||
else router.push(`/?next_path=${pathName}?${queryParam}`);
|
||||
}}
|
||||
className={`flex items-center gap-1 rounded-md text-sm text-custom-text-100 ${
|
||||
reactions.some((r) => r?.actor_details?.id === user?.id && r.reaction === reaction)
|
||||
? "bg-custom-primary-100/10"
|
||||
: "bg-custom-background-80"
|
||||
} ${reactionDimensions}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (user) handleReactionClick(reaction);
|
||||
else router.push(`/?next_path=${pathName}?${queryParam}`);
|
||||
}}
|
||||
className={`flex h-full items-center gap-1 rounded-md px-2 py-1 text-sm text-custom-text-100 ${
|
||||
<span>{renderEmoji(reaction)}</span>
|
||||
<span
|
||||
className={
|
||||
reactions.some((r) => r?.actor_details?.id === user?.id && r.reaction === reaction)
|
||||
? "bg-custom-primary-100/10"
|
||||
: "bg-custom-background-80"
|
||||
}`}
|
||||
? "text-custom-primary-100"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<span>{renderEmoji(reaction)}</span>
|
||||
<span
|
||||
className={
|
||||
reactions.some((r) => r?.actor_details?.id === user?.id && r.reaction === reaction)
|
||||
? "text-custom-primary-100"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{groupedReactions?.[reaction].length}{" "}
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{groupedReactions?.[reaction].length}{" "}
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
});
|
||||
+9
-4
@@ -13,10 +13,12 @@ import useIsInIframe from "@/hooks/use-is-in-iframe";
|
||||
|
||||
type TIssueVotes = {
|
||||
anchor: string;
|
||||
issueIdFromProps?: string;
|
||||
size?: "md" | "sm";
|
||||
};
|
||||
|
||||
export const IssueVotes: React.FC<TIssueVotes> = observer((props) => {
|
||||
const { anchor } = props;
|
||||
const { anchor, issueIdFromProps, size = "md" } = props;
|
||||
// states
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
// router
|
||||
@@ -35,7 +37,7 @@ export const IssueVotes: React.FC<TIssueVotes> = observer((props) => {
|
||||
|
||||
const isInIframe = useIsInIframe();
|
||||
|
||||
const issueId = issueDetailsStore.peekId;
|
||||
const issueId = issueIdFromProps ?? issueDetailsStore.peekId;
|
||||
|
||||
const votes = issueDetailsStore.details[issueId ?? ""]?.vote_items ?? [];
|
||||
|
||||
@@ -66,6 +68,7 @@ export const IssueVotes: React.FC<TIssueVotes> = observer((props) => {
|
||||
|
||||
// derived values
|
||||
const { queryParam } = queryParamGenerator({ peekId, board, state, priority, labels });
|
||||
const votingDimensions = size === "sm" ? "px-1 h-6 min-w-9" : "px-2 h-7";
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -96,7 +99,8 @@ export const IssueVotes: React.FC<TIssueVotes> = observer((props) => {
|
||||
else router.push(`/?next_path=${pathName}?${queryParam}`);
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-x-1 overflow-hidden rounded border px-2 h-7 focus:outline-none",
|
||||
"flex items-center justify-center gap-x-1 overflow-hidden rounded border focus:outline-none bg-custom-background-100",
|
||||
votingDimensions,
|
||||
{
|
||||
"border-custom-primary-200 text-custom-primary-200": isUpVotedByUser,
|
||||
"border-custom-border-300": !isUpVotedByUser,
|
||||
@@ -136,7 +140,8 @@ export const IssueVotes: React.FC<TIssueVotes> = observer((props) => {
|
||||
else router.push(`/?next_path=${pathName}?${queryParam}`);
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-x-1 h-7 overflow-hidden rounded border px-2 focus:outline-none",
|
||||
"flex items-center justify-center gap-x-1 overflow-hidden rounded border focus:outline-none bg-custom-background-100",
|
||||
votingDimensions,
|
||||
{
|
||||
"border-red-600 text-red-600": isDownVotedByUser,
|
||||
"border-custom-border-300": !isDownVotedByUser,
|
||||
@@ -8,7 +8,7 @@ export class CycleService extends APIService {
|
||||
}
|
||||
|
||||
async getCycles(anchor: string): Promise<TPublicCycle[]> {
|
||||
return this.get(`api/public/anchor/${anchor}/cycles/`)
|
||||
return this.get(`/api/public/anchor/${anchor}/cycles/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
|
||||
@@ -8,7 +8,7 @@ export class LabelService extends APIService {
|
||||
}
|
||||
|
||||
async getLabels(anchor: string): Promise<IIssueLabel[]> {
|
||||
return this.get(`api/public/anchor/${anchor}/labels/`)
|
||||
return this.get(`/api/public/anchor/${anchor}/labels/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
|
||||
@@ -8,7 +8,7 @@ export class MemberService extends APIService {
|
||||
}
|
||||
|
||||
async getAnchorMembers(anchor: string): Promise<TPublicMember[]> {
|
||||
return this.get(`api/public/anchor/${anchor}/members/`)
|
||||
return this.get(`/api/public/anchor/${anchor}/members/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user