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