diff --git a/src/DesktopHeader.jsx b/src/DesktopHeader.jsx index fb685f2d..87a48777 100644 --- a/src/DesktopHeader.jsx +++ b/src/DesktopHeader.jsx @@ -1,5 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; + import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { getConfig } from '@edx/frontend-platform'; import { AvatarButton, Dropdown } from '@openedx/paragon'; @@ -10,6 +12,8 @@ import UserMenuGroupSlot from './plugin-slots/UserMenuGroupSlot'; import UserMenuItem from './common/UserMenuItem'; import { Menu, MenuTrigger, MenuContent } from './Menu'; import { LinkedLogo, Logo } from './Logo'; +import Notifications from './Notifications'; +import { mapDispatchToProps, mapStateToProps } from './data/selectors'; // i18n import messages from './Header.messages'; @@ -20,6 +24,20 @@ import { CaretIcon } from './Icons'; class DesktopHeader extends React.Component { constructor(props) { // eslint-disable-line no-useless-constructor super(props); + this.state = { + locationHref: window.location.href, + }; + } + + componentDidMount() { + this.props.fetchAppsNotificationCount(); + } + + componentDidUpdate() { + if (window.location.href !== this.state.locationHref) { + this.setState({ locationHref: window.location.href }); + this.props.fetchAppsNotificationCount(); + } } renderMenu(menu) { @@ -152,6 +170,7 @@ class DesktopHeader extends React.Component { logoAltText, logoDestination, loggedIn, + showNotificationsTray, intl, } = this.props; const logoProps = { src: logo, alt: logoAltText, href: logoDestination }; @@ -177,6 +196,7 @@ class DesktopHeader extends React.Component { ? ( <> {this.renderSecondaryMenu()} + {showNotificationsTray && } {this.renderUserMenu()} ) : this.renderLoggedOutItems()} @@ -220,7 +240,8 @@ DesktopHeader.propTypes = { name: PropTypes.string, email: PropTypes.string, loggedIn: PropTypes.bool, - + showNotificationsTray: PropTypes.bool, + fetchAppsNotificationCount: PropTypes.func.isRequired, // i18n intl: intlShape.isRequired, }; @@ -237,6 +258,7 @@ DesktopHeader.defaultProps = { name: '', email: '', loggedIn: false, + showNotificationsTray: false, }; -export default injectIntl(DesktopHeader); +export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(DesktopHeader)); diff --git a/src/Header.jsx b/src/Header.jsx index ae2f9cf1..9b8a8331 100644 --- a/src/Header.jsx +++ b/src/Header.jsx @@ -2,7 +2,7 @@ import React, { useContext } from 'react'; import Responsive from 'react-responsive'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { sendTrackEvent } from '@edx/frontend-platform/analytics'; -import { AppContext } from '@edx/frontend-platform/react'; +import { AppContext, AppProvider } from '@edx/frontend-platform/react'; import { Badge } from '@openedx/paragon'; import { APP_CONFIG_INITIALIZED, @@ -16,6 +16,7 @@ import { useEnterpriseConfig } from '@edx/frontend-enterprise-utils'; import PropTypes from 'prop-types'; import DesktopHeader from './DesktopHeader'; import MobileHeader from './MobileHeader'; +import store from './store'; import messages from './Header.messages'; @@ -26,6 +27,8 @@ ensureConfig([ 'MARKETING_SITE_BASE_URL', 'ORDER_HISTORY_URL', 'LOGO_URL', + 'ACCOUNT_SETTINGS_URL', + 'NOTIFICATION_FEEDBACK_URL', ], 'Header component'); subscribe(APP_CONFIG_INITIALIZED, () => { @@ -33,6 +36,8 @@ subscribe(APP_CONFIG_INITIALIZED, () => { MINIMAL_HEADER: !!process.env.MINIMAL_HEADER, ENTERPRISE_LEARNER_PORTAL_HOSTNAME: process.env.ENTERPRISE_LEARNER_PORTAL_HOSTNAME, AUTHN_MINIMAL_HEADER: !!process.env.AUTHN_MINIMAL_HEADER, + ACCOUNT_SETTINGS_URL: process.env.ACCOUNT_SETTINGS_URL || '', + NOTIFICATION_FEEDBACK_URL: process.env.NOTIFICATION_FEEDBACK_URL || '', }, 'Header additional config'); }); @@ -194,14 +199,14 @@ const Header = ({ } return ( - <> + - + ); }; diff --git a/src/Header.test.jsx b/src/Header.test.jsx index d932c566..896a46e7 100644 --- a/src/Header.test.jsx +++ b/src/Header.test.jsx @@ -7,8 +7,9 @@ import { useEnterpriseConfig } from '@edx/frontend-enterprise-utils'; import { AppContext } from '@edx/frontend-platform/react'; import { getConfig } from '@edx/frontend-platform'; import { Context as ResponsiveContext } from 'react-responsive'; - import { fireEvent, render, screen } from '@testing-library/react'; + +import { initializeMockApp } from './setupTest'; import Header from './index'; jest.mock('@edx/frontend-platform'); @@ -41,6 +42,10 @@ describe('
', () => { beforeEach(() => { useEnterpriseConfig.mockReturnValue({}); }); + beforeAll(async () => { + // We need to mock AuthService to implicitly use `getAuthenticatedUser` within `AppContext.Provider`. + await initializeMockApp(); + }); const mockUseEnterpriseConfig = () => { useEnterpriseConfig.mockReturnValue({ diff --git a/src/MobileHeader.jsx b/src/MobileHeader.jsx index d5f3da45..888ce74a 100644 --- a/src/MobileHeader.jsx +++ b/src/MobileHeader.jsx @@ -1,16 +1,19 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; + import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { getConfig } from '@edx/frontend-platform'; +import { AvatarButton } from '@openedx/paragon'; // Local Components -import { AvatarButton } from '@openedx/paragon'; import UserMenuGroupSlot from './plugin-slots/UserMenuGroupSlot'; import UserMenuGroupItemSlot from './plugin-slots/UserMenuGroupItemSlot'; import { Menu, MenuTrigger, MenuContent } from './Menu'; import { LinkedLogo, Logo } from './Logo'; import UserMenuItem from './common/UserMenuItem'; - +import Notifications from './Notifications'; +import { mapDispatchToProps, mapStateToProps } from './data/selectors'; // i18n import messages from './Header.messages'; @@ -20,6 +23,20 @@ import { MenuIcon } from './Icons'; class MobileHeader extends React.Component { constructor(props) { // eslint-disable-line no-useless-constructor super(props); + this.state = { + locationHref: window.location.href, + }; + } + + componentDidMount() { + this.props.fetchAppsNotificationCount(); + } + + componentDidUpdate() { + if (window.location.href !== this.state.locationHref) { + this.setState({ locationHref: window.location.href }); + this.props.fetchAppsNotificationCount(); + } } renderMenu(menu) { @@ -135,6 +152,7 @@ class MobileHeader extends React.Component { mainMenu, userMenu, loggedOutItems, + showNotificationsTray, } = this.props; const logoProps = { src: logo, alt: logoAltText, href: logoDestination }; const stickyClassName = stickyOnMobile ? 'sticky-top' : ''; @@ -173,6 +191,7 @@ class MobileHeader extends React.Component { {userMenu.length > 0 || loggedOutItems.length > 0 ? (
+ {showNotificationsTray && loggedIn && } { +const Notifications = ({ showLeftMargin }) => { const intl = useIntl(); const dispatch = useDispatch(); const popoverRef = useRef(null); @@ -144,7 +145,9 @@ const Notifications = () => { iconAs={Icon} variant="light" iconClassNames="text-primary-500" - className="ml-4 mr-1 notification-button" + className={classNames('mr-1 notification-button', { + 'ml-4': showLeftMargin, + })} data-testid="notification-bell-icon" /> {notificationCounts?.count > 0 && ( @@ -168,4 +171,12 @@ const Notifications = () => { ); }; +Notifications.propTypes = { + showLeftMargin: PropTypes.bool, +}; + +Notifications.defaultProps = { + showLeftMargin: true, +}; + export default Notifications; diff --git a/src/__snapshots__/Header.test.jsx.snap b/src/__snapshots__/Header.test.jsx.snap index dee99e09..239abb47 100644 --- a/src/__snapshots__/Header.test.jsx.snap +++ b/src/__snapshots__/Header.test.jsx.snap @@ -1,328 +1,285 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`
minimal renders correctly for authenticated users when minimal 1`] = ` -
- - Skip to main content - -
+ + Skip to main content +
- edX -
+ +
- -
+ + `; exports[`
minimal renders correctly for unauthenticated users when minimal 1`] = ` -
- - Skip to main content - -
+ + Skip to main content +
- edX -
- -
+
+ `; exports[`
renders correctly for authenticated users on desktop 1`] = ` -
- - Skip to main content - -
+ + Skip to main content +
- - edX - - - + + aria-expanded={false} + aria-haspopup={true} + aria-label="Account menu for edX" + className="btn-avatar pgn__avatar-button-avatar pgn__avatar-button-avatar-md dropdown-toggle btn btn-tertiary btn-md" + data-hj-suppress={true} + disabled={false} + id="menu-dropdown" + onClick={[Function]} + type="button" + > + + +
+ +
- -
+
+ `; exports[`
renders correctly for authenticated users on mobile 1`] = ` -
- - Skip to main content - -
-
- -
-
-
- edX + Skip to main content -
-
- -
-
-`; - -exports[`
renders correctly for unauthenticated users on desktop 1`] = ` -
- - Skip to main content - -
+ +
+
edX renders correctly for unauthenticated users on desktop 1`] = src="https://edx-cdn.org/v3/prod/logo.svg" /> +
+
-
+ +`; + +exports[`
renders correctly for unauthenticated users on desktop 1`] = ` +
+
+ + Skip to main content + + -
-
+
+ `; exports[`
renders correctly for unauthenticated users on mobile 1`] = ` -
- - Skip to main content - -
+ + Skip to main content +
- + + + + + + +
- -
- - edX - -
-
- -
-
+ + +
+ +
+
+ `; diff --git a/src/data/selectors.js b/src/data/selectors.js new file mode 100644 index 00000000..89657f40 --- /dev/null +++ b/src/data/selectors.js @@ -0,0 +1,10 @@ +import { fetchAppsNotificationCount } from '../Notifications/data/thunks'; +import { selectShowNotificationTray } from '../Notifications/data/selectors'; + +export const mapDispatchToProps = (dispatch) => ({ + fetchAppsNotificationCount: () => dispatch(fetchAppsNotificationCount()), +}); + +export const mapStateToProps = (state) => ({ + showNotificationsTray: selectShowNotificationTray(state), +});