diff --git a/apiserver/plane/space/urls/project.py b/apiserver/plane/space/urls/project.py index dc97b43a7..f582a65af 100644 --- a/apiserver/plane/space/urls/project.py +++ b/apiserver/plane/space/urls/project.py @@ -4,6 +4,7 @@ from django.urls import path from plane.space.views import ( ProjectDeployBoardPublicSettingsEndpoint, ProjectIssuesPublicEndpoint, + ProjectIssuesPublicGroupedEndpoint, ) urlpatterns = [ @@ -17,4 +18,9 @@ urlpatterns = [ ProjectIssuesPublicEndpoint.as_view(), name="project-deploy-board", ), + path( + "v3/workspaces//project-boards//issues/", + ProjectIssuesPublicGroupedEndpoint.as_view(), + name="project-deploy-board", + ), ] diff --git a/apiserver/plane/space/views/__init__.py b/apiserver/plane/space/views/__init__.py index 5130e04d5..8c9ea4725 100644 --- a/apiserver/plane/space/views/__init__.py +++ b/apiserver/plane/space/views/__init__.py @@ -10,6 +10,7 @@ from .issue import ( IssueVotePublicViewSet, IssueRetrievePublicEndpoint, ProjectIssuesPublicEndpoint, + ProjectIssuesPublicGroupedEndpoint, ) from .inbox import InboxIssuePublicViewSet diff --git a/apiserver/plane/space/views/issue.py b/apiserver/plane/space/views/issue.py index faab8834d..581ca9bf9 100644 --- a/apiserver/plane/space/views/issue.py +++ b/apiserver/plane/space/views/issue.py @@ -653,4 +653,165 @@ class ProjectIssuesPublicEndpoint(BaseAPIView): "labels": labels, }, status=status.HTTP_200_OK, + ) + + +class ProjectIssuesPublicGroupedEndpoint(BaseAPIView): + permission_classes = [ + AllowAny, + ] + + def get(self, request, slug, project_id): + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=slug, project_id=project_id + ) + filters = issue_filters(request.query_params, "GET") + + # Custom ordering for priority and state + priority_order = ["urgent", "high", "medium", "low", "none"] + state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] + + order_by_param = request.GET.get("order_by", "-created_at") + + issue_queryset = ( + Issue.issue_objects.annotate( + sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .filter(project_id=project_id) + .filter(workspace__slug=slug) + .select_related("project", "workspace", "state", "parent") + .prefetch_related("assignees", "labels") + .prefetch_related( + Prefetch( + "issue_reactions", + queryset=IssueReaction.objects.select_related("actor"), + ) + ) + .prefetch_related( + Prefetch( + "votes", + queryset=IssueVote.objects.select_related("actor"), + ) + ) + .filter(**filters) + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate(module_id=F("issue_module__module_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + ) + + # Priority Ordering + if order_by_param == "priority" or order_by_param == "-priority": + priority_order = ( + priority_order if order_by_param == "priority" else priority_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + priority_order=Case( + *[ + When(priority=p, then=Value(i)) + for i, p in enumerate(priority_order) + ], + output_field=CharField(), + ) + ).order_by("priority_order") + + # State Ordering + elif order_by_param in [ + "state__name", + "state__group", + "-state__name", + "-state__group", + ]: + state_order = ( + state_order + if order_by_param in ["state__name", "state__group"] + else state_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + state_order=Case( + *[ + When(state__group=state_group, then=Value(i)) + for i, state_group in enumerate(state_order) + ], + default=Value(len(state_order)), + output_field=CharField(), + ) + ).order_by("state_order") + # assignee and label ordering + elif order_by_param in [ + "labels__name", + "-labels__name", + "assignees__first_name", + "-assignees__first_name", + ]: + issue_queryset = issue_queryset.annotate( + max_values=Max( + order_by_param[1::] + if order_by_param.startswith("-") + else order_by_param + ) + ).order_by( + "-max_values" if order_by_param.startswith("-") else "max_values" + ) + else: + issue_queryset = issue_queryset.order_by(order_by_param) + + + state_group_order = [ + "backlog", + "unstarted", + "started", + "completed", + "cancelled", + ] + + states = ( + State.objects.filter( + ~Q(name="Triage"), + workspace__slug=slug, + project_id=project_id, + ) + .annotate( + custom_order=Case( + *[ + When(group=value, then=Value(index)) + for index, value in enumerate(state_group_order) + ], + default=Value(len(state_group_order)), + output_field=IntegerField(), + ), + ) + .values("name", "group", "color", "id") + .order_by("custom_order", "sequence") + ) + + labels = Label.objects.filter( + workspace__slug=slug, project_id=project_id + ).values("id", "name", "color", "parent") + + issues = IssuePublicSerializer(issue_queryset, many=True).data + issue_dict = {str(issue["id"]): issue for issue in issues} + state_dict = {str(state["id"]): state for state in states} + label_dict = {str(label["id"]): label for label in labels} + + return Response( + { + "issues": issue_dict, + "states": state_dict, + "labels": label_dict, + }, + status=status.HTTP_200_OK, ) \ No newline at end of file