Fix inbox recent visibility
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user