[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
5 changed files with 76 additions and 7 deletions

View File

@@ -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",

View File

@@ -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 (
<Link key={item.key} href={item.href} onClick={handleProjectClick}>
<SidebarNavItem isActive={!!isActive(item)}>
<div className="flex items-center gap-1.5 py-[1px]">
<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 className="flex items-center justify-between gap-1.5 py-[1px] w-full">
<div className="flex items-center gap-1.5">
<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>
</SidebarNavItem>
</Link>

View File

@@ -100,6 +100,7 @@ export class InboxIssueStore implements IInboxIssueStore {
const previousData: Partial<TInboxIssue> = {
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(() => {

View File

@@ -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) {

View File

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