Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7970561181 | ||
|
|
8d46de8fe3 | ||
|
|
8341f17d46 | ||
|
|
d7c3e5a687 | ||
|
|
07b1c5bde1 | ||
|
|
5512faa9b0 | ||
|
|
48c49fe0b2 | ||
|
|
8c7778218b | ||
|
|
0dedbbd589 | ||
|
|
ef0b101fea | ||
|
|
edb22316b8 | ||
|
|
227a97afa1 | ||
|
|
d01486e5f7 | ||
|
|
a58f1eaf19 | ||
|
|
a5024c3fde | ||
|
|
d7be18e717 | ||
|
|
5e405da37e | ||
|
|
901f39f42c | ||
|
|
346a636b76 | ||
|
|
34dcc88880 | ||
|
|
a229c34535 | ||
|
|
5d7b4fecf4 | ||
|
|
f04130a7c6 | ||
|
|
cb7774b325 | ||
|
|
3e4eb21d8c | ||
|
|
a346dccd4c | ||
|
|
c64a201072 | ||
|
|
6496642643 | ||
|
|
a6c36654b4 | ||
|
|
ae5253c822 | ||
|
|
e44001e945 | ||
|
|
e07cf665a4 |
5
.github/workflows/ci.yml
vendored
@@ -9,9 +9,6 @@ on:
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: [18, 20]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -20,7 +17,7 @@ jobs:
|
||||
- name: Setup Nodejs
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
node-version-file: '.nvmrc'
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Validate package-lock.json changes
|
||||
|
||||
2
.gitignore
vendored
@@ -9,4 +9,4 @@ module.config.js
|
||||
.idea/
|
||||
|
||||
.vscode
|
||||
src/i18n/messages
|
||||
src/i18n/messages
|
||||
|
||||
14
catalog-info.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
# This file records information about this repo. Its use is described in OEP-55:
|
||||
# https://open-edx-proposals.readthedocs.io/en/latest/processes/oep-0055-proc-project-maintainers.html
|
||||
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: "frontend-component-header"
|
||||
description: "A generic header for the Open edX micro-frontend applications."
|
||||
annotations:
|
||||
openedx.org/arch-interest-groups: ""
|
||||
spec:
|
||||
owner: group:committers-frontend
|
||||
type: "library"
|
||||
lifecycle: "production"
|
||||
@@ -1,6 +1,4 @@
|
||||
@import "@edx/brand/paragon/fonts";
|
||||
@import "@edx/brand/paragon/variables";
|
||||
@import "@openedx/paragon/scss/core/core";
|
||||
@import "@edx/brand/paragon/overrides";
|
||||
@use "@openedx/paragon/dist/core.min.css" as paragonCore;
|
||||
@use "@openedx/paragon/dist/light.min.css" as paragonLight;
|
||||
|
||||
@import "@edx/frontend-component-header/index";
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
# openedx.yaml
|
||||
|
||||
---
|
||||
owner: edx/fedx-team
|
||||
tags:
|
||||
- library
|
||||
- component
|
||||
- react
|
||||
7281
package-lock.json
generated
16
package.json
@@ -35,10 +35,10 @@
|
||||
"devDependencies": {
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||
"@edx/browserslist-config": "^1.1.1",
|
||||
"@edx/frontend-platform": "8.1.1",
|
||||
"@edx/frontend-platform": "^8.3.1",
|
||||
"@edx/reactifex": "^2.1.1",
|
||||
"@openedx/frontend-build": "14.1.4",
|
||||
"@openedx/paragon": "22.7.0",
|
||||
"@openedx/frontend-build": "14.2.2",
|
||||
"@openedx/paragon": "^23.0.0",
|
||||
"@testing-library/dom": "10.4.0",
|
||||
"@testing-library/jest-dom": "5.17.0",
|
||||
"@testing-library/react": "10.4.9",
|
||||
@@ -49,7 +49,7 @@
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-redux": "7.2.9",
|
||||
"react-router-dom": "6.26.2",
|
||||
"react-router-dom": "6.28.1",
|
||||
"react-test-renderer": "17.0.2",
|
||||
"redux": "4.2.1",
|
||||
"redux-saga": "1.3.0"
|
||||
@@ -60,18 +60,20 @@
|
||||
"@fortawesome/free-regular-svg-icons": "6.6.0",
|
||||
"@fortawesome/free-solid-svg-icons": "6.6.0",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@openedx/frontend-plugin-framework": "^1.3.0",
|
||||
"@openedx/frontend-plugin-framework": "^1.6.0",
|
||||
"axios-mock-adapter": "1.22.0",
|
||||
"babel-polyfill": "6.26.0",
|
||||
"classnames": "^2.5.1",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"react-responsive": "8.2.0",
|
||||
"react-transition-group": "4.4.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@edx/frontend-platform": "^7.0.0 || ^8.0.0",
|
||||
"@openedx/paragon": ">= 21.5.7 < 23.0.0",
|
||||
"@openedx/paragon": ">= 22.0.0 < 24.0.0",
|
||||
"prop-types": "^15.5.10",
|
||||
"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 LogoSlot from './plugin-slots/LogoSlot';
|
||||
|
||||
// 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">
|
||||
<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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import DesktopHeader from './DesktopHeader';
|
||||
import MobileHeader from './MobileHeader';
|
||||
import DesktopHeaderSlot from './plugin-slots/DesktopHeaderSlot';
|
||||
import MobileHeaderSlot from './plugin-slots/MobileHeaderSlot';
|
||||
|
||||
import messages from './Header.messages';
|
||||
|
||||
@@ -123,10 +123,10 @@ const Header = ({
|
||||
return (
|
||||
<>
|
||||
<Responsive maxWidth={769}>
|
||||
<MobileHeader {...props} />
|
||||
<MobileHeaderSlot props={props} />
|
||||
</Responsive>
|
||||
<Responsive minWidth={769}>
|
||||
<DesktopHeader {...props} />
|
||||
<DesktopHeaderSlot props={props} />
|
||||
</Responsive>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -12,10 +12,12 @@ const Logo = ({
|
||||
</a>
|
||||
);
|
||||
|
||||
Logo.propTypes = {
|
||||
export const logoDataShape = {
|
||||
href: PropTypes.string.isRequired,
|
||||
src: PropTypes.string.isRequired,
|
||||
alt: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
Logo.propTypes = logoDataShape;
|
||||
|
||||
export default Logo;
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
.menu {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.menu-content {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
z-index: 10;
|
||||
background: #fff;
|
||||
background: var(--pgn-color-white, #fff);
|
||||
min-width: 10rem;
|
||||
|
||||
&.pin-left {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&.pin-right {
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.menu-dropdown-enter {
|
||||
opacity: 0;
|
||||
transform-origin: 75% 0;
|
||||
transform: scale3d(0.8, 0.8, 1);
|
||||
}
|
||||
|
||||
.menu-dropdown-enter-active {
|
||||
transform-origin: 75% 0;
|
||||
transition: all 250ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transform: scale3d(1, 1, 1);
|
||||
opacity: 1;
|
||||
}
|
||||
.menu-dropdown-enter-done {
|
||||
}
|
||||
|
||||
.menu-dropdown-exit {
|
||||
transform-origin: 75% 0;
|
||||
transform: scale3d(1, 1, 1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.menu-dropdown-exit-active {
|
||||
transform-origin: 75% 0;
|
||||
transform: scale3d(0.8, 0.8, 1);
|
||||
transition: all 250ms cubic-bezier(0.8, 0, 0.6, 1);
|
||||
opacity: 0;
|
||||
}
|
||||
.menu-dropdown-exit-done {
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -1,6 +1,9 @@
|
||||
$spacer: 1rem;
|
||||
$blue: #007db8;
|
||||
$white: #fff;
|
||||
$component-active-bg: #0A3055FF !default;
|
||||
$component-active-color: $white !default;
|
||||
$rounded-pill: 50rem !default;
|
||||
|
||||
@import './Menu/menu.scss';
|
||||
@import './studio-header/StudioHeader.scss';
|
||||
@@ -21,8 +24,9 @@ $white: #fff;
|
||||
padding: .75rem;
|
||||
justify-content: center;
|
||||
align-items:center;
|
||||
|
||||
&:hover, &:focus {
|
||||
background: rgba(0,0,0,.1);
|
||||
background: rgba(0, 0, 0, .1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,17 +40,12 @@ $white: #fff;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding-bottom: 0.1rem;
|
||||
padding-bottom: calc(var(--pgn-spacing-spacer-base, $spacer)* 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.user-dropdown {
|
||||
.btn {
|
||||
height: 3rem;
|
||||
// @media (max-width: -1 + map-get($grid-breakpoints, "sm")) {
|
||||
// padding: 0 0.5rem;
|
||||
// }
|
||||
}
|
||||
.user-dropdown .btn {
|
||||
height: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +62,7 @@ $white: #fff;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 1.5rem;
|
||||
}
|
||||
@@ -70,19 +70,22 @@ $white: #fff;
|
||||
|
||||
|
||||
.site-header-desktop {
|
||||
box-shadow: 0 1px 0 0 rgba(0,0,0,.1);
|
||||
background: $white;
|
||||
box-shadow: 0 1px 0 0 rgba(0, 0, 0, .1);
|
||||
background: var(--pgn-color-white, $white);
|
||||
|
||||
.nav-link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: block;
|
||||
box-sizing: content-box;
|
||||
position: relative;
|
||||
top: -.05em;
|
||||
height: 1.75rem;
|
||||
padding: 1rem 0;
|
||||
margin-right: 1rem;
|
||||
padding: var(--pgn-spacing-spacer-base, $spacer) 0;
|
||||
margin-right: var(--pgn-spacing-spacer-base, $spacer);
|
||||
|
||||
img {
|
||||
display: block;
|
||||
height: 100%;
|
||||
@@ -93,38 +96,42 @@ $white: #fff;
|
||||
.nav-link:focus,
|
||||
.nav-link.active,
|
||||
.expanded .nav-link {
|
||||
background: $component-active-bg;
|
||||
color: $component-active-color;
|
||||
background: var(--pgn-color-bg-active, $component-active-bg);
|
||||
color: var(--pgn-color-active, $component-active-color);
|
||||
}
|
||||
}
|
||||
.main-nav {
|
||||
.nav-link {
|
||||
padding: 1.125rem 1rem;
|
||||
padding: 1.125rem var(--pgn-spacing-spacer-base, $spacer);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
letter-spacing: .01em;
|
||||
}
|
||||
|
||||
.nav-link:hover,
|
||||
.nav-link:focus,
|
||||
.nav-link.active,
|
||||
.expanded .nav-link {
|
||||
background: $component-active-bg;
|
||||
color: $component-active-color;
|
||||
background: var(--pgn-color-bg-active, $component-active-bg);
|
||||
color: var(--pgn-color-active, $component-active-color);
|
||||
}
|
||||
|
||||
.menu {
|
||||
position: static;
|
||||
|
||||
.menu-content {
|
||||
border-top: solid 2px $component-active-bg;
|
||||
border-top: solid 2px var(--pgn-color-bg-active);
|
||||
left: 0;
|
||||
right: 0;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,.25);
|
||||
box-shadow: var(--pgn-elevation-box-shadow-down-1, 0 1px 2px rgba(0,0,0,.25));
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
padding: 1rem;
|
||||
padding: var(--pgn-spacing-spacer-base, $spacer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-input {
|
||||
border-radius: $rounded-pill;
|
||||
border-radius: var(--pgn-size-rounded-pill, $rounded-pill);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,27 +3,25 @@ import React from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@openedx/paragon';
|
||||
import LearningLoggedOutItemsSlot from '../plugin-slots/LearningLoggedOutItemsSlot';
|
||||
|
||||
import genericMessages from '../generic/messages';
|
||||
|
||||
const AnonymousUserMenu = ({ intl }) => (
|
||||
<div>
|
||||
<Button
|
||||
className="mr-3"
|
||||
variant="outline-primary"
|
||||
href={`${getConfig().LMS_BASE_URL}/register?next=${encodeURIComponent(global.location.href)}`}
|
||||
>
|
||||
{intl.formatMessage(genericMessages.registerSentenceCase)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
href={`${getLoginRedirectUrl(global.location.href)}`}
|
||||
>
|
||||
{intl.formatMessage(genericMessages.signInSentenceCase)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
const AnonymousUserMenu = ({ intl }) => {
|
||||
const buttonsInfo = [
|
||||
{
|
||||
message: intl.formatMessage(genericMessages.registerSentenceCase),
|
||||
href: `${getConfig().LMS_BASE_URL}/register?next=${encodeURIComponent(global.location.href)}`,
|
||||
},
|
||||
{
|
||||
message: intl.formatMessage(genericMessages.signInSentenceCase),
|
||||
href: getLoginRedirectUrl(global.location.href),
|
||||
variant: 'primary',
|
||||
},
|
||||
];
|
||||
|
||||
return <LearningLoggedOutItemsSlot buttonsInfo={buttonsInfo} />;
|
||||
};
|
||||
|
||||
AnonymousUserMenu.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -7,44 +7,46 @@ import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Dropdown } from '@openedx/paragon';
|
||||
|
||||
import LearningUserMenuSlot from '../plugin-slots/LearningUserMenuSlot';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const AuthenticatedUserDropdown = ({ intl, username }) => {
|
||||
const dashboardMenuItem = (
|
||||
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/dashboard`}>
|
||||
{intl.formatMessage(messages.dashboard)}
|
||||
</Dropdown.Item>
|
||||
);
|
||||
const dropdownItems = [
|
||||
{
|
||||
message: intl.formatMessage(messages.dashboard),
|
||||
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 (
|
||||
<>
|
||||
<a className="text-gray-700" href={`${getConfig().SUPPORT_URL}`}>{intl.formatMessage(messages.help)}</a>
|
||||
<Dropdown className="user-dropdown ml-3">
|
||||
<Dropdown.Toggle variant="outline-primary">
|
||||
<FontAwesomeIcon icon={faUserCircle} className="d-md-none" size="lg" />
|
||||
<span data-hj-suppress className="d-none d-md-inline">
|
||||
{username}
|
||||
</span>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu className="dropdown-menu-right">
|
||||
{dashboardMenuItem}
|
||||
<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>
|
||||
</>
|
||||
<Dropdown className="user-dropdown ml-3">
|
||||
<Dropdown.Toggle variant="outline-primary">
|
||||
<FontAwesomeIcon icon={faUserCircle} className="d-md-none" size="lg" />
|
||||
<span data-hj-suppress className="d-none d-md-inline">
|
||||
{username}
|
||||
</span>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu className="dropdown-menu-right">
|
||||
<LearningUserMenuSlot items={dropdownItems} />
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,10 @@ import { AppContext } from '@edx/frontend-platform/react';
|
||||
import AnonymousUserMenu from './AnonymousUserMenu';
|
||||
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 LearningHelpSlot from '../plugin-slots/LearningHelpSlot';
|
||||
|
||||
const LearningHeader = ({
|
||||
courseOrg, courseNumber, courseTitle, intl, showUserDropdown,
|
||||
@@ -27,14 +30,16 @@ const LearningHeader = ({
|
||||
<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">
|
||||
{headerLogo}
|
||||
<div className="flex-grow-1 course-title-lockup" style={{ lineHeight: 1 }}>
|
||||
<span className="d-block small m-0">{courseOrg} {courseNumber}</span>
|
||||
<span className="d-block m-0 font-weight-bold course-title">{courseTitle}</span>
|
||||
<div className="flex-grow-1 course-title-lockup d-flex" style={{ lineHeight: 1 }}>
|
||||
<CourseInfoSlot courseOrg={courseOrg} courseNumber={courseNumber} courseTitle={courseTitle} />
|
||||
</div>
|
||||
{showUserDropdown && authenticatedUser && (
|
||||
<AuthenticatedUserDropdown
|
||||
username={authenticatedUser.username}
|
||||
/>
|
||||
<>
|
||||
<LearningHelpSlot />
|
||||
<AuthenticatedUserDropdown
|
||||
username={authenticatedUser.username}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{showUserDropdown && !authenticatedUser && (
|
||||
<AnonymousUserMenu />
|
||||
@@ -45,9 +50,9 @@ const LearningHeader = ({
|
||||
};
|
||||
|
||||
LearningHeader.propTypes = {
|
||||
courseOrg: PropTypes.string,
|
||||
courseNumber: PropTypes.string,
|
||||
courseTitle: PropTypes.string,
|
||||
courseOrg: courseInfoDataShape.courseOrg,
|
||||
courseNumber: courseInfoDataShape.courseNumber,
|
||||
courseTitle: courseInfoDataShape.courseTitle,
|
||||
intl: intlShape.isRequired,
|
||||
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';
|
||||
|
||||
// Local Components
|
||||
import { Menu, MenuTrigger, MenuContent } from './Menu';
|
||||
import Avatar from './Avatar';
|
||||
import LogoSlot from './plugin-slots/LogoSlot';
|
||||
import { Menu, MenuTrigger, MenuContent } from '../Menu';
|
||||
import Avatar from '../Avatar';
|
||||
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
|
||||
import messages from './Header.messages';
|
||||
import messages from '../Header.messages';
|
||||
|
||||
// Assets
|
||||
import { MenuIcon } from './Icons';
|
||||
import { MenuIcon } from '../Icons';
|
||||
|
||||
class MobileHeader 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">
|
||||
<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() {
|
||||
const { mainMenu } = this.props;
|
||||
return this.renderMenu(mainMenu);
|
||||
}
|
||||
|
||||
renderSecondaryMenu() {
|
||||
const { secondaryMenu } = this.props;
|
||||
return this.renderMenu(secondaryMenu);
|
||||
const { mainMenu, secondaryMenu } = this.props;
|
||||
return <MobileMainMenuSlot menu={[...mainMenu, ...secondaryMenu]} />;
|
||||
}
|
||||
|
||||
renderUserMenuItems() {
|
||||
const { userMenu } = this.props;
|
||||
|
||||
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>
|
||||
))
|
||||
));
|
||||
return <MobileUserMenuSlot menu={userMenu} />;
|
||||
}
|
||||
|
||||
renderLoggedOutItems() {
|
||||
const { loggedOutItems } = this.props;
|
||||
|
||||
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>
|
||||
));
|
||||
return <MobileLoggedOutItemsSlot items={loggedOutItems} />;
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -149,7 +82,6 @@ class MobileHeader extends React.Component {
|
||||
className="nav flex-column pin-left pin-right border-top shadow py-2"
|
||||
>
|
||||
{this.renderMainMenu()}
|
||||
{this.renderSecondaryMenu()}
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
</div>
|
||||
@@ -179,30 +111,11 @@ class MobileHeader extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
MobileHeader.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,
|
||||
})),
|
||||
export const mobileHeaderDataShape = {
|
||||
mainMenu: mobileHeaderMainMenuDataShape,
|
||||
secondaryMenu: mobileHeaderMainMenuDataShape,
|
||||
userMenu: mobileHeaderUserMenuDataShape,
|
||||
loggedOutItems: mobileHeaderLoggedOutItemsDataShape,
|
||||
logo: PropTypes.string,
|
||||
logoAltText: PropTypes.string,
|
||||
logoDestination: PropTypes.string,
|
||||
@@ -210,6 +123,20 @@ MobileHeader.propTypes = {
|
||||
username: PropTypes.string,
|
||||
loggedIn: 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
|
||||
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 |
33
src/plugin-slots/CourseInfoSlot/index.jsx
Normal file
@@ -0,0 +1,33 @@
|
||||
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,
|
||||
}}
|
||||
pluginProps={{
|
||||
courseOrg,
|
||||
courseNumber,
|
||||
courseTitle,
|
||||
}}
|
||||
>
|
||||
<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;
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
import Logo from '../../Logo';
|
||||
import Logo, { logoDataShape } from '../../Logo';
|
||||
|
||||
const LogoSlot = ({
|
||||
href, src, alt, ...attributes
|
||||
@@ -16,10 +15,6 @@ const LogoSlot = ({
|
||||
</PluginSlot>
|
||||
);
|
||||
|
||||
LogoSlot.propTypes = {
|
||||
href: PropTypes.string.isRequired,
|
||||
src: PropTypes.string.isRequired,
|
||||
alt: PropTypes.string.isRequired,
|
||||
};
|
||||
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;
|
||||
@@ -1,3 +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 PropTypes from 'prop-types';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const BrandNav = ({
|
||||
studioBaseUrl,
|
||||
logo,
|
||||
logoAltText,
|
||||
}) => (
|
||||
<a href={studioBaseUrl}>
|
||||
<Link to={studioBaseUrl}>
|
||||
<img
|
||||
src={logo}
|
||||
alt={logoAltText}
|
||||
className="d-block logo"
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
|
||||
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,
|
||||
Tooltip,
|
||||
} from '@openedx/paragon';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const CourseLockUp = ({
|
||||
@@ -23,15 +25,15 @@ const CourseLockUp = ({
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<a
|
||||
<Link
|
||||
className="course-title-lockup mr-2"
|
||||
href={outlineLink}
|
||||
to={outlineLink}
|
||||
aria-label={intl.formatMessage(messages['header.label.courseOutline'])}
|
||||
data-testid="course-lock-up-block"
|
||||
>
|
||||
<span className="d-block small m-0 text-gray-800" data-testid="course-org-number">{org} {number}</span>
|
||||
<span className="d-block m-0 font-weight-bold text-gray-800" data-testid="course-title">{title}</span>
|
||||
</a>
|
||||
</Link>
|
||||
</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 PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
ActionRow,
|
||||
Button,
|
||||
@@ -37,6 +38,7 @@ const HeaderBody = ({
|
||||
mainMenuDropdowns,
|
||||
outlineLink,
|
||||
searchButtonAction,
|
||||
containerProps,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
@@ -50,8 +52,14 @@ const HeaderBody = ({
|
||||
/>
|
||||
);
|
||||
|
||||
const { className: containerClassName, ...restContainerProps } = containerProps || {};
|
||||
|
||||
return (
|
||||
<Container size="xl" className="px-2.5">
|
||||
<Container
|
||||
size="xl"
|
||||
className={classNames('px-2.5', containerClassName)}
|
||||
{...restContainerProps}
|
||||
>
|
||||
<ActionRow as="header">
|
||||
{isHiddenMainMenu ? (
|
||||
<Row className="flex-nowrap ml-4">
|
||||
@@ -95,7 +103,12 @@ const HeaderBody = ({
|
||||
{mainMenuDropdowns.map(dropdown => {
|
||||
const { id, buttonTitle, items } = dropdown;
|
||||
return (
|
||||
<NavDropdownMenu key={id} {...{ id, buttonTitle, items }} />
|
||||
<NavDropdownMenu
|
||||
key={id}
|
||||
{...{
|
||||
id, buttonTitle, items,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Nav>
|
||||
@@ -110,6 +123,7 @@ const HeaderBody = ({
|
||||
iconAs={Icon}
|
||||
onClick={searchButtonAction}
|
||||
aria-label={intl.formatMessage(messages['header.label.search.nav'])}
|
||||
alt={intl.formatMessage(messages['header.label.search.nav'])}
|
||||
/>
|
||||
</Nav>
|
||||
)}
|
||||
@@ -147,14 +161,15 @@ HeaderBody.propTypes = {
|
||||
isHiddenMainMenu: PropTypes.bool,
|
||||
mainMenuDropdowns: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
buttonTitle: PropTypes.string,
|
||||
buttonTitle: PropTypes.node,
|
||||
items: PropTypes.arrayOf(PropTypes.shape({
|
||||
href: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
title: PropTypes.node,
|
||||
})),
|
||||
})),
|
||||
outlineLink: PropTypes.string,
|
||||
searchButtonAction: PropTypes.func,
|
||||
containerProps: PropTypes.shape(Container.propTypes),
|
||||
};
|
||||
|
||||
HeaderBody.defaultProps = {
|
||||
@@ -174,6 +189,7 @@ HeaderBody.defaultProps = {
|
||||
mainMenuDropdowns: [],
|
||||
outlineLink: null,
|
||||
searchButtonAction: null,
|
||||
containerProps: {},
|
||||
};
|
||||
|
||||
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,
|
||||
mainMenuDropdowns: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
buttonTitle: PropTypes.string,
|
||||
buttonTitle: PropTypes.node,
|
||||
items: PropTypes.arrayOf(PropTypes.shape({
|
||||
href: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
title: PropTypes.node,
|
||||
})),
|
||||
})),
|
||||
outlineLink: PropTypes.string,
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Collapsible } from '@openedx/paragon';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const MobileMenu = ({
|
||||
mainMenuDropdowns,
|
||||
}) => (
|
||||
const MobileMenu = ({ mainMenuDropdowns }) => (
|
||||
<div
|
||||
className="ml-4 p-2 bg-light-100 border border-gray-200 small rounded"
|
||||
data-testid="mobile-menu"
|
||||
@@ -21,9 +20,9 @@ const MobileMenu = ({
|
||||
<ul className="p-0" style={{ listStyleType: 'none' }}>
|
||||
{items.map(item => (
|
||||
<li className="mobile-menu-item">
|
||||
<a href={item.href}>
|
||||
<Link to={item.href}>
|
||||
{item.title}
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -37,10 +36,10 @@ const MobileMenu = ({
|
||||
MobileMenu.propTypes = {
|
||||
mainMenuDropdowns: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
buttonTitle: PropTypes.string,
|
||||
buttonTitle: PropTypes.node,
|
||||
items: PropTypes.arrayOf(PropTypes.shape({
|
||||
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,
|
||||
DropdownButton,
|
||||
} from '@openedx/paragon';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const NavDropdownMenu = ({
|
||||
id,
|
||||
@@ -18,8 +19,9 @@ const NavDropdownMenu = ({
|
||||
>
|
||||
{items.map(item => (
|
||||
<Dropdown.Item
|
||||
as={Link}
|
||||
key={`${item.title}-dropdown-item`}
|
||||
href={item.href}
|
||||
to={item.href}
|
||||
className="small"
|
||||
>
|
||||
{item.title}
|
||||
@@ -30,10 +32,10 @@ const NavDropdownMenu = ({
|
||||
|
||||
NavDropdownMenu.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
buttonTitle: PropTypes.string.isRequired,
|
||||
buttonTitle: PropTypes.node.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.shape({
|
||||
href: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
href: PropTypes.string.isRequired,
|
||||
title: PropTypes.node.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');
|
||||
|
||||
const StudioHeader = ({
|
||||
number, org, title, isHiddenMainMenu, mainMenuDropdowns, outlineLink, searchButtonAction,
|
||||
number, org, title, containerProps, isHiddenMainMenu, mainMenuDropdowns,
|
||||
outlineLink, searchButtonAction, isNewHomePage,
|
||||
}) => {
|
||||
const { authenticatedUser, config } = useContext(AppContext);
|
||||
const props = {
|
||||
@@ -25,10 +26,11 @@ const StudioHeader = ({
|
||||
number,
|
||||
org,
|
||||
title,
|
||||
containerProps,
|
||||
username: authenticatedUser?.username,
|
||||
isAdmin: authenticatedUser?.administrator,
|
||||
authenticatedUserAvatar: authenticatedUser?.avatar,
|
||||
studioBaseUrl: config.STUDIO_BASE_URL,
|
||||
studioBaseUrl: isNewHomePage ? '/home' : config.STUDIO_BASE_URL,
|
||||
logoutUrl: config.LOGOUT_URL,
|
||||
isHiddenMainMenu,
|
||||
mainMenuDropdowns,
|
||||
@@ -53,22 +55,25 @@ StudioHeader.propTypes = {
|
||||
number: PropTypes.string,
|
||||
org: PropTypes.string,
|
||||
title: PropTypes.string.isRequired,
|
||||
containerProps: HeaderBody.propTypes.containerProps,
|
||||
isHiddenMainMenu: PropTypes.bool,
|
||||
mainMenuDropdowns: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
buttonTitle: PropTypes.string,
|
||||
buttonTitle: PropTypes.node,
|
||||
items: PropTypes.arrayOf(PropTypes.shape({
|
||||
href: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
title: PropTypes.node,
|
||||
})),
|
||||
})),
|
||||
outlineLink: PropTypes.string,
|
||||
searchButtonAction: PropTypes.func,
|
||||
isNewHomePage: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
StudioHeader.defaultProps = {
|
||||
number: '',
|
||||
org: '',
|
||||
containerProps: {},
|
||||
isHiddenMainMenu: false,
|
||||
mainMenuDropdowns: [],
|
||||
outlineLink: null,
|
||||
|
||||