• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

medplum / medplum / 28191490579

25 Jun 2026 06:23PM UTC coverage: 91.859%. Remained the same
28191490579

push

github

web-flow
Add banner when logged in as super admin (#9601)

Signed-off-by: Cody Ebberson <cody@ebberson.com>

21022 of 23937 branches covered (87.82%)

Branch coverage included in aggregate %.

26 of 27 new or added lines in 4 files covered. (96.3%)

37899 of 40206 relevant lines covered (94.26%)

12561.39 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.14
/packages/react/src/AppShell/AppShell.tsx
1
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
2
// SPDX-License-Identifier: Apache-2.0
3
import type { AppShellHeaderConfiguration, AppShellNavbarConfiguration } from '@mantine/core';
4
import { AppShell as MantineAppShell } from '@mantine/core';
5
import { useMedplum, useMedplumProfile } from '@medplum/react-hooks';
6
import type { JSX, ReactNode } from 'react';
7
import { Suspense, useState } from 'react';
8
import { ErrorBoundary } from '../ErrorBoundary/ErrorBoundary';
9
import { Loading } from '../Loading/Loading';
10
import type { AppShellAnnouncement } from './AnnouncementBanners';
11
import { AnnouncementBanners } from './AnnouncementBanners';
12
import classes from './AppShell.module.css';
13
import { Header } from './Header';
14
import type { NavbarMenu } from './Navbar';
15
import { Navbar } from './Navbar';
16

17
const OPEN_WIDTH = 250;
2✔
18
const CLOSED_WIDTH = 59;
2✔
19
const HEADER_HEIGHT = 60;
2✔
20
const ANNOUNCEMENT_HEIGHT = 36;
2✔
21

22
export type { AppShellAnnouncement } from './AnnouncementBanners';
23

24
export interface AppShellProps {
25
  readonly logo: ReactNode;
26
  readonly pathname?: string;
27
  readonly searchParams?: URLSearchParams;
28
  readonly headerSearchDisabled?: boolean;
29
  readonly version?: string;
30
  readonly menus?: NavbarMenu[];
31
  readonly children: ReactNode;
32
  readonly displayAddBookmark?: boolean;
33
  readonly resourceTypeSearchDisabled?: boolean;
34
  readonly notifications?: ReactNode;
35
  readonly announcements?: AppShellAnnouncement[];
36
  readonly layoutVersion?: 'v1' | 'v2';
37
  readonly showLayoutVersionToggle?: boolean;
38
  readonly spotlightPatientsOnly?: boolean;
39
}
40

41
export function AppShell(props: AppShellProps): JSX.Element {
42
  const [navbarOpen, setNavbarOpen] = useState(localStorage['navbarOpen'] === 'true');
18✔
43
  const [layoutVersion] = useState(
18✔
44
    props.layoutVersion ?? (localStorage['appShellLayoutVersion'] as 'v1' | 'v2' | undefined) ?? 'v1'
18!
45
  );
46
  const medplum = useMedplum();
18✔
47
  const profile = useMedplumProfile();
18✔
48
  const [dismissedAnnouncementIds, setDismissedAnnouncementIds] = useState<ReadonlySet<string>>(() => {
18✔
49
    try {
9✔
50
      const dismissed = localStorage['appShellDismissedAnnouncements'];
9✔
51
      return new Set(dismissed ? JSON.parse(dismissed) : []);
9!
52
    } catch (_err) {
NEW
53
      return new Set();
×
54
    }
55
  });
56

57
  const visibleAnnouncements = props.announcements?.filter(
18✔
58
    (announcement) => !announcement.id || !dismissedAnnouncementIds.has(announcement.id)
3✔
59
  );
60
  const announcementHeight = visibleAnnouncements?.length ? visibleAnnouncements.length * ANNOUNCEMENT_HEIGHT : 0;
18✔
61

62
  function setNavbarOpenWrapper(open: boolean): void {
63
    localStorage['navbarOpen'] = open.toString();
8✔
64
    setNavbarOpen(open);
8✔
65
  }
66

67
  function dismissAnnouncement(announcement: AppShellAnnouncement): void {
68
    if (announcement.id) {
1!
69
      setDismissedAnnouncementIds((dismissedIds) => {
1✔
70
        const updated = new Set(dismissedIds);
1✔
71
        updated.add(announcement.id as string);
1✔
72
        localStorage['appShellDismissedAnnouncements'] = JSON.stringify([...updated]);
1✔
73
        return updated;
1✔
74
      });
75
    }
76
    announcement.onDismiss?.(announcement);
1✔
77
  }
78

79
  function closeNavbar(): void {
80
    setNavbarOpenWrapper(false);
×
81
  }
82

83
  function toggleNavbar(): void {
84
    setNavbarOpenWrapper(!navbarOpen);
8✔
85
  }
86

87
  if (medplum.isLoading()) {
18!
88
    return <Loading />;
×
89
  }
90

91
  let headerProp: AppShellHeaderConfiguration | undefined;
92
  let navbarProp: AppShellNavbarConfiguration | undefined;
93
  let headerComponent: ReactNode | undefined;
94
  let navbarComponent: ReactNode | undefined;
95

96
  if (layoutVersion === 'v2') {
18✔
97
    // Layout version v2:
98
    // - No header
99
    // - Navbar is either open or closed based on state
100
    headerProp = { height: announcementHeight };
10✔
101
    navbarProp = {
10✔
102
      width: navbarOpen ? OPEN_WIDTH : CLOSED_WIDTH,
10✔
103
      breakpoint: 0,
104
      collapsed: {
105
        desktop: !profile,
106
        mobile: !profile,
107
      },
108
    };
109
    headerComponent = visibleAnnouncements?.length ? (
10✔
110
      <MantineAppShell.Header style={{ zIndex: 101 }}>
111
        <AnnouncementBanners announcements={visibleAnnouncements} onDismiss={dismissAnnouncement} />
112
      </MantineAppShell.Header>
113
    ) : undefined;
114
    navbarComponent = profile ? (
10!
115
      <Navbar
116
        logo={props.logo}
117
        pathname={props.pathname}
118
        searchParams={props.searchParams}
119
        menus={props.menus}
120
        navbarToggle={toggleNavbar}
121
        closeNavbar={closeNavbar}
122
        displayAddBookmark={props.displayAddBookmark}
123
        resourceTypeSearchDisabled={true}
124
        opened={navbarOpen}
125
        spotlightEnabled={true}
126
        patientsOnly={props.spotlightPatientsOnly}
127
        userMenuEnabled={true}
128
        version={props.version}
129
        showLayoutVersionToggle={props.showLayoutVersionToggle}
130
      />
131
    ) : undefined;
132
  } else {
133
    // Default to layout version v1
134
    headerProp = { height: HEADER_HEIGHT + announcementHeight };
8✔
135
    navbarProp = {
8✔
136
      width: OPEN_WIDTH,
137
      breakpoint: 'sm',
138
      collapsed: {
139
        desktop: !profile || !navbarOpen,
16✔
140
        mobile: !profile || !navbarOpen,
16✔
141
      },
142
    };
143
    headerComponent = profile && (
8✔
144
      <Header
145
        pathname={props.pathname}
146
        searchParams={props.searchParams}
147
        headerSearchDisabled={props.headerSearchDisabled}
148
        logo={props.logo}
149
        version={props.version}
150
        navbarOpen={navbarOpen}
151
        navbarToggle={toggleNavbar}
152
        notifications={props.notifications}
153
        announcements={visibleAnnouncements}
154
        onDismissAnnouncement={dismissAnnouncement}
155
      />
156
    );
157
    navbarComponent =
8✔
158
      profile && navbarOpen ? (
24✔
159
        <Navbar
160
          pathname={props.pathname}
161
          searchParams={props.searchParams}
162
          menus={props.menus}
163
          navbarToggle={toggleNavbar}
164
          closeNavbar={closeNavbar}
165
          displayAddBookmark={props.displayAddBookmark}
166
          resourceTypeSearchDisabled={props.resourceTypeSearchDisabled}
167
          patientsOnly={props.spotlightPatientsOnly}
168
        />
169
      ) : undefined;
170
  }
171

172
  return (
18✔
173
    <MantineAppShell header={headerProp} navbar={navbarProp} padding={0}>
174
      {headerComponent}
175
      {navbarComponent}
176
      <MantineAppShell.Main className={classes.main}>
177
        <ErrorBoundary>
178
          <Suspense fallback={<Loading />}>{props.children}</Suspense>
179
        </ErrorBoundary>
180
      </MantineAppShell.Main>
181
    </MantineAppShell>
182
  );
183
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc