Skip to content

Commit 82d3c19

Browse files
feat: added notification tray for common header/learner dashboard (#592)
* feat: added notification tray for common header/learner dashboard * test: updated snapshot * fix: removed margins * fix: added margins * fix: removed duplication of state * fix: updated structure * fix: removed unused import
1 parent 1642a6e commit 82d3c19

File tree

7 files changed

+516
-419
lines changed

7 files changed

+516
-419
lines changed

src/DesktopHeader.jsx

+24-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import React from 'react';
22
import PropTypes from 'prop-types';
3+
import { connect } from 'react-redux';
4+
35
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
46
import { getConfig } from '@edx/frontend-platform';
57
import { AvatarButton, Dropdown } from '@openedx/paragon';
@@ -10,6 +12,8 @@ import UserMenuGroupSlot from './plugin-slots/UserMenuGroupSlot';
1012
import UserMenuItem from './common/UserMenuItem';
1113
import { Menu, MenuTrigger, MenuContent } from './Menu';
1214
import { LinkedLogo, Logo } from './Logo';
15+
import Notifications from './Notifications';
16+
import { mapDispatchToProps, mapStateToProps } from './data/selectors';
1317

1418
// i18n
1519
import messages from './Header.messages';
@@ -20,6 +24,20 @@ import { CaretIcon } from './Icons';
2024
class DesktopHeader extends React.Component {
2125
constructor(props) { // eslint-disable-line no-useless-constructor
2226
super(props);
27+
this.state = {
28+
locationHref: window.location.href,
29+
};
30+
}
31+
32+
componentDidMount() {
33+
this.props.fetchAppsNotificationCount();
34+
}
35+
36+
componentDidUpdate() {
37+
if (window.location.href !== this.state.locationHref) {
38+
this.setState({ locationHref: window.location.href });
39+
this.props.fetchAppsNotificationCount();
40+
}
2341
}
2442

2543
renderMenu(menu) {
@@ -152,6 +170,7 @@ class DesktopHeader extends React.Component {
152170
logoAltText,
153171
logoDestination,
154172
loggedIn,
173+
showNotificationsTray,
155174
intl,
156175
} = this.props;
157176
const logoProps = { src: logo, alt: logoAltText, href: logoDestination };
@@ -177,6 +196,7 @@ class DesktopHeader extends React.Component {
177196
? (
178197
<>
179198
{this.renderSecondaryMenu()}
199+
{showNotificationsTray && <Notifications showLeftMargin={false} />}
180200
{this.renderUserMenu()}
181201
</>
182202
) : this.renderLoggedOutItems()}
@@ -220,7 +240,8 @@ DesktopHeader.propTypes = {
220240
name: PropTypes.string,
221241
email: PropTypes.string,
222242
loggedIn: PropTypes.bool,
223-
243+
showNotificationsTray: PropTypes.bool,
244+
fetchAppsNotificationCount: PropTypes.func.isRequired,
224245
// i18n
225246
intl: intlShape.isRequired,
226247
};
@@ -237,6 +258,7 @@ DesktopHeader.defaultProps = {
237258
name: '',
238259
email: '',
239260
loggedIn: false,
261+
showNotificationsTray: false,
240262
};
241263

242-
export default injectIntl(DesktopHeader);
264+
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(DesktopHeader));

src/Header.jsx

+8-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, { useContext } from 'react';
22
import Responsive from 'react-responsive';
33
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
44
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
5-
import { AppContext } from '@edx/frontend-platform/react';
5+
import { AppContext, AppProvider } from '@edx/frontend-platform/react';
66
import { Badge } from '@openedx/paragon';
77
import {
88
APP_CONFIG_INITIALIZED,
@@ -16,6 +16,7 @@ import { useEnterpriseConfig } from '@edx/frontend-enterprise-utils';
1616
import PropTypes from 'prop-types';
1717
import DesktopHeader from './DesktopHeader';
1818
import MobileHeader from './MobileHeader';
19+
import store from './store';
1920

2021
import messages from './Header.messages';
2122

@@ -26,13 +27,17 @@ ensureConfig([
2627
'MARKETING_SITE_BASE_URL',
2728
'ORDER_HISTORY_URL',
2829
'LOGO_URL',
30+
'ACCOUNT_SETTINGS_URL',
31+
'NOTIFICATION_FEEDBACK_URL',
2932
], 'Header component');
3033

3134
subscribe(APP_CONFIG_INITIALIZED, () => {
3235
mergeConfig({
3336
MINIMAL_HEADER: !!process.env.MINIMAL_HEADER,
3437
ENTERPRISE_LEARNER_PORTAL_HOSTNAME: process.env.ENTERPRISE_LEARNER_PORTAL_HOSTNAME,
3538
AUTHN_MINIMAL_HEADER: !!process.env.AUTHN_MINIMAL_HEADER,
39+
ACCOUNT_SETTINGS_URL: process.env.ACCOUNT_SETTINGS_URL || '',
40+
NOTIFICATION_FEEDBACK_URL: process.env.NOTIFICATION_FEEDBACK_URL || '',
3641
}, 'Header additional config');
3742
});
3843

@@ -194,14 +199,14 @@ const Header = ({
194199
}
195200

196201
return (
197-
<>
202+
<AppProvider store={store} wrapWithRouter={false}>
198203
<Responsive maxWidth={769}>
199204
<MobileHeader {...props} />
200205
</Responsive>
201206
<Responsive minWidth={769}>
202207
<DesktopHeader {...props} />
203208
</Responsive>
204-
</>
209+
</AppProvider>
205210
);
206211
};
207212

src/Header.test.jsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import { useEnterpriseConfig } from '@edx/frontend-enterprise-utils';
77
import { AppContext } from '@edx/frontend-platform/react';
88
import { getConfig } from '@edx/frontend-platform';
99
import { Context as ResponsiveContext } from 'react-responsive';
10-
1110
import { fireEvent, render, screen } from '@testing-library/react';
11+
12+
import { initializeMockApp } from './setupTest';
1213
import Header from './index';
1314

1415
jest.mock('@edx/frontend-platform');
@@ -41,6 +42,10 @@ describe('<Header />', () => {
4142
beforeEach(() => {
4243
useEnterpriseConfig.mockReturnValue({});
4344
});
45+
beforeAll(async () => {
46+
// We need to mock AuthService to implicitly use `getAuthenticatedUser` within `AppContext.Provider`.
47+
await initializeMockApp();
48+
});
4449

4550
const mockUseEnterpriseConfig = () => {
4651
useEnterpriseConfig.mockReturnValue({

src/MobileHeader.jsx

+25-5
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
import React from 'react';
22
import PropTypes from 'prop-types';
3+
import { connect } from 'react-redux';
4+
35
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
46
import { getConfig } from '@edx/frontend-platform';
7+
import { AvatarButton } from '@openedx/paragon';
58

69
// Local Components
7-
import { AvatarButton } from '@openedx/paragon';
810
import UserMenuGroupSlot from './plugin-slots/UserMenuGroupSlot';
911
import UserMenuGroupItemSlot from './plugin-slots/UserMenuGroupItemSlot';
1012
import { Menu, MenuTrigger, MenuContent } from './Menu';
1113
import { LinkedLogo, Logo } from './Logo';
1214
import UserMenuItem from './common/UserMenuItem';
13-
15+
import Notifications from './Notifications';
16+
import { mapDispatchToProps, mapStateToProps } from './data/selectors';
1417
// i18n
1518
import messages from './Header.messages';
1619

@@ -20,6 +23,20 @@ import { MenuIcon } from './Icons';
2023
class MobileHeader extends React.Component {
2124
constructor(props) { // eslint-disable-line no-useless-constructor
2225
super(props);
26+
this.state = {
27+
locationHref: window.location.href,
28+
};
29+
}
30+
31+
componentDidMount() {
32+
this.props.fetchAppsNotificationCount();
33+
}
34+
35+
componentDidUpdate() {
36+
if (window.location.href !== this.state.locationHref) {
37+
this.setState({ locationHref: window.location.href });
38+
this.props.fetchAppsNotificationCount();
39+
}
2340
}
2441

2542
renderMenu(menu) {
@@ -135,6 +152,7 @@ class MobileHeader extends React.Component {
135152
mainMenu,
136153
userMenu,
137154
loggedOutItems,
155+
showNotificationsTray,
138156
} = this.props;
139157
const logoProps = { src: logo, alt: logoAltText, href: logoDestination };
140158
const stickyClassName = stickyOnMobile ? 'sticky-top' : '';
@@ -173,6 +191,7 @@ class MobileHeader extends React.Component {
173191
</div>
174192
{userMenu.length > 0 || loggedOutItems.length > 0 ? (
175193
<div className="w-100 d-flex justify-content-end align-items-center">
194+
{showNotificationsTray && loggedIn && <Notifications />}
176195
<Menu tag="nav" aria-label={intl.formatMessage(messages['header.label.secondary.nav'])} className="position-static">
177196
<MenuTrigger
178197
tag={AvatarButton}
@@ -227,7 +246,8 @@ MobileHeader.propTypes = {
227246
email: PropTypes.string,
228247
loggedIn: PropTypes.bool,
229248
stickyOnMobile: PropTypes.bool,
230-
249+
showNotificationsTray: PropTypes.bool,
250+
fetchAppsNotificationCount: PropTypes.func.isRequired,
231251
// i18n
232252
intl: intlShape.isRequired,
233253
};
@@ -245,7 +265,7 @@ MobileHeader.defaultProps = {
245265
email: '',
246266
loggedIn: false,
247267
stickyOnMobile: true,
248-
268+
showNotificationsTray: false,
249269
};
250270

251-
export default injectIntl(MobileHeader);
271+
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(MobileHeader));

src/Notifications/index.jsx

+13-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import React, {
66

77
import classNames from 'classnames';
88
import { useDispatch, useSelector } from 'react-redux';
9+
import PropTypes from 'prop-types';
910

1011
import { getConfig } from '@edx/frontend-platform';
1112
import { useIntl } from '@edx/frontend-platform/i18n';
@@ -26,7 +27,7 @@ import NotificationTabs from './NotificationTabs';
2627

2728
import './notification.scss';
2829

29-
const Notifications = () => {
30+
const Notifications = ({ showLeftMargin }) => {
3031
const intl = useIntl();
3132
const dispatch = useDispatch();
3233
const popoverRef = useRef(null);
@@ -144,7 +145,9 @@ const Notifications = () => {
144145
iconAs={Icon}
145146
variant="light"
146147
iconClassNames="text-primary-500"
147-
className="ml-4 mr-1 notification-button"
148+
className={classNames('mr-1 notification-button', {
149+
'ml-4': showLeftMargin,
150+
})}
148151
data-testid="notification-bell-icon"
149152
/>
150153
{notificationCounts?.count > 0 && (
@@ -168,4 +171,12 @@ const Notifications = () => {
168171
);
169172
};
170173

174+
Notifications.propTypes = {
175+
showLeftMargin: PropTypes.bool,
176+
};
177+
178+
Notifications.defaultProps = {
179+
showLeftMargin: true,
180+
};
181+
171182
export default Notifications;

0 commit comments

Comments
 (0)