Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ FAVICON_URL=https://edx-cdn.org/v3/prod/favicon.ico
NOTIFICATION_FEEDBACK_URL=''
CAREERS_URL=''
STUDIO_BASE_URL=''
ENABLE_EDX_PERSONAL_DASHBOARD=true
1 change: 1 addition & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ FAVICON_URL=https://edx-cdn.org/v3/prod/favicon.ico
NOTIFICATION_FEEDBACK_URL=''
CAREERS_URL=''
STUDIO_BASE_URL=''
ENABLE_EDX_PERSONAL_DASHBOARD=true
11 changes: 10 additions & 1 deletion src/DesktopHeader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ class DesktopHeader extends React.Component {
name,
email,
intl,
hasEnterpriseAccount,
logoAltText,
logoDestination,
} = this.props;

return (
Expand All @@ -107,7 +110,11 @@ class DesktopHeader extends React.Component {
/>
</Dropdown.Item>
)}
<UserMenuGroupSlot />
<UserMenuGroupSlot
hasEnterpriseAccount={hasEnterpriseAccount}
logoAltText={logoAltText}
logoDestination={logoDestination}
/>
{userMenu.map((group, index) => (
// eslint-disable-next-line react/jsx-no-comment-textnodes,react/no-array-index-key
<React.Fragment key={index}>
Expand Down Expand Up @@ -221,6 +228,7 @@ DesktopHeader.propTypes = {
avatar: PropTypes.string,
name: PropTypes.string,
email: PropTypes.string,
hasEnterpriseAccount: PropTypes.bool,
loggedIn: PropTypes.bool,
notificationAppData: PropTypes.shape({
apps: PropTypes.objectOf(
Expand Down Expand Up @@ -251,6 +259,7 @@ DesktopHeader.defaultProps = {
name: '',
email: '',
loggedIn: false,
hasEnterpriseAccount: false,
notificationAppData: {
apps: {},
tabsCount: {},
Expand Down
48 changes: 29 additions & 19 deletions src/Header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import {
getConfig,
subscribe,
} from '@edx/frontend-platform';
import { useEnterpriseConfig } from '@edx/frontend-enterprise-utils';
import { useEnterpriseConfig, isEnterpriseUser } from '@edx/frontend-enterprise-utils';
import './index.scss';

import PropTypes from 'prop-types';
import DesktopHeader from './DesktopHeader';
Expand All @@ -35,6 +36,7 @@ subscribe(APP_CONFIG_INITIALIZED, () => {
mergeConfig({
MINIMAL_HEADER: !!process.env.MINIMAL_HEADER,
ENTERPRISE_LEARNER_PORTAL_HOSTNAME: process.env.ENTERPRISE_LEARNER_PORTAL_HOSTNAME,
ENABLE_EDX_PERSONAL_DASHBOARD: process.env.ENABLE_EDX_PERSONAL_DASHBOARD,
AUTHN_MINIMAL_HEADER: !!process.env.AUTHN_MINIMAL_HEADER,
ACCOUNT_SETTINGS_URL: process.env.ACCOUNT_SETTINGS_URL,
NOTIFICATION_FEEDBACK_URL: process.env.NOTIFICATION_FEEDBACK_URL,
Expand Down Expand Up @@ -64,12 +66,14 @@ const Header = ({
enterpriseLearnerPortalLink,
enterpriseCustomerBrandingConfig,
} = useEnterpriseConfig(authenticatedUser, config.ENTERPRISE_LEARNER_PORTAL_HOSTNAME, config.LMS_BASE_URL);
const hasEnterpriseAccount = isEnterpriseUser(authenticatedUser);

const defaultMainMenu = [
{
type: 'item',
href: `${config.LMS_BASE_URL}/dashboard`,
content: intl.formatMessage(messages['header.links.courses']),
isActive: document.title.includes(intl.formatMessage(messages['header.pages.learner.home'])),
},
{
type: 'item',
Expand All @@ -89,32 +93,29 @@ const Header = ({
},
];

const dashboardMenuItem = {
type: 'item',
href: `${config.LMS_BASE_URL}/dashboard`,
content: intl.formatMessage(messages['header.user.menu.dashboard']),
};

const logoutMenuItem = {
type: 'item',
href: config.LOGOUT_URL,
content: intl.formatMessage(messages['header.user.menu.logout']),
};

// If there is an Enterprise LP link, use that instead of the B2C Dashboard
let baseUserMenuDashboardLinks = [];
if (enterpriseLearnerPortalLink) {
baseUserMenuDashboardLinks = [enterpriseLearnerPortalLink];
} else {
baseUserMenuDashboardLinks = [dashboardMenuItem];
}
const defaultSecondaryMenu = [

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This "Help" button is new to the default Header experience, but it has already existed for a while in the Learning and Learner Dashboard pages.

...(getConfig().SUPPORT_URL ? [{
type: 'item',
href: `${getConfig().SUPPORT_URL}`,
content: intl.formatMessage(messages['header.links.help']),
}] : []),
];

const careerItemContent = <>{intl.formatMessage(messages['header.user.menu.career'])}<Badge className="px-2 mx-2" variant="warning">{intl.formatMessage(messages['header.user.menu.newAlert'])}</Badge></>;
const defaultUserMenu = authenticatedUser === null ? [] : [{
heading: '',
items: [
...baseUserMenuDashboardLinks,
{
// Users should only see Career button if they do not have an available
// learner portal, because an available learner portal currently means
// that they access content via B2B Subscriptions, in which context "career" button
// is not relevant.
...(!enterpriseLearnerPortalLink ? [{
type: 'item',
href: 'https://careers.edx.org/',
content: careerItemContent,
Expand All @@ -124,16 +125,18 @@ const Header = ({
{ category: 'header', label: 'header' },
);
},
},
}] : []),

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only render the Career button when the user doesn't have an Enterprise account, similarly with the Order History button.

{
type: 'item',
href: `${config.ACCOUNT_PROFILE_URL}/u/${authenticatedUser.username}`,
content: intl.formatMessage(messages['header.user.menu.profile']),
isActive: document.title.includes(intl.formatMessage(messages['header.user.menu.profile'])),
},
{
type: 'item',
href: config.ACCOUNT_SETTINGS_URL,
content: intl.formatMessage(messages['header.user.menu.account.settings']),
isActive: document.title.includes(intl.formatMessage(messages['header.user.menu.account.settings'])),
},
// Users should only see Order History if they do not have an available
// learner portal and have a ORDER_HISTORY_URL define in the environment,
Expand All @@ -150,14 +153,20 @@ const Header = ({
}];

const mainMenu = mainMenuItems || defaultMainMenu;
const secondaryMenu = secondaryMenuItems || [];
const secondaryMenu = secondaryMenuItems || defaultSecondaryMenu;
let userMenu = authenticatedUser === null ? [] : userMenuItems || defaultUserMenu;

const minimalDashboardMenuItem = {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Minimal header (used in Payment MFE) should only display one dashboard, which has historically pointed to the personal dashboard, so I'm choosing to leave this unchanged.

type: 'item',
href: `${config.LMS_BASE_URL}/dashboard`,
content: intl.formatMessage(messages['header.user.menu.dashboard']),
};

if (getConfig().MINIMAL_HEADER && authenticatedUser !== null) {
userMenu = [{
heading: '',
items: [
dashboardMenuItem,
minimalDashboardMenuItem,
logoutMenuItem,
],
}];
Expand Down Expand Up @@ -190,6 +199,7 @@ const Header = ({
userMenu: getConfig().AUTHN_MINIMAL_HEADER ? [] : userMenu,
loggedOutItems: getConfig().AUTHN_MINIMAL_HEADER ? [] : loggedOutItems,
studioBaseUrl: config.STUDIO_BASE_URL,
hasEnterpriseAccount,
};

if (enterpriseCustomerBrandingConfig) {
Expand Down
10 changes: 10 additions & 0 deletions src/Header.messages.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { defineMessages } from '@edx/frontend-platform/i18n';

const messages = defineMessages({
'header.pages.learner.home': {
id: 'header.pages.learner.home',
defaultMessage: 'Learner Home',
description: 'Title of the page in learner dashboard',
},
'header.links.courses': {
id: 'header.links.courses',
defaultMessage: 'Courses',
Expand All @@ -21,6 +26,11 @@ const messages = defineMessages({
defaultMessage: 'Schools & Partners',
description: 'Link to the schools and partners landing page',
},
'header.links.help': {
id: 'header.links.help',
defaultMessage: 'Help',
description: 'The text for the link to the Help Center',
},
'header.user.menu.career': {
id: 'header.user.menu.career',
defaultMessage: 'Career',
Expand Down
18 changes: 15 additions & 3 deletions src/Header.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React from 'react';
import { act } from 'react-dom/test-utils';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import TestRenderer from 'react-test-renderer';
import { useEnterpriseConfig } from '@edx/frontend-enterprise-utils';
import { isEnterpriseUser, 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';
Expand Down Expand Up @@ -44,6 +44,10 @@ const HeaderContext = ({ width, contextValue }) => (
describe('<Header />', () => {
beforeEach(() => {
useEnterpriseConfig.mockReturnValue({});
getConfig.mockReturnValue({
ENABLE_EDX_PERSONAL_DASHBOARD: true,
SUPPORT_URL: 'http://localhost:18000/support',
});
});

beforeAll(async () => {
Expand All @@ -66,6 +70,10 @@ describe('<Header />', () => {
});
};

const mockIsEnterpriseUser = () => {
isEnterpriseUser.mockReturnValue(true);
};

it('renders correctly for unauthenticated users on desktop', () => {
const contextValue = {
authenticatedUser: null,
Expand Down Expand Up @@ -138,22 +146,26 @@ describe('<Header />', () => {
});

expect(screen.getByText('Order History')).toBeInTheDocument();
expect(screen.queryByText('Career')).toBeInTheDocument();
expect(screen.getByText('Help')).toBeInTheDocument();
expect(screen.getByText('Dashboard')).toBeInTheDocument();

wrapper.unmount();

// When learner portal links are present, Order History should not be a dropdown item
// When learner portal links are present, Order History and Career should not be a dropdown item
// We do this in the same test to avoid weird things about jest wanting you to use
// the "correct" act() function (even though it gives the same warning regardless
// of where you import it from, be it react-dom/test-utils or react-test-renderer).
mockIsEnterpriseUser();
mockUseEnterpriseConfig();
wrapper = render(component);
await act(async () => {
await flushPromises();
fireEvent.click(wrapper.container.querySelector('#menu-dropdown'));
});
expect(screen.queryByText('Order History')).not.toBeInTheDocument();
expect(screen.getByText('Dashboard')).toBeInTheDocument();
expect(screen.queryByText('Career')).not.toBeInTheDocument();
expect(screen.getByText('Personal')).toBeInTheDocument();
});

it('renders correctly for unauthenticated users on mobile', () => {
Expand Down
19 changes: 17 additions & 2 deletions src/MobileHeader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,14 @@ class MobileHeader extends React.Component {
}

renderUserMenuItems() {
const { userMenu, name, email } = this.props;
const {
userMenu,
name,
email,
hasEnterpriseAccount,
logoAltText,
logoDestination,
} = this.props;
const userInfoItem = (name || email) ? (
<li className="nav-item user-info__menu-item" key="user-info">
<UserMenuItem name={name} email={email} />
Expand All @@ -100,7 +107,13 @@ class MobileHeader extends React.Component {
))
));

const userMenuGroupSlot = <UserMenuGroupSlot />;
const userMenuGroupSlot = (
<UserMenuGroupSlot
hasEnterpriseAccount={hasEnterpriseAccount}
logoAltText={logoAltText}
logoDestination={logoDestination}
/>
);
const userMenuGroupItemSlot = <UserMenuGroupItemSlot />;

return userInfoItem
Expand Down Expand Up @@ -227,6 +240,7 @@ MobileHeader.propTypes = {
avatar: PropTypes.string,
name: PropTypes.string,
email: PropTypes.string,
hasEnterpriseAccount: PropTypes.bool,
loggedIn: PropTypes.bool,
stickyOnMobile: PropTypes.bool,
notificationAppData: PropTypes.shape({
Expand Down Expand Up @@ -257,6 +271,7 @@ MobileHeader.defaultProps = {
avatar: null,
name: '',
email: '',
hasEnterpriseAccount: false,
loggedIn: false,
stickyOnMobile: true,
notificationAppData: {
Expand Down
7 changes: 7 additions & 0 deletions src/__snapshots__/Header.test.jsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,13 @@ exports[`<Header /> renders correctly for authenticated users on desktop 1`] = `
aria-label="Secondary"
className="nav secondary-menu-container align-items-center ml-auto"
>
<a
className="nav-link"
href="http://localhost:18000/support"
onClick={null}
>
Help
</a>
<div
className="pgn__dropdown pgn__dropdown-light dropdown"
data-testid="dropdown"
Expand Down
65 changes: 65 additions & 0 deletions src/common/UserDashboardMenu/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
breakpoints, Dropdown, useWindowSize,
} from '@openedx/paragon';

import messages from './messages';

const UserDashboardMenu = ({ enterpriseOrg, enterpriseSrc, hasEnterpriseAccount }) => {
const { formatMessage } = useIntl();
const { width } = useWindowSize();
const isMobile = width <= breakpoints.small.maxWidth;

if (getConfig().ENABLE_EDX_PERSONAL_DASHBOARD) {
if (!isMobile) {
return (
<>
{ hasEnterpriseAccount && <Dropdown.Header>{formatMessage(messages.dashboardSwitch)}</Dropdown.Header> }
<Dropdown.Item as="a" href="/edx-dashboard" className={document.title === 'Learner Home' ? 'active' : ''} key="item-dashboard">
{formatMessage(hasEnterpriseAccount ? messages.dashboardPersonal : messages.dashboard)}
</Dropdown.Item>
{ hasEnterpriseAccount && (
<Dropdown.Item as="a" href={enterpriseSrc} key={enterpriseOrg}>
{enterpriseOrg} {formatMessage(messages.dashboard)}
</Dropdown.Item>
)}
{hasEnterpriseAccount && <Dropdown.Divider />}
</>
);
}
return (
<>
<li className="nav-item" key="item-dashboard">
<a href="/edx-dashboard" className={`nav-link ${document.title === 'Learner Home' ? 'active' : ''})`}>
{formatMessage(messages.dashboardPersonal)}
</a>
</li>
{hasEnterpriseAccount && (
<li className="nav-item" key={enterpriseOrg}>
<a href={enterpriseSrc} className="nav-link">
{enterpriseOrg} {formatMessage(messages.dashboard)}
</a>
</li>
)}
</>
);
}
return null;
};

UserDashboardMenu.defaultProps = {
enterpriseOrg: null,
enterpriseSrc: null,
hasEnterpriseAccount: false,
};

UserDashboardMenu.propTypes = {
enterpriseOrg: PropTypes.string,
enterpriseSrc: PropTypes.string,
hasEnterpriseAccount: PropTypes.bool,
};

export default UserDashboardMenu;
Loading