From 3a99ecf8f3d8e159b5bb65557e5b2c7fd9b81a51 Mon Sep 17 00:00:00 2001 From: Sangeetha Date: Tue, 17 Feb 2026 00:04:03 +0530 Subject: [PATCH] [WEB-5871] chore: added intake count for projects (#8497) * chore: add intake_count in project list endpoint * chore: sidebar project navigation intake count added * fix: filter out closed intake issues in the count * chore: code refactor * chore: code refactor * fix: filter out deleted intake issues --------- Co-authored-by: Anmol Singh Bhatia --- apps/api/plane/app/views/project/base.py | 15 +++++-- .../workspace/sidebar/project-navigation.tsx | 13 ++++-- .../web/core/store/inbox/inbox-issue.store.ts | 43 ++++++++++++++++++- .../core/store/inbox/project-inbox.store.ts | 11 +++++ packages/types/src/project/projects.ts | 1 + 5 files changed, 76 insertions(+), 7 deletions(-) diff --git a/apps/api/plane/app/views/project/base.py b/apps/api/plane/app/views/project/base.py index 6b7a6f06fe..8164b4df1e 100644 --- a/apps/api/plane/app/views/project/base.py +++ b/apps/api/plane/app/views/project/base.py @@ -8,7 +8,7 @@ import json # Django imports from django.core.serializers.json import DjangoJSONEncoder -from django.db.models import Exists, F, OuterRef, Prefetch, Q, Subquery +from django.db.models import Exists, F, OuterRef, Prefetch, Q, Subquery, Count from django.utils import timezone # Third Party imports @@ -28,7 +28,6 @@ from plane.bgtasks.webhook_task import model_activity, webhook_activity from plane.db.models import ( UserFavorite, DeployBoard, - ProjectUserProperty, Intake, Project, ProjectIdentifier, @@ -36,10 +35,10 @@ from plane.db.models import ( ProjectNetwork, State, DEFAULT_STATES, - UserFavorite, Workspace, WorkspaceMember, ) +from plane.db.models.intake import IntakeIssueStatus from plane.utils.host import base_host @@ -155,6 +154,15 @@ class ProjectViewSet(BaseViewSet): is_active=True, ).values("role") ) + .annotate( + intake_count=Count( + "project_intakeissue", + filter=Q( + project_intakeissue__status=IntakeIssueStatus.PENDING.value, + project_intakeissue__deleted_at__isnull=True, + ), + ) + ) .annotate(inbox_view=F("intake_view")) .annotate(sort_order=Subquery(sort_order)) .distinct() @@ -165,6 +173,7 @@ class ProjectViewSet(BaseViewSet): "sort_order", "logo_props", "member_role", + "intake_count", "archived_at", "workspace", "cycle_view", diff --git a/apps/web/core/components/workspace/sidebar/project-navigation.tsx b/apps/web/core/components/workspace/sidebar/project-navigation.tsx index 95914de569..e63e768e4b 100644 --- a/apps/web/core/components/workspace/sidebar/project-navigation.tsx +++ b/apps/web/core/components/workspace/sidebar/project-navigation.tsx @@ -181,12 +181,19 @@ export const ProjectNavigation = observer(function ProjectNavigation(props: TPro const hasAccess = allowPermissions(item.access, EUserPermissionsLevel.PROJECT, workspaceSlug, project.id); if (!hasAccess) return null; + const shouldShowCount = item.key === "intake" && (project.intake_count ?? 0) > 0; + return ( -
- - {t(item.i18n_key)} +
+
+ + {t(item.i18n_key)} +
+ {shouldShowCount && {project.intake_count}}
diff --git a/apps/web/core/store/inbox/inbox-issue.store.ts b/apps/web/core/store/inbox/inbox-issue.store.ts index 4181ba7751..233d0a9bcf 100644 --- a/apps/web/core/store/inbox/inbox-issue.store.ts +++ b/apps/web/core/store/inbox/inbox-issue.store.ts @@ -100,6 +100,7 @@ export class InboxIssueStore implements IInboxIssueStore { const previousData: Partial = { status: this.status, }; + const previousStatus = this.status; try { if (!this.issue.id) return; @@ -107,7 +108,24 @@ export class InboxIssueStore implements IInboxIssueStore { const inboxIssue = await this.inboxIssueService.update(this.workspaceSlug, this.projectId, this.issue.id, { status: status, }); - runInAction(() => set(this, "status", inboxIssue?.status)); + runInAction(() => { + set(this, "status", inboxIssue?.status); + + // Handle intake_count transitions + if (previousStatus === EInboxIssueStatus.PENDING && inboxIssue.status !== EInboxIssueStatus.PENDING) { + // Changed from PENDING to something else: decrement + const currentCount = this.store.projectRoot.project.projectMap[this.projectId]?.intake_count ?? 0; + set( + this.store.projectRoot.project.projectMap, + [this.projectId, "intake_count"], + Math.max(0, currentCount - 1) + ); + } else if (previousStatus !== EInboxIssueStatus.PENDING && inboxIssue.status === EInboxIssueStatus.PENDING) { + // Changed from something else to PENDING: increment + const currentCount = this.store.projectRoot.project.projectMap[this.projectId]?.intake_count ?? 0; + set(this.store.projectRoot.project.projectMap, [this.projectId, "intake_count"], currentCount + 1); + } + }); // If issue accepted sync issue to local db if (status === EInboxIssueStatus.ACCEPTED) { @@ -126,6 +144,7 @@ export class InboxIssueStore implements IInboxIssueStore { duplicate_to: this.duplicate_to, duplicate_issue_detail: this.duplicate_issue_detail, }; + const wasPending = this.status === EInboxIssueStatus.PENDING; try { if (!this.issue.id) return; const inboxIssue = await this.inboxIssueService.update(this.workspaceSlug, this.projectId, this.issue.id, { @@ -136,6 +155,15 @@ export class InboxIssueStore implements IInboxIssueStore { set(this, "status", inboxIssue?.status); set(this, "duplicate_to", inboxIssue?.duplicate_to); set(this, "duplicate_issue_detail", inboxIssue?.duplicate_issue_detail); + // Decrement intake_count if the issue was PENDING + if (wasPending) { + const currentCount = this.store.projectRoot.project.projectMap[this.projectId]?.intake_count ?? 0; + set( + this.store.projectRoot.project.projectMap, + [this.projectId, "intake_count"], + Math.max(0, currentCount - 1) + ); + } }); } catch { runInAction(() => { @@ -152,6 +180,7 @@ export class InboxIssueStore implements IInboxIssueStore { status: this.status, snoozed_till: this.snoozed_till, }; + const previousStatus = this.status; try { if (!this.issue.id) return; const inboxIssue = await this.inboxIssueService.update(this.workspaceSlug, this.projectId, this.issue.id, { @@ -161,6 +190,18 @@ export class InboxIssueStore implements IInboxIssueStore { runInAction(() => { set(this, "status", inboxIssue?.status); set(this, "snoozed_till", inboxIssue?.snoozed_till); + // Handle intake_count transitions + if (previousStatus === EInboxIssueStatus.PENDING && inboxIssue.status === EInboxIssueStatus.SNOOZED) { + const currentCount = this.store.projectRoot.project.projectMap[this.projectId]?.intake_count ?? 0; + set( + this.store.projectRoot.project.projectMap, + [this.projectId, "intake_count"], + Math.max(0, currentCount - 1) + ); + } else if (previousStatus !== EInboxIssueStatus.PENDING && inboxIssue.status === EInboxIssueStatus.PENDING) { + const currentCount = this.store.projectRoot.project.projectMap[this.projectId]?.intake_count ?? 0; + set(this.store.projectRoot.project.projectMap, [this.projectId, "intake_count"], currentCount + 1); + } }); } catch { runInAction(() => { diff --git a/apps/web/core/store/inbox/project-inbox.store.ts b/apps/web/core/store/inbox/project-inbox.store.ts index df776b88ee..6dc1f96b56 100644 --- a/apps/web/core/store/inbox/project-inbox.store.ts +++ b/apps/web/core/store/inbox/project-inbox.store.ts @@ -473,6 +473,11 @@ export class ProjectInboxStore implements IProjectInboxStore { ["inboxIssuePaginationInfo", "total_results"], (this.inboxIssuePaginationInfo?.total_results || 0) + 1 ); + // Increment intake_count if the new issue is PENDING + if (inboxIssueResponse.status === EInboxIssueStatus.PENDING) { + const currentCount = this.store.projectRoot.project.projectMap[projectId]?.intake_count ?? 0; + set(this.store.projectRoot.project.projectMap, [projectId, "intake_count"], currentCount + 1); + } }); return inboxIssueResponse; } catch (error) { @@ -489,6 +494,7 @@ export class ProjectInboxStore implements IProjectInboxStore { */ deleteInboxIssue = async (workspaceSlug: string, projectId: string, inboxIssueId: string) => { const currentIssue = this.inboxIssues?.[inboxIssueId]; + const wasPending = currentIssue?.status === EInboxIssueStatus.PENDING; try { if (!currentIssue) return; await this.inboxIssueService.destroy(workspaceSlug, projectId, inboxIssueId).then(() => { @@ -504,6 +510,11 @@ export class ProjectInboxStore implements IProjectInboxStore { ["inboxIssueIds"], this.inboxIssueIds.filter((id) => id !== inboxIssueId) ); + // Decrement intake_count if the deleted issue was PENDING + if (wasPending) { + const currentCount = this.store.projectRoot.project.projectMap[projectId]?.intake_count ?? 0; + set(this.store.projectRoot.project.projectMap, [projectId, "intake_count"], Math.max(0, currentCount - 1)); + } }); }); } catch (error) { diff --git a/packages/types/src/project/projects.ts b/packages/types/src/project/projects.ts index 358dbba339..3cdbed1deb 100644 --- a/packages/types/src/project/projects.ts +++ b/packages/types/src/project/projects.ts @@ -39,6 +39,7 @@ export interface IPartialProject { // actor created_by?: string; updated_by?: string; + intake_count?: number; } export interface IProject extends IPartialProject {