@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.
- package/README.md +43 -0
- package/__mocks__/mock-session.ts +23 -0
- package/__mocks__/mock-user.ts +23 -0
- package/__mocks__/openmrs-esm-framework.mock.tsx +30 -0
- package/__mocks__/react-i18next.mock.js +49 -0
- package/dist/1480.js +1 -0
- package/dist/1480.js.map +1 -0
- package/dist/1646.js +1 -0
- package/dist/1646.js.map +1 -0
- package/dist/1716.js +1 -0
- package/dist/1716.js.map +1 -0
- package/dist/1797.js +1 -0
- package/dist/1797.js.map +1 -0
- package/dist/1869.js +1 -0
- package/dist/1869.js.map +1 -0
- package/dist/1877.js +1 -0
- package/dist/1877.js.map +1 -0
- package/dist/2317.js +1 -0
- package/dist/2317.js.map +1 -0
- package/dist/2416.js +1 -0
- package/dist/2416.js.map +1 -0
- package/dist/2711.js +1 -0
- package/dist/2711.js.map +1 -0
- package/dist/282.js +1 -0
- package/dist/282.js.map +1 -0
- package/dist/2881.js +1 -0
- package/dist/2881.js.map +1 -0
- package/dist/2939.js +15 -0
- package/dist/2939.js.map +1 -0
- package/dist/3378.js +1 -0
- package/dist/3378.js.map +1 -0
- package/dist/3963.js +1 -0
- package/dist/3963.js.map +1 -0
- package/dist/4106.js +1 -0
- package/dist/4106.js.map +1 -0
- package/dist/4111.js +1 -0
- package/dist/4111.js.map +1 -0
- package/dist/434.js +1 -0
- package/dist/434.js.map +1 -0
- package/dist/4348.js +1 -0
- package/dist/4348.js.map +1 -0
- package/dist/4383.js +1 -0
- package/dist/4383.js.map +1 -0
- package/dist/4658.js +1 -0
- package/dist/4658.js.map +1 -0
- package/dist/466.js +1 -0
- package/dist/466.js.map +1 -0
- package/dist/4928.js +1 -0
- package/dist/4928.js.map +1 -0
- package/dist/5117.js +1 -0
- package/dist/5117.js.map +1 -0
- package/dist/5132.js +1 -0
- package/dist/5132.js.map +1 -0
- package/dist/5145.js +1 -0
- package/dist/5145.js.map +1 -0
- package/dist/5503.js +1 -0
- package/dist/5503.js.map +1 -0
- package/dist/556.js +1 -0
- package/dist/556.js.map +1 -0
- package/dist/5644.js +1 -0
- package/dist/5644.js.map +1 -0
- package/dist/5867.js +12 -0
- package/dist/5867.js.map +1 -0
- package/dist/5940.js +1 -0
- package/dist/5940.js.map +1 -0
- package/dist/6047.js +1 -0
- package/dist/6047.js.map +1 -0
- package/dist/6371.js +1 -0
- package/dist/6371.js.map +1 -0
- package/dist/6377.js +1 -0
- package/dist/6377.js.map +1 -0
- package/dist/6444.js +1 -0
- package/dist/6444.js.map +1 -0
- package/dist/6508.js +1 -0
- package/dist/6508.js.map +1 -0
- package/dist/6724.js +1 -0
- package/dist/6724.js.map +1 -0
- package/dist/689.js +1 -0
- package/dist/689.js.map +1 -0
- package/dist/6904.js +1 -0
- package/dist/6904.js.map +1 -0
- package/dist/7044.js +1 -0
- package/dist/7044.js.map +1 -0
- package/dist/7045.js +1 -0
- package/dist/7045.js.map +1 -0
- package/dist/7175.js +1 -0
- package/dist/7175.js.map +1 -0
- package/dist/7182.js +1 -0
- package/dist/7182.js.map +1 -0
- package/dist/7302.js +1 -0
- package/dist/7302.js.map +1 -0
- package/dist/7587.js +1 -0
- package/dist/7587.js.map +1 -0
- package/dist/7742.js +1 -0
- package/dist/7742.js.map +1 -0
- package/dist/7786.js +1 -0
- package/dist/7786.js.map +1 -0
- package/dist/7912.js +1 -0
- package/dist/7912.js.map +1 -0
- package/dist/8349.js +1 -0
- package/dist/8349.js.map +1 -0
- package/dist/8358.js +1 -0
- package/dist/8358.js.map +1 -0
- package/dist/8359.js +1 -0
- package/dist/8359.js.map +1 -0
- package/dist/8695.js +1 -0
- package/dist/8695.js.map +1 -0
- package/dist/8924.js +1 -0
- package/dist/8924.js.map +1 -0
- package/dist/903.js +1 -0
- package/dist/903.js.map +1 -0
- package/dist/9072.js +1 -0
- package/dist/9072.js.map +1 -0
- package/dist/9105.js +1 -0
- package/dist/9105.js.map +1 -0
- package/dist/9712.js +1 -0
- package/dist/9712.js.map +1 -0
- package/dist/9806.js +1 -0
- package/dist/9806.js.map +1 -0
- package/dist/digihmis-esm-primary-navigation-app.js +6 -0
- package/dist/digihmis-esm-primary-navigation-app.js.buildmanifest.json +1542 -0
- package/dist/digihmis-esm-primary-navigation-app.js.map +1 -0
- package/dist/main.js +6 -0
- package/dist/main.js.map +1 -0
- package/dist/routes.json +1 -0
- package/jest.config.js +18 -0
- package/package.json +68 -0
- package/rspack.config.js +1 -0
- package/src/components/change-language/change-language-link.extension.tsx +37 -0
- package/src/components/change-language/change-language-link.scss +14 -0
- package/src/components/change-language/change-language.modal.tsx +115 -0
- package/src/components/change-language/change-language.resource.ts +35 -0
- package/src/components/change-language/change-language.scss +59 -0
- package/src/components/change-language/change-language.test.tsx +107 -0
- package/src/components/dashboard/dashboard.component.tsx +104 -0
- package/src/components/generic-link/generic-link.component.tsx +25 -0
- package/src/components/logo/logo.component.tsx +28 -0
- package/src/components/logo/logo.scss +11 -0
- package/src/components/logo/logo.test.tsx +79 -0
- package/src/components/nav-group/nav-group.component.tsx +52 -0
- package/src/components/navbar/app-menu-button.component.tsx +43 -0
- package/src/components/navbar/navbar.component.tsx +123 -0
- package/src/components/navbar/navbar.scss +51 -0
- package/src/components/navbar/types.ts +5 -0
- package/src/components/navbar/user-menu-button.component.tsx +44 -0
- package/src/components/navbar-header-panels/app-menu-panel.component.tsx +48 -0
- package/src/components/navbar-header-panels/app-menu-panel.scss +43 -0
- package/src/components/navbar-header-panels/notifications-menu-panel.component.tsx +23 -0
- package/src/components/navbar-header-panels/notifications-menu-panel.test.tsx +9 -0
- package/src/components/navbar-header-panels/notifications-menu.panel.scss +9 -0
- package/src/components/navbar-header-panels/side-menu-panel.component.tsx +37 -0
- package/src/components/navbar-header-panels/user-menu-panel.component.tsx +32 -0
- package/src/components/navbar-header-panels/user-menu-panel.scss +65 -0
- package/src/components/user-panel-switcher-item/user-panel-switcher.component.tsx +15 -0
- package/src/components/user-panel-switcher-item/user-panel-switcher.test.tsx +22 -0
- package/src/config-schema.ts +56 -0
- package/src/constants.ts +2 -0
- package/src/declarations.d.ts +1 -0
- package/src/index.ts +68 -0
- package/src/root.component.test.tsx +57 -0
- package/src/root.component.tsx +25 -0
- package/src/root.resource.tsx +13 -0
- package/src/root.scss +5 -0
- package/src/routes.json +78 -0
- package/src/setupTests.ts +7 -0
- package/src/utils.ts +3 -0
- package/translations/am.json +14 -0
- package/translations/ar.json +14 -0
- package/translations/ar_SY.json +14 -0
- package/translations/bn.json +14 -0
- package/translations/cs.json +14 -0
- package/translations/de.json +14 -0
- package/translations/en.json +14 -0
- package/translations/en_US.json +14 -0
- package/translations/es.json +14 -0
- package/translations/es_MX.json +14 -0
- package/translations/fr.json +14 -0
- package/translations/he.json +14 -0
- package/translations/hi.json +14 -0
- package/translations/hi_IN.json +14 -0
- package/translations/id.json +14 -0
- package/translations/it.json +14 -0
- package/translations/ka.json +14 -0
- package/translations/km.json +14 -0
- package/translations/ku.json +14 -0
- package/translations/ky.json +14 -0
- package/translations/lg.json +14 -0
- package/translations/ne.json +14 -0
- package/translations/pl.json +14 -0
- package/translations/pt.json +14 -0
- package/translations/pt_BR.json +14 -0
- package/translations/qu.json +14 -0
- package/translations/ro_RO.json +14 -0
- package/translations/ru_RU.json +14 -0
- package/translations/si.json +14 -0
- package/translations/sq.json +14 -0
- package/translations/sw.json +12 -0
- package/translations/sw_KE.json +14 -0
- package/translations/tr.json +14 -0
- package/translations/tr_TR.json +14 -0
- package/translations/uk.json +14 -0
- package/translations/uz.json +14 -0
- package/translations/uz@Latn.json +14 -0
- package/translations/uz_UZ.json +14 -0
- package/translations/vi.json +14 -0
- package/translations/zh.json +14 -0
- package/translations/zh_CN.json +14 -0
- package/translations/zh_TW.json +14 -0
- 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,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,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
|
+
};
|
package/src/constants.ts
ADDED
|
@@ -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);
|