fix(capture): leave Mood and Medication out of the add menu with their modules off

This commit is contained in:
Marc-André Bombeck committed 2026-09-24 09:48:05 +02:00
1 parent 94e07d0d90
commit a4f8e6db27
3 files changed
+53 -5

No files matched your search

@@ -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<typeof ReactModule>();
return {
@@ -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<string, boolean> | 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("<CapturePicker> — 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("<CapturePicker> — offered kinds", () => {
expect(html).not.toContain('data-testid="capture-picker-water"');
});
});
describe("<CapturePicker> — 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"');
});
});
+22 -5
View File
@@ -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:<kind>` 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<RecordCapabilities, "canWriteDomain">,
kinds: ReadonlyArray<CaptureKind>,
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<CaptureKind | null>(null);
// Re-derived on every render, never latched: the offer can shrink under a
// sheet that is already open. See `admittedCaptureKind`.