[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:
parent
ef5d481a19
commit
3a99ecf8f3
5 changed files with 76 additions and 7 deletions
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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(() => {
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue