diff --git a/apiserver/plane/app/serializers/module.py b/apiserver/plane/app/serializers/module.py index dfdd265cd..687747242 100644 --- a/apiserver/plane/app/serializers/module.py +++ b/apiserver/plane/app/serializers/module.py @@ -210,6 +210,7 @@ class ModuleSerializer(DynamicBaseSerializer): "backlog_issues", "created_at", "updated_at", + "archived_at", ] read_only_fields = fields diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index 2cdab312c..3d7603e24 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -38,7 +38,7 @@ from .workspace.base import ( WorkSpaceAvailabilityCheckEndpoint, UserWorkspaceDashboardEndpoint, WorkspaceThemeViewSet, - ExportWorkspaceUserActivityEndpoint + ExportWorkspaceUserActivityEndpoint, ) from .workspace.member import ( @@ -91,12 +91,14 @@ from .cycle.base import ( CycleDateCheckEndpoint, CycleFavoriteViewSet, TransferCycleIssueEndpoint, - CycleArchiveUnarchiveEndpoint, CycleUserPropertiesEndpoint, ) from .cycle.issue import ( CycleIssueViewSet, ) +from .cycle.archive import ( + CycleArchiveUnarchiveEndpoint, +) from .asset.base import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet from .issue.base import ( @@ -170,7 +172,6 @@ from .module.base import ( ModuleViewSet, ModuleLinkViewSet, ModuleFavoriteViewSet, - ModuleArchiveUnarchiveEndpoint, ModuleUserPropertiesEndpoint, ) @@ -178,6 +179,10 @@ from .module.issue import ( ModuleIssueViewSet, ) +from .module.archive import ( + ModuleArchiveUnarchiveEndpoint, +) + from .api import ApiTokenEndpoint diff --git a/apiserver/plane/app/views/cycle/archive.py b/apiserver/plane/app/views/cycle/archive.py new file mode 100644 index 000000000..e6d82795a --- /dev/null +++ b/apiserver/plane/app/views/cycle/archive.py @@ -0,0 +1,409 @@ +# 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, + OuterRef, + Prefetch, + Q, + UUIDField, + Value, + When, +) +from django.db.models.functions import Coalesce +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, + CycleFavorite, + Issue, + Label, + User, +) +from plane.utils.analytics_plot import burndown_plot + +# Module imports +from .. import BaseAPIView + + +class CycleArchiveUnarchiveEndpoint(BaseAPIView): + + permission_classes = [ + ProjectEntityPermission, + ] + + def get_queryset(self): + favorite_subquery = CycleFavorite.objects.filter( + user=self.request.user, + cycle_id=OuterRef("pk"), + project_id=self.kwargs.get("project_id"), + workspace__slug=self.kwargs.get("slug"), + ) + return ( + Cycle.objects.filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .filter(archived_at__isnull=False) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) + .filter(project__archived_at__isnull=True) + .select_related("project", "workspace", "owned_by") + .prefetch_related( + Prefetch( + "issue_cycle__issue__assignees", + queryset=User.objects.only( + "avatar", "first_name", "id" + ).distinct(), + ) + ) + .prefetch_related( + Prefetch( + "issue_cycle__issue__labels", + queryset=Label.objects.only( + "name", "color", "id" + ).distinct(), + ) + ) + .annotate(is_favorite=Exists(favorite_subquery)) + .annotate( + total_issues=Count( + "issue_cycle__issue__id", + distinct=True, + filter=Q( + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + completed_issues=Count( + "issue_cycle__issue__id", + distinct=True, + filter=Q( + issue_cycle__issue__state__group="completed", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + cancelled_issues=Count( + "issue_cycle__issue__id", + distinct=True, + filter=Q( + issue_cycle__issue__state__group="cancelled", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + started_issues=Count( + "issue_cycle__issue__id", + distinct=True, + filter=Q( + issue_cycle__issue__state__group="started", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + unstarted_issues=Count( + "issue_cycle__issue__id", + distinct=True, + filter=Q( + issue_cycle__issue__state__group="unstarted", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + backlog_issues=Count( + "issue_cycle__issue__id", + distinct=True, + filter=Q( + issue_cycle__issue__state__group="backlog", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + status=Case( + When( + Q(start_date__lte=timezone.now()) + & Q(end_date__gte=timezone.now()), + then=Value("CURRENT"), + ), + When( + start_date__gt=timezone.now(), then=Value("UPCOMING") + ), + When(end_date__lt=timezone.now(), then=Value("COMPLETED")), + When( + Q(start_date__isnull=True) & Q(end_date__isnull=True), + then=Value("DRAFT"), + ), + default=Value("DRAFT"), + output_field=CharField(), + ) + ) + .annotate( + assignee_ids=Coalesce( + ArrayAgg( + "issue_cycle__issue__assignees__id", + distinct=True, + filter=~Q( + issue_cycle__issue__assignees__id__isnull=True + ), + ), + Value([], output_field=ArrayField(UUIDField())), + ) + ) + .order_by("-is_favorite", "name") + .distinct() + ) + + 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( + # necessary fields + "id", + "workspace_id", + "project_id", + # model fields + "name", + "description", + "start_date", + "end_date", + "owned_by_id", + "view_props", + "sort_order", + "external_source", + "external_id", + "progress_snapshot", + # meta fields + "total_issues", + "is_favorite", + "cancelled_issues", + "completed_issues", + "started_issues", + "unstarted_issues", + "backlog_issues", + "assignee_ids", + "status", + "archived_at", + ) + ).order_by("-is_favorite", "-created_at") + return Response(queryset, status=status.HTTP_200_OK) + else: + queryset = ( + self.get_queryset() + .filter(archived_at__isnull=False) + .filter(pk=pk) + ) + data = ( + self.get_queryset() + .filter(pk=pk) + .annotate( + sub_issues=Issue.issue_objects.filter( + project_id=self.kwargs.get("project_id"), + parent__isnull=False, + issue_cycle__cycle_id=pk, + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .values( + # necessary fields + "id", + "workspace_id", + "project_id", + # model fields + "name", + "description", + "start_date", + "end_date", + "owned_by_id", + "view_props", + "sort_order", + "external_source", + "external_id", + "progress_snapshot", + "sub_issues", + # meta fields + "is_favorite", + "total_issues", + "cancelled_issues", + "completed_issues", + "started_issues", + "unstarted_issues", + "backlog_issues", + "assignee_ids", + "status", + ) + .first() + ) + queryset = queryset.first() + + if data is None: + return Response( + {"error": "Cycle does not exist"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Assignee Distribution + assignee_distribution = ( + Issue.objects.filter( + issue_cycle__cycle_id=pk, + workspace__slug=slug, + project_id=project_id, + ) + .annotate(first_name=F("assignees__first_name")) + .annotate(last_name=F("assignees__last_name")) + .annotate(assignee_id=F("assignees__id")) + .annotate(avatar=F("assignees__avatar")) + .annotate(display_name=F("assignees__display_name")) + .values( + "first_name", + "last_name", + "assignee_id", + "avatar", + "display_name", + ) + .annotate( + total_issues=Count( + "id", + filter=Q(archived_at__isnull=True, is_draft=False), + ), + ) + .annotate( + completed_issues=Count( + "id", + filter=Q( + completed_at__isnull=False, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .annotate( + pending_issues=Count( + "id", + filter=Q( + completed_at__isnull=True, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .order_by("first_name", "last_name") + ) + + # Label Distribution + label_distribution = ( + 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_issues=Count( + "id", + filter=Q(archived_at__isnull=True, is_draft=False), + ), + ) + .annotate( + completed_issues=Count( + "id", + filter=Q( + completed_at__isnull=False, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .annotate( + pending_issues=Count( + "id", + filter=Q( + completed_at__isnull=True, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .order_by("label_name") + ) + + data["distribution"] = { + "assignees": assignee_distribution, + "labels": label_distribution, + "completion_chart": {}, + } + + if queryset.start_date and queryset.end_date: + data["distribution"]["completion_chart"] = burndown_plot( + queryset=queryset, + slug=slug, + project_id=project_id, + cycle_id=pk, + ) + + return Response( + data, + status=status.HTTP_200_OK, + ) + + def post(self, request, slug, project_id, cycle_id): + cycle = Cycle.objects.get( + pk=cycle_id, project_id=project_id, workspace__slug=slug + ) + + if cycle.end_date >= timezone.now().date(): + return Response( + {"error": "Only completed cycles can be archived"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + cycle.archived_at = timezone.now() + cycle.save() + return Response( + {"archived_at": str(cycle.archived_at)}, + status=status.HTTP_200_OK, + ) + + def delete(self, request, slug, project_id, cycle_id): + cycle = Cycle.objects.get( + pk=cycle_id, project_id=project_id, workspace__slug=slug + ) + cycle.archived_at = None + cycle.save() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py index 58dd9891f..dd9826c56 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -1,10 +1,9 @@ # Python imports import json +# Django imports from django.contrib.postgres.aggregates import ArrayAgg from django.contrib.postgres.fields import ArrayField - -# Django imports from django.db.models import ( Case, CharField, @@ -25,7 +24,6 @@ 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, ProjectLitePermission, @@ -686,380 +684,6 @@ class CycleViewSet(WebhookMixin, BaseViewSet): return Response(status=status.HTTP_204_NO_CONTENT) -class CycleArchiveUnarchiveEndpoint(BaseAPIView): - - permission_classes = [ - ProjectEntityPermission, - ] - - def get_queryset(self): - favorite_subquery = CycleFavorite.objects.filter( - user=self.request.user, - cycle_id=OuterRef("pk"), - project_id=self.kwargs.get("project_id"), - workspace__slug=self.kwargs.get("slug"), - ) - return ( - Cycle.objects.filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - .filter(archived_at__isnull=False) - .filter( - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - ) - .filter(project__archived_at__isnull=True) - .select_related("project", "workspace", "owned_by") - .prefetch_related( - Prefetch( - "issue_cycle__issue__assignees", - queryset=User.objects.only( - "avatar", "first_name", "id" - ).distinct(), - ) - ) - .prefetch_related( - Prefetch( - "issue_cycle__issue__labels", - queryset=Label.objects.only( - "name", "color", "id" - ).distinct(), - ) - ) - .annotate(is_favorite=Exists(favorite_subquery)) - .annotate( - total_issues=Count( - "issue_cycle__issue__id", - distinct=True, - filter=Q( - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) - .annotate( - completed_issues=Count( - "issue_cycle__issue__id", - distinct=True, - filter=Q( - issue_cycle__issue__state__group="completed", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) - .annotate( - cancelled_issues=Count( - "issue_cycle__issue__id", - distinct=True, - filter=Q( - issue_cycle__issue__state__group="cancelled", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) - .annotate( - started_issues=Count( - "issue_cycle__issue__id", - distinct=True, - filter=Q( - issue_cycle__issue__state__group="started", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) - .annotate( - unstarted_issues=Count( - "issue_cycle__issue__id", - distinct=True, - filter=Q( - issue_cycle__issue__state__group="unstarted", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) - .annotate( - backlog_issues=Count( - "issue_cycle__issue__id", - distinct=True, - filter=Q( - issue_cycle__issue__state__group="backlog", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) - .annotate( - status=Case( - When( - Q(start_date__lte=timezone.now()) - & Q(end_date__gte=timezone.now()), - then=Value("CURRENT"), - ), - When( - start_date__gt=timezone.now(), then=Value("UPCOMING") - ), - When(end_date__lt=timezone.now(), then=Value("COMPLETED")), - When( - Q(start_date__isnull=True) & Q(end_date__isnull=True), - then=Value("DRAFT"), - ), - default=Value("DRAFT"), - output_field=CharField(), - ) - ) - .annotate( - assignee_ids=Coalesce( - ArrayAgg( - "issue_cycle__issue__assignees__id", - distinct=True, - filter=~Q( - issue_cycle__issue__assignees__id__isnull=True - ), - ), - Value([], output_field=ArrayField(UUIDField())), - ) - ) - .order_by("-is_favorite", "name") - .distinct() - ) - - 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( - # necessary fields - "id", - "workspace_id", - "project_id", - # model fields - "name", - "description", - "start_date", - "end_date", - "owned_by_id", - "view_props", - "sort_order", - "external_source", - "external_id", - "progress_snapshot", - # meta fields - "total_issues", - "is_favorite", - "cancelled_issues", - "completed_issues", - "started_issues", - "unstarted_issues", - "backlog_issues", - "assignee_ids", - "status", - "archived_at", - ) - ).order_by("-is_favorite", "-created_at") - return Response(queryset, status=status.HTTP_200_OK) - else: - queryset = ( - self.get_queryset() - .filter(archived_at__isnull=False) - .filter(pk=pk) - ) - data = ( - self.get_queryset() - .filter(pk=pk) - .annotate( - sub_issues=Issue.issue_objects.filter( - project_id=self.kwargs.get("project_id"), - parent__isnull=False, - issue_cycle__cycle_id=pk, - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .values( - # necessary fields - "id", - "workspace_id", - "project_id", - # model fields - "name", - "description", - "start_date", - "end_date", - "owned_by_id", - "view_props", - "sort_order", - "external_source", - "external_id", - "progress_snapshot", - "sub_issues", - # meta fields - "is_favorite", - "total_issues", - "cancelled_issues", - "completed_issues", - "started_issues", - "unstarted_issues", - "backlog_issues", - "assignee_ids", - "status", - ) - .first() - ) - queryset = queryset.first() - - if data is None: - return Response( - {"error": "Cycle does not exist"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - # Assignee Distribution - assignee_distribution = ( - Issue.objects.filter( - issue_cycle__cycle_id=pk, - workspace__slug=slug, - project_id=project_id, - ) - .annotate(first_name=F("assignees__first_name")) - .annotate(last_name=F("assignees__last_name")) - .annotate(assignee_id=F("assignees__id")) - .annotate(avatar=F("assignees__avatar")) - .annotate(display_name=F("assignees__display_name")) - .values( - "first_name", - "last_name", - "assignee_id", - "avatar", - "display_name", - ) - .annotate( - total_issues=Count( - "id", - filter=Q(archived_at__isnull=True, is_draft=False), - ), - ) - .annotate( - completed_issues=Count( - "id", - filter=Q( - completed_at__isnull=False, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .annotate( - pending_issues=Count( - "id", - filter=Q( - completed_at__isnull=True, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .order_by("first_name", "last_name") - ) - - # Label Distribution - label_distribution = ( - 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_issues=Count( - "id", - filter=Q(archived_at__isnull=True, is_draft=False), - ), - ) - .annotate( - completed_issues=Count( - "id", - filter=Q( - completed_at__isnull=False, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .annotate( - pending_issues=Count( - "id", - filter=Q( - completed_at__isnull=True, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .order_by("label_name") - ) - - data["distribution"] = { - "assignees": assignee_distribution, - "labels": label_distribution, - "completion_chart": {}, - } - - if queryset.start_date and queryset.end_date: - data["distribution"]["completion_chart"] = burndown_plot( - queryset=queryset, - slug=slug, - project_id=project_id, - cycle_id=pk, - ) - - return Response( - data, - status=status.HTTP_200_OK, - ) - - def post(self, request, slug, project_id, cycle_id): - cycle = Cycle.objects.get( - pk=cycle_id, project_id=project_id, workspace__slug=slug - ) - - if cycle.end_date >= timezone.now().date(): - return Response( - {"error": "Only completed cycles can be archived"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - cycle.archived_at = timezone.now() - cycle.save() - return Response( - {"archived_at": str(cycle.archived_at)}, - status=status.HTTP_200_OK, - ) - - def delete(self, request, slug, project_id, cycle_id): - cycle = Cycle.objects.get( - pk=cycle_id, project_id=project_id, workspace__slug=slug - ) - cycle.archived_at = None - cycle.save() - return Response(status=status.HTTP_204_NO_CONTENT) - - class CycleDateCheckEndpoint(BaseAPIView): permission_classes = [ ProjectEntityPermission, diff --git a/apiserver/plane/app/views/dashboard/base.py b/apiserver/plane/app/views/dashboard/base.py index 33b3cf9d5..9558348d9 100644 --- a/apiserver/plane/app/views/dashboard/base.py +++ b/apiserver/plane/app/views/dashboard/base.py @@ -571,14 +571,16 @@ def dashboard_recent_collaborators(self, request, slug): return self.paginate( request=request, queryset=project_members_with_activities, - controller=self.get_results_controller, + controller=lambda qs: self.get_results_controller(qs, slug), ) class DashboardEndpoint(BaseAPIView): - def get_results_controller(self, project_members_with_activities): + def get_results_controller(self, project_members_with_activities, slug): user_active_issue_counts = ( - User.objects.filter(id__in=project_members_with_activities) + User.objects.filter( + id__in=project_members_with_activities, + ) .annotate( active_issue_count=Count( Case( @@ -587,10 +589,13 @@ class DashboardEndpoint(BaseAPIView): "unstarted", "started", ], - then=1, + issue_assignee__issue__workspace__slug=slug, + issue_assignee__issue__project__project_projectmember__is_active=True, + then=F("issue_assignee__issue__id"), ), output_field=IntegerField(), - ) + ), + distinct=True, ) ) .values("active_issue_count", user_id=F("id")) diff --git a/apiserver/plane/app/views/issue/draft.py b/apiserver/plane/app/views/issue/draft.py index 62a0aa25c..077d7dcaf 100644 --- a/apiserver/plane/app/views/issue/draft.py +++ b/apiserver/plane/app/views/issue/draft.py @@ -1,6 +1,7 @@ # Python imports import json +# Django imports from django.contrib.postgres.aggregates import ArrayAgg from django.contrib.postgres.fields import ArrayField from django.core.serializers.json import DjangoJSONEncoder @@ -19,14 +20,12 @@ from django.db.models import ( When, ) from django.db.models.functions import Coalesce - -# Django imports from django.utils import timezone from django.utils.decorators import method_decorator from django.views.decorators.gzip import gzip_page -from rest_framework import status # Third Party imports +from rest_framework import status from rest_framework.response import Response from plane.app.permissions import ProjectEntityPermission diff --git a/apiserver/plane/app/views/module/archive.py b/apiserver/plane/app/views/module/archive.py new file mode 100644 index 000000000..9c0b6cca3 --- /dev/null +++ b/apiserver/plane/app/views/module/archive.py @@ -0,0 +1,356 @@ +from django.contrib.postgres.aggregates import ArrayAgg +from django.contrib.postgres.fields import ArrayField +from django.db.models import ( + Count, + Exists, + F, + Func, + IntegerField, + OuterRef, + Prefetch, + Q, + Subquery, + UUIDField, + Value, +) +from django.db.models.functions import Coalesce +from django.utils import timezone + +# Third party imports +from rest_framework import status +from rest_framework.response import Response +from plane.app.permissions import ( + ProjectEntityPermission, +) +from plane.app.serializers import ( + ModuleDetailSerializer, +) +from plane.db.models import ( + Issue, + Module, + ModuleFavorite, + ModuleLink, +) +from plane.utils.analytics_plot import burndown_plot + +# Module imports +from .. import BaseAPIView + + +class ModuleArchiveUnarchiveEndpoint(BaseAPIView): + + permission_classes = [ + ProjectEntityPermission, + ] + + def get_queryset(self): + favorite_subquery = ModuleFavorite.objects.filter( + user=self.request.user, + module_id=OuterRef("pk"), + project_id=self.kwargs.get("project_id"), + workspace__slug=self.kwargs.get("slug"), + ) + cancelled_issues = ( + Issue.issue_objects.filter( + state__group="cancelled", + issue_module__module_id=OuterRef("pk"), + ) + .values("issue_module__module_id") + .annotate(cnt=Count("pk")) + .values("cnt") + ) + completed_issues = ( + Issue.issue_objects.filter( + state__group="completed", + issue_module__module_id=OuterRef("pk"), + ) + .values("issue_module__module_id") + .annotate(cnt=Count("pk")) + .values("cnt") + ) + started_issues = ( + Issue.issue_objects.filter( + state__group="started", + issue_module__module_id=OuterRef("pk"), + ) + .values("issue_module__module_id") + .annotate(cnt=Count("pk")) + .values("cnt") + ) + unstarted_issues = ( + Issue.issue_objects.filter( + state__group="unstarted", + issue_module__module_id=OuterRef("pk"), + ) + .values("issue_module__module_id") + .annotate(cnt=Count("pk")) + .values("cnt") + ) + backlog_issues = ( + Issue.issue_objects.filter( + state__group="backlog", + issue_module__module_id=OuterRef("pk"), + ) + .values("issue_module__module_id") + .annotate(cnt=Count("pk")) + .values("cnt") + ) + total_issues = ( + Issue.issue_objects.filter( + issue_module__module_id=OuterRef("pk"), + ) + .values("issue_module__module_id") + .annotate(cnt=Count("pk")) + .values("cnt") + ) + return ( + Module.objects.filter(workspace__slug=self.kwargs.get("slug")) + .filter(archived_at__isnull=False) + .annotate(is_favorite=Exists(favorite_subquery)) + .select_related("workspace", "project", "lead") + .prefetch_related("members") + .prefetch_related( + Prefetch( + "link_module", + queryset=ModuleLink.objects.select_related( + "module", "created_by" + ), + ) + ) + .annotate( + completed_issues=Coalesce( + Subquery(completed_issues[:1]), + Value(0, output_field=IntegerField()), + ) + ) + .annotate( + cancelled_issues=Coalesce( + Subquery(cancelled_issues[:1]), + Value(0, output_field=IntegerField()), + ) + ) + .annotate( + started_issues=Coalesce( + Subquery(started_issues[:1]), + Value(0, output_field=IntegerField()), + ) + ) + .annotate( + unstarted_issues=Coalesce( + Subquery(unstarted_issues[:1]), + Value(0, output_field=IntegerField()), + ) + ) + .annotate( + backlog_issues=Coalesce( + Subquery(backlog_issues[:1]), + Value(0, output_field=IntegerField()), + ) + ) + .annotate( + total_issues=Coalesce( + Subquery(total_issues[:1]), + Value(0, output_field=IntegerField()), + ) + ) + .annotate( + member_ids=Coalesce( + ArrayAgg( + "members__id", + distinct=True, + filter=~Q(members__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ) + ) + .order_by("-is_favorite", "-created_at") + ) + + def get(self, request, slug, project_id, pk=None): + if pk is None: + queryset = self.get_queryset() + modules = queryset.values( # Required fields + "id", + "workspace_id", + "project_id", + # Model fields + "name", + "description", + "description_text", + "description_html", + "start_date", + "target_date", + "status", + "lead_id", + "member_ids", + "view_props", + "sort_order", + "external_source", + "external_id", + # computed fields + "total_issues", + "is_favorite", + "cancelled_issues", + "completed_issues", + "started_issues", + "unstarted_issues", + "backlog_issues", + "created_at", + "updated_at", + "archived_at", + ) + return Response(modules, status=status.HTTP_200_OK) + else: + queryset = ( + self.get_queryset() + .filter(pk=pk) + .annotate( + sub_issues=Issue.issue_objects.filter( + project_id=self.kwargs.get("project_id"), + parent__isnull=False, + issue_module__module_id=pk, + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + ) + assignee_distribution = ( + Issue.objects.filter( + issue_module__module_id=pk, + workspace__slug=slug, + project_id=project_id, + ) + .annotate(first_name=F("assignees__first_name")) + .annotate(last_name=F("assignees__last_name")) + .annotate(assignee_id=F("assignees__id")) + .annotate(display_name=F("assignees__display_name")) + .annotate(avatar=F("assignees__avatar")) + .values( + "first_name", + "last_name", + "assignee_id", + "avatar", + "display_name", + ) + .annotate( + total_issues=Count( + "id", + filter=Q( + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .annotate( + completed_issues=Count( + "id", + filter=Q( + completed_at__isnull=False, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .annotate( + pending_issues=Count( + "id", + filter=Q( + completed_at__isnull=True, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .order_by("first_name", "last_name") + ) + + label_distribution = ( + Issue.objects.filter( + issue_module__module_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_issues=Count( + "id", + filter=Q( + archived_at__isnull=True, + is_draft=False, + ), + ), + ) + .annotate( + completed_issues=Count( + "id", + filter=Q( + completed_at__isnull=False, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .annotate( + pending_issues=Count( + "id", + filter=Q( + completed_at__isnull=True, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .order_by("label_name") + ) + + data = ModuleDetailSerializer(queryset.first()).data + data["distribution"] = { + "assignees": assignee_distribution, + "labels": label_distribution, + "completion_chart": {}, + } + + # Fetch the modules + modules = queryset.first() + if modules and modules.start_date and modules.target_date: + data["distribution"]["completion_chart"] = burndown_plot( + queryset=modules, + slug=slug, + project_id=project_id, + module_id=pk, + ) + + return Response( + data, + status=status.HTTP_200_OK, + ) + + def post(self, request, slug, project_id, module_id): + module = Module.objects.get( + pk=module_id, project_id=project_id, workspace__slug=slug + ) + if module.status not in ["completed", "cancelled"]: + return Response( + { + "error": "Only completed or cancelled modules can be archived" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + module.archived_at = timezone.now() + module.save() + return Response( + {"archived_at": str(module.archived_at)}, + status=status.HTTP_200_OK, + ) + + def delete(self, request, slug, project_id, module_id): + module = Module.objects.get( + pk=module_id, project_id=project_id, workspace__slug=slug + ) + module.archived_at = None + module.save() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/module/base.py b/apiserver/plane/app/views/module/base.py index aaaf8fb67..4cd52b3b1 100644 --- a/apiserver/plane/app/views/module/base.py +++ b/apiserver/plane/app/views/module/base.py @@ -515,325 +515,6 @@ class ModuleLinkViewSet(BaseViewSet): ) -class ModuleArchiveUnarchiveEndpoint(BaseAPIView): - - permission_classes = [ - ProjectEntityPermission, - ] - - def get_queryset(self): - favorite_subquery = ModuleFavorite.objects.filter( - user=self.request.user, - module_id=OuterRef("pk"), - project_id=self.kwargs.get("project_id"), - workspace__slug=self.kwargs.get("slug"), - ) - cancelled_issues = ( - Issue.issue_objects.filter( - state__group="cancelled", - issue_module__module_id=OuterRef("pk"), - ) - .values("issue_module__module_id") - .annotate(cnt=Count("pk")) - .values("cnt") - ) - completed_issues = ( - Issue.issue_objects.filter( - state__group="completed", - issue_module__module_id=OuterRef("pk"), - ) - .values("issue_module__module_id") - .annotate(cnt=Count("pk")) - .values("cnt") - ) - started_issues = ( - Issue.issue_objects.filter( - state__group="started", - issue_module__module_id=OuterRef("pk"), - ) - .values("issue_module__module_id") - .annotate(cnt=Count("pk")) - .values("cnt") - ) - unstarted_issues = ( - Issue.issue_objects.filter( - state__group="unstarted", - issue_module__module_id=OuterRef("pk"), - ) - .values("issue_module__module_id") - .annotate(cnt=Count("pk")) - .values("cnt") - ) - backlog_issues = ( - Issue.issue_objects.filter( - state__group="backlog", - issue_module__module_id=OuterRef("pk"), - ) - .values("issue_module__module_id") - .annotate(cnt=Count("pk")) - .values("cnt") - ) - total_issues = ( - Issue.issue_objects.filter( - issue_module__module_id=OuterRef("pk"), - ) - .values("issue_module__module_id") - .annotate(cnt=Count("pk")) - .values("cnt") - ) - return ( - Module.objects.filter(workspace__slug=self.kwargs.get("slug")) - .filter(archived_at__isnull=False) - .annotate(is_favorite=Exists(favorite_subquery)) - .select_related("workspace", "project", "lead") - .prefetch_related("members") - .prefetch_related( - Prefetch( - "link_module", - queryset=ModuleLink.objects.select_related( - "module", "created_by" - ), - ) - ) - .annotate( - completed_issues=Coalesce( - Subquery(completed_issues[:1]), - Value(0, output_field=IntegerField()), - ) - ) - .annotate( - cancelled_issues=Coalesce( - Subquery(cancelled_issues[:1]), - Value(0, output_field=IntegerField()), - ) - ) - .annotate( - started_issues=Coalesce( - Subquery(started_issues[:1]), - Value(0, output_field=IntegerField()), - ) - ) - .annotate( - unstarted_issues=Coalesce( - Subquery(unstarted_issues[:1]), - Value(0, output_field=IntegerField()), - ) - ) - .annotate( - backlog_issues=Coalesce( - Subquery(backlog_issues[:1]), - Value(0, output_field=IntegerField()), - ) - ) - .annotate( - total_issues=Coalesce( - Subquery(total_issues[:1]), - Value(0, output_field=IntegerField()), - ) - ) - .annotate( - member_ids=Coalesce( - ArrayAgg( - "members__id", - distinct=True, - filter=~Q(members__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ) - ) - .order_by("-is_favorite", "-created_at") - ) - - def get(self, request, slug, project_id, pk=None): - if pk is None: - queryset = self.get_queryset() - modules = queryset.values( # Required fields - "id", - "workspace_id", - "project_id", - # Model fields - "name", - "description", - "description_text", - "description_html", - "start_date", - "target_date", - "status", - "lead_id", - "member_ids", - "view_props", - "sort_order", - "external_source", - "external_id", - # computed fields - "total_issues", - "is_favorite", - "cancelled_issues", - "completed_issues", - "started_issues", - "unstarted_issues", - "backlog_issues", - "created_at", - "updated_at", - "archived_at", - ) - return Response(modules, status=status.HTTP_200_OK) - else: - queryset = ( - self.get_queryset() - .filter(pk=pk) - .annotate( - sub_issues=Issue.issue_objects.filter( - project_id=self.kwargs.get("project_id"), - parent__isnull=False, - issue_module__module_id=pk, - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - ) - assignee_distribution = ( - Issue.objects.filter( - issue_module__module_id=pk, - workspace__slug=slug, - project_id=project_id, - ) - .annotate(first_name=F("assignees__first_name")) - .annotate(last_name=F("assignees__last_name")) - .annotate(assignee_id=F("assignees__id")) - .annotate(display_name=F("assignees__display_name")) - .annotate(avatar=F("assignees__avatar")) - .values( - "first_name", - "last_name", - "assignee_id", - "avatar", - "display_name", - ) - .annotate( - total_issues=Count( - "id", - filter=Q( - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .annotate( - completed_issues=Count( - "id", - filter=Q( - completed_at__isnull=False, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .annotate( - pending_issues=Count( - "id", - filter=Q( - completed_at__isnull=True, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .order_by("first_name", "last_name") - ) - - label_distribution = ( - Issue.objects.filter( - issue_module__module_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_issues=Count( - "id", - filter=Q( - archived_at__isnull=True, - is_draft=False, - ), - ), - ) - .annotate( - completed_issues=Count( - "id", - filter=Q( - completed_at__isnull=False, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .annotate( - pending_issues=Count( - "id", - filter=Q( - completed_at__isnull=True, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .order_by("label_name") - ) - - data = ModuleDetailSerializer(queryset.first()).data - data["distribution"] = { - "assignees": assignee_distribution, - "labels": label_distribution, - "completion_chart": {}, - } - - # Fetch the modules - modules = queryset.first() - if modules and modules.start_date and modules.target_date: - data["distribution"]["completion_chart"] = burndown_plot( - queryset=modules, - slug=slug, - project_id=project_id, - module_id=pk, - ) - - return Response( - data, - status=status.HTTP_200_OK, - ) - - def post(self, request, slug, project_id, module_id): - module = Module.objects.get( - pk=module_id, project_id=project_id, workspace__slug=slug - ) - if module.status not in ["completed", "cancelled"]: - return Response( - { - "error": "Only completed or cancelled modules can be archived" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - module.archived_at = timezone.now() - module.save() - return Response( - {"archived_at": str(module.archived_at)}, - status=status.HTTP_200_OK, - ) - - def delete(self, request, slug, project_id, module_id): - module = Module.objects.get( - pk=module_id, project_id=project_id, workspace__slug=slug - ) - module.archived_at = None - module.save() - return Response(status=status.HTTP_204_NO_CONTENT) - - class ModuleFavoriteViewSet(BaseViewSet): serializer_class = ModuleFavoriteSerializer model = ModuleFavorite diff --git a/apiserver/plane/app/views/user/base.py b/apiserver/plane/app/views/user/base.py index 4d69d1cf2..487e365cd 100644 --- a/apiserver/plane/app/views/user/base.py +++ b/apiserver/plane/app/views/user/base.py @@ -49,7 +49,12 @@ class UserEndpoint(BaseViewSet): {"is_instance_admin": is_admin}, status=status.HTTP_200_OK ) - @invalidate_cache(path="/api/users/me/") + @invalidate_cache( + path="/api/users/me/", + ) + @invalidate_cache( + path="/api/users/me/settings/", + ) def partial_update(self, request, *args, **kwargs): return super().partial_update(request, *args, **kwargs) diff --git a/apiserver/plane/db/management/commands/test_email.py b/apiserver/plane/db/management/commands/test_email.py index 99c5d9684..63b602518 100644 --- a/apiserver/plane/db/management/commands/test_email.py +++ b/apiserver/plane/db/management/commands/test_email.py @@ -15,7 +15,7 @@ class Command(BaseCommand): receiver_email = options.get("to_email") if not receiver_email: - raise CommandError("Reciever email is required") + raise CommandError("Receiver email is required") ( EMAIL_HOST, @@ -54,7 +54,7 @@ class Command(BaseCommand): connection=connection, ) msg.send() - self.stdout.write(self.style.SUCCESS("Email succesfully sent")) + self.stdout.write(self.style.SUCCESS("Email successfully sent")) except Exception as e: self.stdout.write( self.style.ERROR( diff --git a/packages/ui/src/form-fields/textarea.tsx b/packages/ui/src/form-fields/textarea.tsx index de225d68f..2c47a65f5 100644 --- a/packages/ui/src/form-fields/textarea.tsx +++ b/packages/ui/src/form-fields/textarea.tsx @@ -11,17 +11,7 @@ export interface TextAreaProps extends React.TextareaHTMLAttributes((props, ref) => { - const { - id, - name, - value = "", - rows = 1, - cols = 1, - mode = "primary", - hasError = false, - className = "", - ...rest - } = props; + const { id, name, value = "", mode = "primary", hasError = false, className = "", ...rest } = props; // refs const textAreaRef = useRef(ref); // auto re-size @@ -33,8 +23,6 @@ const TextArea = React.forwardRef((props, re name={name} ref={textAreaRef} value={value} - rows={rows} - cols={cols} className={cn( "no-scrollbar w-full bg-transparent px-3 py-2 placeholder-custom-text-400 outline-none", { diff --git a/space/components/editor/lite-text-editor.tsx b/space/components/editor/lite-text-editor.tsx index 677a34257..b911ebecb 100644 --- a/space/components/editor/lite-text-editor.tsx +++ b/space/components/editor/lite-text-editor.tsx @@ -34,10 +34,9 @@ export const LiteTextEditor = React.forwardRef

" || - isEmptyHtmlString(props.initialValue ?? ""); + (isEmptyHtmlString(props.initialValue ?? "") && !props.initialValue?.includes("mention-component")); return (
diff --git a/web/components/core/activity.tsx b/web/components/core/activity.tsx index 31a796949..629e9908d 100644 --- a/web/components/core/activity.tsx +++ b/web/components/core/activity.tsx @@ -17,6 +17,7 @@ import { SignalMediumIcon, MessageSquareIcon, UsersIcon, + Inbox, } from "lucide-react"; import { IIssueActivity } from "@plane/types"; import { Tooltip, BlockedIcon, BlockerIcon, RelatedIcon, LayersIcon, DiceIcon } from "@plane/ui"; @@ -112,6 +113,40 @@ const EstimatePoint = observer((props: { point: string }) => { ); }); +const inboxActivityMessage = { + declined: { + showIssue: "declined issue", + noIssue: "declined this issue from inbox.", + }, + snoozed: { + showIssue: "snoozed issue", + noIssue: "snoozed this issue.", + }, + accepted: { + showIssue: "accepted issue", + noIssue: "accepted this issue from inbox.", + }, + markedDuplicate: { + showIssue: "declined issue", + noIssue: "declined this issue from inbox by marking a duplicate issue.", + }, +}; + +const getInboxUserActivityMessage = (activity: IIssueActivity, showIssue: boolean) => { + switch (activity.verb) { + case "-1": + return showIssue ? inboxActivityMessage.declined.showIssue : inboxActivityMessage.declined.noIssue; + case "0": + return showIssue ? inboxActivityMessage.snoozed.showIssue : inboxActivityMessage.snoozed.noIssue; + case "1": + return showIssue ? inboxActivityMessage.accepted.showIssue : inboxActivityMessage.accepted.noIssue; + case "2": + return showIssue ? inboxActivityMessage.markedDuplicate.showIssue : inboxActivityMessage.markedDuplicate.noIssue; + default: + return "updated inbox issue status."; + } +}; + const activityDetails: { [key: string]: { message: (activity: IIssueActivity, showIssue: boolean, workspaceSlug: string) => React.ReactNode; @@ -658,8 +693,7 @@ const activityDetails: { {renderFormattedDate(activity.new_value)} {showIssue && ( <> - {" "} - for + )} @@ -667,6 +701,20 @@ const activityDetails: { }, icon:
- ) : activityTab === "activity" ? ( - ) : (
= observer((prop }; return ( - + {({ open, close: closePopover }) => ( <> diff --git a/web/components/issues/issue-layouts/filters/applied-filters/cycle.tsx b/web/components/issues/issue-layouts/filters/applied-filters/cycle.tsx index 4f139727f..592a12123 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/cycle.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/cycle.tsx @@ -28,9 +28,9 @@ export const AppliedCycleFilters: React.FC = observer((props) => { const cycleStatus = (cycleDetails?.status ? cycleDetails?.status.toLocaleLowerCase() : "draft") as TCycleGroups; return ( -
+
- {cycleDetails.name} + {cycleDetails.name} {editable && ( + + + - removeRoutePeekId()}> - - + + removeRoutePeekId()}> + + + {currentMode && (
setPeekMode(val)} customButton={ - + + + } > {PEEK_OPTIONS.map((mode) => ( diff --git a/web/components/modules/archived-modules/header.tsx b/web/components/modules/archived-modules/header.tsx index f72d35f4e..d4d5ef37a 100644 --- a/web/components/modules/archived-modules/header.tsx +++ b/web/components/modules/archived-modules/header.tsx @@ -43,12 +43,12 @@ export const ArchivedModulesHeader: FC = observer(() => { const handleFilters = useCallback( (key: keyof TModuleFilters, value: string | string[]) => { if (!projectId) return; - const newValues = currentProjectArchivedFilters?.[key] ?? []; if (Array.isArray(value)) value.forEach((val) => { if (!newValues.includes(val)) newValues.push(val); + else newValues.splice(newValues.indexOf(val), 1); }); else { if (currentProjectArchivedFilters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); diff --git a/web/components/modules/dropdowns/filters/start-date.tsx b/web/components/modules/dropdowns/filters/start-date.tsx index 2b55ada35..0b8408c2e 100644 --- a/web/components/modules/dropdowns/filters/start-date.tsx +++ b/web/components/modules/dropdowns/filters/start-date.tsx @@ -6,6 +6,8 @@ import { DateFilterModal } from "@/components/core"; import { FilterHeader, FilterOption } from "@/components/issues"; // constants import { DATE_AFTER_FILTER_OPTIONS } from "@/constants/filters"; +// helpers +import { isInDateFormat } from "@/helpers/date-time.helper"; type Props = { appliedFilters: string[] | null; @@ -25,6 +27,17 @@ export const FilterStartDate: React.FC = observer((props) => { d.name.toLowerCase().includes(searchQuery.toLowerCase()) ); + const isCustomDateSelected = () => { + const isValidDateSelected = appliedFilters?.filter((f) => isInDateFormat(f.split(";")[0])) || []; + return isValidDateSelected.length > 0 ? true : false; + }; + const handleCustomDate = () => { + if (isCustomDateSelected()) { + const updateAppliedFilters = appliedFilters?.filter((f) => f.includes("-")) || []; + handleUpdate(updateAppliedFilters); + } else setIsDateFilterModalOpen(true); + }; + return ( <> {isDateFilterModalOpen && ( @@ -53,7 +66,7 @@ export const FilterStartDate: React.FC = observer((props) => { multiple /> ))} - setIsDateFilterModalOpen(true)} title="Custom" multiple /> + ) : (

No matches found

diff --git a/web/components/modules/dropdowns/filters/target-date.tsx b/web/components/modules/dropdowns/filters/target-date.tsx index cbb45eb7a..f97021720 100644 --- a/web/components/modules/dropdowns/filters/target-date.tsx +++ b/web/components/modules/dropdowns/filters/target-date.tsx @@ -6,6 +6,8 @@ import { DateFilterModal } from "@/components/core"; import { FilterHeader, FilterOption } from "@/components/issues"; // constants import { DATE_AFTER_FILTER_OPTIONS } from "@/constants/filters"; +// helpers +import { isInDateFormat } from "@/helpers/date-time.helper"; type Props = { appliedFilters: string[] | null; @@ -25,6 +27,17 @@ export const FilterTargetDate: React.FC = observer((props) => { d.name.toLowerCase().includes(searchQuery.toLowerCase()) ); + const isCustomDateSelected = () => { + const isValidDateSelected = appliedFilters?.filter((f) => isInDateFormat(f.split(";")[0])) || []; + return isValidDateSelected.length > 0 ? true : false; + }; + const handleCustomDate = () => { + if (isCustomDateSelected()) { + const updateAppliedFilters = appliedFilters?.filter((f) => f.includes("-")) || []; + handleUpdate(updateAppliedFilters); + } else setIsDateFilterModalOpen(true); + }; + return ( <> {isDateFilterModalOpen && ( @@ -53,7 +66,7 @@ export const FilterTargetDate: React.FC = observer((props) => { multiple /> ))} - setIsDateFilterModalOpen(true)} title="Custom" multiple /> + ) : (

No matches found

diff --git a/web/components/pages/list/filters/created-at.tsx b/web/components/pages/list/filters/created-at.tsx index 5a1b75c00..731bfa844 100644 --- a/web/components/pages/list/filters/created-at.tsx +++ b/web/components/pages/list/filters/created-at.tsx @@ -5,6 +5,8 @@ import { DateFilterModal } from "@/components/core"; import { FilterHeader, FilterOption } from "@/components/issues"; // constants import { DATE_BEFORE_FILTER_OPTIONS } from "@/constants/filters"; +// helpers +import { isInDateFormat } from "@/helpers/date-time.helper"; type Props = { appliedFilters: string[] | null; @@ -24,6 +26,17 @@ export const FilterCreatedDate: React.FC = observer((props) => { d.name.toLowerCase().includes(searchQuery.toLowerCase()) ); + const isCustomDateSelected = () => { + const isValidDateSelected = appliedFilters?.filter((f) => isInDateFormat(f.split(";")[0])) || []; + return isValidDateSelected.length > 0 ? true : false; + }; + const handleCustomDate = () => { + if (isCustomDateSelected()) { + const updateAppliedFilters = appliedFilters?.filter((f) => f.includes("-")) || []; + handleUpdate(updateAppliedFilters); + } else setIsDateFilterModalOpen(true); + }; + return ( <> {isDateFilterModalOpen && ( @@ -52,7 +65,7 @@ export const FilterCreatedDate: React.FC = observer((props) => { multiple /> ))} - setIsDateFilterModalOpen(true)} title="Custom" multiple /> + ) : (

No matches found

diff --git a/web/components/pages/list/filters/root.tsx b/web/components/pages/list/filters/root.tsx index 1107bbe95..475f89054 100644 --- a/web/components/pages/list/filters/root.tsx +++ b/web/components/pages/list/filters/root.tsx @@ -27,6 +27,7 @@ export const PageFiltersSelection: React.FC = observer((props) => { if (Array.isArray(value)) value.forEach((val) => { if (!newValues.includes(val)) newValues.push(val); + else newValues.splice(newValues.indexOf(val), 1); }); else if (typeof value === "string") { if (newValues?.includes(value)) newValues.splice(newValues.indexOf(value), 1); diff --git a/web/components/project/confirm-project-member-remove.tsx b/web/components/project/confirm-project-member-remove.tsx index 10cd16f44..bc8141991 100644 --- a/web/components/project/confirm-project-member-remove.tsx +++ b/web/components/project/confirm-project-member-remove.tsx @@ -1,13 +1,14 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { AlertTriangle } from "lucide-react"; import { Dialog, Transition } from "@headlessui/react"; -import { IUserLite } from "@plane/types"; -// hooks -import { Button } from "@plane/ui"; -import { useUser } from "@/hooks/store"; -// ui // types +import { IUserLite } from "@plane/types"; +// ui +import { Button } from "@plane/ui"; +// hooks +import { useProject, useUser } from "@/hooks/store"; type Props = { data: IUserLite; @@ -18,10 +19,14 @@ type Props = { export const ConfirmProjectMemberRemove: React.FC = observer((props) => { const { data, onSubmit, isOpen, onClose } = props; + // router + const router = useRouter(); + const { projectId } = router.query; // states const [isDeleteLoading, setIsDeleteLoading] = useState(false); // store hooks const { currentUser } = useUser(); + const { getProjectById } = useProject(); const handleClose = () => { onClose(); @@ -36,7 +41,10 @@ export const ConfirmProjectMemberRemove: React.FC = observer((props) => { handleClose(); }; + if (!projectId) return <>; + const isCurrentUser = currentUser?.id === data?.id; + const currentProjectDetails = getProjectById(projectId.toString()); return ( @@ -76,9 +84,19 @@ export const ConfirmProjectMemberRemove: React.FC = observer((props) => {

- Are you sure you want to remove member-{" "} - {data?.display_name}? They will no longer have access to - this project. This action cannot be undone. + {isCurrentUser ? ( + <> + Are you sure you want to leave the{" "} + {currentProjectDetails?.name} project? You will be able + to join the project if invited again or if it{"'"}s public. + + ) : ( + <> + Are you sure you want to remove member-{" "} + {data?.display_name}? They will no longer have access + to this project. This action cannot be undone. + + )}

diff --git a/web/components/project/dropdowns/filters/created-at.tsx b/web/components/project/dropdowns/filters/created-at.tsx index 899281421..731bfa844 100644 --- a/web/components/project/dropdowns/filters/created-at.tsx +++ b/web/components/project/dropdowns/filters/created-at.tsx @@ -6,7 +6,7 @@ import { FilterHeader, FilterOption } from "@/components/issues"; // constants import { DATE_BEFORE_FILTER_OPTIONS } from "@/constants/filters"; // helpers -import { isDate } from "@/helpers/date-time.helper"; +import { isInDateFormat } from "@/helpers/date-time.helper"; type Props = { appliedFilters: string[] | null; @@ -27,7 +27,7 @@ export const FilterCreatedDate: React.FC = observer((props) => { ); const isCustomDateSelected = () => { - const isValidDateSelected = appliedFilters?.filter((f) => isDate(f.split(";")[0])) || []; + const isValidDateSelected = appliedFilters?.filter((f) => isInDateFormat(f.split(";")[0])) || []; return isValidDateSelected.length > 0 ? true : false; }; const handleCustomDate = () => { diff --git a/web/components/project/project-feature-update.tsx b/web/components/project/project-feature-update.tsx index 7bf6a8999..2359b6609 100644 --- a/web/components/project/project-feature-update.tsx +++ b/web/components/project/project-feature-update.tsx @@ -35,8 +35,8 @@ export const ProjectFeatureUpdate: FC = observer((props) => {
- Congrats! Project {currentProjectDetails.name}{" "} - created. + Congrats! Project {" "} +

{currentProjectDetails.name}

created.