forked from github/plane
[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:
parent
b57c389c75
commit
01702e9f66
@ -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):
|
||||||
|
@ -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)
|
||||||
|
@ -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,14 +317,14 @@ 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",
|
||||||
|
@ -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",
|
||||||
|
@ -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):
|
||||||
|
1
packages/types/src/cycle/cycle.d.ts
vendored
1
packages/types/src/cycle/cycle.d.ts
vendored
@ -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;
|
||||||
|
1
packages/types/src/modules.d.ts
vendored
1
packages/types/src/modules.d.ts
vendored
@ -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;
|
||||||
|
6
packages/types/src/projects.d.ts
vendored
6
packages/types/src/projects.d.ts
vendored
@ -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;
|
||||||
|
@ -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>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -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>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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">
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user