@dhis2-ui/header-bar 10.16.1 → 10.16.3-alpha.1

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 (125) hide show
  1. package/package.json +17 -16
  2. package/src/__e2e__/header-bar.e2e.stories.js +26 -0
  3. package/src/__e2e__/stories/common.js +226 -0
  4. package/src/__e2e__/stories/custom-application-title.js +19 -0
  5. package/src/__e2e__/stories/default.js +13 -0
  6. package/src/__e2e__/stories/me-with-avatar.js +27 -0
  7. package/src/__e2e__/stories/modulesWithSpecialCharacters.js +229 -0
  8. package/src/__e2e__/stories/online-status-message.js +49 -0
  9. package/src/__e2e__/stories/pwa-enabled.js +17 -0
  10. package/src/__e2e__/stories/user-has-all-authority.js +20 -0
  11. package/src/__e2e__/stories/user-has-no-authorities.js +20 -0
  12. package/src/__e2e__/stories/user-has-web-interpretation-and-messaging-authority.js +22 -0
  13. package/src/__e2e__/stories/user-has-web-interpretation-authority.js +22 -0
  14. package/src/__e2e__/stories/user-has-web-messaging-authority.js +22 -0
  15. package/src/__e2e__/stories/with-debug-info-edge-cases.js +51 -0
  16. package/src/__e2e__/stories/with-special-app-name-character.js +23 -0
  17. package/src/__e2e__/stories/with-update-available-notification.js +39 -0
  18. package/src/__e2e__/stories/zero-unread-interpretations.js +19 -0
  19. package/src/__e2e__/stories/zero-unread-messages.js +19 -0
  20. package/src/apps.js +276 -0
  21. package/src/debug-info/debug-info-menu-item.js +72 -0
  22. package/src/debug-info/debug-info-modal.js +47 -0
  23. package/src/debug-info/debug-info-table.js +51 -0
  24. package/src/debug-info/use-debug-info.js +15 -0
  25. package/src/features/common/index.js +14 -0
  26. package/src/features/the_headerbar_can_display_online_status/the_headerbar_displays_online_status.js +158 -0
  27. package/src/features/the_headerbar_can_display_online_status.feature +57 -0
  28. package/src/features/the_headerbar_conditionally_renders_notification_icons/the_headerbar_conditionally_renders_notification_icons.js +43 -0
  29. package/src/features/the_headerbar_conditionally_renders_notification_icons.feature +26 -0
  30. package/src/features/the_headerbar_contains_a_menu_to_all_apps/common.js +5 -0
  31. package/src/features/the_headerbar_contains_a_menu_to_all_apps/the_app_menu_closes_when_the_user_clicks_outside.js +9 -0
  32. package/src/features/the_headerbar_contains_a_menu_to_all_apps/the_headerbar_contains_a_menu_icon.js +5 -0
  33. package/src/features/the_headerbar_contains_a_menu_to_all_apps/the_user_will_be_offered_a_menu_with_5_apps.js +16 -0
  34. package/src/features/the_headerbar_contains_a_menu_to_all_apps.feature +21 -0
  35. package/src/features/the_headerbar_contains_a_profile_menu/common.js +14 -0
  36. package/src/features/the_headerbar_contains_a_profile_menu/the_headerbar_shows_a_text_icon_if_the_user_does_not_have_an_avatar.js +30 -0
  37. package/src/features/the_headerbar_contains_a_profile_menu/the_headerbar_shows_an_image_icon_if_the_user_has_an_avatar.js +23 -0
  38. package/src/features/the_headerbar_contains_a_profile_menu/the_menu_is_closed_by_default.js +1 -0
  39. package/src/features/the_headerbar_contains_a_profile_menu/the_menu_opens.js +14 -0
  40. package/src/features/the_headerbar_contains_a_profile_menu/the_profile_menu_closes_when_the_user_clicks_outside.js +5 -0
  41. package/src/features/the_headerbar_contains_a_profile_menu/the_user_can_edit_his_profile.js +7 -0
  42. package/src/features/the_headerbar_contains_a_profile_menu/the_user_can_go_to_his_account.js +7 -0
  43. package/src/features/the_headerbar_contains_a_profile_menu/the_user_can_go_to_the_about_dhis2_page.js +7 -0
  44. package/src/features/the_headerbar_contains_a_profile_menu/the_user_can_go_to_the_help_page.js +7 -0
  45. package/src/features/the_headerbar_contains_a_profile_menu/the_user_can_go_to_the_settings.js +7 -0
  46. package/src/features/the_headerbar_contains_a_profile_menu/the_user_can_log_out.js +53 -0
  47. package/src/features/the_headerbar_contains_a_profile_menu/the_user_name_and_email_are_displayed.js +22 -0
  48. package/src/features/the_headerbar_contains_a_profile_menu.feature +73 -0
  49. package/src/features/the_headerbar_displays_a_link_to_interpretations_and_an_unread_count/the_headerbar_displays_a_link_to_the_interpretations.js +5 -0
  50. package/src/features/the_headerbar_displays_a_link_to_interpretations_and_an_unread_count/there_are_no_unread_interpretations.js +9 -0
  51. package/src/features/the_headerbar_displays_a_link_to_interpretations_and_an_unread_count/there_are_some_unread_interpretations.js +12 -0
  52. package/src/features/the_headerbar_displays_a_link_to_interpretations_and_an_unread_count.feature +13 -0
  53. package/src/features/the_headerbar_displays_a_link_to_messages_and_an_unread_count/the_headerbar_displays_a_link_to_the_messages.js +5 -0
  54. package/src/features/the_headerbar_displays_a_link_to_messages_and_an_unread_count/there_are_no_unread_messages.js +9 -0
  55. package/src/features/the_headerbar_displays_a_link_to_messages_and_an_unread_count/there_are_some_unread_messages.js +7 -0
  56. package/src/features/the_headerbar_displays_a_link_to_messages_and_an_unread_count.feature +13 -0
  57. package/src/features/the_headerbar_should_contain_a_logo_that_links_to_the_homepage/headerbar_contains_logo.js +12 -0
  58. package/src/features/the_headerbar_should_contain_a_logo_that_links_to_the_homepage.feature +6 -0
  59. package/src/features/the_headerbar_should_display_app_update_notification/index.js +52 -0
  60. package/src/features/the_headerbar_should_display_app_update_notification.feature +22 -0
  61. package/src/features/the_headerbar_should_display_debug_version_infos/index.js +130 -0
  62. package/src/features/the_headerbar_should_display_debug_version_infos.feature +52 -0
  63. package/src/features/the_headerbar_should_display_the_title_provided_by_the_backend_and_the_app/the_headerbar_displays_the_custom_title.js +11 -0
  64. package/src/features/the_headerbar_should_display_the_title_provided_by_the_backend_and_the_app.feature +5 -0
  65. package/src/features/the_search_should_escape_regexp_character/common.js +6 -0
  66. package/src/features/the_search_should_escape_regexp_character/the_modules_do_not_contain_items_with_special_chars.js +23 -0
  67. package/src/features/the_search_should_escape_regexp_character/the_user_searches_for_an_app_with_a_regex_character.js +29 -0
  68. package/src/features/the_search_should_escape_regexp_character.feature +48 -0
  69. package/src/header-bar-context.js +28 -0
  70. package/src/header-bar.js +145 -0
  71. package/src/header-bar.prod.stories.js +303 -0
  72. package/src/index.js +1 -0
  73. package/src/join-path.js +4 -0
  74. package/src/locales/ar/translations.json +24 -0
  75. package/src/locales/ar_IQ/translations.json +24 -0
  76. package/src/locales/bn/translations.json +12 -0
  77. package/src/locales/ckb/translations.json +23 -0
  78. package/src/locales/cs/translations.json +24 -0
  79. package/src/locales/da/translations.json +24 -0
  80. package/src/locales/en/translations.json +25 -0
  81. package/src/locales/en_US/translations.json +25 -0
  82. package/src/locales/es/translations.json +25 -0
  83. package/src/locales/es_419/translations.json +24 -0
  84. package/src/locales/fr/translations.json +25 -0
  85. package/src/locales/hi_IN/translations.json +25 -0
  86. package/src/locales/id/translations.json +24 -0
  87. package/src/locales/index.js +88 -0
  88. package/src/locales/km/translations.json +24 -0
  89. package/src/locales/lo/translations.json +24 -0
  90. package/src/locales/my/translations.json +24 -0
  91. package/src/locales/nb/translations.json +24 -0
  92. package/src/locales/nl/translations.json +24 -0
  93. package/src/locales/or/translations.json +12 -0
  94. package/src/locales/prs/translations.json +24 -0
  95. package/src/locales/ps/translations.json +24 -0
  96. package/src/locales/pt/translations.json +25 -0
  97. package/src/locales/pt_BR/translations.json +24 -0
  98. package/src/locales/ro/translations.json +23 -0
  99. package/src/locales/ru/translations.json +24 -0
  100. package/src/locales/si/translations.json +24 -0
  101. package/src/locales/sv/translations.json +24 -0
  102. package/src/locales/tet/translations.json +24 -0
  103. package/src/locales/tg/translations.json +24 -0
  104. package/src/locales/uk/translations.json +24 -0
  105. package/src/locales/ur/translations.json +24 -0
  106. package/src/locales/uz_Latn/translations.json +23 -0
  107. package/src/locales/uz_UZ_Cyrl/translations.json +24 -0
  108. package/src/locales/uz_UZ_Latn/translations.json +24 -0
  109. package/src/locales/vi/translations.json +24 -0
  110. package/src/locales/zh/translations.json +25 -0
  111. package/src/locales/zh_CN/translations.json +24 -0
  112. package/src/logo-image.js +71 -0
  113. package/src/logo.js +45 -0
  114. package/src/notification-icon.js +91 -0
  115. package/src/notifications.js +63 -0
  116. package/src/online-status.js +40 -0
  117. package/src/online-status.styles.js +91 -0
  118. package/src/profile/use-on-doc-click.js +23 -0
  119. package/src/profile/use-on-doc-click.test.js +40 -0
  120. package/src/profile-menu/index.js +1 -0
  121. package/src/profile-menu/profile-header.js +118 -0
  122. package/src/profile-menu/profile-menu.js +176 -0
  123. package/src/profile-menu/update-notification.js +67 -0
  124. package/src/profile.js +101 -0
  125. package/src/title.js +23 -0
@@ -0,0 +1,23 @@
1
+ {
2
+ "Search apps": "Ilovalarni qidirish",
3
+ "DHIS2 {{dhis2Version}}": "",
4
+ "DHIS2 version unknown": "",
5
+ "{{appName}} version unknown": "",
6
+ "App {{appVersion}}": "",
7
+ "App version unknown": "",
8
+ "Debug info": "",
9
+ "Close": "Yopmoq",
10
+ "Copy debug info": "",
11
+ "Last online {{relativeTime}}": "",
12
+ "Online": "Onlayn",
13
+ "Offline": "Offlayn",
14
+ "Edit profile": "Profilni tahrir qilish",
15
+ "Settings": "Sozlamalar",
16
+ "Account": "Raqami",
17
+ "Help": "Yordam",
18
+ "About DHIS2": "DHIS2 Haqida",
19
+ "Logout": "Chiqib ketish",
20
+ "New {{appName}} version available": "",
21
+ "New app version available": "",
22
+ "Click to reload": ""
23
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "Search apps": "Иловаларни қидириш",
3
+ "DHIS2 {{dhis2Version}}": "",
4
+ "DHIS2 version unknown": "",
5
+ "{{appName}} version unknown": "",
6
+ "App {{appVersion}}": "",
7
+ "App version unknown": "",
8
+ "Debug info": "",
9
+ "Close": "Ёпиш",
10
+ "Copy debug info": "",
11
+ "Interpretations": "Талқинлар",
12
+ "Messages": "Хабарлар",
13
+ "Online": "Онлайн",
14
+ "Offline": "Оффлайн",
15
+ "Edit profile": "Профилни таҳрир қилиш",
16
+ "Settings": "Созламалар",
17
+ "Account": "Рақами",
18
+ "Help": "Ёрдам",
19
+ "About DHIS2": "DHIS2 Ҳақида",
20
+ "Logout": "Чиқиб кетиш",
21
+ "New {{appName}} version available": "",
22
+ "New app version available": "",
23
+ "Click to reload": ""
24
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "Search apps": "Ilovalarni qidirish",
3
+ "DHIS2 {{dhis2Version}}": "",
4
+ "DHIS2 version unknown": "",
5
+ "{{appName}} version unknown": "",
6
+ "App {{appVersion}}": "",
7
+ "App version unknown": "",
8
+ "Debug info": "",
9
+ "Close": "Yopmoq",
10
+ "Copy debug info": "",
11
+ "Interpretations": "Talqinlar",
12
+ "Messages": "Xabarlar",
13
+ "Online": "Onlayn",
14
+ "Offline": "Offlayn",
15
+ "Edit profile": "Profilni tahrir qilish",
16
+ "Settings": "Sozlamalar",
17
+ "Account": "Raqami",
18
+ "Help": "Yordam",
19
+ "About DHIS2": "DHIS2 Haqida",
20
+ "Logout": "Chiqib ketish",
21
+ "New {{appName}} version available": "",
22
+ "New app version available": "",
23
+ "Click to reload": ""
24
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "Search apps": "Tìm kiếm ứng dụng",
3
+ "DHIS2 {{dhis2Version}}": "",
4
+ "DHIS2 version unknown": "",
5
+ "{{appName}} version unknown": "",
6
+ "App {{appVersion}}": "",
7
+ "App version unknown": "",
8
+ "Debug info": "",
9
+ "Close": "Đóng",
10
+ "Copy debug info": "",
11
+ "Interpretations": "Diễn giải ",
12
+ "Messages": "Tin nhắn",
13
+ "Online": "Trực tuyến",
14
+ "Offline": "Ngoại tuyến",
15
+ "Edit profile": "Chỉnh sửa hồ sơ",
16
+ "Settings": "Cài đặt",
17
+ "Account": "Tài khoản",
18
+ "Help": "Giúp đỡ",
19
+ "About DHIS2": "Giới thiệu DHIS2",
20
+ "Logout": "Đăng xuất",
21
+ "New {{appName}} version available": "",
22
+ "New app version available": "",
23
+ "Click to reload": ""
24
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "Search apps": "搜索 apps",
3
+ "DHIS2 {{dhis2Version}}": "DHIS2 {{dhis2Version}}",
4
+ "DHIS2 version unknown": "DHIS2 版本未知",
5
+ "{{appName}} version unknown": "{{appName}} 版本未知",
6
+ "App {{appVersion}}": "应用 {{appVersion}}",
7
+ "App version unknown": "应用版本未知",
8
+ "Debug info": "调试信息",
9
+ "Close": "关闭",
10
+ "Copy debug info": "复制调试信息",
11
+ "Interpretations": "解释",
12
+ "Messages": "消息",
13
+ "Online": "在线",
14
+ "Offline": "离线",
15
+ "Edit profile": "编辑简介",
16
+ "Settings": "设置",
17
+ "Account": "账号",
18
+ "Help": "帮助",
19
+ "About DHIS2": "关于DHIS2",
20
+ "Logout": "退出",
21
+ "New {{appName}} version available": " {{appName}} 有新版本可用",
22
+ "New app version available": "新的应用程序版本可用",
23
+ "Click to reload": "点击重新加载",
24
+ "Profile menu": "个人简介菜单"
25
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "Search apps": "搜索 apps",
3
+ "DHIS2 {{dhis2Version}}": "",
4
+ "DHIS2 version unknown": "",
5
+ "{{appName}} version unknown": "",
6
+ "App {{appVersion}}": "",
7
+ "App version unknown": "",
8
+ "Debug info": "",
9
+ "Close": "关闭",
10
+ "Copy debug info": "",
11
+ "Interpretations": "注释",
12
+ "Messages": "消息",
13
+ "Online": "在线",
14
+ "Offline": "离线",
15
+ "Edit profile": "编辑个人基本信息",
16
+ "Settings": "设置",
17
+ "Account": "账户",
18
+ "Help": "帮助",
19
+ "About DHIS2": "关于DHIS2",
20
+ "Logout": "退出",
21
+ "New {{appName}} version available": "",
22
+ "New app version available": "",
23
+ "Click to reload": ""
24
+ }
@@ -0,0 +1,71 @@
1
+ import { useDataQuery } from '@dhis2/app-runtime'
2
+ import { LogoIconWhite } from '@dhis2-ui/logo'
3
+ import React from 'react'
4
+ import css from 'styled-jsx/css'
5
+
6
+ const logoStyles = css.resolve`
7
+ svg {
8
+ height: 25px;
9
+ width: 27px;
10
+ }
11
+
12
+ img {
13
+ max-height: 100%;
14
+ min-height: auto;
15
+ width: auto;
16
+ }
17
+ `
18
+
19
+ const query = {
20
+ customLogo: {
21
+ resource: 'staticContent/logo_banner',
22
+ },
23
+ }
24
+
25
+ const pathExists = (data) =>
26
+ data &&
27
+ data.customLogo &&
28
+ data.customLogo.images &&
29
+ data.customLogo.images.png
30
+
31
+ export const LogoImage = () => {
32
+ const { loading, error, data } = useDataQuery(query)
33
+
34
+ if (loading) {
35
+ return null
36
+ }
37
+
38
+ let Logo
39
+ if (!error && pathExists(data)) {
40
+ Logo = (
41
+ <img
42
+ alt="Headerbar Logo"
43
+ src={data.customLogo.images.png}
44
+ className={logoStyles.className}
45
+ />
46
+ )
47
+ } else {
48
+ Logo = <LogoIconWhite className={logoStyles.className} />
49
+ }
50
+
51
+ return (
52
+ <div>
53
+ {Logo}
54
+
55
+ {logoStyles.styles}
56
+ <style jsx>{`
57
+ div {
58
+ padding: 4px;
59
+ min-width: 48px;
60
+ max-width: 250px;
61
+ height: 48px;
62
+
63
+ display: flex;
64
+ justify-content: center;
65
+ align-items: center;
66
+ overflow: hidden;
67
+ }
68
+ `}</style>
69
+ </div>
70
+ )
71
+ }
package/src/logo.js ADDED
@@ -0,0 +1,45 @@
1
+ import { useConfig } from '@dhis2/app-runtime'
2
+ import React from 'react'
3
+ import { LogoImage } from './logo-image.js'
4
+
5
+ export const Logo = () => {
6
+ const { baseUrl } = useConfig()
7
+
8
+ return (
9
+ <div data-test="headerbar-logo">
10
+ <a href={baseUrl}>
11
+ <LogoImage />
12
+ </a>
13
+
14
+ <style jsx>{`
15
+ div {
16
+ box-sizing: border-box;
17
+ min-width: 49px;
18
+ max-height: 48px;
19
+ margin-block: 0;
20
+ margin-inline-end: 12px;
21
+ margin-inline-start: 0;
22
+ border-inline-end: 1px solid rgba(32, 32, 32, 0.15);
23
+ }
24
+ div:hover {
25
+ background-color: #1a557f;
26
+ }
27
+
28
+ a,
29
+ a:hover,
30
+ a:focus,
31
+ a:active,
32
+ a:visited {
33
+ user-select: none;
34
+ }
35
+ a:focus {
36
+ outline: 2px solid white;
37
+ outline-offset: -2px;
38
+ }
39
+ a:focus:not(:focus-visible) {
40
+ outline: none;
41
+ }
42
+ `}</style>
43
+ </div>
44
+ )
45
+ }
@@ -0,0 +1,91 @@
1
+ import { colors, theme, spacers } from '@dhis2/ui-constants'
2
+ import { IconMessages24, IconMail24 } from '@dhis2/ui-icons'
3
+ import PropTypes from 'prop-types'
4
+ import React from 'react'
5
+ import i18n from './locales/index.js'
6
+
7
+ function icon(kind) {
8
+ if (kind === 'message') {
9
+ return <IconMessages24 color={colors.white} />
10
+ } else {
11
+ return <IconMail24 color={colors.white} />
12
+ }
13
+ }
14
+
15
+ export const NotificationIcon = ({
16
+ count = 0,
17
+ href,
18
+ kind,
19
+ dataTestId,
20
+ title,
21
+ 'aria-label': ariaLabel,
22
+ }) => (
23
+ <a
24
+ dir="ltr"
25
+ href={href}
26
+ className={kind}
27
+ data-test={dataTestId}
28
+ title={i18n.t(title)}
29
+ aria-label={i18n.t(ariaLabel)}
30
+ >
31
+ {icon(kind)}
32
+
33
+ {count > 0 && <span data-test={`${dataTestId}-count`}>{count}</span>}
34
+
35
+ <style jsx>{`
36
+ a {
37
+ position: relative;
38
+ margin: 0;
39
+ cursor: pointer;
40
+ padding: 0 ${spacers.dp12};
41
+ height: 100%;
42
+ display: flex;
43
+ align-items: center;
44
+ }
45
+ a:focus {
46
+ outline: 2px solid white;
47
+ outline-offset: -2px;
48
+ }
49
+ a:focus:not(:focus-visible) {
50
+ outline: none;
51
+ }
52
+ a:hover {
53
+ background: #1a557f;
54
+ }
55
+ a:active {
56
+ background: #104067;
57
+ }
58
+ span {
59
+ display: flex;
60
+ justify-content: center;
61
+ align-items: center;
62
+ z-index: 1;
63
+ position: absolute;
64
+ top: 3px;
65
+ inset-inline-end: 2px;
66
+ min-width: 18px;
67
+ min-height: 18px;
68
+ border-radius: ${spacers.dp12};
69
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1),
70
+ 0 1px 2px 0 rgba(0, 0, 0, 0.06);
71
+ background-color: ${theme.secondary500};
72
+ color: ${colors.white};
73
+ font-size: 13px;
74
+ font-weight: 600;
75
+ line-height: 15px;
76
+ text-align: center;
77
+ cursor: inherit;
78
+ padding: 0 ${spacers.dp4};
79
+ }
80
+ `}</style>
81
+ </a>
82
+ )
83
+
84
+ NotificationIcon.propTypes = {
85
+ 'aria-label': PropTypes.string.isRequired,
86
+ href: PropTypes.string.isRequired,
87
+ title: PropTypes.string.isRequired,
88
+ count: PropTypes.number,
89
+ dataTestId: PropTypes.string,
90
+ kind: PropTypes.oneOf(['interpretation', 'message']),
91
+ }
@@ -0,0 +1,63 @@
1
+ import { useConfig } from '@dhis2/app-runtime'
2
+ import PropTypes from 'prop-types'
3
+ import React from 'react'
4
+ import { joinPath } from './join-path.js'
5
+ import i18n from './locales/index.js'
6
+ import { NotificationIcon } from './notification-icon.js'
7
+
8
+ const hasAuthority = (userAuthorities, authId) =>
9
+ Array.isArray(userAuthorities) &&
10
+ userAuthorities.some(
11
+ (userAuthId) => userAuthId === 'ALL' || userAuthId === authId
12
+ )
13
+
14
+ export const Notifications = ({
15
+ interpretations,
16
+ messages,
17
+ userAuthorities,
18
+ }) => {
19
+ const { baseUrl } = useConfig()
20
+
21
+ return (
22
+ <div data-test="headerbar-notifications">
23
+ {hasAuthority(userAuthorities, 'M_dhis-web-interpretation') && (
24
+ <NotificationIcon
25
+ count={interpretations}
26
+ href={joinPath(baseUrl, 'dhis-web-interpretation')}
27
+ kind="message"
28
+ dataTestId="headerbar-interpretations"
29
+ title={i18n.t('Interpretations')}
30
+ aria-label={i18n.t('Interpretations')}
31
+ />
32
+ )}
33
+
34
+ {hasAuthority(userAuthorities, 'M_dhis-web-messaging') && (
35
+ <NotificationIcon
36
+ message="email"
37
+ count={messages}
38
+ href={joinPath(baseUrl, 'dhis-web-messaging')}
39
+ kind="interpretation"
40
+ dataTestId="headerbar-messages"
41
+ title={i18n.t('Messages')}
42
+ aria-label={i18n.t('Messages')}
43
+ />
44
+ )}
45
+
46
+ <style jsx>{`
47
+ div {
48
+ user-select: none;
49
+ display: flex;
50
+ flex-direction: row;
51
+ align-items: center;
52
+ height: 100%;
53
+ }
54
+ `}</style>
55
+ </div>
56
+ )
57
+ }
58
+
59
+ Notifications.propTypes = {
60
+ interpretations: PropTypes.number,
61
+ messages: PropTypes.number,
62
+ userAuthorities: PropTypes.arrayOf(PropTypes.string),
63
+ }
@@ -0,0 +1,40 @@
1
+ import {
2
+ useDhis2ConnectionStatus,
3
+ useOnlineStatusMessage,
4
+ } from '@dhis2/app-runtime'
5
+ import cx from 'classnames'
6
+ import PropTypes from 'prop-types'
7
+ import React from 'react'
8
+ import i18n from './locales/index.js'
9
+ import styles from './online-status.styles.js'
10
+
11
+ /** A badge to display online/offline status in the header bar */
12
+ export function OnlineStatus({ dense }) {
13
+ const { isConnected: online } = useDhis2ConnectionStatus()
14
+ const { onlineStatusMessage } = useOnlineStatusMessage()
15
+
16
+ const displayStatus = online ? i18n.t('Online') : i18n.t('Offline')
17
+
18
+ return (
19
+ <div
20
+ className={cx('container', dense ? 'bar' : 'badge')}
21
+ data-test="headerbar-online-status"
22
+ >
23
+ {onlineStatusMessage && !dense && (
24
+ <span className="info unselectable">{onlineStatusMessage}</span>
25
+ )}
26
+ <div className={cx('icon', online ? 'online' : 'offline')}></div>
27
+ <span className="label unselectable">{displayStatus}</span>
28
+ {onlineStatusMessage && dense && (
29
+ <span className="info-dense unselectable">
30
+ {onlineStatusMessage}
31
+ </span>
32
+ )}
33
+ <style jsx>{styles}</style>
34
+ </div>
35
+ )
36
+ }
37
+ OnlineStatus.propTypes = {
38
+ /** If true, displays as a sub-bar instead of a badge */
39
+ dense: PropTypes.bool,
40
+ }
@@ -0,0 +1,91 @@
1
+ import { colors, spacers } from '@dhis2/ui-constants'
2
+ import css from 'styled-jsx/css'
3
+
4
+ export default css`
5
+ .container {
6
+ display: flex;
7
+ align-items: center;
8
+ justify-content: center; // new
9
+ background-color: #104167;
10
+ flex-shrink: 0; // ?
11
+ color: ${colors.grey100};
12
+ }
13
+
14
+ .container.badge {
15
+ margin-inline-end: ${spacers.dp8};
16
+ padding: ${spacers.dp8};
17
+ border-radius: 5px;
18
+ font-size: 14px;
19
+ }
20
+
21
+ .container.bar {
22
+ display: none;
23
+ padding: 0px ${spacers.dp4};
24
+ min-height: 24px;
25
+ font-size: 13px;
26
+ }
27
+
28
+ @media (max-width: 480px) {
29
+ .container.badge {
30
+ display: none;
31
+ }
32
+
33
+ .container.bar {
34
+ display: flex;
35
+ }
36
+ }
37
+
38
+ .unselectable {
39
+ cursor: default;
40
+ user-select: none;
41
+ }
42
+
43
+ .info {
44
+ margin-inline-end: ${spacers.dp16};
45
+ }
46
+
47
+ .info-dense {
48
+ margin-inline-start: ${spacers.dp12};
49
+ font-size: 12px;
50
+ }
51
+
52
+ .icon {
53
+ width: 8px;
54
+ min-width: 8px;
55
+ height: 8px;
56
+ border-radius: 8px;
57
+ margin-inline-end: ${spacers.dp4};
58
+ }
59
+
60
+ .icon.online {
61
+ background-color: ${colors.teal400};
62
+ }
63
+
64
+ .icon.offline {
65
+ background-color: transparent;
66
+ border: 1px solid ${colors.yellow300};
67
+ }
68
+
69
+ .icon.reconnecting {
70
+ background: ${colors.grey300};
71
+ -webkit-animation: fadeinout 2s linear infinite;
72
+ animation: fadeinout 2s linear infinite;
73
+ opacity: 0;
74
+ }
75
+
76
+ @-webkit-keyframes fadeinout {
77
+ 50% {
78
+ opacity: 1;
79
+ }
80
+ }
81
+
82
+ @keyframes fadeinout {
83
+ 50% {
84
+ opacity: 1;
85
+ }
86
+ }
87
+
88
+ .label {
89
+ letter-spacing: 0.5px;
90
+ }
91
+ `
@@ -0,0 +1,23 @@
1
+ import { useEffect, useMemo } from 'react'
2
+
3
+ export const useOnDocClick = (containerRef, hide) => {
4
+ const onDocClick = useMemo(() => {
5
+ return (evt) => {
6
+ if (!containerRef.current) {
7
+ return null
8
+ }
9
+
10
+ if (!containerRef.current.contains(evt.target)) {
11
+ hide()
12
+ }
13
+ }
14
+ }, [containerRef, hide])
15
+
16
+ useEffect(() => {
17
+ document.addEventListener('click', onDocClick)
18
+
19
+ return () => {
20
+ document.removeEventListener('click', onDocClick)
21
+ }
22
+ }, [onDocClick])
23
+ }
@@ -0,0 +1,40 @@
1
+ import { renderHook } from '@testing-library/react'
2
+ import { useOnDocClick } from './use-on-doc-click.js'
3
+
4
+ describe('useOnDocClick', () => {
5
+ let eventListenerMap = {}
6
+ const hide = jest.fn()
7
+
8
+ jest.spyOn(document, 'addEventListener').mockImplementation(
9
+ (event, callback) => {
10
+ eventListenerMap[event] = callback
11
+ }
12
+ )
13
+
14
+ beforeEach(() => {
15
+ eventListenerMap = {}
16
+ })
17
+
18
+ afterEach(() => {
19
+ document.addEventListener.mockClear()
20
+ hide.mockClear()
21
+ })
22
+
23
+ it('should call the hide function when clicking outside', () => {
24
+ const el = document.createElement('span')
25
+ const containerRef = { current: el }
26
+ renderHook(() => useOnDocClick(containerRef, hide))
27
+
28
+ eventListenerMap.click({ target: document.body })
29
+ expect(hide).toHaveBeenCalled()
30
+ })
31
+
32
+ it('should not call the hide function when clicking inside', () => {
33
+ const el = document.createElement('span')
34
+ const containerRef = { current: el }
35
+ renderHook(() => useOnDocClick(containerRef, hide))
36
+
37
+ eventListenerMap.click({ target: el })
38
+ expect(hide).not.toHaveBeenCalled()
39
+ })
40
+ })
@@ -0,0 +1 @@
1
+ export * from './profile-menu.js'