Skip to content

Commit

Permalink
feat: added notification tray for common header/learner dashboard (#592)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
sundasnoreen12 authored Sep 5, 2024
1 parent 1642a6e commit 82d3c19
Show file tree
Hide file tree
Showing 7 changed files with 516 additions and 419 deletions.
26 changes: 24 additions & 2 deletions src/DesktopHeader.jsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand All @@ -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) {
Expand Down Expand Up @@ -152,6 +170,7 @@ class DesktopHeader extends React.Component {
logoAltText,
logoDestination,
loggedIn,
showNotificationsTray,
intl,
} = this.props;
const logoProps = { src: logo, alt: logoAltText, href: logoDestination };
Expand All @@ -177,6 +196,7 @@ class DesktopHeader extends React.Component {
? (
<>
{this.renderSecondaryMenu()}
{showNotificationsTray && <Notifications showLeftMargin={false} />}
{this.renderUserMenu()}
</>
) : this.renderLoggedOutItems()}
Expand Down Expand Up @@ -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,
};
Expand All @@ -237,6 +258,7 @@ DesktopHeader.defaultProps = {
name: '',
email: '',
loggedIn: false,
showNotificationsTray: false,
};

export default injectIntl(DesktopHeader);
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(DesktopHeader));
11 changes: 8 additions & 3 deletions src/Header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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';

Expand All @@ -26,13 +27,17 @@ ensureConfig([
'MARKETING_SITE_BASE_URL',
'ORDER_HISTORY_URL',
'LOGO_URL',
'ACCOUNT_SETTINGS_URL',
'NOTIFICATION_FEEDBACK_URL',
], 'Header component');

subscribe(APP_CONFIG_INITIALIZED, () => {
mergeConfig({
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');
});

Expand Down Expand Up @@ -194,14 +199,14 @@ const Header = ({
}

return (
<>
<AppProvider store={store} wrapWithRouter={false}>
<Responsive maxWidth={769}>
<MobileHeader {...props} />
</Responsive>
<Responsive minWidth={769}>
<DesktopHeader {...props} />
</Responsive>
</>
</AppProvider>
);
};

Expand Down
7 changes: 6 additions & 1 deletion src/Header.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -41,6 +42,10 @@ describe('<Header />', () => {
beforeEach(() => {
useEnterpriseConfig.mockReturnValue({});
});
beforeAll(async () => {
// We need to mock AuthService to implicitly use `getAuthenticatedUser` within `AppContext.Provider`.
await initializeMockApp();
});

const mockUseEnterpriseConfig = () => {
useEnterpriseConfig.mockReturnValue({
Expand Down
30 changes: 25 additions & 5 deletions src/MobileHeader.jsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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) {
Expand Down Expand Up @@ -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' : '';
Expand Down Expand Up @@ -173,6 +191,7 @@ class MobileHeader extends React.Component {
</div>
{userMenu.length > 0 || loggedOutItems.length > 0 ? (
<div className="w-100 d-flex justify-content-end align-items-center">
{showNotificationsTray && loggedIn && <Notifications />}
<Menu tag="nav" aria-label={intl.formatMessage(messages['header.label.secondary.nav'])} className="position-static">
<MenuTrigger
tag={AvatarButton}
Expand Down Expand Up @@ -227,7 +246,8 @@ MobileHeader.propTypes = {
email: PropTypes.string,
loggedIn: PropTypes.bool,
stickyOnMobile: PropTypes.bool,

showNotificationsTray: PropTypes.bool,
fetchAppsNotificationCount: PropTypes.func.isRequired,
// i18n
intl: intlShape.isRequired,
};
Expand All @@ -245,7 +265,7 @@ MobileHeader.defaultProps = {
email: '',
loggedIn: false,
stickyOnMobile: true,

showNotificationsTray: false,
};

export default injectIntl(MobileHeader);
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(MobileHeader));
15 changes: 13 additions & 2 deletions src/Notifications/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import React, {

import classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux';
import PropTypes from 'prop-types';

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

import './notification.scss';

const Notifications = () => {
const Notifications = ({ showLeftMargin }) => {
const intl = useIntl();
const dispatch = useDispatch();
const popoverRef = useRef(null);
Expand Down Expand Up @@ -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 && (
Expand All @@ -168,4 +171,12 @@ const Notifications = () => {
);
};

Notifications.propTypes = {
showLeftMargin: PropTypes.bool,
};

Notifications.defaultProps = {
showLeftMargin: true,
};

export default Notifications;
Loading

0 comments on commit 82d3c19

Please sign in to comment.