Compare commits

..

23 Commits

Author SHA1 Message Date
Kyle McCormick
9f2b2a4026 feat: Remove Studio Maintenance link (empty commit for release CI)
This was already merged in
6d424edbd5,
but we are pushing this empty commit with BREAKING CHANGE message
in order to trigger the release of a new semantic version.

BREAKING CHANGE: The Studio "Maintenance" link has been removed
as part of https://github.com/openedx/edx-platform/issues/36263
2025-10-07 15:28:43 -04:00
Feanil Patel
21953ec96d Merge pull request #629 from openedx/renovate/actions-checkout-5.x
chore(deps): update actions/checkout action to v5
2025-10-07 14:06:33 -04:00
renovate[bot]
57671449b1 chore(deps): update actions/checkout action to v5 2025-10-07 17:59:08 +00:00
Feanil Patel
66012905b2 Merge pull request #641 from openedx/renovate/actions-setup-node-5.x
chore(deps): update actions/setup-node action to v5
2025-10-07 13:57:38 -04:00
renovate[bot]
209d52ed60 chore(deps): update actions/setup-node action to v5 2025-10-07 17:55:09 +00:00
Kyle McCormick
6d424edbd5 feat!: Remove Studio Maintenance link (#642)
This reverts commit 48c49fe0b2.

The original commit (a229c34535)
was merged prematurely, so it was reverted. However, now that
we have properly DEPR'd the underlying feature, we can revert
that revert.

Part of: https://github.com/openedx/edx-platform/issues/36263
2025-10-07 13:54:30 -04:00
renovate[bot]
5a4279a7f3 chore(deps): update dependency @openedx/paragon to v23.14.4 (#640)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-07 13:54:01 -04:00
Peter Kulko
ef0ea1378d docs: [FC-86] Restructure Modify Items section with Add/Replace examples (#598)
* docs: example of adding Marketing links via the plugin slots

* refactor: code refactoring

* feat: added logoDestination props for Header component

* refactor: after rebase

* refactor: updated tests

* refactor: after review

* refactor: removed logoDestination prop

* docs: Custom Marketing links mobile example

* refactor: after review
2025-09-29 12:44:28 -04:00
oleksandr.buhaienko
b8b39bffbc test: Remove support for Node 20 2025-09-29 12:26:17 -03:00
renovate[bot]
7f778adda9 chore(deps): update jest monorepo to v30.2.0 (#637)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-29 06:50:45 +00:00
renovate[bot]
4cf48d8ba8 chore(deps): update dependency @openedx/paragon to v23.14.3 (#636)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-29 06:50:27 +00:00
Feanil Patel
2e82ba910d build: remove unused @edx/reactifex package
Remove @edx/reactifex package from devDependencies as it is no longer
needed. Translation extraction functionality has been verified to work
correctly without these dependencies.

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-26 09:58:55 -03:00
oleksandr.buhaienko
02533b0474 build: Upgrade to Node 24 2025-09-26 09:18:14 -03:00
Brian Smith
ecf7f1dfc1 feat!: release breaking change as breaking (#634)
Add README note about typescript

BREAKING CHANGE: release breaking change (typescript) as breaking
2025-09-24 13:51:11 -04:00
renovate[bot]
dbec796ceb chore(deps): update jest monorepo (#633)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 06:06:34 +00:00
bydawen
4a797a59cc test: Add Node 24 to CI matrix (#611) 2025-09-16 10:35:41 -04:00
renovate[bot]
a8a7348605 chore(deps): update dependency @edx/frontend-platform to v8.5.1 (#632)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 04:56:33 +00:00
renovate[bot]
1ff9ecaf15 chore(deps): update dependency @openedx/frontend-build to v14.6.2 (#631)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 05:27:56 +00:00
Jacobo Dominguez
8f67fdba68 refactor: replacement of injectIntl (#625) 2025-08-29 13:05:16 -04:00
renovate[bot]
43684dce91 fix(deps): update dependency @fortawesome/react-fontawesome to v0.2.6 (#630)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 06:16:18 +00:00
renovate[bot]
3ddfbab1ef chore(deps): update dependency @openedx/paragon to v23.14.2 (#628)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-18 04:53:34 +00:00
renovate[bot]
030758e078 chore(deps): update dependency @edx/frontend-platform to v8.5.0 (#627)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-11 04:25:30 +00:00
renovate[bot]
c56b945f0d chore(deps): update dependency @openedx/paragon to v23.14.1 (#626)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-11 04:25:16 +00:00
24 changed files with 8271 additions and 4466 deletions

View File

@@ -11,11 +11,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Setup Nodejs
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version-file: '.nvmrc'
- name: Install dependencies

View File

@@ -10,13 +10,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: ${{ env.NODE_VER }}
- name: Install dependencies

2
.nvmrc
View File

@@ -1 +1 @@
20
24

View File

@@ -35,6 +35,7 @@ Requirements
This component uses ``@edx/frontend-platform`` services such as i18n, analytics, configuration, and the ``AppContext`` React component, and expects that it has been loaded into a micro-frontend that has been properly initialized via ``@edx/frontend-platform``'s ``initialize`` function. `Please visit the frontend template application to see an example. <https://github.com/openedx/frontend-template-application/blob/master/src/index.jsx>`_
As of version 7.x, consuming applications must support typescript.
Environment Variables
====================
@@ -71,9 +72,9 @@ Cloning and Startup
``git clone https://github.com/openedx/frontend-component-header.git``
2. Use node v18.x.
2. Use node v24.x.
The current version of the micro-frontend build scripts support node 18.
The current version of the micro-frontend build scripts support node 24.
Using other major versions of node *may* work, but this is unsupported. For
convenience, this repository includes an .nvmrc file to help in setting the
correct node version via `nvm <https://github.com/nvm-sh/nvm>`_.
@@ -190,4 +191,4 @@ Please do not report security issues in public. Please email security@openedx.or
.. |license| image:: https://img.shields.io/npm/l/@edx/frontend-component-header.svg
:target: @edx/frontend-component-header
.. |semantic-release| image:: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg
:target: https://github.com/semantic-release/semantic-release
:target: https://github.com/semantic-release/semantic-release

12146
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -34,13 +34,12 @@
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/browserslist-config": "^1.1.1",
"@edx/frontend-platform": "^8.3.1",
"@edx/reactifex": "^2.1.1",
"@openedx/frontend-build": "^14.3.2",
"@openedx/paragon": "^23.0.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "5.17.0",
"@testing-library/react": "^16.2.0",
"jest": "30.0.5",
"jest": "30.2.0",
"jest-environment-jsdom": "^30.0.0",
"prop-types": "15.8.1",
"react": "^18.3.1",
@@ -49,7 +48,8 @@
"react-router-dom": "6.30.1",
"react-test-renderer": "^18.3.1",
"redux": "4.2.1",
"redux-saga": "1.3.0"
"redux-saga": "1.3.0",
"ts-jest": "^29.4.4"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "6.7.2",
@@ -71,4 +71,3 @@
"react-router-dom": "^6.14.2"
}
}

View File

@@ -1,6 +1,6 @@
import React, { useContext } from 'react';
import Responsive from 'react-responsive';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import {
APP_CONFIG_INITIALIZED,
@@ -47,9 +47,10 @@ subscribe(APP_CONFIG_INITIALIZED, () => {
* See the documentation for the structure of user menu item.
*/
const Header = ({
intl, mainMenuItems, secondaryMenuItems, userMenuItems,
mainMenuItems, secondaryMenuItems, userMenuItems,
}) => {
const { authenticatedUser, config } = useContext(AppContext);
const intl = useIntl();
const defaultMainMenu = [
{
@@ -139,7 +140,6 @@ Header.defaultProps = {
};
Header.propTypes = {
intl: intlShape.isRequired,
mainMenuItems: PropTypes.oneOfType([
PropTypes.node,
PropTypes.array,
@@ -159,4 +159,4 @@ Header.propTypes = {
})),
};
export default injectIntl(Header);
export default Header;

View File

@@ -61,11 +61,6 @@ const messages = defineMessages({
defaultMessage: 'Studio Home',
description: 'Link to the Studio Home',
},
'header.user.menu.studio.maintenance': {
id: 'header.user.menu.studio.maintenance',
defaultMessage: 'Maintenance',
description: 'Link to the Studio Maintenance',
},
'header.label.account.nav': {
id: 'header.label.account.nav',
defaultMessage: 'Account',

View File

@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
// Local Components
@@ -21,91 +21,73 @@ import messages from '../Header.messages';
// Assets
class DesktopHeader extends React.Component {
constructor(props) { // eslint-disable-line @typescript-eslint/no-useless-constructor
super(props);
}
const DesktopHeader = ({
mainMenu,
secondaryMenu,
userMenu,
loggedOutItems,
logo,
logoAltText,
logoDestination,
avatar,
username,
loggedIn,
}) => {
const intl = useIntl();
renderMainMenu() {
const { mainMenu } = this.props;
return <DesktopMainMenuSlot menu={mainMenu} />;
}
const renderMainMenu = () => <DesktopMainMenuSlot menu={mainMenu} />;
renderSecondaryMenu() {
const { secondaryMenu } = this.props;
return <DesktopSecondaryMenuSlot menu={secondaryMenu} />;
}
const renderSecondaryMenu = () => <DesktopSecondaryMenuSlot menu={secondaryMenu} />;
renderUserMenu() {
const {
userMenu,
avatar,
username,
intl,
} = this.props;
const renderUserMenu = () => (
<Menu transitionClassName="menu-dropdown" transitionTimeout={250}>
<MenuTrigger
tag="button"
aria-label={intl.formatMessage(messages['header.label.account.menu.for'], { username })}
className="btn btn-outline-primary d-inline-flex align-items-center pl-2 pr-3"
>
<DesktopUserMenuToggleSlot avatar={avatar} label={username} />
</MenuTrigger>
<MenuContent className="mb-0 dropdown-menu show dropdown-menu-right pin-right shadow py-2">
<DesktopUserMenuSlot menu={userMenu} />
</MenuContent>
</Menu>
);
return (
<Menu transitionClassName="menu-dropdown" transitionTimeout={250}>
<MenuTrigger
tag="button"
aria-label={intl.formatMessage(messages['header.label.account.menu.for'], { username })}
className="btn btn-outline-primary d-inline-flex align-items-center pl-2 pr-3"
>
<DesktopUserMenuToggleSlot avatar={avatar} label={username} />
</MenuTrigger>
<MenuContent className="mb-0 dropdown-menu show dropdown-menu-right pin-right shadow py-2">
<DesktopUserMenuSlot menu={userMenu} />
</MenuContent>
</Menu>
);
}
const renderLoggedOutItems = () => <DesktopLoggedOutItemsSlot items={loggedOutItems} />;
renderLoggedOutItems() {
const { loggedOutItems } = this.props;
return <DesktopLoggedOutItemsSlot items={loggedOutItems} />;
}
const logoProps = { src: logo, alt: logoAltText, href: logoDestination };
const logoClasses = getConfig().AUTHN_MINIMAL_HEADER ? 'mw-100' : null;
render() {
const {
logo,
logoAltText,
logoDestination,
loggedIn,
intl,
} = this.props;
const logoProps = { src: logo, alt: logoAltText, href: logoDestination };
const logoClasses = getConfig().AUTHN_MINIMAL_HEADER ? 'mw-100' : null;
return (
<header className="site-header-desktop">
<a className="nav-skip sr-only sr-only-focusable" href="#main">{intl.formatMessage(messages['header.label.skip.nav'])}</a>
<div className={`container-fluid ${logoClasses}`}>
<div className="nav-container position-relative d-flex align-items-center">
<LogoSlot {...logoProps} />
<nav
aria-label={intl.formatMessage(messages['header.label.main.nav'])}
className="nav main-nav"
>
{this.renderMainMenu()}
</nav>
<nav
aria-label={intl.formatMessage(messages['header.label.secondary.nav'])}
className="nav secondary-menu-container align-items-center ml-auto"
>
{loggedIn
? (
<>
{this.renderSecondaryMenu()}
{this.renderUserMenu()}
</>
) : this.renderLoggedOutItems()}
</nav>
</div>
return (
<header className="site-header-desktop">
<a className="nav-skip sr-only sr-only-focusable" href="#main">{intl.formatMessage(messages['header.label.skip.nav'])}</a>
<div className={`container-fluid ${logoClasses}`}>
<div className="nav-container position-relative d-flex align-items-center">
<LogoSlot {...logoProps} />
<nav
aria-label={intl.formatMessage(messages['header.label.main.nav'])}
className="nav main-nav"
>
{renderMainMenu()}
</nav>
<nav
aria-label={intl.formatMessage(messages['header.label.secondary.nav'])}
className="nav secondary-menu-container align-items-center ml-auto"
>
{loggedIn
? (
<>
{renderSecondaryMenu()}
{renderUserMenu()}
</>
) : renderLoggedOutItems()}
</nav>
</div>
</header>
);
}
}
</div>
</header>
);
};
export const desktopHeaderDataShape = {
mainMenu: desktopHeaderMainOrSecondaryMenuDataShape,
@@ -131,9 +113,6 @@ DesktopHeader.propTypes = {
avatar: desktopHeaderDataShape.avatar,
username: desktopHeaderDataShape.username,
loggedIn: desktopHeaderDataShape.loggedIn,
// i18n
intl: intlShape.isRequired,
};
DesktopHeader.defaultProps = {
@@ -149,4 +128,4 @@ DesktopHeader.defaultProps = {
loggedIn: false,
};
export default injectIntl(DesktopHeader);
export default DesktopHeader;

View File

@@ -2,12 +2,13 @@ import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import LearningLoggedOutItemsSlot from '../plugin-slots/LearningLoggedOutItemsSlot';
import genericMessages from '../generic/messages';
const AnonymousUserMenu = ({ intl }) => {
const AnonymousUserMenu = () => {
const intl = useIntl();
const buttonsInfo = [
{
message: intl.formatMessage(genericMessages.registerSentenceCase),
@@ -23,8 +24,4 @@ const AnonymousUserMenu = ({ intl }) => {
return <LearningLoggedOutItemsSlot buttonsInfo={buttonsInfo} />;
};
AnonymousUserMenu.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(AnonymousUserMenu);
export default AnonymousUserMenu;

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { faUserCircle } from '@fortawesome/free-solid-svg-icons';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Dropdown } from '@openedx/paragon';
import LearningUserMenuToggleSlot from '../plugin-slots/LearningUserMenuToggleSlot';
@@ -11,7 +11,8 @@ import LearningUserMenuSlot from '../plugin-slots/LearningUserMenuSlot';
import messages from './messages';
const AuthenticatedUserDropdown = ({ intl, username }) => {
const AuthenticatedUserDropdown = ({ username }) => {
const intl = useIntl();
const dropdownItems = [
{
message: intl.formatMessage(messages.dashboard),
@@ -48,8 +49,7 @@ const AuthenticatedUserDropdown = ({ intl, username }) => {
};
AuthenticatedUserDropdown.propTypes = {
intl: intlShape.isRequired,
username: PropTypes.string.isRequired,
};
export default injectIntl(AuthenticatedUserDropdown);
export default AuthenticatedUserDropdown;

View File

@@ -1,7 +1,7 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import AnonymousUserMenu from './AnonymousUserMenu';
@@ -13,8 +13,12 @@ import messages from './messages';
import LearningHelpSlot from '../plugin-slots/LearningHelpSlot';
const LearningHeader = ({
courseOrg, courseNumber, courseTitle, intl, showUserDropdown,
courseOrg,
courseNumber,
courseTitle,
showUserDropdown,
}) => {
const intl = useIntl();
const { authenticatedUser } = useContext(AppContext);
const headerLogo = (
@@ -53,7 +57,6 @@ LearningHeader.propTypes = {
courseOrg: courseInfoDataShape.courseOrg,
courseNumber: courseInfoDataShape.courseNumber,
courseTitle: courseInfoDataShape.courseTitle,
intl: intlShape.isRequired,
showUserDropdown: PropTypes.bool,
};
@@ -64,4 +67,4 @@ LearningHeader.defaultProps = {
showUserDropdown: true,
};
export default injectIntl(LearningHeader);
export default LearningHeader;

View File

@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
// Local Components
@@ -20,99 +20,84 @@ import messages from '../Header.messages';
// Assets
import { MenuIcon } from '../Icons';
class MobileHeader extends React.Component {
constructor(props) { // eslint-disable-line @typescript-eslint/no-useless-constructor
super(props);
}
const MobileHeader = ({
mainMenu,
secondaryMenu,
userMenu,
loggedOutItems,
logo,
logoAltText,
logoDestination,
avatar,
username,
loggedIn,
stickyOnMobile,
}) => {
const intl = useIntl();
renderMainMenu() {
const { mainMenu, secondaryMenu } = this.props;
return <MobileMainMenuSlot menu={[...mainMenu, ...secondaryMenu]} />;
}
const renderMainMenu = () => <MobileMainMenuSlot menu={[...mainMenu, ...secondaryMenu]} />;
renderUserMenuItems() {
const { userMenu } = this.props;
return <MobileUserMenuSlot menu={userMenu} />;
}
const renderUserMenuItems = () => <MobileUserMenuSlot menu={userMenu} />;
renderLoggedOutItems() {
const { loggedOutItems } = this.props;
return <MobileLoggedOutItemsSlot items={loggedOutItems} />;
}
const renderLoggedOutItems = () => <MobileLoggedOutItemsSlot items={loggedOutItems} />;
renderUserMenuToggle() {
const { avatar, username } = this.props;
return <MobileUserMenuToggleSlot avatar={avatar} label={username} />;
}
const renderUserMenuToggle = () => <MobileUserMenuToggleSlot avatar={avatar} label={username} />;
render() {
const {
logo,
logoAltText,
logoDestination,
loggedIn,
stickyOnMobile,
intl,
mainMenu,
userMenu,
loggedOutItems,
} = this.props;
const logoProps = { src: logo, alt: logoAltText, href: logoDestination };
const stickyClassName = stickyOnMobile ? 'sticky-top' : '';
const logoClasses = getConfig().AUTHN_MINIMAL_HEADER ? 'justify-content-left pl-3' : 'justify-content-center';
const logoProps = { src: logo, alt: logoAltText, href: logoDestination };
const stickyClassName = stickyOnMobile ? 'sticky-top' : '';
const logoClasses = getConfig().AUTHN_MINIMAL_HEADER ? 'justify-content-left pl-3' : 'justify-content-center';
return (
<header
aria-label={intl.formatMessage(messages['header.label.main.header'])}
className={`site-header-mobile d-flex justify-content-between align-items-center shadow ${stickyClassName}`}
>
<a className="nav-skip sr-only sr-only-focusable" href="#main">{intl.formatMessage(messages['header.label.skip.nav'])}</a>
{mainMenu.length > 0 ? (
<div className="w-100 d-flex justify-content-start">
return (
<header
aria-label={intl.formatMessage(messages['header.label.main.header'])}
className={`site-header-mobile d-flex justify-content-between align-items-center shadow ${stickyClassName}`}
>
<a className="nav-skip sr-only sr-only-focusable" href="#main">{intl.formatMessage(messages['header.label.skip.nav'])}</a>
{mainMenu.length > 0 ? (
<div className="w-100 d-flex justify-content-start">
<Menu className="position-static">
<MenuTrigger
tag="button"
className="icon-button"
aria-label={intl.formatMessage(messages['header.label.main.menu'])}
title={intl.formatMessage(messages['header.label.main.menu'])}
>
<MenuIcon role="img" aria-hidden focusable="false" style={{ width: '1.5rem', height: '1.5rem' }} />
</MenuTrigger>
<MenuContent
tag="nav"
aria-label={intl.formatMessage(messages['header.label.main.nav'])}
className="nav flex-column pin-left pin-right border-top shadow py-2"
>
{this.renderMainMenu()}
</MenuContent>
</Menu>
</div>
) : null}
<div className={`w-100 d-flex ${logoClasses}`}>
<LogoSlot {...logoProps} itemType="http://schema.org/Organization" />
<Menu className="position-static">
<MenuTrigger
tag="button"
className="icon-button"
aria-label={intl.formatMessage(messages['header.label.main.menu'])}
title={intl.formatMessage(messages['header.label.main.menu'])}
>
<MenuIcon role="img" aria-hidden focusable="false" style={{ width: '1.5rem', height: '1.5rem' }} />
</MenuTrigger>
<MenuContent
tag="nav"
aria-label={intl.formatMessage(messages['header.label.main.nav'])}
className="nav flex-column pin-left pin-right border-top shadow py-2"
>
{renderMainMenu()}
</MenuContent>
</Menu>
</div>
{userMenu.length > 0 || loggedOutItems.length > 0 ? (
<div className="w-100 d-flex justify-content-end align-items-center">
<Menu tag="nav" aria-label={intl.formatMessage(messages['header.label.secondary.nav'])} className="position-static">
<MenuTrigger
tag="button"
className="icon-button"
aria-label={intl.formatMessage(messages['header.label.account.menu'])}
title={intl.formatMessage(messages['header.label.account.menu'])}
>
{this.renderUserMenuToggle()}
</MenuTrigger>
<MenuContent tag="ul" className="nav flex-column pin-left pin-right border-top shadow py-2">
{loggedIn ? this.renderUserMenuItems() : this.renderLoggedOutItems()}
</MenuContent>
</Menu>
</div>
) : null}
</header>
);
}
}
) : null}
<div className={`w-100 d-flex ${logoClasses}`}>
<LogoSlot {...logoProps} itemType="http://schema.org/Organization" />
</div>
{userMenu.length > 0 || loggedOutItems.length > 0 ? (
<div className="w-100 d-flex justify-content-end align-items-center">
<Menu tag="nav" aria-label={intl.formatMessage(messages['header.label.secondary.nav'])} className="position-static">
<MenuTrigger
tag="button"
className="icon-button"
aria-label={intl.formatMessage(messages['header.label.account.menu'])}
title={intl.formatMessage(messages['header.label.account.menu'])}
>
{renderUserMenuToggle()}
</MenuTrigger>
<MenuContent tag="ul" className="nav flex-column pin-left pin-right border-top shadow py-2">
{loggedIn ? renderUserMenuItems() : renderLoggedOutItems()}
</MenuContent>
</Menu>
</div>
) : null}
</header>
);
};
export const mobileHeaderDataShape = {
mainMenu: mobileHeaderMainMenuDataShape,
@@ -140,9 +125,6 @@ MobileHeader.propTypes = {
username: mobileHeaderDataShape.username,
loggedIn: mobileHeaderDataShape.loggedIn,
stickyOnMobile: mobileHeaderDataShape.stickyOnMobile,
// i18n
intl: intlShape.isRequired,
};
MobileHeader.defaultProps = {
@@ -160,4 +142,4 @@ MobileHeader.defaultProps = {
};
export default injectIntl(MobileHeader);
export default MobileHeader;

View File

@@ -13,9 +13,11 @@ This slot is used to replace/modify/hide the desktop main menu.
### Modify Items
The following `env.config.jsx` will modify the items in the desktop main menu.
#### Replace All Items
![Screenshot of modified items](./images/desktop_main_menu_modify_items.png)
The following `env.config.jsx` will replace all items in the desktop main menu.
![Screenshot of modified items](./images/desktop_main_menu_replace_all_items.png)
```jsx
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
@@ -59,6 +61,58 @@ const config = {
export default config;
```
#### Add Items
The following `env.config.jsx` will add items in the desktop main menu.
![Screenshot of custom marketing links](./images/desktop_main_menu_add_items.png)
```jsx
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const modifyMainMenu = (widget) => {
const existingMenu = widget.RenderWidget.props.menu || [];
const newMarketingLinks = [
{
type: 'item',
href: 'https://example.com/how-it-works',
content: 'How it works',
},
{
type: 'item',
href: 'https://example.com/courses',
content: 'Courses',
},
{
type: 'item',
href: 'https://example.com/schools',
content: 'Schools',
}
];
widget.content.menu = [...existingMenu, ...newMarketingLinks];
return widget;
};
const config = {
pluginSlots: {
'org.openedx.frontend.layout.header_desktop_main_menu.v1': {
keepDefault: true,
plugins: [
{
op: PLUGIN_OPERATIONS.Modify,
widgetId: 'default_contents',
fn: modifyMainMenu,
},
]
},
},
}
export default config;
```
### Replace Menu with Custom Component
The following `env.config.jsx` will replace the desktop main menu entirely (in this case with a centered 🗺️ `h1`)
@@ -134,4 +188,3 @@ const config = {
export default config;
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -13,9 +13,11 @@ This slot is used to replace/modify/hide the mobile main menu.
### Modify Items
The following `env.config.jsx` will modify the items in the mobile main menu.
#### Replace All Items
![Screenshot of modified items](./images/mobile_main_menu_modify_items.png)
The following `env.config.jsx` will replace all items in the mobile main menu.
![Screenshot of modified items](./images/mobile_main_menu_replace_all_items.png)
```jsx
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
@@ -59,6 +61,58 @@ const config = {
export default config;
```
#### Add Items
The following `env.config.jsx` will add items in the mobile main menu.
![Screenshot of custom marketing links](./images/mobile_main_menu_add_items.png)
```jsx
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const modifyMainMenu = (widget) => {
const existingMenu = widget.RenderWidget.props.menu || [];
const newMarketingLinks = [
{
type: 'item',
href: 'https://example.com/how-it-works',
content: 'How it works',
},
{
type: 'item',
href: 'https://example.com/courses',
content: 'Courses',
},
{
type: 'item',
href: 'https://example.com/schools',
content: 'Schools',
}
];
widget.content.menu = [...existingMenu, ...newMarketingLinks];
return widget;
};
const config = {
pluginSlots: {
'org.openedx.frontend.layout.header_mobile_main_menu.v1': {
keepDefault: true,
plugins: [
{
op: PLUGIN_OPERATIONS.Modify,
widgetId: 'default_contents',
fn: modifyMainMenu,
},
]
},
},
}
export default config;
```
### Replace Menu with Custom Component
The following `env.config.jsx` will replace the mobile main menu entirely (in this case with a centered 🗺️ `h1`)

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
OverlayTrigger,
Tooltip,
@@ -9,41 +9,43 @@ import { Link } from 'react-router-dom';
import messages from './messages';
const CourseLockUp = ({
outlineLink,
org,
number,
title,
// injected
intl,
}) => (
<OverlayTrigger
placement="bottom"
overlay={(
<Tooltip id="course-lock-up">
{title}
</Tooltip>
)}
>
<Link
className="course-title-lockup mr-2"
to={outlineLink}
aria-label={intl.formatMessage(messages['header.label.courseOutline'])}
data-testid="course-lock-up-block"
const CourseLockUp = (
{
outlineLink,
org,
number,
title,
},
) => {
const intl = useIntl();
return (
<OverlayTrigger
placement="bottom"
overlay={(
<Tooltip id="course-lock-up">
{title}
</Tooltip>
)}
>
<span className="d-block small m-0 text-gray-800" data-testid="course-org-number">{org} {number}</span>
<span className="d-block m-0 font-weight-bold text-gray-800" data-testid="course-title">{title}</span>
</Link>
</OverlayTrigger>
);
<Link
className="course-title-lockup mr-2"
to={outlineLink}
aria-label={intl.formatMessage(messages['header.label.courseOutline'])}
data-testid="course-lock-up-block"
>
<span className="d-block small m-0 text-gray-800" data-testid="course-org-number">{org} {number}</span>
<span className="d-block m-0 font-weight-bold text-gray-800" data-testid="course-title">{title}</span>
</Link>
</OverlayTrigger>
);
};
CourseLockUp.propTypes = {
number: PropTypes.string,
org: PropTypes.string,
title: PropTypes.string,
outlineLink: PropTypes.string,
// injected
intl: intlShape.isRequired,
};
CourseLockUp.defaultProps = {
@@ -53,4 +55,4 @@ CourseLockUp.defaultProps = {
outlineLink: null,
};
export default injectIntl(CourseLockUp);
export default CourseLockUp;

View File

@@ -12,7 +12,6 @@ import { Context as ResponsiveContext } from 'react-responsive';
import { MemoryRouter } from 'react-router-dom';
import StudioHeader from './StudioHeader';
import messages from './messages';
const authenticatedUser = {
userId: 3,
@@ -119,16 +118,6 @@ describe('Header', () => {
expect(dropdownOption).toBeVisible();
});
it('maintenance should not be in user menu', async () => {
currentUser = { ...authenticatedUser, administrator: false };
const { getAllByRole, queryByText } = render(<RootWrapper {...props} />);
const userMenu = getAllByRole('button')[1];
await waitFor(() => fireEvent.click(userMenu));
const maintenanceButton = queryByText(messages['header.user.menu.maintenance'].defaultMessage);
expect(maintenanceButton).toBeNull();
});
it('user menu should use avatar icon', async () => {
currentUser = { ...authenticatedUser, avatar: null };
const { getByTestId } = render(<RootWrapper {...props} />);
@@ -190,15 +179,6 @@ describe('Header', () => {
expect(desktopMenu).toBeNull();
});
it('maintenance should be in user menu', async () => {
const { getAllByRole, getByText } = render(<RootWrapper {...props} />);
const userMenu = getAllByRole('button')[1];
await waitFor(() => fireEvent.click(userMenu));
const maintenanceButton = getByText(messages['header.user.menu.maintenance'].defaultMessage);
expect(maintenanceButton).toBeVisible();
});
it('user menu should use avatar image', async () => {
const { getByTestId } = render(<RootWrapper {...props} />);
const avatarImage = getByTestId('avatar-image');

View File

@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Avatar,
} from '@openedx/paragon';
@@ -14,9 +14,8 @@ const UserMenu = ({
authenticatedUserAvatar,
isMobile,
isAdmin,
// injected
intl,
}) => {
const intl = useIntl();
const avatar = authenticatedUserAvatar ? (
<img
className="d-block w-100 h-100"
@@ -55,8 +54,6 @@ UserMenu.propTypes = {
authenticatedUserAvatar: PropTypes.string,
isMobile: PropTypes.bool,
isAdmin: PropTypes.bool,
// injected
intl: intlShape.isRequired,
};
UserMenu.defaultProps = {
@@ -66,4 +63,4 @@ UserMenu.defaultProps = {
username: null,
};
export default injectIntl(UserMenu);
export default UserMenu;

View File

@@ -6,11 +6,6 @@ const messages = defineMessages({
defaultMessage: 'Studio Home',
description: 'Link to Studio Home',
},
'header.user.menu.maintenance': {
id: 'header.user.menu.maintenance',
defaultMessage: 'Maintenance',
description: 'Link to the Studio maintenance page',
},
'header.user.menu.logout': {
id: 'header.user.menu.logout',
defaultMessage: 'Logout',

View File

@@ -1,4 +1,3 @@
import { getConfig } from '@edx/frontend-platform';
import messages from './messages';
const getUserMenuItems = ({
@@ -21,9 +20,6 @@ const getUserMenuItems = ({
{
href: `${studioBaseUrl}`,
title: intl.formatMessage(messages['header.user.menu.studio']),
}, {
href: `${getConfig().STUDIO_BASE_URL}/maintenance`,
title: intl.formatMessage(messages['header.user.menu.maintenance']),
}, {
href: `${logoutUrl}`,
title: intl.formatMessage(messages['header.user.menu.logout']),