Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 (
<InboxReportDetailGate
reportId={reportId}
cachedReport={cachedReport}
backTo="/code/inbox/dismissed"
backLabel="Back to archive"
backLinkTo={back.to}
backLinkLabel={back.label}
missingCopy="This report couldn't be found. It may have been deleted."
>
{(report) => <DismissedReportDetailContent report={report} />}
{(report) => <DismissedReportDetailContent report={report} back={back} />}
</InboxReportDetailGate>
);
}

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 (
<InboxDetailFrame
report={report}
backTo="/code/inbox/dismissed"
backLabel="Back to archive"
backTo={back.to}
backLabel={back.label}
fallbackTitle="Untitled report"
showDismiss={false}
primaryAction={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { SignalReportPriorityBadge } from "@posthog/ui/features/inbox/components
import { SignalReportStatusBadge } from "@posthog/ui/features/inbox/components/utils/SignalReportStatusBadge";
import { SignalReportSummaryMarkdown } from "@posthog/ui/features/inbox/components/utils/SignalReportSummaryMarkdown";
import { hasKnownSourceProduct } from "@posthog/ui/features/inbox/components/utils/source-product-icons";
import type { InboxListRoute } from "@posthog/ui/features/inbox/hooks/useInboxBackTarget";
import { useInboxReportDismissAction } from "@posthog/ui/features/inbox/hooks/useInboxReportDismissAction";
import { useInboxReportSignals } from "@posthog/ui/features/inbox/hooks/useInboxReports";
import { RelativeTimestamp } from "@posthog/ui/primitives/RelativeTimestamp";
Expand All @@ -27,7 +28,7 @@ import type { ComponentType, ReactNode } from "react";
interface InboxDetailFrameProps {
report: SignalReport;
/** List route for the back-link (e.g. "/code/inbox/pulls"). */
backTo: "/code/inbox/pulls" | "/code/inbox/reports" | "/code/inbox/dismissed";
backTo: InboxListRoute;
backLabel: string;
/**
* Whether to render the Dismiss button + dialog. Off for already-dismissed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
import { Spinner } from "@posthog/quill";
import type { SignalReport } from "@posthog/shared/types";
import { DetailBackLink } from "@posthog/ui/features/inbox/components/DetailBackLink";
import type { InboxListRoute } from "@posthog/ui/features/inbox/hooks/useInboxBackTarget";
import { useInboxReportById } from "@posthog/ui/features/inbox/hooks/useInboxReports";
import {
type InboxDetailTab,
Expand All @@ -18,12 +19,17 @@ import { type ReactNode, useEffect } from "react";
interface InboxReportDetailGateProps {
reportId: string;
cachedReport?: SignalReport | null;
backTo:
| "/code/inbox/pulls"
| "/code/inbox/reports"
| "/code/inbox/runs"
| "/code/inbox/dismissed";
backTo: InboxListRoute;
backLabel: string;
/**
* Where the missing-report shell's back link points, when it should differ
* from `backTo`. The Archive detail sets these to the recorded origin so the
* link follows the user's path in, while `backTo` stays the route identity
* that drives the status↔route redirect and engagement tracking below.
* Defaults to `backTo`/`backLabel`.
*/
backLinkTo?: string;
backLinkLabel?: string;
missingCopy: string;
children: (report: SignalReport) => ReactNode;
}
Expand Down Expand Up @@ -57,6 +63,8 @@ export function InboxReportDetailGate({
cachedReport = null,
backTo,
backLabel,
backLinkTo,
backLinkLabel,
missingCopy,
children,
}: InboxReportDetailGateProps) {
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -139,7 +155,10 @@ export function InboxReportDetailGate({
gap="3"
className="border-(--gray-5) border-b px-6 py-6"
>
<DetailBackLink to={backTo} label={backLabel} />
<DetailBackLink
to={backLinkTo ?? backTo}
label={backLinkLabel ?? backLabel}
/>
<Text className="text-[13px] text-gray-11">{missingCopy}</Text>
</Flex>
</Flex>
Expand Down
36 changes: 36 additions & 0 deletions packages/ui/src/features/inbox/hooks/useInboxBackTarget.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
59 changes: 59 additions & 0 deletions packages/ui/src/features/inbox/hooks/useInboxBackTarget.ts
Original file line number Diff line number Diff line change
@@ -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<InboxListRoute>(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<string, unknown>;
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;
}
Loading