mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
feat(nav): gate the Insights destination on the insights module
Mark the top-level `/insights` nav destination `requiresModule: "insights"` so it drops from both bars when the account has the insights module off, mirroring mood / labs / coach / achievements. The bottom-nav pinned `/insights` as a fixed primary slot regardless of the module map; it now filters that slot through the same per-user map the More hub honours (fail-open on a missing key / unloaded /me payload). A disabled insights module is hidden everywhere — the primary-slot filter keeps it out of the More hub too, so it is hidden, not relocated. Extend the nav-model + bottom-nav tests with insights on/off coverage.
This commit is contained in:
1 parent
4cf923647d
commit
b2684a2e21
4 files changed
+58
-4
No files matched your search
@@ -150,6 +150,27 @@ describe("<BottomNav> iOS-parity layout", () => {
|
||||
mockUserRef.value = { id: "u1", modules: {} };
|
||||
});
|
||||
|
||||
it("drops the Insights primary slot when the insights module is off (v1.18.0)", () => {
|
||||
// The Insights slot is a fixed primary anchor, but it is now
|
||||
// module-gated: a disabled insights module must remove it from the
|
||||
// strip (and it is excluded from the More hub by the primary-slot
|
||||
// filter, so the module is hidden everywhere, not relocated).
|
||||
mockUserRef.value = { id: "u1", modules: { insights: false } };
|
||||
const html = render();
|
||||
expect(html).not.toContain('href="/insights"');
|
||||
// Sibling primary slots are unaffected.
|
||||
expect(html).toContain('href="/"');
|
||||
expect(html).toContain('href="/medications"');
|
||||
mockUserRef.value = { id: "u1", modules: {} };
|
||||
});
|
||||
|
||||
it("keeps the Insights primary slot when the insights module is on", () => {
|
||||
mockUserRef.value = { id: "u1", modules: { insights: true } };
|
||||
const html = render();
|
||||
expect(html).toContain('href="/insights"');
|
||||
mockUserRef.value = { id: "u1", modules: {} };
|
||||
});
|
||||
|
||||
it("gating on a disabled module never throws (v1.18.0)", () => {
|
||||
mockUserRef.value = {
|
||||
id: "u1",
|
||||
|
||||
@@ -115,17 +115,21 @@ describe("visibleNavDestinations module gate", () => {
|
||||
expect(on.indexOf("/cycle")).toBeLessThan(on.indexOf("/insights"));
|
||||
});
|
||||
|
||||
it("drops a module-gated entry (mood / labs / coach / achievements) when its module is disabled", () => {
|
||||
it("drops a module-gated entry (mood / labs / coach / achievements / insights) when its module is disabled", () => {
|
||||
const disabled = visibleNavDestinations({
|
||||
mood: false,
|
||||
labs: false,
|
||||
coach: false,
|
||||
achievements: false,
|
||||
// v1.18.0 — Insights is now `requiresModule: "insights"`, so the
|
||||
// top-level /insights entry drops when the module is off.
|
||||
insights: false,
|
||||
}).map((d) => d.href);
|
||||
expect(disabled).not.toContain("/mood");
|
||||
expect(disabled).not.toContain("/labs");
|
||||
expect(disabled).not.toContain("/coach");
|
||||
expect(disabled).not.toContain("/achievements");
|
||||
expect(disabled).not.toContain("/insights");
|
||||
});
|
||||
|
||||
it("keeps every module-gated entry when its module is enabled", () => {
|
||||
@@ -134,11 +138,13 @@ describe("visibleNavDestinations module gate", () => {
|
||||
labs: true,
|
||||
coach: true,
|
||||
achievements: true,
|
||||
insights: true,
|
||||
}).map((d) => d.href);
|
||||
expect(enabled).toContain("/mood");
|
||||
expect(enabled).toContain("/labs");
|
||||
expect(enabled).toContain("/coach");
|
||||
expect(enabled).toContain("/achievements");
|
||||
expect(enabled).toContain("/insights");
|
||||
});
|
||||
|
||||
it("fails open: a missing key or an undefined map keeps the gated entry", () => {
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
isNavDestinationActive,
|
||||
mobileMoreHubDestinations,
|
||||
} from "@/components/layout/nav-model";
|
||||
import type { ModuleKey } from "@/lib/modules/registry";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useAppSettings } from "@/components/app-settings-provider";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
@@ -57,8 +58,21 @@ const PRIMARY_LEFT: ReadonlyArray<NavLink> = [
|
||||
{ href: "/medications", tKey: "nav.medications", icon: Pill },
|
||||
];
|
||||
|
||||
const PRIMARY_RIGHT: ReadonlyArray<NavLink> = [
|
||||
{ href: "/insights", tKey: "nav.insights", icon: Lightbulb },
|
||||
// v1.18.0 — the Insights primary slot is module-gated. Insights is now a
|
||||
// toggleable module (`requiresModule: "insights"` in the shared nav model),
|
||||
// so the fixed slot must respect the same per-user map the More hub already
|
||||
// honours rather than pinning a destination the account turned off. When
|
||||
// insights is disabled the slot is dropped (it is excluded from the More hub
|
||||
// by `BOTTOM_NAV_PRIMARY_SLOT_HREFS`, so a disabled module is hidden
|
||||
// everywhere, not relocated). Fail-open: a missing key / unloaded map keeps
|
||||
// the slot, mirroring the gate's default-on contract.
|
||||
const PRIMARY_RIGHT: ReadonlyArray<NavLink & { requiresModule?: ModuleKey }> = [
|
||||
{
|
||||
href: "/insights",
|
||||
tKey: "nav.insights",
|
||||
icon: Lightbulb,
|
||||
requiresModule: "insights",
|
||||
},
|
||||
];
|
||||
|
||||
export function BottomNav() {
|
||||
@@ -86,6 +100,18 @@ export function BottomNav() {
|
||||
[user?.modules, bugReportEnabled],
|
||||
);
|
||||
|
||||
// v1.18.0 — drop a module-gated primary slot (Insights) when the account
|
||||
// has that module disabled. Fail-open: a missing key / unloaded map keeps
|
||||
// the slot, mirroring the gate + More-hub default-on contract.
|
||||
const primaryRight = useMemo(
|
||||
() =>
|
||||
PRIMARY_RIGHT.filter(
|
||||
(item) =>
|
||||
!item.requiresModule || user?.modules?.[item.requiresModule] !== false,
|
||||
),
|
||||
[user?.modules],
|
||||
);
|
||||
|
||||
function isActiveLink(href: string) {
|
||||
return isNavDestinationActive(href, pathname);
|
||||
}
|
||||
@@ -160,7 +186,7 @@ export function BottomNav() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{PRIMARY_RIGHT.map(renderPrimary)}
|
||||
{primaryRight.map(renderPrimary)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -118,6 +118,7 @@ export const NAV_DESTINATIONS: ReadonlyArray<NavDestination> = [
|
||||
tKey: "nav.insights",
|
||||
icon: Lightbulb,
|
||||
tourId: "nav-insights",
|
||||
requiresModule: "insights",
|
||||
},
|
||||
// v1.17.1 (F-3) — the Coach finally gets a single labeled nav home. It
|
||||
// was reachable from seven scattered entry points (FAB, hero CTA, empty
|
||||
|
||||
Reference in new issue
Block a user