@dhis2-ui/header-bar 8.4.17 → 8.5.0-beta.2

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 (53) hide show
  1. package/build/cjs/__e2e__/header-bar.stories.e2e.js +40 -0
  2. package/build/cjs/__e2e__/stories/common.js +99 -32
  3. package/build/cjs/__e2e__/stories/with-debug-info-edge-cases.js +45 -0
  4. package/build/cjs/__e2e__/stories/with-update-available-notification.js +38 -0
  5. package/build/cjs/apps.js +1 -1
  6. package/build/cjs/debug-info/debug-info-menu-item.js +67 -0
  7. package/build/cjs/debug-info/debug-info-modal.js +59 -0
  8. package/build/cjs/debug-info/debug-info-table.js +47 -0
  9. package/build/cjs/debug-info/use-debug-info.js +28 -0
  10. package/build/cjs/features/the_headerbar_should_display_app_update_notification/index.js +34 -0
  11. package/build/cjs/features/the_headerbar_should_display_app_update_notification.feature +22 -0
  12. package/build/cjs/features/the_headerbar_should_display_debug_version_infos/index.js +69 -0
  13. package/build/cjs/features/the_headerbar_should_display_debug_version_infos.feature +52 -0
  14. package/build/cjs/header-bar-context.js +46 -0
  15. package/build/cjs/header-bar.js +22 -11
  16. package/build/cjs/header-bar.stories.js +70 -72
  17. package/build/cjs/locales/en/translations.json +12 -1
  18. package/build/cjs/profile/use-on-doc-click.js +30 -0
  19. package/build/cjs/profile/use-on-doc-click.test.js +42 -0
  20. package/build/cjs/profile-menu/index.js +18 -0
  21. package/build/cjs/{profile → profile-menu}/profile-header.js +0 -0
  22. package/build/cjs/{profile → profile-menu}/profile-menu.js +23 -15
  23. package/build/cjs/profile-menu/update-notification.js +70 -0
  24. package/build/cjs/profile.js +52 -62
  25. package/build/es/__e2e__/header-bar.stories.e2e.js +5 -3
  26. package/build/es/__e2e__/stories/common.js +96 -33
  27. package/build/es/__e2e__/stories/with-debug-info-edge-cases.js +22 -0
  28. package/build/es/__e2e__/stories/with-update-available-notification.js +19 -0
  29. package/build/es/apps.js +1 -1
  30. package/build/es/debug-info/debug-info-menu-item.js +48 -0
  31. package/build/es/debug-info/debug-info-modal.js +41 -0
  32. package/build/es/debug-info/debug-info-table.js +35 -0
  33. package/build/es/debug-info/use-debug-info.js +15 -0
  34. package/build/es/features/the_headerbar_should_display_app_update_notification/index.js +31 -0
  35. package/build/es/features/the_headerbar_should_display_app_update_notification.feature +22 -0
  36. package/build/es/features/the_headerbar_should_display_debug_version_infos/index.js +66 -0
  37. package/build/es/features/the_headerbar_should_display_debug_version_infos.feature +52 -0
  38. package/build/es/header-bar-context.js +25 -0
  39. package/build/es/header-bar.js +21 -11
  40. package/build/es/header-bar.stories.js +65 -71
  41. package/build/es/locales/en/translations.json +12 -1
  42. package/build/es/profile/use-on-doc-click.js +20 -0
  43. package/build/es/profile/use-on-doc-click.test.js +38 -0
  44. package/build/es/profile-menu/index.js +1 -0
  45. package/build/es/{profile → profile-menu}/profile-header.js +0 -0
  46. package/build/es/{profile → profile-menu}/profile-menu.js +23 -17
  47. package/build/es/profile-menu/update-notification.js +51 -0
  48. package/build/es/profile.js +49 -64
  49. package/package.json +15 -13
  50. package/build/cjs/features/the_headerbar_displays_instance_and_app_infos/index.js +0 -15
  51. package/build/cjs/features/the_headerbar_displays_instance_and_app_infos.feature +0 -29
  52. package/build/es/features/the_headerbar_displays_instance_and_app_infos/index.js +0 -12
  53. package/build/es/features/the_headerbar_displays_instance_and_app_infos.feature +0 -29
@@ -0,0 +1,66 @@
1
+ import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps';
2
+ Given('the HeaderBar is rendered without an instance version in runtime context', () => {
3
+ cy.visitStory('HeaderBarTesting', 'With Unknown Instance Version');
4
+ });
5
+ Given('the HeaderBar is rendered with an app name and app version in runtime context', () => {
6
+ cy.visitStory('HeaderBarTesting', 'default');
7
+ });
8
+ Given('the HeaderBar is rendered without app name in runtime context', () => {
9
+ cy.visitStory('HeaderBarTesting', 'With Unknown App Name');
10
+ });
11
+ Given('the HeaderBar is rendered with an app name but without app version in runtime context', () => {
12
+ cy.visitStory('HeaderBarTesting', 'With Unknown App Version');
13
+ });
14
+ Given('the HeaderBar is rendered without app name or app version in runtime context', () => {
15
+ cy.visitStory('HeaderBarTesting', 'With Unknown App Name And Version');
16
+ });
17
+ When('the user opens the profile menu', () => {
18
+ cy.get('[data-test="headerbar-profile"] > button').click();
19
+ });
20
+ Then("the app's name and version should be displayed", () => {
21
+ cy.get('[data-test="dhis2-ui-headerbar-appinfo"]').should('contain', 'TestApp 101.2.3-beta.4');
22
+ });
23
+ Then("the app's name with unknown version should be displayed", () => {
24
+ cy.get('[data-test="dhis2-ui-headerbar-appinfo"]').should('contain', 'TestApp version unknown');
25
+ });
26
+ Then("the unknown app with app's version should be displayed", () => {
27
+ cy.get('[data-test="dhis2-ui-headerbar-appinfo"]').should('contain', 'App 101.2.3-beta.4');
28
+ });
29
+ Then('the unknown app with unknown version should be displayed', () => {
30
+ cy.get('[data-test="dhis2-ui-headerbar-appinfo"]').should('contain', 'App version unknown');
31
+ });
32
+ Then('the instance version should be displayed', () => {
33
+ cy.get('[data-test="dhis2-ui-headerbar-instanceinfo"]').should('contain', 'DHIS2 2.39.2.1-SNAPSHOT');
34
+ });
35
+ Then('the instance version should show as unknown', () => {
36
+ cy.get('[data-test="dhis2-ui-headerbar-instanceinfo"]').should('contain', 'DHIS2 version unknown');
37
+ });
38
+ When('the user clicks the debug info menu item', () => {
39
+ cy.get('[data-test="dhis2-ui-headerbar-debuginfo"] > a').click();
40
+ });
41
+ Then('the debug info modal should be shown', () => {
42
+ cy.get('[data-test="dhis2-ui-headerbar-debuginfomodal"]').should('be.visible');
43
+ });
44
+ Then('the debug info modal should not be shown', () => {
45
+ cy.get('[data-test="dhis2-ui-headerbar-debuginfomodal"]').should('not.exist');
46
+ });
47
+ Then('the debug info modal should contain debug info', () => {
48
+ cy.get('[data-test="dhis2-ui-headerbar-debuginfotable"]').should('contain', '2.39.2.1-SNAPSHOT' // DHIS2 version
49
+ ).should('contain', '6607c3c' // Revision
50
+ ).should('contain', 'TestApp' // App name
51
+ ).should('contain', '101.2.3-beta.4' // App version
52
+ );
53
+ });
54
+ When('the user clicks the copy debug info button', () => {
55
+ cy.contains('Copy debug info').click();
56
+ });
57
+ Then('the debug info should be copied to clipboard', () => {
58
+ cy.window().then(win => {
59
+ win.navigator.clipboard.readText().then(text => {
60
+ expect(text).to.contain('2.39.2.1-SNAPSHOT');
61
+ });
62
+ });
63
+ });
64
+ Then('the debug info copied to clipboard alert should be shown', () => {
65
+ cy.contains('Debug information copied to clipboard').should('exist');
66
+ });
@@ -0,0 +1,52 @@
1
+ Feature: The HeaderBar should display debug version infos
2
+
3
+ Scenario: The debug version infos are displayed in the profile menu
4
+ Given the HeaderBar is rendered with an app name and app version in runtime context
5
+ When the user opens the profile menu
6
+ Then the instance version should be displayed
7
+ And the app's name and version should be displayed
8
+
9
+ Scenario: The debug version info modal is displayed when clicking on the menu item
10
+ Given the HeaderBar is rendered with an app name and app version in runtime context
11
+ When the user opens the profile menu
12
+ When the user clicks the debug info menu item
13
+ Then the debug info modal should be shown
14
+
15
+ Scenario: The debug version info modal displays debug info
16
+ Given the HeaderBar is rendered with an app name and app version in runtime context
17
+ When the user opens the profile menu
18
+ When the user clicks the debug info menu item
19
+ Then the debug info modal should contain debug info
20
+
21
+ Scenario: The debug version info should be copied to clipboard
22
+ Given the HeaderBar is rendered with an app name and app version in runtime context
23
+ When the user opens the profile menu
24
+ When the user clicks the debug info menu item
25
+ When the user clicks the copy debug info button
26
+ Then the debug info should be copied to clipboard
27
+ And the debug info copied to clipboard alert should be shown
28
+ And the debug info modal should not be shown
29
+
30
+ Scenario: The debug version infos are displayed with unknown dhis2 version in the profile menu
31
+ Given the HeaderBar is rendered without an instance version in runtime context
32
+ When the user opens the profile menu
33
+ Then the instance version should show as unknown
34
+ And the app's name and version should be displayed
35
+
36
+ Scenario: The debug version infos are displayed with unknown app name and version in the profile menu
37
+ Given the HeaderBar is rendered without app name or app version in runtime context
38
+ When the user opens the profile menu
39
+ Then the instance version should be displayed
40
+ And the unknown app with unknown version should be displayed
41
+
42
+ Scenario: The debug version infos are displayed with unknown app name in the profile menu
43
+ Given the HeaderBar is rendered without app name in runtime context
44
+ When the user opens the profile menu
45
+ Then the instance version should be displayed
46
+ And the unknown app with app's version should be displayed
47
+
48
+ Scenario: The debug version infos are displayed with unknown app version in the profile menu
49
+ Given the HeaderBar is rendered with an app name but without app version in runtime context
50
+ When the user opens the profile menu
51
+ Then the instance version should be displayed
52
+ And the app's name with unknown version should be displayed
@@ -0,0 +1,25 @@
1
+ import PropTypes from 'prop-types';
2
+ import React, { createContext, useContext } from 'react';
3
+ const headerBarContext = /*#__PURE__*/createContext({
4
+ updateAvailable: false,
5
+ onApplyAvailableUpdate: () => {}
6
+ });
7
+ export const HeaderBarContextProvider = _ref => {
8
+ let {
9
+ updateAvailable,
10
+ onApplyAvailableUpdate,
11
+ children
12
+ } = _ref;
13
+ return /*#__PURE__*/React.createElement(headerBarContext.Provider, {
14
+ value: {
15
+ updateAvailable,
16
+ onApplyAvailableUpdate
17
+ }
18
+ }, children);
19
+ };
20
+ HeaderBarContextProvider.propTypes = {
21
+ children: PropTypes.node,
22
+ updateAvailable: PropTypes.bool,
23
+ onApplyAvailableUpdate: PropTypes.func
24
+ };
25
+ export const useHeaderBarContext = () => useContext(headerBarContext);
@@ -4,6 +4,7 @@ import { colors } from '@dhis2/ui-constants';
4
4
  import PropTypes from 'prop-types';
5
5
  import React, { useMemo } from 'react';
6
6
  import Apps from './apps.js';
7
+ import { HeaderBarContextProvider } from './header-bar-context.js';
7
8
  import { joinPath } from './join-path.js';
8
9
  import i18n from './locales/index.js';
9
10
  import { Logo } from './logo.js';
@@ -36,9 +37,12 @@ export const HeaderBar = _ref => {
36
37
 
37
38
  let {
38
39
  appName,
39
- className
40
+ className,
41
+ updateAvailable,
42
+ onApplyAvailableUpdate
40
43
  } = _ref;
41
44
  const {
45
+ appName: configAppName,
42
46
  baseUrl,
43
47
  pwaEnabled,
44
48
  headerbar: {
@@ -57,24 +61,28 @@ export const HeaderBar = _ref => {
57
61
  icon: getPath(app.icon),
58
62
  defaultAction: getPath(app.defaultAction)
59
63
  }));
60
- }, [data]); // See https://jira.dhis2.org/browse/LIBS-180
64
+ }, [data, baseUrl]); // See https://jira.dhis2.org/browse/LIBS-180
61
65
 
62
66
  if (!loading && !error) {
63
- // TODO: This will run every render which is probably wrong! Also, setting the global locale shouldn't be done in the headerbar
67
+ // TODO: This will run every render which is probably wrong!
68
+ // Also, setting the global locale shouldn't be done in the headerbar
64
69
  const locale = data.user.settings.keyUiLocale || 'en';
65
70
  i18n.setDefaultNamespace('default');
66
71
  i18n.changeLanguage(locale);
67
72
  }
68
73
 
69
- return /*#__PURE__*/React.createElement("header", {
70
- className: _JSXStyle.dynamic([["3517276904", [colors.white]]]) + " " + (className || "")
74
+ return /*#__PURE__*/React.createElement(HeaderBarContextProvider, {
75
+ updateAvailable: updateAvailable,
76
+ onApplyAvailableUpdate: onApplyAvailableUpdate
77
+ }, /*#__PURE__*/React.createElement("header", {
78
+ className: _JSXStyle.dynamic([["3860078320", [colors.white]]]) + " " + (className || "")
71
79
  }, /*#__PURE__*/React.createElement("div", {
72
- className: _JSXStyle.dynamic([["3517276904", [colors.white]]]) + " " + "main"
80
+ className: _JSXStyle.dynamic([["3860078320", [colors.white]]]) + " " + "main"
73
81
  }, !loading && !error && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Logo, null), /*#__PURE__*/React.createElement(Title, {
74
- app: appName,
82
+ app: appName || configAppName,
75
83
  instance: data.title.applicationTitle
76
84
  }), /*#__PURE__*/React.createElement("div", {
77
- className: _JSXStyle.dynamic([["3517276904", [colors.white]]]) + " " + "right-control-spacer"
85
+ className: _JSXStyle.dynamic([["3860078320", [colors.white]]]) + " " + "right-control-spacer"
78
86
  }), (pwaEnabled || showOnlineStatus) && /*#__PURE__*/React.createElement(OnlineStatus, null), /*#__PURE__*/React.createElement(Notifications, {
79
87
  interpretations: data.notifications.unreadInterpretations,
80
88
  messages: data.notifications.unreadMessageConversations,
@@ -89,11 +97,13 @@ export const HeaderBar = _ref => {
89
97
  }))), (pwaEnabled || showOnlineStatus) && !loading && !error && /*#__PURE__*/React.createElement(OnlineStatus, {
90
98
  dense: true
91
99
  }), /*#__PURE__*/React.createElement(_JSXStyle, {
92
- id: "3517276904",
100
+ id: "3860078320",
93
101
  dynamic: [colors.white]
94
- }, [".main.__jsx-style-dynamic-selector{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;background-color:#2c6693;border-bottom:1px solid rgba(32,32,32,0.15);color:".concat(colors.white, ";height:48px;}"), ".right-control-spacer.__jsx-style-dynamic-selector{margin-left:auto;}"]));
102
+ }, [".main.__jsx-style-dynamic-selector{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;background-color:#2c6693;border-bottom:1px solid rgba(32,32,32,0.15);color:".concat(colors.white, ";height:48px;}"), ".right-control-spacer.__jsx-style-dynamic-selector{margin-left:auto;}"])));
95
103
  };
96
104
  HeaderBar.propTypes = {
97
105
  appName: PropTypes.string,
98
- className: PropTypes.string
106
+ className: PropTypes.string,
107
+ updateAvailable: PropTypes.bool,
108
+ onApplyAvailableUpdate: PropTypes.func
99
109
  };
@@ -1,27 +1,9 @@
1
- import { CustomDataProvider, Provider } from '@dhis2/app-runtime';
1
+ import { CustomDataProvider } from '@dhis2/app-runtime';
2
2
  import React from 'react';
3
+ import { createDecoratorProvider, providerConfig } from './__e2e__/stories/common.js';
3
4
  import { HeaderBar } from './header-bar.js';
4
5
  const subtitle = 'The common navigation bar used in all DHIS2 apps';
5
6
  const description = "\nThe header bar is mandatory for all apps. This creates a stable, understandable point of reference for the user across all kinds of different apps. It must always be displayed fixed to the top of the screen. Do not interfere or obstruct interaction with the header bar.\n\nThe header bar is included automatically with the App Shell and should not need any configuration.\n\n#### Theme\n\nThe header bar can be themeed to suit the brand/color of your DHIS2 instance. The color of the text/icons will be automatically adjusted based on the selected color.\n\n```js\nimport { HeaderBar } from '@dhis2/ui'\n```\n";
6
- export default {
7
- title: 'Header Bar',
8
- component: HeaderBar,
9
- parameters: {
10
- componentSubtitle: subtitle,
11
- docs: {
12
- description: {
13
- component: description
14
- }
15
- }
16
- },
17
- args: {
18
- appName: 'Example!'
19
- }
20
- };
21
- const mockConfig = {
22
- baseUrl: 'https://debug.dhis2.org/dev/',
23
- apiVersion: 33
24
- };
25
7
  const customData = {
26
8
  'systemSettings/applicationTitle': {
27
9
  applicationTitle: 'Foobar'
@@ -142,56 +124,64 @@ const customAuthoritiesData = { ...customData,
142
124
  authorities: ['M_dhis-web-messaging']
143
125
  }
144
126
  };
145
- export const Default = args => /*#__PURE__*/React.createElement(Provider, {
146
- config: mockConfig
147
- }, /*#__PURE__*/React.createElement(CustomDataProvider, {
127
+ export default {
128
+ title: 'Header Bar',
129
+ component: HeaderBar,
130
+ parameters: {
131
+ componentSubtitle: subtitle,
132
+ docs: {
133
+ description: {
134
+ component: description
135
+ }
136
+ }
137
+ },
138
+ decorators: [createDecoratorProvider()]
139
+ };
140
+ export const Default = () => /*#__PURE__*/React.createElement(CustomDataProvider, {
148
141
  data: customData
149
- }, /*#__PURE__*/React.createElement(HeaderBar, args)));
150
- export const CustomLogoWideDimension = args => /*#__PURE__*/React.createElement(Provider, {
151
- config: mockConfig
152
- }, /*#__PURE__*/React.createElement(CustomDataProvider, {
142
+ }, /*#__PURE__*/React.createElement(HeaderBar, {
143
+ appName: "Example!"
144
+ }));
145
+ export const CustomLogoWideDimension = () => /*#__PURE__*/React.createElement(CustomDataProvider, {
153
146
  data: customLogoData
154
- }, /*#__PURE__*/React.createElement(HeaderBar, args)));
147
+ }, /*#__PURE__*/React.createElement(HeaderBar, {
148
+ appName: "Example!"
149
+ }));
155
150
  CustomLogoWideDimension.storyName = 'Custom Logo (wide dimension)';
156
- export const NonEnglishUserLocale = args => /*#__PURE__*/React.createElement(Provider, {
157
- config: mockConfig
158
- }, /*#__PURE__*/React.createElement(CustomDataProvider, {
151
+ export const NonEnglishUserLocale = () => /*#__PURE__*/React.createElement(CustomDataProvider, {
159
152
  data: customLocaleData
160
- }, /*#__PURE__*/React.createElement(HeaderBar, args)));
161
- NonEnglishUserLocale.args = {
162
- appName: 'Exemple!'
163
- };
153
+ }, /*#__PURE__*/React.createElement(HeaderBar, {
154
+ appName: "Exemple!"
155
+ }));
164
156
  NonEnglishUserLocale.storyName = 'Non-english user locale';
165
- export const NoAuthorityForInterpretationsApp = args => /*#__PURE__*/React.createElement(Provider, {
166
- config: mockConfig
167
- }, /*#__PURE__*/React.createElement(CustomDataProvider, {
157
+ export const NoAuthorityForInterpretationsApp = () => /*#__PURE__*/React.createElement(CustomDataProvider, {
168
158
  data: customAuthoritiesData
169
- }, /*#__PURE__*/React.createElement(HeaderBar, args)));
159
+ }, /*#__PURE__*/React.createElement(HeaderBar, {
160
+ appName: "Example!"
161
+ }));
170
162
  NoAuthorityForInterpretationsApp.storyName = 'No authority for interpretations app';
171
- export const Loading = args => /*#__PURE__*/React.createElement(Provider, {
172
- config: mockConfig
173
- }, /*#__PURE__*/React.createElement(CustomDataProvider, {
163
+ export const Loading = () => /*#__PURE__*/React.createElement(CustomDataProvider, {
174
164
  options: {
175
165
  loadForever: true
176
166
  }
177
- }, /*#__PURE__*/React.createElement(HeaderBar, args)));
167
+ }, /*#__PURE__*/React.createElement(HeaderBar, {
168
+ appName: "Example!"
169
+ }));
178
170
  Loading.storyName = 'Loading...';
179
- export const Error = args => /*#__PURE__*/React.createElement(Provider, {
180
- config: mockConfig
181
- }, /*#__PURE__*/React.createElement(CustomDataProvider, {
171
+ export const Error = () => /*#__PURE__*/React.createElement(CustomDataProvider, {
182
172
  data: {}
183
- }, /*#__PURE__*/React.createElement(HeaderBar, args)));
173
+ }, /*#__PURE__*/React.createElement(HeaderBar, {
174
+ appName: "Exemple!"
175
+ }));
184
176
  Error.storyName = 'Error!';
185
- export const WithOnlineStatus = args => {
186
- const config = { ...mockConfig,
187
- pwaEnabled: true
188
- };
189
- return /*#__PURE__*/React.createElement(Provider, {
190
- config: config
191
- }, /*#__PURE__*/React.createElement(CustomDataProvider, {
192
- data: customData
193
- }, /*#__PURE__*/React.createElement(HeaderBar, args)));
194
- };
177
+ export const WithOnlineStatus = () => /*#__PURE__*/React.createElement(CustomDataProvider, {
178
+ data: customData
179
+ }, /*#__PURE__*/React.createElement(HeaderBar, {
180
+ appName: "Exemple!"
181
+ }));
182
+ WithOnlineStatus.decorators = [createDecoratorProvider({ ...providerConfig,
183
+ pwaEnabled: true
184
+ })];
195
185
  WithOnlineStatus.parameters = {
196
186
  docs: {
197
187
  description: {
@@ -202,19 +192,17 @@ WithOnlineStatus.parameters = {
202
192
  }
203
193
  }
204
194
  };
205
- export const WithLastOnlineInfo = args => {
206
- const config = { ...mockConfig,
207
- pwaEnabled: true,
208
- headerbar: {
209
- onlineStatusInfo: 'LAST_ONLINE'
210
- }
211
- };
212
- return /*#__PURE__*/React.createElement(Provider, {
213
- config: config
214
- }, /*#__PURE__*/React.createElement(CustomDataProvider, {
215
- data: customData
216
- }, /*#__PURE__*/React.createElement(HeaderBar, args)));
217
- };
195
+ export const WithLastOnlineInfo = () => /*#__PURE__*/React.createElement(CustomDataProvider, {
196
+ data: customData
197
+ }, /*#__PURE__*/React.createElement(HeaderBar, {
198
+ appName: "Exemple!"
199
+ }));
200
+ WithLastOnlineInfo.decorators = [createDecoratorProvider({ ...providerConfig,
201
+ pwaEnabled: true,
202
+ headerbar: {
203
+ onlineStatusInfo: 'LAST_ONLINE'
204
+ }
205
+ })];
218
206
  WithLastOnlineInfo.parameters = {
219
207
  docs: {
220
208
  description: {
@@ -222,4 +210,10 @@ WithLastOnlineInfo.parameters = {
222
210
  time since last online.'
223
211
  }
224
212
  }
225
- };
213
+ };
214
+ export const WithUpdateNotification = () => /*#__PURE__*/React.createElement(CustomDataProvider, {
215
+ data: customData
216
+ }, /*#__PURE__*/React.createElement(HeaderBar, {
217
+ appName: "Data Visualizer",
218
+ updateAvailable: true
219
+ }));
@@ -1,5 +1,13 @@
1
1
  {
2
2
  "Search apps": "Search apps",
3
+ "DHIS2 {{dhis2Version}}": "DHIS2 {{dhis2Version}}",
4
+ "DHIS2 version unknown": "DHIS2 version unknown",
5
+ "{{appName}} version unknown": "{{appName}} version unknown",
6
+ "App {{appVersion}}": "App {{appVersion}}",
7
+ "App version unknown": "App version unknown",
8
+ "Debug info": "Debug info",
9
+ "Close": "Close",
10
+ "Copy debug info": "Copy debug info",
3
11
  "Last online {{relativeTime}}": "Last online {{relativeTime}}",
4
12
  "Online": "Online",
5
13
  "Offline": "Offline",
@@ -8,5 +16,8 @@
8
16
  "Account": "Account",
9
17
  "Help": "Help",
10
18
  "About DHIS2": "About DHIS2",
11
- "Logout": "Logout"
19
+ "Logout": "Logout",
20
+ "New {{appName}} version available": "New {{appName}} version available",
21
+ "New app version available": "New app version available",
22
+ "Click to reload": "Click to reload"
12
23
  }
@@ -0,0 +1,20 @@
1
+ import { useEffect, useMemo } from 'react';
2
+ export const useOnDocClick = (containerRef, hide) => {
3
+ const onDocClick = useMemo(() => {
4
+ return evt => {
5
+ if (!containerRef.current) {
6
+ return null;
7
+ }
8
+
9
+ if (!containerRef.current.contains(evt.target)) {
10
+ hide();
11
+ }
12
+ };
13
+ }, [containerRef, hide]);
14
+ useEffect(() => {
15
+ document.addEventListener('click', onDocClick);
16
+ return () => {
17
+ document.removeEventListener('click', onDocClick);
18
+ };
19
+ }, [onDocClick]);
20
+ };
@@ -0,0 +1,38 @@
1
+ import { renderHook } from '@testing-library/react-hooks';
2
+ import { useOnDocClick } from './use-on-doc-click.js';
3
+ describe('useOnDocClick', () => {
4
+ let eventListenerMap = {};
5
+ const hide = jest.fn();
6
+ jest.spyOn(document, 'addEventListener').mockImplementation((event, callback) => {
7
+ eventListenerMap[event] = callback;
8
+ });
9
+ beforeEach(() => {
10
+ eventListenerMap = {};
11
+ });
12
+ afterEach(() => {
13
+ document.addEventListener.mockClear();
14
+ hide.mockClear();
15
+ });
16
+ it('should call the hide function when clicking outside', () => {
17
+ const el = document.createElement('span');
18
+ const containerRef = {
19
+ current: el
20
+ };
21
+ renderHook(() => useOnDocClick(containerRef, hide));
22
+ eventListenerMap.click({
23
+ target: document.body
24
+ });
25
+ expect(hide).toHaveBeenCalled();
26
+ });
27
+ it('should not call the hide function when clicking inside', () => {
28
+ const el = document.createElement('span');
29
+ const containerRef = {
30
+ current: el
31
+ };
32
+ renderHook(() => useOnDocClick(containerRef, hide));
33
+ eventListenerMap.click({
34
+ target: el
35
+ });
36
+ expect(hide).not.toHaveBeenCalled();
37
+ });
38
+ });
@@ -0,0 +1 @@
1
+ export * from './profile-menu.js';
@@ -4,15 +4,17 @@ import { Center } from '@dhis2-ui/center';
4
4
  import { Divider } from '@dhis2-ui/divider';
5
5
  import { Layer } from '@dhis2-ui/layer';
6
6
  import { CircularLoader } from '@dhis2-ui/loader';
7
- import { MenuItem } from '@dhis2-ui/menu';
8
- import { useConfig, clearSensitiveCaches } from '@dhis2/app-runtime';
7
+ import { MenuDivider, MenuItem } from '@dhis2-ui/menu';
8
+ import { clearSensitiveCaches, useConfig } from '@dhis2/app-runtime';
9
9
  import { colors } from '@dhis2/ui-constants';
10
10
  import { IconSettings24, IconInfo24, IconLogOut24, IconUser24, IconQuestion24 } from '@dhis2/ui-icons';
11
11
  import PropTypes from 'prop-types';
12
12
  import React, { useState } from 'react';
13
+ import { DebugInfoMenuItem } from '../debug-info/debug-info-menu-item.js';
13
14
  import { joinPath } from '../join-path.js';
14
15
  import i18n from '../locales/index.js';
15
16
  import { ProfileHeader } from './profile-header.js';
17
+ import { UpdateNotification } from './update-notification.js';
16
18
 
17
19
  const LoadingMask = () => /*#__PURE__*/React.createElement(Layer, {
18
20
  translucent: true,
@@ -25,7 +27,9 @@ const ProfileContents = _ref => {
25
27
  name,
26
28
  email,
27
29
  avatarId,
28
- helpUrl
30
+ helpUrl,
31
+ hideProfileMenu,
32
+ showDebugInfoModal
29
33
  } = _ref;
30
34
  const {
31
35
  baseUrl
@@ -86,37 +90,39 @@ const ProfileContents = _ref => {
86
90
  icon: /*#__PURE__*/React.createElement(IconLogOut24, {
87
91
  color: colors.grey700
88
92
  })
93
+ }), /*#__PURE__*/React.createElement(MenuDivider, {
94
+ dense: true
95
+ }), /*#__PURE__*/React.createElement(DebugInfoMenuItem, {
96
+ hideProfileMenu: hideProfileMenu,
97
+ showDebugInfoModal: showDebugInfoModal
98
+ }), /*#__PURE__*/React.createElement(UpdateNotification, {
99
+ hideProfileMenu: hideProfileMenu
89
100
  }))), loading && /*#__PURE__*/React.createElement(LoadingMask, null), /*#__PURE__*/React.createElement(_JSXStyle, {
90
101
  id: "2099675810"
91
102
  }, ["div.jsx-2099675810{width:100%;padding:0;}", "ul.jsx-2099675810{padding:0;margin:0;}", "a.jsx-2099675810,a.jsx-2099675810:hover,a.jsx-2099675810:focus,a.jsx-2099675810:active,a.jsx-2099675810:visited{-webkit-text-decoration:none;text-decoration:none;display:block;}"]));
92
103
  };
93
104
 
94
105
  ProfileContents.propTypes = {
106
+ hideProfileMenu: PropTypes.func.isRequired,
107
+ showDebugInfoModal: PropTypes.func.isRequired,
95
108
  avatarId: PropTypes.string,
96
109
  email: PropTypes.string,
97
110
  helpUrl: PropTypes.string,
98
111
  name: PropTypes.string
99
112
  };
100
113
  export const ProfileMenu = _ref2 => {
101
- let {
102
- avatarId,
103
- name,
104
- email,
105
- helpUrl
114
+ let { ...props
106
115
  } = _ref2;
107
116
  return /*#__PURE__*/React.createElement("div", {
108
117
  "data-test": "headerbar-profile-menu",
109
- className: "jsx-3620236321"
110
- }, /*#__PURE__*/React.createElement(ProfileContents, {
111
- name: name,
112
- email: email,
113
- avatarId: avatarId,
114
- helpUrl: helpUrl
115
- }), /*#__PURE__*/React.createElement(_JSXStyle, {
116
- id: "3620236321"
117
- }, ["div.jsx-3620236321{z-index:10000;position:absolute;top:34px;right:-6px;width:310px;border-top:4px solid transparent;}"]));
118
+ className: "jsx-1689179824"
119
+ }, /*#__PURE__*/React.createElement(ProfileContents, props), /*#__PURE__*/React.createElement(_JSXStyle, {
120
+ id: "1689179824"
121
+ }, ["div.jsx-1689179824{z-index:10000;position:absolute;top:34px;right:-6px;width:320px;border-top:4px solid transparent;}"]));
118
122
  };
119
123
  ProfileMenu.propTypes = {
124
+ hideProfileMenu: PropTypes.func.isRequired,
125
+ showDebugInfoModal: PropTypes.func.isRequired,
120
126
  avatarId: PropTypes.string,
121
127
  email: PropTypes.string,
122
128
  helpUrl: PropTypes.string,
@@ -0,0 +1,51 @@
1
+ import _JSXStyle from "styled-jsx/style";
2
+ import { MenuItem } from '@dhis2-ui/menu';
3
+ import { useConfig } from '@dhis2/app-runtime';
4
+ import { colors } from '@dhis2/ui-constants';
5
+ import PropTypes from 'prop-types';
6
+ import React from 'react';
7
+ import { useHeaderBarContext } from '../header-bar-context.js';
8
+ import i18n from '../locales/index.js';
9
+ export function UpdateNotification(_ref) {
10
+ let {
11
+ hideProfileMenu
12
+ } = _ref;
13
+ const {
14
+ appName
15
+ } = useConfig();
16
+ const {
17
+ updateAvailable,
18
+ onApplyAvailableUpdate
19
+ } = useHeaderBarContext();
20
+
21
+ const onClick = () => {
22
+ hideProfileMenu();
23
+ onApplyAvailableUpdate === null || onApplyAvailableUpdate === void 0 ? void 0 : onApplyAvailableUpdate();
24
+ };
25
+
26
+ const updateNotificationLabel = /*#__PURE__*/React.createElement("div", {
27
+ className: _JSXStyle.dynamic([["4135170305", [colors.blue600]]]) + " " + "root"
28
+ }, /*#__PURE__*/React.createElement("div", {
29
+ className: _JSXStyle.dynamic([["4135170305", [colors.blue600]]]) + " " + "badge"
30
+ }), /*#__PURE__*/React.createElement("div", {
31
+ className: _JSXStyle.dynamic([["4135170305", [colors.blue600]]]) + " " + "spacer"
32
+ }), /*#__PURE__*/React.createElement("div", {
33
+ className: _JSXStyle.dynamic([["4135170305", [colors.blue600]]]) + " " + "message"
34
+ }, appName ? i18n.t('New {{appName}} version available', {
35
+ appName
36
+ }) : i18n.t('New app version available'), /*#__PURE__*/React.createElement("br", {
37
+ className: _JSXStyle.dynamic([["4135170305", [colors.blue600]]])
38
+ }), i18n.t('Click to reload')), /*#__PURE__*/React.createElement(_JSXStyle, {
39
+ id: "4135170305",
40
+ dynamic: [colors.blue600]
41
+ }, [".root.__jsx-style-dynamic-selector{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;font-size:14px;line-height:17px;}", ".badge.__jsx-style-dynamic-selector{display:inline-block;width:12px;height:12px;margin:0 8px;border-radius:6px;background-color:".concat(colors.blue600, ";}"), ".spacer.__jsx-style-dynamic-selector{display:inline-block;width:8px;}", ".message.__jsx-style-dynamic-selector{display:inline-block;}"]));
42
+ return updateAvailable ? /*#__PURE__*/React.createElement(MenuItem, {
43
+ dense: true,
44
+ onClick: onClick,
45
+ label: updateNotificationLabel,
46
+ dataTest: "dhis2-ui-headerbar-updatenotification"
47
+ }) : null;
48
+ }
49
+ UpdateNotification.propTypes = {
50
+ hideProfileMenu: PropTypes.func.isRequired
51
+ };