Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/organisation/systems/app-experience.json
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@
"tests/services-tap-floor.test.ts",
"tests/settings-*",
"tests/shared-*",
"tests/side-menu-leave-guard.dom.test.tsx",
"tests/sidebar-*",
"tests/signed-*",
"tests/smart-search-intent.test.ts",
Expand Down
14 changes: 1 addition & 13 deletions src/components/clinical-dashboard/ClinicalSidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"use client";

import dynamic from "next/dynamic";
import {
useEffect,
useRef,
Expand Down Expand Up @@ -61,25 +60,14 @@ import {
ClinicalMenuWorkAreas,
clinicalMenuTabId,
} from "@/components/clinical-dashboard/clinical-menu-drawer";
import { LazyWorkSideCounts } from "@/components/work-frame/lazy-work-side-counts";
import { useWorkSideCounts } from "@/components/work-frame/work-frame-store";
import { useNewWorkMode } from "@/components/work-mode-launch/work-mode-launch-provider";
import { needsYouBellVisibleForAuth } from "@/lib/needs-you/homes";
import type { ModeMenuSideId } from "@/lib/phone-mode-groups";
import { useAuthSession } from "@/lib/supabase/client";
import type { WorkAreaId } from "@/lib/work-frame/areas";

// The counts reader is loaded straight from its own file. Going through
// work-frame/lazy-work-side-nav would pull in the two-pane menu, which imports
// this file back and makes an import cycle.
const LazyCountsReader = dynamic(
() => import("@/components/work-frame/work-side-counts-reader").then((module) => module.WorkSideCountsReader),
{ ssr: false },
);

function LazyWorkSideCounts({ active }: { readonly active: boolean }) {
return active ? <LazyCountsReader /> : null;
}

export type SidebarIdentity = {
displayName: string;
initials: string;
Expand Down
15 changes: 15 additions & 0 deletions src/components/work-frame/lazy-work-side-counts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"use client";

import dynamic from "next/dynamic";

// Its own leaf module: the clinical sidebar imports it, so it must not pull in the
// side menus (two-pane-side-menu imports the clinical sidebar back).
const LazyCounts = dynamic(
() => import("@/components/work-frame/work-side-counts-reader").then((module) => module.WorkSideCountsReader),
{ ssr: false },
);

/** The side menu and rail's counts, read only while one of them is on screen. */
export function LazyWorkSideCounts({ active }: { readonly active: boolean }) {
return active ? <LazyCounts /> : null;
}
12 changes: 3 additions & 9 deletions src/components/work-frame/lazy-work-side-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,15 @@
import dynamic from "next/dynamic";
import { useState, type ComponentProps } from "react";

import { LazyWorkSideCounts } from "@/components/work-frame/lazy-work-side-counts";

const loadWorkSideMenu = () => import("@/components/work-frame/work-side-menu").then((module) => module.WorkSideMenu);
const LazyMenu = dynamic(loadWorkSideMenu, { ssr: false });

const loadTwoPaneSideMenu = () =>
import("@/components/work-frame/two-pane-side-menu").then((module) => module.TwoPaneSideMenu);
const LazyTwoPane = dynamic(loadTwoPaneSideMenu, { ssr: false });

const LazyCounts = dynamic(
() => import("@/components/work-frame/work-side-counts-reader").then((module) => module.WorkSideCountsReader),
{ ssr: false },
);

/** Warms the side menu's chunk when the menu button is about to be pressed. */
export function prefetchWorkSideMenu() {
void loadWorkSideMenu();
Expand Down Expand Up @@ -42,10 +39,7 @@ export function LazyTwoPaneSideMenu(props: ComponentProps<typeof LazyTwoPane>) {
return opened ? <LazyTwoPane {...props} /> : null;
}

/** The side menu and rail's counts, read only while one of them is on screen. */
export function LazyWorkSideCounts({ active }: { readonly active: boolean }) {
return active ? <LazyCounts /> : null;
}
export { LazyWorkSideCounts };

/**
* The two-pane side menu with its own counts reader, for a page that has no
Expand Down
2 changes: 1 addition & 1 deletion src/components/work-frame/two-pane-side-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -239,8 +239,8 @@ export function TwoPaneSideMenu({
const visibleAreas = workAvailable ? WORK_SIDE_AREAS.filter((entry) => routeVisible(entry.href)) : [];

const openReminders = () => {
if (!runReminders()) return;
close();
runReminders();
};

const needle = find.trim().toLowerCase();
Expand Down
11 changes: 9 additions & 2 deletions src/components/work-frame/two-pane-side-strip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { appModeIcons } from "@/lib/app-mode-icons";
import { BRAND_NAME } from "@/lib/brand";
import type { ModeMenuSideId } from "@/lib/phone-mode-groups";
import type { ThemePreference } from "@/lib/theme";
import { confirmShellNavigation } from "@/lib/navigation-guards";

/**
* The strip shared by the two-pane side menu and its tablet rail. On a phone the strip lives inside the menu. From 768 px the same
Expand Down Expand Up @@ -61,11 +62,17 @@ export function useThemeCycle() {
}

/** Opens Reminders. Only a page that offers it can open it; anywhere else goes to My Day first. */
export function useOpenReminders(): () => void {
export function useOpenReminders(): () => boolean {
const router = useRouter();
return () => {
if (!workFrameActionHandler("my-day-reminders")) router.push("/my-day");
const handled = workFrameActionHandler("my-day-reminders");
if (!handled) {
// An unsaved page (the safety plan) may keep the reader where they are.
if (!confirmShellNavigation()) return false;
router.push("/my-day");
}
requestWorkFrameAction("my-day-reminders");
return true;
};
}

Expand Down
7 changes: 5 additions & 2 deletions src/components/work-frame/work-side-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { appModeIcons } from "@/lib/app-mode-icons";
import { BRAND_NAME } from "@/lib/brand";
import type { ModeMenuSideId } from "@/lib/phone-mode-groups";
import { WORK_AREAS, type WorkAreaId } from "@/lib/work-frame/areas";
import { confirmShellNavigation } from "@/lib/navigation-guards";
import {
WORK_SIDE_AREAS,
WORK_SIDE_NOTIFICATIONS_HREF,
Expand Down Expand Up @@ -83,9 +84,11 @@ export function WorkSideMenu({
const close = () => onOpenChange(false);

const openReminders = () => {
close();
// Only a page that offers Reminders can open it; anywhere else goes to My Day first.
if (!workFrameActionHandler("my-day-reminders")) router.push("/my-day");
const handled = workFrameActionHandler("my-day-reminders");
if (!handled && !confirmShellNavigation()) return;
close();
if (!handled) router.push("/my-day");
requestWorkFrameAction("my-day-reminders");
};

Expand Down
34 changes: 34 additions & 0 deletions tests/side-menu-leave-guard.dom.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/** @vitest-environment jsdom */
Comment thread
BigSimmo marked this conversation as resolved.
Resolved

import { act, renderHook } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";

import { useOpenReminders } from "@/components/work-frame/two-pane-side-strip";
import { registerNavigationGuard } from "@/lib/navigation-guards";

const router = vi.hoisted(() => ({ push: vi.fn() }));

vi.mock("next/navigation", () => ({
useRouter: () => router,
usePathname: () => "/psychiatry/safety-plan",
}));

afterEach(() => {
router.push.mockReset();
});

describe("side menu Reminders shortcut", () => {
it("stays on a page with unsaved work when the reader says stay", () => {
const remove = registerNavigationGuard(() => false);
const { result } = renderHook(() => useOpenReminders());
act(() => result.current());
expect(router.push).not.toHaveBeenCalled();
remove();
});

it("goes to My Day when nothing holds the reader", () => {
const { result } = renderHook(() => useOpenReminders());
act(() => result.current());
expect(router.push).toHaveBeenCalledWith("/my-day");
});
});
Loading