@digihmis/esm-primary-navigation-app 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/README.md +43 -0
  2. package/__mocks__/mock-session.ts +23 -0
  3. package/__mocks__/mock-user.ts +23 -0
  4. package/__mocks__/openmrs-esm-framework.mock.tsx +30 -0
  5. package/__mocks__/react-i18next.mock.js +49 -0
  6. package/dist/1480.js +1 -0
  7. package/dist/1480.js.map +1 -0
  8. package/dist/1646.js +1 -0
  9. package/dist/1646.js.map +1 -0
  10. package/dist/1716.js +1 -0
  11. package/dist/1716.js.map +1 -0
  12. package/dist/1797.js +1 -0
  13. package/dist/1797.js.map +1 -0
  14. package/dist/1869.js +1 -0
  15. package/dist/1869.js.map +1 -0
  16. package/dist/1877.js +1 -0
  17. package/dist/1877.js.map +1 -0
  18. package/dist/2317.js +1 -0
  19. package/dist/2317.js.map +1 -0
  20. package/dist/2416.js +1 -0
  21. package/dist/2416.js.map +1 -0
  22. package/dist/2711.js +1 -0
  23. package/dist/2711.js.map +1 -0
  24. package/dist/282.js +1 -0
  25. package/dist/282.js.map +1 -0
  26. package/dist/2881.js +1 -0
  27. package/dist/2881.js.map +1 -0
  28. package/dist/2939.js +15 -0
  29. package/dist/2939.js.map +1 -0
  30. package/dist/3378.js +1 -0
  31. package/dist/3378.js.map +1 -0
  32. package/dist/3963.js +1 -0
  33. package/dist/3963.js.map +1 -0
  34. package/dist/4106.js +1 -0
  35. package/dist/4106.js.map +1 -0
  36. package/dist/4111.js +1 -0
  37. package/dist/4111.js.map +1 -0
  38. package/dist/434.js +1 -0
  39. package/dist/434.js.map +1 -0
  40. package/dist/4348.js +1 -0
  41. package/dist/4348.js.map +1 -0
  42. package/dist/4383.js +1 -0
  43. package/dist/4383.js.map +1 -0
  44. package/dist/4658.js +1 -0
  45. package/dist/4658.js.map +1 -0
  46. package/dist/466.js +1 -0
  47. package/dist/466.js.map +1 -0
  48. package/dist/4928.js +1 -0
  49. package/dist/4928.js.map +1 -0
  50. package/dist/5117.js +1 -0
  51. package/dist/5117.js.map +1 -0
  52. package/dist/5132.js +1 -0
  53. package/dist/5132.js.map +1 -0
  54. package/dist/5145.js +1 -0
  55. package/dist/5145.js.map +1 -0
  56. package/dist/5503.js +1 -0
  57. package/dist/5503.js.map +1 -0
  58. package/dist/556.js +1 -0
  59. package/dist/556.js.map +1 -0
  60. package/dist/5644.js +1 -0
  61. package/dist/5644.js.map +1 -0
  62. package/dist/5867.js +12 -0
  63. package/dist/5867.js.map +1 -0
  64. package/dist/5940.js +1 -0
  65. package/dist/5940.js.map +1 -0
  66. package/dist/6047.js +1 -0
  67. package/dist/6047.js.map +1 -0
  68. package/dist/6371.js +1 -0
  69. package/dist/6371.js.map +1 -0
  70. package/dist/6377.js +1 -0
  71. package/dist/6377.js.map +1 -0
  72. package/dist/6444.js +1 -0
  73. package/dist/6444.js.map +1 -0
  74. package/dist/6508.js +1 -0
  75. package/dist/6508.js.map +1 -0
  76. package/dist/6724.js +1 -0
  77. package/dist/6724.js.map +1 -0
  78. package/dist/689.js +1 -0
  79. package/dist/689.js.map +1 -0
  80. package/dist/6904.js +1 -0
  81. package/dist/6904.js.map +1 -0
  82. package/dist/7044.js +1 -0
  83. package/dist/7044.js.map +1 -0
  84. package/dist/7045.js +1 -0
  85. package/dist/7045.js.map +1 -0
  86. package/dist/7175.js +1 -0
  87. package/dist/7175.js.map +1 -0
  88. package/dist/7182.js +1 -0
  89. package/dist/7182.js.map +1 -0
  90. package/dist/7302.js +1 -0
  91. package/dist/7302.js.map +1 -0
  92. package/dist/7587.js +1 -0
  93. package/dist/7587.js.map +1 -0
  94. package/dist/7742.js +1 -0
  95. package/dist/7742.js.map +1 -0
  96. package/dist/7786.js +1 -0
  97. package/dist/7786.js.map +1 -0
  98. package/dist/7912.js +1 -0
  99. package/dist/7912.js.map +1 -0
  100. package/dist/8349.js +1 -0
  101. package/dist/8349.js.map +1 -0
  102. package/dist/8358.js +1 -0
  103. package/dist/8358.js.map +1 -0
  104. package/dist/8359.js +1 -0
  105. package/dist/8359.js.map +1 -0
  106. package/dist/8695.js +1 -0
  107. package/dist/8695.js.map +1 -0
  108. package/dist/8924.js +1 -0
  109. package/dist/8924.js.map +1 -0
  110. package/dist/903.js +1 -0
  111. package/dist/903.js.map +1 -0
  112. package/dist/9072.js +1 -0
  113. package/dist/9072.js.map +1 -0
  114. package/dist/9105.js +1 -0
  115. package/dist/9105.js.map +1 -0
  116. package/dist/9712.js +1 -0
  117. package/dist/9712.js.map +1 -0
  118. package/dist/9806.js +1 -0
  119. package/dist/9806.js.map +1 -0
  120. package/dist/digihmis-esm-primary-navigation-app.js +6 -0
  121. package/dist/digihmis-esm-primary-navigation-app.js.buildmanifest.json +1542 -0
  122. package/dist/digihmis-esm-primary-navigation-app.js.map +1 -0
  123. package/dist/main.js +6 -0
  124. package/dist/main.js.map +1 -0
  125. package/dist/routes.json +1 -0
  126. package/jest.config.js +18 -0
  127. package/package.json +68 -0
  128. package/rspack.config.js +1 -0
  129. package/src/components/change-language/change-language-link.extension.tsx +37 -0
  130. package/src/components/change-language/change-language-link.scss +14 -0
  131. package/src/components/change-language/change-language.modal.tsx +115 -0
  132. package/src/components/change-language/change-language.resource.ts +35 -0
  133. package/src/components/change-language/change-language.scss +59 -0
  134. package/src/components/change-language/change-language.test.tsx +107 -0
  135. package/src/components/dashboard/dashboard.component.tsx +104 -0
  136. package/src/components/generic-link/generic-link.component.tsx +25 -0
  137. package/src/components/logo/logo.component.tsx +28 -0
  138. package/src/components/logo/logo.scss +11 -0
  139. package/src/components/logo/logo.test.tsx +79 -0
  140. package/src/components/nav-group/nav-group.component.tsx +52 -0
  141. package/src/components/navbar/app-menu-button.component.tsx +43 -0
  142. package/src/components/navbar/navbar.component.tsx +123 -0
  143. package/src/components/navbar/navbar.scss +51 -0
  144. package/src/components/navbar/types.ts +5 -0
  145. package/src/components/navbar/user-menu-button.component.tsx +44 -0
  146. package/src/components/navbar-header-panels/app-menu-panel.component.tsx +48 -0
  147. package/src/components/navbar-header-panels/app-menu-panel.scss +43 -0
  148. package/src/components/navbar-header-panels/notifications-menu-panel.component.tsx +23 -0
  149. package/src/components/navbar-header-panels/notifications-menu-panel.test.tsx +9 -0
  150. package/src/components/navbar-header-panels/notifications-menu.panel.scss +9 -0
  151. package/src/components/navbar-header-panels/side-menu-panel.component.tsx +37 -0
  152. package/src/components/navbar-header-panels/user-menu-panel.component.tsx +32 -0
  153. package/src/components/navbar-header-panels/user-menu-panel.scss +65 -0
  154. package/src/components/user-panel-switcher-item/user-panel-switcher.component.tsx +15 -0
  155. package/src/components/user-panel-switcher-item/user-panel-switcher.test.tsx +22 -0
  156. package/src/config-schema.ts +56 -0
  157. package/src/constants.ts +2 -0
  158. package/src/declarations.d.ts +1 -0
  159. package/src/index.ts +68 -0
  160. package/src/root.component.test.tsx +57 -0
  161. package/src/root.component.tsx +25 -0
  162. package/src/root.resource.tsx +13 -0
  163. package/src/root.scss +5 -0
  164. package/src/routes.json +78 -0
  165. package/src/setupTests.ts +7 -0
  166. package/src/utils.ts +3 -0
  167. package/translations/am.json +14 -0
  168. package/translations/ar.json +14 -0
  169. package/translations/ar_SY.json +14 -0
  170. package/translations/bn.json +14 -0
  171. package/translations/cs.json +14 -0
  172. package/translations/de.json +14 -0
  173. package/translations/en.json +14 -0
  174. package/translations/en_US.json +14 -0
  175. package/translations/es.json +14 -0
  176. package/translations/es_MX.json +14 -0
  177. package/translations/fr.json +14 -0
  178. package/translations/he.json +14 -0
  179. package/translations/hi.json +14 -0
  180. package/translations/hi_IN.json +14 -0
  181. package/translations/id.json +14 -0
  182. package/translations/it.json +14 -0
  183. package/translations/ka.json +14 -0
  184. package/translations/km.json +14 -0
  185. package/translations/ku.json +14 -0
  186. package/translations/ky.json +14 -0
  187. package/translations/lg.json +14 -0
  188. package/translations/ne.json +14 -0
  189. package/translations/pl.json +14 -0
  190. package/translations/pt.json +14 -0
  191. package/translations/pt_BR.json +14 -0
  192. package/translations/qu.json +14 -0
  193. package/translations/ro_RO.json +14 -0
  194. package/translations/ru_RU.json +14 -0
  195. package/translations/si.json +14 -0
  196. package/translations/sq.json +14 -0
  197. package/translations/sw.json +12 -0
  198. package/translations/sw_KE.json +14 -0
  199. package/translations/tr.json +14 -0
  200. package/translations/tr_TR.json +14 -0
  201. package/translations/uk.json +14 -0
  202. package/translations/uz.json +14 -0
  203. package/translations/uz@Latn.json +14 -0
  204. package/translations/uz_UZ.json +14 -0
  205. package/translations/vi.json +14 -0
  206. package/translations/zh.json +14 -0
  207. package/translations/zh_CN.json +14 -0
  208. package/translations/zh_TW.json +14 -0
  209. package/tsconfig.json +24 -0
@@ -0,0 +1,123 @@
1
+ import { Header, HeaderContainer, HeaderGlobalBar, HeaderMenuButton } from '@carbon/react';
2
+ import {
3
+ ConfigurableLink,
4
+ ExtensionSlot,
5
+ useAssignedExtensions,
6
+ useConfig,
7
+ useLayoutType,
8
+ useLeftNavStore,
9
+ useSession,
10
+ } from '@openmrs/esm-framework';
11
+ import React, { useCallback, useMemo, useState } from 'react';
12
+ import { Navigate } from 'react-router-dom';
13
+ import { isDesktop } from '../../utils';
14
+ import Logo from '../logo/logo.component';
15
+ import NotificationsMenuPanel from '../navbar-header-panels/notifications-menu-panel.component';
16
+ import SideMenuPanel from '../navbar-header-panels/side-menu-panel.component';
17
+ import styles from './navbar.scss';
18
+
19
+ const HeaderItems: React.FC = () => {
20
+ const config = useConfig();
21
+ const [activeHeaderPanel, setActiveHeaderPanel] = useState<string>(null);
22
+ const layout = useLayoutType();
23
+ const { slotName, mode } = useLeftNavStore();
24
+ const navMenuItems = useAssignedExtensions(slotName);
25
+ const isActivePanel = useCallback((panelName: string) => activeHeaderPanel === panelName, [activeHeaderPanel]);
26
+
27
+ const togglePanel = useCallback((panelName: string) => {
28
+ setActiveHeaderPanel((activeHeaderPanel) => (activeHeaderPanel === panelName ? null : panelName));
29
+ }, []);
30
+
31
+ const hidePanel = useCallback(
32
+ (panelName: string) => () => {
33
+ setActiveHeaderPanel((activeHeaderPanel) => (activeHeaderPanel === panelName ? null : activeHeaderPanel));
34
+ },
35
+ [],
36
+ );
37
+
38
+ const showHamburger = useMemo(
39
+ () => (!isDesktop(layout) || mode === 'collapsed') && mode !== 'hidden' && navMenuItems.length > 0,
40
+ [navMenuItems.length, layout, mode],
41
+ );
42
+
43
+ return (
44
+ <>
45
+ <Header aria-label="OpenMRS" className={styles.topNavHeader}>
46
+ {showHamburger && (
47
+ <HeaderMenuButton
48
+ aria-label="Open menu"
49
+ isCollapsible
50
+ className={styles.headerMenuButton}
51
+ onClick={() => {
52
+ togglePanel('sideMenu');
53
+ }}
54
+ onMouseDown={(e) => e.stopPropagation()}
55
+ onTouchStart={(e) => e.stopPropagation()}
56
+ isActive={isActivePanel('sideMenu')}
57
+ />
58
+ )}
59
+ <ConfigurableLink to={config.logo.link}>
60
+ <div className={showHamburger ? '' : styles.spacedLogo}>
61
+ <Logo />
62
+ </div>
63
+ </ConfigurableLink>
64
+ <div className={styles.divider} />
65
+ <ExtensionSlot name="top-nav-info-slot" className={styles.topNavInfoSlot} />
66
+ <HeaderGlobalBar className={styles.headerGlobalBar}>
67
+ <ExtensionSlot
68
+ name="top-nav-actions-slot"
69
+ state={{ isActivePanel, togglePanel, hidePanel }}
70
+ className={styles.topNavActionsSlot}
71
+ />
72
+ <ExtensionSlot
73
+ name="notifications-menu-button-slot"
74
+ state={{
75
+ isActivePanel: isActivePanel,
76
+ togglePanel: togglePanel,
77
+ }}
78
+ />
79
+ <ExtensionSlot
80
+ name="top-nav-app-menu-slot"
81
+ state={{ isActivePanel, togglePanel, hidePanel }}
82
+ className={styles.topNavActionsSlot}
83
+ />
84
+ <SideMenuPanel hidePanel={hidePanel('sideMenu')} expanded={isActivePanel('sideMenu')} />
85
+ <NotificationsMenuPanel expanded={isActivePanel('notificationsMenu')} />
86
+ </HeaderGlobalBar>
87
+ </Header>
88
+ </>
89
+ );
90
+ };
91
+
92
+ const Navbar: React.FC = () => {
93
+ const session = useSession();
94
+ const openmrsSpaBase = window['getOpenmrsSpaBase']();
95
+
96
+ if (session?.user?.person) {
97
+ return session.sessionLocation ? (
98
+ <HeaderContainer render={HeaderItems}></HeaderContainer>
99
+ ) : (
100
+ <Navigate
101
+ to={`/login/location`}
102
+ state={{
103
+ referrer: window.location.pathname.slice(
104
+ window.location.pathname.indexOf(openmrsSpaBase) + openmrsSpaBase.length - 1,
105
+ ),
106
+ }}
107
+ />
108
+ );
109
+ }
110
+
111
+ return (
112
+ <Navigate
113
+ to={`/login`}
114
+ state={{
115
+ referrer: window.location.pathname.slice(
116
+ window.location.pathname.indexOf(openmrsSpaBase) + openmrsSpaBase.length - 1,
117
+ ),
118
+ }}
119
+ />
120
+ );
121
+ };
122
+
123
+ export default Navbar;
@@ -0,0 +1,51 @@
1
+ @use '@carbon/layout';
2
+ @use '@openmrs/esm-styleguide/src/vars' as *;
3
+
4
+ .topNavActionsSlot {
5
+ display: flex;
6
+ align-items: center;
7
+
8
+ > a {
9
+ color: white;
10
+ text-decoration: none;
11
+ margin: 0 layout.$spacing-04;
12
+ }
13
+ }
14
+
15
+ .headerGlobalBarButton {
16
+ @include brand-02(background-color);
17
+ }
18
+
19
+ .topNavHeader {
20
+ top: var(--omrs-offline-banner-height);
21
+ }
22
+
23
+ .activePanel {
24
+ @include brand-01(background-color);
25
+ }
26
+
27
+ .headerMenuButton {
28
+ height: layout.$spacing-09;
29
+ width: layout.$spacing-09;
30
+ padding: layout.$spacing-04;
31
+ margin-inline-end: layout.$spacing-02;
32
+ }
33
+
34
+ .spacedLogo {
35
+ margin-inline-start: layout.$spacing-05;
36
+ }
37
+
38
+ .divider {
39
+ width: 1px;
40
+ height: layout.$spacing-06;
41
+ background-color: rgba(244, 244, 244, 0.4);
42
+ margin-inline-start: layout.$spacing-04;
43
+ }
44
+
45
+ .topNavInfoSlot {
46
+ gap: layout.$spacing-03;
47
+ }
48
+
49
+ .panelWrapper {
50
+ display: contents;
51
+ }
@@ -0,0 +1,5 @@
1
+ export interface MenuButtonProps {
2
+ isActivePanel(panelName: string): boolean;
3
+ togglePanel(panelName: string): void;
4
+ hidePanel: (panelName: string) => () => void;
5
+ }
@@ -0,0 +1,44 @@
1
+ import { HeaderGlobalAction } from '@carbon/react';
2
+ import { CloseIcon, useAssignedExtensions, UserAvatarIcon, useOnClickOutside } from '@openmrs/esm-framework';
3
+ import classNames from 'classnames';
4
+ import React, { useMemo } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+ import UserMenuPanel from '../navbar-header-panels/user-menu-panel.component';
7
+ import { type MenuButtonProps } from './types';
8
+ import styles from './navbar.scss';
9
+
10
+ /**
11
+ * This component displays the user menu button and the menu itself (when toggled on)
12
+ */
13
+ const UserMenuButton: React.FC<MenuButtonProps> = ({ isActivePanel, togglePanel, hidePanel }) => {
14
+ const userMenuItems = useAssignedExtensions('user-panel-slot');
15
+ const showUserMenu = useMemo(() => userMenuItems.length > 0, [userMenuItems.length]);
16
+ const { t } = useTranslation();
17
+ const userMenuRef = useOnClickOutside<HTMLDivElement>(hidePanel('userMenu'), isActivePanel('userMenu'));
18
+
19
+ return (
20
+ showUserMenu && (
21
+ <div ref={userMenuRef} className={styles.panelWrapper}>
22
+ <HeaderGlobalAction
23
+ aria-label={t('userMenuTooltip', 'My Account')}
24
+ aria-labelledby="Users Avatar Icon"
25
+ className={classNames({
26
+ [styles.headerGlobalBarButton]: isActivePanel('userMenu'),
27
+ [styles.activePanel]: !isActivePanel('userMenu'),
28
+ })}
29
+ // @ts-ignore - `name` is not a valid prop for the HeaderGlobalAction component, but we need the name prop for user onboarding app to work correctly
30
+ name="User"
31
+ isActive={isActivePanel('userMenu')}
32
+ onClick={() => {
33
+ togglePanel('userMenu');
34
+ }}
35
+ >
36
+ {isActivePanel('userMenu') ? <CloseIcon size={20} /> : <UserAvatarIcon size={20} />}
37
+ </HeaderGlobalAction>
38
+ <UserMenuPanel expanded={isActivePanel('userMenu')} hidePanel={hidePanel('userMenu')} />
39
+ </div>
40
+ )
41
+ );
42
+ };
43
+
44
+ export default UserMenuButton;
@@ -0,0 +1,48 @@
1
+ import React, { useEffect } from 'react';
2
+ import classNames from 'classnames';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { HeaderPanel } from '@carbon/react';
5
+ import { Launch } from '@carbon/react/icons';
6
+ import { ExtensionSlot, useConfig } from '@openmrs/esm-framework';
7
+ import styles from './app-menu-panel.scss';
8
+
9
+ interface AppMenuProps {
10
+ expanded: boolean;
11
+ hidePanel: () => void;
12
+ }
13
+
14
+ const AppMenuPanel: React.FC<AppMenuProps> = ({ expanded, hidePanel }) => {
15
+ const config = useConfig();
16
+ const { t } = useTranslation();
17
+
18
+ useEffect(() => {
19
+ window.addEventListener('popstate', hidePanel);
20
+ return () => window.removeEventListener('popstate', hidePanel);
21
+ }, [hidePanel]);
22
+
23
+ return (
24
+ expanded && (
25
+ <div style={{ display: 'inline' }}>
26
+ <HeaderPanel
27
+ className={classNames({ [styles.headerPanel]: expanded })}
28
+ aria-label="App Menu Panel"
29
+ expanded={expanded}
30
+ >
31
+ <ExtensionSlot className={styles.menuLink} name="app-menu-slot" />
32
+ {config?.externalRefLinks?.length > 0 && (
33
+ <div className={classNames(styles.menuLink, styles.externalLinks)}>
34
+ {config?.externalRefLinks?.map((link) => (
35
+ <a target="_blank" rel="noopener noreferrer" href={link?.redirect}>
36
+ {t(link?.title)}
37
+ <Launch size={16} className={styles.launchIcon} />
38
+ </a>
39
+ ))}
40
+ </div>
41
+ )}
42
+ </HeaderPanel>
43
+ </div>
44
+ )
45
+ );
46
+ };
47
+
48
+ export default AppMenuPanel;
@@ -0,0 +1,43 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @use '@openmrs/esm-styleguide/src/vars' as *;
4
+
5
+ .productiveHeading01 {
6
+ @include type.type-style('heading-compact-01');
7
+ }
8
+
9
+ .headerPanel {
10
+ height: max-content;
11
+ }
12
+
13
+ .primaryNavContainer {
14
+ height: var(--omrs-topnav-height);
15
+ }
16
+
17
+ .menuLink a {
18
+ display: flex;
19
+ padding: layout.$spacing-05;
20
+ height: layout.$spacing-09;
21
+ width: 100%;
22
+ text-decoration: none;
23
+ color: $ui-02;
24
+ background-color: transparent;
25
+ @extend .productiveHeading01;
26
+ }
27
+
28
+ .menuLink a:hover {
29
+ @include brand-01(background-color);
30
+ }
31
+
32
+ .externalLinks {
33
+ border-top: 1px solid;
34
+ @include brand-03(border-top-color);
35
+ }
36
+
37
+ .launchIcon {
38
+ margin-left: layout.$spacing-03;
39
+ }
40
+
41
+ .headerPanel {
42
+ width: fit-content;
43
+ }
@@ -0,0 +1,23 @@
1
+ import React, { useMemo } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { HeaderPanel, type HeaderPanelProps } from '@carbon/react';
4
+ import { ExtensionSlot } from '@openmrs/esm-framework';
5
+ import styles from './notifications-menu.panel.scss';
6
+
7
+ interface NotificationsMenuPanelProps extends HeaderPanelProps {
8
+ expanded: boolean;
9
+ }
10
+
11
+ const NotificationsMenuPanel: React.FC<NotificationsMenuPanelProps> = ({ expanded }) => {
12
+ const { t } = useTranslation();
13
+ const state = useMemo(() => ({ expanded }), [expanded]);
14
+
15
+ return (
16
+ <HeaderPanel aria-label="Notifications Panel" expanded={expanded}>
17
+ <h1 className={styles.heading}>{t('notifications', 'Notifications')}</h1>
18
+ <ExtensionSlot name="notifications-nav-menu-slot" state={state} />
19
+ </HeaderPanel>
20
+ );
21
+ };
22
+
23
+ export default NotificationsMenuPanel;
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import { screen, render } from '@testing-library/react';
3
+ import NotificationsMenuPanel from './notifications-menu-panel.component';
4
+
5
+ test('renders the notifications menu panel scaffold', () => {
6
+ render(<NotificationsMenuPanel expanded />);
7
+
8
+ expect(screen.getByRole('heading', { name: /notifications/i })).toBeInTheDocument();
9
+ });
@@ -0,0 +1,9 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @use '@openmrs/esm-styleguide/src/vars' as *;
4
+
5
+ .heading {
6
+ @include type.type-style('heading-03');
7
+ color: $ui-background;
8
+ margin: layout.$spacing-05;
9
+ }
@@ -0,0 +1,37 @@
1
+ import React, { useEffect } from 'react';
2
+ import { isDesktop, LeftNavMenu, useLayoutType, useLeftNavStore, useOnClickOutside } from '@openmrs/esm-framework';
3
+ import { createPortal } from 'react-dom';
4
+
5
+ interface SideMenuPanelProps {
6
+ expanded: boolean;
7
+ hidePanel: Parameters<typeof useOnClickOutside>[0];
8
+ }
9
+
10
+ /**
11
+ * This is the menu that pops up when clicking on the hamburger button
12
+ * on the top nav. It's also responsible for rendering the left nav
13
+ * in desktop mode (via a react portal).
14
+ */
15
+ const SideMenuPanel: React.FC<SideMenuPanelProps> = ({ expanded, hidePanel }) => {
16
+ const menuRef = useOnClickOutside(hidePanel, expanded);
17
+
18
+ useEffect(() => {
19
+ window.addEventListener('popstate', hidePanel);
20
+ return () => window.removeEventListener('popstate', hidePanel);
21
+ }, [hidePanel]);
22
+ const layout = useLayoutType();
23
+ const { mode } = useLeftNavStore();
24
+
25
+ const leftNavContainer = window.document.getElementById('omrs-left-nav-container');
26
+ return (
27
+ <>
28
+ {(!isDesktop(layout) || mode === 'collapsed') && expanded && <LeftNavMenu ref={menuRef} isChildOfHeader />}
29
+ {isDesktop(layout) &&
30
+ mode === 'normal' &&
31
+ leftNavContainer &&
32
+ createPortal(<LeftNavMenu ref={menuRef} isChildOfHeader />, leftNavContainer)}
33
+ </>
34
+ );
35
+ };
36
+
37
+ export default SideMenuPanel;
@@ -0,0 +1,32 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { HeaderPanel, type HeaderPanelProps, Switcher, SwitcherDivider } from '@carbon/react';
4
+ import { ExtensionSlot } from '@openmrs/esm-framework';
5
+ import styles from './user-menu-panel.scss';
6
+
7
+ interface UserMenuPanelProps extends HeaderPanelProps {
8
+ expanded: boolean;
9
+ hidePanel: () => void;
10
+ }
11
+
12
+ /**
13
+ * Extensions attaching to `user-panel-slot` or `user-panel-bottom-slot` should in
14
+ * general be wrapped in the `SwitcherItem` Carbon component.
15
+ */
16
+ const UserMenuPanel: React.FC<UserMenuPanelProps> = ({ expanded, hidePanel }) => {
17
+ const { t } = useTranslation();
18
+
19
+ return (
20
+ <div style={{ display: 'inline' }}>
21
+ <HeaderPanel className={styles.headerPanel} expanded={expanded} aria-label={t('userMenu', 'User menu')}>
22
+ <Switcher className={styles.userPanelSwitcher} aria-label={t('userMenuOptions', 'User menu options')}>
23
+ <ExtensionSlot className={styles.fullWidth} name="user-panel-slot" />
24
+ <SwitcherDivider className={styles.divider} aria-hidden="true" />
25
+ <ExtensionSlot className={styles.fullWidth} name="user-panel-bottom-slot" />
26
+ </Switcher>
27
+ </HeaderPanel>
28
+ </div>
29
+ );
30
+ };
31
+
32
+ export default UserMenuPanel;
@@ -0,0 +1,65 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @use '@openmrs/esm-styleguide/src/vars' as *;
4
+
5
+ .bodyLong01 {
6
+ @include type.type-style('body-01');
7
+ }
8
+
9
+ .headerPanel {
10
+ height: max-content;
11
+ }
12
+
13
+ .primaryNavContainer {
14
+ height: var(--omrs-topnav-height);
15
+ }
16
+
17
+ .headerPanel {
18
+ transition: none;
19
+ }
20
+
21
+ .fullWidth {
22
+ width: 100%;
23
+ }
24
+
25
+ .userPanelSwitcher {
26
+ padding-top: layout.$spacing-05;
27
+ align-items: start;
28
+ }
29
+
30
+ .userPanelSwitcher :global(.cds--switcher__item) {
31
+ height: layout.$spacing-09;
32
+ }
33
+
34
+ .userPanelSwitcher :global(.cds--switcher__item:first-child) {
35
+ margin-top: 0;
36
+ }
37
+
38
+ .userPanelSwitcher :global(.cds--switcher__item-link) {
39
+ display: flex;
40
+ align-items: center;
41
+ color: $field-01;
42
+ padding: 0;
43
+ height: layout.$spacing-09;
44
+ @extend .bodyLong01;
45
+ }
46
+
47
+ .userPanelSwitcher :global(.cds--switcher__item-link):hover:not(.cds--switcher__item-link--selected) {
48
+ color: $field-01;
49
+ @include brand-02(background-color);
50
+ }
51
+
52
+ .userPanelSwitcher :global(.cds--switcher__item) svg {
53
+ margin: 0 layout.$spacing-04;
54
+ fill: $field-01;
55
+ flex-shrink: 0;
56
+ }
57
+
58
+ .userPanelSwitcher :global(.cds--switcher__item) button {
59
+ color: white;
60
+ }
61
+
62
+ .userPanelSwitcher :global(.cds--switcher__item) button:hover {
63
+ @include brand-03(background-color);
64
+ color: white;
65
+ }
@@ -0,0 +1,15 @@
1
+ import React from 'react';
2
+ import { SwitcherItem } from '@carbon/react';
3
+ import { UserAvatarIcon, useSession } from '@openmrs/esm-framework';
4
+
5
+ const UserPanelSwitcher: React.FC = () => {
6
+ const session = useSession();
7
+ return (
8
+ <SwitcherItem aria-label="User">
9
+ <UserAvatarIcon size={20} />
10
+ <p>{session?.user?.person?.display}</p>
11
+ </SwitcherItem>
12
+ );
13
+ };
14
+
15
+ export default UserPanelSwitcher;
@@ -0,0 +1,22 @@
1
+ import React from 'react';
2
+ import { screen, render } from '@testing-library/react';
3
+ import { type LoggedInUser, type Session, useSession } from '@openmrs/esm-framework';
4
+ import { mockLoggedInUser } from '../../../__mocks__/mock-user';
5
+ import UserPanelSwitcher from './user-panel-switcher.component';
6
+
7
+ const mockUseSession = jest.mocked(useSession);
8
+
9
+ describe('UserPanelSwitcher', () => {
10
+ beforeEach(() => {
11
+ mockUseSession.mockReturnValue({
12
+ authenticated: true,
13
+ user: mockLoggedInUser as unknown as LoggedInUser,
14
+ } as unknown as Session);
15
+ });
16
+
17
+ it('should display user name', async () => {
18
+ render(<UserPanelSwitcher />);
19
+
20
+ expect(await screen.findByText(/Dr Healther Morgan/i)).toBeInTheDocument();
21
+ });
22
+ });
@@ -0,0 +1,56 @@
1
+ import { Type, validators } from '@openmrs/esm-framework';
2
+
3
+ export const configSchema = {
4
+ logo: {
5
+ src: {
6
+ _type: Type.String,
7
+ _default: '',
8
+ _description:
9
+ 'The path or URL to the logo image. If set to an empty string, the default OpenMRS SVG sprite will be used.',
10
+ _validators: [validators.isUrl],
11
+ },
12
+ alt: {
13
+ _type: Type.String,
14
+ _default: 'Logo',
15
+ _description: 'The alternative text for the logo image, displayed when the image cannot be loaded or on hover.',
16
+ },
17
+ name: {
18
+ _type: Type.String,
19
+ _default: '',
20
+ _description: 'The organization name displayed when image is absent',
21
+ },
22
+ link: {
23
+ _type: Type.String,
24
+ _default: '${openmrsSpaBase}/home',
25
+ _description: 'The link to redirect to when the logo is clicked',
26
+ _validators: [validators.isUrl],
27
+ },
28
+ },
29
+ externalRefLinks: {
30
+ _type: Type.Array,
31
+ _elements: {
32
+ _type: Type.Object,
33
+ title: {
34
+ _type: Type.String,
35
+ _description: 'Title of the link',
36
+ },
37
+ redirect: {
38
+ _type: Type.String,
39
+ _description: 'Link to redirect to (must be an external link)',
40
+ _validators: [validators.isUrl],
41
+ },
42
+ },
43
+ _default: [],
44
+ _description: 'The external links to be showcased in the app menu',
45
+ },
46
+ };
47
+
48
+ export type ConfigSchema = {
49
+ logo: {
50
+ src: string;
51
+ alt: string;
52
+ name: string;
53
+ link: string;
54
+ };
55
+ externalRefLinks: { title: string; redirect: string }[];
56
+ };
@@ -0,0 +1,2 @@
1
+ export const userPropertyChange = 'user-property-change';
2
+ export const moduleName = '@digihmis/esm-primary-navigation-app';
@@ -0,0 +1 @@
1
+ declare module '*.scss';
package/src/index.ts ADDED
@@ -0,0 +1,68 @@
1
+ import {
2
+ defineConfigSchema,
3
+ defineExtensionConfigSchema,
4
+ getAsyncLifecycle,
5
+ getSyncLifecycle,
6
+ navigate,
7
+ } from '@openmrs/esm-framework';
8
+ import { type Application } from 'single-spa';
9
+ import { configSchema } from './config-schema';
10
+ import { moduleName } from './constants';
11
+ import primaryNavRootComponent from './root.component';
12
+ import userPanelComponent from './components/user-panel-switcher-item/user-panel-switcher.component';
13
+ import changeLanguageLinkComponent from './components/change-language/change-language-link.extension';
14
+ import { NavGroup, navGroupConfigSchema } from './components/nav-group/nav-group.component';
15
+ import { dashboardConfigSchema } from './components/dashboard/dashboard.component';
16
+ import genericLinkComponent, { genericLinkConfigSchema } from './components/generic-link/generic-link.component';
17
+ import UserMenuButton from './components/navbar/user-menu-button.component';
18
+ import AppMenuButton from './components/navbar/app-menu-button.component';
19
+
20
+ export const importTranslation = require.context('../translations', false, /.json$/, 'lazy');
21
+
22
+ const options = {
23
+ featureName: 'primary navigation',
24
+ moduleName,
25
+ };
26
+
27
+ export function startupApp() {
28
+ defineConfigSchema(moduleName, configSchema);
29
+ defineExtensionConfigSchema('link', genericLinkConfigSchema);
30
+ defineExtensionConfigSchema('nav-group', navGroupConfigSchema);
31
+ defineExtensionConfigSchema('dashboard', dashboardConfigSchema);
32
+ }
33
+
34
+ export const root = getSyncLifecycle(primaryNavRootComponent, options);
35
+
36
+ export const redirect: Application = async () => ({
37
+ bootstrap: async () => navigate({ to: '${openmrsSpaBase}/home' }),
38
+ mount: async () => undefined,
39
+ unmount: async () => undefined,
40
+ });
41
+
42
+ export const userMenuButton = getSyncLifecycle(UserMenuButton, {
43
+ featureName: 'user-menu-button',
44
+ moduleName,
45
+ });
46
+
47
+ export const userPanel = getSyncLifecycle(userPanelComponent, options);
48
+
49
+ export const changeLanguageLink = getSyncLifecycle(changeLanguageLinkComponent, options);
50
+
51
+ export const changeLanguageModal = getAsyncLifecycle(
52
+ () => import('./components/change-language/change-language.modal'),
53
+ options,
54
+ );
55
+
56
+ export const appMenuButton = getSyncLifecycle(AppMenuButton, {
57
+ featureName: 'app-menu-button',
58
+ moduleName,
59
+ });
60
+
61
+ export const linkComponent = getSyncLifecycle(genericLinkComponent, {
62
+ featureName: 'Link',
63
+ moduleName,
64
+ });
65
+
66
+ export const navGroup = getSyncLifecycle(NavGroup, options);
67
+
68
+ export const dashboard = getAsyncLifecycle(() => import('./components/dashboard/dashboard.component'), options);