mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-17 03:10:38 +09:00
Persist non-issue inbox dismissals
This commit is contained in:
parent
1de5fb9316
commit
5640d29ab0
23 changed files with 13623 additions and 54 deletions
41
server/src/services/inbox-dismissals.ts
Normal file
41
server/src/services/inbox-dismissals.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { and, desc, eq } from "drizzle-orm";
|
||||
import type { Db } from "@paperclipai/db";
|
||||
import { inboxDismissals } from "@paperclipai/db";
|
||||
|
||||
export function inboxDismissalService(db: Db) {
|
||||
return {
|
||||
list: async (companyId: string, userId: string) =>
|
||||
db
|
||||
.select()
|
||||
.from(inboxDismissals)
|
||||
.where(and(eq(inboxDismissals.companyId, companyId), eq(inboxDismissals.userId, userId)))
|
||||
.orderBy(desc(inboxDismissals.updatedAt)),
|
||||
|
||||
dismiss: async (
|
||||
companyId: string,
|
||||
userId: string,
|
||||
itemKey: string,
|
||||
dismissedAt: Date = new Date(),
|
||||
) => {
|
||||
const now = new Date();
|
||||
const [row] = await db
|
||||
.insert(inboxDismissals)
|
||||
.values({
|
||||
companyId,
|
||||
userId,
|
||||
itemKey,
|
||||
dismissedAt,
|
||||
updatedAt: now,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [inboxDismissals.companyId, inboxDismissals.userId, inboxDismissals.itemKey],
|
||||
set: {
|
||||
dismissedAt,
|
||||
updatedAt: now,
|
||||
},
|
||||
})
|
||||
.returning();
|
||||
return row;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ export { financeService } from "./finance.js";
|
|||
export { heartbeatService } from "./heartbeat.js";
|
||||
export { dashboardService } from "./dashboard.js";
|
||||
export { sidebarBadgeService } from "./sidebar-badges.js";
|
||||
export { inboxDismissalService } from "./inbox-dismissals.js";
|
||||
export { accessService } from "./access.js";
|
||||
export { boardAuthService } from "./board-auth.js";
|
||||
export { instanceSettingsService } from "./instance-settings.js";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { and, desc, eq, inArray, not, sql } from "drizzle-orm";
|
||||
import { and, desc, eq, inArray, not } from "drizzle-orm";
|
||||
import type { Db } from "@paperclipai/db";
|
||||
import { agents, approvals, heartbeatRuns } from "@paperclipai/db";
|
||||
import type { SidebarBadges } from "@paperclipai/shared";
|
||||
|
|
@ -6,14 +6,34 @@ import type { SidebarBadges } from "@paperclipai/shared";
|
|||
const ACTIONABLE_APPROVAL_STATUSES = ["pending", "revision_requested"];
|
||||
const FAILED_HEARTBEAT_STATUSES = ["failed", "timed_out"];
|
||||
|
||||
function normalizeTimestamp(value: Date | string | null | undefined): number {
|
||||
if (!value) return 0;
|
||||
const timestamp = new Date(value).getTime();
|
||||
return Number.isFinite(timestamp) ? timestamp : 0;
|
||||
}
|
||||
|
||||
function isDismissed(
|
||||
dismissedAtByKey: ReadonlyMap<string, number>,
|
||||
itemKey: string,
|
||||
activityAt: Date | string | null | undefined,
|
||||
) {
|
||||
const dismissedAt = dismissedAtByKey.get(itemKey);
|
||||
if (dismissedAt == null) return false;
|
||||
return dismissedAt >= normalizeTimestamp(activityAt);
|
||||
}
|
||||
|
||||
export function sidebarBadgeService(db: Db) {
|
||||
return {
|
||||
get: async (
|
||||
companyId: string,
|
||||
extra?: { joinRequests?: number; unreadTouchedIssues?: number },
|
||||
extra?: {
|
||||
dismissals?: ReadonlyMap<string, number>;
|
||||
joinRequests?: Array<{ id: string; updatedAt: Date | string | null; createdAt: Date | string }>;
|
||||
unreadTouchedIssues?: number;
|
||||
},
|
||||
): Promise<SidebarBadges> => {
|
||||
const actionableApprovals = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.select({ id: approvals.id, updatedAt: approvals.updatedAt })
|
||||
.from(approvals)
|
||||
.where(
|
||||
and(
|
||||
|
|
@ -21,11 +41,15 @@ export function sidebarBadgeService(db: Db) {
|
|||
inArray(approvals.status, ACTIONABLE_APPROVAL_STATUSES),
|
||||
),
|
||||
)
|
||||
.then((rows) => Number(rows[0]?.count ?? 0));
|
||||
.then((rows) =>
|
||||
rows.filter((row) => !isDismissed(extra?.dismissals ?? new Map(), `approval:${row.id}`, row.updatedAt)).length
|
||||
);
|
||||
|
||||
const latestRunByAgent = await db
|
||||
.selectDistinctOn([heartbeatRuns.agentId], {
|
||||
id: heartbeatRuns.id,
|
||||
runStatus: heartbeatRuns.status,
|
||||
createdAt: heartbeatRuns.createdAt,
|
||||
})
|
||||
.from(heartbeatRuns)
|
||||
.innerJoin(agents, eq(heartbeatRuns.agentId, agents.id))
|
||||
|
|
@ -39,10 +63,17 @@ export function sidebarBadgeService(db: Db) {
|
|||
.orderBy(heartbeatRuns.agentId, desc(heartbeatRuns.createdAt));
|
||||
|
||||
const failedRuns = latestRunByAgent.filter((row) =>
|
||||
FAILED_HEARTBEAT_STATUSES.includes(row.runStatus),
|
||||
FAILED_HEARTBEAT_STATUSES.includes(row.runStatus)
|
||||
&& !isDismissed(extra?.dismissals ?? new Map(), `run:${row.id}`, row.createdAt),
|
||||
).length;
|
||||
|
||||
const joinRequests = extra?.joinRequests ?? 0;
|
||||
const joinRequests = (extra?.joinRequests ?? []).filter((row) =>
|
||||
!isDismissed(
|
||||
extra?.dismissals ?? new Map(),
|
||||
`join:${row.id}`,
|
||||
row.updatedAt ?? row.createdAt,
|
||||
)
|
||||
).length;
|
||||
const unreadTouchedIssues = extra?.unreadTouchedIssues ?? 0;
|
||||
return {
|
||||
inbox: actionableApprovals + failedRuns + joinRequests + unreadTouchedIssues,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue