Mix approvals into inbox activity feed
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -3,8 +3,9 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import type { Approval, DashboardSummary, HeartbeatRun, Issue, JoinRequest } from "@paperclipai/shared";
|
||||
import {
|
||||
getApprovalsForTab,
|
||||
computeInboxBadgeData,
|
||||
getApprovalsForTab,
|
||||
getInboxWorkItems,
|
||||
getRecentTouchedIssues,
|
||||
getUnreadTouchedIssues,
|
||||
loadLastInboxTab,
|
||||
@@ -271,6 +272,31 @@ describe("inbox helpers", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("mixes approvals into the inbox feed by most recent activity", () => {
|
||||
const newerIssue = makeIssue("1", true);
|
||||
newerIssue.lastExternalCommentAt = new Date("2026-03-11T04:00:00.000Z");
|
||||
|
||||
const olderIssue = makeIssue("2", false);
|
||||
olderIssue.lastExternalCommentAt = new Date("2026-03-11T02:00:00.000Z");
|
||||
|
||||
const approval = makeApprovalWithTimestamps(
|
||||
"approval-between",
|
||||
"pending",
|
||||
"2026-03-11T03:00:00.000Z",
|
||||
);
|
||||
|
||||
expect(
|
||||
getInboxWorkItems({
|
||||
issues: [olderIssue, newerIssue],
|
||||
approvals: [approval],
|
||||
}).map((item) => item.kind === "issue" ? `issue:${item.issue.id}` : `approval:${item.approval.id}`),
|
||||
).toEqual([
|
||||
"issue:1",
|
||||
"approval:approval-between",
|
||||
"issue:2",
|
||||
]);
|
||||
});
|
||||
|
||||
it("can include sections on recent without forcing them to be unread", () => {
|
||||
expect(
|
||||
shouldShowInboxSection({
|
||||
|
||||
@@ -13,6 +13,17 @@ export const DISMISSED_KEY = "paperclip:inbox:dismissed";
|
||||
export const INBOX_LAST_TAB_KEY = "paperclip:inbox:last-tab";
|
||||
export type InboxTab = "recent" | "unread" | "all";
|
||||
export type InboxApprovalFilter = "all" | "actionable" | "resolved";
|
||||
export type InboxWorkItem =
|
||||
| {
|
||||
kind: "issue";
|
||||
timestamp: number;
|
||||
issue: Issue;
|
||||
}
|
||||
| {
|
||||
kind: "approval";
|
||||
timestamp: number;
|
||||
approval: Approval;
|
||||
};
|
||||
|
||||
export interface InboxBadgeData {
|
||||
inbox: number;
|
||||
@@ -126,6 +137,45 @@ export function getApprovalsForTab(
|
||||
});
|
||||
}
|
||||
|
||||
export function approvalActivityTimestamp(approval: Approval): number {
|
||||
const updatedAt = normalizeTimestamp(approval.updatedAt);
|
||||
if (updatedAt > 0) return updatedAt;
|
||||
return normalizeTimestamp(approval.createdAt);
|
||||
}
|
||||
|
||||
export function getInboxWorkItems({
|
||||
issues,
|
||||
approvals,
|
||||
}: {
|
||||
issues: Issue[];
|
||||
approvals: Approval[];
|
||||
}): InboxWorkItem[] {
|
||||
return [
|
||||
...issues.map((issue) => ({
|
||||
kind: "issue" as const,
|
||||
timestamp: issueLastActivityTimestamp(issue),
|
||||
issue,
|
||||
})),
|
||||
...approvals.map((approval) => ({
|
||||
kind: "approval" as const,
|
||||
timestamp: approvalActivityTimestamp(approval),
|
||||
approval,
|
||||
})),
|
||||
].sort((a, b) => {
|
||||
const timestampDiff = b.timestamp - a.timestamp;
|
||||
if (timestampDiff !== 0) return timestampDiff;
|
||||
|
||||
if (a.kind === "issue" && b.kind === "issue") {
|
||||
return sortIssuesByMostRecentActivity(a.issue, b.issue);
|
||||
}
|
||||
if (a.kind === "approval" && b.kind === "approval") {
|
||||
return approvalActivityTimestamp(b.approval) - approvalActivityTimestamp(a.approval);
|
||||
}
|
||||
|
||||
return a.kind === "approval" ? -1 : 1;
|
||||
});
|
||||
}
|
||||
|
||||
export function shouldShowInboxSection({
|
||||
tab,
|
||||
hasItems,
|
||||
|
||||
Reference in New Issue
Block a user