Fix inbox recent visibility

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta
2026-03-17 16:10:26 -05:00
parent f598a556dc
commit c121f4d4a7
3 changed files with 138 additions and 26 deletions

View File

@@ -3,12 +3,14 @@
import { beforeEach, describe, expect, it } from "vitest"; import { beforeEach, describe, expect, it } from "vitest";
import type { Approval, DashboardSummary, HeartbeatRun, Issue, JoinRequest } from "@paperclipai/shared"; import type { Approval, DashboardSummary, HeartbeatRun, Issue, JoinRequest } from "@paperclipai/shared";
import { import {
getApprovalsForTab,
computeInboxBadgeData, computeInboxBadgeData,
getRecentTouchedIssues, getRecentTouchedIssues,
getUnreadTouchedIssues, getUnreadTouchedIssues,
loadLastInboxTab, loadLastInboxTab,
RECENT_ISSUES_LIMIT, RECENT_ISSUES_LIMIT,
saveLastInboxTab, saveLastInboxTab,
shouldShowInboxSection,
} from "./inbox"; } from "./inbox";
const storage = new Map<string, string>(); const storage = new Map<string, string>();
@@ -46,6 +48,19 @@ function makeApproval(status: Approval["status"]): Approval {
}; };
} }
function makeApprovalWithTimestamps(
id: string,
status: Approval["status"],
updatedAt: string,
): Approval {
return {
...makeApproval(status),
id,
createdAt: new Date(updatedAt),
updatedAt: new Date(updatedAt),
};
}
function makeJoinRequest(id: string): JoinRequest { function makeJoinRequest(id: string): JoinRequest {
return { return {
id, id,
@@ -231,6 +246,52 @@ describe("inbox helpers", () => {
expect(issues).toHaveLength(2); expect(issues).toHaveLength(2);
}); });
it("shows recent approvals in updated order and unread approvals as actionable only", () => {
const approvals = [
makeApprovalWithTimestamps("approval-approved", "approved", "2026-03-11T02:00:00.000Z"),
makeApprovalWithTimestamps("approval-pending", "pending", "2026-03-11T01:00:00.000Z"),
makeApprovalWithTimestamps(
"approval-revision",
"revision_requested",
"2026-03-11T03:00:00.000Z",
),
];
expect(getApprovalsForTab(approvals, "recent", "all").map((approval) => approval.id)).toEqual([
"approval-revision",
"approval-approved",
"approval-pending",
]);
expect(getApprovalsForTab(approvals, "unread", "all").map((approval) => approval.id)).toEqual([
"approval-revision",
"approval-pending",
]);
expect(getApprovalsForTab(approvals, "all", "resolved").map((approval) => approval.id)).toEqual([
"approval-approved",
]);
});
it("can include sections on recent without forcing them to be unread", () => {
expect(
shouldShowInboxSection({
tab: "recent",
hasItems: true,
showOnRecent: true,
showOnUnread: false,
showOnAll: false,
}),
).toBe(true);
expect(
shouldShowInboxSection({
tab: "unread",
hasItems: true,
showOnRecent: true,
showOnUnread: false,
showOnAll: false,
}),
).toBe(false);
});
it("limits recent touched issues before unread badge counting", () => { it("limits recent touched issues before unread badge counting", () => {
const issues = Array.from({ length: RECENT_ISSUES_LIMIT + 5 }, (_, index) => { const issues = Array.from({ length: RECENT_ISSUES_LIMIT + 5 }, (_, index) => {
const issue = makeIssue(String(index + 1), index < 3); const issue = makeIssue(String(index + 1), index < 3);

View File

@@ -12,6 +12,7 @@ export const ACTIONABLE_APPROVAL_STATUSES = new Set(["pending", "revision_reques
export const DISMISSED_KEY = "paperclip:inbox:dismissed"; export const DISMISSED_KEY = "paperclip:inbox:dismissed";
export const INBOX_LAST_TAB_KEY = "paperclip:inbox:last-tab"; export const INBOX_LAST_TAB_KEY = "paperclip:inbox:last-tab";
export type InboxTab = "recent" | "unread" | "all"; export type InboxTab = "recent" | "unread" | "all";
export type InboxApprovalFilter = "all" | "actionable" | "resolved";
export interface InboxBadgeData { export interface InboxBadgeData {
inbox: number; inbox: number;
@@ -104,6 +105,46 @@ export function getUnreadTouchedIssues(issues: Issue[]): Issue[] {
return issues.filter((issue) => issue.isUnreadForMe); return issues.filter((issue) => issue.isUnreadForMe);
} }
export function getApprovalsForTab(
approvals: Approval[],
tab: InboxTab,
filter: InboxApprovalFilter,
): Approval[] {
const sortedApprovals = [...approvals].sort(
(a, b) => normalizeTimestamp(b.updatedAt) - normalizeTimestamp(a.updatedAt),
);
if (tab === "recent") return sortedApprovals;
if (tab === "unread") {
return sortedApprovals.filter((approval) => ACTIONABLE_APPROVAL_STATUSES.has(approval.status));
}
if (filter === "all") return sortedApprovals;
return sortedApprovals.filter((approval) => {
const isActionable = ACTIONABLE_APPROVAL_STATUSES.has(approval.status);
return filter === "actionable" ? isActionable : !isActionable;
});
}
export function shouldShowInboxSection({
tab,
hasItems,
showOnRecent,
showOnUnread,
showOnAll,
}: {
tab: InboxTab;
hasItems: boolean;
showOnRecent: boolean;
showOnUnread: boolean;
showOnAll: boolean;
}): boolean {
if (!hasItems) return false;
if (tab === "recent") return showOnRecent;
if (tab === "unread") return showOnUnread;
return showOnAll;
}
export function computeInboxBadgeData({ export function computeInboxBadgeData({
approvals, approvals,
joinRequests, joinRequests,

View File

@@ -43,10 +43,13 @@ import { PageTabBar } from "../components/PageTabBar";
import type { HeartbeatRun, Issue, JoinRequest } from "@paperclipai/shared"; import type { HeartbeatRun, Issue, JoinRequest } from "@paperclipai/shared";
import { import {
ACTIONABLE_APPROVAL_STATUSES, ACTIONABLE_APPROVAL_STATUSES,
getApprovalsForTab,
getLatestFailedRunsByAgent, getLatestFailedRunsByAgent,
getRecentTouchedIssues, getRecentTouchedIssues,
InboxApprovalFilter,
type InboxTab, type InboxTab,
saveLastInboxTab, saveLastInboxTab,
shouldShowInboxSection,
} from "../lib/inbox"; } from "../lib/inbox";
import { useDismissedInboxItems } from "../hooks/useInboxBadge"; import { useDismissedInboxItems } from "../hooks/useInboxBadge";
@@ -57,7 +60,6 @@ type InboxCategoryFilter =
| "approvals" | "approvals"
| "failed_runs" | "failed_runs"
| "alerts"; | "alerts";
type InboxApprovalFilter = "all" | "actionable" | "resolved";
type SectionKey = type SectionKey =
| "issues_i_touched" | "issues_i_touched"
| "join_requests" | "join_requests"
@@ -362,10 +364,7 @@ export function Inbox() {
}, [heartbeatRuns]); }, [heartbeatRuns]);
const allApprovals = useMemo( const allApprovals = useMemo(
() => () => getApprovalsForTab(approvals ?? [], "recent", "all"),
[...(approvals ?? [])].sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
),
[approvals], [approvals],
); );
@@ -374,14 +373,10 @@ export function Inbox() {
[allApprovals], [allApprovals],
); );
const filteredAllApprovals = useMemo(() => { const approvalsToRender = useMemo(
if (allApprovalFilter === "all") return allApprovals; () => getApprovalsForTab(approvals ?? [], tab, allApprovalFilter),
[approvals, tab, allApprovalFilter],
return allApprovals.filter((approval) => { );
const isActionable = ACTIONABLE_APPROVAL_STATUSES.has(approval.status);
return allApprovalFilter === "actionable" ? isActionable : !isActionable;
});
}, [allApprovals, allApprovalFilter]);
const agentName = (id: string | null) => { const agentName = (id: string | null) => {
if (!id) return null; if (!id) return null;
@@ -516,21 +511,36 @@ export function Inbox() {
allCategoryFilter === "everything" || allCategoryFilter === "failed_runs"; allCategoryFilter === "everything" || allCategoryFilter === "failed_runs";
const showAlertsCategory = allCategoryFilter === "everything" || allCategoryFilter === "alerts"; const showAlertsCategory = allCategoryFilter === "everything" || allCategoryFilter === "alerts";
const approvalsToRender = tab === "all" ? filteredAllApprovals : actionableApprovals; const showTouchedSection = shouldShowInboxSection({
const showTouchedSection = tab,
tab === "all" hasItems: tab === "unread" ? unreadTouchedIssues.length > 0 : hasTouchedIssues,
? showTouchedCategory && hasTouchedIssues showOnRecent: hasTouchedIssues,
: tab === "unread" showOnUnread: unreadTouchedIssues.length > 0,
? unreadTouchedIssues.length > 0 showOnAll: showTouchedCategory && hasTouchedIssues,
: hasTouchedIssues; });
const showJoinRequestsSection = const showJoinRequestsSection =
tab === "all" ? showJoinRequestsCategory && hasJoinRequests : tab === "unread" && hasJoinRequests; tab === "all" ? showJoinRequestsCategory && hasJoinRequests : tab === "unread" && hasJoinRequests;
const showApprovalsSection = tab === "all" const showApprovalsSection = shouldShowInboxSection({
? showApprovalsCategory && filteredAllApprovals.length > 0 tab,
: actionableApprovals.length > 0; hasItems: approvalsToRender.length > 0,
const showFailedRunsSection = showOnRecent: approvalsToRender.length > 0,
tab === "all" ? showFailedRunsCategory && hasRunFailures : tab === "unread" && hasRunFailures; showOnUnread: actionableApprovals.length > 0,
const showAlertsSection = tab === "all" ? showAlertsCategory && hasAlerts : tab === "unread" && hasAlerts; showOnAll: showApprovalsCategory && approvalsToRender.length > 0,
});
const showFailedRunsSection = shouldShowInboxSection({
tab,
hasItems: hasRunFailures,
showOnRecent: hasRunFailures,
showOnUnread: hasRunFailures,
showOnAll: showFailedRunsCategory && hasRunFailures,
});
const showAlertsSection = shouldShowInboxSection({
tab,
hasItems: hasAlerts,
showOnRecent: hasAlerts,
showOnUnread: hasAlerts,
showOnAll: showAlertsCategory && hasAlerts,
});
const visibleSections = [ const visibleSections = [
showFailedRunsSection ? "failed_runs" : null, showFailedRunsSection ? "failed_runs" : null,