[WEB-402] chore: project issues count (#3911)

* chore: added project issues count

* chore: project module and cycle issue count

* chore: resolved merge conflicts

* chore: added import statement

* chore: issue count type added

* chore: issue count added in project, cycle and module issues

* fix: lint fixes

* chore: tooltip added in issue count badge

* chore: tooltip added in issue count badge

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
This commit is contained in:
Anmol Singh Bhatia 2024-03-11 21:05:00 +05:30 committed by GitHub
parent b57c389c75
commit 01702e9f66
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 256 additions and 51 deletions

View File

@ -215,9 +215,10 @@ class ModuleSerializer(DynamicBaseSerializer):
class ModuleDetailSerializer(ModuleSerializer): class ModuleDetailSerializer(ModuleSerializer):
link_module = ModuleLinkSerializer(read_only=True, many=True) link_module = ModuleLinkSerializer(read_only=True, many=True)
sub_issues = serializers.IntegerField(read_only=True)
class Meta(ModuleSerializer.Meta): class Meta(ModuleSerializer.Meta):
fields = ModuleSerializer.Meta.fields + ["link_module"] fields = ModuleSerializer.Meta.fields + ["link_module", "sub_issues"]
class ModuleFavoriteSerializer(BaseSerializer): class ModuleFavoriteSerializer(BaseSerializer):

View File

@ -102,6 +102,12 @@ class ProjectLiteSerializer(BaseSerializer):
class ProjectListSerializer(DynamicBaseSerializer): 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) is_favorite = serializers.BooleanField(read_only=True)
total_members = serializers.IntegerField(read_only=True) total_members = serializers.IntegerField(read_only=True)
total_cycles = serializers.IntegerField(read_only=True) total_cycles = serializers.IntegerField(read_only=True)

View File

@ -106,15 +106,6 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
) )
) )
.annotate(is_favorite=Exists(favorite_subquery)) .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( .annotate(
completed_issues=Count( completed_issues=Count(
"issue_cycle__issue__state__group", "issue_cycle__issue__state__group",
@ -232,7 +223,6 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
"progress_snapshot", "progress_snapshot",
# meta fields # meta fields
"is_favorite", "is_favorite",
"total_issues",
"cancelled_issues", "cancelled_issues",
"completed_issues", "completed_issues",
"started_issues", "started_issues",
@ -327,13 +317,13 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
} }
if data[0]["start_date"] and data[0]["end_date"]: if data[0]["start_date"] and data[0]["end_date"]:
data[0]["distribution"][ data[0]["distribution"]["completion_chart"] = (
"completion_chart" burndown_plot(
] = burndown_plot( queryset=queryset.first(),
queryset=queryset.first(), slug=slug,
slug=slug, project_id=project_id,
project_id=project_id, cycle_id=data[0]["id"],
cycle_id=data[0]["id"], )
) )
return Response(data, status=status.HTTP_200_OK) return Response(data, status=status.HTTP_200_OK)
@ -356,7 +346,6 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
"progress_snapshot", "progress_snapshot",
# meta fields # meta fields
"is_favorite", "is_favorite",
"total_issues",
"cancelled_issues", "cancelled_issues",
"completed_issues", "completed_issues",
"started_issues", "started_issues",
@ -402,7 +391,6 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
"progress_snapshot", "progress_snapshot",
# meta fields # meta fields
"is_favorite", "is_favorite",
"total_issues",
"cancelled_issues", "cancelled_issues",
"completed_issues", "completed_issues",
"started_issues", "started_issues",
@ -474,7 +462,6 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
"progress_snapshot", "progress_snapshot",
# meta fields # meta fields
"is_favorite", "is_favorite",
"total_issues",
"cancelled_issues", "cancelled_issues",
"completed_issues", "completed_issues",
"started_issues", "started_issues",
@ -487,10 +474,42 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def retrieve(self, request, slug, project_id, pk): 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 = ( data = (
self.get_queryset() self.get_queryset()
.filter(pk=pk) .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( .values(
# necessary fields # necessary fields
"id", "id",
@ -507,6 +526,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
"external_source", "external_source",
"external_id", "external_id",
"progress_snapshot", "progress_snapshot",
"sub_issues",
# meta fields # meta fields
"is_favorite", "is_favorite",
"total_issues", "total_issues",

View File

@ -3,7 +3,7 @@ import json
# Django Imports # Django Imports
from django.utils import timezone 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.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.fields import ArrayField
from django.db.models import Value, UUIDField from django.db.models import Value, UUIDField
@ -79,15 +79,6 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
), ),
) )
) )
.annotate(
total_issues=Count(
"issue_module",
filter=Q(
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
),
)
.annotate( .annotate(
completed_issues=Count( completed_issues=Count(
"issue_module__issue__state__group", "issue_module__issue__state__group",
@ -183,7 +174,6 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
"external_id", "external_id",
# computed fields # computed fields
"is_favorite", "is_favorite",
"total_issues",
"cancelled_issues", "cancelled_issues",
"completed_issues", "completed_issues",
"started_issues", "started_issues",
@ -225,7 +215,6 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
"external_id", "external_id",
# computed fields # computed fields
"is_favorite", "is_favorite",
"total_issues",
"cancelled_issues", "cancelled_issues",
"completed_issues", "completed_issues",
"started_issues", "started_issues",
@ -237,7 +226,30 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
return Response(modules, status=status.HTTP_200_OK) return Response(modules, status=status.HTTP_200_OK)
def retrieve(self, request, slug, project_id, pk): 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 = ( assignee_distribution = (
Issue.objects.filter( Issue.objects.filter(
@ -380,7 +392,6 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
"external_id", "external_id",
# computed fields # computed fields
"is_favorite", "is_favorite",
"total_issues",
"cancelled_issues", "cancelled_issues",
"completed_issues", "completed_issues",
"started_issues", "started_issues",

View File

@ -46,9 +46,11 @@ from plane.db.models import (
Inbox, Inbox,
ProjectDeployBoard, ProjectDeployBoard,
IssueProperty, IssueProperty,
Issue,
) )
from plane.utils.cache import cache_response from plane.utils.cache import cache_response
class ProjectViewSet(WebhookMixin, BaseViewSet): class ProjectViewSet(WebhookMixin, BaseViewSet):
serializer_class = ProjectListSerializer serializer_class = ProjectListSerializer
model = Project model = Project
@ -171,6 +173,73 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
).data ).data
return Response(projects, status=status.HTTP_200_OK) 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): def create(self, request, slug):
try: try:
workspace = Workspace.objects.get(slug=slug) workspace = Workspace.objects.get(slug=slug)
@ -471,6 +540,7 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
permission_classes = [ permission_classes = [
AllowAny, AllowAny,
] ]
# Cache the below api for 24 hours # Cache the below api for 24 hours
@cache_response(60 * 60 * 24, user=False) @cache_response(60 * 60 * 24, user=False)
def get(self, request): def get(self, request):

View File

@ -26,6 +26,7 @@ export interface ICycle {
sort_order: number; sort_order: number;
start_date: string | null; start_date: string | null;
started_issues: number; started_issues: number;
sub_issues: number;
total_issues: number; total_issues: number;
unstarted_issues: number; unstarted_issues: number;
updated_at: Date; updated_at: Date;

View File

@ -30,6 +30,7 @@ export interface IModule {
name: string; name: string;
project_id: string; project_id: string;
sort_order: number; sort_order: number;
sub_issues: number;
start_date: string | null; start_date: string | null;
started_issues: number; started_issues: number;
status: TModuleStatus; status: TModuleStatus;

View File

@ -23,6 +23,8 @@ export type TProjectLogoProps = {
export interface IProject { export interface IProject {
archive_in: number; archive_in: number;
archived_issues: number;
archived_sub_issues: number;
close_in: number; close_in: number;
created_at: Date; created_at: Date;
created_by: string; created_by: string;
@ -35,6 +37,8 @@ export interface IProject {
default_assignee: IUser | string | null; default_assignee: IUser | string | null;
default_state: string | null; default_state: string | null;
description: string; description: string;
draft_issues: number;
draft_sub_issues: number;
estimate: string | null; estimate: string | null;
id: string; id: string;
identifier: string; identifier: string;
@ -48,7 +52,9 @@ export interface IProject {
network: number; network: number;
project_lead: IUserLite | string | null; project_lead: IUserLite | string | null;
sort_order: number | null; sort_order: number | null;
sub_issues: number;
total_cycles: number; total_cycles: number;
total_issues: number;
total_members: number; total_members: number;
total_modules: number; total_modules: number;
updated_at: Date; updated_at: Date;

View File

@ -4,7 +4,7 @@ import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// hooks // hooks
import { ArrowRight, Plus, PanelRight } from "lucide-react"; 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 { ProjectAnalyticsModal } from "components/analytics";
import { BreadcrumbLink } from "components/common"; import { BreadcrumbLink } from "components/common";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
@ -142,6 +142,12 @@ export const CycleIssuesHeader: React.FC = observer(() => {
const canUserCreateIssue = const canUserCreateIssue =
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); 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 ( return (
<> <>
<ProjectAnalyticsModal <ProjectAnalyticsModal
@ -197,15 +203,29 @@ export const CycleIssuesHeader: React.FC = observer(() => {
label={ label={
<> <>
<ContrastIcon className="h-3 w-3" /> <ContrastIcon className="h-3 w-3" />
<div className=" w-auto max-w-[70px] sm:max-w-[200px] inline-block truncate line-clamp-1 overflow-hidden whitespace-nowrap"> <div className="flex items-center gap-2 w-auto max-w-[70px] sm:max-w-[200px] truncate">
{cycleDetails?.name && cycleDetails.name} <p className="truncate">{cycleDetails?.name && cycleDetails.name}</p>
{issueCount && issueCount > 0 ? (
<Tooltip
tooltipContent={`There are ${issueCount} ${
issueCount > 1 ? "issues" : "issue"
} in this cycle`}
position="bottom"
>
<span className="cursor-default flex items-center text-center justify-center px-2 flex-shrink-0 bg-custom-primary-100/20 text-custom-primary-100 text-xs font-semibold rounded-xl">
{issueCount}
</span>
</Tooltip>
) : null}
</div> </div>
</> </>
} }
className="ml-1.5 flex-shrink-0" className="ml-1.5 flex-shrink-0 truncate"
placement="bottom-start" placement="bottom-start"
> >
{currentProjectCycleIds?.map((cycleId) => <CycleDropdownOption key={cycleId} cycleId={cycleId} />)} {currentProjectCycleIds?.map((cycleId) => (
<CycleDropdownOption key={cycleId} cycleId={cycleId} />
))}
</CustomMenu> </CustomMenu>
} }
/> />

View File

@ -4,7 +4,7 @@ import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// hooks // hooks
import { ArrowRight, PanelRight, Plus } from "lucide-react"; 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 { ProjectAnalyticsModal } from "components/analytics";
import { BreadcrumbLink } from "components/common"; import { BreadcrumbLink } from "components/common";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
@ -143,6 +143,12 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
const canUserCreateIssue = const canUserCreateIssue =
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); 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 ( return (
<> <>
<ProjectAnalyticsModal <ProjectAnalyticsModal
@ -198,15 +204,29 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
label={ label={
<> <>
<DiceIcon className="h-3 w-3" /> <DiceIcon className="h-3 w-3" />
<div className="w-auto max-w-[70px] sm:max-w-[200px] inline-block truncate line-clamp-1 overflow-hidden whitespace-nowrap"> <div className="flex items-center gap-2 w-auto max-w-[70px] sm:max-w-[200px] truncate">
{moduleDetails?.name && moduleDetails.name} <p className="truncate">{moduleDetails?.name && moduleDetails.name}</p>
{issueCount && issueCount > 0 ? (
<Tooltip
tooltipContent={`There are ${issueCount} ${
issueCount > 1 ? "issues" : "issue"
} in this module`}
position="bottom"
>
<span className="cursor-default flex items-center text-center justify-center px-2 flex-shrink-0 bg-custom-primary-100/20 text-custom-primary-100 text-xs font-semibold rounded-xl">
{issueCount}
</span>
</Tooltip>
) : null}
</div> </div>
</> </>
} }
className="ml-1.5 flex-shrink-0" className="ml-1.5 flex-shrink-0"
placement="bottom-start" placement="bottom-start"
> >
{projectModuleIds?.map((moduleId) => <ModuleDropdownOption key={moduleId} moduleId={moduleId} />)} {projectModuleIds?.map((moduleId) => (
<ModuleDropdownOption key={moduleId} moduleId={moduleId} />
))}
</CustomMenu> </CustomMenu>
} }
/> />

View File

@ -5,7 +5,7 @@ import { ArrowLeft } from "lucide-react";
// hooks // hooks
// constants // constants
// ui // ui
import { Breadcrumbs, LayersIcon } from "@plane/ui"; import { Breadcrumbs, LayersIcon, Tooltip } from "@plane/ui";
// components // components
import { BreadcrumbLink } from "components/common"; import { BreadcrumbLink } from "components/common";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; 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); 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 ( return (
<div className="relative z-10 flex h-14 w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4"> <div className="relative z-10 flex h-14 w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap"> <div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
@ -82,7 +88,7 @@ export const ProjectArchivedIssuesHeader: FC = observer(() => {
<ArrowLeft fontSize={14} strokeWidth={2} /> <ArrowLeft fontSize={14} strokeWidth={2} />
</button> </button>
</div> </div>
<div> <div className="flex items-center gap-2.5">
<Breadcrumbs> <Breadcrumbs>
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.BreadcrumbItem
type="text" type="text"
@ -111,6 +117,16 @@ export const ProjectArchivedIssuesHeader: FC = observer(() => {
} }
/> />
</Breadcrumbs> </Breadcrumbs>
{issueCount && issueCount > 0 ? (
<Tooltip
tooltipContent={`There are ${issueCount} ${issueCount > 1 ? "issues" : "issue"} in project's archived`}
position="bottom"
>
<span className="cursor-default flex items-center text-center justify-center px-2.5 py-0.5 flex-shrink-0 bg-custom-primary-100/20 text-custom-primary-100 text-xs font-semibold rounded-xl">
{issueCount}
</span>
</Tooltip>
) : null}
</div> </div>
</div> </div>

View File

@ -3,7 +3,7 @@ import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// hooks // hooks
// components // components
import { Breadcrumbs, LayersIcon } from "@plane/ui"; import { Breadcrumbs, LayersIcon, Tooltip } from "@plane/ui";
import { BreadcrumbLink } from "components/common"; import { BreadcrumbLink } from "components/common";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
@ -73,11 +73,18 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
}, },
[workspaceSlug, projectId, updateFilters] [workspaceSlug, projectId, updateFilters]
); );
const issueCount = currentProjectDetails
? issueFilters?.displayFilters?.sub_issue
? currentProjectDetails.draft_issues + currentProjectDetails.draft_sub_issues
: currentProjectDetails.draft_issues
: undefined;
return ( return (
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4"> <div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap"> <div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<SidebarHamburgerToggle /> <SidebarHamburgerToggle />
<div> <div className="flex items-center gap-2.5">
<Breadcrumbs> <Breadcrumbs>
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.BreadcrumbItem
type="text" type="text"
@ -103,6 +110,16 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
} }
/> />
</Breadcrumbs> </Breadcrumbs>
{issueCount && issueCount > 0 ? (
<Tooltip
tooltipContent={`There are ${issueCount} ${issueCount > 1 ? "issues" : "issue"} in project's draft`}
position="bottom"
>
<span className="cursor-default flex items-center text-center justify-center px-2.5 py-0.5 flex-shrink-0 bg-custom-primary-100/20 text-custom-primary-100 text-xs font-semibold rounded-xl">
{issueCount}
</span>
</Tooltip>
) : null}
</div> </div>
<div className="ml-auto flex items-center gap-2"> <div className="ml-auto flex items-center gap-2">

View File

@ -3,7 +3,7 @@ import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { Briefcase, Circle, ExternalLink, Plus } from "lucide-react"; import { Briefcase, Circle, ExternalLink, Plus } from "lucide-react";
// hooks // hooks
import { Breadcrumbs, Button, LayersIcon } from "@plane/ui"; import { Breadcrumbs, Button, LayersIcon, Tooltip } from "@plane/ui";
import { ProjectAnalyticsModal } from "components/analytics"; import { ProjectAnalyticsModal } from "components/analytics";
import { BreadcrumbLink } from "components/common"; import { BreadcrumbLink } from "components/common";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
@ -102,6 +102,12 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
const canUserCreateIssue = const canUserCreateIssue =
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); 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 ( return (
<> <>
<ProjectAnalyticsModal <ProjectAnalyticsModal
@ -113,7 +119,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
<div className="flex items-center gap-2 p-4 border-b border-custom-border-200 bg-custom-sidebar-background-100"> <div className="flex items-center gap-2 p-4 border-b border-custom-border-200 bg-custom-sidebar-background-100">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap"> <div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<SidebarHamburgerToggle /> <SidebarHamburgerToggle />
<div> <div className="flex items-center gap-2.5">
<Breadcrumbs onBack={() => router.back()}> <Breadcrumbs onBack={() => router.back()}>
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.BreadcrumbItem
type="text" type="text"
@ -145,6 +151,16 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
} }
/> />
</Breadcrumbs> </Breadcrumbs>
{issueCount && issueCount > 0 ? (
<Tooltip
tooltipContent={`There are ${issueCount} ${issueCount > 1 ? "issues" : "issue"} in this project`}
position="bottom"
>
<span className="cursor-default flex items-center text-center justify-center px-2.5 py-0.5 flex-shrink-0 bg-custom-primary-100/20 text-custom-primary-100 text-xs font-semibold rounded-xl">
{issueCount}
</span>
</Tooltip>
) : null}
</div> </div>
{currentProjectDetails?.is_deployed && deployUrl && ( {currentProjectDetails?.is_deployed && deployUrl && (
<a <a