mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-18 19:50:38 +09:00
Add the inbox mine tab and archive flow
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
b34fa3b273
commit
995f5b0b66
21 changed files with 12514 additions and 43 deletions
|
|
@ -210,7 +210,7 @@ describe("inbox helpers", () => {
|
|||
makeRun("run-latest", "timed_out", "2026-03-11T01:00:00.000Z"),
|
||||
makeRun("run-other-agent", "failed", "2026-03-11T02:00:00.000Z", "agent-2"),
|
||||
],
|
||||
unreadIssues: [makeIssue("1", true)],
|
||||
mineIssues: [makeIssue("1", true)],
|
||||
dismissed: new Set<string>(),
|
||||
});
|
||||
|
||||
|
|
@ -219,7 +219,7 @@ describe("inbox helpers", () => {
|
|||
approvals: 1,
|
||||
failedRuns: 2,
|
||||
joinRequests: 1,
|
||||
unreadTouchedIssues: 1,
|
||||
mineIssues: 1,
|
||||
alerts: 1,
|
||||
});
|
||||
});
|
||||
|
|
@ -230,7 +230,7 @@ describe("inbox helpers", () => {
|
|||
joinRequests: [],
|
||||
dashboard,
|
||||
heartbeatRuns: [makeRun("run-1", "failed", "2026-03-11T00:00:00.000Z")],
|
||||
unreadIssues: [],
|
||||
mineIssues: [],
|
||||
dismissed: new Set<string>(["run:run-1", "alert:budget", "alert:agent-errors"]),
|
||||
});
|
||||
|
||||
|
|
@ -239,7 +239,7 @@ describe("inbox helpers", () => {
|
|||
approvals: 0,
|
||||
failedRuns: 0,
|
||||
joinRequests: 0,
|
||||
unreadTouchedIssues: 0,
|
||||
mineIssues: 0,
|
||||
alerts: 0,
|
||||
});
|
||||
});
|
||||
|
|
@ -262,6 +262,11 @@ describe("inbox helpers", () => {
|
|||
),
|
||||
];
|
||||
|
||||
expect(getApprovalsForTab(approvals, "mine", "all").map((approval) => approval.id)).toEqual([
|
||||
"approval-revision",
|
||||
"approval-approved",
|
||||
"approval-pending",
|
||||
]);
|
||||
expect(getApprovalsForTab(approvals, "recent", "all").map((approval) => approval.id)).toEqual([
|
||||
"approval-revision",
|
||||
"approval-approved",
|
||||
|
|
@ -338,10 +343,21 @@ describe("inbox helpers", () => {
|
|||
});
|
||||
|
||||
it("can include sections on recent without forcing them to be unread", () => {
|
||||
expect(
|
||||
shouldShowInboxSection({
|
||||
tab: "mine",
|
||||
hasItems: true,
|
||||
showOnMine: true,
|
||||
showOnRecent: false,
|
||||
showOnUnread: false,
|
||||
showOnAll: false,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldShowInboxSection({
|
||||
tab: "recent",
|
||||
hasItems: true,
|
||||
showOnMine: false,
|
||||
showOnRecent: true,
|
||||
showOnUnread: false,
|
||||
showOnAll: false,
|
||||
|
|
@ -351,6 +367,7 @@ describe("inbox helpers", () => {
|
|||
shouldShowInboxSection({
|
||||
tab: "unread",
|
||||
hasItems: true,
|
||||
showOnMine: true,
|
||||
showOnRecent: true,
|
||||
showOnUnread: false,
|
||||
showOnAll: false,
|
||||
|
|
@ -371,16 +388,16 @@ describe("inbox helpers", () => {
|
|||
expect(getUnreadTouchedIssues(recentIssues).map((issue) => issue.id)).toEqual(["1", "2", "3"]);
|
||||
});
|
||||
|
||||
it("defaults the remembered inbox tab to recent and persists all", () => {
|
||||
it("defaults the remembered inbox tab to mine and persists all", () => {
|
||||
localStorage.clear();
|
||||
expect(loadLastInboxTab()).toBe("recent");
|
||||
expect(loadLastInboxTab()).toBe("mine");
|
||||
|
||||
saveLastInboxTab("all");
|
||||
expect(loadLastInboxTab()).toBe("all");
|
||||
});
|
||||
|
||||
it("maps legacy new-tab storage to recent", () => {
|
||||
it("maps legacy new-tab storage to mine", () => {
|
||||
localStorage.setItem("paperclip:inbox:last-tab", "new");
|
||||
expect(loadLastInboxTab()).toBe("recent");
|
||||
expect(loadLastInboxTab()).toBe("mine");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export const FAILED_RUN_STATUSES = new Set(["failed", "timed_out"]);
|
|||
export const ACTIONABLE_APPROVAL_STATUSES = new Set(["pending", "revision_requested"]);
|
||||
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 InboxTab = "mine" | "recent" | "unread" | "all";
|
||||
export type InboxApprovalFilter = "all" | "actionable" | "resolved";
|
||||
export type InboxWorkItem =
|
||||
| {
|
||||
|
|
@ -40,7 +40,7 @@ export interface InboxBadgeData {
|
|||
approvals: number;
|
||||
failedRuns: number;
|
||||
joinRequests: number;
|
||||
unreadTouchedIssues: number;
|
||||
mineIssues: number;
|
||||
alerts: number;
|
||||
}
|
||||
|
||||
|
|
@ -64,11 +64,11 @@ export function saveDismissedInboxItems(ids: Set<string>) {
|
|||
export function loadLastInboxTab(): InboxTab {
|
||||
try {
|
||||
const raw = localStorage.getItem(INBOX_LAST_TAB_KEY);
|
||||
if (raw === "all" || raw === "unread" || raw === "recent") return raw;
|
||||
if (raw === "new") return "recent";
|
||||
return "recent";
|
||||
if (raw === "all" || raw === "unread" || raw === "recent" || raw === "mine") return raw;
|
||||
if (raw === "new") return "mine";
|
||||
return "mine";
|
||||
} catch {
|
||||
return "recent";
|
||||
return "mine";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -135,7 +135,7 @@ export function getApprovalsForTab(
|
|||
(a, b) => normalizeTimestamp(b.updatedAt) - normalizeTimestamp(a.updatedAt),
|
||||
);
|
||||
|
||||
if (tab === "recent") return sortedApprovals;
|
||||
if (tab === "mine" || tab === "recent") return sortedApprovals;
|
||||
if (tab === "unread") {
|
||||
return sortedApprovals.filter((approval) => ACTIONABLE_APPROVAL_STATUSES.has(approval.status));
|
||||
}
|
||||
|
|
@ -203,17 +203,20 @@ export function getInboxWorkItems({
|
|||
export function shouldShowInboxSection({
|
||||
tab,
|
||||
hasItems,
|
||||
showOnMine,
|
||||
showOnRecent,
|
||||
showOnUnread,
|
||||
showOnAll,
|
||||
}: {
|
||||
tab: InboxTab;
|
||||
hasItems: boolean;
|
||||
showOnMine: boolean;
|
||||
showOnRecent: boolean;
|
||||
showOnUnread: boolean;
|
||||
showOnAll: boolean;
|
||||
}): boolean {
|
||||
if (!hasItems) return false;
|
||||
if (tab === "mine") return showOnMine;
|
||||
if (tab === "recent") return showOnRecent;
|
||||
if (tab === "unread") return showOnUnread;
|
||||
return showOnAll;
|
||||
|
|
@ -224,14 +227,14 @@ export function computeInboxBadgeData({
|
|||
joinRequests,
|
||||
dashboard,
|
||||
heartbeatRuns,
|
||||
unreadIssues,
|
||||
mineIssues,
|
||||
dismissed,
|
||||
}: {
|
||||
approvals: Approval[];
|
||||
joinRequests: JoinRequest[];
|
||||
dashboard: DashboardSummary | undefined;
|
||||
heartbeatRuns: HeartbeatRun[];
|
||||
unreadIssues: Issue[];
|
||||
mineIssues: Issue[];
|
||||
dismissed: Set<string>;
|
||||
}): InboxBadgeData {
|
||||
const actionableApprovals = approvals.filter((approval) =>
|
||||
|
|
@ -240,7 +243,7 @@ export function computeInboxBadgeData({
|
|||
const failedRuns = getLatestFailedRunsByAgent(heartbeatRuns).filter(
|
||||
(run) => !dismissed.has(`run:${run.id}`),
|
||||
).length;
|
||||
const unreadTouchedIssues = unreadIssues.length;
|
||||
const visibleMineIssues = mineIssues.length;
|
||||
const agentErrorCount = dashboard?.agents.error ?? 0;
|
||||
const monthBudgetCents = dashboard?.costs.monthBudgetCents ?? 0;
|
||||
const monthUtilizationPercent = dashboard?.costs.monthUtilizationPercent ?? 0;
|
||||
|
|
@ -255,11 +258,11 @@ export function computeInboxBadgeData({
|
|||
const alerts = Number(showAggregateAgentError) + Number(showBudgetAlert);
|
||||
|
||||
return {
|
||||
inbox: actionableApprovals + joinRequests.length + failedRuns + unreadTouchedIssues + alerts,
|
||||
inbox: actionableApprovals + joinRequests.length + failedRuns + visibleMineIssues + alerts,
|
||||
approvals: actionableApprovals,
|
||||
failedRuns,
|
||||
joinRequests: joinRequests.length,
|
||||
unreadTouchedIssues,
|
||||
mineIssues: visibleMineIssues,
|
||||
alerts,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export const queryKeys = {
|
|||
search: (companyId: string, q: string, projectId?: string) =>
|
||||
["issues", companyId, "search", q, projectId ?? "__all-projects__"] as const,
|
||||
listAssignedToMe: (companyId: string) => ["issues", companyId, "assigned-to-me"] as const,
|
||||
listMineByMe: (companyId: string) => ["issues", companyId, "mine-by-me"] as const,
|
||||
listTouchedByMe: (companyId: string) => ["issues", companyId, "touched-by-me"] as const,
|
||||
listUnreadTouchedByMe: (companyId: string) => ["issues", companyId, "unread-touched-by-me"] as const,
|
||||
labels: (companyId: string) => ["issues", companyId, "labels"] as const,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue