mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
refactor(medications): drop the side-effect slot nobody wires
The card menu took an optional side-effect item for the GLP-1 card, but no page ever passed the handler, so the item never rendered; side effects are logged on the detail page. The slot, its message key in ten locales and the tests that passed it by hand go. One test pinned behaviour only the dead slot produced: a delegate who may write but not manage kept a menu trigger because the test supplied the handler. In the app that delegate never had one, and the test now says so.
This commit is contained in:
1 parent
fc1fadcdab
commit
507d4e6fac
16 files changed
+30
-122
No files matched your search
@@ -320,20 +320,16 @@ describe("marking a dose", () => {
|
||||
|
||||
describe("the medication card menu", () => {
|
||||
const node = (
|
||||
<MedicationCardMenu
|
||||
onEdit={() => {}}
|
||||
onOpenHistory={() => {}}
|
||||
onLogSideEffect={() => {}}
|
||||
/>
|
||||
<MedicationCardMenu onEdit={() => {}} onOpenHistory={() => {}} />
|
||||
);
|
||||
|
||||
it("renders its trigger in the caller's own record", () => {
|
||||
expect(render(OWN_RECORD, node)).toContain("<button");
|
||||
});
|
||||
|
||||
it("keeps a trigger for a delegate who may write", () => {
|
||||
// One item survives for them — noting a side effect — so the menu stays.
|
||||
expect(render(WRITABLE, node)).toContain("<button");
|
||||
it("disappears for a delegate who may write but not manage", () => {
|
||||
// Editing and the history editor are owner work; nothing survives.
|
||||
expect(render(WRITABLE, node)).toBe("");
|
||||
});
|
||||
|
||||
it("disappears entirely for a read-only delegate", () => {
|
||||
|
||||
@@ -465,49 +465,9 @@ describe("<Glp1MedicationCard> — GLP-1 variant rendering", () => {
|
||||
expect(html).not.toContain("Low stock");
|
||||
});
|
||||
|
||||
it("folds the side-effect quick-log into the same overflow kebab", () => {
|
||||
// v1.7.2 — the side-effect quick-log folds into the SAME overflow
|
||||
// menu as edit / history / advanced, so the GLP-1 header carries one
|
||||
// kebab whether or not the hand-off is wired (parity with the generic
|
||||
// card). SSR doesn't open Radix Portal content, so we assert the
|
||||
// single trigger surfaces and smoke-check the handler payload.
|
||||
const client = makeClient();
|
||||
seedCompliance(client, med7p5.id);
|
||||
seedGlp1Details(client, med7p5.id, {});
|
||||
|
||||
const handler = vi.fn();
|
||||
const html = render(
|
||||
<Glp1MedicationCard
|
||||
medication={med7p5}
|
||||
onEdit={() => {}}
|
||||
onOpenHistory={() => {}}
|
||||
onLogSideEffect={handler}
|
||||
/>,
|
||||
client,
|
||||
);
|
||||
|
||||
// Exactly one kebab trigger with the localised aria-label.
|
||||
const triggers = html.match(/aria-label="More options"/g) ?? [];
|
||||
expect(triggers).toHaveLength(1);
|
||||
// The side-effect button must no longer ride alongside
|
||||
// Eingenommen / Übersprungen in the primary actions row.
|
||||
expect(html).not.toMatch(
|
||||
/class="[^"]*flex[^"]*gap-2[^"]*"[^>]*>[\s\S]*?Log side effect/,
|
||||
);
|
||||
|
||||
// Invoke the supplied handler the way the menu item's onClick
|
||||
// would — pins the prefill payload contract for MoodEntry.
|
||||
handler(med7p5);
|
||||
expect(handler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ name: "Mounjaro", treatmentClass: "GLP1" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("still renders the single kebab when onLogSideEffect is not supplied", () => {
|
||||
// v1.7.2 — the kebab is unconditional now (it always carries edit
|
||||
// / history / advanced); the side-effect item is the only opt-in
|
||||
// part. Mounjaro and Ramipril therefore share the same one-kebab
|
||||
// header shape regardless of the hand-off prop.
|
||||
it("renders the single overflow kebab", () => {
|
||||
// The kebab carries edit and history, so Mounjaro and Ramipril share
|
||||
// the same one-kebab header shape.
|
||||
const client = makeClient();
|
||||
seedCompliance(client, med7p5.id);
|
||||
seedGlp1Details(client, med7p5.id, {});
|
||||
|
||||
@@ -190,9 +190,8 @@ describe("medication card symmetry — Ramipril vs Mounjaro", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("Mounjaro keeps the single kebab even when onLogSideEffect is supplied (GLP-1-only)", () => {
|
||||
// The side-effect quick-log folds into the SAME overflow menu, so the
|
||||
// GLP-1 header shape stays byte-symmetric with the generic card: one
|
||||
it("Mounjaro carries the same single kebab as the generic card", () => {
|
||||
// The GLP-1 header shape stays byte-symmetric with the generic card: one
|
||||
// kebab trigger, no extra control.
|
||||
const client = makeClient();
|
||||
seedCompliance(client, mounjaro.id);
|
||||
@@ -203,7 +202,6 @@ describe("medication card symmetry — Ramipril vs Mounjaro", () => {
|
||||
medication={mounjaro}
|
||||
onEdit={() => {}}
|
||||
onOpenHistory={() => {}}
|
||||
onLogSideEffect={() => {}}
|
||||
/>,
|
||||
client,
|
||||
);
|
||||
@@ -294,7 +292,6 @@ describe("medication card symmetry — Ramipril vs Mounjaro", () => {
|
||||
medication={mounjaro}
|
||||
onEdit={() => {}}
|
||||
onOpenHistory={() => {}}
|
||||
onLogSideEffect={() => {}}
|
||||
/>,
|
||||
client,
|
||||
);
|
||||
|
||||
@@ -178,7 +178,6 @@ interface Glp1MedicationCardProps {
|
||||
* card. The parent owns the navigation.
|
||||
*/
|
||||
onOpenHistory: (med: Glp1Medication) => void;
|
||||
onLogSideEffect?: (med: Glp1Medication) => void;
|
||||
/**
|
||||
* v1.29.x — briefly ringed when this card is the Today digest's "Log
|
||||
* dose" deep-link target (`/medications?highlight=<id>`).
|
||||
@@ -215,7 +214,6 @@ export function Glp1MedicationCard({
|
||||
medication,
|
||||
onEdit,
|
||||
onOpenHistory,
|
||||
onLogSideEffect,
|
||||
highlighted = false,
|
||||
}: Glp1MedicationCardProps) {
|
||||
const queryClient = useQueryClient();
|
||||
@@ -478,19 +476,11 @@ export function Glp1MedicationCard({
|
||||
// The four former header icon-buttons (open / edit / history /
|
||||
// advanced) collapse into a SINGLE overflow menu so the GLP-1 card
|
||||
// stays symmetric with the generic medication card. The card header
|
||||
// itself links to the detail page (the former chevron target). The
|
||||
// optional side-effect quick-log would fold in as a last menu item
|
||||
// when `onLogSideEffect` is wired, but the medications list page does
|
||||
// not wire it — side-effect logging lives on the detail page's
|
||||
// SideEffectsSection — so the menu renders the same items as the
|
||||
// generic card.
|
||||
// itself links to the detail page (the former chevron target).
|
||||
const headerActions = (
|
||||
<MedicationCardMenu
|
||||
onEdit={() => onEdit(medication)}
|
||||
onOpenHistory={() => onOpenHistory(medication)}
|
||||
onLogSideEffect={
|
||||
onLogSideEffect ? () => onLogSideEffect(medication) : undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -14,13 +14,8 @@
|
||||
* target as the name tap); the structural wizard lives under the detail
|
||||
* page's Erweitert → Lebenszyklus row.
|
||||
*
|
||||
* The optional `onLogSideEffect` slot exists for the GLP-1 card to fold a
|
||||
* "Log side effect" item into the SAME menu, but it is NOT wired on the
|
||||
* medications list page — both card variants there receive only the
|
||||
* edit / history / advanced handlers, so the item never renders. Side-
|
||||
* effect logging lives on the detail page's `<SideEffectsSection>`. When
|
||||
* the slot is left undefined the two card variants stay byte-symmetric in
|
||||
* the header (one kebab, identical items).
|
||||
* Side-effect logging lives on the detail page's `<SideEffectsSection>`, so
|
||||
* the two card variants carry the same menu (one kebab, identical items).
|
||||
*
|
||||
* The trigger is a 44px ghost icon-button (WCAG 2.5.5); every
|
||||
* `DropdownMenuItem` already carries `min-h-11` from the shadcn
|
||||
@@ -33,14 +28,13 @@
|
||||
*/
|
||||
|
||||
import { useRecordCapabilities } from "@/hooks/use-record-capabilities";
|
||||
import { History, MoreVertical, Pencil, Stethoscope } from "lucide-react";
|
||||
import { History, MoreVertical, Pencil } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
@@ -48,25 +42,19 @@ import { useTranslations } from "@/lib/i18n/context";
|
||||
export interface MedicationCardMenuProps {
|
||||
onEdit: () => void;
|
||||
onOpenHistory: () => void;
|
||||
/** GLP-1-only: folds the side-effect quick-log into the same menu. */
|
||||
onLogSideEffect?: () => void;
|
||||
}
|
||||
|
||||
export function MedicationCardMenu({
|
||||
onEdit,
|
||||
onOpenHistory,
|
||||
onLogSideEffect,
|
||||
}: MedicationCardMenuProps) {
|
||||
const { t } = useTranslations();
|
||||
// v1.36.x — noting a side effect is an admitted delegated write; editing the
|
||||
// medication and opening its history editor are not. When nothing in the
|
||||
// menu survives, the trigger goes with it rather than opening onto an empty
|
||||
// sheet. The card itself is untouched: no wash, no badge, no colour change.
|
||||
const { canWriteDomain, canManageDomain } = useRecordCapabilities();
|
||||
const canAddSideEffect = canWriteDomain("medications");
|
||||
const canManageMedications = canManageDomain("medications");
|
||||
const showSideEffect = onLogSideEffect != null && canAddSideEffect;
|
||||
if (!canManageMedications && !showSideEffect) return null;
|
||||
// Editing the medication and opening its history editor are owner work, not
|
||||
// delegated writes. Without either, the trigger goes too rather than opening
|
||||
// onto an empty menu. The card itself is untouched: no wash, no badge, no
|
||||
// colour change.
|
||||
const { canManageDomain } = useRecordCapabilities();
|
||||
if (!canManageDomain("medications")) return null;
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
@@ -89,27 +77,14 @@ export function MedicationCardMenu({
|
||||
className="w-56"
|
||||
data-slot="medication-card-menu"
|
||||
>
|
||||
{canManageMedications && (
|
||||
<>
|
||||
<DropdownMenuItem onClick={onEdit}>
|
||||
<Pencil className="mr-2 h-4 w-4" />
|
||||
{t("common.edit")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={onOpenHistory}>
|
||||
<History className="mr-2 h-4 w-4" />
|
||||
{t("medications.detail.header.historyLabel")}
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
{showSideEffect && onLogSideEffect && (
|
||||
<>
|
||||
{canManageMedications && <DropdownMenuSeparator />}
|
||||
<DropdownMenuItem onClick={onLogSideEffect}>
|
||||
<Stethoscope className="mr-2 h-4 w-4" />
|
||||
{t("medications.glp1LogSideEffect")}
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuItem onClick={onEdit}>
|
||||
<Pencil className="mr-2 h-4 w-4" />
|
||||
{t("common.edit")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={onOpenHistory}>
|
||||
<History className="mr-2 h-4 w-4" />
|
||||
{t("medications.detail.header.historyLabel")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user