diff --git a/.env.development b/.env.development index cd76ce6aa..f22c2c67a 100644 --- a/.env.development +++ b/.env.development @@ -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 diff --git a/.env.test b/.env.test index ff42aeae6..125f98687 100644 --- a/.env.test +++ b/.env.test @@ -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 diff --git a/src/DesktopHeader.jsx b/src/DesktopHeader.jsx index 32eb1fbd7..4ed4a37c2 100644 --- a/src/DesktopHeader.jsx +++ b/src/DesktopHeader.jsx @@ -83,6 +83,9 @@ class DesktopHeader extends React.Component { name, email, intl, + hasEnterpriseAccount, + logoAltText, + logoDestination, } = this.props; return ( @@ -107,7 +110,11 @@ class DesktopHeader extends React.Component { /> )} - + {userMenu.map((group, index) => ( // eslint-disable-next-line react/jsx-no-comment-textnodes,react/no-array-index-key @@ -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( @@ -251,6 +259,7 @@ DesktopHeader.defaultProps = { name: '', email: '', loggedIn: false, + hasEnterpriseAccount: false, notificationAppData: { apps: {}, tabsCount: {}, diff --git a/src/Header.jsx b/src/Header.jsx index 2b1a8fbe1..30ae82749 100644 --- a/src/Header.jsx +++ b/src/Header.jsx @@ -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'; @@ -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, @@ -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', @@ -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 = [ + ...(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'])}{intl.formatMessage(messages['header.user.menu.newAlert'])}; 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, @@ -124,16 +125,18 @@ const Header = ({ { category: 'header', label: 'header' }, ); }, - }, + }] : []), { 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, @@ -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 = { + 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, ], }]; @@ -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) { diff --git a/src/Header.messages.jsx b/src/Header.messages.jsx index 2283f8acc..a7ff5432b 100644 --- a/src/Header.messages.jsx +++ b/src/Header.messages.jsx @@ -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', @@ -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', diff --git a/src/Header.test.jsx b/src/Header.test.jsx index 2cecf3b9f..306367aff 100644 --- a/src/Header.test.jsx +++ b/src/Header.test.jsx @@ -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'; @@ -44,6 +44,10 @@ const HeaderContext = ({ width, contextValue }) => ( describe('
', () => { beforeEach(() => { useEnterpriseConfig.mockReturnValue({}); + getConfig.mockReturnValue({ + ENABLE_EDX_PERSONAL_DASHBOARD: true, + SUPPORT_URL: 'http://localhost:18000/support', + }); }); beforeAll(async () => { @@ -66,6 +70,10 @@ describe('
', () => { }); }; + const mockIsEnterpriseUser = () => { + isEnterpriseUser.mockReturnValue(true); + }; + it('renders correctly for unauthenticated users on desktop', () => { const contextValue = { authenticatedUser: null, @@ -138,14 +146,17 @@ describe('
', () => { }); 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 () => { @@ -153,7 +164,8 @@ describe('
', () => { 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', () => { diff --git a/src/MobileHeader.jsx b/src/MobileHeader.jsx index 3b49d233e..db2c8cf44 100644 --- a/src/MobileHeader.jsx +++ b/src/MobileHeader.jsx @@ -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) ? (
  • @@ -100,7 +107,13 @@ class MobileHeader extends React.Component { )) )); - const userMenuGroupSlot = ; + const userMenuGroupSlot = ( + + ); const userMenuGroupItemSlot = ; return userInfoItem @@ -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({ @@ -257,6 +271,7 @@ MobileHeader.defaultProps = { avatar: null, name: '', email: '', + hasEnterpriseAccount: false, loggedIn: false, stickyOnMobile: true, notificationAppData: { diff --git a/src/__snapshots__/Header.test.jsx.snap b/src/__snapshots__/Header.test.jsx.snap index dee99e09b..f831f0ca4 100644 --- a/src/__snapshots__/Header.test.jsx.snap +++ b/src/__snapshots__/Header.test.jsx.snap @@ -161,6 +161,13 @@ exports[`
    renders correctly for authenticated users on desktop 1`] = ` aria-label="Secondary" className="nav secondary-menu-container align-items-center ml-auto" > + + Help +
    { + const { formatMessage } = useIntl(); + const { width } = useWindowSize(); + const isMobile = width <= breakpoints.small.maxWidth; + + if (getConfig().ENABLE_EDX_PERSONAL_DASHBOARD) { + if (!isMobile) { + return ( + <> + { hasEnterpriseAccount && {formatMessage(messages.dashboardSwitch)} } + + {formatMessage(hasEnterpriseAccount ? messages.dashboardPersonal : messages.dashboard)} + + { hasEnterpriseAccount && ( + + {enterpriseOrg} {formatMessage(messages.dashboard)} + + )} + {hasEnterpriseAccount && } + + ); + } + return ( + <> +
  • + + {formatMessage(messages.dashboardPersonal)} + +
  • + {hasEnterpriseAccount && ( +
  • + + {enterpriseOrg} {formatMessage(messages.dashboard)} + +
  • + )} + + ); + } + return null; +}; + +UserDashboardMenu.defaultProps = { + enterpriseOrg: null, + enterpriseSrc: null, + hasEnterpriseAccount: false, +}; + +UserDashboardMenu.propTypes = { + enterpriseOrg: PropTypes.string, + enterpriseSrc: PropTypes.string, + hasEnterpriseAccount: PropTypes.bool, +}; + +export default UserDashboardMenu; diff --git a/src/common/UserDashboardMenu/index.test.jsx b/src/common/UserDashboardMenu/index.test.jsx new file mode 100644 index 000000000..f419e1e74 --- /dev/null +++ b/src/common/UserDashboardMenu/index.test.jsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { mergeConfig } from '@edx/frontend-platform'; +import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n'; +import { + screen, + render, +} from '@testing-library/react'; + +import UserDashboardMenu from './index'; // eslint-disable-line + +mergeConfig({ ENABLE_EDX_PERSONAL_DASHBOARD: true }); +const IntlUserDashboardMenu = injectIntl(UserDashboardMenu); + +describe('User Dashboard Menu', () => { + const Wrapper = children => ( + // eslint-disable-next-line react/jsx-filename-extension + + {children} + + ); + + it('should render enterprise dashboard menu in the user menu dropdown', () => { + render(Wrapper()); + + const dashboardSwitchMenu = screen.getByText('SWITCH DASHBOARD'); + const personalDashboard = screen.getByText('Personal'); + const enterpriseDashboard = screen.getByText('Fake Enterprise Dashboard'); + expect(dashboardSwitchMenu).toBeTruthy(); + expect(personalDashboard).toBeTruthy(); + expect(enterpriseDashboard).toBeTruthy(); + }); + it('should render only one dashboard menu in the user menu dropdown when not enterprise user', () => { + render(Wrapper()); + + const personalDashboard = screen.getByText('Dashboard'); + expect(personalDashboard).toBeTruthy(); + }); +}); diff --git a/src/common/UserDashboardMenu/messages.js b/src/common/UserDashboardMenu/messages.js new file mode 100644 index 000000000..65d1b171e --- /dev/null +++ b/src/common/UserDashboardMenu/messages.js @@ -0,0 +1,21 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + dashboard: { + id: 'learnerVariantDashboard.menu.dashboard.label', + defaultMessage: 'Dashboard', + description: 'The text for the user menu Dashboard navigation link.', + }, + dashboardPersonal: { + id: 'learnerVariantDashboard.menu.dashboardPersonal.label', + defaultMessage: 'Personal', + description: 'Link to personal dashboard in user menu', + }, + dashboardSwitch: { + id: 'learnerVariantDashboard.menu.dashboardSwitch.label', + defaultMessage: 'SWITCH DASHBOARD', + description: 'Switch Dashboard header in the user menu', + }, +}); + +export default messages; diff --git a/src/plugin-slots/UserMenuGroupSlot/README.md b/src/plugin-slots/UserMenuGroupSlot/README.md index 47dbc65dc..dc63c94c9 100644 --- a/src/plugin-slots/UserMenuGroupSlot/README.md +++ b/src/plugin-slots/UserMenuGroupSlot/README.md @@ -15,8 +15,11 @@ for both mobile and desktop screens. **Default Behaviour:** -Desktop: -![Screenshot of Default Header_User_Menu_Desktop](./images/default_user_menu_desktop.png) +Desktop (non-enterprise): +![Screenshot of Default Header_User_Menu_Desktop_Non_Enterprise](./images/default_user_menu_desktop_non_enterprise.png) + +Desktop (enterprise): +![Screenshot of Default Header_User_Menu_Desktop_Enterprise](./images/default_user_menu_desktop_enterprise.png) Mobile: ![Screenshot of Default Header_User_Menu_Mobile](./images/default_user_menu_mobile.png) diff --git a/src/plugin-slots/UserMenuGroupSlot/images/default_user_menu_desktop.png b/src/plugin-slots/UserMenuGroupSlot/images/default_user_menu_desktop.png deleted file mode 100644 index ad26802ce..000000000 Binary files a/src/plugin-slots/UserMenuGroupSlot/images/default_user_menu_desktop.png and /dev/null differ diff --git a/src/plugin-slots/UserMenuGroupSlot/images/default_user_menu_desktop_enterprise.png b/src/plugin-slots/UserMenuGroupSlot/images/default_user_menu_desktop_enterprise.png new file mode 100644 index 000000000..260c88ccf Binary files /dev/null and b/src/plugin-slots/UserMenuGroupSlot/images/default_user_menu_desktop_enterprise.png differ diff --git a/src/plugin-slots/UserMenuGroupSlot/images/default_user_menu_desktop_non_enterprise.png b/src/plugin-slots/UserMenuGroupSlot/images/default_user_menu_desktop_non_enterprise.png new file mode 100644 index 000000000..046b8cb62 Binary files /dev/null and b/src/plugin-slots/UserMenuGroupSlot/images/default_user_menu_desktop_non_enterprise.png differ diff --git a/src/plugin-slots/UserMenuGroupSlot/index.jsx b/src/plugin-slots/UserMenuGroupSlot/index.jsx index 8a06242e0..d6ffbabe8 100644 --- a/src/plugin-slots/UserMenuGroupSlot/index.jsx +++ b/src/plugin-slots/UserMenuGroupSlot/index.jsx @@ -1,11 +1,31 @@ import React from 'react'; +import PropTypes from 'prop-types'; import { PluginSlot } from '@openedx/frontend-plugin-framework'; +import UserDashboardMenu from '../../common/UserDashboardMenu'; -const UserMenuGroupSlot = () => ( +const UserMenuGroupSlot = ({ hasEnterpriseAccount, logoAltText, logoDestination }) => ( + > + + ); +UserMenuGroupSlot.defaultProps = { + hasEnterpriseAccount: false, + logoAltText: '', + logoDestination: '', +}; + +UserMenuGroupSlot.propTypes = { + hasEnterpriseAccount: PropTypes.bool, + logoAltText: PropTypes.string, + logoDestination: PropTypes.string, +}; + export default UserMenuGroupSlot;