mirror of
https://github.com/makeplane/plane.git
synced 2026-02-24 04:00:14 +01:00
[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:
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -39,6 +39,7 @@ export interface IPartialProject {
|
||||
// actor
|
||||
created_by?: string;
|
||||
updated_by?: string;
|
||||
intake_count?: number;
|
||||
}
|
||||
|
||||
export interface IProject extends IPartialProject {
|
||||
|
||||
Reference in New Issue
Block a user