diff --git a/src/components/layout/__tests__/capture-picker-discard.test.tsx b/src/components/layout/__tests__/capture-picker-discard.test.tsx index f701b3a94..a62bfc7b4 100644 --- a/src/components/layout/__tests__/capture-picker-discard.test.tsx +++ b/src/components/layout/__tests__/capture-picker-discard.test.tsx @@ -24,6 +24,11 @@ vi.mock("@/hooks/use-record-capabilities", () => ({ }), })); +// Every module on: this file is about the discard guard, not the module mask. +vi.mock("@/hooks/use-auth", () => ({ + useAuth: () => ({ user: { modules: {} } }), +})); + vi.mock("react", async (importOriginal) => { const actual = await importOriginal(); return { diff --git a/src/components/layout/__tests__/capture-picker.test.tsx b/src/components/layout/__tests__/capture-picker.test.tsx index ecb0d3463..20d5e86af 100644 --- a/src/components/layout/__tests__/capture-picker.test.tsx +++ b/src/components/layout/__tests__/capture-picker.test.tsx @@ -17,6 +17,14 @@ vi.mock("@/hooks/use-record-capabilities", () => ({ }), })); +// The record's resolved module map; each test sets what it needs. +const modulesRef: { value: Record | undefined } = { + value: undefined, +}; +vi.mock("@/hooks/use-auth", () => ({ + useAuth: () => ({ user: { modules: modulesRef.value } }), +})); + vi.mock("@/components/ui/responsive-sheet", () => ({ ResponsiveSheet: ({ open, @@ -51,6 +59,7 @@ function render() { describe(" — offered kinds", () => { it("offers measurement, medication and mood, and no water entry", () => { + modulesRef.value = undefined; const html = render(); expect(html).toContain('data-testid="capture-picker-measurement"'); @@ -61,3 +70,20 @@ describe(" — offered kinds", () => { expect(html).not.toContain('data-testid="capture-picker-water"'); }); }); + +describe(" — switched-off modules", () => { + it("drops Mood with the mood module off and Medication with medications off", () => { + modulesRef.value = { mood: false, medications: false }; + const html = render(); + expect(html).toContain('data-testid="capture-picker-measurement"'); + expect(html).not.toContain('data-testid="capture-picker-mood"'); + expect(html).not.toContain('data-testid="capture-picker-medication"'); + }); + + it("keeps both while their modules are on", () => { + modulesRef.value = { mood: true, medications: true }; + const html = render(); + expect(html).toContain('data-testid="capture-picker-mood"'); + expect(html).toContain('data-testid="capture-picker-medication"'); + }); +}); diff --git a/src/components/layout/capture-picker.tsx b/src/components/layout/capture-picker.tsx index b3415e891..068dd5f46 100644 --- a/src/components/layout/capture-picker.tsx +++ b/src/components/layout/capture-picker.tsx @@ -19,7 +19,9 @@ import { } from "@/components/ui/alert-dialog"; import { sheetBodyHasUnsavedInput } from "@/components/dashboard/quick-entry-sheets"; import { useTranslations } from "@/lib/i18n/context"; +import { isSurfaceVisible, type SurfaceModuleMap } from "@/lib/modules/surface"; import type { ShareDomain } from "@/lib/sharing/scope"; +import { useAuth } from "@/hooks/use-auth"; import { useRecordCapabilities, type RecordCapabilities, @@ -106,15 +108,25 @@ export function admittedCaptureKind( } /** - * The capture kinds to offer, given what the record allows. Exported pure so - * the delegation rule can be pinned without opening a sheet: an SSR test - * cannot tap the button that opens it. + * The capture kinds to offer, given what the record allows and which of its + * modules are on. Exported pure so both rules can be pinned without opening a + * sheet: an SSR test cannot tap the button that opens it. + * + * A kind whose module is off (`capture:` in the surface map) is not + * offered: a Mood entry with the mood module off would post to a route that + * refuses it. An absent module map (the account still loading) offers every + * kind the record allows, the gate's default-on contract. */ export function visibleCaptureKinds( caps: Pick, kinds: ReadonlyArray, + modules?: SurfaceModuleMap | null, ): CaptureKind[] { - return kinds.filter((kind) => caps.canWriteDomain(CAPTURE_KIND_DOMAIN[kind])); + return kinds.filter( + (kind) => + caps.canWriteDomain(CAPTURE_KIND_DOMAIN[kind]) && + isSurfaceVisible(`capture:${kind}`, modules), + ); } interface CapturePickerProps { @@ -127,7 +139,12 @@ interface CapturePickerProps { export function CapturePicker({ open, onOpenChange }: CapturePickerProps) { const { t } = useTranslations(); const capabilities = useRecordCapabilities(); - const offered = visibleCaptureKinds(capabilities, CAPTURE_KIND_ORDER); + const { user } = useAuth(); + const offered = visibleCaptureKinds( + capabilities, + CAPTURE_KIND_ORDER, + user?.modules, + ); const [kind, setKind] = useState(null); // Re-derived on every render, never latched: the offer can shrink under a // sheet that is already open. See `admittedCaptureKind`.