From 97b636eba9787ed9998f40e7358aaa65fe216b15 Mon Sep 17 00:00:00 2001 From: Rafael Audibert <32079912+rafaeelaudibert@users.noreply.github.com> Date: Tue, 23 Jun 2026 13:23:04 -0300 Subject: [PATCH 1/3] fix(inbox): keep detail back link pointing at the tab you came from MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Archiving a report while viewing it flipped the back link from "Back to reports" to "Back to archive": the gate redirects the now-suppressed report to /code/inbox/dismissed/$reportId, whose detail view hard-codes the Archive back target. The link followed the report's new state instead of the path the user took in. Carry the origin tab through the status↔route redirect in navigation state so the Archive detail's back link returns to Reports/Pulls/Runs (whichever the user came from), falling back to "Back to archive" only when arriving directly (deep link, Archive-tab click, or a refresh that drops history state). The redirect itself is unchanged, so archived reports still stay off the triage view. - New useInboxBackTarget hook: owns the back-target types, augments TanStack Router HistoryState with inboxBackOrigin, and validates the untyped state at the boundary. - InboxReportDetailGate stamps the origin on the to-Archive redirect and gains display-only backLinkTo/backLinkLabel overrides; backTo stays the route identity driving the redirect and engagement tracking. - DismissedReportDetail resolves and uses the recorded origin. - Widen InboxDetailFrame backTo to the shared InboxListRoute union. - Unit-test the history-state validator. Generated-By: PostHog Code Task-Id: 5e171745-ab49-45aa-82e7-c7b7e3ec7584 --- .../components/DismissedReportDetail.tsx | 28 +++++++-- .../inbox/components/InboxDetailFrame.tsx | 3 +- .../components/InboxReportDetailGate.tsx | 33 +++++++--- .../inbox/hooks/useInboxBackTarget.test.ts | 30 +++++++++ .../inbox/hooks/useInboxBackTarget.ts | 61 +++++++++++++++++++ 5 files changed, 143 insertions(+), 12 deletions(-) create mode 100644 packages/ui/src/features/inbox/hooks/useInboxBackTarget.test.ts create mode 100644 packages/ui/src/features/inbox/hooks/useInboxBackTarget.ts diff --git a/packages/ui/src/features/inbox/components/DismissedReportDetail.tsx b/packages/ui/src/features/inbox/components/DismissedReportDetail.tsx index 4127484f85..54f36bd310 100644 --- a/packages/ui/src/features/inbox/components/DismissedReportDetail.tsx +++ b/packages/ui/src/features/inbox/components/DismissedReportDetail.tsx @@ -8,6 +8,10 @@ import { Button } from "@posthog/quill"; import type { SignalReport } from "@posthog/shared/types"; import { InboxDetailFrame } from "@posthog/ui/features/inbox/components/InboxDetailFrame"; import { InboxReportDetailGate } from "@posthog/ui/features/inbox/components/InboxReportDetailGate"; +import { + type InboxBackTarget, + useInboxBackTarget, +} from "@posthog/ui/features/inbox/hooks/useInboxBackTarget"; import { useInboxRestoreReport } from "@posthog/ui/features/inbox/hooks/useInboxRestoreReport"; import { copyInboxReportLink } from "@posthog/ui/features/inbox/utils/copyInboxReportLink"; import { Spinner } from "@radix-ui/themes"; @@ -35,28 +39,44 @@ export function DismissedReportDetail({ reportId, cachedReport = null, }: DismissedReportDetailProps) { + // Follow the user's path in: if they archived a report while viewing it, the + // redirect here recorded its origin (Reports / Pulls / Runs) so the back link + // returns there. Arriving directly (deep link, Archive-tab click, refresh) + // falls back to "Back to archive". + const back = useInboxBackTarget({ + to: "/code/inbox/dismissed", + label: "Back to archive", + }); return ( - {(report) => } + {(report) => } ); } -function DismissedReportDetailContent({ report }: { report: SignalReport }) { +function DismissedReportDetailContent({ + report, + back, +}: { + report: SignalReport; + back: InboxBackTarget; +}) { // Resolved reports are terminal (their PR already merged) — nothing to // restore, so only suppressed reports get a Restore action. const canRestore = report.status === "suppressed"; return ( ReactNode; } @@ -57,6 +63,8 @@ export function InboxReportDetailGate({ cachedReport = null, backTo, backLabel, + backLinkTo, + backLinkLabel, missingCopy, children, }: InboxReportDetailGateProps) { @@ -110,8 +118,16 @@ export function InboxReportDetailGate({ to: redirectTo, params: { reportId: redirectReportId }, replace: true, + // Carry where we came from into the Archive route so its back link reads + // "Back to reports/pulls/runs" rather than "Back to archive". This branch + // only fires from a non-Archive route, so `backTo` is the pipeline origin + // the user is returning to. + state: + redirectTo === "/code/inbox/dismissed/$reportId" + ? { inboxBackOrigin: { to: backTo, label: backLabel } } + : undefined, }); - }, [redirectTo, redirectReportId, navigate]); + }, [redirectTo, redirectReportId, navigate, backTo, backLabel]); if ((isLoading && !resolvedReport) || statusUnconfirmed) { return ( @@ -139,7 +155,10 @@ export function InboxReportDetailGate({ gap="3" className="border-(--gray-5) border-b px-6 py-6" > - + {missingCopy} diff --git a/packages/ui/src/features/inbox/hooks/useInboxBackTarget.test.ts b/packages/ui/src/features/inbox/hooks/useInboxBackTarget.test.ts new file mode 100644 index 0000000000..42465a96ac --- /dev/null +++ b/packages/ui/src/features/inbox/hooks/useInboxBackTarget.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from "vitest"; +import { asInboxBackTarget } from "./useInboxBackTarget"; + +describe("asInboxBackTarget", () => { + it.each([ + ["reports origin", { to: "/code/inbox/reports", label: "Back to reports" }], + ["pulls origin", { to: "/code/inbox/pulls", label: "Back to pull requests" }], + ["runs origin", { to: "/code/inbox/runs", label: "Back to runs" }], + [ + "archive origin", + { to: "/code/inbox/dismissed", label: "Back to archive" }, + ], + ])("accepts a valid %s", (_label, value) => { + expect(asInboxBackTarget(value)).toEqual(value); + }); + + it.each([ + ["undefined (no history state)", undefined], + ["null", null], + ["a non-object", "reports"], + ["a route outside the inbox", { to: "/code/tasks", label: "Back" }], + ["a non-list inbox route", { to: "/code/inbox/reports/abc", label: "Back" }], + ["a missing label", { to: "/code/inbox/reports" }], + ["an empty label", { to: "/code/inbox/reports", label: "" }], + ["a missing route", { label: "Back to reports" }], + ["a non-string route", { to: 7, label: "Back" }], + ])("rejects %s and falls back", (_label, value) => { + expect(asInboxBackTarget(value)).toBeNull(); + }); +}); diff --git a/packages/ui/src/features/inbox/hooks/useInboxBackTarget.ts b/packages/ui/src/features/inbox/hooks/useInboxBackTarget.ts new file mode 100644 index 0000000000..1bff03e2ba --- /dev/null +++ b/packages/ui/src/features/inbox/hooks/useInboxBackTarget.ts @@ -0,0 +1,61 @@ +import { useLocation } from "@tanstack/react-router"; + +/** List routes an inbox detail screen's back link can return to. */ +export type InboxListRoute = + | "/code/inbox/pulls" + | "/code/inbox/reports" + | "/code/inbox/runs" + | "/code/inbox/dismissed"; + +/** + * Where a detail screen's back link should go. The Archive redirect records the + * origin here so a report archived while open keeps "Back to reports" (or pulls + * / runs) instead of flipping to "Back to archive" the moment its status + * changes. The back link follows the path the user took in, not the report's + * current state. + */ +export interface InboxBackTarget { + to: InboxListRoute; + label: string; +} + +// Carried in history state across the status↔route redirect; declared here so +// `navigate({ state })` and `useLocation().state` stay typed. +declare module "@tanstack/react-router" { + interface HistoryState { + inboxBackOrigin?: InboxBackTarget; + } +} + +const INBOX_LIST_ROUTES = new Set([ + "/code/inbox/pulls", + "/code/inbox/reports", + "/code/inbox/runs", + "/code/inbox/dismissed", +]); + +/** + * Validate untyped history state before trusting it: it may have come from an + * older app version, a hand-edited URL, or a corrupted entry. + */ +export function asInboxBackTarget(value: unknown): InboxBackTarget | null { + if (!value || typeof value !== "object") return null; + const { to, label } = value as Record; + if (typeof label !== "string" || label.length === 0) return null; + if (typeof to !== "string" || !INBOX_LIST_ROUTES.has(to as InboxListRoute)) { + return null; + } + return { to: to as InboxListRoute, label }; +} + +/** + * Resolves a detail screen's back link: the origin recorded when the status↔route + * redirect carried us here, or `fallback` when we arrived directly (deep link, + * tab click, or a page refresh that dropped the history state). + */ +export function useInboxBackTarget(fallback: InboxBackTarget): InboxBackTarget { + const origin = useLocation({ + select: (location) => location.state.inboxBackOrigin, + }); + return asInboxBackTarget(origin) ?? fallback; +} From bdefbfa44eac3332179186d6a248a7e8cc40b12f Mon Sep 17 00:00:00 2001 From: Rafa Audibert Date: Thu, 25 Jun 2026 20:11:24 -0300 Subject: [PATCH 2/3] fix: Fix formatting --- .../features/inbox/hooks/useInboxBackTarget.test.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/features/inbox/hooks/useInboxBackTarget.test.ts b/packages/ui/src/features/inbox/hooks/useInboxBackTarget.test.ts index 42465a96ac..aa6d352b92 100644 --- a/packages/ui/src/features/inbox/hooks/useInboxBackTarget.test.ts +++ b/packages/ui/src/features/inbox/hooks/useInboxBackTarget.test.ts @@ -4,7 +4,10 @@ import { asInboxBackTarget } from "./useInboxBackTarget"; describe("asInboxBackTarget", () => { it.each([ ["reports origin", { to: "/code/inbox/reports", label: "Back to reports" }], - ["pulls origin", { to: "/code/inbox/pulls", label: "Back to pull requests" }], + [ + "pulls origin", + { to: "/code/inbox/pulls", label: "Back to pull requests" }, + ], ["runs origin", { to: "/code/inbox/runs", label: "Back to runs" }], [ "archive origin", @@ -19,7 +22,10 @@ describe("asInboxBackTarget", () => { ["null", null], ["a non-object", "reports"], ["a route outside the inbox", { to: "/code/tasks", label: "Back" }], - ["a non-list inbox route", { to: "/code/inbox/reports/abc", label: "Back" }], + [ + "a non-list inbox route", + { to: "/code/inbox/reports/abc", label: "Back" }, + ], ["a missing label", { to: "/code/inbox/reports" }], ["an empty label", { to: "/code/inbox/reports", label: "" }], ["a missing route", { label: "Back to reports" }], From c7dd774f9d840af951961073ba11dfa10b9fb2c8 Mon Sep 17 00:00:00 2001 From: Rafa Audibert Date: Tue, 14 Jul 2026 12:26:49 -0300 Subject: [PATCH 3/3] refactor(inbox): derive InboxListRoute from single route array Address review feedback: the four inbox list routes were listed twice (once for the InboxListRoute union, once for the runtime Set). Derive the type from a single as-const array so a new route only needs one edit. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../inbox/hooks/useInboxBackTarget.ts | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/ui/src/features/inbox/hooks/useInboxBackTarget.ts b/packages/ui/src/features/inbox/hooks/useInboxBackTarget.ts index 1bff03e2ba..a35413e8c7 100644 --- a/packages/ui/src/features/inbox/hooks/useInboxBackTarget.ts +++ b/packages/ui/src/features/inbox/hooks/useInboxBackTarget.ts @@ -1,11 +1,14 @@ import { useLocation } from "@tanstack/react-router"; +const INBOX_LIST_ROUTE_VALUES = [ + "/code/inbox/pulls", + "/code/inbox/reports", + "/code/inbox/runs", + "/code/inbox/dismissed", +] as const; + /** List routes an inbox detail screen's back link can return to. */ -export type InboxListRoute = - | "/code/inbox/pulls" - | "/code/inbox/reports" - | "/code/inbox/runs" - | "/code/inbox/dismissed"; +export type InboxListRoute = (typeof INBOX_LIST_ROUTE_VALUES)[number]; /** * Where a detail screen's back link should go. The Archive redirect records the @@ -27,12 +30,7 @@ declare module "@tanstack/react-router" { } } -const INBOX_LIST_ROUTES = new Set([ - "/code/inbox/pulls", - "/code/inbox/reports", - "/code/inbox/runs", - "/code/inbox/dismissed", -]); +const INBOX_LIST_ROUTES = new Set(INBOX_LIST_ROUTE_VALUES); /** * Validate untyped history state before trusting it: it may have come from an