Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: set username behind flag #465

Closed
wants to merge 1 commit into from
Closed
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 README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

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

General question (hoping to hear what others think): Do we prefer this as HIDE or as SHOW? I could be persuaded either way.

Copy link
Contributor

@brian-smith-tcril brian-smith-tcril Feb 13, 2024

Choose a reason for hiding this comment

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

Come to think of it, I could even see this being non-boolean: something like NAME_IN_MENU_BUTTON with the options being username, fullName, none

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.

Expand Down
1 change: 1 addition & 0 deletions example/index.js
Copy link
Contributor

Choose a reason for hiding this comment

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

name should be added to the authenticatedUser in the context for the StudioHeader too

Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ subscribe(APP_READY, () => {
authenticatedUser: {
userId: '123abc',
username: 'testuser',
name: 'test user',
Copy link
Contributor

Choose a reason for hiding this comment

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

Another general question: Would we prefer this as fullName? With this test data I'm assuming we'd expect both first and last name in here. If that's the case I feel fullName is more descriptive.

Copy link

@zainab-amir zainab-amir Feb 14, 2024

Choose a reason for hiding this comment

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

The authenticated data sent in JWT has "name" not "fullName"

roles: [],
administrator: false,
},
Expand Down
12 changes: 9 additions & 3 deletions src/DesktopHeader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,21 +74,25 @@ class DesktopHeader extends React.Component {

renderUserMenu() {
const {
intl,
userMenu,
avatar,
name,
Copy link
Contributor

Choose a reason for hiding this comment

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

I'll copy-paste from another review on a related PR:

This would make so there's no longer a 1:1 relationship between the DesktopHeader props and what is actually shown. In other words, it's not immediately obvious without looking at the implementation what happens if you provide both username and name.

In other words, unless I'm missing something, DesktopHeader has no reason to distinguish between username and name. It should just use a single prop, and the business logic of what to actually put there should be up to the user of the component.

username,
intl,
} = this.props;
const hideUsername = getConfig().HIDE_USERNAME_FROM_HEADER;
const usernameOrName = hideUsername ? name : username;
Comment on lines +83 to +84
Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like this has been implemented in a way that assumes the "hide username from menu button in header" is tied to "don't use usernames." It seems to me those are two separate things. I could definitely see someone running an Open edX instance that uses usernames but not wanting to show them in the menu dropdown button.

In this case the difference seems quite minor, but I can imagine scenarios where having "hide username" and "we don't use usernames" coupled could be problematic.

Choose a reason for hiding this comment

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

We will be auto-generating username and showing that in alt text doesn't make sense. I can see how HIDE_USERNAME_FROM_HEADER doesn't provide more context around this.

Copy link

@zainab-amir zainab-amir Feb 14, 2024

Choose a reason for hiding this comment

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

Maybe we can go with your suggestion of NAME_IN_MENU_BUTTON. Which tells us whether we want to use username or fullName in the header and use the same in screen reader text too.


return (
<Menu transitionClassName="menu-dropdown" transitionTimeout={250}>
<MenuTrigger
tag="button"
aria-label={intl.formatMessage(messages['header.label.account.menu.for'], { username })}
aria-label={intl.formatMessage(messages['header.label.account.menu.using.name.or.username'], { usernameOrName })}
Copy link
Contributor

Choose a reason for hiding this comment

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

Another open question: Do we want to use a new message key here?

An alternative solution that comes to mind is

Suggested change
aria-label={intl.formatMessage(messages['header.label.account.menu.using.name.or.username'], { usernameOrName })}
aria-label={intl.formatMessage(messages['header.label.account.menu.for'], { username: usernameOrName })}

className="btn btn-outline-primary d-inline-flex align-items-center pl-2 pr-3"
>
<Avatar size="1.5em" src={avatar} alt="" className="mr-2" />
Copy link
Contributor

Choose a reason for hiding this comment

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

I know this wasn't added in this PR, but StudioHeader uses name/username for the alt test in UserMenu. Does it make sense to do the same here for consistency?

{username} <CaretIcon role="img" aria-hidden focusable="false" />
{!hideUsername && username}
<CaretIcon role="img" aria-hidden focusable="false" />
</MenuTrigger>
<MenuContent className="mb-0 dropdown-menu show dropdown-menu-right pin-right shadow py-2">
{userMenu.map(({ type, href, content }) => (
Expand Down Expand Up @@ -178,6 +182,7 @@ DesktopHeader.propTypes = {
logoDestination: PropTypes.string,
avatar: PropTypes.string,
username: PropTypes.string,
name: PropTypes.string,
loggedIn: PropTypes.bool,

// i18n
Expand Down Expand Up @@ -207,6 +212,7 @@ DesktopHeader.defaultProps = {
logoDestination: null,
avatar: null,
username: null,
name: null,
loggedIn: false,
appMenu: null,
};
Expand Down
2 changes: 2 additions & 0 deletions src/Header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});

Expand Down Expand Up @@ -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,
Expand Down
8 changes: 4 additions & 4 deletions src/Header.messages.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
28 changes: 27 additions & 1 deletion src/Header.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,12 @@ describe('<Header />', () => {
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,
},
Expand All @@ -61,6 +62,30 @@ describe('<Header />', () => {
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 = <HeaderComponent width={{ width: 1280 }} contextValue={contextValue} />;

const wrapper = TestRenderer.create(component);

expect(wrapper.toJSON()).toMatchSnapshot();
});

it('renders correctly for anonymous mobile', () => {
const contextValue = {
authenticatedUser: null,
Expand All @@ -84,6 +109,7 @@ describe('<Header />', () => {
authenticatedUser: {
userId: 'abc123',
username: 'edX',
name: 'edX',
roles: [],
administrator: false,
},
Expand Down
110 changes: 108 additions & 2 deletions src/__snapshots__/Header.test.jsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ exports[`<Header /> renders correctly for anonymous mobile 1`] = `
</header>
`;

exports[`<Header /> renders correctly for authenticated desktop 1`] = `
exports[`<Header /> renders correctly for authenticated desktop with username 1`] = `
<header
className="site-header-desktop"
>
Expand Down Expand Up @@ -281,7 +281,113 @@ exports[`<Header /> renders correctly for authenticated desktop 1`] = `
</svg>
</span>
edX

<svg
aria-hidden={true}
focusable="false"
height="16px"
role="img"
version="1.1"
viewBox="0 0 16 16"
width="16px"
>
<path
d="M7,4 L7,8 L11,8 L11,10 L5,10 L5,4 L7,4 Z"
fill="currentColor"
transform="translate(8.000000, 7.000000) rotate(-45.000000) translate(-8.000000, -7.000000) "
/>
</svg>
</button>
</div>
</nav>
</div>
</div>
</header>
`;

exports[`<Header /> renders correctly for authenticated desktop without username 1`] = `
<header
className="site-header-desktop"
>
<a
className="nav-skip sr-only sr-only-focusable"
href="#main"
>
Skip to main content
</a>
<div
className="container-fluid null"
>
<div
className="nav-container position-relative d-flex align-items-center"
>
<a
className="logo"
href="http://localhost:18000/dashboard"
>
<img
alt="edX"
className="d-block"
src="https://edx-cdn.org/v3/default/logo.svg"
/>
</a>
<nav
aria-label="Main"
className="nav main-nav"
>
<a
className="nav-link"
href="http://localhost:18000/dashboard"
>
Courses
</a>
</nav>
<nav
aria-label="Secondary"
className="nav secondary-menu-container align-items-center ml-auto"
>
<div
className="menu null"
onKeyDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<button
aria-expanded={false}
aria-haspopup="menu"
aria-label="Account menu for "
className="menu-trigger btn btn-outline-primary d-inline-flex align-items-center pl-2 pr-3"
onClick={[Function]}
>
<span
className="avatar overflow-hidden d-inline-flex rounded-circle mr-2"
style={
Object {
"height": "1.5em",
"width": "1.5em",
}
}
>
<svg
aria-hidden={true}
focusable="false"
height="24px"
role="img"
style={
Object {
"height": "1.5em",
"width": "1.5em",
}
}
version="1.1"
viewBox="0 0 24 24"
width="24px"
>
<path
d="M4.10255106,18.1351061 C4.7170266,16.0581859 8.01891846,14.4720277 12,14.4720277 C15.9810815,14.4720277 19.2829734,16.0581859 19.8974489,18.1351061 C21.215206,16.4412566 22,14.3122775 22,12 C22,6.4771525 17.5228475,2 12,2 C6.4771525,2 2,6.4771525 2,12 C2,14.3122775 2.78479405,16.4412566 4.10255106,18.1351061 Z M12,24 C5.372583,24 0,18.627417 0,12 C0,5.372583 5.372583,0 12,0 C18.627417,0 24,5.372583 24,12 C24,18.627417 18.627417,24 12,24 Z M12,13 C9.790861,13 8,11.209139 8,9 C8,6.790861 9.790861,5 12,5 C14.209139,5 16,6.790861 16,9 C16,11.209139 14.209139,13 12,13 Z"
fill="currentColor"
/>
</svg>
</span>
<svg
aria-hidden={true}
focusable="false"
Expand Down
32 changes: 24 additions & 8 deletions src/learning-header/AuthenticatedUserDropdown.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,38 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faUserCircle } from '@fortawesome/free-solid-svg-icons';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Dropdown } from '@openedx/paragon';
import { Avatar, Dropdown } from '@openedx/paragon';

import messages from './messages';

const AuthenticatedUserDropdown = ({ intl, username }) => {
const AuthenticatedUserDropdown = ({
intl, username, name,
}) => {
const dashboardMenuItem = (
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/dashboard`}>
{intl.formatMessage(messages.dashboard)}
</Dropdown.Item>
);

// show avatar instead username if HIDE_USERNAME_FROM_HEADER flag is enabled

Choose a reason for hiding this comment

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

Suggested change
// show avatar instead username if HIDE_USERNAME_FROM_HEADER flag is enabled
// show avatar instead of username if `HIDE_USERNAME_FROM_HEADER flag` is enabled

const dropdownToggle = (
<Dropdown.Toggle variant="outline-primary">
<FontAwesomeIcon icon={faUserCircle} className="d-md-none" size="lg" />
{getConfig().HIDE_USERNAME_FROM_HEADER ? (
<Avatar size="sm" alt={name} className="mr-2" />
) : (
<span data-hj-suppress className="d-none d-md-inline" data-testid="username">
{username}
</span>
)}
</Dropdown.Toggle>
);

return (
<>
<a className="text-gray-700" href={`${getConfig().SUPPORT_URL}`}>{intl.formatMessage(messages.help)}</a>
<Dropdown className="user-dropdown ml-3">
<Dropdown.Toggle variant="outline-primary">
<FontAwesomeIcon icon={faUserCircle} className="d-md-none" size="lg" />
<span data-hj-suppress className="d-none d-md-inline">
{username}
</span>
</Dropdown.Toggle>
{dropdownToggle}
<Dropdown.Menu className="dropdown-menu-right">
{dashboardMenuItem}
<Dropdown.Item href={`${getConfig().ACCOUNT_PROFILE_URL}/u/${username}`}>
Expand All @@ -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);
1 change: 1 addition & 0 deletions src/learning-header/LearningHeader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const LearningHeader = ({
{showUserDropdown && authenticatedUser && (
<AuthenticatedUserDropdown
username={authenticatedUser.username}
name={authenticatedUser.name}
/>
)}
{showUserDropdown && !authenticatedUser && (
Expand Down
12 changes: 12 additions & 0 deletions src/learning-header/LearningHeader.test.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { getConfig, mergeConfig } from '@edx/frontend-platform';
import {
authenticatedUser, initializeMockApp, render, screen,
} from '../setupTest';
Expand All @@ -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(<Header />);
const userName = queryByTestId('username');
expect(userName).not.toBeInTheDocument();
});

it('displays course data', () => {
const courseData = {
courseOrg: 'course-org',
Expand Down
4 changes: 4 additions & 0 deletions src/studio-header/HeaderBody.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const HeaderBody = ({
number,
org,
title,
name,
username,
isAdmin,
studioBaseUrl,
Expand Down Expand Up @@ -99,6 +100,7 @@ const HeaderBody = ({
<Nav>
<UserMenu
{...{
name,
username,
studioBaseUrl,
logoutUrl,
Expand All @@ -124,6 +126,7 @@ HeaderBody.propTypes = {
logo: PropTypes.string,
logoAltText: PropTypes.string,
authenticatedUserAvatar: PropTypes.string,
name: PropTypes.string,
username: PropTypes.string,
isAdmin: PropTypes.bool,
isMobile: PropTypes.bool,
Expand All @@ -149,6 +152,7 @@ HeaderBody.defaultProps = {
org: '',
title: '',
authenticatedUserAvatar: null,
name: null,
username: null,
isAdmin: false,
isMobile: false,
Expand Down
1 change: 1 addition & 0 deletions src/studio-header/StudioHeader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const StudioHeader = ({
number,
org,
title,
name: authenticatedUser?.name,
username: authenticatedUser?.username,
isAdmin: authenticatedUser?.administrator,
authenticatedUserAvatar: authenticatedUser?.avatar,
Expand Down
Loading
Loading