[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 <anmolsinghbhatia@plane.so>
This commit is contained in:
Sangeetha 2026-02-17 00:04:03 +05:30 committed by GitHub
parent ef5d481a19
commit 3a99ecf8f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 76 additions and 7 deletions

View file

@ -8,7 +8,7 @@ import json
# Django imports # Django imports
from django.core.serializers.json import DjangoJSONEncoder 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 from django.utils import timezone
# Third Party imports # Third Party imports
@ -28,7 +28,6 @@ from plane.bgtasks.webhook_task import model_activity, webhook_activity
from plane.db.models import ( from plane.db.models import (
UserFavorite, UserFavorite,
DeployBoard, DeployBoard,
ProjectUserProperty,
Intake, Intake,
Project, Project,
ProjectIdentifier, ProjectIdentifier,
@ -36,10 +35,10 @@ from plane.db.models import (
ProjectNetwork, ProjectNetwork,
State, State,
DEFAULT_STATES, DEFAULT_STATES,
UserFavorite,
Workspace, Workspace,
WorkspaceMember, WorkspaceMember,
) )
from plane.db.models.intake import IntakeIssueStatus
from plane.utils.host import base_host from plane.utils.host import base_host
@ -155,6 +154,15 @@ class ProjectViewSet(BaseViewSet):
is_active=True, is_active=True,
).values("role") ).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(inbox_view=F("intake_view"))
.annotate(sort_order=Subquery(sort_order)) .annotate(sort_order=Subquery(sort_order))
.distinct() .distinct()
@ -165,6 +173,7 @@ class ProjectViewSet(BaseViewSet):
"sort_order", "sort_order",
"logo_props", "logo_props",
"member_role", "member_role",
"intake_count",
"archived_at", "archived_at",
"workspace", "workspace",
"cycle_view", "cycle_view",

View file

@ -181,12 +181,19 @@ export const ProjectNavigation = observer(function ProjectNavigation(props: TPro
const hasAccess = allowPermissions(item.access, EUserPermissionsLevel.PROJECT, workspaceSlug, project.id); const hasAccess = allowPermissions(item.access, EUserPermissionsLevel.PROJECT, workspaceSlug, project.id);
if (!hasAccess) return null; if (!hasAccess) return null;
const shouldShowCount = item.key === "intake" && (project.intake_count ?? 0) > 0;
return ( return (
<Link key={item.key} href={item.href} onClick={handleProjectClick}> <Link key={item.key} href={item.href} onClick={handleProjectClick}>
<SidebarNavItem isActive={!!isActive(item)}> <SidebarNavItem isActive={!!isActive(item)}>
<div className="flex items-center gap-1.5 py-[1px]"> <div className="flex items-center justify-between gap-1.5 py-[1px] w-full">
<item.icon className={`flex-shrink-0 size-4 ${item.name === "Intake" ? "stroke-1" : "stroke-[1.5]"}`} /> <div className="flex items-center gap-1.5">
<span className="text-11 font-medium">{t(item.i18n_key)}</span> <item.icon
className={`flex-shrink-0 size-4 ${item.name === "Intake" ? "stroke-1" : "stroke-[1.5]"}`}
/>
<span className="text-11 font-medium">{t(item.i18n_key)}</span>
</div>
{shouldShowCount && <span className="text-11 font-medium text-tertiary">{project.intake_count}</span>}
</div> </div>
</SidebarNavItem> </SidebarNavItem>
</Link> </Link>

View file

@ -100,6 +100,7 @@ export class InboxIssueStore implements IInboxIssueStore {
const previousData: Partial<TInboxIssue> = { const previousData: Partial<TInboxIssue> = {
status: this.status, status: this.status,
}; };
const previousStatus = this.status;
try { try {
if (!this.issue.id) return; 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, { const inboxIssue = await this.inboxIssueService.update(this.workspaceSlug, this.projectId, this.issue.id, {
status: status, 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 issue accepted sync issue to local db
if (status === EInboxIssueStatus.ACCEPTED) { if (status === EInboxIssueStatus.ACCEPTED) {
@ -126,6 +144,7 @@ export class InboxIssueStore implements IInboxIssueStore {
duplicate_to: this.duplicate_to, duplicate_to: this.duplicate_to,
duplicate_issue_detail: this.duplicate_issue_detail, duplicate_issue_detail: this.duplicate_issue_detail,
}; };
const wasPending = this.status === EInboxIssueStatus.PENDING;
try { try {
if (!this.issue.id) return; if (!this.issue.id) return;
const inboxIssue = await this.inboxIssueService.update(this.workspaceSlug, this.projectId, this.issue.id, { 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, "status", inboxIssue?.status);
set(this, "duplicate_to", inboxIssue?.duplicate_to); set(this, "duplicate_to", inboxIssue?.duplicate_to);
set(this, "duplicate_issue_detail", inboxIssue?.duplicate_issue_detail); 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 { } catch {
runInAction(() => { runInAction(() => {
@ -152,6 +180,7 @@ export class InboxIssueStore implements IInboxIssueStore {
status: this.status, status: this.status,
snoozed_till: this.snoozed_till, snoozed_till: this.snoozed_till,
}; };
const previousStatus = this.status;
try { try {
if (!this.issue.id) return; if (!this.issue.id) return;
const inboxIssue = await this.inboxIssueService.update(this.workspaceSlug, this.projectId, this.issue.id, { const inboxIssue = await this.inboxIssueService.update(this.workspaceSlug, this.projectId, this.issue.id, {
@ -161,6 +190,18 @@ export class InboxIssueStore implements IInboxIssueStore {
runInAction(() => { runInAction(() => {
set(this, "status", inboxIssue?.status); set(this, "status", inboxIssue?.status);
set(this, "snoozed_till", inboxIssue?.snoozed_till); 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 { } catch {
runInAction(() => { runInAction(() => {

View file

@ -473,6 +473,11 @@ export class ProjectInboxStore implements IProjectInboxStore {
["inboxIssuePaginationInfo", "total_results"], ["inboxIssuePaginationInfo", "total_results"],
(this.inboxIssuePaginationInfo?.total_results || 0) + 1 (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; return inboxIssueResponse;
} catch (error) { } catch (error) {
@ -489,6 +494,7 @@ export class ProjectInboxStore implements IProjectInboxStore {
*/ */
deleteInboxIssue = async (workspaceSlug: string, projectId: string, inboxIssueId: string) => { deleteInboxIssue = async (workspaceSlug: string, projectId: string, inboxIssueId: string) => {
const currentIssue = this.inboxIssues?.[inboxIssueId]; const currentIssue = this.inboxIssues?.[inboxIssueId];
const wasPending = currentIssue?.status === EInboxIssueStatus.PENDING;
try { try {
if (!currentIssue) return; if (!currentIssue) return;
await this.inboxIssueService.destroy(workspaceSlug, projectId, inboxIssueId).then(() => { await this.inboxIssueService.destroy(workspaceSlug, projectId, inboxIssueId).then(() => {
@ -504,6 +510,11 @@ export class ProjectInboxStore implements IProjectInboxStore {
["inboxIssueIds"], ["inboxIssueIds"],
this.inboxIssueIds.filter((id) => id !== inboxIssueId) 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) { } catch (error) {

View file

@ -39,6 +39,7 @@ export interface IPartialProject {
// actor // actor
created_by?: string; created_by?: string;
updated_by?: string; updated_by?: string;
intake_count?: number;
} }
export interface IProject extends IPartialProject { export interface IProject extends IPartialProject {