Compare commits
17 Commits
ags/webpac
...
v5.7.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a229c34535 | ||
|
|
5d7b4fecf4 | ||
|
|
f04130a7c6 | ||
|
|
cb7774b325 | ||
|
|
3e4eb21d8c | ||
|
|
a346dccd4c | ||
|
|
c64a201072 | ||
|
|
6496642643 | ||
|
|
a6c36654b4 | ||
|
|
ae5253c822 | ||
|
|
e44001e945 | ||
|
|
e07cf665a4 | ||
|
|
8213ee7460 | ||
|
|
8a7d6eecdf | ||
|
|
a2497eeb22 | ||
|
|
a703abad76 | ||
|
|
3f4d987d12 |
4
.github/workflows/ci.yml
vendored
@@ -14,12 +14,10 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Setup Nodejs Env
|
|
||||||
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
|
||||||
- name: Setup Nodejs
|
- name: Setup Nodejs
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VER }}
|
node-version-file: '.nvmrc'
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Validate package-lock.json changes
|
- name: Validate package-lock.json changes
|
||||||
|
|||||||
2
.github/workflows/lockfileversion-check.yml
vendored
@@ -10,4 +10,4 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
version-check:
|
version-check:
|
||||||
uses: openedx/.github/.github/workflows/lockfile-check.yml@master
|
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
|
||||||
|
|||||||
@@ -95,6 +95,12 @@ This library has the following exports:
|
|||||||
* ``messages``: Internationalization messages suitable for use with `@edx/frontend-platform/i18n <https://edx.github.io/frontend-platform/module-Internationalization.html>`_
|
* ``messages``: Internationalization messages suitable for use with `@edx/frontend-platform/i18n <https://edx.github.io/frontend-platform/module-Internationalization.html>`_
|
||||||
* ``dist/index.scss``: A SASS file which contains style information for the component. It should be imported into the micro-frontend's own SCSS file.
|
* ``dist/index.scss``: A SASS file which contains style information for the component. It should be imported into the micro-frontend's own SCSS file.
|
||||||
|
|
||||||
|
Plugins
|
||||||
|
-------
|
||||||
|
This can be customized using `Frontend Plugin Framework <https://github.com/openedx/frontend-plugin-framework>`_.
|
||||||
|
|
||||||
|
The parts of this that can be customized in that manner are documented `here </src/plugin-slots>`_.
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
========
|
========
|
||||||
|
|
||||||
|
|||||||
4607
package-lock.json
generated
13
package.json
@@ -35,10 +35,10 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||||
"@edx/browserslist-config": "^1.1.1",
|
"@edx/browserslist-config": "^1.1.1",
|
||||||
"@edx/frontend-platform": "8.1.1",
|
"@edx/frontend-platform": "8.1.2",
|
||||||
"@edx/reactifex": "^2.1.1",
|
"@edx/reactifex": "^2.1.1",
|
||||||
"@openedx/frontend-build": "14.1.2",
|
"@openedx/frontend-build": "14.1.5",
|
||||||
"@openedx/paragon": "22.7.0",
|
"@openedx/paragon": "22.9.0",
|
||||||
"@testing-library/dom": "10.4.0",
|
"@testing-library/dom": "10.4.0",
|
||||||
"@testing-library/jest-dom": "5.17.0",
|
"@testing-library/jest-dom": "5.17.0",
|
||||||
"@testing-library/react": "10.4.9",
|
"@testing-library/react": "10.4.9",
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-redux": "7.2.9",
|
"react-redux": "7.2.9",
|
||||||
"react-router-dom": "6.26.1",
|
"react-router-dom": "6.28.0",
|
||||||
"react-test-renderer": "17.0.2",
|
"react-test-renderer": "17.0.2",
|
||||||
"redux": "4.2.1",
|
"redux": "4.2.1",
|
||||||
"redux-saga": "1.3.0"
|
"redux-saga": "1.3.0"
|
||||||
@@ -60,8 +60,10 @@
|
|||||||
"@fortawesome/free-regular-svg-icons": "6.6.0",
|
"@fortawesome/free-regular-svg-icons": "6.6.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "6.6.0",
|
"@fortawesome/free-solid-svg-icons": "6.6.0",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
|
"@openedx/frontend-plugin-framework": "^1.3.0",
|
||||||
"axios-mock-adapter": "1.22.0",
|
"axios-mock-adapter": "1.22.0",
|
||||||
"babel-polyfill": "6.26.0",
|
"babel-polyfill": "6.26.0",
|
||||||
|
"classnames": "^2.5.1",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"react-responsive": "8.2.0",
|
"react-responsive": "8.2.0",
|
||||||
"react-transition-group": "4.4.5"
|
"react-transition-group": "4.4.5"
|
||||||
@@ -71,6 +73,7 @@
|
|||||||
"@openedx/paragon": ">= 21.5.7 < 23.0.0",
|
"@openedx/paragon": ">= 21.5.7 < 23.0.0",
|
||||||
"prop-types": "^15.5.10",
|
"prop-types": "^15.5.10",
|
||||||
"react": "^16.9.0 || ^17.0.0",
|
"react": "^16.9.0 || ^17.0.0",
|
||||||
"react-dom": "^16.9.0 || ^17.0.0"
|
"react-dom": "^16.9.0 || ^17.0.0",
|
||||||
|
"react-router-dom": "^6.14.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,222 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
|
|
||||||
// Local Components
|
|
||||||
import { Menu, MenuTrigger, MenuContent } from './Menu';
|
|
||||||
import Avatar from './Avatar';
|
|
||||||
import { LinkedLogo, Logo } from './Logo';
|
|
||||||
|
|
||||||
// i18n
|
|
||||||
import messages from './Header.messages';
|
|
||||||
|
|
||||||
// Assets
|
|
||||||
import { CaretIcon } from './Icons';
|
|
||||||
|
|
||||||
class DesktopHeader extends React.Component {
|
|
||||||
constructor(props) { // eslint-disable-line no-useless-constructor
|
|
||||||
super(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderMenu(menu) {
|
|
||||||
// Nodes are accepted as a prop
|
|
||||||
if (!Array.isArray(menu)) {
|
|
||||||
return menu;
|
|
||||||
}
|
|
||||||
|
|
||||||
return menu.map((menuItem) => {
|
|
||||||
const {
|
|
||||||
type,
|
|
||||||
href,
|
|
||||||
content,
|
|
||||||
submenuContent,
|
|
||||||
disabled,
|
|
||||||
isActive,
|
|
||||||
onClick,
|
|
||||||
} = menuItem;
|
|
||||||
|
|
||||||
if (type === 'item') {
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
key={`${type}-${content}`}
|
|
||||||
className={`nav-link${disabled ? ' disabled' : ''}${isActive ? ' active' : ''}`}
|
|
||||||
href={href}
|
|
||||||
onClick={onClick || null}
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Menu key={`${type}-${content}`} tag="div" className="nav-item" respondToPointerEvents>
|
|
||||||
<MenuTrigger onClick={onClick || null} tag="a" className="nav-link d-inline-flex align-items-center" href={href}>
|
|
||||||
{content} <CaretIcon role="img" aria-hidden focusable="false" />
|
|
||||||
</MenuTrigger>
|
|
||||||
<MenuContent className="pin-left pin-right shadow py-2">
|
|
||||||
{submenuContent}
|
|
||||||
</MenuContent>
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
renderMainMenu() {
|
|
||||||
const { mainMenu } = this.props;
|
|
||||||
return this.renderMenu(mainMenu);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSecondaryMenu() {
|
|
||||||
const { secondaryMenu } = this.props;
|
|
||||||
return this.renderMenu(secondaryMenu);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderUserMenu() {
|
|
||||||
const {
|
|
||||||
userMenu,
|
|
||||||
avatar,
|
|
||||||
username,
|
|
||||||
intl,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
<Avatar size="1.5em" src={avatar} alt="" className="mr-2" />
|
|
||||||
{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((group, index) => (
|
|
||||||
// eslint-disable-next-line react/jsx-no-comment-textnodes,react/no-array-index-key
|
|
||||||
<React.Fragment key={index}>
|
|
||||||
{group.heading && <div className="dropdown-header" role="heading" aria-level="1">{group.heading}</div>}
|
|
||||||
{group.items.map(({
|
|
||||||
type, content, href, disabled, isActive, onClick,
|
|
||||||
}) => (
|
|
||||||
<a
|
|
||||||
className={`dropdown-${type}${isActive ? ' active' : ''}${disabled ? ' disabled' : ''}`}
|
|
||||||
key={`${type}-${content}`}
|
|
||||||
href={href}
|
|
||||||
onClick={onClick || null}
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
{index < userMenu.length - 1 && <div className="dropdown-divider" role="separator" />}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</MenuContent>
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderLoggedOutItems() {
|
|
||||||
const { loggedOutItems } = this.props;
|
|
||||||
|
|
||||||
return loggedOutItems.map((item, i, arr) => (
|
|
||||||
<a
|
|
||||||
key={`${item.type}-${item.content}`}
|
|
||||||
className={i < arr.length - 1 ? 'btn mr-2 btn-link' : 'btn mr-2 btn-outline-primary'}
|
|
||||||
href={item.href}
|
|
||||||
>
|
|
||||||
{item.content}
|
|
||||||
</a>
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
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">
|
|
||||||
{logoDestination === null ? <Logo className="logo" src={logo} alt={logoAltText} /> : <LinkedLogo className="logo" {...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>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DesktopHeader.propTypes = {
|
|
||||||
mainMenu: PropTypes.oneOfType([
|
|
||||||
PropTypes.node,
|
|
||||||
PropTypes.array,
|
|
||||||
]),
|
|
||||||
secondaryMenu: PropTypes.oneOfType([
|
|
||||||
PropTypes.node,
|
|
||||||
PropTypes.array,
|
|
||||||
]),
|
|
||||||
userMenu: PropTypes.arrayOf(PropTypes.shape({
|
|
||||||
heading: PropTypes.string,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.shape({
|
|
||||||
type: PropTypes.oneOf(['item', 'menu']),
|
|
||||||
href: PropTypes.string,
|
|
||||||
content: PropTypes.string,
|
|
||||||
isActive: PropTypes.bool,
|
|
||||||
onClick: PropTypes.func,
|
|
||||||
})),
|
|
||||||
})),
|
|
||||||
loggedOutItems: PropTypes.arrayOf(PropTypes.shape({
|
|
||||||
type: PropTypes.oneOf(['item', 'menu']),
|
|
||||||
href: PropTypes.string,
|
|
||||||
content: PropTypes.string,
|
|
||||||
})),
|
|
||||||
logo: PropTypes.string,
|
|
||||||
logoAltText: PropTypes.string,
|
|
||||||
logoDestination: PropTypes.string,
|
|
||||||
avatar: PropTypes.string,
|
|
||||||
username: PropTypes.string,
|
|
||||||
loggedIn: PropTypes.bool,
|
|
||||||
|
|
||||||
// i18n
|
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
DesktopHeader.defaultProps = {
|
|
||||||
mainMenu: [],
|
|
||||||
secondaryMenu: [],
|
|
||||||
userMenu: [],
|
|
||||||
loggedOutItems: [],
|
|
||||||
logo: null,
|
|
||||||
logoAltText: null,
|
|
||||||
logoDestination: null,
|
|
||||||
avatar: null,
|
|
||||||
username: null,
|
|
||||||
loggedIn: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(DesktopHeader);
|
|
||||||
@@ -11,8 +11,8 @@ import {
|
|||||||
} from '@edx/frontend-platform';
|
} from '@edx/frontend-platform';
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import DesktopHeader from './DesktopHeader';
|
import DesktopHeaderSlot from './plugin-slots/DesktopHeaderSlot';
|
||||||
import MobileHeader from './MobileHeader';
|
import MobileHeaderSlot from './plugin-slots/MobileHeaderSlot';
|
||||||
|
|
||||||
import messages from './Header.messages';
|
import messages from './Header.messages';
|
||||||
|
|
||||||
@@ -123,10 +123,10 @@ const Header = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Responsive maxWidth={769}>
|
<Responsive maxWidth={769}>
|
||||||
<MobileHeader {...props} />
|
<MobileHeaderSlot props={props} />
|
||||||
</Responsive>
|
</Responsive>
|
||||||
<Responsive minWidth={769}>
|
<Responsive minWidth={769}>
|
||||||
<DesktopHeader {...props} />
|
<DesktopHeaderSlot props={props} />
|
||||||
</Responsive>
|
</Responsive>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -61,11 +61,6 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'Studio Home',
|
defaultMessage: 'Studio Home',
|
||||||
description: 'Link to the 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': {
|
'header.label.account.nav': {
|
||||||
id: 'header.label.account.nav',
|
id: 'header.label.account.nav',
|
||||||
defaultMessage: 'Account',
|
defaultMessage: 'Account',
|
||||||
|
|||||||
18
src/Logo.jsx
@@ -1,31 +1,23 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
const Logo = ({ src, alt, ...attributes }) => (
|
const Logo = ({
|
||||||
<img src={src} alt={alt} {...attributes} />
|
|
||||||
);
|
|
||||||
|
|
||||||
Logo.propTypes = {
|
|
||||||
src: PropTypes.string.isRequired,
|
|
||||||
alt: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
const LinkedLogo = ({
|
|
||||||
href,
|
href,
|
||||||
src,
|
src,
|
||||||
alt,
|
alt,
|
||||||
...attributes
|
...attributes
|
||||||
}) => (
|
}) => (
|
||||||
<a href={href} {...attributes}>
|
<a href={href} className="logo" {...attributes}>
|
||||||
<img className="d-block" src={src} alt={alt} />
|
<img className="d-block" src={src} alt={alt} />
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|
||||||
LinkedLogo.propTypes = {
|
export const logoDataShape = {
|
||||||
href: PropTypes.string.isRequired,
|
href: PropTypes.string.isRequired,
|
||||||
src: PropTypes.string.isRequired,
|
src: PropTypes.string.isRequired,
|
||||||
alt: PropTypes.string.isRequired,
|
alt: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export { LinkedLogo, Logo };
|
Logo.propTypes = logoDataShape;
|
||||||
|
|
||||||
export default Logo;
|
export default Logo;
|
||||||
|
|||||||
153
src/desktop-header/DesktopHeader.jsx
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
|
||||||
|
// Local Components
|
||||||
|
import { Menu, MenuTrigger, MenuContent } from '../Menu';
|
||||||
|
import Avatar from '../Avatar';
|
||||||
|
import LogoSlot from '../plugin-slots/LogoSlot';
|
||||||
|
import DesktopLoggedOutItemsSlot from '../plugin-slots/DesktopLoggedOutItemsSlot';
|
||||||
|
import { desktopLoggedOutItemsDataShape } from './DesktopLoggedOutItems';
|
||||||
|
import DesktopMainMenuSlot from '../plugin-slots/DesktopMainMenuSlot';
|
||||||
|
import { desktopHeaderMainOrSecondaryMenuDataShape } from './DesktopHeaderMainOrSecondaryMenu';
|
||||||
|
import DesktopSecondaryMenuSlot from '../plugin-slots/DesktopSecondaryMenuSlot';
|
||||||
|
import DesktopUserMenuSlot from '../plugin-slots/DesktopUserMenuSlot';
|
||||||
|
import { desktopUserMenuDataShape } from './DesktopHeaderUserMenu';
|
||||||
|
|
||||||
|
// i18n
|
||||||
|
import messages from '../Header.messages';
|
||||||
|
|
||||||
|
// Assets
|
||||||
|
import { CaretIcon } from '../Icons';
|
||||||
|
|
||||||
|
class DesktopHeader extends React.Component {
|
||||||
|
constructor(props) { // eslint-disable-line no-useless-constructor
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderMainMenu() {
|
||||||
|
const { mainMenu } = this.props;
|
||||||
|
return <DesktopMainMenuSlot menu={mainMenu} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSecondaryMenu() {
|
||||||
|
const { secondaryMenu } = this.props;
|
||||||
|
return <DesktopSecondaryMenuSlot menu={secondaryMenu} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderUserMenu() {
|
||||||
|
const {
|
||||||
|
userMenu,
|
||||||
|
avatar,
|
||||||
|
username,
|
||||||
|
intl,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<Avatar size="1.5em" src={avatar} alt="" className="mr-2" />
|
||||||
|
{username} <CaretIcon role="img" aria-hidden focusable="false" />
|
||||||
|
</MenuTrigger>
|
||||||
|
<MenuContent className="mb-0 dropdown-menu show dropdown-menu-right pin-right shadow py-2">
|
||||||
|
<DesktopUserMenuSlot menu={userMenu} />
|
||||||
|
</MenuContent>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderLoggedOutItems() {
|
||||||
|
const { loggedOutItems } = this.props;
|
||||||
|
return <DesktopLoggedOutItemsSlot items={loggedOutItems} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const desktopHeaderDataShape = {
|
||||||
|
mainMenu: desktopHeaderMainOrSecondaryMenuDataShape,
|
||||||
|
secondaryMenu: desktopHeaderMainOrSecondaryMenuDataShape,
|
||||||
|
userMenu: desktopUserMenuDataShape,
|
||||||
|
loggedOutItems: desktopLoggedOutItemsDataShape,
|
||||||
|
logo: PropTypes.string,
|
||||||
|
logoAltText: PropTypes.string,
|
||||||
|
logoDestination: PropTypes.string,
|
||||||
|
avatar: PropTypes.string,
|
||||||
|
username: PropTypes.string,
|
||||||
|
loggedIn: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
DesktopHeader.propTypes = {
|
||||||
|
mainMenu: desktopHeaderDataShape.mainMenu,
|
||||||
|
secondaryMenu: desktopHeaderDataShape.secondaryMenumainMenu,
|
||||||
|
userMenu: desktopHeaderDataShape.userMenumainMenu,
|
||||||
|
loggedOutItems: desktopHeaderDataShape.loggedOutItemsmainMenu,
|
||||||
|
logo: desktopHeaderDataShape.logomainMenu,
|
||||||
|
logoAltText: desktopHeaderDataShape.logoAltTextmainMenu,
|
||||||
|
logoDestination: desktopHeaderDataShape.logoDestinationmainMenu,
|
||||||
|
avatar: desktopHeaderDataShape.avatarmainMenu,
|
||||||
|
username: desktopHeaderDataShape.usernamemainMenu,
|
||||||
|
loggedIn: desktopHeaderDataShape.loggedInmainMenu,
|
||||||
|
|
||||||
|
// i18n
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
DesktopHeader.defaultProps = {
|
||||||
|
mainMenu: [],
|
||||||
|
secondaryMenu: [],
|
||||||
|
userMenu: [],
|
||||||
|
loggedOutItems: [],
|
||||||
|
logo: null,
|
||||||
|
logoAltText: null,
|
||||||
|
logoDestination: null,
|
||||||
|
avatar: null,
|
||||||
|
username: null,
|
||||||
|
loggedIn: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(DesktopHeader);
|
||||||
59
src/desktop-header/DesktopHeaderMainOrSecondaryMenu.jsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { Menu, MenuTrigger, MenuContent } from '../Menu';
|
||||||
|
import { CaretIcon } from '../Icons';
|
||||||
|
|
||||||
|
const DesktopHeaderMainOrSecondaryMenu = ({ menu }) => {
|
||||||
|
// Nodes are accepted as a prop
|
||||||
|
if (!Array.isArray(menu)) {
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
return menu.map((menuItem) => {
|
||||||
|
const {
|
||||||
|
type,
|
||||||
|
href,
|
||||||
|
content,
|
||||||
|
submenuContent,
|
||||||
|
disabled,
|
||||||
|
isActive,
|
||||||
|
onClick,
|
||||||
|
} = menuItem;
|
||||||
|
|
||||||
|
if (type === 'item') {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
key={`${type}-${content}`}
|
||||||
|
className={`nav-link${disabled ? ' disabled' : ''}${isActive ? ' active' : ''}`}
|
||||||
|
href={href}
|
||||||
|
onClick={onClick || null}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu key={`${type}-${content}`} tag="div" className="nav-item" respondToPointerEvents>
|
||||||
|
<MenuTrigger onClick={onClick || null} tag="a" className="nav-link d-inline-flex align-items-center" href={href}>
|
||||||
|
{content} <CaretIcon role="img" aria-hidden focusable="false" />
|
||||||
|
</MenuTrigger>
|
||||||
|
<MenuContent className="pin-left pin-right shadow py-2">
|
||||||
|
{submenuContent}
|
||||||
|
</MenuContent>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const desktopHeaderMainOrSecondaryMenuDataShape = PropTypes.oneOfType([
|
||||||
|
PropTypes.node,
|
||||||
|
PropTypes.array,
|
||||||
|
]);
|
||||||
|
|
||||||
|
DesktopHeaderMainOrSecondaryMenu.propTypes = {
|
||||||
|
menu: desktopHeaderMainOrSecondaryMenuDataShape,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DesktopHeaderMainOrSecondaryMenu;
|
||||||
39
src/desktop-header/DesktopHeaderUserMenu.jsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const DesktopHeaderUserMenu = ({ menu }) => menu.map((group, index) => (
|
||||||
|
// eslint-disable-next-line react/jsx-no-comment-textnodes,react/no-array-index-key
|
||||||
|
<React.Fragment key={index}>
|
||||||
|
{group.heading && <div className="dropdown-header" role="heading" aria-level="1">{group.heading}</div>}
|
||||||
|
{group.items.map(({
|
||||||
|
type, content, href, disabled, isActive, onClick,
|
||||||
|
}) => (
|
||||||
|
<a
|
||||||
|
className={`dropdown-${type}${isActive ? ' active' : ''}${disabled ? ' disabled' : ''}`}
|
||||||
|
key={`${type}-${content}`}
|
||||||
|
href={href}
|
||||||
|
onClick={onClick || null}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
{index < menu.length - 1 && <div className="dropdown-divider" role="separator" />}
|
||||||
|
</React.Fragment>
|
||||||
|
));
|
||||||
|
|
||||||
|
export const desktopUserMenuDataShape = PropTypes.arrayOf(PropTypes.shape({
|
||||||
|
heading: PropTypes.string,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.shape({
|
||||||
|
type: PropTypes.oneOf(['item', 'menu']),
|
||||||
|
href: PropTypes.string,
|
||||||
|
content: PropTypes.string,
|
||||||
|
isActive: PropTypes.bool,
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
DesktopHeaderUserMenu.propTypes = {
|
||||||
|
menu: desktopUserMenuDataShape,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DesktopHeaderUserMenu;
|
||||||
24
src/desktop-header/DesktopLoggedOutItems.jsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const DesktopLoggedOutItems = ({ items }) => items.map((item, i, arr) => (
|
||||||
|
<a
|
||||||
|
key={`${item.type}-${item.content}`}
|
||||||
|
className={i < arr.length - 1 ? 'btn mr-2 btn-link' : 'btn mr-2 btn-outline-primary'}
|
||||||
|
href={item.href}
|
||||||
|
>
|
||||||
|
{item.content}
|
||||||
|
</a>
|
||||||
|
));
|
||||||
|
|
||||||
|
export const desktopLoggedOutItemsDataShape = PropTypes.arrayOf(PropTypes.shape({
|
||||||
|
type: PropTypes.oneOf(['item', 'menu']),
|
||||||
|
href: PropTypes.string,
|
||||||
|
content: PropTypes.string,
|
||||||
|
}));
|
||||||
|
|
||||||
|
DesktopLoggedOutItems.propTypes = {
|
||||||
|
items: desktopLoggedOutItemsDataShape,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DesktopLoggedOutItems;
|
||||||
@@ -3,27 +3,25 @@ import React from 'react';
|
|||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
|
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import { Button } from '@openedx/paragon';
|
import LearningLoggedOutItemsSlot from '../plugin-slots/LearningLoggedOutItemsSlot';
|
||||||
|
|
||||||
import genericMessages from '../generic/messages';
|
import genericMessages from '../generic/messages';
|
||||||
|
|
||||||
const AnonymousUserMenu = ({ intl }) => (
|
const AnonymousUserMenu = ({ intl }) => {
|
||||||
<div>
|
const buttonsInfo = [
|
||||||
<Button
|
{
|
||||||
className="mr-3"
|
message: intl.formatMessage(genericMessages.registerSentenceCase),
|
||||||
variant="outline-primary"
|
href: `${getConfig().LMS_BASE_URL}/register?next=${encodeURIComponent(global.location.href)}`,
|
||||||
href={`${getConfig().LMS_BASE_URL}/register?next=${encodeURIComponent(global.location.href)}`}
|
},
|
||||||
>
|
{
|
||||||
{intl.formatMessage(genericMessages.registerSentenceCase)}
|
message: intl.formatMessage(genericMessages.signInSentenceCase),
|
||||||
</Button>
|
href: getLoginRedirectUrl(global.location.href),
|
||||||
<Button
|
variant: 'primary',
|
||||||
variant="primary"
|
},
|
||||||
href={`${getLoginRedirectUrl(global.location.href)}`}
|
];
|
||||||
>
|
|
||||||
{intl.formatMessage(genericMessages.signInSentenceCase)}
|
return <LearningLoggedOutItemsSlot buttonsInfo={buttonsInfo} />;
|
||||||
</Button>
|
};
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
AnonymousUserMenu.propTypes = {
|
AnonymousUserMenu.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
|
|||||||
@@ -7,44 +7,46 @@ import { getConfig } from '@edx/frontend-platform';
|
|||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import { Dropdown } from '@openedx/paragon';
|
import { Dropdown } from '@openedx/paragon';
|
||||||
|
|
||||||
|
import LearningUserMenuSlot from '../plugin-slots/LearningUserMenuSlot';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
const AuthenticatedUserDropdown = ({ intl, username }) => {
|
const AuthenticatedUserDropdown = ({ intl, username }) => {
|
||||||
const dashboardMenuItem = (
|
const dropdownItems = [
|
||||||
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/dashboard`}>
|
{
|
||||||
{intl.formatMessage(messages.dashboard)}
|
message: intl.formatMessage(messages.dashboard),
|
||||||
</Dropdown.Item>
|
href: `${getConfig().LMS_BASE_URL}/dashboard`,
|
||||||
);
|
},
|
||||||
|
{
|
||||||
|
message: intl.formatMessage(messages.profile),
|
||||||
|
href: `${getConfig().ACCOUNT_PROFILE_URL}/u/${username}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: intl.formatMessage(messages.account),
|
||||||
|
href: getConfig().ACCOUNT_SETTINGS_URL,
|
||||||
|
},
|
||||||
|
...(getConfig().ORDER_HISTORY_URL ? [{
|
||||||
|
message: intl.formatMessage(messages.orderHistory),
|
||||||
|
href: getConfig().ORDER_HISTORY_URL,
|
||||||
|
}] : []),
|
||||||
|
{
|
||||||
|
message: intl.formatMessage(messages.signOut),
|
||||||
|
href: getConfig().LOGOUT_URL,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Dropdown className="user-dropdown ml-3">
|
||||||
<a className="text-gray-700" href={`${getConfig().SUPPORT_URL}`}>{intl.formatMessage(messages.help)}</a>
|
<Dropdown.Toggle variant="outline-primary">
|
||||||
<Dropdown className="user-dropdown ml-3">
|
<FontAwesomeIcon icon={faUserCircle} className="d-md-none" size="lg" />
|
||||||
<Dropdown.Toggle variant="outline-primary">
|
<span data-hj-suppress className="d-none d-md-inline">
|
||||||
<FontAwesomeIcon icon={faUserCircle} className="d-md-none" size="lg" />
|
{username}
|
||||||
<span data-hj-suppress className="d-none d-md-inline">
|
</span>
|
||||||
{username}
|
</Dropdown.Toggle>
|
||||||
</span>
|
<Dropdown.Menu className="dropdown-menu-right">
|
||||||
</Dropdown.Toggle>
|
<LearningUserMenuSlot items={dropdownItems} />
|
||||||
<Dropdown.Menu className="dropdown-menu-right">
|
</Dropdown.Menu>
|
||||||
{dashboardMenuItem}
|
</Dropdown>
|
||||||
<Dropdown.Item href={`${getConfig().ACCOUNT_PROFILE_URL}/u/${username}`}>
|
|
||||||
{intl.formatMessage(messages.profile)}
|
|
||||||
</Dropdown.Item>
|
|
||||||
<Dropdown.Item href={getConfig().ACCOUNT_SETTINGS_URL}>
|
|
||||||
{intl.formatMessage(messages.account)}
|
|
||||||
</Dropdown.Item>
|
|
||||||
{ getConfig().ORDER_HISTORY_URL && (
|
|
||||||
<Dropdown.Item href={getConfig().ORDER_HISTORY_URL}>
|
|
||||||
{intl.formatMessage(messages.orderHistory)}
|
|
||||||
</Dropdown.Item>
|
|
||||||
)}
|
|
||||||
<Dropdown.Item href={getConfig().LOGOUT_URL}>
|
|
||||||
{intl.formatMessage(messages.signOut)}
|
|
||||||
</Dropdown.Item>
|
|
||||||
</Dropdown.Menu>
|
|
||||||
</Dropdown>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,24 +6,11 @@ import { AppContext } from '@edx/frontend-platform/react';
|
|||||||
|
|
||||||
import AnonymousUserMenu from './AnonymousUserMenu';
|
import AnonymousUserMenu from './AnonymousUserMenu';
|
||||||
import AuthenticatedUserDropdown from './AuthenticatedUserDropdown';
|
import AuthenticatedUserDropdown from './AuthenticatedUserDropdown';
|
||||||
|
import LogoSlot from '../plugin-slots/LogoSlot';
|
||||||
|
import CourseInfoSlot from '../plugin-slots/CourseInfoSlot';
|
||||||
|
import { courseInfoDataShape } from './LearningHeaderCourseInfo';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
import LearningHelpSlot from '../plugin-slots/LearningHelpSlot';
|
||||||
const LinkedLogo = ({
|
|
||||||
href,
|
|
||||||
src,
|
|
||||||
alt,
|
|
||||||
...attributes
|
|
||||||
}) => (
|
|
||||||
<a href={href} {...attributes}>
|
|
||||||
<img className="d-block" src={src} alt={alt} />
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
|
|
||||||
LinkedLogo.propTypes = {
|
|
||||||
href: PropTypes.string.isRequired,
|
|
||||||
src: PropTypes.string.isRequired,
|
|
||||||
alt: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
const LearningHeader = ({
|
const LearningHeader = ({
|
||||||
courseOrg, courseNumber, courseTitle, intl, showUserDropdown,
|
courseOrg, courseNumber, courseTitle, intl, showUserDropdown,
|
||||||
@@ -31,8 +18,7 @@ const LearningHeader = ({
|
|||||||
const { authenticatedUser } = useContext(AppContext);
|
const { authenticatedUser } = useContext(AppContext);
|
||||||
|
|
||||||
const headerLogo = (
|
const headerLogo = (
|
||||||
<LinkedLogo
|
<LogoSlot
|
||||||
className="logo"
|
|
||||||
href={`${getConfig().LMS_BASE_URL}/dashboard`}
|
href={`${getConfig().LMS_BASE_URL}/dashboard`}
|
||||||
src={getConfig().LOGO_URL}
|
src={getConfig().LOGO_URL}
|
||||||
alt={getConfig().SITE_NAME}
|
alt={getConfig().SITE_NAME}
|
||||||
@@ -44,14 +30,16 @@ const LearningHeader = ({
|
|||||||
<a className="sr-only sr-only-focusable" href="#main-content">{intl.formatMessage(messages.skipNavLink)}</a>
|
<a className="sr-only sr-only-focusable" href="#main-content">{intl.formatMessage(messages.skipNavLink)}</a>
|
||||||
<div className="container-xl py-2 d-flex align-items-center">
|
<div className="container-xl py-2 d-flex align-items-center">
|
||||||
{headerLogo}
|
{headerLogo}
|
||||||
<div className="flex-grow-1 course-title-lockup" style={{ lineHeight: 1 }}>
|
<div className="flex-grow-1 course-title-lockup d-flex" style={{ lineHeight: 1 }}>
|
||||||
<span className="d-block small m-0">{courseOrg} {courseNumber}</span>
|
<CourseInfoSlot courseOrg={courseOrg} courseNumber={courseNumber} courseTitle={courseTitle} />
|
||||||
<span className="d-block m-0 font-weight-bold course-title">{courseTitle}</span>
|
|
||||||
</div>
|
</div>
|
||||||
{showUserDropdown && authenticatedUser && (
|
{showUserDropdown && authenticatedUser && (
|
||||||
<AuthenticatedUserDropdown
|
<>
|
||||||
username={authenticatedUser.username}
|
<LearningHelpSlot />
|
||||||
/>
|
<AuthenticatedUserDropdown
|
||||||
|
username={authenticatedUser.username}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{showUserDropdown && !authenticatedUser && (
|
{showUserDropdown && !authenticatedUser && (
|
||||||
<AnonymousUserMenu />
|
<AnonymousUserMenu />
|
||||||
@@ -62,9 +50,9 @@ const LearningHeader = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
LearningHeader.propTypes = {
|
LearningHeader.propTypes = {
|
||||||
courseOrg: PropTypes.string,
|
courseOrg: courseInfoDataShape.courseOrg,
|
||||||
courseNumber: PropTypes.string,
|
courseNumber: courseInfoDataShape.courseNumber,
|
||||||
courseTitle: PropTypes.string,
|
courseTitle: courseInfoDataShape.courseTitle,
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
showUserDropdown: PropTypes.bool,
|
showUserDropdown: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|||||||
23
src/learning-header/LearningHeaderCourseInfo.jsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const LearningHeaderCourseInfo = ({
|
||||||
|
courseOrg,
|
||||||
|
courseNumber,
|
||||||
|
courseTitle,
|
||||||
|
}) => (
|
||||||
|
<div style={{ minWidth: 0 }}>
|
||||||
|
<span className="d-block small m-0">{courseOrg} {courseNumber}</span>
|
||||||
|
<span className="d-block m-0 font-weight-bold course-title">{courseTitle}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const courseInfoDataShape = {
|
||||||
|
courseOrg: PropTypes.string,
|
||||||
|
courseNumber: PropTypes.string,
|
||||||
|
courseTitle: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
LearningHeaderCourseInfo.propTypes = courseInfoDataShape;
|
||||||
|
|
||||||
|
export default LearningHeaderCourseInfo;
|
||||||
14
src/learning-header/LearningHeaderHelpLink.jsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
const LearningHeaderHelpLink = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
return (
|
||||||
|
<a className="text-gray-700" href={`${getConfig().SUPPORT_URL}`}>{intl.formatMessage(messages.help)}</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LearningHeaderHelpLink;
|
||||||
21
src/learning-header/LearningHeaderUserMenuItems.jsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { Dropdown } from '@openedx/paragon';
|
||||||
|
|
||||||
|
const LearningHeaderUserMenuItems = ({ items }) => items.map((item) => (
|
||||||
|
<Dropdown.Item href={item.href}>
|
||||||
|
{item.message}
|
||||||
|
</Dropdown.Item>
|
||||||
|
));
|
||||||
|
|
||||||
|
export const learningHeaderUserMenuDataShape = {
|
||||||
|
items: PropTypes.arrayOf(PropTypes.shape({
|
||||||
|
message: PropTypes.string,
|
||||||
|
href: PropTypes.string,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
LearningHeaderUserMenuItems.propTypes = learningHeaderUserMenuDataShape;
|
||||||
|
|
||||||
|
export default LearningHeaderUserMenuItems;
|
||||||
26
src/learning-header/LearningLoggedOutButtons.jsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { Button } from '@openedx/paragon';
|
||||||
|
|
||||||
|
const LearningLoggedOutButtons = ({ buttonsInfo }) => buttonsInfo.map(buttonInfo => (
|
||||||
|
<Button
|
||||||
|
className="ml-3"
|
||||||
|
variant={buttonInfo.variant ?? 'outline-primary'}
|
||||||
|
href={buttonInfo.href}
|
||||||
|
>
|
||||||
|
{buttonInfo.message}
|
||||||
|
</Button>
|
||||||
|
));
|
||||||
|
|
||||||
|
export const learningHeaderLoggedOutItemsDataShape = {
|
||||||
|
buttonsInfo: PropTypes.arrayOf(PropTypes.shape({
|
||||||
|
message: PropTypes.string,
|
||||||
|
href: PropTypes.string,
|
||||||
|
variant: PropTypes.string,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
LearningLoggedOutButtons.propTypes = learningHeaderLoggedOutItemsDataShape;
|
||||||
|
|
||||||
|
export default LearningLoggedOutButtons;
|
||||||
@@ -4,107 +4,40 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
|||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
|
||||||
// Local Components
|
// Local Components
|
||||||
import { Menu, MenuTrigger, MenuContent } from './Menu';
|
import { Menu, MenuTrigger, MenuContent } from '../Menu';
|
||||||
import Avatar from './Avatar';
|
import Avatar from '../Avatar';
|
||||||
import { LinkedLogo, Logo } from './Logo';
|
import LogoSlot from '../plugin-slots/LogoSlot';
|
||||||
|
import MobileLoggedOutItemsSlot from '../plugin-slots/MobileLoggedOutItemsSlot';
|
||||||
|
import { mobileHeaderLoggedOutItemsDataShape } from './MobileLoggedOutItems';
|
||||||
|
import MobileMainMenuSlot from '../plugin-slots/MobileMainMenuSlot';
|
||||||
|
import { mobileHeaderMainMenuDataShape } from './MobileHeaderMainMenu';
|
||||||
|
import MobileUserMenuSlot from '../plugin-slots/MobileUserMenuSlot';
|
||||||
|
import { mobileHeaderUserMenuDataShape } from './MobileHeaderUserMenu';
|
||||||
|
|
||||||
// i18n
|
// i18n
|
||||||
import messages from './Header.messages';
|
import messages from '../Header.messages';
|
||||||
|
|
||||||
// Assets
|
// Assets
|
||||||
import { MenuIcon } from './Icons';
|
import { MenuIcon } from '../Icons';
|
||||||
|
|
||||||
class MobileHeader extends React.Component {
|
class MobileHeader extends React.Component {
|
||||||
constructor(props) { // eslint-disable-line no-useless-constructor
|
constructor(props) { // eslint-disable-line no-useless-constructor
|
||||||
super(props);
|
super(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderMenu(menu) {
|
|
||||||
// Nodes are accepted as a prop
|
|
||||||
if (!Array.isArray(menu)) {
|
|
||||||
return menu;
|
|
||||||
}
|
|
||||||
|
|
||||||
return menu.map((menuItem) => {
|
|
||||||
const {
|
|
||||||
type,
|
|
||||||
href,
|
|
||||||
content,
|
|
||||||
submenuContent,
|
|
||||||
disabled,
|
|
||||||
isActive,
|
|
||||||
onClick,
|
|
||||||
} = menuItem;
|
|
||||||
|
|
||||||
if (type === 'item') {
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
key={`${type}-${content}`}
|
|
||||||
className={`nav-link${disabled ? ' disabled' : ''}${isActive ? ' active' : ''}`}
|
|
||||||
href={href}
|
|
||||||
onClick={onClick || null}
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Menu key={`${type}-${content}`} tag="div" className="nav-item">
|
|
||||||
<MenuTrigger onClick={onClick || null} tag="a" role="button" tabIndex="0" className="nav-link">
|
|
||||||
{content}
|
|
||||||
</MenuTrigger>
|
|
||||||
<MenuContent className="position-static pin-left pin-right py-2">
|
|
||||||
{submenuContent}
|
|
||||||
</MenuContent>
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
renderMainMenu() {
|
renderMainMenu() {
|
||||||
const { mainMenu } = this.props;
|
const { mainMenu, secondaryMenu } = this.props;
|
||||||
return this.renderMenu(mainMenu);
|
return <MobileMainMenuSlot menu={[...mainMenu, ...secondaryMenu]} />;
|
||||||
}
|
|
||||||
|
|
||||||
renderSecondaryMenu() {
|
|
||||||
const { secondaryMenu } = this.props;
|
|
||||||
return this.renderMenu(secondaryMenu);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderUserMenuItems() {
|
renderUserMenuItems() {
|
||||||
const { userMenu } = this.props;
|
const { userMenu } = this.props;
|
||||||
|
return <MobileUserMenuSlot menu={userMenu} />;
|
||||||
return userMenu.map((group) => (
|
|
||||||
group.items.map(({
|
|
||||||
type, content, href, disabled, isActive, onClick,
|
|
||||||
}) => (
|
|
||||||
<li className="nav-item" key={`${type}-${content}`}>
|
|
||||||
<a
|
|
||||||
className={`nav-link${isActive ? ' active' : ''}${disabled ? ' disabled' : ''}`}
|
|
||||||
href={href}
|
|
||||||
onClick={onClick || null}
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
))
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderLoggedOutItems() {
|
renderLoggedOutItems() {
|
||||||
const { loggedOutItems } = this.props;
|
const { loggedOutItems } = this.props;
|
||||||
|
return <MobileLoggedOutItemsSlot items={loggedOutItems} />;
|
||||||
return loggedOutItems.map(({ type, href, content }, i, arr) => (
|
|
||||||
<li className="nav-item px-3 my-2" key={`${type}-${content}`}>
|
|
||||||
<a
|
|
||||||
className={i < arr.length - 1 ? 'btn btn-block btn-outline-primary' : 'btn btn-block btn-primary'}
|
|
||||||
href={href}
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -149,13 +82,12 @@ class MobileHeader extends React.Component {
|
|||||||
className="nav flex-column pin-left pin-right border-top shadow py-2"
|
className="nav flex-column pin-left pin-right border-top shadow py-2"
|
||||||
>
|
>
|
||||||
{this.renderMainMenu()}
|
{this.renderMainMenu()}
|
||||||
{this.renderSecondaryMenu()}
|
|
||||||
</MenuContent>
|
</MenuContent>
|
||||||
</Menu>
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className={`w-100 d-flex ${logoClasses}`}>
|
<div className={`w-100 d-flex ${logoClasses}`}>
|
||||||
{ logoDestination === null ? <Logo className="logo" src={logo} alt={logoAltText} /> : <LinkedLogo className="logo" {...logoProps} itemType="http://schema.org/Organization" />}
|
<LogoSlot {...logoProps} itemType="http://schema.org/Organization" />
|
||||||
</div>
|
</div>
|
||||||
{userMenu.length > 0 || loggedOutItems.length > 0 ? (
|
{userMenu.length > 0 || loggedOutItems.length > 0 ? (
|
||||||
<div className="w-100 d-flex justify-content-end align-items-center">
|
<div className="w-100 d-flex justify-content-end align-items-center">
|
||||||
@@ -179,30 +111,11 @@ class MobileHeader extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MobileHeader.propTypes = {
|
export const mobileHeaderDataShape = {
|
||||||
mainMenu: PropTypes.oneOfType([
|
mainMenu: mobileHeaderMainMenuDataShape,
|
||||||
PropTypes.node,
|
secondaryMenu: mobileHeaderMainMenuDataShape,
|
||||||
PropTypes.array,
|
userMenu: mobileHeaderUserMenuDataShape,
|
||||||
]),
|
loggedOutItems: mobileHeaderLoggedOutItemsDataShape,
|
||||||
secondaryMenu: PropTypes.oneOfType([
|
|
||||||
PropTypes.node,
|
|
||||||
PropTypes.array,
|
|
||||||
]),
|
|
||||||
userMenu: PropTypes.arrayOf(PropTypes.shape({
|
|
||||||
heading: PropTypes.string,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.shape({
|
|
||||||
type: PropTypes.oneOf(['item', 'menu']),
|
|
||||||
href: PropTypes.string,
|
|
||||||
content: PropTypes.string,
|
|
||||||
isActive: PropTypes.bool,
|
|
||||||
onClick: PropTypes.func,
|
|
||||||
})),
|
|
||||||
})),
|
|
||||||
loggedOutItems: PropTypes.arrayOf(PropTypes.shape({
|
|
||||||
type: PropTypes.oneOf(['item', 'menu']),
|
|
||||||
href: PropTypes.string,
|
|
||||||
content: PropTypes.string,
|
|
||||||
})),
|
|
||||||
logo: PropTypes.string,
|
logo: PropTypes.string,
|
||||||
logoAltText: PropTypes.string,
|
logoAltText: PropTypes.string,
|
||||||
logoDestination: PropTypes.string,
|
logoDestination: PropTypes.string,
|
||||||
@@ -210,6 +123,20 @@ MobileHeader.propTypes = {
|
|||||||
username: PropTypes.string,
|
username: PropTypes.string,
|
||||||
loggedIn: PropTypes.bool,
|
loggedIn: PropTypes.bool,
|
||||||
stickyOnMobile: PropTypes.bool,
|
stickyOnMobile: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
MobileHeader.propTypes = {
|
||||||
|
mainMenu: mobileHeaderDataShape.mainMenu,
|
||||||
|
secondaryMenu: mobileHeaderDataShape.secondaryMenu,
|
||||||
|
userMenu: mobileHeaderDataShape.userMenu,
|
||||||
|
loggedOutItems: mobileHeaderDataShape.loggedOutItems,
|
||||||
|
logo: mobileHeaderDataShape.logo,
|
||||||
|
logoAltText: mobileHeaderDataShape.logoAltText,
|
||||||
|
logoDestination: mobileHeaderDataShape.logoDestination,
|
||||||
|
avatar: mobileHeaderDataShape.avatar,
|
||||||
|
username: mobileHeaderDataShape.username,
|
||||||
|
loggedIn: mobileHeaderDataShape.loggedIn,
|
||||||
|
stickyOnMobile: mobileHeaderDataShape.stickyOnMobile,
|
||||||
|
|
||||||
// i18n
|
// i18n
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
58
src/mobile-header/MobileHeaderMainMenu.jsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { Menu, MenuTrigger, MenuContent } from '../Menu';
|
||||||
|
|
||||||
|
const MobileHeaderMainMenu = ({ menu }) => {
|
||||||
|
// Nodes are accepted as a prop
|
||||||
|
if (!Array.isArray(menu)) {
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
return menu.map((menuItem) => {
|
||||||
|
const {
|
||||||
|
type,
|
||||||
|
href,
|
||||||
|
content,
|
||||||
|
submenuContent,
|
||||||
|
disabled,
|
||||||
|
isActive,
|
||||||
|
onClick,
|
||||||
|
} = menuItem;
|
||||||
|
|
||||||
|
if (type === 'item') {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
key={`${type}-${content}`}
|
||||||
|
className={`nav-link${disabled ? ' disabled' : ''}${isActive ? ' active' : ''}`}
|
||||||
|
href={href}
|
||||||
|
onClick={onClick || null}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu key={`${type}-${content}`} tag="div" className="nav-item">
|
||||||
|
<MenuTrigger onClick={onClick || null} tag="a" role="button" tabIndex="0" className="nav-link">
|
||||||
|
{content}
|
||||||
|
</MenuTrigger>
|
||||||
|
<MenuContent className="position-static pin-left pin-right py-2">
|
||||||
|
{submenuContent}
|
||||||
|
</MenuContent>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mobileHeaderMainMenuDataShape = PropTypes.oneOfType([
|
||||||
|
PropTypes.node,
|
||||||
|
PropTypes.array,
|
||||||
|
]);
|
||||||
|
|
||||||
|
MobileHeaderMainMenu.propTypes = {
|
||||||
|
menu: mobileHeaderMainMenuDataShape,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MobileHeaderMainMenu;
|
||||||
35
src/mobile-header/MobileHeaderUserMenu.jsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const MobileHeaderUserMenu = ({ menu }) => menu.map((group) => (
|
||||||
|
group.items.map(({
|
||||||
|
type, content, href, disabled, isActive, onClick,
|
||||||
|
}) => (
|
||||||
|
<li className="nav-item" key={`${type}-${content}`}>
|
||||||
|
<a
|
||||||
|
className={`nav-link${isActive ? ' active' : ''}${disabled ? ' disabled' : ''}`}
|
||||||
|
href={href}
|
||||||
|
onClick={onClick || null}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
));
|
||||||
|
|
||||||
|
export const mobileHeaderUserMenuDataShape = PropTypes.arrayOf(PropTypes.shape({
|
||||||
|
heading: PropTypes.string,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.shape({
|
||||||
|
type: PropTypes.oneOf(['item', 'menu']),
|
||||||
|
href: PropTypes.string,
|
||||||
|
content: PropTypes.string,
|
||||||
|
isActive: PropTypes.bool,
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
MobileHeaderUserMenu.propTypes = {
|
||||||
|
menu: mobileHeaderUserMenuDataShape,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MobileHeaderUserMenu;
|
||||||
25
src/mobile-header/MobileLoggedOutItems.jsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const MobileLoggedOutItems = ({ items }) => items.map(({ type, href, content }, i, arr) => (
|
||||||
|
<li className="nav-item px-3 my-2" key={`${type}-${content}`}>
|
||||||
|
<a
|
||||||
|
className={i < arr.length - 1 ? 'btn btn-block btn-outline-primary' : 'btn btn-block btn-primary'}
|
||||||
|
href={href}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
));
|
||||||
|
|
||||||
|
export const mobileHeaderLoggedOutItemsDataShape = PropTypes.arrayOf(PropTypes.shape({
|
||||||
|
type: PropTypes.oneOf(['item', 'menu']),
|
||||||
|
href: PropTypes.string,
|
||||||
|
content: PropTypes.string,
|
||||||
|
}));
|
||||||
|
|
||||||
|
MobileLoggedOutItems.propTypes = {
|
||||||
|
menu: mobileHeaderLoggedOutItemsDataShape,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MobileLoggedOutItems;
|
||||||
125
src/plugin-slots/CourseInfoSlot/README.md
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
# Course Info Slot
|
||||||
|
|
||||||
|
### Slot ID: `course_info_slot`
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
This slot is used to replace/modify/hide the course info.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Replace Course Title
|
||||||
|
|
||||||
|
The following `env.config.jsx` will replace the course title.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
|
const replaceCourseTitle = ( widget ) => {
|
||||||
|
widget.content.courseTitle = "Custom Course Title";
|
||||||
|
return widget;
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
course_info_slot: {
|
||||||
|
keepDefault: true,
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Modify,
|
||||||
|
widgetId: 'default_contents',
|
||||||
|
fn: replaceCourseTitle,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Replace Course Info with Custom Component
|
||||||
|
|
||||||
|
The following `env.config.jsx` will replace the course info entirely (in this case with a centered 🗺️ `h1`)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
course_info_slot: {
|
||||||
|
keepDefault: false,
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Insert,
|
||||||
|
widget: {
|
||||||
|
id: 'custom_course_info_component',
|
||||||
|
type: DIRECT_PLUGIN,
|
||||||
|
RenderWidget: () => (
|
||||||
|
<h1 style={{textAlign: 'center'}}>🗺️</h1>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add Custom Components before and after Course Info
|
||||||
|
|
||||||
|
The following `env.config.jsx` will place custom components before and after the course info (in this case centered `h1`s with 🌜 and 🌛).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
course_info_slot: {
|
||||||
|
keepDefault: true,
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Insert,
|
||||||
|
widget: {
|
||||||
|
id: 'custom_before_course_info_component',
|
||||||
|
type: DIRECT_PLUGIN,
|
||||||
|
priority: 10,
|
||||||
|
RenderWidget: () => (
|
||||||
|
<h3 style={{
|
||||||
|
marginTop: 'auto',
|
||||||
|
marginBottom: 'auto',
|
||||||
|
marginRight: '0.5rem',
|
||||||
|
}}>🌜</h3>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Insert,
|
||||||
|
widget: {
|
||||||
|
id: 'custom_after_course_info_component',
|
||||||
|
type: DIRECT_PLUGIN,
|
||||||
|
priority: 90,
|
||||||
|
RenderWidget: () => (
|
||||||
|
<h3 style={{
|
||||||
|
marginTop: 'auto',
|
||||||
|
marginBottom: 'auto',
|
||||||
|
marginLeft: '0.5rem',
|
||||||
|
}}>🌛</h3>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 13 KiB |
BIN
src/plugin-slots/CourseInfoSlot/images/replace_course_title.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
28
src/plugin-slots/CourseInfoSlot/index.jsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||||
|
import LearningHeaderCourseInfo, { courseInfoDataShape } from '../../learning-header/LearningHeaderCourseInfo';
|
||||||
|
|
||||||
|
const CourseInfoSlot = ({
|
||||||
|
courseOrg,
|
||||||
|
courseNumber,
|
||||||
|
courseTitle,
|
||||||
|
...attributes
|
||||||
|
}) => (
|
||||||
|
<PluginSlot
|
||||||
|
id="course_info_slot"
|
||||||
|
slotOptions={{
|
||||||
|
mergeProps: true,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LearningHeaderCourseInfo
|
||||||
|
courseOrg={courseOrg}
|
||||||
|
courseNumber={courseNumber}
|
||||||
|
courseTitle={courseTitle}
|
||||||
|
{...attributes}
|
||||||
|
/>
|
||||||
|
</PluginSlot>
|
||||||
|
);
|
||||||
|
|
||||||
|
CourseInfoSlot.propTypes = courseInfoDataShape;
|
||||||
|
|
||||||
|
export default CourseInfoSlot;
|
||||||
41
src/plugin-slots/DesktopHeaderSlot/README.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Desktop Header Slot
|
||||||
|
|
||||||
|
### Slot ID: `desktop_header_slot`
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
This slot is used to replace/modify/hide the entire desktop header.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Custom Component
|
||||||
|
|
||||||
|
The following `env.config.jsx` will replace the desktop header entirely (in this case with a centered 🗺️ `h1`)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
desktop_header_slot: {
|
||||||
|
keepDefault: false,
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Insert,
|
||||||
|
widget: {
|
||||||
|
id: 'custom_desktop_header_component',
|
||||||
|
type: DIRECT_PLUGIN,
|
||||||
|
RenderWidget: () => (
|
||||||
|
<h1 style={{textAlign: 'center'}}>🗺️</h1>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
After Width: | Height: | Size: 27 KiB |
20
src/plugin-slots/DesktopHeaderSlot/index.jsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||||
|
import DesktopHeader, { desktopHeaderDataShape } from '../../desktop-header/DesktopHeader';
|
||||||
|
|
||||||
|
const DesktopHeaderSlot = ({
|
||||||
|
props,
|
||||||
|
}) => (
|
||||||
|
<PluginSlot
|
||||||
|
id="desktop_header_slot"
|
||||||
|
slotOptions={{
|
||||||
|
mergeProps: true,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DesktopHeader {...props} />
|
||||||
|
</PluginSlot>
|
||||||
|
);
|
||||||
|
|
||||||
|
DesktopHeaderSlot.propTypes = desktopHeaderDataShape;
|
||||||
|
|
||||||
|
export default DesktopHeaderSlot;
|
||||||
134
src/plugin-slots/DesktopLoggedOutItemsSlot/README.md
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
# Desktop Logged Out Items Slot
|
||||||
|
|
||||||
|
### Slot ID: `desktop_logged_out_items_slot`
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
This slot is used to replace/modify/hide the items shown on desktop when the user is logged out.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Modify Items
|
||||||
|
|
||||||
|
The following `env.config.jsx` will modify the items shown on desktop when the user is logged out.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
|
const modifyLoggedOutItems = ( widget ) => {
|
||||||
|
widget.content.items = [
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
href: 'https://openedx.org/',
|
||||||
|
content: 'openedx.org',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
href: 'https://docs.openedx.org/en/latest/',
|
||||||
|
content: 'Documentation',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
href: 'https://discuss.openedx.org/',
|
||||||
|
content: 'Forums',
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return widget;
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
desktop_logged_out_items_slot: {
|
||||||
|
keepDefault: true,
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Modify,
|
||||||
|
widgetId: 'default_contents',
|
||||||
|
fn: modifyLoggedOutItems,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Replace with Custom Component
|
||||||
|
|
||||||
|
The following `env.config.jsx` will replace the items shown on desktop when the user is logged out entirely (in this case with a centered 🗺️ `h1`)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
desktop_logged_out_items_slot: {
|
||||||
|
keepDefault: false,
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Insert,
|
||||||
|
widget: {
|
||||||
|
id: 'custom_logged_out_items_component',
|
||||||
|
type: DIRECT_PLUGIN,
|
||||||
|
RenderWidget: () => (
|
||||||
|
<h1 style={{textAlign: 'center'}}>🗺️</h1>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add Custom Components before and after
|
||||||
|
|
||||||
|
The following `env.config.jsx` will place custom components before and after the items shown on desktop when the user is logged out (in this case centered `h1`s with 🌜 and 🌛).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
desktop_logged_out_items_slot: {
|
||||||
|
keepDefault: true,
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Insert,
|
||||||
|
widget: {
|
||||||
|
id: 'custom_before_logged_out_items_component',
|
||||||
|
type: DIRECT_PLUGIN,
|
||||||
|
priority: 10,
|
||||||
|
RenderWidget: () => (
|
||||||
|
<h1 style={{textAlign: 'center'}}>🌜</h1>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Insert,
|
||||||
|
widget: {
|
||||||
|
id: 'custom_after_logged_out_items_component',
|
||||||
|
type: DIRECT_PLUGIN,
|
||||||
|
priority: 90,
|
||||||
|
RenderWidget: () => (
|
||||||
|
<h1 style={{textAlign: 'center'}}>🌛</h1>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 13 KiB |
22
src/plugin-slots/DesktopLoggedOutItemsSlot/index.jsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||||
|
import DesktopLoggedOutItems, { desktopLoggedOutItemsDataShape } from '../../desktop-header/DesktopLoggedOutItems';
|
||||||
|
|
||||||
|
const DesktopLoggedOutItemsSlot = ({
|
||||||
|
items,
|
||||||
|
}) => (
|
||||||
|
<PluginSlot
|
||||||
|
id="desktop_logged_out_items_slot"
|
||||||
|
slotOptions={{
|
||||||
|
mergeProps: true,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DesktopLoggedOutItems items={items} />
|
||||||
|
</PluginSlot>
|
||||||
|
);
|
||||||
|
|
||||||
|
DesktopLoggedOutItemsSlot.propTypes = {
|
||||||
|
items: desktopLoggedOutItemsDataShape,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DesktopLoggedOutItemsSlot;
|
||||||
134
src/plugin-slots/DesktopMainMenuSlot/README.md
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
# Desktop Main Menu Slot
|
||||||
|
|
||||||
|
### Slot ID: `desktop_main_menu_slot`
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
This slot is used to replace/modify/hide the desktop main menu.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Modify Items
|
||||||
|
|
||||||
|
The following `env.config.jsx` will modify the items in the desktop main menu.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
|
const modifyMainMenu = ( widget ) => {
|
||||||
|
widget.content.menu = [
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
href: 'https://openedx.org/',
|
||||||
|
content: 'openedx.org',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
href: 'https://docs.openedx.org/en/latest/',
|
||||||
|
content: 'Documentation',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
href: 'https://discuss.openedx.org/',
|
||||||
|
content: 'Forums',
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return widget;
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
desktop_main_menu_slot: {
|
||||||
|
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`)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
desktop_main_menu_slot: {
|
||||||
|
keepDefault: false,
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Insert,
|
||||||
|
widget: {
|
||||||
|
id: 'custom_main_menu_component',
|
||||||
|
type: DIRECT_PLUGIN,
|
||||||
|
RenderWidget: () => (
|
||||||
|
<h1 style={{textAlign: 'center'}}>🗺️</h1>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add Custom Components before and after Menu
|
||||||
|
|
||||||
|
The following `env.config.jsx` will place custom components before and after the desktop main menu (in this case centered `h1`s with 🌜 and 🌛).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
desktop_main_menu_slot: {
|
||||||
|
keepDefault: true,
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Insert,
|
||||||
|
widget: {
|
||||||
|
id: 'custom_before_main_menu_component',
|
||||||
|
type: DIRECT_PLUGIN,
|
||||||
|
priority: 10,
|
||||||
|
RenderWidget: () => (
|
||||||
|
<h1 style={{textAlign: 'center'}}>🌜</h1>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Insert,
|
||||||
|
widget: {
|
||||||
|
id: 'custom_after_main_menu_component',
|
||||||
|
type: DIRECT_PLUGIN,
|
||||||
|
priority: 90,
|
||||||
|
RenderWidget: () => (
|
||||||
|
<h1 style={{textAlign: 'center'}}>🌛</h1>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 10 KiB |
22
src/plugin-slots/DesktopMainMenuSlot/index.jsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||||
|
import DesktopHeaderMainOrSecondaryMenu, { desktopHeaderMainOrSecondaryMenuDataShape } from '../../desktop-header/DesktopHeaderMainOrSecondaryMenu';
|
||||||
|
|
||||||
|
const DesktopMainMenuSlot = ({
|
||||||
|
menu,
|
||||||
|
}) => (
|
||||||
|
<PluginSlot
|
||||||
|
id="desktop_main_menu_slot"
|
||||||
|
slotOptions={{
|
||||||
|
mergeProps: true,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DesktopHeaderMainOrSecondaryMenu menu={menu} />
|
||||||
|
</PluginSlot>
|
||||||
|
);
|
||||||
|
|
||||||
|
DesktopMainMenuSlot.propTypes = {
|
||||||
|
menu: desktopHeaderMainOrSecondaryMenuDataShape,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DesktopMainMenuSlot;
|
||||||
129
src/plugin-slots/DesktopSecondaryMenuSlot/README.md
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
# Desktop Secondary Menu Slot
|
||||||
|
|
||||||
|
### Slot ID: `desktop_secondary_menu_slot`
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
This slot is used to replace/modify/hide the desktop secondary menu.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Modify Items
|
||||||
|
|
||||||
|
The following `env.config.jsx` will modify the items in the desktop secondary menu.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
|
const modifySecondaryMenu = ( widget ) => {
|
||||||
|
widget.content.menu = [
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
href: 'https://www.youtube.com/c/openedx',
|
||||||
|
content: 'Open edX on YouTube',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
href: 'https://github.com/openedx/',
|
||||||
|
content: 'Open edX on GitHub',
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return widget;
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
desktop_secondary_menu_slot: {
|
||||||
|
keepDefault: true,
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Modify,
|
||||||
|
widgetId: 'default_contents',
|
||||||
|
fn: modifySecondaryMenu,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Replace Menu with Custom Component
|
||||||
|
|
||||||
|
The following `env.config.jsx` will replace the desktop secondary menu entirely (in this case with a centered 🗺️ `h1`)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
desktop_secondary_menu_slot: {
|
||||||
|
keepDefault: false,
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Insert,
|
||||||
|
widget: {
|
||||||
|
id: 'custom_secondary_menu_component',
|
||||||
|
type: DIRECT_PLUGIN,
|
||||||
|
RenderWidget: () => (
|
||||||
|
<h1 style={{textAlign: 'center'}}>🗺️</h1>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add Custom Components before and after Menu
|
||||||
|
|
||||||
|
The following `env.config.jsx` will place custom components before and after the desktop secondary menu (in this case centered `h1`s with 🌜 and 🌛).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
desktop_secondary_menu_slot: {
|
||||||
|
keepDefault: true,
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Insert,
|
||||||
|
widget: {
|
||||||
|
id: 'custom_before_secondary_menu_component',
|
||||||
|
type: DIRECT_PLUGIN,
|
||||||
|
priority: 10,
|
||||||
|
RenderWidget: () => (
|
||||||
|
<h1 style={{textAlign: 'center'}}>🌜</h1>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Insert,
|
||||||
|
widget: {
|
||||||
|
id: 'custom_after_secondary_menu_component',
|
||||||
|
type: DIRECT_PLUGIN,
|
||||||
|
priority: 90,
|
||||||
|
RenderWidget: () => (
|
||||||
|
<h1 style={{textAlign: 'center'}}>🌛</h1>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 16 KiB |
22
src/plugin-slots/DesktopSecondaryMenuSlot/index.jsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||||
|
import DesktopHeaderMainOrSecondaryMenu, { desktopHeaderMainOrSecondaryMenuDataShape } from '../../desktop-header/DesktopHeaderMainOrSecondaryMenu';
|
||||||
|
|
||||||
|
const DesktopSecondaryMenuSlot = ({
|
||||||
|
menu,
|
||||||
|
}) => (
|
||||||
|
<PluginSlot
|
||||||
|
id="desktop_secondary_menu_slot"
|
||||||
|
slotOptions={{
|
||||||
|
mergeProps: true,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DesktopHeaderMainOrSecondaryMenu menu={menu} />
|
||||||
|
</PluginSlot>
|
||||||
|
);
|
||||||
|
|
||||||
|
DesktopSecondaryMenuSlot.propTypes = {
|
||||||
|
menu: desktopHeaderMainOrSecondaryMenuDataShape,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DesktopSecondaryMenuSlot;
|
||||||
141
src/plugin-slots/DesktopUserMenuSlot/README.md
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
# Desktop User Menu Slot
|
||||||
|
|
||||||
|
### Slot ID: `desktop_user_menu_slot`
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
This slot is used to replace/modify/hide the desktop user menu.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Modify Items
|
||||||
|
|
||||||
|
The following `env.config.jsx` will modify the items in the desktop user menu.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
|
const modifyUserMenu = ( widget ) => {
|
||||||
|
widget.content.menu = [
|
||||||
|
{
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
href: 'https://openedx.org/',
|
||||||
|
content: 'openedx.org',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
href: 'https://docs.openedx.org/en/latest/',
|
||||||
|
content: 'Documentation',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
href: 'https://discuss.openedx.org/',
|
||||||
|
content: 'Forums',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return widget;
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
desktop_user_menu_slot: {
|
||||||
|
keepDefault: true,
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Modify,
|
||||||
|
widgetId: 'default_contents',
|
||||||
|
fn: modifyUserMenu,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Replace Menu with Custom Component
|
||||||
|
|
||||||
|
The following `env.config.jsx` will replace the desktop user menu entirely (in this case with a centered 🗺️ `h1`)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
desktop_user_menu_slot: {
|
||||||
|
keepDefault: false,
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Insert,
|
||||||
|
widget: {
|
||||||
|
id: 'custom_user_menu_component',
|
||||||
|
type: DIRECT_PLUGIN,
|
||||||
|
RenderWidget: () => (
|
||||||
|
<h1 style={{textAlign: 'center'}}>🗺️</h1>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add Custom Components before and after Menu
|
||||||
|
|
||||||
|
The following `env.config.jsx` will place custom components before and after the desktop user menu (in this case centered `h1`s with 🌞 and 🌚).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
desktop_user_menu_slot: {
|
||||||
|
keepDefault: true,
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Insert,
|
||||||
|
widget: {
|
||||||
|
id: 'custom_before_user_menu_component',
|
||||||
|
type: DIRECT_PLUGIN,
|
||||||
|
priority: 10,
|
||||||
|
RenderWidget: () => (
|
||||||
|
<h1 style={{textAlign: 'center'}}>🌞</h1>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Insert,
|
||||||
|
widget: {
|
||||||
|
id: 'custom_after_user_menu_component',
|
||||||
|
type: DIRECT_PLUGIN,
|
||||||
|
priority: 90,
|
||||||
|
RenderWidget: () => (
|
||||||
|
<h1 style={{textAlign: 'center'}}>🌚</h1>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 11 KiB |
22
src/plugin-slots/DesktopUserMenuSlot/index.jsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||||
|
import DesktopHeaderUserMenu, { desktopUserMenuDataShape } from '../../desktop-header/DesktopHeaderUserMenu';
|
||||||
|
|
||||||
|
const DesktopUserMenuSlot = ({
|
||||||
|
menu,
|
||||||
|
}) => (
|
||||||
|
<PluginSlot
|
||||||
|
id="desktop_user_menu_slot"
|
||||||
|
slotOptions={{
|
||||||
|
mergeProps: true,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DesktopHeaderUserMenu menu={menu} />
|
||||||
|
</PluginSlot>
|
||||||
|
);
|
||||||
|
|
||||||
|
DesktopUserMenuSlot.propTypes = {
|
||||||
|
menu: desktopUserMenuDataShape,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DesktopUserMenuSlot;
|
||||||
41
src/plugin-slots/LearningHelpSlot/README.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Learning Help Slot
|
||||||
|
|
||||||
|
### Slot ID: `learning_help_slot`
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
This slot is used to replace/modify/hide the learning help link.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Custom Component
|
||||||
|
|
||||||
|
The following `env.config.jsx` will replace the help link entirely (in this case with a centered 🗺️ `h1`)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
learning_help_slot: {
|
||||||
|
keepDefault: false,
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Insert,
|
||||||
|
widget: {
|
||||||
|
id: 'custom_learning_help_component',
|
||||||
|
type: DIRECT_PLUGIN,
|
||||||
|
RenderWidget: () => (
|
||||||
|
<h1 style={{textAlign: 'center'}}>🗺️</h1>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
After Width: | Height: | Size: 18 KiB |
11
src/plugin-slots/LearningHelpSlot/index.jsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||||
|
import LearningHeaderHelpLink from '../../learning-header/LearningHeaderHelpLink';
|
||||||
|
|
||||||
|
const LearningHelpSlot = () => (
|
||||||
|
<PluginSlot id="learning_help_slot">
|
||||||
|
<LearningHeaderHelpLink />
|
||||||
|
</PluginSlot>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default LearningHelpSlot;
|
||||||
132
src/plugin-slots/LearningLoggedOutItemsSlot/README.md
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
# Learning Logged Out Items Slot
|
||||||
|
|
||||||
|
### Slot ID: `learning_logged_out_items_slot`
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
This slot is used to replace/modify/hide the items shown on the learning header when the user is logged out.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Modify Items
|
||||||
|
|
||||||
|
The following `env.config.jsx` will modify the items shown on the learning header when the user is logged out.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
|
const modifyLoggedOutItems = ( widget ) => {
|
||||||
|
widget.content.buttonsInfo = [
|
||||||
|
{
|
||||||
|
href: 'https://docs.openedx.org/en/latest/',
|
||||||
|
message: 'Documentation',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'https://discuss.openedx.org/',
|
||||||
|
message: 'Forums',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'https://openedx.org/',
|
||||||
|
message: 'openedx.org',
|
||||||
|
variant: 'primary',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return widget;
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
learning_logged_out_items_slot: {
|
||||||
|
keepDefault: true,
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Modify,
|
||||||
|
widgetId: 'default_contents',
|
||||||
|
fn: modifyLoggedOutItems,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Replace with Custom Component
|
||||||
|
|
||||||
|
The following `env.config.jsx` will replace the items shown in the learning header when the user is logged out entirely (in this case with a centered 🗺️ `h1`)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
learning_logged_out_items_slot: {
|
||||||
|
keepDefault: false,
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Insert,
|
||||||
|
widget: {
|
||||||
|
id: 'custom_logged_out_items_component',
|
||||||
|
type: DIRECT_PLUGIN,
|
||||||
|
RenderWidget: () => (
|
||||||
|
<h1 style={{textAlign: 'center'}}>🗺️</h1>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add Custom Components before and after
|
||||||
|
|
||||||
|
The following `env.config.jsx` will place custom components before and after the items shown in the learning header when the user is logged out (in this case centered `h1`s with 🌜 and 🌛).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
learning_logged_out_items_slot: {
|
||||||
|
keepDefault: true,
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Insert,
|
||||||
|
widget: {
|
||||||
|
id: 'custom_before_logged_out_items_component',
|
||||||
|
type: DIRECT_PLUGIN,
|
||||||
|
priority: 10,
|
||||||
|
RenderWidget: () => (
|
||||||
|
<h1 style={{textAlign: 'center'}}>🌜</h1>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Insert,
|
||||||
|
widget: {
|
||||||
|
id: 'custom_after_logged_out_items_component',
|
||||||
|
type: DIRECT_PLUGIN,
|
||||||
|
priority: 90,
|
||||||
|
RenderWidget: () => (
|
||||||
|
<h1 style={{textAlign: 'center'}}>🌛</h1>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 18 KiB |
20
src/plugin-slots/LearningLoggedOutItemsSlot/index.jsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||||
|
import LearningLoggedOutButtons, { learningHeaderLoggedOutItemsDataShape } from '../../learning-header/LearningLoggedOutButtons';
|
||||||
|
|
||||||
|
const LearningLoggedOutItemsSlot = ({
|
||||||
|
buttonsInfo,
|
||||||
|
}) => (
|
||||||
|
<PluginSlot
|
||||||
|
id="learning_logged_out_items_slot"
|
||||||
|
slotOptions={{
|
||||||
|
mergeProps: true,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LearningLoggedOutButtons buttonsInfo={buttonsInfo} />
|
||||||
|
</PluginSlot>
|
||||||
|
);
|
||||||
|
|
||||||
|
LearningLoggedOutItemsSlot.propTypes = learningHeaderLoggedOutItemsDataShape;
|
||||||
|
|
||||||
|
export default LearningLoggedOutItemsSlot;
|
||||||
130
src/plugin-slots/LearningUserMenuSlot/README.md
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
# Learning User Menu Slot
|
||||||
|
|
||||||
|
### Slot ID: `learning_user_menu_slot`
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
This slot is used to replace/modify/hide the learning user menu.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Modify Items
|
||||||
|
|
||||||
|
The following `env.config.jsx` will modify the items in the learning user menu.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
|
const modifyUserMenu = ( widget ) => {
|
||||||
|
widget.content.items = [
|
||||||
|
{
|
||||||
|
href: 'https://openedx.org/',
|
||||||
|
message: 'openedx.org',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'https://docs.openedx.org/en/latest/',
|
||||||
|
message: 'Documentation',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'https://discuss.openedx.org/',
|
||||||
|
message: 'Forums',
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return widget;
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
learning_user_menu_slot: {
|
||||||
|
keepDefault: true,
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Modify,
|
||||||
|
widgetId: 'default_contents',
|
||||||
|
fn: modifyUserMenu,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Replace Menu with Custom Component
|
||||||
|
|
||||||
|
The following `env.config.jsx` will replace the items in the learning user menu entirely (in this case with a centered 🗺️ `h1`)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
learning_user_menu_slot: {
|
||||||
|
keepDefault: false,
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Insert,
|
||||||
|
widget: {
|
||||||
|
id: 'custom_user_menu_component',
|
||||||
|
type: DIRECT_PLUGIN,
|
||||||
|
RenderWidget: () => (
|
||||||
|
<h1 style={{textAlign: 'center'}}>🗺️</h1>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add Custom Components before and after Menu
|
||||||
|
|
||||||
|
The following `env.config.jsx` will place custom components before and after the learning user menu (in this case centered `h1`s with 🌞 and 🌚).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
learning_user_menu_slot: {
|
||||||
|
keepDefault: true,
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Insert,
|
||||||
|
widget: {
|
||||||
|
id: 'custom_before_user_menu_component',
|
||||||
|
type: DIRECT_PLUGIN,
|
||||||
|
priority: 10,
|
||||||
|
RenderWidget: () => (
|
||||||
|
<h1 style={{textAlign: 'center'}}>🌞</h1>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Insert,
|
||||||
|
widget: {
|
||||||
|
id: 'custom_after_user_menu_component',
|
||||||
|
type: DIRECT_PLUGIN,
|
||||||
|
priority: 90,
|
||||||
|
RenderWidget: () => (
|
||||||
|
<h1 style={{textAlign: 'center'}}>🌚</h1>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
20
src/plugin-slots/LearningUserMenuSlot/index.jsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||||
|
import LearningHeaderUserMenuItems, { learningHeaderUserMenuDataShape } from '../../learning-header/LearningHeaderUserMenuItems';
|
||||||
|
|
||||||
|
const LearningUserMenuSlot = ({
|
||||||
|
items,
|
||||||
|
}) => (
|
||||||
|
<PluginSlot
|
||||||
|
id="learning_user_menu_slot"
|
||||||
|
slotOptions={{
|
||||||
|
mergeProps: true,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LearningHeaderUserMenuItems items={items} />
|
||||||
|
</PluginSlot>
|
||||||
|
);
|
||||||
|
|
||||||
|
LearningUserMenuSlot.propTypes = learningHeaderUserMenuDataShape;
|
||||||
|
|
||||||
|
export default LearningUserMenuSlot;
|
||||||
69
src/plugin-slots/LogoSlot/README.md
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# Logo Slot
|
||||||
|
|
||||||
|
### Slot ID: `logo_slot`
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
This slot is used to replace/modify/hide the logo.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Modify URL
|
||||||
|
|
||||||
|
The following `env.config.jsx` will modify the link href for the logo.
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
|
const modifyLogoHref = ( widget ) => {
|
||||||
|
widget.content.href = "https://openedx.org/";
|
||||||
|
return widget;
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
logo_slot: {
|
||||||
|
keepDefault: true,
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Modify,
|
||||||
|
widgetId: 'default_contents',
|
||||||
|
fn: modifyLogoHref,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Component
|
||||||
|
|
||||||
|
The following `env.config.jsx` will replace the logo entirely (in this case with a centered 🗺️ `h1`)
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
logo_slot: {
|
||||||
|
keepDefault: false,
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Insert,
|
||||||
|
widget: {
|
||||||
|
id: 'custom_logo_component',
|
||||||
|
type: DIRECT_PLUGIN,
|
||||||
|
RenderWidget: () => (
|
||||||
|
<h1 style={{textAlign: 'center'}}>🗺️</h1>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
20
src/plugin-slots/LogoSlot/index.jsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||||
|
import Logo, { logoDataShape } from '../../Logo';
|
||||||
|
|
||||||
|
const LogoSlot = ({
|
||||||
|
href, src, alt, ...attributes
|
||||||
|
}) => (
|
||||||
|
<PluginSlot
|
||||||
|
id="logo_slot"
|
||||||
|
slotOptions={{
|
||||||
|
mergeProps: true,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Logo href={href} src={src} alt={alt} {...attributes} />
|
||||||
|
</PluginSlot>
|
||||||
|
);
|
||||||
|
|
||||||
|
LogoSlot.propTypes = logoDataShape;
|
||||||
|
|
||||||
|
export default LogoSlot;
|
||||||
41
src/plugin-slots/MobileHeaderSlot/README.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Mobile Header Slot
|
||||||
|
|
||||||
|
### Slot ID: `mobile_header_slot`
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
This slot is used to replace/modify/hide the entire mobile header.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Custom Component
|
||||||
|
|
||||||
|
The following `env.config.jsx` will replace the mobile header entirely (in this case with a centered 🗺️ `h1`)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
mobile_header_slot: {
|
||||||
|
keepDefault: false,
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Insert,
|
||||||
|
widget: {
|
||||||
|
id: 'custom_mobile_header_component',
|
||||||
|
type: DIRECT_PLUGIN,
|
||||||
|
RenderWidget: () => (
|
||||||
|
<h1 style={{textAlign: 'center'}}>🗺️</h1>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
After Width: | Height: | Size: 16 KiB |
20
src/plugin-slots/MobileHeaderSlot/index.jsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||||
|
import MobileHeader, { mobileHeaderDataShape } from '../../mobile-header/MobileHeader';
|
||||||
|
|
||||||
|
const MobileHeaderSlot = ({
|
||||||
|
props,
|
||||||
|
}) => (
|
||||||
|
<PluginSlot
|
||||||
|
id="mobile_header_slot"
|
||||||
|
slotOptions={{
|
||||||
|
mergeProps: true,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MobileHeader {...props} />
|
||||||
|
</PluginSlot>
|
||||||
|
);
|
||||||
|
|
||||||
|
MobileHeaderSlot.propTypes = mobileHeaderDataShape;
|
||||||
|
|
||||||
|
export default MobileHeaderSlot;
|
||||||
134
src/plugin-slots/MobileLoggedOutItemsSlot/README.md
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
# Mobile Logged Out Items Slot
|
||||||
|
|
||||||
|
### Slot ID: `mobile_logged_out_items_slot`
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
This slot is used to replace/modify/hide the mobile user menu when logged out.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Modify Items
|
||||||
|
|
||||||
|
The following `env.config.jsx` will modify the items in mobile user menu when logged out.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
|
const modifyLoggedOutItems = ( widget ) => {
|
||||||
|
widget.content.items = [
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
href: 'https://openedx.org/',
|
||||||
|
content: 'openedx.org',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
href: 'https://docs.openedx.org/en/latest/',
|
||||||
|
content: 'Documentation',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
href: 'https://discuss.openedx.org/',
|
||||||
|
content: 'Forums',
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return widget;
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
mobile_logged_out_items_slot: {
|
||||||
|
keepDefault: true,
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Modify,
|
||||||
|
widgetId: 'default_contents',
|
||||||
|
fn: modifyLoggedOutItems,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Replace Items with Custom Component
|
||||||
|
|
||||||
|
The following `env.config.jsx` will replace the items in mobile user menu when logged out entirely (in this case with a centered 🗺️ `h1`)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
mobile_logged_out_items_slot: {
|
||||||
|
keepDefault: false,
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Insert,
|
||||||
|
widget: {
|
||||||
|
id: 'custom_logged_out_items_component',
|
||||||
|
type: DIRECT_PLUGIN,
|
||||||
|
RenderWidget: () => (
|
||||||
|
<h1 style={{textAlign: 'center'}}>🗺️</h1>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add Custom Components before and after Items
|
||||||
|
|
||||||
|
The following `env.config.jsx` will place custom components before and after the items in mobile user menu when logged out (in this case centered `h1`s with 🌞 and 🌚).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
mobile_logged_out_items_slot: {
|
||||||
|
keepDefault: true,
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Insert,
|
||||||
|
widget: {
|
||||||
|
id: 'custom_before_logged_out_items_component',
|
||||||
|
type: DIRECT_PLUGIN,
|
||||||
|
priority: 10,
|
||||||
|
RenderWidget: () => (
|
||||||
|
<h1 style={{textAlign: 'center'}}>🌞</h1>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Insert,
|
||||||
|
widget: {
|
||||||
|
id: 'custom_after_logged_out_items_component',
|
||||||
|
type: DIRECT_PLUGIN,
|
||||||
|
priority: 90,
|
||||||
|
RenderWidget: () => (
|
||||||
|
<h1 style={{textAlign: 'center'}}>🌚</h1>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
22
src/plugin-slots/MobileLoggedOutItemsSlot/index.jsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||||
|
import MobileLoggedOutItems, { mobileHeaderLoggedOutItemsDataShape } from '../../mobile-header/MobileLoggedOutItems';
|
||||||
|
|
||||||
|
const MobileLoggedOutItemsSlot = ({
|
||||||
|
items,
|
||||||
|
}) => (
|
||||||
|
<PluginSlot
|
||||||
|
id="mobile_logged_out_items_slot"
|
||||||
|
slotOptions={{
|
||||||
|
mergeProps: true,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MobileLoggedOutItems items={items} />
|
||||||
|
</PluginSlot>
|
||||||
|
);
|
||||||
|
|
||||||
|
MobileLoggedOutItemsSlot.propTypes = {
|
||||||
|
items: mobileHeaderLoggedOutItemsDataShape,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MobileLoggedOutItemsSlot;
|
||||||
134
src/plugin-slots/MobileMainMenuSlot/README.md
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
# Mobile Main Menu Slot
|
||||||
|
|
||||||
|
### Slot ID: `mobile_main_menu_slot`
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
This slot is used to replace/modify/hide the mobile main menu.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Modify Items
|
||||||
|
|
||||||
|
The following `env.config.jsx` will modify the items in the mobile main menu.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
|
const modifyMainMenu = ( widget ) => {
|
||||||
|
widget.content.menu = [
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
href: 'https://openedx.org/',
|
||||||
|
content: 'openedx.org',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
href: 'https://docs.openedx.org/en/latest/',
|
||||||
|
content: 'Documentation',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
href: 'https://discuss.openedx.org/',
|
||||||
|
content: 'Forums',
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return widget;
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
mobile_main_menu_slot: {
|
||||||
|
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`)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
mobile_main_menu_slot: {
|
||||||
|
keepDefault: false,
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Insert,
|
||||||
|
widget: {
|
||||||
|
id: 'custom_main_menu_component',
|
||||||
|
type: DIRECT_PLUGIN,
|
||||||
|
RenderWidget: () => (
|
||||||
|
<h1 style={{textAlign: 'center'}}>🗺️</h1>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add Custom Components before and after Menu
|
||||||
|
|
||||||
|
The following `env.config.jsx` will place custom components before and after the mobile main menu (in this case centered `h1`s with 🌞 and 🌚).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
mobile_main_menu_slot: {
|
||||||
|
keepDefault: true,
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Insert,
|
||||||
|
widget: {
|
||||||
|
id: 'custom_before_main_menu_component',
|
||||||
|
type: DIRECT_PLUGIN,
|
||||||
|
priority: 10,
|
||||||
|
RenderWidget: () => (
|
||||||
|
<h1 style={{textAlign: 'center'}}>🌞</h1>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Insert,
|
||||||
|
widget: {
|
||||||
|
id: 'custom_after_main_menu_component',
|
||||||
|
type: DIRECT_PLUGIN,
|
||||||
|
priority: 90,
|
||||||
|
RenderWidget: () => (
|
||||||
|
<h1 style={{textAlign: 'center'}}>🌚</h1>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
22
src/plugin-slots/MobileMainMenuSlot/index.jsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||||
|
import MobileHeaderMainMenu, { mobileHeaderMainMenuDataShape } from '../../mobile-header/MobileHeaderMainMenu';
|
||||||
|
|
||||||
|
const MobileMainMenuSlot = ({
|
||||||
|
menu,
|
||||||
|
}) => (
|
||||||
|
<PluginSlot
|
||||||
|
id="mobile_main_menu_slot"
|
||||||
|
slotOptions={{
|
||||||
|
mergeProps: true,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MobileHeaderMainMenu menu={menu} />
|
||||||
|
</PluginSlot>
|
||||||
|
);
|
||||||
|
|
||||||
|
MobileMainMenuSlot.propTypes = {
|
||||||
|
menu: mobileHeaderMainMenuDataShape,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MobileMainMenuSlot;
|
||||||
142
src/plugin-slots/MobileUserMenuSlot/README.md
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
# Mobile User Menu Slot
|
||||||
|
|
||||||
|
### Slot ID: `mobile_user_menu_slot`
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
This slot is used to replace/modify/hide the mobile user menu.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Modify Items
|
||||||
|
|
||||||
|
The following `env.config.jsx` will modify the items in the mobile user menu.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
|
const modifyUserMenu = ( widget ) => {
|
||||||
|
widget.content.menu = [
|
||||||
|
{
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
href: 'https://openedx.org/',
|
||||||
|
content: 'openedx.org',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
href: 'https://docs.openedx.org/en/latest/',
|
||||||
|
content: 'Documentation',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
href: 'https://discuss.openedx.org/',
|
||||||
|
content: 'Forums',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return widget;
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
mobile_user_menu_slot: {
|
||||||
|
keepDefault: true,
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Modify,
|
||||||
|
widgetId: 'default_contents',
|
||||||
|
fn: modifyUserMenu,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Replace Menu with Custom Component
|
||||||
|
|
||||||
|
The following `env.config.jsx` will replace the mobile main user entirely (in this case with a centered 🗺️ `h1`)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
mobile_user_menu_slot: {
|
||||||
|
keepDefault: false,
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Insert,
|
||||||
|
widget: {
|
||||||
|
id: 'custom_user_menu_component',
|
||||||
|
type: DIRECT_PLUGIN,
|
||||||
|
RenderWidget: () => (
|
||||||
|
<h1 style={{textAlign: 'center'}}>🗺️</h1>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add Custom Components before and after Menu
|
||||||
|
|
||||||
|
The following `env.config.jsx` will place custom components before and after the mobile user menu (in this case centered `h1`s with 🌞 and 🌚).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
mobile_user_menu_slot: {
|
||||||
|
keepDefault: true,
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Insert,
|
||||||
|
widget: {
|
||||||
|
id: 'custom_before_user_menu_component',
|
||||||
|
type: DIRECT_PLUGIN,
|
||||||
|
priority: 10,
|
||||||
|
RenderWidget: () => (
|
||||||
|
<h1 style={{textAlign: 'center'}}>🌞</h1>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Insert,
|
||||||
|
widget: {
|
||||||
|
id: 'custom_after_user_menu_component',
|
||||||
|
type: DIRECT_PLUGIN,
|
||||||
|
priority: 90,
|
||||||
|
RenderWidget: () => (
|
||||||
|
<h1 style={{textAlign: 'center'}}>🌚</h1>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
22
src/plugin-slots/MobileUserMenuSlot/index.jsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||||
|
import MobileHeaderUserMenu, { mobileHeaderUserMenuDataShape } from '../../mobile-header/MobileHeaderUserMenu';
|
||||||
|
|
||||||
|
const MobileUserMenuSlot = ({
|
||||||
|
menu,
|
||||||
|
}) => (
|
||||||
|
<PluginSlot
|
||||||
|
id="mobile_user_menu_slot"
|
||||||
|
slotOptions={{
|
||||||
|
mergeProps: true,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MobileHeaderUserMenu menu={menu} />
|
||||||
|
</PluginSlot>
|
||||||
|
);
|
||||||
|
|
||||||
|
MobileUserMenuSlot.propTypes = {
|
||||||
|
menu: mobileHeaderUserMenuDataShape,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MobileUserMenuSlot;
|
||||||
15
src/plugin-slots/README.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# `frontend-component-header` Plugin Slots
|
||||||
|
|
||||||
|
* [`logo_slot`](./LogoSlot/)
|
||||||
|
* [`desktop_main_menu_slot`](./DesktopMainMenuSlot/)
|
||||||
|
* [`desktop_secondary_menu_slot`](./DesktopSecondaryMenuSlot/)
|
||||||
|
* [`mobile_main_menu_slot`](./MobileMainMenuSlot/)
|
||||||
|
* [`course_info_slot`](./CourseInfoSlot/)
|
||||||
|
* [`learning_help_slot`](./LearningHelpSlot/)
|
||||||
|
* [`desktop_logged_out_items_slot`](./DesktopLoggedOutItemsSlot/)
|
||||||
|
* [`mobile_logged_out_items_slot`](./MobileLoggedOutItemsSlot/)
|
||||||
|
* [`mobile_user_menu_slot`](./MobileUserMenuSlot/)
|
||||||
|
* [`desktop_user_menu_slot`](./DesktopUserMenuSlot/)
|
||||||
|
* [`learning_user_menu_slot`](./LearningUserMenuSlot/)
|
||||||
|
* [`learning_logged_out_items_slot`](./LearningLoggedOutItemsSlot/)
|
||||||
|
* [`desktop_header_slot`](./DesktopHeaderSlot/)
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
const BrandNav = ({
|
const BrandNav = ({
|
||||||
studioBaseUrl,
|
studioBaseUrl,
|
||||||
logo,
|
logo,
|
||||||
logoAltText,
|
logoAltText,
|
||||||
}) => (
|
}) => (
|
||||||
<a href={studioBaseUrl}>
|
<Link to={studioBaseUrl}>
|
||||||
<img
|
<img
|
||||||
src={logo}
|
src={logo}
|
||||||
alt={logoAltText}
|
alt={logoAltText}
|
||||||
className="d-block logo"
|
className="d-block logo"
|
||||||
/>
|
/>
|
||||||
</a>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|
||||||
BrandNav.propTypes = {
|
BrandNav.propTypes = {
|
||||||
|
|||||||
40
src/studio-header/BrandNav.test.jsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import '@testing-library/jest-dom/extend-expect';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
|
||||||
|
import BrandNav from './BrandNav';
|
||||||
|
|
||||||
|
const studioBaseUrl = 'https://example.com/';
|
||||||
|
const logo = 'logo.png';
|
||||||
|
const logoAltText = 'Example Logo';
|
||||||
|
|
||||||
|
const RootWrapper = () => (
|
||||||
|
<MemoryRouter>
|
||||||
|
<BrandNav
|
||||||
|
studioBaseUrl={studioBaseUrl}
|
||||||
|
logo={logo}
|
||||||
|
logoAltText={logoAltText}
|
||||||
|
/>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('BrandNav Component', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the logo with the correct alt text', () => {
|
||||||
|
render(<RootWrapper />);
|
||||||
|
|
||||||
|
const img = screen.getByAltText(logoAltText);
|
||||||
|
expect(img).toHaveAttribute('src', logo);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays a link that navigates to studioBaseUrl', () => {
|
||||||
|
render(<RootWrapper />);
|
||||||
|
|
||||||
|
const link = screen.getByRole('link');
|
||||||
|
expect(link.href).toBe(studioBaseUrl);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -5,6 +5,8 @@ import {
|
|||||||
OverlayTrigger,
|
OverlayTrigger,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@openedx/paragon';
|
} from '@openedx/paragon';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
const CourseLockUp = ({
|
const CourseLockUp = ({
|
||||||
@@ -23,15 +25,15 @@ const CourseLockUp = ({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<a
|
<Link
|
||||||
className="course-title-lockup mr-2"
|
className="course-title-lockup mr-2"
|
||||||
href={outlineLink}
|
to={outlineLink}
|
||||||
aria-label={intl.formatMessage(messages['header.label.courseOutline'])}
|
aria-label={intl.formatMessage(messages['header.label.courseOutline'])}
|
||||||
data-testid="course-lock-up-block"
|
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 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>
|
<span className="d-block m-0 font-weight-bold text-gray-800" data-testid="course-title">{title}</span>
|
||||||
</a>
|
</Link>
|
||||||
</OverlayTrigger>
|
</OverlayTrigger>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
58
src/studio-header/CourseLockUp.test.jsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import '@testing-library/jest-dom/extend-expect';
|
||||||
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
|
||||||
|
import CourseLockUp from './CourseLockUp';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
const mockProps = {
|
||||||
|
number: '101',
|
||||||
|
org: 'EDX',
|
||||||
|
title: 'Course Title',
|
||||||
|
outlineLink: 'https://example.com/course-outline',
|
||||||
|
};
|
||||||
|
|
||||||
|
const RootWrapper = (props) => (
|
||||||
|
<MemoryRouter>
|
||||||
|
<IntlProvider locale="en" messages={messages}>
|
||||||
|
<CourseLockUp {...props} />
|
||||||
|
</IntlProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('CourseLockUp Component', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders course org, number, and title', () => {
|
||||||
|
render(<RootWrapper {...mockProps} />);
|
||||||
|
|
||||||
|
const courseOrgNumber = screen.getByTestId('course-org-number');
|
||||||
|
const courseTitle = screen.getByTestId('course-title');
|
||||||
|
|
||||||
|
expect(courseOrgNumber).toBeInTheDocument();
|
||||||
|
expect(courseOrgNumber).toHaveTextContent(`${mockProps.org} ${mockProps.number}`);
|
||||||
|
expect(courseTitle).toBeInTheDocument();
|
||||||
|
expect(courseTitle).toHaveTextContent(mockProps.title);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the link with correct aria-label', () => {
|
||||||
|
render(<RootWrapper {...mockProps} />);
|
||||||
|
|
||||||
|
const link = screen.getByTestId('course-lock-up-block');
|
||||||
|
expect(link).toHaveAttribute(
|
||||||
|
'aria-label',
|
||||||
|
messages['header.label.courseOutline'].defaultMessage,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('navigates to an absolute URL when clicked', () => {
|
||||||
|
render(<RootWrapper {...mockProps} />);
|
||||||
|
|
||||||
|
const link = screen.getByTestId('course-lock-up-block');
|
||||||
|
expect(link.href).toBe(mockProps.outlineLink);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import classNames from 'classnames';
|
||||||
import {
|
import {
|
||||||
ActionRow,
|
ActionRow,
|
||||||
Button,
|
Button,
|
||||||
@@ -37,6 +38,7 @@ const HeaderBody = ({
|
|||||||
mainMenuDropdowns,
|
mainMenuDropdowns,
|
||||||
outlineLink,
|
outlineLink,
|
||||||
searchButtonAction,
|
searchButtonAction,
|
||||||
|
containerProps,
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
@@ -50,8 +52,14 @@ const HeaderBody = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { className: containerClassName, ...restContainerProps } = containerProps || {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container size="xl" className="px-2.5">
|
<Container
|
||||||
|
size="xl"
|
||||||
|
className={classNames('px-2.5', containerClassName)}
|
||||||
|
{...restContainerProps}
|
||||||
|
>
|
||||||
<ActionRow as="header">
|
<ActionRow as="header">
|
||||||
{isHiddenMainMenu ? (
|
{isHiddenMainMenu ? (
|
||||||
<Row className="flex-nowrap ml-4">
|
<Row className="flex-nowrap ml-4">
|
||||||
@@ -95,7 +103,12 @@ const HeaderBody = ({
|
|||||||
{mainMenuDropdowns.map(dropdown => {
|
{mainMenuDropdowns.map(dropdown => {
|
||||||
const { id, buttonTitle, items } = dropdown;
|
const { id, buttonTitle, items } = dropdown;
|
||||||
return (
|
return (
|
||||||
<NavDropdownMenu key={id} {...{ id, buttonTitle, items }} />
|
<NavDropdownMenu
|
||||||
|
key={id}
|
||||||
|
{...{
|
||||||
|
id, buttonTitle, items,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Nav>
|
</Nav>
|
||||||
@@ -110,6 +123,7 @@ const HeaderBody = ({
|
|||||||
iconAs={Icon}
|
iconAs={Icon}
|
||||||
onClick={searchButtonAction}
|
onClick={searchButtonAction}
|
||||||
aria-label={intl.formatMessage(messages['header.label.search.nav'])}
|
aria-label={intl.formatMessage(messages['header.label.search.nav'])}
|
||||||
|
alt={intl.formatMessage(messages['header.label.search.nav'])}
|
||||||
/>
|
/>
|
||||||
</Nav>
|
</Nav>
|
||||||
)}
|
)}
|
||||||
@@ -147,14 +161,15 @@ HeaderBody.propTypes = {
|
|||||||
isHiddenMainMenu: PropTypes.bool,
|
isHiddenMainMenu: PropTypes.bool,
|
||||||
mainMenuDropdowns: PropTypes.arrayOf(PropTypes.shape({
|
mainMenuDropdowns: PropTypes.arrayOf(PropTypes.shape({
|
||||||
id: PropTypes.string,
|
id: PropTypes.string,
|
||||||
buttonTitle: PropTypes.string,
|
buttonTitle: PropTypes.node,
|
||||||
items: PropTypes.arrayOf(PropTypes.shape({
|
items: PropTypes.arrayOf(PropTypes.shape({
|
||||||
href: PropTypes.string,
|
href: PropTypes.string,
|
||||||
title: PropTypes.string,
|
title: PropTypes.node,
|
||||||
})),
|
})),
|
||||||
})),
|
})),
|
||||||
outlineLink: PropTypes.string,
|
outlineLink: PropTypes.string,
|
||||||
searchButtonAction: PropTypes.func,
|
searchButtonAction: PropTypes.func,
|
||||||
|
containerProps: PropTypes.shape(Container.propTypes),
|
||||||
};
|
};
|
||||||
|
|
||||||
HeaderBody.defaultProps = {
|
HeaderBody.defaultProps = {
|
||||||
@@ -174,6 +189,7 @@ HeaderBody.defaultProps = {
|
|||||||
mainMenuDropdowns: [],
|
mainMenuDropdowns: [],
|
||||||
outlineLink: null,
|
outlineLink: null,
|
||||||
searchButtonAction: null,
|
searchButtonAction: null,
|
||||||
|
containerProps: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default HeaderBody;
|
export default HeaderBody;
|
||||||
|
|||||||
102
src/studio-header/HeaderBody.test.jsx
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
|
import '@testing-library/jest-dom/extend-expect';
|
||||||
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
|
||||||
|
import HeaderBody from './HeaderBody';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
const mockOnNavigate = jest.fn();
|
||||||
|
const mockSearchButtonAction = jest.fn();
|
||||||
|
const mockToggleModalPopup = jest.fn();
|
||||||
|
const mockSetModalPopupTarget = jest.fn();
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
studioBaseUrl: 'https://example.com',
|
||||||
|
logoutUrl: 'https://example.com/logout',
|
||||||
|
onNavigate: mockOnNavigate,
|
||||||
|
setModalPopupTarget: mockSetModalPopupTarget,
|
||||||
|
toggleModalPopup: mockToggleModalPopup,
|
||||||
|
searchButtonAction: mockSearchButtonAction,
|
||||||
|
username: 'testuser',
|
||||||
|
authenticatedUserAvatar: 'avatar.png',
|
||||||
|
isAdmin: true,
|
||||||
|
isMobile: false,
|
||||||
|
isHiddenMainMenu: false,
|
||||||
|
mainMenuDropdowns: [],
|
||||||
|
logo: 'logo.png',
|
||||||
|
logoAltText: 'Test Logo',
|
||||||
|
number: '101',
|
||||||
|
org: 'EDX',
|
||||||
|
title: 'Test Course',
|
||||||
|
outlineLink: '/courses/edx/course-101',
|
||||||
|
};
|
||||||
|
|
||||||
|
const RootWrapper = (props) => (
|
||||||
|
<MemoryRouter>
|
||||||
|
<IntlProvider locale="en" messages={messages}>
|
||||||
|
<HeaderBody {...props} />
|
||||||
|
</IntlProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('HeaderBody Component', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the logo and brand navigation', () => {
|
||||||
|
render(<RootWrapper {...defaultProps} />);
|
||||||
|
|
||||||
|
const logoImage = screen.getByAltText(defaultProps.logoAltText);
|
||||||
|
expect(logoImage).toBeInTheDocument();
|
||||||
|
expect(logoImage).toHaveAttribute('src', defaultProps.logo);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders course lockup information', () => {
|
||||||
|
render(<RootWrapper {...defaultProps} />);
|
||||||
|
|
||||||
|
const courseTitle = screen.getByText(defaultProps.title);
|
||||||
|
const courseOrgNumber = screen.getByText(`${defaultProps.org} ${defaultProps.number}`);
|
||||||
|
|
||||||
|
expect(courseTitle).toBeInTheDocument();
|
||||||
|
expect(courseOrgNumber).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders a course lock-up link with the correct outline URL', () => {
|
||||||
|
render(<RootWrapper {...defaultProps} />);
|
||||||
|
|
||||||
|
const courseLockUpLink = screen.getByTestId('course-lock-up-block');
|
||||||
|
expect(courseLockUpLink.getAttribute('href')).toBe(defaultProps.outlineLink);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays search button and triggers searchButtonAction on click', () => {
|
||||||
|
render(<RootWrapper {...defaultProps} />);
|
||||||
|
|
||||||
|
const searchButton = screen.getByLabelText(messages['header.label.search.nav'].defaultMessage);
|
||||||
|
expect(searchButton).toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(searchButton);
|
||||||
|
expect(mockSearchButtonAction).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays user menu with username and avatar', () => {
|
||||||
|
render(<RootWrapper {...defaultProps} />);
|
||||||
|
|
||||||
|
const userMenu = screen.getByText(defaultProps.username);
|
||||||
|
const avatarImage = screen.getByAltText(defaultProps.username);
|
||||||
|
|
||||||
|
expect(userMenu).toBeInTheDocument();
|
||||||
|
expect(avatarImage).toHaveAttribute('src', defaultProps.authenticatedUserAvatar);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('toggles mobile menu popup when button is clicked in mobile view', () => {
|
||||||
|
render(<RootWrapper {...defaultProps} isMobile isModalPopupOpen={false} />);
|
||||||
|
|
||||||
|
const menuButton = screen.getByTestId('mobile-menu-button');
|
||||||
|
fireEvent.click(menuButton);
|
||||||
|
|
||||||
|
expect(mockToggleModalPopup).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -48,10 +48,10 @@ MobileHeader.propTypes = {
|
|||||||
isAdmin: PropTypes.bool,
|
isAdmin: PropTypes.bool,
|
||||||
mainMenuDropdowns: PropTypes.arrayOf(PropTypes.shape({
|
mainMenuDropdowns: PropTypes.arrayOf(PropTypes.shape({
|
||||||
id: PropTypes.string,
|
id: PropTypes.string,
|
||||||
buttonTitle: PropTypes.string,
|
buttonTitle: PropTypes.node,
|
||||||
items: PropTypes.arrayOf(PropTypes.shape({
|
items: PropTypes.arrayOf(PropTypes.shape({
|
||||||
href: PropTypes.string,
|
href: PropTypes.string,
|
||||||
title: PropTypes.string,
|
title: PropTypes.node,
|
||||||
})),
|
})),
|
||||||
})),
|
})),
|
||||||
outlineLink: PropTypes.string,
|
outlineLink: PropTypes.string,
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Collapsible } from '@openedx/paragon';
|
import { Collapsible } from '@openedx/paragon';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
const MobileMenu = ({
|
const MobileMenu = ({ mainMenuDropdowns }) => (
|
||||||
mainMenuDropdowns,
|
|
||||||
}) => (
|
|
||||||
<div
|
<div
|
||||||
className="ml-4 p-2 bg-light-100 border border-gray-200 small rounded"
|
className="ml-4 p-2 bg-light-100 border border-gray-200 small rounded"
|
||||||
data-testid="mobile-menu"
|
data-testid="mobile-menu"
|
||||||
@@ -21,9 +20,9 @@ const MobileMenu = ({
|
|||||||
<ul className="p-0" style={{ listStyleType: 'none' }}>
|
<ul className="p-0" style={{ listStyleType: 'none' }}>
|
||||||
{items.map(item => (
|
{items.map(item => (
|
||||||
<li className="mobile-menu-item">
|
<li className="mobile-menu-item">
|
||||||
<a href={item.href}>
|
<Link to={item.href}>
|
||||||
{item.title}
|
{item.title}
|
||||||
</a>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -37,10 +36,10 @@ const MobileMenu = ({
|
|||||||
MobileMenu.propTypes = {
|
MobileMenu.propTypes = {
|
||||||
mainMenuDropdowns: PropTypes.arrayOf(PropTypes.shape({
|
mainMenuDropdowns: PropTypes.arrayOf(PropTypes.shape({
|
||||||
id: PropTypes.string,
|
id: PropTypes.string,
|
||||||
buttonTitle: PropTypes.string,
|
buttonTitle: PropTypes.node,
|
||||||
items: PropTypes.arrayOf(PropTypes.shape({
|
items: PropTypes.arrayOf(PropTypes.shape({
|
||||||
href: PropTypes.string,
|
href: PropTypes.string,
|
||||||
title: PropTypes.string,
|
title: PropTypes.node,
|
||||||
})),
|
})),
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|||||||
81
src/studio-header/MobileMenu.test.jsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
|
||||||
|
import '@testing-library/jest-dom/extend-expect';
|
||||||
|
import MobileMenu from './MobileMenu';
|
||||||
|
|
||||||
|
const mockOnNavigate = jest.fn();
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
mainMenuDropdowns: [
|
||||||
|
{
|
||||||
|
id: 'menu1',
|
||||||
|
buttonTitle: 'Menu 1',
|
||||||
|
items: [
|
||||||
|
{ href: '/menu1/item1', title: 'Item 1' },
|
||||||
|
{ href: '/menu1/item2', title: 'Item 2' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'menu2',
|
||||||
|
buttonTitle: 'Menu 2',
|
||||||
|
items: [
|
||||||
|
{ href: 'https://external-link.com', title: 'External Link' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onNavigate: mockOnNavigate,
|
||||||
|
};
|
||||||
|
|
||||||
|
const RootWrapper = (props) => (
|
||||||
|
<MemoryRouter>
|
||||||
|
<MobileMenu {...props} />
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('MobileMenu Component', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders the mobile menu with dropdowns and items', () => {
|
||||||
|
render(<RootWrapper {...defaultProps} />);
|
||||||
|
|
||||||
|
const menu1Title = screen.getByText('Menu 1');
|
||||||
|
const menu2Title = screen.getByText('Menu 2');
|
||||||
|
|
||||||
|
expect(menu1Title).toBeInTheDocument();
|
||||||
|
expect(menu2Title).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('navigates to internal URL when item is clicked', () => {
|
||||||
|
render(<RootWrapper {...defaultProps} />);
|
||||||
|
|
||||||
|
const menu1Title = screen.getByText(defaultProps.mainMenuDropdowns[0].buttonTitle);
|
||||||
|
fireEvent.click(menu1Title);
|
||||||
|
|
||||||
|
const menuItem = screen.getByText(defaultProps.mainMenuDropdowns[0].items[0].title);
|
||||||
|
expect(menuItem.getAttribute('href')).toBe(defaultProps.mainMenuDropdowns[0].items[0].href);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('navigates to an external URL when external link is clicked', () => {
|
||||||
|
render(<RootWrapper {...defaultProps} />);
|
||||||
|
|
||||||
|
const menu2Title = screen.getByText(defaultProps.mainMenuDropdowns[1].buttonTitle);
|
||||||
|
fireEvent.click(menu2Title);
|
||||||
|
|
||||||
|
const externalLink = screen.getByText(defaultProps.mainMenuDropdowns[1].items[0].title);
|
||||||
|
expect(externalLink.getAttribute('href')).toBe(defaultProps.mainMenuDropdowns[1].items[0].href);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders empty state when there are no dropdowns', () => {
|
||||||
|
render(<RootWrapper mainMenuDropdowns={[]} onNavigate={mockOnNavigate} />);
|
||||||
|
|
||||||
|
const mobileMenu = screen.getByTestId('mobile-menu');
|
||||||
|
expect(mobileMenu).toBeInTheDocument();
|
||||||
|
|
||||||
|
const menuItems = screen.queryAllByRole('listitem');
|
||||||
|
expect(menuItems.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
Dropdown,
|
Dropdown,
|
||||||
DropdownButton,
|
DropdownButton,
|
||||||
} from '@openedx/paragon';
|
} from '@openedx/paragon';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
const NavDropdownMenu = ({
|
const NavDropdownMenu = ({
|
||||||
id,
|
id,
|
||||||
@@ -18,8 +19,9 @@ const NavDropdownMenu = ({
|
|||||||
>
|
>
|
||||||
{items.map(item => (
|
{items.map(item => (
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
|
as={Link}
|
||||||
key={`${item.title}-dropdown-item`}
|
key={`${item.title}-dropdown-item`}
|
||||||
href={item.href}
|
to={item.href}
|
||||||
className="small"
|
className="small"
|
||||||
>
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
@@ -30,10 +32,10 @@ const NavDropdownMenu = ({
|
|||||||
|
|
||||||
NavDropdownMenu.propTypes = {
|
NavDropdownMenu.propTypes = {
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
buttonTitle: PropTypes.string.isRequired,
|
buttonTitle: PropTypes.node.isRequired,
|
||||||
items: PropTypes.arrayOf(PropTypes.shape({
|
items: PropTypes.arrayOf(PropTypes.shape({
|
||||||
href: PropTypes.string,
|
href: PropTypes.string.isRequired,
|
||||||
title: PropTypes.string,
|
title: PropTypes.node.isRequired,
|
||||||
})).isRequired,
|
})).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
67
src/studio-header/NavDropdownMenu.test.jsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
|
import '@testing-library/jest-dom/extend-expect';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
|
||||||
|
import NavDropdownMenu from './NavDropdownMenu';
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
id: 'menu-id',
|
||||||
|
buttonTitle: 'Menu',
|
||||||
|
items: [
|
||||||
|
{ href: '/item1', title: 'Item 1' },
|
||||||
|
{ href: 'https://external.com', title: 'External Link' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const RootWrapper = (props) => (
|
||||||
|
<MemoryRouter>
|
||||||
|
<NavDropdownMenu {...props} />
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('NavDropdownMenu Component', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders the dropdown button with correct title', () => {
|
||||||
|
render(<NavDropdownMenu {...defaultProps} />);
|
||||||
|
|
||||||
|
const dropdownButton = screen.getByRole('button', { name: defaultProps.buttonTitle });
|
||||||
|
expect(dropdownButton).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders all dropdown items', () => {
|
||||||
|
render(<RootWrapper {...defaultProps} />);
|
||||||
|
|
||||||
|
const dropdownButton = screen.getByRole('button', { name: defaultProps.buttonTitle });
|
||||||
|
fireEvent.click(dropdownButton);
|
||||||
|
|
||||||
|
const item1 = screen.getByText(defaultProps.items[0].title);
|
||||||
|
const externalLink = screen.getByText(defaultProps.items[1].title);
|
||||||
|
|
||||||
|
expect(item1).toBeInTheDocument();
|
||||||
|
expect(externalLink).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('calls onNavigate with the correct URL for internal link', () => {
|
||||||
|
render(<RootWrapper {...defaultProps} />);
|
||||||
|
|
||||||
|
const dropdownButton = screen.getByRole('button', { name: defaultProps.buttonTitle });
|
||||||
|
fireEvent.click(dropdownButton);
|
||||||
|
|
||||||
|
const item1 = screen.getByText(defaultProps.items[0].title);
|
||||||
|
expect(item1.getAttribute('href')).toBe(defaultProps.items[0].href);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('navigates to external URL when external link is clicked', () => {
|
||||||
|
render(<RootWrapper {...defaultProps} />);
|
||||||
|
|
||||||
|
const dropdownButton = screen.getByRole('button', { name: defaultProps.buttonTitle });
|
||||||
|
fireEvent.click(dropdownButton);
|
||||||
|
|
||||||
|
const externalLink = screen.getByText(defaultProps.items[1].title);
|
||||||
|
expect(externalLink.getAttribute('href')).toBe(defaultProps.items[1].href);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -16,7 +16,8 @@ ensureConfig([
|
|||||||
], 'Studio Header component');
|
], 'Studio Header component');
|
||||||
|
|
||||||
const StudioHeader = ({
|
const StudioHeader = ({
|
||||||
number, org, title, isHiddenMainMenu, mainMenuDropdowns, outlineLink, searchButtonAction,
|
number, org, title, containerProps, isHiddenMainMenu, mainMenuDropdowns,
|
||||||
|
outlineLink, searchButtonAction, isNewHomePage,
|
||||||
}) => {
|
}) => {
|
||||||
const { authenticatedUser, config } = useContext(AppContext);
|
const { authenticatedUser, config } = useContext(AppContext);
|
||||||
const props = {
|
const props = {
|
||||||
@@ -25,10 +26,11 @@ const StudioHeader = ({
|
|||||||
number,
|
number,
|
||||||
org,
|
org,
|
||||||
title,
|
title,
|
||||||
|
containerProps,
|
||||||
username: authenticatedUser?.username,
|
username: authenticatedUser?.username,
|
||||||
isAdmin: authenticatedUser?.administrator,
|
isAdmin: authenticatedUser?.administrator,
|
||||||
authenticatedUserAvatar: authenticatedUser?.avatar,
|
authenticatedUserAvatar: authenticatedUser?.avatar,
|
||||||
studioBaseUrl: config.STUDIO_BASE_URL,
|
studioBaseUrl: isNewHomePage ? '/home' : config.STUDIO_BASE_URL,
|
||||||
logoutUrl: config.LOGOUT_URL,
|
logoutUrl: config.LOGOUT_URL,
|
||||||
isHiddenMainMenu,
|
isHiddenMainMenu,
|
||||||
mainMenuDropdowns,
|
mainMenuDropdowns,
|
||||||
@@ -53,22 +55,25 @@ StudioHeader.propTypes = {
|
|||||||
number: PropTypes.string,
|
number: PropTypes.string,
|
||||||
org: PropTypes.string,
|
org: PropTypes.string,
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
|
containerProps: HeaderBody.propTypes.containerProps,
|
||||||
isHiddenMainMenu: PropTypes.bool,
|
isHiddenMainMenu: PropTypes.bool,
|
||||||
mainMenuDropdowns: PropTypes.arrayOf(PropTypes.shape({
|
mainMenuDropdowns: PropTypes.arrayOf(PropTypes.shape({
|
||||||
id: PropTypes.string,
|
id: PropTypes.string,
|
||||||
buttonTitle: PropTypes.string,
|
buttonTitle: PropTypes.node,
|
||||||
items: PropTypes.arrayOf(PropTypes.shape({
|
items: PropTypes.arrayOf(PropTypes.shape({
|
||||||
href: PropTypes.string,
|
href: PropTypes.string,
|
||||||
title: PropTypes.string,
|
title: PropTypes.node,
|
||||||
})),
|
})),
|
||||||
})),
|
})),
|
||||||
outlineLink: PropTypes.string,
|
outlineLink: PropTypes.string,
|
||||||
searchButtonAction: PropTypes.func,
|
searchButtonAction: PropTypes.func,
|
||||||
|
isNewHomePage: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
StudioHeader.defaultProps = {
|
StudioHeader.defaultProps = {
|
||||||
number: '',
|
number: '',
|
||||||
org: '',
|
org: '',
|
||||||
|
containerProps: {},
|
||||||
isHiddenMainMenu: false,
|
isHiddenMainMenu: false,
|
||||||
mainMenuDropdowns: [],
|
mainMenuDropdowns: [],
|
||||||
outlineLink: null,
|
outlineLink: null,
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import {
|
|||||||
import { AppContext } from '@edx/frontend-platform/react';
|
import { AppContext } from '@edx/frontend-platform/react';
|
||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
import { Context as ResponsiveContext } from 'react-responsive';
|
import { Context as ResponsiveContext } from 'react-responsive';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
|
||||||
import StudioHeader from './StudioHeader';
|
import StudioHeader from './StudioHeader';
|
||||||
import messages from './messages';
|
|
||||||
|
|
||||||
const authenticatedUser = {
|
const authenticatedUser = {
|
||||||
userId: 3,
|
userId: 3,
|
||||||
@@ -40,15 +40,17 @@ const RootWrapper = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line react/jsx-no-constructed-context-values, react/prop-types
|
// eslint-disable-next-line react/jsx-no-constructed-context-values, react/prop-types
|
||||||
<IntlProvider locale="en">
|
<MemoryRouter>
|
||||||
<AppContext.Provider value={appContextValue}>
|
<IntlProvider locale="en">
|
||||||
<ResponsiveContext.Provider value={responsiveContextValue}>
|
<AppContext.Provider value={appContextValue}>
|
||||||
<StudioHeader
|
<ResponsiveContext.Provider value={responsiveContextValue}>
|
||||||
{...props}
|
<StudioHeader
|
||||||
/>
|
{...props}
|
||||||
</ResponsiveContext.Provider>
|
/>
|
||||||
</AppContext.Provider>
|
</ResponsiveContext.Provider>
|
||||||
</IntlProvider>
|
</AppContext.Provider>
|
||||||
|
</IntlProvider>
|
||||||
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -70,6 +72,7 @@ const props = {
|
|||||||
],
|
],
|
||||||
outlineLink: 'tEsTLInK',
|
outlineLink: 'tEsTLInK',
|
||||||
searchButtonAction: null,
|
searchButtonAction: null,
|
||||||
|
isNewHomePage: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Header', () => {
|
describe('Header', () => {
|
||||||
@@ -111,16 +114,6 @@ describe('Header', () => {
|
|||||||
expect(dropdownOption).toBeVisible();
|
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 () => {
|
it('user menu should use avatar icon', async () => {
|
||||||
currentUser = { ...authenticatedUser, avatar: null };
|
currentUser = { ...authenticatedUser, avatar: null };
|
||||||
const { getByTestId } = render(<RootWrapper {...props} />);
|
const { getByTestId } = render(<RootWrapper {...props} />);
|
||||||
@@ -182,15 +175,6 @@ describe('Header', () => {
|
|||||||
expect(desktopMenu).toBeNull();
|
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 () => {
|
it('user menu should use avatar image', async () => {
|
||||||
const { getByTestId } = render(<RootWrapper {...props} />);
|
const { getByTestId } = render(<RootWrapper {...props} />);
|
||||||
const avatarImage = getByTestId('avatar-image');
|
const avatarImage = getByTestId('avatar-image');
|
||||||
|
|||||||