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..aa6d352b92
--- /dev/null
+++ b/packages/ui/src/features/inbox/hooks/useInboxBackTarget.test.ts
@@ -0,0 +1,36 @@
+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..a35413e8c7
--- /dev/null
+++ b/packages/ui/src/features/inbox/hooks/useInboxBackTarget.ts
@@ -0,0 +1,59 @@
+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 = (typeof INBOX_LIST_ROUTE_VALUES)[number];
+
+/**
+ * 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(INBOX_LIST_ROUTE_VALUES);
+
+/**
+ * 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;
+}