Skip to content

Commit e17a9f2

Browse files
committed
feat(inbox): add read-only detail view for dismissed reports
Dismissed cards now link into a detail view (summary + evidence) with a single Restore action; no triage affordances since the report is out of the pipeline. - DismissedReportDetail reuses InboxReportDetailGate + InboxDetailFrame. - InboxDetailFrame gains a showDismiss opt-out (a dismissed report shouldn't show 'Dismiss'); the gate skips OPENED/CLOSED engagement tracking for the dismissed tab (its rank would be measured against the wrong list). - Routes restructured: dismissed.tsx is now an Outlet layout, with dismissed.index (list) and dismissed.$reportId (detail). - The route loader resolves the cached report from the dismissed list cache, so navigation renders instantly; the detail's own fetch depends on the backend serving suppressed reports on retrieve/signals (PostHog/posthog#64019).
1 parent f308711 commit e17a9f2

9 files changed

Lines changed: 209 additions & 26 deletions

File tree

packages/ui/src/features/inbox/CLAUDE.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,16 @@ Inbox has four tabs and one reviewer-scope control:
2727
Detail pages live under the same tab: `/code/inbox/<tab>/$reportId`.
2828

2929
The Dismissed tab is the exception: suppressed reports are excluded from the
30-
main pipeline query and from the report detail endpoint, so the tab fetches
31-
them with a dedicated `status=suppressed` query (`useInboxDismissedReports`) and
32-
its cards do **not** link to a detail page. Each card can be restored to the
33-
inbox via `useInboxRestoreReport`, which reuses the `state` action's `potential`
34-
("reopen") transition — the only reopen path the backend exposes. The reviewer
35-
scope control is hidden on this tab since the dismissed list is not scoped.
30+
main pipeline query, so the tab fetches them with a dedicated `status=suppressed`
31+
query (`useInboxDismissedReports`). Its detail view (`DismissedReportDetail`) is
32+
read-only — summary + evidence + a single Restore action, no triage affordances —
33+
and depends on the backend serving suppressed reports on the `retrieve`/`signals`
34+
read paths (PostHog/posthog#64019). Restore uses `useInboxRestoreReport`, which
35+
reuses the `state` action's `potential` ("reopen") transition — the only reopen
36+
path the backend exposes. The reviewer scope control is hidden on this tab since
37+
the dismissed list is not scoped, and the tab carries no count badge. The
38+
Dismissed detail is **not** a tracked `InboxDetailTab` (no OPENED/CLOSED
39+
engagement events), since its rank would be measured against the wrong list.
3640

3741
Responder configuration is **not** an Inbox tab. It is the top-level Responders sidebar item at `/code/agents`. The legacy `/code/inbox/agents` route redirects there.
3842

packages/ui/src/features/inbox/components/DismissedReportCard.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { PriorityMonogram } from "@posthog/ui/features/inbox/components/Priority
1616
import { hasKnownSourceProduct } from "@posthog/ui/features/inbox/components/utils/source-product-icons";
1717
import { Button as UiButton } from "@posthog/ui/primitives/Button";
1818
import { Flex, Text } from "@radix-ui/themes";
19+
import { Link } from "@tanstack/react-router";
1920

2021
interface DismissedReportCardProps {
2122
report: SignalReport;
@@ -24,9 +25,8 @@ interface DismissedReportCardProps {
2425
}
2526

2627
/**
27-
* Read-only card for the Dismissed tab. Suppressed reports have no reachable
28-
* detail page (the backend detail endpoint excludes them), so the card does not
29-
* link out — its only action is restoring the report back to the inbox.
28+
* Card for the Dismissed tab. Links into the read-only dismissed detail view;
29+
* the Restore button (right column) stops propagation so it doesn't navigate.
3030
*/
3131
export function DismissedReportCard({
3232
report,
@@ -56,7 +56,12 @@ export function DismissedReportCard({
5656
"group flex w-full items-stretch gap-3 rounded-(--radius-2) border border-(--gray-6) border-dashed bg-(--color-panel-solid) px-4 py-3.5 opacity-90 transition duration-150 hover:border-(--gray-7) hover:bg-(--gray-2)",
5757
)}
5858
>
59-
<div className="flex min-w-0 flex-1 items-start gap-3">
59+
<Link
60+
to="/code/inbox/dismissed/$reportId"
61+
params={{ reportId: report.id }}
62+
preload="intent"
63+
className="flex min-w-0 flex-1 items-start gap-3 text-left text-inherit no-underline focus-visible:outline-none"
64+
>
6065
<PriorityMonogram priority={report.priority} />
6166

6267
<Flex direction="column" gap="1.5" className="min-w-0 flex-1">
@@ -92,7 +97,7 @@ export function DismissedReportCard({
9297
</Flex>
9398
)}
9499
</Flex>
95-
</div>
100+
</Link>
96101

97102
<Flex
98103
direction="column"
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import {
2+
ArrowCounterClockwiseIcon,
3+
FileTextIcon,
4+
MagnifyingGlassIcon,
5+
} from "@phosphor-icons/react";
6+
import { Button } from "@posthog/quill";
7+
import type { SignalReport } from "@posthog/shared/types";
8+
import { InboxDetailFrame } from "@posthog/ui/features/inbox/components/InboxDetailFrame";
9+
import { InboxReportDetailGate } from "@posthog/ui/features/inbox/components/InboxReportDetailGate";
10+
import { useInboxRestoreReport } from "@posthog/ui/features/inbox/hooks/useInboxRestoreReport";
11+
import { Spinner } from "@radix-ui/themes";
12+
import { useNavigate } from "@tanstack/react-router";
13+
14+
interface DismissedReportDetailProps {
15+
reportId: string;
16+
cachedReport?: SignalReport | null;
17+
}
18+
19+
/**
20+
* Detail view for a dismissed (suppressed) report. Read-only re-read of what the
21+
* report was — summary + evidence — with a single Restore action. No triage
22+
* affordances (dismiss, discuss, create PR, reviewers): the report is out of the
23+
* pipeline until it's restored.
24+
*/
25+
export function DismissedReportDetail({
26+
reportId,
27+
cachedReport = null,
28+
}: DismissedReportDetailProps) {
29+
return (
30+
<InboxReportDetailGate
31+
reportId={reportId}
32+
cachedReport={cachedReport}
33+
backTo="/code/inbox/dismissed"
34+
backLabel="Back to dismissed"
35+
missingCopy="This report couldn't be found. It may have been deleted."
36+
>
37+
{(report) => <DismissedReportDetailContent report={report} />}
38+
</InboxReportDetailGate>
39+
);
40+
}
41+
42+
function DismissedReportDetailContent({ report }: { report: SignalReport }) {
43+
return (
44+
<InboxDetailFrame
45+
report={report}
46+
backTo="/code/inbox/dismissed"
47+
backLabel="Back to dismissed"
48+
fallbackTitle="Untitled report"
49+
showDismiss={false}
50+
primaryAction={<RestoreReportButton report={report} />}
51+
summarySection={{ Icon: FileTextIcon, title: "Summary" }}
52+
evidenceSection={{ Icon: MagnifyingGlassIcon, title: "Evidence" }}
53+
/>
54+
);
55+
}
56+
57+
function RestoreReportButton({ report }: { report: SignalReport }) {
58+
const restore = useInboxRestoreReport();
59+
const navigate = useNavigate();
60+
61+
return (
62+
<Button
63+
type="button"
64+
variant="primary"
65+
size="sm"
66+
disabled={restore.isPending}
67+
className="gap-1"
68+
title="Restore this report to the inbox"
69+
onClick={() =>
70+
restore.mutate(report.id, {
71+
onSuccess: () => navigate({ to: "/code/inbox/dismissed" }),
72+
})
73+
}
74+
>
75+
{restore.isPending ? (
76+
<Spinner size="1" />
77+
) : (
78+
<ArrowCounterClockwiseIcon size={12} />
79+
)}
80+
Restore
81+
</Button>
82+
);
83+
}

packages/ui/src/features/inbox/components/InboxDetailFrame.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,13 @@ import type { ComponentType, ReactNode } from "react";
2727
interface InboxDetailFrameProps {
2828
report: SignalReport;
2929
/** List route for the back-link (e.g. "/code/inbox/pulls"). */
30-
backTo: "/code/inbox/pulls" | "/code/inbox/reports";
30+
backTo: "/code/inbox/pulls" | "/code/inbox/reports" | "/code/inbox/dismissed";
3131
backLabel: string;
32+
/**
33+
* Whether to render the Dismiss button + dialog. Off for already-dismissed
34+
* reports (the Dismissed tab), where dismissing again makes no sense.
35+
*/
36+
showDismiss?: boolean;
3237
/** Title fallback when `report.title` is blank. */
3338
fallbackTitle: string;
3439
/** Optional breadcrumb fragment (e.g. PR repo slug + number). */
@@ -72,6 +77,7 @@ export function InboxDetailFrame({
7277
primaryAction,
7378
summarySection,
7479
evidenceSection,
80+
showDismiss = true,
7581
children,
7682
}: InboxDetailFrameProps) {
7783
const { data: signalsResp } = useInboxReportSignals(report.id);
@@ -150,7 +156,7 @@ export function InboxDetailFrame({
150156
}
151157
actions={
152158
<Flex align="center" className="gap-2.5">
153-
{dismissButton}
159+
{showDismiss && dismissButton}
154160
{primaryAction}
155161
</Flex>
156162
}
@@ -200,7 +206,7 @@ export function InboxDetailFrame({
200206
{children}
201207
</div>
202208
</div>
203-
{dismissDialog}
209+
{showDismiss && dismissDialog}
204210
</div>
205211
</Flex>
206212
);

packages/ui/src/features/inbox/components/InboxReportDetailGate.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ import type { ReactNode } from "react";
1212
interface InboxReportDetailGateProps {
1313
reportId: string;
1414
cachedReport?: SignalReport | null;
15-
backTo: "/code/inbox/pulls" | "/code/inbox/reports" | "/code/inbox/runs";
15+
backTo:
16+
| "/code/inbox/pulls"
17+
| "/code/inbox/reports"
18+
| "/code/inbox/runs"
19+
| "/code/inbox/dismissed";
1620
backLabel: string;
1721
missingCopy: string;
1822
children: (report: SignalReport) => ReactNode;
@@ -57,19 +61,26 @@ export function InboxReportDetailGate({
5761
);
5862
}
5963

64+
const trackTab = tabFromBackTo(backTo);
6065
return (
6166
<>
62-
<ReportOpenTracker report={resolvedReport} tab={tabFromBackTo(backTo)} />
67+
{trackTab && <ReportOpenTracker report={resolvedReport} tab={trackTab} />}
6368
{children(resolvedReport)}
6469
</>
6570
);
6671
}
6772

73+
/**
74+
* The Dismissed tab isn't part of the triage funnel and isn't a tracked
75+
* `InboxDetailTab` (its rank would be measured against the wrong list), so it
76+
* returns `null` and the open/close engagement events are skipped for it.
77+
*/
6878
function tabFromBackTo(
6979
backTo: InboxReportDetailGateProps["backTo"],
70-
): InboxDetailTab {
80+
): InboxDetailTab | null {
7181
if (backTo === "/code/inbox/pulls") return "pulls";
7282
if (backTo === "/code/inbox/runs") return "runs";
83+
if (backTo === "/code/inbox/dismissed") return null;
7384
return "reports";
7485
}
7586

0 commit comments

Comments
 (0)