diff --git a/example/index.js b/example/index.js index d89e7344..b0a61349 100644 --- a/example/index.js +++ b/example/index.js @@ -26,7 +26,8 @@ subscribe(APP_READY, () => { authenticatedUser: { userId: '123abc', username: 'testuser', - name: 'test user', + name: 'Test User', + email: 'test@example.com', roles: [], administrator: false, }, @@ -38,8 +39,9 @@ subscribe(APP_READY, () => { @@ -72,13 +71,16 @@ class DesktopHeader extends React.Component { as={AvatarButton} src={avatar} alt="" - aria-label={intl.formatMessage(messages['header.label.account.menu.for'], { username: usernameOrName })} + aria-label={intl.formatMessage(messages['header.label.account.menu.for'], { name })} data-hj-suppress - > - {!hideUsername && username} - - + /> + + + {userMenu.map(({ type, href, content }) => ( {content} @@ -158,8 +160,8 @@ DesktopHeader.propTypes = { logoAltText: PropTypes.string, logoDestination: PropTypes.string, avatar: PropTypes.string, - username: PropTypes.string, name: PropTypes.string, + email: PropTypes.string, loggedIn: PropTypes.bool, // i18n @@ -174,8 +176,8 @@ DesktopHeader.defaultProps = { logoAltText: null, logoDestination: null, avatar: null, - username: null, name: '', + email: '', loggedIn: false, }; diff --git a/src/Header.jsx b/src/Header.jsx index 760e6940..53329046 100644 --- a/src/Header.jsx +++ b/src/Header.jsx @@ -32,9 +32,6 @@ 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, - // this flag is introduced to unblock for new release. - // the flag would be removed in follow up PR - HIDE_USERNAME_FROM_HEADER: !!process.env.HIDE_USERNAME_FROM_HEADER, }, 'Header additional config'); }); @@ -157,8 +154,8 @@ const Header = ({ intl }) => { siteName: 'edX', logoDestination: getConfig().MINIMAL_HEADER ? null : `${config.LMS_BASE_URL}/dashboard`, loggedIn: authenticatedUser !== null, - username: authenticatedUser !== null ? authenticatedUser.username : null, - name: authenticatedUser !== null ? authenticatedUser.name : null, + name: authenticatedUser !== null ? authenticatedUser.name : '', + email: authenticatedUser !== null ? authenticatedUser.email : '', avatar: authenticatedUser !== null ? authenticatedUser.avatar : null, mainMenu: getConfig().MINIMAL_HEADER || getConfig().AUTHN_MINIMAL_HEADER ? [] : mainMenu, userMenu: getConfig().AUTHN_MINIMAL_HEADER ? [] : userMenu, diff --git a/src/Header.messages.jsx b/src/Header.messages.jsx index 0a21908f..2283f8ac 100644 --- a/src/Header.messages.jsx +++ b/src/Header.messages.jsx @@ -78,8 +78,8 @@ const messages = defineMessages({ }, 'header.label.account.menu.for': { id: 'header.label.account.menu.for', - defaultMessage: 'Account menu for {username}', - description: 'The aria label for the account menu trigger when the username is displayed in it', + defaultMessage: 'Account menu for {name}', + description: 'The aria label for the account menu trigger', }, 'header.label.main.nav': { id: 'header.label.main.nav', diff --git a/src/Header.test.jsx b/src/Header.test.jsx index 9a8dce5c..d932c566 100644 --- a/src/Header.test.jsx +++ b/src/Header.test.jsx @@ -87,6 +87,27 @@ describe('
', () => { expect(wrapper.toJSON()).toMatchSnapshot(); }); + it('displays user menu in dropdown', () => { + const authenticatedUser = { + userId: 'abc123', + username: 'edX', + name: 'edX', + email: 'test@example.com', + roles: [], + administrator: false, + }; + const contextValue = { + authenticatedUser, + config: APP_CONTEXT_CONFIG, + }; + const component = ; + const wrapper = render(component); + fireEvent.click(wrapper.container.querySelector('#menu-dropdown')); + + expect(screen.getByText(authenticatedUser.name)).toBeInTheDocument(); + expect(screen.getByText(authenticatedUser.email)).toBeInTheDocument(); + }); + it('renders correctly for authenticated users on desktop with or without learner portal links', async () => { const contextValue = { authenticatedUser: { @@ -143,6 +164,7 @@ describe('
', () => { authenticatedUser: { userId: 'abc123', username: 'edX', + name: 'edX Test', roles: [], administrator: false, }, @@ -185,6 +207,7 @@ describe('
', () => { authenticatedUser: { userId: 'abc123', username: 'edX', + name: 'edX Test', roles: [], administrator: false, }, diff --git a/src/MobileHeader.jsx b/src/MobileHeader.jsx index a01a5901..414426b4 100644 --- a/src/MobileHeader.jsx +++ b/src/MobileHeader.jsx @@ -7,6 +7,7 @@ import { getConfig } from '@edx/frontend-platform'; import { AvatarButton } from '@openedx/paragon'; import { Menu, MenuTrigger, MenuContent } from './Menu'; import { LinkedLogo, Logo } from './Logo'; +import UserMenuItem from './common/UserMenuItem'; // i18n import messages from './Header.messages'; @@ -57,13 +58,18 @@ class MobileHeader extends React.Component { } renderUserMenuItems() { - const { userMenu } = this.props; - - return userMenu.map(({ type, href, content }) => ( + const { userMenu, name, email } = this.props; + const userInfoItem = ( +
  • + +
  • + ); + const userMenuItems = userMenu.map(({ type, href, content }) => (
  • {content}
  • )); + return [userInfoItem, ...userMenuItems]; } renderLoggedOutItems() { @@ -88,7 +94,7 @@ class MobileHeader extends React.Component { logoDestination, loggedIn, avatar, - username, + name, stickyOnMobile, intl, mainMenu, @@ -139,7 +145,7 @@ class MobileHeader extends React.Component { src={avatar} showLabel={false} > - {username} + {name} {loggedIn ? this.renderUserMenuItems() : this.renderLoggedOutItems()} @@ -172,7 +178,8 @@ MobileHeader.propTypes = { logoAltText: PropTypes.string, logoDestination: PropTypes.string, avatar: PropTypes.string, - username: PropTypes.string, + name: PropTypes.string, + email: PropTypes.string, loggedIn: PropTypes.bool, stickyOnMobile: PropTypes.bool, @@ -188,7 +195,8 @@ MobileHeader.defaultProps = { logoAltText: null, logoDestination: null, avatar: null, - username: null, + name: '', + email: '', loggedIn: false, stickyOnMobile: true, diff --git a/src/__snapshots__/Header.test.jsx.snap b/src/__snapshots__/Header.test.jsx.snap index 87bdaa0a..5794de89 100644 --- a/src/__snapshots__/Header.test.jsx.snap +++ b/src/__snapshots__/Header.test.jsx.snap @@ -37,7 +37,7 @@ exports[`
    minimal renders correctly for authenticated users when minim alt="" aria-expanded={false} aria-haspopup={true} - aria-label="Account menu for edX" + aria-label="Account menu for edX Test" 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} @@ -50,7 +50,6 @@ exports[`
    minimal renders correctly for authenticated users when minim className="pgn__avatar pgn__avatar-sm" src="icon/mock/path" /> - edX @@ -180,7 +179,6 @@ exports[`
    renders correctly for authenticated users on desktop 1`] = ` className="pgn__avatar pgn__avatar-sm" src="icon/mock/path" /> - edX @@ -293,7 +291,7 @@ exports[`
    renders correctly for authenticated users on mobile 1`] = ` type="button" > edX @@ -478,7 +476,7 @@ exports[`
    renders correctly for unauthenticated users on mobile 1`] = type="button" > {null} diff --git a/src/common/UserMenuItem.jsx b/src/common/UserMenuItem.jsx new file mode 100644 index 00000000..6bf8c8fa --- /dev/null +++ b/src/common/UserMenuItem.jsx @@ -0,0 +1,34 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { Avatar } from '@openedx/paragon'; +import { injectIntl } from '@edx/frontend-platform/i18n'; + +import './style.scss'; + +const UserMenuItem = ({ name, email }) => ( + <> + +
    + {name && {name}} + {email && {email}} +
    + +); + +UserMenuItem.propTypes = { + name: PropTypes.string, + email: PropTypes.string, +}; + +UserMenuItem.defaultProps = { + name: '', + email: '', +}; + +export default injectIntl(UserMenuItem); diff --git a/src/common/style.scss b/src/common/style.scss new file mode 100644 index 00000000..bdddd4f8 --- /dev/null +++ b/src/common/style.scss @@ -0,0 +1,18 @@ +.dropdown-menu a:first-child { + pointer-events: none; +} + +@media screen and (max-width: 768px) { + .menu-content li:first-child { + display: flex; + align-items: center; + padding: 0 16px; + border-bottom: 1px solid #70828E; + + a { + display: flex; + align-items: center; + border-bottom: 1px solid #70828E !important; + } + } +} diff --git a/src/index.scss b/src/index.scss index 1d31c9ba..27856351 100644 --- a/src/index.scss +++ b/src/index.scss @@ -119,3 +119,8 @@ $white: #fff; border-radius: $rounded-pill; } } + +// bottom boarder of first child of user dropdown menu +.dropdown-menu a:first-child { + border-bottom: 1px solid #70828E !important; +} diff --git a/src/learning-header/AuthenticatedUserDropdown.jsx b/src/learning-header/AuthenticatedUserDropdown.jsx index 4062c8cf..bd293534 100644 --- a/src/learning-header/AuthenticatedUserDropdown.jsx +++ b/src/learning-header/AuthenticatedUserDropdown.jsx @@ -7,10 +7,11 @@ import { faUserCircle } from '@fortawesome/free-solid-svg-icons'; import { getConfig } from '@edx/frontend-platform'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import { Avatar, Dropdown, Badge } from '@openedx/paragon'; +import { Dropdown, Badge } from '@openedx/paragon'; import messages from './messages'; import Notifications from '../Notifications'; +import UserMenuItem from '../common/UserMenuItem'; import { selectShowNotificationTray } from '../Notifications/data/selectors'; import { fetchAppsNotificationCount } from '../Notifications/data/thunks'; @@ -20,6 +21,7 @@ const AuthenticatedUserDropdown = (props) => { enterpriseLearnerPortalLink, username, name, + email, } = props; const dispatch = useDispatch(); const showNotificationsTray = useSelector(selectShowNotificationTray); @@ -42,6 +44,11 @@ const AuthenticatedUserDropdown = (props) => { ); + const userMenuItem = ( + + + + ); if (enterpriseLearnerPortalLink && Object.keys(enterpriseLearnerPortalLink).length > 0) { dashboardMenuItem = ( { careersMenuItem = ''; } - const dropdownToggle = ( - - - {getConfig().HIDE_USERNAME_FROM_HEADER ? ( - - ) : ( - - {username} - - )} - - ); - return ( <> {intl.formatMessage(messages.help)} {showNotificationsTray && } - {dropdownToggle} + + + + {userMenuItem} {dashboardMenuItem} {careersMenuItem} @@ -104,11 +101,13 @@ AuthenticatedUserDropdown.propTypes = { intl: intlShape.isRequired, username: PropTypes.string.isRequired, name: PropTypes.string, + email: PropTypes.string, }; AuthenticatedUserDropdown.defaultProps = { enterpriseLearnerPortalLink: '', name: '', + email: '', }; export default injectIntl(AuthenticatedUserDropdown); diff --git a/src/learning-header/LearningHeader.jsx b/src/learning-header/LearningHeader.jsx index 9b8fa96a..f6f0f0e7 100644 --- a/src/learning-header/LearningHeader.jsx +++ b/src/learning-header/LearningHeader.jsx @@ -23,7 +23,6 @@ subscribe(APP_CONFIG_INITIALIZED, () => { mergeConfig({ ACCOUNT_SETTINGS_URL: process.env.ACCOUNT_SETTINGS_URL || '', NOTIFICATION_FEEDBACK_URL: process.env.NOTIFICATION_FEEDBACK_URL || '', - HIDE_USERNAME_FROM_HEADER: !!process.env.HIDE_USERNAME_FROM_HEADER, }, 'Learning Header additional config'); }); @@ -94,6 +93,7 @@ const LearningHeader = ({ enterpriseLearnerPortalLink={enterpriseLearnerPortalLink} username={authenticatedUser.username} name={authenticatedUser.name} + email={authenticatedUser.email} /> )} {showUserDropdown && !authenticatedUser && ( diff --git a/src/learning-header/LearningHeader.test.jsx b/src/learning-header/LearningHeader.test.jsx index c48082a3..2bb14069 100644 --- a/src/learning-header/LearningHeader.test.jsx +++ b/src/learning-header/LearningHeader.test.jsx @@ -1,7 +1,6 @@ import React from 'react'; -import { getConfig, mergeConfig } from '@edx/frontend-platform'; import { - authenticatedUser, initializeMockApp, render, screen, + fireEvent, initializeMockApp, render, screen, } from '../setupTest'; import { LearningHeader as Header } from '../index'; @@ -13,18 +12,15 @@ describe('Header', () => { it('displays user button', () => { render(
    ); - expect(screen.getByText(authenticatedUser.username)).toBeInTheDocument(); + expect(screen.getByRole('button', { className: 'dropdown-toggle' })).toBeInTheDocument(); }); - it('displays user button without username', () => { - const config = getConfig(); - mergeConfig({ - ...config, - HIDE_USERNAME_FROM_HEADER: true, - }); - const { queryByTestId } = render(
    ); - const username = queryByTestId('username'); - expect(username).not.toBeInTheDocument(); + it('displays user menu in dropdown', () => { + render(
    ); + const button = screen.getByRole('button', { className: 'dropdown-toggle' }); + fireEvent.click(button); + const userMenuItem = screen.queryByTestId('user-item'); + expect(userMenuItem).toBeInTheDocument(); }); it('displays course data', () => { diff --git a/src/studio-header/HeaderBody.jsx b/src/studio-header/HeaderBody.jsx index 130d0170..6e4a2dd0 100644 --- a/src/studio-header/HeaderBody.jsx +++ b/src/studio-header/HeaderBody.jsx @@ -20,8 +20,8 @@ const HeaderBody = ({ number, org, title, - username, name, + email, isAdmin, studioBaseUrl, logoutUrl, @@ -100,8 +100,8 @@ const HeaderBody = ({