Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a1ac916c07 |
@@ -88,7 +88,7 @@ jobs:
|
||||
|
||||
full_build_push:
|
||||
if: ${{ needs.branch_build_setup.outputs.do_full_build == 'true' }}
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
env:
|
||||
BUILD_TYPE: full
|
||||
@@ -148,7 +148,7 @@ jobs:
|
||||
|
||||
slim_build_push:
|
||||
if: ${{ needs.branch_build_setup.outputs.do_slim_build == 'true' }}
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
env:
|
||||
BUILD_TYPE: slim
|
||||
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
full_build_push:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
env:
|
||||
BUILD_TYPE: full
|
||||
|
||||
@@ -11,7 +11,7 @@ env:
|
||||
|
||||
jobs:
|
||||
sync_changes:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
|
||||
@@ -121,6 +121,8 @@ from .exporter import ExporterHistorySerializer
|
||||
|
||||
from .webhook import WebhookSerializer, WebhookLogSerializer
|
||||
|
||||
from .dashboard import DashboardSerializer, WidgetSerializer
|
||||
|
||||
from .favorite import UserFavoriteSerializer
|
||||
|
||||
from .draft import (
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Module imports
|
||||
from .base import BaseSerializer
|
||||
from plane.db.models import DeprecatedDashboard, DeprecatedWidget
|
||||
|
||||
# Third party frameworks
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class DashboardSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = DeprecatedDashboard
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class WidgetSerializer(BaseSerializer):
|
||||
is_visible = serializers.BooleanField(read_only=True)
|
||||
widget_filters = serializers.JSONField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = DeprecatedWidget
|
||||
fields = ["id", "key", "is_visible", "widget_filters"]
|
||||
@@ -2,6 +2,7 @@ from .analytic import urlpatterns as analytic_urls
|
||||
from .api import urlpatterns as api_urls
|
||||
from .asset import urlpatterns as asset_urls
|
||||
from .cycle import urlpatterns as cycle_urls
|
||||
from .dashboard import urlpatterns as dashboard_urls
|
||||
from .estimate import urlpatterns as estimate_urls
|
||||
from .external import urlpatterns as external_urls
|
||||
from .intake import urlpatterns as intake_urls
|
||||
@@ -22,6 +23,7 @@ urlpatterns = [
|
||||
*analytic_urls,
|
||||
*asset_urls,
|
||||
*cycle_urls,
|
||||
*dashboard_urls,
|
||||
*estimate_urls,
|
||||
*external_urls,
|
||||
*intake_urls,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
from django.urls import path
|
||||
|
||||
|
||||
from plane.app.views import DashboardEndpoint, WidgetsEndpoint
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/dashboard/",
|
||||
DashboardEndpoint.as_view(),
|
||||
name="dashboard",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/dashboard/<uuid:dashboard_id>/",
|
||||
DashboardEndpoint.as_view(),
|
||||
name="dashboard",
|
||||
),
|
||||
path(
|
||||
"dashboard/<uuid:dashboard_id>/widgets/<uuid:widget_id>/",
|
||||
WidgetsEndpoint.as_view(),
|
||||
name="widgets",
|
||||
),
|
||||
]
|
||||
@@ -210,6 +210,8 @@ from .webhook.base import (
|
||||
WebhookSecretRegenerateEndpoint,
|
||||
)
|
||||
|
||||
from .dashboard.base import DashboardEndpoint, WidgetsEndpoint
|
||||
|
||||
from .error_404 import custom_404_view
|
||||
|
||||
from .notification.base import MarkAllReadNotificationViewSet
|
||||
|
||||
@@ -0,0 +1,812 @@
|
||||
# Django imports
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db.models import (
|
||||
Case,
|
||||
CharField,
|
||||
Count,
|
||||
Exists,
|
||||
F,
|
||||
Func,
|
||||
IntegerField,
|
||||
JSONField,
|
||||
OuterRef,
|
||||
Prefetch,
|
||||
Q,
|
||||
Subquery,
|
||||
UUIDField,
|
||||
Value,
|
||||
When,
|
||||
)
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils import timezone
|
||||
from rest_framework import status
|
||||
|
||||
# Third Party imports
|
||||
from rest_framework.response import Response
|
||||
|
||||
from plane.app.serializers import (
|
||||
DashboardSerializer,
|
||||
IssueActivitySerializer,
|
||||
IssueSerializer,
|
||||
WidgetSerializer,
|
||||
)
|
||||
from plane.db.models import (
|
||||
DeprecatedDashboard,
|
||||
DeprecatedDashboardWidget,
|
||||
Issue,
|
||||
IssueActivity,
|
||||
FileAsset,
|
||||
IssueLink,
|
||||
IssueRelation,
|
||||
Project,
|
||||
DeprecatedWidget,
|
||||
WorkspaceMember,
|
||||
CycleIssue,
|
||||
)
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
|
||||
# Module imports
|
||||
from .. import BaseAPIView
|
||||
|
||||
|
||||
def dashboard_overview_stats(self, request, slug):
|
||||
assigned_issues = (
|
||||
Issue.issue_objects.filter(
|
||||
(Q(assignees__in=[request.user]) & Q(issue_assignee__deleted_at__isnull=True)),
|
||||
project__project_projectmember__is_active=True,
|
||||
project__project_projectmember__member=request.user,
|
||||
workspace__slug=slug,
|
||||
)
|
||||
.filter(
|
||||
Q(
|
||||
project__project_projectmember__role=5,
|
||||
project__guest_view_all_features=True,
|
||||
)
|
||||
| Q(
|
||||
project__project_projectmember__role=5,
|
||||
project__guest_view_all_features=False,
|
||||
created_by=self.request.user,
|
||||
)
|
||||
|
|
||||
# For other roles (role < 5), show all issues
|
||||
Q(project__project_projectmember__role__gt=5),
|
||||
project__project_projectmember__member=self.request.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
)
|
||||
.count()
|
||||
)
|
||||
|
||||
pending_issues_count = (
|
||||
Issue.issue_objects.filter(
|
||||
~Q(state__group__in=["completed", "cancelled"]),
|
||||
target_date__lt=timezone.now().date(),
|
||||
project__project_projectmember__is_active=True,
|
||||
project__project_projectmember__member=request.user,
|
||||
workspace__slug=slug,
|
||||
assignees__in=[request.user],
|
||||
)
|
||||
.filter(
|
||||
Q(
|
||||
project__project_projectmember__role=5,
|
||||
project__guest_view_all_features=True,
|
||||
)
|
||||
| Q(
|
||||
project__project_projectmember__role=5,
|
||||
project__guest_view_all_features=False,
|
||||
created_by=self.request.user,
|
||||
)
|
||||
|
|
||||
# For other roles (role < 5), show all issues
|
||||
Q(project__project_projectmember__role__gt=5),
|
||||
project__project_projectmember__member=self.request.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
)
|
||||
.count()
|
||||
)
|
||||
|
||||
created_issues_count = (
|
||||
Issue.issue_objects.filter(
|
||||
workspace__slug=slug,
|
||||
project__project_projectmember__is_active=True,
|
||||
project__project_projectmember__member=request.user,
|
||||
created_by_id=request.user.id,
|
||||
)
|
||||
.filter(
|
||||
Q(
|
||||
project__project_projectmember__role=5,
|
||||
project__guest_view_all_features=True,
|
||||
)
|
||||
| Q(
|
||||
project__project_projectmember__role=5,
|
||||
project__guest_view_all_features=False,
|
||||
created_by=self.request.user,
|
||||
)
|
||||
|
|
||||
# For other roles (role < 5), show all issues
|
||||
Q(project__project_projectmember__role__gt=5),
|
||||
project__project_projectmember__member=self.request.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
)
|
||||
.count()
|
||||
)
|
||||
|
||||
completed_issues_count = (
|
||||
Issue.issue_objects.filter(
|
||||
(
|
||||
Q(assignees__in=[request.user])
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
workspace__slug=slug,
|
||||
project__project_projectmember__is_active=True,
|
||||
project__project_projectmember__member=request.user,
|
||||
state__group="completed",
|
||||
)
|
||||
.filter(
|
||||
Q(
|
||||
project__project_projectmember__role=5,
|
||||
project__guest_view_all_features=True,
|
||||
)
|
||||
| Q(
|
||||
project__project_projectmember__role=5,
|
||||
project__guest_view_all_features=False,
|
||||
created_by=self.request.user,
|
||||
)
|
||||
|
|
||||
# For other roles (role < 5), show all issues
|
||||
Q(project__project_projectmember__role__gt=5),
|
||||
project__project_projectmember__member=self.request.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
)
|
||||
.count()
|
||||
)
|
||||
|
||||
return Response(
|
||||
{
|
||||
"assigned_issues_count": assigned_issues,
|
||||
"pending_issues_count": pending_issues_count,
|
||||
"completed_issues_count": completed_issues_count,
|
||||
"created_issues_count": created_issues_count,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
|
||||
def dashboard_assigned_issues(self, request, slug):
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
issue_type = request.GET.get("issue_type", None)
|
||||
|
||||
# get all the assigned issues
|
||||
assigned_issues = (
|
||||
Issue.issue_objects.filter(
|
||||
(
|
||||
Q(assignees__in=[request.user])
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
workspace__slug=slug,
|
||||
project__project_projectmember__member=request.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
)
|
||||
.filter(**filters)
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_relation",
|
||||
queryset=IssueRelation.objects.select_related(
|
||||
"related_issue"
|
||||
).select_related("issue"),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=FileAsset.objects.filter(
|
||||
issue_id=OuterRef("id"),
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
label_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(labels__id__isnull=True)
|
||||
& Q(label_issue__deleted_at__isnull=True)
|
||||
),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
assignee_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True)
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
module_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(issue_module__module_id__isnull=True)
|
||||
& Q(issue_module__module__archived_at__isnull=True)
|
||||
& Q(issue_module__deleted_at__isnull=True)
|
||||
),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
if WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug, member=request.user, role=5, is_active=True
|
||||
).exists():
|
||||
assigned_issues = assigned_issues.filter(created_by=request.user)
|
||||
|
||||
# Priority Ordering
|
||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||
assigned_issues = assigned_issues.annotate(
|
||||
priority_order=Case(
|
||||
*[When(priority=p, then=Value(i)) for i, p in enumerate(priority_order)],
|
||||
output_field=CharField(),
|
||||
)
|
||||
).order_by("priority_order")
|
||||
|
||||
if issue_type == "pending":
|
||||
pending_issues_count = assigned_issues.filter(
|
||||
state__group__in=["backlog", "started", "unstarted"]
|
||||
).count()
|
||||
pending_issues = assigned_issues.filter(
|
||||
state__group__in=["backlog", "started", "unstarted"]
|
||||
)[:5]
|
||||
return Response(
|
||||
{
|
||||
"issues": IssueSerializer(
|
||||
pending_issues, many=True, expand=self.expand
|
||||
).data,
|
||||
"count": pending_issues_count,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
if issue_type == "completed":
|
||||
completed_issues_count = assigned_issues.filter(
|
||||
state__group__in=["completed"]
|
||||
).count()
|
||||
completed_issues = assigned_issues.filter(state__group__in=["completed"])[:5]
|
||||
return Response(
|
||||
{
|
||||
"issues": IssueSerializer(
|
||||
completed_issues, many=True, expand=self.expand
|
||||
).data,
|
||||
"count": completed_issues_count,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
if issue_type == "overdue":
|
||||
overdue_issues_count = assigned_issues.filter(
|
||||
state__group__in=["backlog", "unstarted", "started"],
|
||||
target_date__lt=timezone.now(),
|
||||
).count()
|
||||
overdue_issues = assigned_issues.filter(
|
||||
state__group__in=["backlog", "unstarted", "started"],
|
||||
target_date__lt=timezone.now(),
|
||||
)[:5]
|
||||
return Response(
|
||||
{
|
||||
"issues": IssueSerializer(
|
||||
overdue_issues, many=True, expand=self.expand
|
||||
).data,
|
||||
"count": overdue_issues_count,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
if issue_type == "upcoming":
|
||||
upcoming_issues_count = assigned_issues.filter(
|
||||
state__group__in=["backlog", "unstarted", "started"],
|
||||
target_date__gte=timezone.now(),
|
||||
).count()
|
||||
upcoming_issues = assigned_issues.filter(
|
||||
state__group__in=["backlog", "unstarted", "started"],
|
||||
target_date__gte=timezone.now(),
|
||||
)[:5]
|
||||
return Response(
|
||||
{
|
||||
"issues": IssueSerializer(
|
||||
upcoming_issues, many=True, expand=self.expand
|
||||
).data,
|
||||
"count": upcoming_issues_count,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
return Response(
|
||||
{"error": "Please specify a valid issue type"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
def dashboard_created_issues(self, request, slug):
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
issue_type = request.GET.get("issue_type", None)
|
||||
|
||||
# get all the assigned issues
|
||||
created_issues = (
|
||||
Issue.issue_objects.filter(
|
||||
workspace__slug=slug,
|
||||
project__project_projectmember__member=request.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
created_by=request.user,
|
||||
)
|
||||
.filter(**filters)
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=FileAsset.objects.filter(
|
||||
issue_id=OuterRef("id"),
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
label_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(labels__id__isnull=True)
|
||||
& Q(label_issue__deleted_at__isnull=True)
|
||||
),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
assignee_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True)
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
module_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(issue_module__module_id__isnull=True)
|
||||
& Q(issue_module__module__archived_at__isnull=True)
|
||||
& Q(issue_module__deleted_at__isnull=True)
|
||||
),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
)
|
||||
.order_by("created_at")
|
||||
)
|
||||
|
||||
# Priority Ordering
|
||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||
created_issues = created_issues.annotate(
|
||||
priority_order=Case(
|
||||
*[When(priority=p, then=Value(i)) for i, p in enumerate(priority_order)],
|
||||
output_field=CharField(),
|
||||
)
|
||||
).order_by("priority_order")
|
||||
|
||||
if issue_type == "pending":
|
||||
pending_issues_count = created_issues.filter(
|
||||
state__group__in=["backlog", "started", "unstarted"]
|
||||
).count()
|
||||
pending_issues = created_issues.filter(
|
||||
state__group__in=["backlog", "started", "unstarted"]
|
||||
)[:5]
|
||||
return Response(
|
||||
{
|
||||
"issues": IssueSerializer(
|
||||
pending_issues, many=True, expand=self.expand
|
||||
).data,
|
||||
"count": pending_issues_count,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
if issue_type == "completed":
|
||||
completed_issues_count = created_issues.filter(
|
||||
state__group__in=["completed"]
|
||||
).count()
|
||||
completed_issues = created_issues.filter(state__group__in=["completed"])[:5]
|
||||
return Response(
|
||||
{
|
||||
"issues": IssueSerializer(completed_issues, many=True).data,
|
||||
"count": completed_issues_count,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
if issue_type == "overdue":
|
||||
overdue_issues_count = created_issues.filter(
|
||||
state__group__in=["backlog", "unstarted", "started"],
|
||||
target_date__lt=timezone.now(),
|
||||
).count()
|
||||
overdue_issues = created_issues.filter(
|
||||
state__group__in=["backlog", "unstarted", "started"],
|
||||
target_date__lt=timezone.now(),
|
||||
)[:5]
|
||||
return Response(
|
||||
{
|
||||
"issues": IssueSerializer(overdue_issues, many=True).data,
|
||||
"count": overdue_issues_count,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
if issue_type == "upcoming":
|
||||
upcoming_issues_count = created_issues.filter(
|
||||
state__group__in=["backlog", "unstarted", "started"],
|
||||
target_date__gte=timezone.now(),
|
||||
).count()
|
||||
upcoming_issues = created_issues.filter(
|
||||
state__group__in=["backlog", "unstarted", "started"],
|
||||
target_date__gte=timezone.now(),
|
||||
)[:5]
|
||||
return Response(
|
||||
{
|
||||
"issues": IssueSerializer(upcoming_issues, many=True).data,
|
||||
"count": upcoming_issues_count,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
return Response(
|
||||
{"error": "Please specify a valid issue type"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
def dashboard_issues_by_state_groups(self, request, slug):
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
||||
extra_filters = {}
|
||||
|
||||
if WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug, member=request.user, role=5, is_active=True
|
||||
).exists():
|
||||
extra_filters = {"created_by": request.user}
|
||||
|
||||
issues_by_state_groups = (
|
||||
Issue.issue_objects.filter(
|
||||
workspace__slug=slug,
|
||||
project__project_projectmember__is_active=True,
|
||||
project__project_projectmember__member=request.user,
|
||||
assignees__in=[request.user],
|
||||
)
|
||||
.filter(**filters, **extra_filters)
|
||||
.values("state__group")
|
||||
.annotate(count=Count("id"))
|
||||
)
|
||||
|
||||
# default state
|
||||
all_groups = {state: 0 for state in state_order}
|
||||
|
||||
# Update counts for existing groups
|
||||
for entry in issues_by_state_groups:
|
||||
all_groups[entry["state__group"]] = entry["count"]
|
||||
|
||||
# Prepare output including all groups with their counts
|
||||
output_data = [
|
||||
{"state": group, "count": count} for group, count in all_groups.items()
|
||||
]
|
||||
|
||||
return Response(output_data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
def dashboard_issues_by_priority(self, request, slug):
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||
extra_filters = {}
|
||||
|
||||
if WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug, member=request.user, role=5, is_active=True
|
||||
).exists():
|
||||
extra_filters = {"created_by": request.user}
|
||||
|
||||
issues_by_priority = (
|
||||
Issue.issue_objects.filter(
|
||||
workspace__slug=slug,
|
||||
project__project_projectmember__is_active=True,
|
||||
project__project_projectmember__member=request.user,
|
||||
assignees__in=[request.user],
|
||||
)
|
||||
.filter(**filters, **extra_filters)
|
||||
.values("priority")
|
||||
.annotate(count=Count("id"))
|
||||
)
|
||||
|
||||
# default priority
|
||||
all_groups = {priority: 0 for priority in priority_order}
|
||||
|
||||
# Update counts for existing groups
|
||||
for entry in issues_by_priority:
|
||||
all_groups[entry["priority"]] = entry["count"]
|
||||
|
||||
# Prepare output including all groups with their counts
|
||||
output_data = [
|
||||
{"priority": group, "count": count} for group, count in all_groups.items()
|
||||
]
|
||||
|
||||
return Response(output_data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
def dashboard_recent_activity(self, request, slug):
|
||||
queryset = IssueActivity.objects.filter(
|
||||
~Q(field__in=["comment", "vote", "reaction", "draft"]),
|
||||
workspace__slug=slug,
|
||||
project__project_projectmember__member=request.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
project__archived_at__isnull=True,
|
||||
actor=request.user,
|
||||
).select_related("actor", "workspace", "issue", "project")[:8]
|
||||
|
||||
return Response(
|
||||
IssueActivitySerializer(queryset, many=True).data, status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
|
||||
def dashboard_recent_projects(self, request, slug):
|
||||
project_ids = (
|
||||
IssueActivity.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project__project_projectmember__member=request.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
project__archived_at__isnull=True,
|
||||
actor=request.user,
|
||||
)
|
||||
.values_list("project_id", flat=True)
|
||||
.distinct()
|
||||
)
|
||||
|
||||
# Extract project IDs from the recent projects
|
||||
unique_project_ids = set(project_id for project_id in project_ids)
|
||||
|
||||
# Fetch additional projects only if needed
|
||||
if len(unique_project_ids) < 4:
|
||||
additional_projects = Project.objects.filter(
|
||||
project_projectmember__member=request.user,
|
||||
project_projectmember__is_active=True,
|
||||
archived_at__isnull=True,
|
||||
workspace__slug=slug,
|
||||
).exclude(id__in=unique_project_ids)
|
||||
|
||||
# Append additional project IDs to the existing list
|
||||
unique_project_ids.update(additional_projects.values_list("id", flat=True))
|
||||
|
||||
return Response(list(unique_project_ids)[:4], status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
def dashboard_recent_collaborators(self, request, slug):
|
||||
project_members_with_activities = (
|
||||
WorkspaceMember.objects.filter(workspace__slug=slug, is_active=True)
|
||||
.annotate(
|
||||
active_issue_count=Count(
|
||||
Case(
|
||||
When(
|
||||
member__issue_assignee__issue__state__group__in=[
|
||||
"unstarted",
|
||||
"started",
|
||||
],
|
||||
member__issue_assignee__issue__workspace__slug=slug,
|
||||
member__issue_assignee__issue__project__project_projectmember__member=request.user,
|
||||
member__issue_assignee__issue__project__project_projectmember__is_active=True,
|
||||
then=F("member__issue_assignee__issue__id"),
|
||||
),
|
||||
distinct=True,
|
||||
output_field=IntegerField(),
|
||||
),
|
||||
distinct=True,
|
||||
),
|
||||
user_id=F("member_id"),
|
||||
)
|
||||
.values("user_id", "active_issue_count")
|
||||
.order_by("-active_issue_count")
|
||||
.distinct()
|
||||
)
|
||||
return Response((project_members_with_activities), status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class DashboardEndpoint(BaseAPIView):
|
||||
def create(self, request, slug):
|
||||
serializer = DashboardSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def patch(self, request, slug, pk):
|
||||
serializer = DashboardSerializer(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, pk):
|
||||
serializer = DashboardSerializer(data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_204_NO_CONTENT)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def get(self, request, slug, dashboard_id=None):
|
||||
if not dashboard_id:
|
||||
dashboard_type = request.GET.get("dashboard_type", None)
|
||||
if dashboard_type == "home":
|
||||
dashboard, created = DeprecatedDashboard.objects.get_or_create(
|
||||
type_identifier=dashboard_type,
|
||||
owned_by=request.user,
|
||||
is_default=True,
|
||||
)
|
||||
|
||||
if created:
|
||||
widgets_to_fetch = [
|
||||
"overview_stats",
|
||||
"assigned_issues",
|
||||
"created_issues",
|
||||
"issues_by_state_groups",
|
||||
"issues_by_priority",
|
||||
"recent_activity",
|
||||
"recent_projects",
|
||||
"recent_collaborators",
|
||||
]
|
||||
|
||||
updated_dashboard_widgets = []
|
||||
for widget_key in widgets_to_fetch:
|
||||
widget = DeprecatedWidget.objects.filter(
|
||||
key=widget_key
|
||||
).values_list("id", flat=True)
|
||||
if widget:
|
||||
updated_dashboard_widgets.append(
|
||||
DeprecatedDashboardWidget(
|
||||
widget_id=widget, dashboard_id=dashboard.id
|
||||
)
|
||||
)
|
||||
|
||||
DeprecatedDashboardWidget.objects.bulk_create(
|
||||
updated_dashboard_widgets, batch_size=100
|
||||
)
|
||||
|
||||
widgets = (
|
||||
DeprecatedWidget.objects.annotate(
|
||||
is_visible=Exists(
|
||||
DeprecatedDashboardWidget.objects.filter(
|
||||
widget_id=OuterRef("pk"),
|
||||
dashboard_id=dashboard.id,
|
||||
is_visible=True,
|
||||
)
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
dashboard_filters=Subquery(
|
||||
DeprecatedDashboardWidget.objects.filter(
|
||||
widget_id=OuterRef("pk"),
|
||||
dashboard_id=dashboard.id,
|
||||
filters__isnull=False,
|
||||
)
|
||||
.exclude(filters={})
|
||||
.values("filters")[:1]
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
widget_filters=Case(
|
||||
When(
|
||||
dashboard_filters__isnull=False,
|
||||
then=F("dashboard_filters"),
|
||||
),
|
||||
default=F("filters"),
|
||||
output_field=JSONField(),
|
||||
)
|
||||
)
|
||||
)
|
||||
return Response(
|
||||
{
|
||||
"dashboard": DashboardSerializer(dashboard).data,
|
||||
"widgets": WidgetSerializer(widgets, many=True).data,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
return Response(
|
||||
{"error": "Please specify a valid dashboard type"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
widget_key = request.GET.get("widget_key", "overview_stats")
|
||||
|
||||
WIDGETS_MAPPER = {
|
||||
"overview_stats": dashboard_overview_stats,
|
||||
"assigned_issues": dashboard_assigned_issues,
|
||||
"created_issues": dashboard_created_issues,
|
||||
"issues_by_state_groups": dashboard_issues_by_state_groups,
|
||||
"issues_by_priority": dashboard_issues_by_priority,
|
||||
"recent_activity": dashboard_recent_activity,
|
||||
"recent_projects": dashboard_recent_projects,
|
||||
"recent_collaborators": dashboard_recent_collaborators,
|
||||
}
|
||||
|
||||
func = WIDGETS_MAPPER.get(widget_key)
|
||||
if func is not None:
|
||||
response = func(self, request=request, slug=slug)
|
||||
if isinstance(response, Response):
|
||||
return response
|
||||
|
||||
return Response(
|
||||
{"error": "Please specify a valid widget key"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class WidgetsEndpoint(BaseAPIView):
|
||||
def patch(self, request, dashboard_id, widget_id):
|
||||
dashboard_widget = DeprecatedDashboardWidget.objects.filter(
|
||||
widget_id=widget_id, dashboard_id=dashboard_id
|
||||
).first()
|
||||
dashboard_widget.is_visible = request.data.get(
|
||||
"is_visible", dashboard_widget.is_visible
|
||||
)
|
||||
dashboard_widget.sort_order = request.data.get(
|
||||
"sort_order", dashboard_widget.sort_order
|
||||
)
|
||||
dashboard_widget.filters = request.data.get("filters", dashboard_widget.filters)
|
||||
dashboard_widget.save()
|
||||
return Response({"message": "successfully updated"}, status=status.HTTP_200_OK)
|
||||
@@ -853,7 +853,7 @@ def delete_cycle_issue_activity(
|
||||
issues = requested_data.get("issues")
|
||||
for issue in issues:
|
||||
current_issue = Issue.objects.filter(pk=issue).first()
|
||||
if current_issue:
|
||||
if issue:
|
||||
current_issue.updated_at = timezone.now()
|
||||
current_issue.save(update_fields=["updated_at"])
|
||||
issue_activities.append(
|
||||
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
# Generated by Django 4.2.18 on 2025-02-25 15:48
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("db", "0091_issuecomment_edited_at_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name="deprecateddashboardwidget",
|
||||
unique_together=None,
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="deprecateddashboardwidget",
|
||||
name="created_by",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="deprecateddashboardwidget",
|
||||
name="dashboard",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="deprecateddashboardwidget",
|
||||
name="updated_by",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="deprecateddashboardwidget",
|
||||
name="widget",
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name="DeprecatedDashboard",
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name="DeprecatedDashboardWidget",
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name="DeprecatedWidget",
|
||||
),
|
||||
]
|
||||
@@ -1,10 +1,13 @@
|
||||
# Django imports
|
||||
from django.db import models
|
||||
from django.db import models, transaction
|
||||
from django.utils import timezone
|
||||
|
||||
# Module imports
|
||||
from plane.bgtasks.deletion_task import soft_delete_related_objects
|
||||
|
||||
#Relative imports
|
||||
from .signals import post_bulk_create, post_bulk_update
|
||||
|
||||
|
||||
class TimeAuditModel(models.Model):
|
||||
"""To path when the record was created and last modified"""
|
||||
@@ -38,7 +41,39 @@ class UserAuditModel(models.Model):
|
||||
abstract = True
|
||||
|
||||
|
||||
class SoftDeletionQuerySet(models.QuerySet):
|
||||
class BulkOperationHooks:
|
||||
def update(self, **kwargs):
|
||||
"""
|
||||
Custom update method to trigger a signal with
|
||||
all the updated objs on bulk update.
|
||||
"""
|
||||
rows = super().update(**kwargs)
|
||||
if rows:
|
||||
post_bulk_update.send(sender=self.__class__, model=self.model, objs=self)
|
||||
return rows
|
||||
|
||||
@transaction.atomic
|
||||
def bulk_create(self, *args, **kwargs):
|
||||
"""
|
||||
Custom bulk_create method to handle any pre operations
|
||||
on the model instance and also trigger a signal with all the objs.
|
||||
"""
|
||||
if len(args):
|
||||
objs = args[0]
|
||||
else:
|
||||
objs = kwargs.get('objs')
|
||||
for obj in objs:
|
||||
if hasattr(obj, "pre_bulk_create"):
|
||||
obj.pre_bulk_create()
|
||||
|
||||
objs = super().bulk_create(*args, **kwargs)
|
||||
|
||||
post_bulk_create.send(sender=self.__class__, model=self.model, objs=objs)
|
||||
|
||||
return objs
|
||||
|
||||
|
||||
class SoftDeletionQuerySet(BulkOperationHooks, models.QuerySet):
|
||||
def delete(self, soft=True):
|
||||
if soft:
|
||||
return self.update(deleted_at=timezone.now())
|
||||
|
||||
@@ -3,6 +3,7 @@ from .api import APIActivityLog, APIToken
|
||||
from .asset import FileAsset
|
||||
from .base import BaseModel
|
||||
from .cycle import Cycle, CycleIssue, CycleUserProperties
|
||||
from .dashboard import DeprecatedDashboard, DeprecatedDashboardWidget, DeprecatedWidget
|
||||
from .deploy_board import DeployBoard
|
||||
from .draft import (
|
||||
DraftIssue,
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import uuid
|
||||
|
||||
# Django imports
|
||||
from django.db import models
|
||||
|
||||
# Module imports
|
||||
from ..mixins import TimeAuditModel
|
||||
from .base import BaseModel
|
||||
|
||||
|
||||
class DeprecatedDashboard(BaseModel):
|
||||
DASHBOARD_CHOICES = (
|
||||
("workspace", "Workspace"),
|
||||
("project", "Project"),
|
||||
("home", "Home"),
|
||||
("team", "Team"),
|
||||
("user", "User"),
|
||||
)
|
||||
name = models.CharField(max_length=255)
|
||||
description_html = models.TextField(blank=True, default="<p></p>")
|
||||
identifier = models.UUIDField(null=True)
|
||||
owned_by = models.ForeignKey(
|
||||
"db.User", on_delete=models.CASCADE, related_name="dashboards"
|
||||
)
|
||||
is_default = models.BooleanField(default=False)
|
||||
type_identifier = models.CharField(
|
||||
max_length=30,
|
||||
choices=DASHBOARD_CHOICES,
|
||||
verbose_name="Dashboard Type",
|
||||
default="home",
|
||||
)
|
||||
logo_props = models.JSONField(default=dict)
|
||||
|
||||
def __str__(self):
|
||||
"""Return name of the dashboard"""
|
||||
return f"{self.name}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "DeprecatedDashboard"
|
||||
verbose_name_plural = "DeprecatedDashboards"
|
||||
db_table = "deprecated_dashboards"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
|
||||
class DeprecatedWidget(TimeAuditModel):
|
||||
id = models.UUIDField(
|
||||
default=uuid.uuid4, unique=True, editable=False, db_index=True, primary_key=True
|
||||
)
|
||||
key = models.CharField(max_length=255)
|
||||
filters = models.JSONField(default=dict)
|
||||
logo_props = models.JSONField(default=dict)
|
||||
|
||||
def __str__(self):
|
||||
"""Return name of the widget"""
|
||||
return f"{self.key}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "DeprecatedWidget"
|
||||
verbose_name_plural = "DeprecatedWidgets"
|
||||
db_table = "deprecated_widgets"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
|
||||
class DeprecatedDashboardWidget(BaseModel):
|
||||
widget = models.ForeignKey(
|
||||
DeprecatedWidget, on_delete=models.CASCADE, related_name="dashboard_widgets"
|
||||
)
|
||||
dashboard = models.ForeignKey(
|
||||
DeprecatedDashboard, on_delete=models.CASCADE, related_name="dashboard_widgets"
|
||||
)
|
||||
is_visible = models.BooleanField(default=True)
|
||||
sort_order = models.FloatField(default=65535)
|
||||
filters = models.JSONField(default=dict)
|
||||
properties = models.JSONField(default=dict)
|
||||
|
||||
def __str__(self):
|
||||
"""Return name of the dashboard"""
|
||||
return f"{self.dashboard.name} {self.widget.key}"
|
||||
|
||||
class Meta:
|
||||
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 = "Deprecated Dashboard Widget"
|
||||
verbose_name_plural = "Deprecated Dashboard Widgets"
|
||||
db_table = "deprecated_dashboard_widgets"
|
||||
ordering = ("-created_at",)
|
||||
@@ -0,0 +1,5 @@
|
||||
from django.dispatch import Signal
|
||||
|
||||
|
||||
post_bulk_create = Signal()
|
||||
post_bulk_update = Signal()
|
||||
@@ -1,9 +1,9 @@
|
||||
FROM nginx:1.25.0-alpine
|
||||
|
||||
RUN rm /etc/nginx/conf.d/default.conf
|
||||
COPY nginx/nginx.conf.template /etc/nginx/nginx.conf.template
|
||||
COPY nginx.conf.template /etc/nginx/nginx.conf.template
|
||||
|
||||
COPY nginx/env.sh /docker-entrypoint.sh
|
||||
COPY ./env.sh /docker-entrypoint.sh
|
||||
|
||||
RUN chmod +x /docker-entrypoint.sh
|
||||
# Update all environment variables
|
||||
@@ -1,9 +1,9 @@
|
||||
FROM nginx:1.25.0-alpine
|
||||
|
||||
RUN rm /etc/nginx/conf.d/default.conf
|
||||
COPY nginx/nginx.conf.dev /etc/nginx/nginx.conf.template
|
||||
COPY nginx.conf.dev /etc/nginx/nginx.conf.template
|
||||
|
||||
COPY nginx/env.sh /docker-entrypoint.sh
|
||||
COPY ./env.sh /docker-entrypoint.sh
|
||||
|
||||
RUN chmod +x /docker-entrypoint.sh
|
||||
# Update all environment variables
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Editor } from "@tiptap/react";
|
||||
import { FC, ReactNode } from "react";
|
||||
import { Editor } from "@tiptap/react";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
// constants
|
||||
@@ -71,7 +71,7 @@ export const EditorContainer: FC<EditorContainerProps> = (props) => {
|
||||
onClick={handleContainerClick}
|
||||
onMouseLeave={handleContainerMouseLeave}
|
||||
className={cn(
|
||||
`editor-container cursor-text relative line-spacing-${displayConfig.lineSpacing ?? DEFAULT_DISPLAY_CONFIG.lineSpacing}`,
|
||||
"editor-container cursor-text relative",
|
||||
{
|
||||
"active-editor": editor?.isFocused && editor?.isEditable,
|
||||
},
|
||||
|
||||
@@ -4,7 +4,6 @@ import { TDisplayConfig } from "@/types";
|
||||
export const DEFAULT_DISPLAY_CONFIG: TDisplayConfig = {
|
||||
fontSize: "large-font",
|
||||
fontStyle: "sans-serif",
|
||||
lineSpacing: "regular",
|
||||
};
|
||||
|
||||
export const ACCEPTED_FILE_MIME_TYPES = ["image/jpeg", "image/jpg", "image/png", "image/webp", "image/gif"];
|
||||
|
||||
@@ -26,12 +26,12 @@ export const CoreEditorExtensionsWithoutProps = [
|
||||
StarterKit.configure({
|
||||
bulletList: {
|
||||
HTMLAttributes: {
|
||||
class: "list-disc pl-7 space-y-[--list-spacing-y]",
|
||||
class: "list-disc pl-7 space-y-2",
|
||||
},
|
||||
},
|
||||
orderedList: {
|
||||
HTMLAttributes: {
|
||||
class: "list-decimal pl-7 space-y-[--list-spacing-y]",
|
||||
class: "list-decimal pl-7 space-y-2",
|
||||
},
|
||||
},
|
||||
listItem: {
|
||||
|
||||
@@ -55,12 +55,12 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
|
||||
StarterKit.configure({
|
||||
bulletList: {
|
||||
HTMLAttributes: {
|
||||
class: "list-disc pl-7 space-y-[--list-spacing-y]",
|
||||
class: "list-disc pl-7 space-y-2",
|
||||
},
|
||||
},
|
||||
orderedList: {
|
||||
HTMLAttributes: {
|
||||
class: "list-decimal pl-7 space-y-[--list-spacing-y]",
|
||||
class: "list-decimal pl-7 space-y-2",
|
||||
},
|
||||
},
|
||||
listItem: {
|
||||
|
||||
@@ -46,12 +46,12 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => {
|
||||
StarterKit.configure({
|
||||
bulletList: {
|
||||
HTMLAttributes: {
|
||||
class: "list-disc pl-7 space-y-[--list-spacing-y]",
|
||||
class: "list-disc pl-7 space-y-2",
|
||||
},
|
||||
},
|
||||
orderedList: {
|
||||
HTMLAttributes: {
|
||||
class: "list-decimal pl-7 space-y-[--list-spacing-y]",
|
||||
class: "list-decimal pl-7 space-y-2",
|
||||
},
|
||||
},
|
||||
listItem: {
|
||||
|
||||
@@ -23,10 +23,7 @@ export type TEditorFontStyle = "sans-serif" | "serif" | "monospace";
|
||||
|
||||
export type TEditorFontSize = "small-font" | "large-font";
|
||||
|
||||
export type TEditorLineSpacing = "regular" | "small";
|
||||
|
||||
export type TDisplayConfig = {
|
||||
fontStyle?: TEditorFontStyle;
|
||||
fontSize?: TEditorFontSize;
|
||||
lineSpacing?: TEditorLineSpacing;
|
||||
};
|
||||
|
||||
@@ -252,7 +252,7 @@ ul[data-type="taskList"] li[data-checked="true"] {
|
||||
|
||||
div[data-type="horizontalRule"] {
|
||||
line-height: 0;
|
||||
padding: var(--divider-padding-top) 0 var(--divider-padding-bottom) 0;
|
||||
padding: 0.25rem 0;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
@@ -335,10 +335,10 @@ p.editor-paragraph-block {
|
||||
/* tailwind typography */
|
||||
.prose :where(h1.editor-heading-block):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||
&:not(:first-child) {
|
||||
padding-top: var(--heading-1-padding-top);
|
||||
padding-top: 28px;
|
||||
}
|
||||
|
||||
padding-bottom: var(--heading-1-padding-bottom);
|
||||
padding-bottom: 4px;
|
||||
font-size: var(--font-size-h1);
|
||||
line-height: var(--line-height-h1);
|
||||
font-weight: 600;
|
||||
@@ -346,10 +346,10 @@ p.editor-paragraph-block {
|
||||
|
||||
.prose :where(h2.editor-heading-block):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||
&:not(:first-child) {
|
||||
padding-top: var(--heading-2-padding-top);
|
||||
padding-top: 28px;
|
||||
}
|
||||
|
||||
padding-bottom: var(--heading-2-padding-bottom);
|
||||
padding-bottom: 4px;
|
||||
font-size: var(--font-size-h2);
|
||||
line-height: var(--line-height-h2);
|
||||
font-weight: 600;
|
||||
@@ -357,10 +357,10 @@ p.editor-paragraph-block {
|
||||
|
||||
.prose :where(h3.editor-heading-block):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||
&:not(:first-child) {
|
||||
padding-top: var(--heading-3-padding-top);
|
||||
padding-top: 28px;
|
||||
}
|
||||
|
||||
padding-bottom: var(--heading-3-padding-bottom);
|
||||
padding-bottom: 4px;
|
||||
font-size: var(--font-size-h3);
|
||||
line-height: var(--line-height-h3);
|
||||
font-weight: 600;
|
||||
@@ -368,10 +368,10 @@ p.editor-paragraph-block {
|
||||
|
||||
.prose :where(h4.editor-heading-block):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||
&:not(:first-child) {
|
||||
padding-top: var(--heading-4-padding-top);
|
||||
padding-top: 28px;
|
||||
}
|
||||
|
||||
padding-bottom: var(--heading-4-padding-bottom);
|
||||
padding-bottom: 4px;
|
||||
font-size: var(--font-size-h4);
|
||||
line-height: var(--line-height-h4);
|
||||
font-weight: 600;
|
||||
@@ -379,10 +379,10 @@ p.editor-paragraph-block {
|
||||
|
||||
.prose :where(h5.editor-heading-block):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||
&:not(:first-child) {
|
||||
padding-top: var(--heading-5-padding-top);
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
padding-bottom: var(--heading-5-padding-bottom);
|
||||
padding-bottom: 4px;
|
||||
font-size: var(--font-size-h5);
|
||||
line-height: var(--line-height-h5);
|
||||
font-weight: 600;
|
||||
@@ -390,10 +390,10 @@ p.editor-paragraph-block {
|
||||
|
||||
.prose :where(h6.editor-heading-block):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||
&:not(:first-child) {
|
||||
padding-top: var(--heading-6-padding-top);
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
padding-bottom: var(--heading-6-padding-bottom);
|
||||
padding-bottom: 4px;
|
||||
font-size: var(--font-size-h6);
|
||||
line-height: var(--line-height-h6);
|
||||
font-weight: 600;
|
||||
@@ -405,16 +405,16 @@ p.editor-paragraph-block {
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
padding-top: var(--paragraph-padding-top);
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
&:not(td p.editor-paragraph-block, th p.editor-paragraph-block) {
|
||||
&:last-child {
|
||||
padding-bottom: var(--paragraph-padding-bottom);
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
padding-bottom: var(--paragraph-padding-between);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,7 +423,7 @@ p.editor-paragraph-block {
|
||||
}
|
||||
|
||||
p.editor-paragraph-block + p.editor-paragraph-block {
|
||||
padding-top: var(--paragraph-padding-between) !important;
|
||||
padding-top: 8px !important;
|
||||
}
|
||||
|
||||
.prose :where(ol):not(:where([class~="not-prose"], [class~="not-prose"] *)) li p.editor-paragraph-block,
|
||||
|
||||
@@ -57,48 +57,4 @@
|
||||
--font-style: monospace;
|
||||
}
|
||||
/* end font styles */
|
||||
|
||||
/* spacing */
|
||||
&.line-spacing-regular {
|
||||
--heading-1-padding-top: 28px;
|
||||
--heading-1-padding-bottom: 4px;
|
||||
--heading-2-padding-top: 28px;
|
||||
--heading-2-padding-bottom: 4px;
|
||||
--heading-3-padding-top: 28px;
|
||||
--heading-3-padding-bottom: 4px;
|
||||
--heading-4-padding-top: 28px;
|
||||
--heading-4-padding-bottom: 4px;
|
||||
--heading-5-padding-top: 20px;
|
||||
--heading-5-padding-bottom: 4px;
|
||||
--heading-6-padding-top: 20px;
|
||||
--heading-6-padding-bottom: 4px;
|
||||
--paragraph-padding-top: 4px;
|
||||
--paragraph-padding-bottom: 4px;
|
||||
--paragraph-padding-between: 8px;
|
||||
--list-spacing-y: 8px;
|
||||
--divider-padding-top: 4px;
|
||||
--divider-padding-bottom: 4px;
|
||||
}
|
||||
|
||||
&.line-spacing-small {
|
||||
--heading-1-padding-top: 16px;
|
||||
--heading-1-padding-bottom: 4px;
|
||||
--heading-2-padding-top: 16px;
|
||||
--heading-2-padding-bottom: 4px;
|
||||
--heading-3-padding-top: 16px;
|
||||
--heading-3-padding-bottom: 4px;
|
||||
--heading-4-padding-top: 16px;
|
||||
--heading-4-padding-bottom: 4px;
|
||||
--heading-5-padding-top: 12px;
|
||||
--heading-5-padding-bottom: 4px;
|
||||
--heading-6-padding-top: 12px;
|
||||
--heading-6-padding-bottom: 4px;
|
||||
--paragraph-padding-top: 2px;
|
||||
--paragraph-padding-bottom: 2px;
|
||||
--paragraph-padding-between: 4px;
|
||||
--list-spacing-y: 0px;
|
||||
--divider-padding-top: 0px;
|
||||
--divider-padding-bottom: 4px;
|
||||
}
|
||||
/* end spacing */
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ export const SUPPORTED_LANGUAGES: ILanguageOption[] = [
|
||||
{ label: "日本語", value: "ja" },
|
||||
{ label: "中文", value: "zh-CN" },
|
||||
{ label: "Русский", value: "ru" },
|
||||
{ label: "Italian", value: "it" },
|
||||
];
|
||||
|
||||
export const STORAGE_KEY = "userLanguage";
|
||||
|
||||
@@ -461,8 +461,6 @@
|
||||
"states": "States",
|
||||
"state": "State",
|
||||
"state_groups": "State groups",
|
||||
"state_group": "State group",
|
||||
"priorities": "Priorities",
|
||||
"priority": "Priority",
|
||||
"team_project": "Team project",
|
||||
"project": "Project",
|
||||
@@ -471,16 +469,12 @@
|
||||
"module": "Module",
|
||||
"modules": "Modules",
|
||||
"labels": "Labels",
|
||||
"label": "Label",
|
||||
"assignees": "Assignees",
|
||||
"assignee": "Assignee",
|
||||
"created_by": "Created by",
|
||||
"none": "None",
|
||||
"link": "Link",
|
||||
"estimates": "Estimates",
|
||||
"estimate": "Estimate",
|
||||
"created_at": "Created at",
|
||||
"completed_at": "Completed at",
|
||||
"layout": "Layout",
|
||||
"filters": "Filters",
|
||||
"display": "Display",
|
||||
@@ -523,6 +517,7 @@
|
||||
"add_more": "Add more",
|
||||
"defaults": "Defaults",
|
||||
"add_label": "Add label",
|
||||
"estimates": "Estimates",
|
||||
"customize_time_range": "Customize time range",
|
||||
"loading": "Loading",
|
||||
"attachments": "Attachments",
|
||||
@@ -661,6 +656,8 @@
|
||||
"select": "Select",
|
||||
"upgrade": "Upgrade",
|
||||
"add_seats": "Add Seats",
|
||||
"label": "Label",
|
||||
"priorities": "Priorities",
|
||||
"projects": "Projects",
|
||||
"workspace": "Workspace",
|
||||
"workspaces": "Workspaces",
|
||||
|
||||
@@ -633,8 +633,6 @@
|
||||
"states": "Estados",
|
||||
"state": "Estado",
|
||||
"state_groups": "Grupos de estados",
|
||||
"state_group": "Grupos de estado",
|
||||
"priorities": "Prioridades",
|
||||
"priority": "Prioridad",
|
||||
"team_project": "Proyecto de equipo",
|
||||
"project": "Proyecto",
|
||||
@@ -643,16 +641,12 @@
|
||||
"module": "Módulo",
|
||||
"modules": "Módulos",
|
||||
"labels": "Etiquetas",
|
||||
"label": "Etiqueta",
|
||||
"assignees": "Asignados",
|
||||
"assignee": "Asignado",
|
||||
"created_by": "Creado por",
|
||||
"none": "Ninguno",
|
||||
"link": "Enlace",
|
||||
"estimates": "Estimaciones",
|
||||
"estimate": "Estimación",
|
||||
"created_at": "Creado en",
|
||||
"completed_at": "Completado en",
|
||||
"layout": "Diseño",
|
||||
"filters": "Filtros",
|
||||
"display": "Mostrar",
|
||||
@@ -695,6 +689,7 @@
|
||||
"add_more": "Agregar más",
|
||||
"defaults": "Valores predeterminados",
|
||||
"add_label": "Agregar etiqueta",
|
||||
"estimates": "Estimaciones",
|
||||
"customize_time_range": "Personalizar rango de tiempo",
|
||||
"loading": "Cargando",
|
||||
"attachments": "Archivos adjuntos",
|
||||
@@ -832,6 +827,8 @@
|
||||
"select": "Seleccionar",
|
||||
"upgrade": "Mejorar",
|
||||
"add_seats": "Agregar asientos",
|
||||
"label": "Etiqueta",
|
||||
"priorities": "Prioridades",
|
||||
"projects": "Proyectos",
|
||||
"workspace": "Espacio de trabajo",
|
||||
"workspaces": "Espacios de trabajo",
|
||||
|
||||
@@ -631,8 +631,6 @@
|
||||
"states": "États",
|
||||
"state": "État",
|
||||
"state_groups": "Groupes d'états",
|
||||
"state_group": "Groupe d'état",
|
||||
"priorities": "Priorités",
|
||||
"priority": "Priorité",
|
||||
"team_project": "Projet d'équipe",
|
||||
"project": "Projet",
|
||||
@@ -641,16 +639,12 @@
|
||||
"module": "Module",
|
||||
"modules": "Modules",
|
||||
"labels": "Étiquettes",
|
||||
"label": "Étiquette",
|
||||
"assignees": "Assignés",
|
||||
"assignee": "Assigné",
|
||||
"created_by": "Créé par",
|
||||
"none": "Aucun",
|
||||
"link": "Lien",
|
||||
"estimates": "Estimations",
|
||||
"estimate": "Estimation",
|
||||
"created_at": "Créé le",
|
||||
"completed_at": "Terminé le",
|
||||
"layout": "Disposition",
|
||||
"filters": "Filtres",
|
||||
"display": "Affichage",
|
||||
@@ -693,6 +687,7 @@
|
||||
"add_more": "Ajouter plus",
|
||||
"defaults": "Par défaut",
|
||||
"add_label": "Ajouter une étiquette",
|
||||
"estimates": "Estimations",
|
||||
"customize_time_range": "Personnaliser la plage de temps",
|
||||
"loading": "Chargement",
|
||||
"attachments": "Pièces jointes",
|
||||
@@ -830,6 +825,8 @@
|
||||
"select": "Sélectionner",
|
||||
"upgrade": "Mettre à niveau",
|
||||
"add_seats": "Ajouter des sièges",
|
||||
"label": "Étiquette",
|
||||
"priorities": "Priorités",
|
||||
"projects": "Projets",
|
||||
"workspace": "Espace de travail",
|
||||
"workspaces": "Espaces de travail",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -631,8 +631,6 @@
|
||||
"states": "ステータス",
|
||||
"state": "ステータス",
|
||||
"state_groups": "ステータスグループ",
|
||||
"state_group": "ステート グループ",
|
||||
"priorities": "優先度",
|
||||
"priority": "優先度",
|
||||
"team_project": "チームプロジェクト",
|
||||
"project": "プロジェクト",
|
||||
@@ -641,16 +639,12 @@
|
||||
"module": "モジュール",
|
||||
"modules": "モジュール",
|
||||
"labels": "ラベル",
|
||||
"label": "ラベル",
|
||||
"assignees": "担当者",
|
||||
"assignee": "担当者",
|
||||
"created_by": "作成者",
|
||||
"none": "なし",
|
||||
"link": "リンク",
|
||||
"estimates": "見積もり",
|
||||
"estimate": "見積もり",
|
||||
"created_at": "クリエイテッド アット",
|
||||
"completed_at": "コンプリーテッド アット",
|
||||
"layout": "レイアウト",
|
||||
"filters": "フィルター",
|
||||
"display": "表示",
|
||||
@@ -693,6 +687,7 @@
|
||||
"add_more": "さらに追加",
|
||||
"defaults": "デフォルト",
|
||||
"add_label": "ラベルを追加",
|
||||
"estimates": "見積もり",
|
||||
"customize_time_range": "期間をカスタマイズ",
|
||||
"loading": "読み込み中",
|
||||
"attachments": "添付ファイル",
|
||||
@@ -830,6 +825,8 @@
|
||||
"select": "選択",
|
||||
"upgrade": "アップグレード",
|
||||
"add_seats": "シートを追加",
|
||||
"label": "ラベル",
|
||||
"priorities": "優先度",
|
||||
"projects": "プロジェクト",
|
||||
"workspace": "ワークスペース",
|
||||
"workspaces": "ワークスペース",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -631,8 +631,6 @@
|
||||
"states": "状态",
|
||||
"state": "状态",
|
||||
"state_groups": "状态组",
|
||||
"state_group": "状态组",
|
||||
"priorities": "优先级",
|
||||
"priority": "优先级",
|
||||
"team_project": "团队项目",
|
||||
"project": "项目",
|
||||
@@ -641,16 +639,12 @@
|
||||
"module": "模块",
|
||||
"modules": "模块",
|
||||
"labels": "标签",
|
||||
"label": "标签",
|
||||
"assignees": "负责人",
|
||||
"assignee": "负责人",
|
||||
"created_by": "创建者",
|
||||
"none": "无",
|
||||
"link": "链接",
|
||||
"estimates": "估算",
|
||||
"estimate": "估算",
|
||||
"created_at": "创建于",
|
||||
"completed_at": "完成于",
|
||||
"layout": "布局",
|
||||
"filters": "筛选",
|
||||
"display": "显示",
|
||||
@@ -693,6 +687,7 @@
|
||||
"add_more": "添加更多",
|
||||
"defaults": "默认值",
|
||||
"add_label": "添加标签",
|
||||
"estimates": "估算",
|
||||
"customize_time_range": "自定义时间范围",
|
||||
"loading": "加载中",
|
||||
"attachments": "附件",
|
||||
@@ -830,6 +825,8 @@
|
||||
"select": "选择",
|
||||
"upgrade": "升级",
|
||||
"add_seats": "添加席位",
|
||||
"label": "标签",
|
||||
"priorities": "优先级",
|
||||
"projects": "项目",
|
||||
"workspace": "工作区",
|
||||
"workspaces": "工作区",
|
||||
|
||||
@@ -149,8 +149,6 @@ export class TranslationStore {
|
||||
return import("../locales/zh-CN/translations.json");
|
||||
case "ru":
|
||||
return import("../locales/ru/translations.json");
|
||||
case "it":
|
||||
return import("../locales/it/translations.json");
|
||||
default:
|
||||
throw new Error(`Unsupported language: ${language}`);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type TLanguage = "en" | "fr" | "es" | "ja" | "zh-CN" | "ru" | "it";
|
||||
export type TLanguage = "en" | "fr" | "es" | "ja" | "zh-CN" | "ru";
|
||||
|
||||
export interface ILanguageOption {
|
||||
label: string;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
nginx.conf.template
|
||||
@@ -1,39 +0,0 @@
|
||||
(plane_proxy) {
|
||||
request_body {
|
||||
max_size {$FILE_SIZE_LIMIT}
|
||||
}
|
||||
|
||||
reverse_proxy /spaces/* space:3000
|
||||
reverse_proxy /god-mode/* admin:3000
|
||||
reverse_proxy /api/* api:8000
|
||||
reverse_proxy /auth/* api:8000
|
||||
reverse_proxy /live/* live:3000
|
||||
reverse_proxy /{$BUCKET_NAME} plane-minio:9000
|
||||
reverse_proxy /{$BUCKET_NAME}/* plane-minio:9000
|
||||
reverse_proxy /* web:3000
|
||||
}
|
||||
|
||||
{
|
||||
email {$CERT_EMAIL:admin@example.com}
|
||||
acme_ca {$CERT_ACME_CA}
|
||||
{$CERT_ACME_DNS}
|
||||
servers {
|
||||
timeouts {
|
||||
read_body 600s
|
||||
read_header 30s
|
||||
write 600s
|
||||
idle 600s
|
||||
}
|
||||
max_header_size 25MB
|
||||
client_ip_headers X-Forwarded-For X-Real-IP
|
||||
trusted_proxies static {$TRUSTED_PROXIES:0.0.0.0/0}
|
||||
}
|
||||
log {
|
||||
output {$LOG_OUTPUT:stdout}
|
||||
level {$LOG_LEVEL:INFO}
|
||||
}
|
||||
}
|
||||
|
||||
{$SITE_ADDRESS} {
|
||||
import plane_proxy
|
||||
}
|
||||
@@ -108,7 +108,7 @@ export const ExtendedProjectSidebar = observer(() => {
|
||||
<div
|
||||
ref={extendedProjectSidebarRef}
|
||||
className={cn(
|
||||
"absolute top-0 h-full z-[19] flex flex-col gap-2 w-[300px] transform transition-all duration-300 ease-in-out bg-custom-sidebar-background-100 border-r border-custom-sidebar-border-200 shadow-md",
|
||||
"fixed top-0 h-full z-[19] flex flex-col gap-2 w-[300px] transform transition-all duration-300 ease-in-out bg-custom-sidebar-background-100 border-r border-custom-sidebar-border-200 shadow-md",
|
||||
{
|
||||
"translate-x-0 opacity-100 pointer-events-auto": extendedProjectSidebarCollapsed,
|
||||
"-translate-x-full opacity-0 pointer-events-none": !extendedProjectSidebarCollapsed,
|
||||
|
||||
@@ -104,7 +104,7 @@ export const ExtendedAppSidebar = observer(() => {
|
||||
<div
|
||||
ref={extendedSidebarRef}
|
||||
className={cn(
|
||||
"absolute top-0 h-full z-[19] flex flex-col gap-0.5 w-[300px] transform transition-all duration-300 ease-in-out bg-custom-sidebar-background-100 border-r border-custom-sidebar-border-200 p-4 shadow-md pb-6",
|
||||
"fixed top-0 h-full z-[19] flex flex-col gap-0.5 w-[300px] transform transition-all duration-300 ease-in-out bg-custom-sidebar-background-100 border-r border-custom-sidebar-border-200 p-4 shadow-md pb-6",
|
||||
{
|
||||
"translate-x-0 opacity-100 pointer-events-auto": extendedSidebarCollapsed,
|
||||
"-translate-x-full opacity-0 pointer-events-none": !extendedSidebarCollapsed,
|
||||
|
||||
@@ -101,10 +101,10 @@ export const CycleSidebarHeader: FC<Props> = observer((props) => {
|
||||
});
|
||||
};
|
||||
|
||||
const submitChanges = async (data: Partial<ICycle>, changedProperty: string) => {
|
||||
const submitChanges = (data: Partial<ICycle>, changedProperty: string) => {
|
||||
if (!workspaceSlug || !projectId || !cycleDetails.id) return;
|
||||
|
||||
await updateCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleDetails.id.toString(), data)
|
||||
updateCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleDetails.id.toString(), data)
|
||||
.then((res) => {
|
||||
captureCycleEvent({
|
||||
eventName: CYCLE_UPDATED,
|
||||
@@ -146,21 +146,22 @@ export const CycleSidebarHeader: FC<Props> = observer((props) => {
|
||||
};
|
||||
|
||||
const handleDateChange = async (startDate: Date | undefined, endDate: Date | undefined) => {
|
||||
if (!startDate || !endDate) return;
|
||||
|
||||
let isDateValid = false;
|
||||
|
||||
const payload = {
|
||||
start_date: renderFormattedPayloadDate(startDate) || null,
|
||||
end_date: renderFormattedPayloadDate(endDate) || null,
|
||||
start_date: renderFormattedPayloadDate(startDate),
|
||||
end_date: renderFormattedPayloadDate(endDate),
|
||||
};
|
||||
|
||||
if (payload?.start_date && payload.end_date) {
|
||||
if (cycleDetails?.start_date && cycleDetails.end_date)
|
||||
isDateValid = await dateChecker({
|
||||
...payload,
|
||||
cycle_id: cycleDetails.id,
|
||||
});
|
||||
} else {
|
||||
isDateValid = true;
|
||||
}
|
||||
else isDateValid = await dateChecker(payload);
|
||||
|
||||
if (isDateValid) {
|
||||
submitChanges(payload, "date_range");
|
||||
setToast({
|
||||
@@ -174,8 +175,8 @@ export const CycleSidebarHeader: FC<Props> = observer((props) => {
|
||||
title: t("project_cycles.action.update.failed.title"),
|
||||
message: t("project_cycles.action.update.error.already_exists"),
|
||||
});
|
||||
reset({ ...cycleDetails });
|
||||
}
|
||||
return isDateValid;
|
||||
};
|
||||
|
||||
const isEditingAllowed = allowPermissions(
|
||||
@@ -295,18 +296,16 @@ export const CycleSidebarHeader: FC<Props> = observer((props) => {
|
||||
render={({ field: { value: endDateValue, onChange: onChangeEndDate } }) => (
|
||||
<DateRangeDropdown
|
||||
className="h-7"
|
||||
buttonVariant="border-with-text"
|
||||
buttonVariant="transparent-with-text"
|
||||
minDate={new Date()}
|
||||
value={{
|
||||
from: getDate(startDateValue),
|
||||
to: getDate(endDateValue),
|
||||
}}
|
||||
onSelect={async (val) => {
|
||||
const isDateValid = await handleDateChange(val?.from, val?.to);
|
||||
if (isDateValid) {
|
||||
onChangeStartDate(val?.from ? renderFormattedPayloadDate(val.from) : null);
|
||||
onChangeEndDate(val?.to ? renderFormattedPayloadDate(val.to) : null);
|
||||
}
|
||||
onSelect={(val) => {
|
||||
onChangeStartDate(val?.from ? renderFormattedPayloadDate(val.from) : null);
|
||||
onChangeEndDate(val?.to ? renderFormattedPayloadDate(val.to) : null);
|
||||
handleDateChange(val?.from, val?.to);
|
||||
}}
|
||||
placeholder={{
|
||||
from: "Start date",
|
||||
|
||||
@@ -3,21 +3,31 @@
|
||||
import React, { FC, MouseEvent, useEffect, useMemo, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams, usePathname, useSearchParams } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { Eye, Users } from "lucide-react";
|
||||
// types
|
||||
import { CYCLE_FAVORITED, CYCLE_UNFAVORITED, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { ICycle, TCycleGroups } from "@plane/types";
|
||||
// ui
|
||||
import { Avatar, AvatarGroup, FavoriteStar, LayersIcon, Tooltip, TransferIcon, setPromiseToast } from "@plane/ui";
|
||||
import {
|
||||
Avatar,
|
||||
AvatarGroup,
|
||||
FavoriteStar,
|
||||
LayersIcon,
|
||||
TOAST_TYPE,
|
||||
Tooltip,
|
||||
TransferIcon,
|
||||
setPromiseToast,
|
||||
setToast,
|
||||
} from "@plane/ui";
|
||||
// components
|
||||
import { CycleQuickActions, TransferIssuesModal } from "@/components/cycles";
|
||||
import { DateRangeDropdown } from "@/components/dropdowns";
|
||||
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
|
||||
// constants
|
||||
// helpers
|
||||
import { getDate } from "@/helpers/date-time.helper";
|
||||
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
// hooks
|
||||
import { generateQueryParams } from "@/helpers/router.helper";
|
||||
@@ -26,6 +36,11 @@ import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web components
|
||||
import { CycleAdditionalActions } from "@/plane-web/components/cycles";
|
||||
// plane web constants
|
||||
// services
|
||||
import { CycleService } from "@/services/cycle.service";
|
||||
|
||||
const cycleService = new CycleService();
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
@@ -62,7 +77,7 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
|
||||
const { getUserDetails } = useMember();
|
||||
|
||||
// form
|
||||
const { control, reset, getValues } = useForm({
|
||||
const { control, reset } = useForm({
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
@@ -83,6 +98,7 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
|
||||
workspaceSlug,
|
||||
projectId
|
||||
);
|
||||
const renderIcon = Boolean(cycleDetails.start_date) || Boolean(cycleDetails.end_date);
|
||||
|
||||
// handlers
|
||||
const handleAddToFavorites = (e: MouseEvent<HTMLButtonElement>) => {
|
||||
@@ -141,6 +157,54 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
|
||||
});
|
||||
};
|
||||
|
||||
const submitChanges = (data: Partial<ICycle>) => {
|
||||
if (!workspaceSlug || !projectId || !cycleId) return;
|
||||
updateCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), data);
|
||||
};
|
||||
|
||||
const dateChecker = async (payload: any) => {
|
||||
try {
|
||||
const res = await cycleService.cycleDateCheck(workspaceSlug as string, projectId as string, payload);
|
||||
return res.status;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDateChange = async (startDate: Date | undefined, endDate: Date | undefined) => {
|
||||
if (!startDate || !endDate) return;
|
||||
|
||||
let isDateValid = false;
|
||||
|
||||
const payload = {
|
||||
start_date: renderFormattedPayloadDate(startDate),
|
||||
end_date: renderFormattedPayloadDate(endDate),
|
||||
};
|
||||
|
||||
if (cycleDetails && cycleDetails.start_date && cycleDetails.end_date)
|
||||
isDateValid = await dateChecker({
|
||||
...payload,
|
||||
cycle_id: cycleDetails.id,
|
||||
});
|
||||
else isDateValid = await dateChecker(payload);
|
||||
|
||||
if (isDateValid) {
|
||||
submitChanges(payload);
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: t("project_cycles.action.update.success.title"),
|
||||
message: t("project_cycles.action.update.success.description"),
|
||||
});
|
||||
} else {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("project_cycles.action.update.failed.title"),
|
||||
message: t("project_cycles.action.update.error.already_exists"),
|
||||
});
|
||||
reset({ ...cycleDetails });
|
||||
}
|
||||
};
|
||||
|
||||
const createdByDetails = cycleDetails.created_by ? getUserDetails(cycleDetails.created_by) : undefined;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -150,6 +214,10 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
|
||||
});
|
||||
}, [cycleDetails, reset]);
|
||||
|
||||
const isArchived = Boolean(cycleDetails.archived_at);
|
||||
const isCompleted = cycleStatus === "completed";
|
||||
|
||||
const isDisabled = !isEditingAllowed || isArchived || isCompleted;
|
||||
// handlers
|
||||
const openCycleOverview = (e: MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => {
|
||||
e.preventDefault();
|
||||
@@ -198,27 +266,39 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isActive && cycleDetails.start_date && (
|
||||
<DateRangeDropdown
|
||||
buttonVariant={"transparent-with-text"}
|
||||
buttonContainerClassName={`h-6 w-full cursor-auto flex items-center gap-1.5 text-custom-text-300 rounded text-xs [&>div]:hover:bg-transparent`}
|
||||
buttonClassName="p-0"
|
||||
minDate={new Date()}
|
||||
value={{
|
||||
from: getDate(cycleDetails.start_date),
|
||||
to: getDate(cycleDetails.end_date),
|
||||
}}
|
||||
placeholder={{
|
||||
from: "Start date",
|
||||
to: "End date",
|
||||
}}
|
||||
showTooltip
|
||||
required={cycleDetails.status !== "draft"}
|
||||
disabled
|
||||
hideIcon={{
|
||||
from: false,
|
||||
to: false,
|
||||
}}
|
||||
{!isActive && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="start_date"
|
||||
render={({ field: { value: startDateValue, onChange: onChangeStartDate } }) => (
|
||||
<Controller
|
||||
control={control}
|
||||
name="end_date"
|
||||
render={({ field: { value: endDateValue, onChange: onChangeEndDate } }) => (
|
||||
<DateRangeDropdown
|
||||
buttonContainerClassName={`h-6 w-full flex ${isDisabled ? "cursor-not-allowed" : "cursor-pointer"} items-center gap-1.5 text-custom-text-300 border-[0.5px] border-custom-border-300 rounded text-xs`}
|
||||
buttonVariant="transparent-with-text"
|
||||
minDate={new Date()}
|
||||
value={{
|
||||
from: getDate(startDateValue),
|
||||
to: getDate(endDateValue),
|
||||
}}
|
||||
onSelect={(val) => {
|
||||
onChangeStartDate(val?.from ? renderFormattedPayloadDate(val.from) : null);
|
||||
onChangeEndDate(val?.to ? renderFormattedPayloadDate(val.to) : null);
|
||||
handleDateChange(val?.from, val?.to);
|
||||
}}
|
||||
placeholder={{
|
||||
from: "Start date",
|
||||
to: "End date",
|
||||
}}
|
||||
required={cycleDetails.status !== "draft"}
|
||||
disabled={isDisabled}
|
||||
hideIcon={{ from: renderIcon ?? true, to: renderIcon }}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ type Props = {
|
||||
};
|
||||
minDate?: Date;
|
||||
maxDate?: Date;
|
||||
onSelect?: (range: DateRange | undefined) => void;
|
||||
onSelect: (range: DateRange | undefined) => void;
|
||||
placeholder?: {
|
||||
from?: string;
|
||||
to?: string;
|
||||
@@ -204,7 +204,11 @@ export const DateRangeDropdown: React.FC<Props> = (props) => {
|
||||
classNames={{ root: `p-3 rounded-md` }}
|
||||
selected={dateRange}
|
||||
onSelect={(val) => {
|
||||
onSelect?.(val);
|
||||
onSelect(val);
|
||||
setDateRange({
|
||||
from: val?.from ?? undefined,
|
||||
to: val?.to ?? undefined,
|
||||
});
|
||||
}}
|
||||
mode="range"
|
||||
disabled={disabledDays}
|
||||
|
||||
@@ -234,11 +234,11 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
||||
filteredOptions.length > 0 ? (
|
||||
filteredOptions.map((option) => (
|
||||
<StateOption
|
||||
{...props}
|
||||
key={option.value}
|
||||
option={option}
|
||||
selectedValue={value}
|
||||
className="flex w-full cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5"
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
|
||||
@@ -61,7 +61,6 @@ export const InboxIssueProperties: FC<TInboxIssueProperties> = observer((props)
|
||||
projectId={projectId}
|
||||
buttonVariant="border-with-text"
|
||||
tabIndex={getIndex("state_id")}
|
||||
isForWorkItemCreation={!data?.id}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -411,49 +411,50 @@ export const ModuleAnalyticsSidebar: React.FC<Props> = observer((props) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-5 pb-6 pt-2.5">
|
||||
<div className="flex items-center justify-start gap-1">
|
||||
<div className="flex w-2/5 items-center justify-start gap-2 text-custom-text-300">
|
||||
<CalendarClock className="h-4 w-4" />
|
||||
<span className="text-base">{t("date_range")}</span>
|
||||
</div>
|
||||
<div className="h-7">
|
||||
<Controller
|
||||
control={control}
|
||||
name="start_date"
|
||||
render={({ field: { value: startDateValue, onChange: onChangeStartDate } }) => (
|
||||
<Controller
|
||||
control={control}
|
||||
name="target_date"
|
||||
render={({ field: { value: endDateValue, onChange: onChangeEndDate } }) => {
|
||||
const startDate = getDate(startDateValue);
|
||||
const endDate = getDate(endDateValue);
|
||||
return (
|
||||
<DateRangeDropdown
|
||||
buttonContainerClassName="w-full"
|
||||
buttonVariant="background-with-text"
|
||||
value={{
|
||||
from: startDate,
|
||||
to: endDate,
|
||||
}}
|
||||
onSelect={(val) => {
|
||||
onChangeStartDate(val?.from ? renderFormattedPayloadDate(val.from) : null);
|
||||
onChangeEndDate(val?.to ? renderFormattedPayloadDate(val.to) : null);
|
||||
handleDateChange(val?.from, val?.to);
|
||||
}}
|
||||
placeholder={{
|
||||
from: t("start_date"),
|
||||
to: t("end_date"),
|
||||
}}
|
||||
disabled={!isEditingAllowed || isArchived}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-start gap-1">
|
||||
<div className="flex w-2/5 items-center justify-start gap-2 text-custom-text-300">
|
||||
<CalendarClock className="h-4 w-4" />
|
||||
<span className="text-base">{t("date_range")}</span>
|
||||
</div>
|
||||
<div className="h-7 w-3/5">
|
||||
<Controller
|
||||
control={control}
|
||||
name="start_date"
|
||||
render={({ field: { value: startDateValue, onChange: onChangeStartDate } }) => (
|
||||
<Controller
|
||||
control={control}
|
||||
name="target_date"
|
||||
render={({ field: { value: endDateValue, onChange: onChangeEndDate } }) => {
|
||||
const startDate = getDate(startDateValue);
|
||||
const endDate = getDate(endDateValue);
|
||||
return (
|
||||
<DateRangeDropdown
|
||||
buttonContainerClassName="w-full"
|
||||
buttonVariant="background-with-text"
|
||||
value={{
|
||||
from: startDate,
|
||||
to: endDate,
|
||||
}}
|
||||
onSelect={(val) => {
|
||||
onChangeStartDate(val?.from ? renderFormattedPayloadDate(val.from) : null);
|
||||
onChangeEndDate(val?.to ? renderFormattedPayloadDate(val.to) : null);
|
||||
handleDateChange(val?.from, val?.to);
|
||||
}}
|
||||
placeholder={{
|
||||
from: t("start_date"),
|
||||
to: t("target_date"),
|
||||
}}
|
||||
disabled={!isEditingAllowed || isArchived}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-5 pb-6 pt-2.5">
|
||||
<div className="flex items-center justify-start gap-1">
|
||||
<div className="flex w-2/5 items-center justify-start gap-2 text-custom-text-300">
|
||||
<SquareUser className="h-4 w-4" />
|
||||
|
||||
@@ -6,13 +6,7 @@ import { useParams } from "next/navigation";
|
||||
// icons
|
||||
import { SquareUser } from "lucide-react";
|
||||
// types
|
||||
import {
|
||||
MODULE_STATUS,
|
||||
MODULE_FAVORITED,
|
||||
MODULE_UNFAVORITED,
|
||||
EUserPermissions,
|
||||
EUserPermissionsLevel,
|
||||
} from "@plane/constants";
|
||||
import { MODULE_STATUS, MODULE_FAVORITED, MODULE_UNFAVORITED , EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IModule } from "@plane/types";
|
||||
// ui
|
||||
|
||||
Reference in New Issue
Block a user