diff --git a/src/components/insights/__tests__/metric-target-summary.test.tsx b/src/components/insights/__tests__/metric-target-summary.test.tsx index 0ff5bbaf9..9b9aaa980 100644 --- a/src/components/insights/__tests__/metric-target-summary.test.tsx +++ b/src/components/insights/__tests__/metric-target-summary.test.tsx @@ -23,8 +23,13 @@ vi.mock("@/hooks/use-auth", () => ({ })); const useQueryMock = vi.fn(); +// The inline (mounted by the panel once the +// adjust-target button is clicked) also pulls `useMutation` + +// `useQueryClient`, so stub those alongside `useQuery`. vi.mock("@tanstack/react-query", () => ({ useQuery: (opts: unknown) => useQueryMock(opts), + useMutation: () => ({ mutate: vi.fn(), isPending: false }), + useQueryClient: () => ({ invalidateQueries: vi.fn() }), })); const { MetricTargetSummary } = await import("../metric-target-summary"); @@ -188,10 +193,41 @@ describe("", () => { expect(html).toContain("30-day average: 122/76 mmHg"); }); - it("renders the adjust-target link to /targets", () => { + it("renders the inline adjust-target button instead of a /targets link", () => { const html = renderWith("weight", WEIGHT_DATA); - expect(html).toContain('href="/targets"'); + // v1.8.6 — the /targets page is deprecated; the editor moved inline, + // so the footer is a button that opens the sheet, not a route link. + expect(html).toContain('data-slot="metric-target-adjust"'); expect(html).toContain("Adjust target range"); + expect(html).not.toContain('href="/targets"'); + }); + + it("keeps the inline editor lazy-mounted while the panel is closed", () => { + // The panel mounts alongside itself; + // the sheet body (and its TanStack Query hooks) only instantiate once + // the button flips the open flag, so a closed panel paints no editor. + const html = renderWith("weight", WEIGHT_DATA); + expect(html).not.toContain('data-slot="target-edit-sheet"'); + }); + + it("renders the adjust-target affordance as a button, not a navigation link", () => { + // The migration's load-bearing change: the affordance is a local + // button (opens the inline sheet) rather than an / routing to + // the deprecated /targets page. + const html = renderWith("weight", WEIGHT_DATA); + const button = html.match(/]*data-slot="metric-target-adjust"[^>]*/)?.[0] ?? ""; + expect(button).toContain('type="button"'); + expect(html).not.toContain('href="/targets"'); + }); + + it("threads the diastolic range into the editor for blood pressure", () => { + // The panel passes `bpDiastolic.range` as the sheet's + // `initialDiastolicRange`. Render the panel for BP and confirm the + // adjust button is present so the BP editor is reachable inline (the + // sheet body itself portals, so it isn't part of the static markup). + const html = renderWith("blood-pressure", BP_DATA); + expect(html).toContain('data-slot="metric-target-adjust"'); + expect(html).not.toContain('href="/targets"'); }); it("suppresses the share + strip when the route flags insufficient data", () => { diff --git a/src/components/insights/metric-target-summary.tsx b/src/components/insights/metric-target-summary.tsx index c444dc25c..b875518c1 100644 --- a/src/components/insights/metric-target-summary.tsx +++ b/src/components/insights/metric-target-summary.tsx @@ -1,6 +1,6 @@ "use client"; -import Link from "next/link"; +import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { ExternalLink, Target } from "lucide-react"; @@ -11,6 +11,7 @@ import { convertGlucose, resolveGlucoseUnit } from "@/lib/glucose"; import { RangeBar } from "@/components/targets/range-bar"; import { ConsistencyStrip } from "@/components/targets/consistency-strip"; import { TargetStatusPill } from "@/components/targets/target-status-pill"; +import { TargetEditSheet } from "@/components/targets/target-edit-sheet"; import { getTargetSourceLink } from "@/lib/targets/source-link"; /** @@ -36,7 +37,9 @@ import { getTargetSourceLink } from "@/lib/targets/source-link"; * (the spatial answer). BP renders the second diastolic bar. * • Row 3: in-target share % + the 30-day average. * • Row 4: `` — the last seven days at a glance. - * • Footer: the "Adjust target range" link to `/targets`. + * • Footer: an "Adjust target range" button that opens the + * `` inline (v1.8.6 — the `/targets` page is + * deprecated, so the editor mounts here rather than routing). * * Each insights slug maps to one target `type`, except blood glucose * which maps to up to four per-context cards (fasting / postprandial / @@ -169,9 +172,9 @@ export function MetricTargetSummary({ slug }: MetricTargetSummaryProps) { if (panels.length === 0) return null; - // Glucose fans out to up to four per-context panels. The - // "Adjust target range" link is identical on every one, so render - // it once for the whole group rather than repeating it per panel. + // Glucose fans out to up to four per-context panels. Each context + // maps to its own editable threshold, so every panel carries its own + // inline edit button rather than a single shared one. return (
{panels.map((panel) => ( @@ -179,12 +182,8 @@ export function MetricTargetSummary({ slug }: MetricTargetSummaryProps) { key={panel.type} target={panel} heading={panel.label} - hideAdjustLink /> ))} -
- -
); } @@ -207,25 +206,6 @@ interface TargetReferencePanelProps { bpDiastolic?: TargetsResponse["bpDiastolic"]; /** Optional sub-heading shown above the panel (used for glucose contexts). */ heading?: string; - /** - * Suppress the per-panel "Adjust target range" link. Glucose stacks - * up to four panels and lifts a single link to the group wrapper, so - * each panel omits its own to avoid repeating the same link. - */ - hideAdjustLink?: boolean; -} - -/** The shared "Adjust target range" link to the `/targets` editing surface. */ -function AdjustTargetLink() { - const { t } = useTranslations(); - return ( - - {t("insights.subPage.target.adjustLink")} - - ); } /** @@ -239,10 +219,17 @@ function TargetReferencePanel({ target, bpDiastolic, heading, - hideAdjustLink, }: TargetReferencePanelProps) { const { t } = useTranslations(); + // v1.8.6 — the `/targets` page is deprecated; the per-metric editor + // moved here. The button below opens the same self-contained + // `` the targets card mounts, seeded with this + // metric's type / unit / range (and the diastolic range for BP). The + // sheet writes `PUT /api/user/thresholds` and invalidates the + // `insightsTargets()` cache on save, so the panel repaints in place. + const [editOpen, setEditOpen] = useState(false); + const { range, unit } = target; if (!range) return null; @@ -378,7 +365,7 @@ function TargetReferencePanel({ /> ) : null} - {/* Footer: source link + adjust-target link. */} + {/* Footer: source link + inline adjust-target button. */}
{sourceLink ? ( )} - {hideAdjustLink ? null : } +
+ + {/* v1.8.6 — inline target editor, mounted alongside the panel and + portalled by the sheet primitive. The body only instantiates its + TanStack Query hooks once `editOpen` is true (lazy mount inside + the sheet), so closed panels stay cheap. */} + ); } diff --git a/src/components/targets/target-edit-sheet.tsx b/src/components/targets/target-edit-sheet.tsx index ed3ffd7b8..35c6903d6 100644 --- a/src/components/targets/target-edit-sheet.tsx +++ b/src/components/targets/target-edit-sheet.tsx @@ -216,7 +216,7 @@ function TargetEditSheetBody({ }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["user", "thresholds"] }); - queryClient.invalidateQueries({ queryKey: ["insights", "targets"] }); + queryClient.invalidateQueries({ queryKey: queryKeys.insightsTargets() }); queryClient.invalidateQueries({ queryKey: queryKeys.analytics() }); queryClient.invalidateQueries({ queryKey: ["insights"] }); toast.success(t("targets.edit.saveSuccess")); @@ -248,7 +248,7 @@ function TargetEditSheetBody({ }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["user", "thresholds"] }); - queryClient.invalidateQueries({ queryKey: ["insights", "targets"] }); + queryClient.invalidateQueries({ queryKey: queryKeys.insightsTargets() }); queryClient.invalidateQueries({ queryKey: queryKeys.analytics() }); queryClient.invalidateQueries({ queryKey: ["insights"] }); toast.success(t("targets.edit.resetSuccess"));