Compare commits

..

3 Commits

Author SHA1 Message Date
Syed Ali Abbas Zaidi
0f3c4d50b5 temp: testing react router support 2023-08-16 12:42:35 +05:00
Syed Ali Abbas Zaidi
aae3c2553b Merge branch 'master' of github.com:openedx/frontend-component-header into Ali-Abbas/upgrade-react-router 2023-08-08 15:08:32 +05:00
Syed Ali Abbas Zaidi
55fe69473d feat: upgrade react router to v6 2023-03-16 13:36:37 +05:00
140 changed files with 14850 additions and 9950 deletions

View File

@@ -1,4 +1,4 @@
// eslint-disable-next-line import/no-extraneous-dependencies
const { createConfig } = require('@openedx/frontend-build');
const { createConfig } = require('@edx/frontend-build');
module.exports = createConfig('eslint');

View File

@@ -11,13 +11,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- name: Setup Nodejs
uses: actions/setup-node@v4
uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
node-version: ${{ env.NODE_VER }}
- name: Install dependencies
run: npm ci
- name: Validate package-lock.json changes
@@ -31,7 +33,4 @@ jobs:
- name: i18n_extract
run: npm run i18n_extract
- name: Coverage
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
uses: codecov/codecov-action@v3

View File

@@ -3,20 +3,19 @@ on:
push:
branches:
- master
- alpha
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VER }}
- name: Install dependencies
@@ -30,10 +29,7 @@ jobs:
- name: i18n_extract
run: npm run i18n_extract
- name: Coverage
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
uses: codecov/codecov-action@v3
- name: Build
run: npm run build
- name: Release

1
.gitignore vendored
View File

@@ -9,4 +9,3 @@ module.config.js
.idea/
.vscode
src/i18n/messages

2
.nvmrc
View File

@@ -1 +1 @@
20
18

View File

@@ -1,8 +1,5 @@
{
"branches": [
"master",
{name: "alpha", prerelease: true}
],
"branch": "master",
"tagFormat": "v${version}",
"verifyConditions": [
"@semantic-release/npm",

9
.tx/config Normal file
View File

@@ -0,0 +1,9 @@
[main]
host = https://www.transifex.com
[o:open-edx:p:edx-platform:r:frontend-component-header]
file_filter = src/i18n/messages/<lang>.json
source_file = src/i18n/transifex_input.json
source_lang = en
type = KEYVALUEJSON

View File

@@ -1,9 +1,12 @@
export TRANSIFEX_RESOURCE = frontend-component-header
transifex_langs = "ar,fr,es_419,zh_CN,pt,it,de,uk,ru,hi,fr_CA"
transifex_utils = ./node_modules/.bin/transifex-utils.js
i18n = ./src/i18n
transifex_input = $(i18n)/transifex_input.json
# This directory must match .babelrc .
transifex_temp = ./temp/babel-plugin-formatjs
transifex_temp = ./temp/babel-plugin-react-intl
build:
rm -rf ./dist
@@ -14,7 +17,7 @@ build:
@rm -rf dist/__mocks__
requirements:
npm ci
npm install
i18n.extract:
# Pulling display strings from .jsx files into .json files...
@@ -32,6 +35,21 @@ detect_changed_source_translations:
# Checking for changed translations...
git diff --exit-code $(i18n)
# Pushes translations to Transifex. You must run make extract_translations first.
push_translations:
# Pushing strings to Transifex...
tx push -s
# Fetching hashes from Transifex...
./node_modules/@edx/reactifex/bash_scripts/get_hashed_strings_v3.sh
# Writing out comments to file...
$(transifex_utils) $(transifex_temp) --comments --v3-scripts-path
# Pushing comments to Transifex...
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
# Pulls translations from Transifex.
pull_translations:
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
# This target is used by Travis.
validate-no-uncommitted-package-lock-changes:
# Checking for package-lock.json changes...

View File

@@ -2,42 +2,22 @@
frontend-component-header
#########################
|license| |Build Status| |Codecov| |npm_version| |npm_downloads| |semantic-release|
|Build Status| |Codecov| |npm_version| |npm_downloads| |license| |semantic-release|
********
Purpose
Overview
********
A generic header for Open edX micro-frontend applications.
************
Getting Started
************
Prerequisites
=============
The `devstack`_ is currently recommended as a development environment for your
new MFE. If you start it with ``make dev.up.lms`` that should give you
everything you need as a companion to this frontend.
Note that it is also possible to use `Tutor`_ to develop an MFE. You can refer
to the `relevant tutor-mfe documentation`_ to get started using it.
.. _Devstack: https://github.com/openedx/devstack
.. _Tutor: https://github.com/overhangio/tutor
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe#mfe-development
Requirements
============
************
This component uses ``@edx/frontend-platform`` services such as i18n, analytics, configuration, and the ``AppContext`` React component, and expects that it has been loaded into a micro-frontend that has been properly initialized via ``@edx/frontend-platform``'s ``initialize`` function. `Please visit the frontend template application to see an example. <https://github.com/openedx/frontend-template-application/blob/master/src/index.jsx>`_
Environment Variables
====================
=====================
* ``LMS_BASE_URL`` - The URL of the LMS of your Open edX instance.
* ``LOGOUT_URL`` - The URL of the API endpoint which performs a user logout.
@@ -52,8 +32,9 @@ Environment Variables
menu items when truthy. This is intended to be used in micro-frontends like
frontend-app-authentication in which these menus are considered distractions from the user's task.
************
Installation
============
************
To install this header into your Open edX micro-frontend, run the following command in your MFE:
@@ -61,33 +42,9 @@ To install this header into your Open edX micro-frontend, run the following comm
This will make the component available to be imported into your application.
Cloning and Startup
===================
.. code-block::
1. Clone your new repo:
``git clone https://github.com/openedx/frontend-component-header.git``
2. Use node v18.x.
The current version of the micro-frontend build scripts support node 18.
Using other major versions of node *may* work, but this is unsupported. For
convenience, this repository includes an .nvmrc file to help in setting the
correct node version via `nvm <https://github.com/nvm-sh/nvm>`_.
3. Install npm dependencies:
``cd frontend-component-header && npm ci``
4. Start the dev server:
``npm start``
*****
Usage
=====
*****
This library has the following exports:
@@ -95,20 +52,16 @@ This library has the following exports:
* ``messages``: Internationalization messages suitable for use with `@edx/frontend-platform/i18n <https://edx.github.io/frontend-platform/module-Internationalization.html>`_
* ``dist/index.scss``: A SASS file which contains style information for the component. It should be imported into the micro-frontend's own SCSS file.
Plugins
-------
This can be customized using `Frontend Plugin Framework <https://github.com/openedx/frontend-plugin-framework>`_.
The parts of this that can be customized in that manner are documented `here </src/plugin-slots>`_.
Examples
========
* `An example of component and messages usage. <https://github.com/openedx/frontend-template-application/blob/3355bb3a96232390e9056f35b06ffa8f105ed7ca/src/index.jsx#L21>`_
* `An example of SCSS file usage. <https://github.com/openedx/frontend-template-application/blob/3cd5485bf387b8c479baf6b02bf59e3061dc3465/src/index.scss#L8>`_
***********
Development
===========
***********
Install dependencies::
@@ -122,63 +75,6 @@ Build a production distribution::
npm run build
License
=======
The code in this repository is licensed under the AGPLv3 unless otherwise
noted.
Please see `LICENSE <LICENSE>`_ for details.
Contributing
============
Contributions are very welcome. Please read `How To Contribute`_ for details.
.. _How To Contribute: https://openedx.org/r/how-to-contribute
This project is currently accepting all types of contributions, bug fixes,
security fixes, maintenance work, or new features. However, please make sure
to have a discussion about your new feature idea with the maintainers prior to
beginning development to maximize the chances of your change being accepted.
You can start a conversation by creating a new issue on this repo summarizing
your idea.
Getting Help
===========
If you're having trouble, we have discussion forums at
https://discuss.openedx.org where you can connect with others in the community.
Our real-time conversations are on Slack. You can request a `Slack
invitation`_, then join our `community Slack workspace`_. Because this is a
frontend repository, the best place to discuss it would be in the `#wg-frontend
channel`_.
For anything non-trivial, the best path is to open an issue in this repository
with as many details about the issue you are facing as you can provide.
https://github.com/openedx/frontend-component-header/issues
For more information about these options, see the `Getting Help`_ page.
.. _Slack invitation: https://openedx.org/slack
.. _community Slack workspace: https://openedx.slack.com/
.. _#wg-frontend channel: https://openedx.slack.com/archives/C04BM6YC7A6
.. _Getting Help: https://openedx.org/community/connect
The Open edX Code of Conduct
============================
All community members are expected to follow the `Open edX Code of Conduct`_.
.. _Open edX Code of Conduct: https://openedx.org/code-of-conduct/
Reporting Security Issues
=========================
Please do not report security issues in public. Please email security@openedx.org.
.. |Build Status| image:: https://api.travis-ci.com/edx/frontend-component-header.svg?branch=master
:target: https://travis-ci.com/edx/frontend-component-header
.. |Codecov| image:: https://img.shields.io/codecov/c/github/edx/frontend-component-header
@@ -190,4 +86,4 @@ Please do not report security issues in public. Please email security@openedx.or
.. |license| image:: https://img.shields.io/npm/l/@edx/frontend-component-header.svg
:target: @edx/frontend-component-header
.. |semantic-release| image:: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg
:target: https://github.com/semantic-release/semantic-release
:target: https://github.com/semantic-release/semantic-release

View File

@@ -1,3 +1,3 @@
const { createConfig } = require('@openedx/frontend-build');
const { createConfig } = require('@edx/frontend-build');
module.exports = createConfig('babel-preserve-modules');

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -1,111 +0,0 @@
.. title:: Custom Header Component Documentation
Custom Header Component
=======================
Overview
--------
The ``Header`` component is used to display a header with a provided ``mainMenuItems``,
``secondaryMenuItems``, and ``userMenuItems`` props. If props are provided, the component will use them; otherwise,
if any of the props ``(mainMenuItems, secondaryMenuItems, userMenuItems)`` are not provided, default
items will be displayed. This component provides flexibility in customization, making it suitable for a wide
range of applications.
Props Details
-------------
The `Header` component accepts the following **optional** props for customization:
``mainMenuItems``
*****************
The main menu items is a list of menu items objects. On desktop screens, these items are displayed on the left side next to the logo icon.
On mobile screens, the main menu is displayed as a dropdown menu triggered by a hamburger icon. The main menu dropdown appears below the logo when opened.
Example:
::
[
{ type: 'item', href: '/courses', content: 'Courses', isActive: true },
{ type: 'item', href: '/programs', content: 'Programs' },
{ type: 'item', href: '/discover', content: 'Discover New', disabled, true },
{
type: 'submenu',
content: 'Sub Menu Item',
submenuContent: (
<>
<div className="mb-1"><a rel="noopener" href="#">Submenu item 1</a></div>
<div className="mb-1"><a rel="noopener" href="#">Submenu item 2</a></div>
</>
),
},
]
**Submenu Implementation**
To implement a submenu, set the type to ``submenu`` and provide a ``submenuContent`` property.
The submenuContent should be a React component (as shown in above example) that can be rendered.
**Note:**
- The ``type`` should be ``item`` or ``submenu``. If type is ``submenu``, it should contain ``submenuContent`` instead of ``href``.
- If any item is to be disabled, we can pass optional ``disabled: true`` in that item object and
- If any item is to be active, we can pass optional ``isActive: true`` in that item object
secondaryMenuItems
******************
The secondary menu items has same structure as ``mainMenuItems``. On desktop screen, these items are displayed on the right of header just before the userMenu avatar and on mobile screen,
these items are displayed below the mainMenu items in dropdown.
Example:
::
[
{ type: 'item', href: '/help', content: 'Help' },
]
userMenuItems
*************
The user menu items is list of objects. On desktop screens, these items are displayed as a dropdown menu on the most right side of the header. The dropdown is opened by clicking on the avatar icon, which is typically located at the far right of the header.
On mobile screens, the user menu is also displayed as a dropdown menu, appearing under the avatar icon.
Each object represents a group in the user menu. Each object contains the ``heading`` and
list of menu items to be displayed in that group. Heading is optional and will be displayed only if passed. There can
be multiple groups. For a normal user menu, a single group can be passed with empty heading.
Example:
::
[
{
heading: '',
items: [
{ type: 'item', href: '/profile', content: 'Profile' },
{ type: 'item', href: '/logout', content: 'Logout' }
]
},
]
Screenshots
***********
Desktop:
.. image:: ./images/desktop_header.png
Mobile:
.. image:: ./images/mobile_main_menu.png
.. image:: ./images/mobile_user_menu.png
Some Important Notes
--------------------
- Intl formatted strings should be passed in content attribute.
- Only menu items in the main menu can be disabled.
- Menu items in the main menu and user menu can have ``isActive`` prop.

View File

@@ -7,7 +7,6 @@ import { AppContext, AppProvider } from '@edx/frontend-platform/react';
import Header from '@edx/frontend-component-header';
import './index.scss';
import StudioHeader from '../src/studio-header/StudioHeader';
subscribe(APP_READY, () => {
ReactDOM.render(
@@ -33,35 +32,7 @@ subscribe(APP_READY, () => {
}}>
<Header />
</AppContext.Provider>
<h5 className="mt-2 mb-5">Logged in state</h5>
<AppContext.Provider value={{
authenticatedUser: {
userId: '123abc',
username: 'testuser',
roles: [],
administrator: false,
},
config: getConfig(),
}}>
<StudioHeader
number="run123"
org="testX"
title="Course Name"
isHiddenMainMenu={false}
mainMenuDropdowns={[
{
id: 'content-dropdown',
buttonTitle: 'Content',
items: [{
href: '#',
title: 'Outline',
}],
},
]}
outlineLink="#"
/>
</AppContext.Provider>
<h5 className="mt-2">Logged in state for Studio header</h5>
<h5 className="mt-2">Logged in state</h5>
</AppProvider>,
document.getElementById('root'),
);

View File

@@ -1,6 +1,6 @@
@import "@edx/brand/paragon/fonts";
@import "@edx/brand/paragon/variables";
@import "@openedx/paragon/scss/core/core";
@import "@edx/paragon/scss/core/core";
@import "@edx/brand/paragon/overrides";
@import "@edx/frontend-component-header/index";

View File

@@ -1,4 +1,4 @@
const { createConfig } = require('@openedx/frontend-build');
const { createConfig } = require('@edx/frontend-build');
module.exports = createConfig('jest', {
setupFilesAfterEnv: [

19236
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@
},
"scripts": {
"build": "make build",
"i18n_extract": "fedx-scripts formatjs extract",
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
"snapshot": "fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress",
@@ -33,47 +33,44 @@
},
"homepage": "https://github.com/openedx/frontend-component-header#readme",
"devDependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
"@edx/browserslist-config": "^1.1.1",
"@edx/frontend-platform": "8.1.2",
"@edx/frontend-build": "12.9.4",
"@edx/frontend-platform": "5.0.0",
"@edx/reactifex": "^2.1.1",
"@openedx/frontend-build": "14.1.5",
"@openedx/paragon": "22.10.0",
"@testing-library/dom": "10.4.0",
"@testing-library/dom": "9.3.1",
"@testing-library/jest-dom": "5.17.0",
"@testing-library/react": "10.4.9",
"@wojtekmaj/enzyme-adapter-react-17": "0.8.0",
"enzyme": "3.11.0",
"husky": "8.0.3",
"jest": "29.7.0",
"jest": "29.6.2",
"jest-chain": "1.1.6",
"prop-types": "15.8.1",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-redux": "7.2.9",
"react-router-dom": "6.28.0",
"react-router-dom": "6.14.2",
"react-test-renderer": "17.0.2",
"redux": "4.2.1",
"redux-saga": "1.3.0"
"redux-saga": "1.2.3"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "6.6.0",
"@fortawesome/free-brands-svg-icons": "6.6.0",
"@fortawesome/free-regular-svg-icons": "6.6.0",
"@fortawesome/free-solid-svg-icons": "6.6.0",
"@edx/paragon": "20.45.5",
"@fortawesome/fontawesome-svg-core": "6.4.0",
"@fortawesome/free-brands-svg-icons": "6.4.0",
"@fortawesome/free-regular-svg-icons": "6.4.0",
"@fortawesome/free-solid-svg-icons": "6.4.0",
"@fortawesome/react-fontawesome": "^0.2.0",
"@openedx/frontend-plugin-framework": "^1.3.0",
"axios-mock-adapter": "1.22.0",
"axios-mock-adapter": "1.21.5",
"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",
"@edx/frontend-platform": "^4.0.0 || ^5.0.0",
"prop-types": "^15.5.10",
"react": "^16.9.0 || ^17.0.0",
"react-dom": "^16.9.0 || ^17.0.0",
"react-router-dom": "^6.14.2"
"react-dom": "^16.9.0 || ^17.0.0"
}
}

View File

@@ -24,7 +24,7 @@
"automerge": true
},
{
"matchPackagePatterns": ["@edx", "@openedx"],
"matchPackagePatterns": ["@edx"],
"matchUpdateTypes": ["minor", "patch"],
"automerge": true
}

214
src/DesktopHeader.jsx Normal file
View File

@@ -0,0 +1,214 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
// Local Components
import { Menu, MenuTrigger, MenuContent } from './Menu';
import Avatar from './Avatar';
import { LinkedLogo, Logo } from './Logo';
// i18n
import messages from './Header.messages';
// Assets
import { CaretIcon } from './Icons';
class DesktopHeader extends React.Component {
constructor(props) { // eslint-disable-line no-useless-constructor
super(props);
}
renderMainMenu() {
const { mainMenu } = this.props;
// Nodes are accepted as a prop
if (!Array.isArray(mainMenu)) {
return mainMenu;
}
return mainMenu.map((menuItem) => {
const {
type,
href,
content,
submenuContent,
} = menuItem;
if (type === 'item') {
return (
<a key={`${type}-${content}`} className="nav-link" href={href}>{content}</a>
);
}
return (
<Menu key={`${type}-${content}`} tag="div" className="nav-item" respondToPointerEvents>
<MenuTrigger 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>
);
});
}
// Renders an optional App Menu for
renderAppMenu() {
const { appMenu } = this.props;
const { content: appMenuContent, menuItems } = appMenu;
return (
<Menu transitionClassName="menu-dropdown" transitionTimeout={250}>
<MenuTrigger tag="a" className="nav-link d-inline-flex align-items-center">
{appMenuContent} <CaretIcon role="img" aria-hidden focusable="false" />
</MenuTrigger>
<MenuContent className="mb-0 dropdown-menu show dropdown-menu-right pin-right shadow py-2">
{menuItems.map(({ type, href, content }) => (
<a className={`dropdown-${type}`} key={`${type}-${content}`} href={href}>{content}</a>
))}
</MenuContent>
</Menu>
);
}
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(({ type, href, content }) => (
<a className={`dropdown-${type}`} key={`${type}-${content}`} href={href}>{content}</a>
))}
</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,
appMenu,
} = this.props;
const logoProps = { src: logo, alt: logoAltText, href: logoDestination };
const logoClasses = getConfig().AUTHN_MINIMAL_HEADER ? 'mw-100' : null;
return (
<header className="site-header-desktop">
<a className="nav-skip sr-only sr-only-focusable" href="#main">{intl.formatMessage(messages['header.label.skip.nav'])}</a>
<div className={`container-fluid ${logoClasses}`}>
<div className="nav-container position-relative d-flex align-items-center">
{logoDestination === null ? <Logo className="logo" src={logo} alt={logoAltText} /> : <LinkedLogo className="logo" {...logoProps} />}
<nav
aria-label={intl.formatMessage(messages['header.label.main.nav'])}
className="nav main-nav"
>
{this.renderMainMenu()}
</nav>
{appMenu ? (
<nav
aria-label={intl.formatMessage(messages['header.label.app.nav'])}
className="nav app-nav"
>
{this.renderAppMenu()}
</nav>
) : null}
<nav
aria-label={intl.formatMessage(messages['header.label.secondary.nav'])}
className="nav secondary-menu-container align-items-center ml-auto"
>
{loggedIn ? this.renderUserMenu() : this.renderLoggedOutItems()}
</nav>
</div>
</div>
</header>
);
}
}
DesktopHeader.propTypes = {
mainMenu: PropTypes.oneOfType([
PropTypes.node,
PropTypes.array,
]),
userMenu: PropTypes.arrayOf(PropTypes.shape({
type: PropTypes.oneOf(['item', 'menu']),
href: PropTypes.string,
content: PropTypes.string,
})),
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,
// appMenu
appMenu: PropTypes.shape(
{
content: PropTypes.string,
menuItems: PropTypes.arrayOf(
PropTypes.shape({
type: PropTypes.string,
href: PropTypes.string,
content: PropTypes.string,
}),
),
},
),
};
DesktopHeader.defaultProps = {
mainMenu: [],
userMenu: [],
loggedOutItems: [],
logo: null,
logoAltText: null,
logoDestination: null,
avatar: null,
username: null,
loggedIn: false,
appMenu: null,
};
export default injectIntl(DesktopHeader);

View File

@@ -10,9 +10,8 @@ import {
subscribe,
} from '@edx/frontend-platform';
import PropTypes from 'prop-types';
import DesktopHeaderSlot from './plugin-slots/DesktopHeaderSlot';
import MobileHeaderSlot from './plugin-slots/MobileHeaderSlot';
import DesktopHeader from './DesktopHeader';
import MobileHeader from './MobileHeader';
import messages from './Header.messages';
@@ -31,68 +30,50 @@ subscribe(APP_CONFIG_INITIALIZED, () => {
}, 'Header additional config');
});
/**
* Header component for the application.
* Displays a header with the provided main menu, secondary menu, and user menu when the user is authenticated.
* If any of the props (mainMenuItems, secondaryMenuItems, userMenuItems) are not provided, default
* items are displayed.
* For more details on how to use this component, please refer to this document:
* https://github.com/openedx/frontend-component-header/blob/master/docs/using_custom_header.rst
*
* @param {list} mainMenuItems - The list of main menu items to display.
* See the documentation for the structure of main menu item.
* @param {list} secondaryMenuItems - The list of secondary menu items to display.
* See the documentation for the structure of secondary menu item.
* @param {list} userMenuItems - The list of user menu items to display.
* See the documentation for the structure of user menu item.
*/
const Header = ({
intl, mainMenuItems, secondaryMenuItems, userMenuItems,
}) => {
const Header = ({ intl }) => {
const { authenticatedUser, config } = useContext(AppContext);
const defaultMainMenu = [
const mainMenu = [
{
type: 'item',
href: `${config.LMS_BASE_URL}/dashboard`,
content: intl.formatMessage(messages['header.links.courses']),
},
];
const defaultUserMenu = authenticatedUser === null ? [] : [{
heading: '',
items: [
{
type: 'item',
href: `${config.LMS_BASE_URL}/dashboard`,
content: intl.formatMessage(messages['header.user.menu.dashboard']),
},
{
type: 'item',
href: `${config.ACCOUNT_PROFILE_URL}/u/${authenticatedUser.username}`,
content: intl.formatMessage(messages['header.user.menu.profile']),
},
{
type: 'item',
href: config.ACCOUNT_SETTINGS_URL,
content: intl.formatMessage(messages['header.user.menu.account.settings']),
},
// Users should only see Order History if have a ORDER_HISTORY_URL define in the environment.
...(config.ORDER_HISTORY_URL ? [{
type: 'item',
href: config.ORDER_HISTORY_URL,
content: intl.formatMessage(messages['header.user.menu.order.history']),
}] : []),
{
type: 'item',
href: config.LOGOUT_URL,
content: intl.formatMessage(messages['header.user.menu.logout']),
},
],
}];
const mainMenu = mainMenuItems || defaultMainMenu;
const secondaryMenu = secondaryMenuItems || [];
const userMenu = authenticatedUser === null ? [] : userMenuItems || defaultUserMenu;
const orderHistoryItem = {
type: 'item',
href: config.ORDER_HISTORY_URL,
content: intl.formatMessage(messages['header.user.menu.order.history']),
};
const userMenu = authenticatedUser === null ? [] : [
{
type: 'item',
href: `${config.LMS_BASE_URL}/dashboard`,
content: intl.formatMessage(messages['header.user.menu.dashboard']),
},
{
type: 'item',
href: `${config.ACCOUNT_PROFILE_URL}/u/${authenticatedUser.username}`,
content: intl.formatMessage(messages['header.user.menu.profile']),
},
{
type: 'item',
href: config.ACCOUNT_SETTINGS_URL,
content: intl.formatMessage(messages['header.user.menu.account.settings']),
},
{
type: 'item',
href: config.LOGOUT_URL,
content: intl.formatMessage(messages['header.user.menu.logout']),
},
];
// Users should only see Order History if have a ORDER_HISTORY_URL define in the environment.
if (config.ORDER_HISTORY_URL) {
userMenu.splice(-1, 0, orderHistoryItem);
}
const loggedOutItems = [
{
@@ -115,48 +96,24 @@ const Header = ({
username: authenticatedUser !== null ? authenticatedUser.username : null,
avatar: authenticatedUser !== null ? authenticatedUser.avatar : null,
mainMenu: getConfig().AUTHN_MINIMAL_HEADER ? [] : mainMenu,
secondaryMenu: getConfig().AUTHN_MINIMAL_HEADER ? [] : secondaryMenu,
userMenu: getConfig().AUTHN_MINIMAL_HEADER ? [] : userMenu,
loggedOutItems: getConfig().AUTHN_MINIMAL_HEADER ? [] : loggedOutItems,
};
return (
<>
<Responsive maxWidth={769}>
<MobileHeaderSlot props={props} />
<Responsive maxWidth={768}>
<MobileHeader {...props} />
</Responsive>
<Responsive minWidth={769}>
<DesktopHeaderSlot props={props} />
<DesktopHeader {...props} />
</Responsive>
</>
);
};
Header.defaultProps = {
mainMenuItems: null,
secondaryMenuItems: null,
userMenuItems: null,
};
Header.propTypes = {
intl: intlShape.isRequired,
mainMenuItems: PropTypes.oneOfType([
PropTypes.node,
PropTypes.array,
]),
secondaryMenuItems: PropTypes.oneOfType([
PropTypes.node,
PropTypes.array,
]),
userMenuItems: 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,
})),
})),
};
export default injectIntl(Header);

View File

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

View File

@@ -1,23 +1,31 @@
import React from 'react';
import PropTypes from 'prop-types';
const Logo = ({
const Logo = ({ src, alt, ...attributes }) => (
<img src={src} alt={alt} {...attributes} />
);
Logo.propTypes = {
src: PropTypes.string.isRequired,
alt: PropTypes.string.isRequired,
};
const LinkedLogo = ({
href,
src,
alt,
...attributes
}) => (
<a href={href} className="logo" {...attributes}>
<a href={href} {...attributes}>
<img className="d-block" src={src} alt={alt} />
</a>
);
export const logoDataShape = {
LinkedLogo.propTypes = {
href: PropTypes.string.isRequired,
src: PropTypes.string.isRequired,
alt: PropTypes.string.isRequired,
};
Logo.propTypes = logoDataShape;
export { LinkedLogo, Logo };
export default Logo;

View File

@@ -14,8 +14,7 @@ MenuTrigger.defaultProps = {
tag: 'div',
className: null,
};
const MenuTriggerComp = <MenuTrigger />;
const MenuTriggerType = MenuTriggerComp.type;
const MenuTriggerType = <MenuTrigger />.type;
const MenuContent = ({ tag, className, ...attributes }) => React.createElement(tag, {
className: ['menu-content', className].join(' '),

View File

@@ -4,21 +4,15 @@ 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 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';
import { Menu, MenuTrigger, MenuContent } from './Menu';
import Avatar from './Avatar';
import { LinkedLogo, Logo } from './Logo';
// 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
@@ -26,18 +20,65 @@ class MobileHeader extends React.Component {
}
renderMainMenu() {
const { mainMenu, secondaryMenu } = this.props;
return <MobileMainMenuSlot menu={[...mainMenu, ...secondaryMenu]} />;
const { mainMenu } = this.props;
// Nodes are accepted as a prop
if (!Array.isArray(mainMenu)) {
return mainMenu;
}
return mainMenu.map((menuItem) => {
const {
type,
href,
content,
submenuContent,
} = menuItem;
if (type === 'item') {
return (
<a key={`${type}-${content}`} className="nav-link" href={href}>
{content}
</a>
);
}
return (
<Menu key={`${type}-${content}`} tag="div" className="nav-item">
<MenuTrigger tag="a" role="button" tabIndex="0" className="nav-link">
{content}
</MenuTrigger>
<MenuContent className="position-static pin-left pin-right py-2">
{submenuContent}
</MenuContent>
</Menu>
);
});
}
renderUserMenuItems() {
const { userMenu } = this.props;
return <MobileUserMenuSlot menu={userMenu} />;
return userMenu.map(({ type, href, content }) => (
<li className="nav-item" key={`${type}-${content}`}>
<a className="nav-link" href={href}>{content}</a>
</li>
));
}
renderLoggedOutItems() {
const { loggedOutItems } = this.props;
return <MobileLoggedOutItemsSlot items={loggedOutItems} />;
return loggedOutItems.map(({ type, href, content }, i, arr) => (
<li className="nav-item px-3 my-2" key={`${type}-${content}`}>
<a
className={i < arr.length - 1 ? 'btn btn-block btn-outline-primary' : 'btn btn-block btn-primary'}
href={href}
>
{content}
</a>
</li>
));
}
render() {
@@ -87,7 +128,7 @@ class MobileHeader extends React.Component {
</div>
) : null}
<div className={`w-100 d-flex ${logoClasses}`}>
<LogoSlot {...logoProps} itemType="http://schema.org/Organization" />
{ logoDestination === null ? <Logo className="logo" src={logo} alt={logoAltText} /> : <LinkedLogo className="logo" {...logoProps} itemType="http://schema.org/Organization" />}
</div>
{userMenu.length > 0 || loggedOutItems.length > 0 ? (
<div className="w-100 d-flex justify-content-end align-items-center">
@@ -111,11 +152,22 @@ class MobileHeader extends React.Component {
}
}
export const mobileHeaderDataShape = {
mainMenu: mobileHeaderMainMenuDataShape,
secondaryMenu: mobileHeaderMainMenuDataShape,
userMenu: mobileHeaderUserMenuDataShape,
loggedOutItems: mobileHeaderLoggedOutItemsDataShape,
MobileHeader.propTypes = {
mainMenu: PropTypes.oneOfType([
PropTypes.node,
PropTypes.array,
]),
userMenu: PropTypes.arrayOf(PropTypes.shape({
type: PropTypes.oneOf(['item', 'menu']),
href: PropTypes.string,
content: PropTypes.string,
})),
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,
@@ -123,20 +175,6 @@ export const mobileHeaderDataShape = {
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,
@@ -144,7 +182,6 @@ MobileHeader.propTypes = {
MobileHeader.defaultProps = {
mainMenu: [],
secondaryMenu: [],
userMenu: [],
loggedOutItems: [],
logo: null,

200
src/StudioHeader.jsx Normal file
View File

@@ -0,0 +1,200 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import {
APP_CONFIG_INITIALIZED,
ensureConfig,
getConfig,
mergeConfig,
subscribe,
} from '@edx/frontend-platform';
import { ActionRow } from '@edx/paragon';
import { Menu, MenuTrigger, MenuContent } from './Menu';
import Avatar from './Avatar';
import { LinkedLogo, Logo } from './Logo';
import { CaretIcon } from './Icons';
import messages from './Header.messages';
ensureConfig([
'STUDIO_BASE_URL',
'LOGOUT_URL',
'LOGIN_URL',
'SITE_NAME',
'LOGO_URL',
'ORDER_HISTORY_URL',
], 'StudioHeader component');
subscribe(APP_CONFIG_INITIALIZED, () => {
mergeConfig({
AUTHN_MINIMAL_HEADER: !!process.env.AUTHN_MINIMAL_HEADER,
}, 'StudioHeader additional config');
});
class StudioDesktopHeaderBase extends React.Component {
constructor(props) { // eslint-disable-line no-useless-constructor
super(props);
}
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(({ type, href, content }) => (
<a className={`dropdown-${type}`} key={`${type}-${content}`} href={href}>{content}</a>
))}
</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,
actionRowContent,
} = this.props;
const logoProps = { src: logo, alt: logoAltText, href: logoDestination };
const logoClasses = getConfig().AUTHN_MINIMAL_HEADER ? 'mw-100' : null;
return (
<header className="site-header-desktop">
<a className="nav-skip sr-only sr-only-focusable" href="#main">{intl.formatMessage(messages['header.label.skip.nav'])}</a>
<div className={`container-fluid ${logoClasses}`}>
<div className="nav-container position-relative d-flex align-items-center">
{logoDestination === null ? <Logo className="logo" src={logo} alt={logoAltText} /> : <LinkedLogo className="logo" {...logoProps} />}
<ActionRow>
{actionRowContent}
<nav
aria-label={intl.formatMessage(messages['header.label.secondary.nav'])}
className="nav secondary-menu-container align-items-center ml-auto"
>
{loggedIn ? this.renderUserMenu() : this.renderLoggedOutItems()}
</nav>
</ActionRow>
</div>
</div>
</header>
);
}
}
StudioDesktopHeaderBase.propTypes = {
userMenu: PropTypes.arrayOf(PropTypes.shape({
type: PropTypes.oneOf(['item', 'menu']),
href: PropTypes.string,
content: PropTypes.string,
})),
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,
actionRowContent: PropTypes.element,
// i18n
intl: intlShape.isRequired,
};
StudioDesktopHeaderBase.defaultProps = {
userMenu: [],
loggedOutItems: [],
logo: null,
logoAltText: null,
logoDestination: null,
avatar: null,
username: null,
loggedIn: false,
actionRowContent: null,
};
const StudioDesktopHeader = injectIntl(StudioDesktopHeaderBase);
const StudioHeader = ({ intl, actionRowContent }) => {
const { authenticatedUser, config } = useContext(AppContext);
const userMenu = authenticatedUser === null ? [] : [
{
type: 'item',
href: `${config.STUDIO_BASE_URL}`,
content: intl.formatMessage(messages['header.user.menu.studio.home']),
},
{
type: 'item',
href: `${config.STUDIO_BASE_URL}/maintenance`,
content: intl.formatMessage(messages['header.user.menu.studio.maintenance']),
},
{
type: 'item',
href: config.LOGOUT_URL,
content: intl.formatMessage(messages['header.user.menu.logout']),
},
];
const props = {
logo: config.LOGO_URL,
logoAltText: config.SITE_NAME,
logoDestination: config.STUDIO_BASE_URL,
loggedIn: authenticatedUser !== null,
username: authenticatedUser !== null ? authenticatedUser.username : null,
avatar: authenticatedUser !== null ? authenticatedUser.avatar : null,
actionRowContent,
userMenu,
loggedOutItems: [],
};
return <StudioDesktopHeader {...props} />;
};
StudioHeader.propTypes = {
intl: intlShape.isRequired,
actionRowContent: PropTypes.element,
};
StudioHeader.defaultProps = {
// eslint-disable-next-line react/jsx-no-useless-fragment
actionRowContent: <></>,
};
export default injectIntl(StudioHeader);

108
src/StudioHeader.test.jsx Normal file
View File

@@ -0,0 +1,108 @@
/* eslint-disable react/prop-types */
import React, { useMemo } from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import TestRenderer from 'react-test-renderer';
import { Link } from 'react-router-dom';
import { AppContext } from '@edx/frontend-platform/react';
import {
ActionRow,
Button,
Dropdown,
} from '@edx/paragon';
import { StudioHeader } from './index';
const StudioHeaderComponent = ({ contextValue, appMenu = null, mainMenu = [] }) => (
<IntlProvider locale="en" messages={{}}>
<AppContext.Provider
value={contextValue}
>
<StudioHeader appMenu={appMenu} mainMenu={mainMenu} />
</AppContext.Provider>
</IntlProvider>
);
const StudioHeaderContext = ({ actionRowContent = null }) => {
const headerContextValue = useMemo(() => ({
authenticatedUser: {
userId: 'abc123',
username: 'edX',
roles: [],
administrator: false,
},
config: {
STUDIO_BASE_URL: process.env.STUDIO_BASE_URL,
SITE_NAME: process.env.SITE_NAME,
LOGIN_URL: process.env.LOGIN_URL,
LOGOUT_URL: process.env.LOGOUT_URL,
LOGO_URL: process.env.LOGO_URL,
},
}), []);
return (
<IntlProvider locale="en" messages={{}}>
<AppContext.Provider
value={headerContextValue}
>
<StudioHeader actionRowContent={actionRowContent} />
</AppContext.Provider>
</IntlProvider>
);
};
describe('<StudioHeader />', () => {
it('renders correctly', () => {
const contextValue = {
authenticatedUser: {
userId: 'abc123',
username: 'edX',
roles: [],
administrator: false,
},
config: {
STUDIO_BASE_URL: process.env.STUDIO_BASE_URL,
SITE_NAME: process.env.SITE_NAME,
LOGIN_URL: process.env.LOGIN_URL,
LOGOUT_URL: process.env.LOGOUT_URL,
LOGO_URL: process.env.LOGO_URL,
},
};
const component = <StudioHeaderComponent contextValue={contextValue} />;
const wrapper = TestRenderer.create(component);
expect(wrapper.toJSON()).toMatchSnapshot();
});
it('renders correctly with optional action row content', () => {
const actionRowContent = (
<>
<Dropdown>
<Dropdown.Toggle variant="outline-primary" id="library-header-menu-dropdown">
Settings
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Item as={Link} to="#">Dropdown Item 1</Dropdown.Item>
<Dropdown.Item as={Link} to="#">Dropdown Item 2</Dropdown.Item>
<Dropdown.Item as={Link} to="#">Dropdown Item 3</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
<ActionRow.Spacer />
<Button
variant="tertiary"
href="#"
rel="noopener noreferrer"
target="_blank"
title="Help Button"
>Help
</Button>
</>
);
const component = <StudioHeaderContext actionRowContent={actionRowContent} />;
const wrapper = TestRenderer.create(component);
expect(wrapper.toJSON()).toMatchSnapshot();
});
});

View File

@@ -33,7 +33,6 @@ exports[`<Header /> renders correctly for anonymous desktop 1`] = `
<a
className="nav-link"
href="http://localhost:18000/dashboard"
onClick={null}
>
Courses
</a>
@@ -94,7 +93,7 @@ exports[`<Header /> renders correctly for anonymous mobile 1`] = `
height="24px"
role="img"
style={
{
Object {
"height": "1.5rem",
"width": "1.5rem",
}
@@ -164,7 +163,7 @@ exports[`<Header /> renders correctly for anonymous mobile 1`] = `
<span
className="avatar overflow-hidden d-inline-flex rounded-circle null"
style={
{
Object {
"height": "1.5rem",
"width": "1.5rem",
}
@@ -176,7 +175,7 @@ exports[`<Header /> renders correctly for anonymous mobile 1`] = `
height="24px"
role="img"
style={
{
Object {
"height": "1.5rem",
"width": "1.5rem",
}
@@ -230,7 +229,6 @@ exports[`<Header /> renders correctly for authenticated desktop 1`] = `
<a
className="nav-link"
href="http://localhost:18000/dashboard"
onClick={null}
>
Courses
</a>
@@ -255,7 +253,7 @@ exports[`<Header /> renders correctly for authenticated desktop 1`] = `
<span
className="avatar overflow-hidden d-inline-flex rounded-circle mr-2"
style={
{
Object {
"height": "1.5em",
"width": "1.5em",
}
@@ -267,7 +265,7 @@ exports[`<Header /> renders correctly for authenticated desktop 1`] = `
height="24px"
role="img"
style={
{
Object {
"height": "1.5em",
"width": "1.5em",
}
@@ -341,7 +339,7 @@ exports[`<Header /> renders correctly for authenticated mobile 1`] = `
height="24px"
role="img"
style={
{
Object {
"height": "1.5rem",
"width": "1.5rem",
}
@@ -411,7 +409,7 @@ exports[`<Header /> renders correctly for authenticated mobile 1`] = `
<span
className="avatar overflow-hidden d-inline-flex rounded-circle null"
style={
{
Object {
"height": "1.5rem",
"width": "1.5rem",
}
@@ -423,7 +421,7 @@ exports[`<Header /> renders correctly for authenticated mobile 1`] = `
height="24px"
role="img"
style={
{
Object {
"height": "1.5rem",
"width": "1.5rem",
}

View File

@@ -0,0 +1,226 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<StudioHeader /> renders correctly 1`] = `
<header
className="site-header-desktop"
>
<a
className="nav-skip sr-only sr-only-focusable"
href="#main"
>
Skip to main content
</a>
<div
className="container-fluid null"
>
<div
className="nav-container position-relative d-flex align-items-center"
>
<img
alt="edX"
className="logo"
src="https://edx-cdn.org/v3/default/logo.svg"
/>
<div
className="pgn__action-row"
>
<nav
aria-label="Secondary"
className="nav secondary-menu-container align-items-center ml-auto"
>
<div
className="menu null"
onKeyDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<button
aria-expanded={false}
aria-haspopup="menu"
aria-label="Account menu for edX"
className="menu-trigger btn btn-outline-primary d-inline-flex align-items-center pl-2 pr-3"
onClick={[Function]}
>
<span
className="avatar overflow-hidden d-inline-flex rounded-circle mr-2"
style={
Object {
"height": "1.5em",
"width": "1.5em",
}
}
>
<svg
aria-hidden={true}
focusable="false"
height="24px"
role="img"
style={
Object {
"height": "1.5em",
"width": "1.5em",
}
}
version="1.1"
viewBox="0 0 24 24"
width="24px"
>
<path
d="M4.10255106,18.1351061 C4.7170266,16.0581859 8.01891846,14.4720277 12,14.4720277 C15.9810815,14.4720277 19.2829734,16.0581859 19.8974489,18.1351061 C21.215206,16.4412566 22,14.3122775 22,12 C22,6.4771525 17.5228475,2 12,2 C6.4771525,2 2,6.4771525 2,12 C2,14.3122775 2.78479405,16.4412566 4.10255106,18.1351061 Z M12,24 C5.372583,24 0,18.627417 0,12 C0,5.372583 5.372583,0 12,0 C18.627417,0 24,5.372583 24,12 C24,18.627417 18.627417,24 12,24 Z M12,13 C9.790861,13 8,11.209139 8,9 C8,6.790861 9.790861,5 12,5 C14.209139,5 16,6.790861 16,9 C16,11.209139 14.209139,13 12,13 Z"
fill="currentColor"
/>
</svg>
</span>
edX
<svg
aria-hidden={true}
focusable="false"
height="16px"
role="img"
version="1.1"
viewBox="0 0 16 16"
width="16px"
>
<path
d="M7,4 L7,8 L11,8 L11,10 L5,10 L5,4 L7,4 Z"
fill="currentColor"
transform="translate(8.000000, 7.000000) rotate(-45.000000) translate(-8.000000, -7.000000) "
/>
</svg>
</button>
</div>
</nav>
</div>
</div>
</div>
</header>
`;
exports[`<StudioHeader /> renders correctly with optional action row content 1`] = `
<header
className="site-header-desktop"
>
<a
className="nav-skip sr-only sr-only-focusable"
href="#main"
>
Skip to main content
</a>
<div
className="container-fluid null"
>
<div
className="nav-container position-relative d-flex align-items-center"
>
<img
alt="edX"
className="logo"
src="https://edx-cdn.org/v3/default/logo.svg"
/>
<div
className="pgn__action-row"
>
<div
className="pgn__dropdown pgn__dropdown-light dropdown"
data-testid="dropdown"
>
<button
aria-expanded={false}
aria-haspopup={true}
className="dropdown-toggle btn btn-outline-primary"
disabled={false}
id="library-header-menu-dropdown"
onClick={[Function]}
type="button"
>
Settings
</button>
</div>
<span
className="pgn__action-row-spacer"
/>
<a
className="btn btn-tertiary"
href="#"
onClick={[Function]}
onKeyDown={[Function]}
rel="noopener noreferrer"
role="button"
target="_blank"
title="Help Button"
>
Help
</a>
<nav
aria-label="Secondary"
className="nav secondary-menu-container align-items-center ml-auto"
>
<div
className="menu null"
onKeyDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<button
aria-expanded={false}
aria-haspopup="menu"
aria-label="Account menu for edX"
className="menu-trigger btn btn-outline-primary d-inline-flex align-items-center pl-2 pr-3"
onClick={[Function]}
>
<span
className="avatar overflow-hidden d-inline-flex rounded-circle mr-2"
style={
Object {
"height": "1.5em",
"width": "1.5em",
}
}
>
<svg
aria-hidden={true}
focusable="false"
height="24px"
role="img"
style={
Object {
"height": "1.5em",
"width": "1.5em",
}
}
version="1.1"
viewBox="0 0 24 24"
width="24px"
>
<path
d="M4.10255106,18.1351061 C4.7170266,16.0581859 8.01891846,14.4720277 12,14.4720277 C15.9810815,14.4720277 19.2829734,16.0581859 19.8974489,18.1351061 C21.215206,16.4412566 22,14.3122775 22,12 C22,6.4771525 17.5228475,2 12,2 C6.4771525,2 2,6.4771525 2,12 C2,14.3122775 2.78479405,16.4412566 4.10255106,18.1351061 Z M12,24 C5.372583,24 0,18.627417 0,12 C0,5.372583 5.372583,0 12,0 C18.627417,0 24,5.372583 24,12 C24,18.627417 18.627417,24 12,24 Z M12,13 C9.790861,13 8,11.209139 8,9 C8,6.790861 9.790861,5 12,5 C14.209139,5 16,6.790861 16,9 C16,11.209139 14.209139,13 12,13 Z"
fill="currentColor"
/>
</svg>
</span>
edX
<svg
aria-hidden={true}
focusable="false"
height="16px"
role="img"
version="1.1"
viewBox="0 0 16 16"
width="16px"
>
<path
d="M7,4 L7,8 L11,8 L11,10 L5,10 L5,4 L7,4 Z"
fill="currentColor"
transform="translate(8.000000, 7.000000) rotate(-45.000000) translate(-8.000000, -7.000000) "
/>
</svg>
</button>
</div>
</nav>
</div>
</div>
</div>
</header>
`;

View File

@@ -1,153 +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';
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);

View File

@@ -1,59 +0,0 @@
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;

View File

@@ -1,39 +0,0 @@
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;

View File

@@ -1,24 +0,0 @@
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;

View File

@@ -1 +1,28 @@
export default {};
import arMessages from './messages/ar.json';
import frMessages from './messages/fr.json';
import es419Messages from './messages/es_419.json';
import zhcnMessages from './messages/zh_CN.json';
import ptMessages from './messages/pt.json';
import itMessages from './messages/it.json';
import ukMessages from './messages/uk.json';
import deMessages from './messages/de.json';
import ruMessages from './messages/ru.json';
import hiMessages from './messages/hi.json';
import frCAMessages from './messages/fr_CA.json';
// no need to import en messages-- they are in the defaultMessage field
const messages = {
ar: arMessages,
'es-419': es419Messages,
fr: frMessages,
'zh-cn': zhcnMessages,
pt: ptMessages,
it: itMessages,
de: deMessages,
hi: hiMessages,
'fr-ca': frCAMessages,
ru: ruMessages,
uk: ukMessages,
};
export default messages;

33
src/i18n/messages/ar.json Normal file
View File

@@ -0,0 +1,33 @@
{
"general.register.sentenceCase": "التسجيل",
"general.signIn.sentenceCase": "تسجيل الدخول",
"header.links.courses": "المساقات",
"header.links.programs": "البرامج",
"header.links.content.search": "اكتشف الجديد",
"header.links.schools": "المدارس و الشركاء",
"header.user.menu.dashboard": "لوحة المعلومات",
"header.user.menu.profile": "الملف الشخصي",
"header.user.menu.account.settings": "الحساب",
"header.user.menu.order.history": "سجل الطلبيات",
"header.user.menu.logout": "تسجيل الخروج",
"header.user.menu.login": "تسجيل الدخول",
"header.user.menu.register": "التسجيل",
"header.user.menu.studio.home": "صفحة الاستوديو الرئيسية",
"header.user.menu.studio.maintenance": "الصيانة",
"header.label.account.nav": "الحساب",
"header.label.account.menu": "قائمة الحساب",
"header.label.account.menu.for": "قائمة حساب المستخدم {username}",
"header.label.main.nav": "القا|مة الرئيسية",
"header.label.main.menu": "القائمة الرئيسية",
"header.label.main.header": "الرئيسية",
"header.label.secondary.nav": "القائمة الثانوية",
"header.label.skip.nav": "التخطي إلى المحتوى الرئيسي",
"header.label.app.nav": "تطبيق",
"header.menu.dashboard.label": "لوحة المعلومات",
"header.help.label": "المساعدة",
"header.menu.profile.label": "الملف الشخصي",
"header.menu.account.label": "الحساب",
"header.menu.orderHistory.label": "سجل الطلبيات",
"header.navigation.skipNavLink": "التخطي إلى المحتوى الرئيسي",
"header.menu.signOut.label": "تسجيل الخروج"
}

33
src/i18n/messages/de.json Normal file
View File

@@ -0,0 +1,33 @@
{
"general.register.sentenceCase": "Register",
"general.signIn.sentenceCase": "Sign in",
"header.links.courses": "Courses",
"header.links.programs": "Programs",
"header.links.content.search": "Discover New",
"header.links.schools": "Schools & Partners",
"header.user.menu.dashboard": "Dashboard",
"header.user.menu.profile": "Profile",
"header.user.menu.account.settings": "Account",
"header.user.menu.order.history": "Order History",
"header.user.menu.logout": "Logout",
"header.user.menu.login": "Login",
"header.user.menu.register": "Sign Up",
"header.user.menu.studio.home": "Studio Home",
"header.user.menu.studio.maintenance": "Maintenance",
"header.label.account.nav": "Account",
"header.label.account.menu": "Account Menu",
"header.label.account.menu.for": "Account menu for {username}",
"header.label.main.nav": "Main",
"header.label.main.menu": "Main Menu",
"header.label.main.header": "Main",
"header.label.secondary.nav": "Secondary",
"header.label.skip.nav": "Skip to main content",
"header.label.app.nav": "App",
"header.menu.dashboard.label": "Dashboard",
"header.help.label": "Help",
"header.menu.profile.label": "Profile",
"header.menu.account.label": "Account",
"header.menu.orderHistory.label": "Order History",
"header.navigation.skipNavLink": "Skip to main content.",
"header.menu.signOut.label": "Sign Out"
}

View File

@@ -0,0 +1,33 @@
{
"general.register.sentenceCase": "Registrarse",
"general.signIn.sentenceCase": "Iniciar sesión",
"header.links.courses": "Cursos",
"header.links.programs": "Programas",
"header.links.content.search": "Encontrar nuevo",
"header.links.schools": "Escuelas y Socios",
"header.user.menu.dashboard": "Panel de Control",
"header.user.menu.profile": "Perfil",
"header.user.menu.account.settings": "Cuenta",
"header.user.menu.order.history": "Historial de órdenes",
"header.user.menu.logout": "Cerrar sesión",
"header.user.menu.login": "Login",
"header.user.menu.register": "Registrarse",
"header.user.menu.studio.home": "Inicio Studio",
"header.user.menu.studio.maintenance": "Mantenimiento",
"header.label.account.nav": "Cuenta",
"header.label.account.menu": "Menú de la cuenta",
"header.label.account.menu.for": "Menú de la cuenta para {username}",
"header.label.main.nav": "Principal",
"header.label.main.menu": "Menú Principal",
"header.label.main.header": "Principal",
"header.label.secondary.nav": "Secondary",
"header.label.skip.nav": "Ir al contenido principal",
"header.label.app.nav": "Aplicación",
"header.menu.dashboard.label": "Panel de Control",
"header.help.label": "Ayuda",
"header.menu.profile.label": "Perfil",
"header.menu.account.label": "Cuenta",
"header.menu.orderHistory.label": "Historial de órdenes",
"header.navigation.skipNavLink": "Dirígete al contenido principal.",
"header.menu.signOut.label": "Cerrar sesión"
}

33
src/i18n/messages/fr.json Normal file
View File

@@ -0,0 +1,33 @@
{
"general.register.sentenceCase": "S'inscrire",
"general.signIn.sentenceCase": "Connectez-vous",
"header.links.courses": "Cours",
"header.links.programs": "Programmes",
"header.links.content.search": "Explorer les cours",
"header.links.schools": "Écoles et partenaires",
"header.user.menu.dashboard": "Tableau de bord",
"header.user.menu.profile": "Profil",
"header.user.menu.account.settings": "Compte",
"header.user.menu.order.history": "Historique des commandes",
"header.user.menu.logout": "Déconnexion",
"header.user.menu.login": "Connexion",
"header.user.menu.register": "S'inscrire",
"header.user.menu.studio.home": "Accueil Studio",
"header.user.menu.studio.maintenance": "Maintenance",
"header.label.account.nav": "Compte",
"header.label.account.menu": "Menu du compte",
"header.label.account.menu.for": "Menu du compte pour {username}",
"header.label.main.nav": "Principal",
"header.label.main.menu": "Menu Principal",
"header.label.main.header": "Principal",
"header.label.secondary.nav": "Secondaire",
"header.label.skip.nav": "Passer au contenu principal",
"header.label.app.nav": "Application",
"header.menu.dashboard.label": "Tableau de bord",
"header.help.label": "Aide",
"header.menu.profile.label": "Profil",
"header.menu.account.label": "Compte",
"header.menu.orderHistory.label": "Historique des commandes",
"header.navigation.skipNavLink": "Passer au contenu principal",
"header.menu.signOut.label": "Se déconnecter"
}

View File

@@ -0,0 +1,33 @@
{
"general.register.sentenceCase": "Inscription",
"general.signIn.sentenceCase": "Connexion",
"header.links.courses": "Cours",
"header.links.programs": "Programmes",
"header.links.content.search": "Découvrir les nouveautés",
"header.links.schools": "Écoles et Partenaires",
"header.user.menu.dashboard": "Tableau de bord",
"header.user.menu.profile": "Profil",
"header.user.menu.account.settings": "Compte",
"header.user.menu.order.history": "Historique des commandes",
"header.user.menu.logout": "Déconnexion",
"header.user.menu.login": "Connexion",
"header.user.menu.register": "S'inscrire",
"header.user.menu.studio.home": "Accueil Studio",
"header.user.menu.studio.maintenance": "Entretien",
"header.label.account.nav": "Compte",
"header.label.account.menu": "Menu de compte",
"header.label.account.menu.for": "Menu de compte pour {username}",
"header.label.main.nav": "Principal",
"header.label.main.menu": "Menu principal",
"header.label.main.header": "Principal",
"header.label.secondary.nav": "Secondaire",
"header.label.skip.nav": "Passer au contenu de cette vue",
"header.label.app.nav": "Application",
"header.menu.dashboard.label": "Tableau de bord",
"header.help.label": "Aide",
"header.menu.profile.label": "Profil",
"header.menu.account.label": "Compte",
"header.menu.orderHistory.label": "Historique des commandes",
"header.navigation.skipNavLink": "Passer au contenu principal.",
"header.menu.signOut.label": "Se déconnecter"
}

33
src/i18n/messages/hi.json Normal file
View File

@@ -0,0 +1,33 @@
{
"general.register.sentenceCase": "Register",
"general.signIn.sentenceCase": "Sign in",
"header.links.courses": "Courses",
"header.links.programs": "Programs",
"header.links.content.search": "Discover New",
"header.links.schools": "Schools & Partners",
"header.user.menu.dashboard": "Dashboard",
"header.user.menu.profile": "Profile",
"header.user.menu.account.settings": "Account",
"header.user.menu.order.history": "Order History",
"header.user.menu.logout": "Logout",
"header.user.menu.login": "Login",
"header.user.menu.register": "Sign Up",
"header.user.menu.studio.home": "Studio Home",
"header.user.menu.studio.maintenance": "Maintenance",
"header.label.account.nav": "Account",
"header.label.account.menu": "Account Menu",
"header.label.account.menu.for": "Account menu for {username}",
"header.label.main.nav": "Main",
"header.label.main.menu": "Main Menu",
"header.label.main.header": "Main",
"header.label.secondary.nav": "Secondary",
"header.label.skip.nav": "Skip to main content",
"header.label.app.nav": "App",
"header.menu.dashboard.label": "Dashboard",
"header.help.label": "Help",
"header.menu.profile.label": "Profile",
"header.menu.account.label": "Account",
"header.menu.orderHistory.label": "Order History",
"header.navigation.skipNavLink": "Skip to main content.",
"header.menu.signOut.label": "Sign Out"
}

33
src/i18n/messages/it.json Normal file
View File

@@ -0,0 +1,33 @@
{
"general.register.sentenceCase": "Register",
"general.signIn.sentenceCase": "Sign in",
"header.links.courses": "Courses",
"header.links.programs": "Programs",
"header.links.content.search": "Discover New",
"header.links.schools": "Schools & Partners",
"header.user.menu.dashboard": "Dashboard",
"header.user.menu.profile": "Profile",
"header.user.menu.account.settings": "Account",
"header.user.menu.order.history": "Order History",
"header.user.menu.logout": "Logout",
"header.user.menu.login": "Login",
"header.user.menu.register": "Sign Up",
"header.user.menu.studio.home": "Studio Home",
"header.user.menu.studio.maintenance": "Maintenance",
"header.label.account.nav": "Account",
"header.label.account.menu": "Account Menu",
"header.label.account.menu.for": "Account menu for {username}",
"header.label.main.nav": "Main",
"header.label.main.menu": "Main Menu",
"header.label.main.header": "Main",
"header.label.secondary.nav": "Secondary",
"header.label.skip.nav": "Skip to main content",
"header.label.app.nav": "App",
"header.menu.dashboard.label": "Dashboard",
"header.help.label": "Help",
"header.menu.profile.label": "Profile",
"header.menu.account.label": "Account",
"header.menu.orderHistory.label": "Order History",
"header.navigation.skipNavLink": "Skip to main content.",
"header.menu.signOut.label": "Sign Out"
}

33
src/i18n/messages/pt.json Normal file
View File

@@ -0,0 +1,33 @@
{
"general.register.sentenceCase": "Register",
"general.signIn.sentenceCase": "Sign in",
"header.links.courses": "Courses",
"header.links.programs": "Programs",
"header.links.content.search": "Discover New",
"header.links.schools": "Schools & Partners",
"header.user.menu.dashboard": "Dashboard",
"header.user.menu.profile": "Profile",
"header.user.menu.account.settings": "Account",
"header.user.menu.order.history": "Order History",
"header.user.menu.logout": "Logout",
"header.user.menu.login": "Login",
"header.user.menu.register": "Sign Up",
"header.user.menu.studio.home": "Studio Home",
"header.user.menu.studio.maintenance": "Maintenance",
"header.label.account.nav": "Account",
"header.label.account.menu": "Account Menu",
"header.label.account.menu.for": "Account menu for {username}",
"header.label.main.nav": "Main",
"header.label.main.menu": "Main Menu",
"header.label.main.header": "Main",
"header.label.secondary.nav": "Secondary",
"header.label.skip.nav": "Skip to main content",
"header.label.app.nav": "App",
"header.menu.dashboard.label": "Dashboard",
"header.help.label": "Help",
"header.menu.profile.label": "Profile",
"header.menu.account.label": "Account",
"header.menu.orderHistory.label": "Order History",
"header.navigation.skipNavLink": "Skip to main content.",
"header.menu.signOut.label": "Sign Out"
}

33
src/i18n/messages/ru.json Normal file
View File

@@ -0,0 +1,33 @@
{
"general.register.sentenceCase": "Register",
"general.signIn.sentenceCase": "Sign in",
"header.links.courses": "Courses",
"header.links.programs": "Programs",
"header.links.content.search": "Discover New",
"header.links.schools": "Schools & Partners",
"header.user.menu.dashboard": "Dashboard",
"header.user.menu.profile": "Profile",
"header.user.menu.account.settings": "Account",
"header.user.menu.order.history": "Order History",
"header.user.menu.logout": "Logout",
"header.user.menu.login": "Login",
"header.user.menu.register": "Sign Up",
"header.user.menu.studio.home": "Studio Home",
"header.user.menu.studio.maintenance": "Maintenance",
"header.label.account.nav": "Account",
"header.label.account.menu": "Account Menu",
"header.label.account.menu.for": "Account menu for {username}",
"header.label.main.nav": "Main",
"header.label.main.menu": "Main Menu",
"header.label.main.header": "Main",
"header.label.secondary.nav": "Secondary",
"header.label.skip.nav": "Skip to main content",
"header.label.app.nav": "App",
"header.menu.dashboard.label": "Dashboard",
"header.help.label": "Help",
"header.menu.profile.label": "Profile",
"header.menu.account.label": "Account",
"header.menu.orderHistory.label": "Order History",
"header.navigation.skipNavLink": "Skip to main content.",
"header.menu.signOut.label": "Sign Out"
}

33
src/i18n/messages/uk.json Normal file
View File

@@ -0,0 +1,33 @@
{
"general.register.sentenceCase": "Register",
"general.signIn.sentenceCase": "Увійти",
"header.links.courses": "Курси",
"header.links.programs": "Програми",
"header.links.content.search": "Discover New",
"header.links.schools": "Schools & Partners",
"header.user.menu.dashboard": "Dashboard",
"header.user.menu.profile": "Profile",
"header.user.menu.account.settings": "Account",
"header.user.menu.order.history": "Order History",
"header.user.menu.logout": "Logout",
"header.user.menu.login": "Login",
"header.user.menu.register": "Sign Up",
"header.user.menu.studio.home": "Studio Home",
"header.user.menu.studio.maintenance": "Maintenance",
"header.label.account.nav": "Account",
"header.label.account.menu": "Меню облікового запису",
"header.label.account.menu.for": "Меню облікового запису для {username}",
"header.label.main.nav": "Main",
"header.label.main.menu": "Main Menu",
"header.label.main.header": "Main",
"header.label.secondary.nav": "Secondary",
"header.label.skip.nav": "Перейти до головного змісту",
"header.label.app.nav": "App",
"header.menu.dashboard.label": "Dashboard",
"header.help.label": "Help",
"header.menu.profile.label": "Profile",
"header.menu.account.label": "Account",
"header.menu.orderHistory.label": "Order History",
"header.navigation.skipNavLink": "Перейти до головного змісту.",
"header.menu.signOut.label": "Sign Out"
}

View File

@@ -0,0 +1,33 @@
{
"general.register.sentenceCase": "Register",
"general.signIn.sentenceCase": "Sign in",
"header.links.courses": "Courses",
"header.links.programs": "Programs",
"header.links.content.search": "Discover New",
"header.links.schools": "Schools & Partners",
"header.user.menu.dashboard": "Dashboard",
"header.user.menu.profile": "Profile",
"header.user.menu.account.settings": "Account",
"header.user.menu.order.history": "Order History",
"header.user.menu.logout": "Logout",
"header.user.menu.login": "Login",
"header.user.menu.register": "Sign Up",
"header.user.menu.studio.home": "Studio Home",
"header.user.menu.studio.maintenance": "Maintenance",
"header.label.account.nav": "Account",
"header.label.account.menu": "Account Menu",
"header.label.account.menu.for": "Account menu for {username}",
"header.label.main.nav": "Main",
"header.label.main.menu": "Main Menu",
"header.label.main.header": "Main",
"header.label.secondary.nav": "Secondary",
"header.label.skip.nav": "Skip to main content",
"header.label.app.nav": "App",
"header.menu.dashboard.label": "Dashboard",
"header.help.label": "Help",
"header.menu.profile.label": "Profile",
"header.menu.account.label": "Account",
"header.menu.orderHistory.label": "Order History",
"header.navigation.skipNavLink": "Skip to main content.",
"header.menu.signOut.label": "Sign Out"
}

View File

@@ -1,7 +1,7 @@
import Header from './Header';
import LearningHeader from './learning-header/LearningHeader';
import messages from './i18n/index';
import StudioHeader from './studio-header';
import StudioHeader from './StudioHeader';
export { LearningHeader, messages, StudioHeader };

View File

@@ -3,7 +3,6 @@ $blue: #007db8;
$white: #fff;
@import './Menu/menu.scss';
@import './studio-header/StudioHeader.scss';
.dropdown-item a {
text-decoration: none;
@@ -43,9 +42,9 @@ $white: #fff;
.user-dropdown {
.btn {
height: 3rem;
// @media (max-width: -1 + map-get($grid-breakpoints, "sm")) {
// padding: 0 0.5rem;
// }
@media (max-width: -1 + map-get($grid-breakpoints, "sm")) {
padding: 0 0.5rem;
}
}
}
}
@@ -88,15 +87,6 @@ $white: #fff;
height: 100%;
}
}
.secondary-menu-container {
.nav-link:hover,
.nav-link:focus,
.nav-link.active,
.expanded .nav-link {
background: $component-active-bg;
color: $component-active-color;
}
}
.main-nav {
.nav-link {
padding: 1.125rem 1rem;

View File

@@ -3,25 +3,27 @@ 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 LearningLoggedOutItemsSlot from '../plugin-slots/LearningLoggedOutItemsSlot';
import { Button } from '@edx/paragon';
import genericMessages from '../generic/messages';
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} />;
};
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>
);
AnonymousUserMenu.propTypes = {
intl: intlShape.isRequired,

View File

@@ -5,48 +5,46 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faUserCircle } from '@fortawesome/free-solid-svg-icons';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Dropdown } from '@openedx/paragon';
import LearningUserMenuSlot from '../plugin-slots/LearningUserMenuSlot';
import { Dropdown } from '@edx/paragon';
import messages from './messages';
const AuthenticatedUserDropdown = ({ intl, username }) => {
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,
},
];
const dashboardMenuItem = (
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/dashboard`}>
{intl.formatMessage(messages.dashboard)}
</Dropdown.Item>
);
return (
<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>
<>
<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>
</>
);
};

View File

@@ -6,11 +6,24 @@ 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 LinkedLogo = ({
href,
src,
alt,
...attributes
}) => (
<a href={href} {...attributes}>
<img className="d-block" src={src} alt={alt} />
</a>
);
LinkedLogo.propTypes = {
href: PropTypes.string.isRequired,
src: PropTypes.string.isRequired,
alt: PropTypes.string.isRequired,
};
const LearningHeader = ({
courseOrg, courseNumber, courseTitle, intl, showUserDropdown,
@@ -18,7 +31,8 @@ const LearningHeader = ({
const { authenticatedUser } = useContext(AppContext);
const headerLogo = (
<LogoSlot
<LinkedLogo
className="logo"
href={`${getConfig().LMS_BASE_URL}/dashboard`}
src={getConfig().LOGO_URL}
alt={getConfig().SITE_NAME}
@@ -30,16 +44,14 @@ 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 d-flex" style={{ lineHeight: 1 }}>
<CourseInfoSlot courseOrg={courseOrg} courseNumber={courseNumber} courseTitle={courseTitle} />
<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>
{showUserDropdown && authenticatedUser && (
<>
<LearningHelpSlot />
<AuthenticatedUserDropdown
username={authenticatedUser.username}
/>
</>
<AuthenticatedUserDropdown
username={authenticatedUser.username}
/>
)}
{showUserDropdown && !authenticatedUser && (
<AnonymousUserMenu />
@@ -50,9 +62,9 @@ const LearningHeader = ({
};
LearningHeader.propTypes = {
courseOrg: courseInfoDataShape.courseOrg,
courseNumber: courseInfoDataShape.courseNumber,
courseTitle: courseInfoDataShape.courseTitle,
courseOrg: PropTypes.string,
courseNumber: PropTypes.string,
courseTitle: PropTypes.string,
intl: intlShape.isRequired,
showUserDropdown: PropTypes.bool,
};

View File

@@ -1,23 +0,0 @@
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;

View File

@@ -1,14 +0,0 @@
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;

View File

@@ -1,21 +0,0 @@
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;

View File

@@ -1,26 +0,0 @@
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;

View File

@@ -1,58 +0,0 @@
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;

View File

@@ -1,35 +0,0 @@
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;

View File

@@ -1,25 +0,0 @@
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;

View File

@@ -1,125 +0,0 @@
# 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.
![Screenshot of replaced course title](./images/replace_course_title.png)
```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`)
![Screenshot of replaced course info with custom component](./images/replace_course_info_with_custom_component.png)
```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 🌛).
![Screenshot of added custom components before and after course info](./images/add_custom_components_before_and_after_course_info.png)
```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;
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -1,33 +0,0 @@
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;

View File

@@ -1,41 +0,0 @@
# 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`)
![Screenshot of custom component](./images/desktop_header_custom_component.png)
```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;
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -1,20 +0,0 @@
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;

View File

@@ -1,134 +0,0 @@
# 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.
![Screenshot of modified items](./images/desktop_logged_out_items_modify_items.png)
```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`)
![Screenshot of custom component](./images/desktop_logged_out_items_custom_component.png)
```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 🌛).
![Screenshot of custom components before and after](./images/desktop_logged_out_items_custom_components_before_after.png)
```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;
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1,22 +0,0 @@
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;

View File

@@ -1,134 +0,0 @@
# 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.
![Screenshot of modified items](./images/desktop_main_menu_modify_items.png)
```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`)
![Screenshot of custom component](./images/desktop_main_menu_custom_component.png)
```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 🌛).
![Screenshot of custom components before and after](./images/desktop_main_menu_custom_components_before_after.png)
```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;
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -1,22 +0,0 @@
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;

View File

@@ -1,129 +0,0 @@
# 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.
![Screenshot of modified items](./images/desktop_secondary_menu_modify_items.png)
```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`)
![Screenshot of custom component](./images/desktop_secondary_menu_custom_component.png)
```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 🌛).
![Screenshot of custom components before and after](./images/desktop_secondary_menu_custom_components_before_after.png)
```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;
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -1,22 +0,0 @@
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;

View File

@@ -1,141 +0,0 @@
# 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.
![Screenshot of modified items](./images/desktop_user_menu_modify_items.png)
```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`)
![Screenshot of custom component](./images/desktop_user_menu_custom_component.png)
```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 🌚).
![Screenshot of custom components before and after](./images/desktop_user_menu_custom_components_before_after.png)
```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;
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -1,22 +0,0 @@
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;

View File

@@ -1,41 +0,0 @@
# 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`)
![Screenshot of replaced learning help with custom component](./images/learning_help_custom_component.png)
```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;
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -1,11 +0,0 @@
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;

View File

@@ -1,132 +0,0 @@
# 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.
![Screenshot of modified items](./images/learning_logged_out_items_modified_items.png)
```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`)
![Screenshot of replaced with custom component](./images/learning_logged_out_items_custom_component.png)
```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 🌛).
![Screenshot of added custom components before and after](./images/learning_logged_out_items_custom_components_before_after.png)
```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;
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -1,20 +0,0 @@
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;

View File

@@ -1,130 +0,0 @@
# 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.
![Screenshot of modified items](./images/learning_user_menu_modified_items.png)
```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`)
![Screenshot of replaced with custom component](./images/learning_user_menu_custom_component.png)
```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 🌚).
![Screenshot of custom components before and after](./images/learning_user_menu_custom_components_before_after.png)
```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;
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -1,20 +0,0 @@
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;

View File

@@ -1,69 +0,0 @@
# Logo Slot
### Slot ID: `logo_slot`
## Description
This slot is used to replace/modify/hide the logo.
## Examples
### Modify URL
The following `env.config.jsx` will modify the link href for the logo.
```jsx
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const modifyLogoHref = ( widget ) => {
widget.content.href = "https://openedx.org/";
return widget;
};
const config = {
pluginSlots: {
logo_slot: {
keepDefault: true,
plugins: [
{
op: PLUGIN_OPERATIONS.Modify,
widgetId: 'default_contents',
fn: modifyLogoHref,
},
]
},
},
}
export default config;
```
### Custom Component
The following `env.config.jsx` will replace the logo entirely (in this case with a centered 🗺️ `h1`)
```jsx
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const config = {
pluginSlots: {
logo_slot: {
keepDefault: false,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_logo_component',
type: DIRECT_PLUGIN,
RenderWidget: () => (
<h1 style={{textAlign: 'center'}}>🗺</h1>
),
},
},
]
}
},
}
export default config;
```

Some files were not shown because too many files have changed in this diff Show More