From c72715bb62f0903a475e1e0d6d5b05bafd90ec66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Mon, 1 Jun 2026 23:04:36 +0200 Subject: [PATCH] feat(insights): edit target ranges inline instead of routing to the targets page The per-metric reference panel on each insights category page now opens the self-contained target editor in place rather than linking out to the targets page. The panel mounts the same the targets card uses, seeded with the metric's type, unit, and range (plus the diastolic band for blood pressure); the sheet writes the threshold override and invalidates the shared targets cache on save, so the panel repaints without a navigation. Switch the sheet's targets-cache invalidation to the query-key factory call so it stays aligned with the centralised key. --- .../__tests__/metric-target-summary.test.tsx | 40 ++++++++++- .../insights/metric-target-summary.tsx | 70 +++++++++++-------- src/components/targets/target-edit-sheet.tsx | 4 +- 3 files changed, 79 insertions(+), 35 deletions(-) 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. */}
+ + {/* 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"));