diff --git a/README.rst b/README.rst
index ab9e4db72..b6f7c5ae9 100644
--- a/README.rst
+++ b/README.rst
@@ -49,6 +49,7 @@ Environment Variables
* ``ACCOUNT_PROFILE_URL`` - The URL of the account profile page.
* ``ACCOUNT_SETTINGS_URL`` - The URL of the account settings page.
* ``AUTHN_MINIMAL_HEADER`` - A boolean flag which hides the main menu, user menu, and logged-out
+* ``HIDE_USERNAME_FROM_HEADER`` - A boolean flag which hides the username from the header
menu items when truthy. This is intended to be used in micro-frontends like
frontend-app-authentication in which these menus are considered distractions from the user's task.
diff --git a/example/index.js b/example/index.js
index 2559c03d4..ac41c4960 100644
--- a/example/index.js
+++ b/example/index.js
@@ -26,6 +26,7 @@ subscribe(APP_READY, () => {
authenticatedUser: {
userId: '123abc',
username: 'testuser',
+ name: 'test user',
roles: [],
administrator: false,
},
diff --git a/src/DesktopHeader.jsx b/src/DesktopHeader.jsx
index 8bd142fb9..849ee2df6 100644
--- a/src/DesktopHeader.jsx
+++ b/src/DesktopHeader.jsx
@@ -74,21 +74,25 @@ class DesktopHeader extends React.Component {
renderUserMenu() {
const {
+ intl,
userMenu,
avatar,
+ name,
username,
- intl,
} = this.props;
+ const hideUsername = getConfig().HIDE_USERNAME_FROM_HEADER;
+ const usernameOrName = hideUsername ? name : username;
return (
- {username}
+ {!hideUsername && username}
+
{userMenu.map(({ type, href, content }) => (
@@ -178,6 +182,7 @@ DesktopHeader.propTypes = {
logoDestination: PropTypes.string,
avatar: PropTypes.string,
username: PropTypes.string,
+ name: PropTypes.string,
loggedIn: PropTypes.bool,
// i18n
@@ -207,6 +212,7 @@ DesktopHeader.defaultProps = {
logoDestination: null,
avatar: null,
username: null,
+ name: null,
loggedIn: false,
appMenu: null,
};
diff --git a/src/Header.jsx b/src/Header.jsx
index c0db257c7..cd40126ea 100644
--- a/src/Header.jsx
+++ b/src/Header.jsx
@@ -27,6 +27,7 @@ ensureConfig([
subscribe(APP_CONFIG_INITIALIZED, () => {
mergeConfig({
AUTHN_MINIMAL_HEADER: !!process.env.AUTHN_MINIMAL_HEADER,
+ HIDE_USERNAME_FROM_HEADER: !!process.env.HIDE_USERNAME_FROM_HEADER,
}, 'Header additional config');
});
@@ -94,6 +95,7 @@ const Header = ({ intl }) => {
logoDestination: `${config.LMS_BASE_URL}/dashboard`,
loggedIn: authenticatedUser !== null,
username: authenticatedUser !== null ? authenticatedUser.username : null,
+ name: authenticatedUser !== null ? authenticatedUser.name : null,
avatar: authenticatedUser !== null ? authenticatedUser.avatar : null,
mainMenu: getConfig().AUTHN_MINIMAL_HEADER ? [] : mainMenu,
userMenu: getConfig().AUTHN_MINIMAL_HEADER ? [] : userMenu,
diff --git a/src/Header.messages.jsx b/src/Header.messages.jsx
index a5ff3a589..1071c9cb3 100644
--- a/src/Header.messages.jsx
+++ b/src/Header.messages.jsx
@@ -76,10 +76,10 @@ const messages = defineMessages({
defaultMessage: 'Account Menu',
description: 'The aria label for the account menu trigger',
},
- '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',
+ 'header.label.account.menu.using.name.or.username': {
+ id: 'header.label.account.menu.using.name.or.username',
+ defaultMessage: 'Account menu for {usernameOrName}',
+ description: 'The aria label for the account menu trigger when the username is displayed or hide in it',
},
'header.label.main.nav': {
id: 'header.label.main.nav',
diff --git a/src/Header.test.jsx b/src/Header.test.jsx
index 51fef2089..b4f09f61e 100644
--- a/src/Header.test.jsx
+++ b/src/Header.test.jsx
@@ -38,11 +38,12 @@ describe('', () => {
expect(wrapper.toJSON()).toMatchSnapshot();
});
- it('renders correctly for authenticated desktop', () => {
+ it('renders correctly for authenticated desktop with username', () => {
const contextValue = {
authenticatedUser: {
userId: 'abc123',
username: 'edX',
+ name: 'edX',
roles: [],
administrator: false,
},
@@ -61,6 +62,30 @@ describe('', () => {
expect(wrapper.toJSON()).toMatchSnapshot();
});
+ it('renders correctly for authenticated desktop without username', () => {
+ const contextValue = {
+ authenticatedUser: {
+ userId: 'abc123',
+ name: 'edX',
+ roles: [],
+ administrator: false,
+ },
+ config: {
+ LMS_BASE_URL: process.env.LMS_BASE_URL,
+ SITE_NAME: process.env.SITE_NAME,
+ LOGIN_URL: process.env.LOGIN_URL,
+ LOGOUT_URL: process.env.LOGOUT_URL,
+ LOGO_URL: process.env.LOGO_URL,
+ HIDE_USERNAME_FROM_HEADER: true,
+ },
+ };
+ const component = ;
+
+ const wrapper = TestRenderer.create(component);
+
+ expect(wrapper.toJSON()).toMatchSnapshot();
+ });
+
it('renders correctly for anonymous mobile', () => {
const contextValue = {
authenticatedUser: null,
@@ -84,6 +109,7 @@ describe('', () => {
authenticatedUser: {
userId: 'abc123',
username: 'edX',
+ name: 'edX',
roles: [],
administrator: false,
},
diff --git a/src/__snapshots__/Header.test.jsx.snap b/src/__snapshots__/Header.test.jsx.snap
index f83161b15..04aeaf59a 100644
--- a/src/__snapshots__/Header.test.jsx.snap
+++ b/src/__snapshots__/Header.test.jsx.snap
@@ -196,7 +196,7 @@ exports[` renders correctly for anonymous mobile 1`] = `
`;
-exports[` renders correctly for authenticated desktop 1`] = `
+exports[` renders correctly for authenticated desktop with username 1`] = `
@@ -281,7 +281,113 @@ exports[` renders correctly for authenticated desktop 1`] = `
edX
-
+
+
+
+
+
+
+
+
+
+`;
+
+exports[` renders correctly for authenticated desktop without username 1`] = `
+
+
+ Skip to main content
+
+
+
+
+
+
+
+
+ Courses
+
+
+
+
+
+
+
+
+
+
{
+const AuthenticatedUserDropdown = ({
+ intl, username, name,
+}) => {
const dashboardMenuItem = (
{intl.formatMessage(messages.dashboard)}
);
+ // show avatar instead username if HIDE_USERNAME_FROM_HEADER flag is enabled
+ const dropdownToggle = (
+
+
+ {getConfig().HIDE_USERNAME_FROM_HEADER ? (
+
+ ) : (
+
+ {username}
+
+ )}
+
+ );
+
return (
<>
{intl.formatMessage(messages.help)}
-
-
-
- {username}
-
-
+ {dropdownToggle}
{dashboardMenuItem}
@@ -51,6 +62,11 @@ const AuthenticatedUserDropdown = ({ intl, username }) => {
AuthenticatedUserDropdown.propTypes = {
intl: intlShape.isRequired,
username: PropTypes.string.isRequired,
+ name: PropTypes.string,
+};
+
+AuthenticatedUserDropdown.defaultProps = {
+ name: null,
};
export default injectIntl(AuthenticatedUserDropdown);
diff --git a/src/learning-header/LearningHeader.jsx b/src/learning-header/LearningHeader.jsx
index 373001d19..88dce7d7c 100644
--- a/src/learning-header/LearningHeader.jsx
+++ b/src/learning-header/LearningHeader.jsx
@@ -51,6 +51,7 @@ const LearningHeader = ({
{showUserDropdown && authenticatedUser && (
)}
{showUserDropdown && !authenticatedUser && (
diff --git a/src/learning-header/LearningHeader.test.jsx b/src/learning-header/LearningHeader.test.jsx
index 3d80888ef..64029815e 100644
--- a/src/learning-header/LearningHeader.test.jsx
+++ b/src/learning-header/LearningHeader.test.jsx
@@ -1,4 +1,5 @@
import React from 'react';
+import { getConfig, mergeConfig } from '@edx/frontend-platform';
import {
authenticatedUser, initializeMockApp, render, screen,
} from '../setupTest';
@@ -15,6 +16,17 @@ describe('Header', () => {
expect(screen.getByText(authenticatedUser.username)).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 course data', () => {
const courseData = {
courseOrg: 'course-org',
diff --git a/src/studio-header/HeaderBody.jsx b/src/studio-header/HeaderBody.jsx
index af3690610..2ca70e125 100644
--- a/src/studio-header/HeaderBody.jsx
+++ b/src/studio-header/HeaderBody.jsx
@@ -20,6 +20,7 @@ const HeaderBody = ({
number,
org,
title,
+ name,
username,
isAdmin,
studioBaseUrl,
@@ -99,6 +100,7 @@ const HeaderBody = ({
{
expect(avatarIcon).toBeVisible();
});
+ it('user menu should not contain username', async () => {
+ const config = getConfig();
+ mergeConfig({
+ ...config,
+ HIDE_USERNAME_FROM_HEADER: true,
+ });
+ const { container } = render( );
+ const userMenue = container.querySelector('#user-dropdown-menu');
+ expect(userMenue.textContent).toContain('');
+ });
+
it('should hide nav items if prop isHiddenMainMenu true', async () => {
const initialProps = { ...props, isHiddenMainMenu: true };
const { queryByTestId } = render( );
diff --git a/src/studio-header/UserMenu.jsx b/src/studio-header/UserMenu.jsx
index 03695c165..6d6b1f367 100644
--- a/src/studio-header/UserMenu.jsx
+++ b/src/studio-header/UserMenu.jsx
@@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import { getConfig } from '@edx/frontend-platform';
import {
Avatar,
} from '@openedx/paragon';
@@ -8,6 +9,7 @@ import NavDropdownMenu from './NavDropdownMenu';
import getUserMenuItems from './utils';
const UserMenu = ({
+ name,
username,
studioBaseUrl,
logoutUrl,
@@ -17,22 +19,24 @@ const UserMenu = ({
// injected
intl,
}) => {
+ const hideUsername = getConfig().HIDE_USERNAME_FROM_HEADER;
+ const avatarAlt = hideUsername ? name : username;
const avatar = authenticatedUserAvatar ? (
) : (
);
- const title = isMobile ? avatar : <>{avatar}{username}>;
+ const title = (isMobile || hideUsername) ? avatar : <>{avatar}{username}>;
return (