diff --git a/src/__tests__/shared-record-navigation.test.tsx b/src/__tests__/shared-record-navigation.test.tsx index 8610e31de..93ac58b02 100644 --- a/src/__tests__/shared-record-navigation.test.tsx +++ b/src/__tests__/shared-record-navigation.test.tsx @@ -88,19 +88,43 @@ describe("shared-record navigation", () => { ).toEqual(["/checkups"]); }); - it("uses the active record scope instead of the actor's module flags", () => { - const actorModules = { - medications: false, - labs: false, - mood: false, - mentalHealth: false, - cycle: false, - illness: false, - inboundDocuments: false, - }; + /** + * Scope and modules are two filters and they answer different questions. + * + * This case used to assert that the module map was IGNORED inside a shared + * record, and the reason was sound at the time: the map `GET /api/auth/me` + * published was the ACTOR's, which describes their own dashboard, so letting + * it hide a domain the record had granted would have been the actor's + * preference deciding somebody else's navigation. + * + * v1.38.14 (#939) removed the premise rather than the filter. The payload + * now resolves the map for the RECORD the session is inside, so the map is + * the record's own answer to "what does this record track" — and honouring + * it is what makes a guardian's Modules toggle visible inside the profile it + * was set for. The scope still decides which doors the grant opens; the map + * decides which of them the record uses at all. + */ + it("applies the record's own module map on top of its scope", () => { + const scope = ["medications", "labs"] as const; expect( - visibleNavDestinations(actorModules, true, true, ["medications"]).map( + visibleNavDestinations({}, true, true, [...scope]).map( + (destination) => destination.href, + ), + ).toEqual(["/medications", "/labs"]); + + // The record tracks medications and not labs. Both are in scope; one door + // is offered. + expect( + visibleNavDestinations({ labs: false }, true, true, [...scope]).map( + (destination) => destination.href, + ), + ).toEqual(["/medications"]); + + // And the scope still binds: a module the record tracks is not a door the + // grant opens. + expect( + visibleNavDestinations({}, true, true, ["medications"]).map( (destination) => destination.href, ), ).toEqual(["/medications"]); diff --git a/src/components/layout/__tests__/nav-model-shared-record.test.ts b/src/components/layout/__tests__/nav-model-shared-record.test.ts index 431106898..9da920c9e 100644 --- a/src/components/layout/__tests__/nav-model-shared-record.test.ts +++ b/src/components/layout/__tests__/nav-model-shared-record.test.ts @@ -89,6 +89,32 @@ describe("the destination list under a switch", () => { }); }); +describe("a module the RECORD does not track", () => { + /** + * #939 — the map `GET /api/auth/me` publishes is the RECORD's while a + * session is switched into one, so this filter is what makes a guardian's + * toggle visible. Before it, turning Cycle off for a profile left the Cycle + * door standing in that profile's own navigation, and the toggle wrote a + * column nothing on screen was reading. + */ + it("drops its destination inside the record", () => { + const withCycle = visibleNavDestinations(ALL_MODULES, true, true, null).map( + (d) => d.href, + ); + expect(withCycle).toContain("/cycle"); + + const withoutCycle = visibleNavDestinations( + { ...ALL_MODULES, cycle: false }, + true, + true, + null, + ).map((d) => d.href); + expect(withoutCycle).not.toContain("/cycle"); + // Only that one door: a module toggle is not a scope change. + expect(withoutCycle).toContain("/measurements"); + }); +}); + describe("the utility tail under a switch", () => { it("disappears entirely", () => { // Settings holds credentials, integrations, notification channels and diff --git a/src/components/layout/nav-model.ts b/src/components/layout/nav-model.ts index cc3059dcb..300dcec0e 100644 --- a/src/components/layout/nav-model.ts +++ b/src/components/layout/nav-model.ts @@ -342,13 +342,22 @@ function isNavDestinationVisible( ) { return false; } - // A shared record's server-resolved scope decides which health-domain doors - // exist. The actor's module preferences describe their own dashboard and - // must not hide a domain the target record explicitly granted. - if (sharedRecord) return true; - if (d.sharedRecordOnly) return false; + // The inverse marker: a destination that exists ONLY inside somebody else's + // record (the read-only profile summary). Guarded on `!sharedRecord` rather + // than reached only through the shared arm, which is what it relied on + // before the module gate below started applying to a shared record too. + if (!sharedRecord && d.sharedRecordOnly) return false; if (!d.requiresModule) return true; if (!mounted) return false; + // The module map, and whose it is, is the whole point. A shared record used + // to skip this line: the grant's scope decided which doors existed, and the + // module map was the ACTOR's, which describes their own dashboard and must + // not hide a domain the record granted. `GET /api/auth/me` now resolves the + // map for the RECORD the session is inside (#939), so the objection is gone + // and the line is the one that makes a guardian's toggle visible — without + // it, turning Cycle off for a profile left the Cycle door standing in that + // profile's own navigation. Scope decides which doors the grant opens; + // this decides which of them the record tracks at all. return modules?.[d.requiresModule] !== false; }