diff --git a/apiserver/plane/app/serializers/module.py b/apiserver/plane/app/serializers/module.py index 3b2468aee54..100b6314a84 100644 --- a/apiserver/plane/app/serializers/module.py +++ b/apiserver/plane/app/serializers/module.py @@ -215,9 +215,10 @@ class Meta: class ModuleDetailSerializer(ModuleSerializer): link_module = ModuleLinkSerializer(read_only=True, many=True) + sub_issues = serializers.IntegerField(read_only=True) class Meta(ModuleSerializer.Meta): - fields = ModuleSerializer.Meta.fields + ["link_module"] + fields = ModuleSerializer.Meta.fields + ["link_module", "sub_issues"] class ModuleFavoriteSerializer(BaseSerializer): diff --git a/apiserver/plane/app/serializers/project.py b/apiserver/plane/app/serializers/project.py index 6840fa8f79a..a0c2318e381 100644 --- a/apiserver/plane/app/serializers/project.py +++ b/apiserver/plane/app/serializers/project.py @@ -102,6 +102,12 @@ class Meta: class ProjectListSerializer(DynamicBaseSerializer): + total_issues = serializers.IntegerField(read_only=True) + archived_issues = serializers.IntegerField(read_only=True) + archived_sub_issues = serializers.IntegerField(read_only=True) + draft_issues = serializers.IntegerField(read_only=True) + draft_sub_issues = serializers.IntegerField(read_only=True) + sub_issues = serializers.IntegerField(read_only=True) is_favorite = serializers.BooleanField(read_only=True) total_members = serializers.IntegerField(read_only=True) total_cycles = serializers.IntegerField(read_only=True) diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py index 9dc25474fff..42904a8fce1 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -106,15 +106,6 @@ def get_queryset(self): ) ) .annotate(is_favorite=Exists(favorite_subquery)) - .annotate( - total_issues=Count( - "issue_cycle", - filter=Q( - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) .annotate( completed_issues=Count( "issue_cycle__issue__state__group", @@ -232,7 +223,6 @@ def list(self, request, slug, project_id): "progress_snapshot", # meta fields "is_favorite", - "total_issues", "cancelled_issues", "completed_issues", "started_issues", @@ -327,13 +317,13 @@ def list(self, request, slug, project_id): } if data[0]["start_date"] and data[0]["end_date"]: - data[0]["distribution"][ - "completion_chart" - ] = burndown_plot( - queryset=queryset.first(), - slug=slug, - project_id=project_id, - cycle_id=data[0]["id"], + data[0]["distribution"]["completion_chart"] = ( + burndown_plot( + queryset=queryset.first(), + slug=slug, + project_id=project_id, + cycle_id=data[0]["id"], + ) ) return Response(data, status=status.HTTP_200_OK) @@ -356,7 +346,6 @@ def list(self, request, slug, project_id): "progress_snapshot", # meta fields "is_favorite", - "total_issues", "cancelled_issues", "completed_issues", "started_issues", @@ -402,7 +391,6 @@ def create(self, request, slug, project_id): "progress_snapshot", # meta fields "is_favorite", - "total_issues", "cancelled_issues", "completed_issues", "started_issues", @@ -474,7 +462,6 @@ def partial_update(self, request, slug, project_id, pk): "progress_snapshot", # meta fields "is_favorite", - "total_issues", "cancelled_issues", "completed_issues", "started_issues", @@ -487,10 +474,42 @@ def partial_update(self, request, slug, project_id, pk): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def retrieve(self, request, slug, project_id, pk): - queryset = self.get_queryset().filter(pk=pk) + queryset = ( + self.get_queryset() + .filter(pk=pk) + .annotate( + total_issues=Count( + "issue_cycle", + filter=Q( + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + ) data = ( self.get_queryset() .filter(pk=pk) + .annotate( + total_issues=Issue.issue_objects.filter( + project_id=self.kwargs.get("project_id"), + parent__isnull=True, + issue_cycle__cycle_id=pk, + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .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", @@ -507,6 +526,7 @@ def retrieve(self, request, slug, project_id, pk): "external_source", "external_id", "progress_snapshot", + "sub_issues", # meta fields "is_favorite", "total_issues", diff --git a/apiserver/plane/app/views/module/base.py b/apiserver/plane/app/views/module/base.py index cd87442d2f6..ee9718b59c6 100644 --- a/apiserver/plane/app/views/module/base.py +++ b/apiserver/plane/app/views/module/base.py @@ -3,7 +3,7 @@ # Django Imports from django.utils import timezone -from django.db.models import Prefetch, F, OuterRef, Exists, Count, Q +from django.db.models import Prefetch, F, OuterRef, Exists, Count, Q, Func from django.contrib.postgres.aggregates import ArrayAgg from django.contrib.postgres.fields import ArrayField from django.db.models import Value, UUIDField @@ -79,15 +79,6 @@ def get_queryset(self): ), ) ) - .annotate( - total_issues=Count( - "issue_module", - filter=Q( - issue_module__issue__archived_at__isnull=True, - issue_module__issue__is_draft=False, - ), - ), - ) .annotate( completed_issues=Count( "issue_module__issue__state__group", @@ -183,7 +174,6 @@ def create(self, request, slug, project_id): "external_id", # computed fields "is_favorite", - "total_issues", "cancelled_issues", "completed_issues", "started_issues", @@ -225,7 +215,6 @@ def list(self, request, slug, project_id): "external_id", # computed fields "is_favorite", - "total_issues", "cancelled_issues", "completed_issues", "started_issues", @@ -237,7 +226,30 @@ def list(self, request, slug, project_id): return Response(modules, status=status.HTTP_200_OK) def retrieve(self, request, slug, project_id, pk): - queryset = self.get_queryset().filter(pk=pk) + queryset = ( + self.get_queryset() + .filter(pk=pk) + .annotate( + total_issues=Issue.issue_objects.filter( + project_id=self.kwargs.get("project_id"), + parent__isnull=True, + issue_module__module_id=pk, + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .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( @@ -380,7 +392,6 @@ def partial_update(self, request, slug, project_id, pk): "external_id", # computed fields "is_favorite", - "total_issues", "cancelled_issues", "completed_issues", "started_issues", diff --git a/apiserver/plane/app/views/project/base.py b/apiserver/plane/app/views/project/base.py index 6deeea1447c..74d4e3466e9 100644 --- a/apiserver/plane/app/views/project/base.py +++ b/apiserver/plane/app/views/project/base.py @@ -46,9 +46,11 @@ Inbox, ProjectDeployBoard, IssueProperty, + Issue, ) from plane.utils.cache import cache_response + class ProjectViewSet(WebhookMixin, BaseViewSet): serializer_class = ProjectListSerializer model = Project @@ -171,6 +173,73 @@ def list(self, request, slug): ).data return Response(projects, status=status.HTTP_200_OK) + def retrieve(self, request, slug, pk): + project = ( + self.get_queryset() + .filter(pk=pk) + .annotate( + total_issues=Issue.issue_objects.filter( + project_id=self.kwargs.get("pk"), + parent__isnull=True, + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + sub_issues=Issue.issue_objects.filter( + project_id=self.kwargs.get("pk"), + parent__isnull=False, + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + archived_issues=Issue.objects.filter( + project_id=self.kwargs.get("pk"), + archived_at__isnull=False, + parent__isnull=True, + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + archived_sub_issues=Issue.objects.filter( + project_id=self.kwargs.get("pk"), + archived_at__isnull=False, + parent__isnull=False, + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + draft_issues=Issue.objects.filter( + project_id=self.kwargs.get("pk"), + is_draft=True, + parent__isnull=True, + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + draft_sub_issues=Issue.objects.filter( + project_id=self.kwargs.get("pk"), + is_draft=True, + parent__isnull=False, + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + ).first() + + serializer = ProjectListSerializer(project) + return Response(serializer.data, status=status.HTTP_200_OK) + def create(self, request, slug): try: workspace = Workspace.objects.get(slug=slug) @@ -471,6 +540,7 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView): permission_classes = [ AllowAny, ] + # Cache the below api for 24 hours @cache_response(60 * 60 * 24, user=False) def get(self, request): diff --git a/packages/types/src/cycle/cycle.d.ts b/packages/types/src/cycle/cycle.d.ts index 6d21e05b882..c41ab279b9b 100644 --- a/packages/types/src/cycle/cycle.d.ts +++ b/packages/types/src/cycle/cycle.d.ts @@ -26,6 +26,7 @@ export interface ICycle { sort_order: number; start_date: string | null; started_issues: number; + sub_issues: number; total_issues: number; unstarted_issues: number; updated_at: Date; diff --git a/packages/types/src/modules.d.ts b/packages/types/src/modules.d.ts index c532a467c7e..0af293e5070 100644 --- a/packages/types/src/modules.d.ts +++ b/packages/types/src/modules.d.ts @@ -30,6 +30,7 @@ export interface IModule { name: string; project_id: string; sort_order: number; + sub_issues: number; start_date: string | null; started_issues: number; status: TModuleStatus; diff --git a/packages/types/src/projects.d.ts b/packages/types/src/projects.d.ts index a6da364b9b7..afae5199f07 100644 --- a/packages/types/src/projects.d.ts +++ b/packages/types/src/projects.d.ts @@ -23,6 +23,8 @@ export type TProjectLogoProps = { export interface IProject { archive_in: number; + archived_issues: number; + archived_sub_issues: number; close_in: number; created_at: Date; created_by: string; @@ -35,6 +37,8 @@ export interface IProject { default_assignee: IUser | string | null; default_state: string | null; description: string; + draft_issues: number; + draft_sub_issues: number; estimate: string | null; id: string; identifier: string; @@ -48,7 +52,9 @@ export interface IProject { network: number; project_lead: IUserLite | string | null; sort_order: number | null; + sub_issues: number; total_cycles: number; + total_issues: number; total_members: number; total_modules: number; updated_at: Date; diff --git a/web/components/headers/cycle-issues.tsx b/web/components/headers/cycle-issues.tsx index 5ef1ebf2c05..6f9c615453d 100644 --- a/web/components/headers/cycle-issues.tsx +++ b/web/components/headers/cycle-issues.tsx @@ -4,7 +4,7 @@ import Link from "next/link"; import { useRouter } from "next/router"; // hooks import { ArrowRight, Plus, PanelRight } from "lucide-react"; -import { Breadcrumbs, Button, ContrastIcon, CustomMenu } from "@plane/ui"; +import { Breadcrumbs, Button, ContrastIcon, CustomMenu, Tooltip } from "@plane/ui"; import { ProjectAnalyticsModal } from "components/analytics"; import { BreadcrumbLink } from "components/common"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; @@ -142,6 +142,12 @@ export const CycleIssuesHeader: React.FC = observer(() => { const canUserCreateIssue = currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); + const issueCount = cycleDetails + ? issueFilters?.displayFilters?.sub_issue + ? cycleDetails.total_issues + cycleDetails?.sub_issues + : cycleDetails.total_issues + : undefined; + return ( <> { label={ <> -
- {cycleDetails?.name && cycleDetails.name} +
+

{cycleDetails?.name && cycleDetails.name}

+ {issueCount && issueCount > 0 ? ( + 1 ? "issues" : "issue" + } in this cycle`} + position="bottom" + > + + {issueCount} + + + ) : null}
} - className="ml-1.5 flex-shrink-0" + className="ml-1.5 flex-shrink-0 truncate" placement="bottom-start" > - {currentProjectCycleIds?.map((cycleId) => )} + {currentProjectCycleIds?.map((cycleId) => ( + + ))} } /> diff --git a/web/components/headers/module-issues.tsx b/web/components/headers/module-issues.tsx index 10717ecc39c..9505d714590 100644 --- a/web/components/headers/module-issues.tsx +++ b/web/components/headers/module-issues.tsx @@ -4,7 +4,7 @@ import Link from "next/link"; import { useRouter } from "next/router"; // hooks import { ArrowRight, PanelRight, Plus } from "lucide-react"; -import { Breadcrumbs, Button, CustomMenu, DiceIcon } from "@plane/ui"; +import { Breadcrumbs, Button, CustomMenu, DiceIcon, Tooltip } from "@plane/ui"; import { ProjectAnalyticsModal } from "components/analytics"; import { BreadcrumbLink } from "components/common"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; @@ -143,6 +143,12 @@ export const ModuleIssuesHeader: React.FC = observer(() => { const canUserCreateIssue = currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); + const issueCount = moduleDetails + ? issueFilters?.displayFilters?.sub_issue + ? moduleDetails.total_issues + moduleDetails.sub_issues + : moduleDetails.total_issues + : undefined; + return ( <> { label={ <> -
- {moduleDetails?.name && moduleDetails.name} +
+

{moduleDetails?.name && moduleDetails.name}

+ {issueCount && issueCount > 0 ? ( + 1 ? "issues" : "issue" + } in this module`} + position="bottom" + > + + {issueCount} + + + ) : null}
} className="ml-1.5 flex-shrink-0" placement="bottom-start" > - {projectModuleIds?.map((moduleId) => )} + {projectModuleIds?.map((moduleId) => ( + + ))} } /> diff --git a/web/components/headers/project-archived-issues.tsx b/web/components/headers/project-archived-issues.tsx index db208aa21cc..ce226b58eaf 100644 --- a/web/components/headers/project-archived-issues.tsx +++ b/web/components/headers/project-archived-issues.tsx @@ -5,7 +5,7 @@ import { ArrowLeft } from "lucide-react"; // hooks // constants // ui -import { Breadcrumbs, LayersIcon } from "@plane/ui"; +import { Breadcrumbs, LayersIcon, Tooltip } from "@plane/ui"; // components import { BreadcrumbLink } from "components/common"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; @@ -69,6 +69,12 @@ export const ProjectArchivedIssuesHeader: FC = observer(() => { updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.DISPLAY_PROPERTIES, property); }; + const issueCount = currentProjectDetails + ? issueFilters?.displayFilters?.sub_issue + ? currentProjectDetails.archived_issues + currentProjectDetails.archived_sub_issues + : currentProjectDetails.archived_issues + : undefined; + return (
@@ -82,7 +88,7 @@ export const ProjectArchivedIssuesHeader: FC = observer(() => {
-
+
{ } /> + {issueCount && issueCount > 0 ? ( + 1 ? "issues" : "issue"} in project's archived`} + position="bottom" + > + + {issueCount} + + + ) : null}
diff --git a/web/components/headers/project-draft-issues.tsx b/web/components/headers/project-draft-issues.tsx index 4f292962116..789c3f60fb9 100644 --- a/web/components/headers/project-draft-issues.tsx +++ b/web/components/headers/project-draft-issues.tsx @@ -3,7 +3,7 @@ import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hooks // components -import { Breadcrumbs, LayersIcon } from "@plane/ui"; +import { Breadcrumbs, LayersIcon, Tooltip } from "@plane/ui"; import { BreadcrumbLink } from "components/common"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; @@ -73,11 +73,18 @@ export const ProjectDraftIssueHeader: FC = observer(() => { }, [workspaceSlug, projectId, updateFilters] ); + + const issueCount = currentProjectDetails + ? issueFilters?.displayFilters?.sub_issue + ? currentProjectDetails.draft_issues + currentProjectDetails.draft_sub_issues + : currentProjectDetails.draft_issues + : undefined; + return (
-
+
{ } /> + {issueCount && issueCount > 0 ? ( + 1 ? "issues" : "issue"} in project's draft`} + position="bottom" + > + + {issueCount} + + + ) : null}
diff --git a/web/components/headers/project-issues.tsx b/web/components/headers/project-issues.tsx index 19eaf4f4fb4..9739e78321e 100644 --- a/web/components/headers/project-issues.tsx +++ b/web/components/headers/project-issues.tsx @@ -3,7 +3,7 @@ import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { Briefcase, Circle, ExternalLink, Plus } from "lucide-react"; // hooks -import { Breadcrumbs, Button, LayersIcon } from "@plane/ui"; +import { Breadcrumbs, Button, LayersIcon, Tooltip } from "@plane/ui"; import { ProjectAnalyticsModal } from "components/analytics"; import { BreadcrumbLink } from "components/common"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; @@ -102,6 +102,12 @@ export const ProjectIssuesHeader: React.FC = observer(() => { const canUserCreateIssue = currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); + const issueCount = currentProjectDetails + ? issueFilters?.displayFilters?.sub_issue + ? currentProjectDetails?.total_issues + currentProjectDetails?.sub_issues + : currentProjectDetails?.total_issues + : undefined; + return ( <> {
-
+
router.back()}> { } /> + {issueCount && issueCount > 0 ? ( + 1 ? "issues" : "issue"} in this project`} + position="bottom" + > + + {issueCount} + + + ) : null}
{currentProjectDetails?.is_deployed && deployUrl && (