Compare commits

..

2 Commits

Author SHA1 Message Date
Jawayria
916848577e fix: update release workflow 2022-05-13 18:21:44 +05:00
Jawayria
eb1eb84f56 chore!: Dropped support for Node 12 2022-05-13 18:20:27 +05:00
154 changed files with 41196 additions and 13560 deletions

View File

@@ -1,6 +1,4 @@
ACCESS_TOKEN_COOKIE_NAME=edx-jwt-cookie-header-payload
ACCOUNT_PROFILE_URL=http://localhost:1995
ACCOUNT_SETTINGS_URL=http://localhost:1997
BASE_URL=localhost:8080
CREDENTIALS_BASE_URL=http://localhost:18150
CSRF_TOKEN_API_PATH=/csrf/api/v1/token

View File

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

View File

@@ -16,4 +16,4 @@ jobs:
secrets:
GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }}
GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}

View File

@@ -1,20 +0,0 @@
# This workflow runs when a comment is made on the ticket
# If the comment starts with "label: " it tries to apply
# the label indicated in rest of comment.
# If the comment starts with "remove label: ", it tries
# to remove the indicated label.
# Note: Labels are allowed to have spaces and this script does
# not parse spaces (as often a space is legitimate), so the command
# "label: really long lots of words label" will apply the
# label "really long lots of words label"
name: Allows for the adding and removing of labels via comment
on:
issue_comment:
types: [created]
jobs:
add_remove_labels:
uses: openedx/.github/.github/workflows/add-remove-label-on-comment.yml@master

View File

@@ -2,22 +2,25 @@ name: Default CI
on:
push:
branches:
- master
- 'master'
pull_request:
branches:
- '**'
- '**'
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
node: [16]
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Setup Nodejs
uses: actions/setup-node@v4
uses: actions/setup-node@v2
with:
node-version-file: '.nvmrc'
node-version: ${{ matrix.node }}
- name: Install dependencies
run: npm ci
- name: Validate package-lock.json changes
@@ -26,12 +29,7 @@ jobs:
run: npm run lint
- name: Test
run: npm run test
- name: Build
run: npm run build
- 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@v2

View File

@@ -7,4 +7,4 @@ on:
jobs:
commitlint:
uses: openedx/.github/.github/workflows/commitlint.yml@master
uses: edx/.github/.github/workflows/commitlint.yml@master

View File

@@ -10,4 +10,5 @@ on:
jobs:
version-check:
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
uses: openedx/.github/.github/workflows/lockfileversion-check.yml@master

View File

@@ -2,44 +2,38 @@ name: Release CI
on:
push:
branches:
- master
- alpha
- master
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
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
with:
node-version: ${{ env.NODE_VER }}
- name: Install dependencies
run: npm ci
- name: Validate package-lock.json changes
run: make validate-no-uncommitted-package-lock-changes
- name: Lint
run: npm run lint
- name: Test
run: npm run test
- 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
- name: Build
run: npm run build
- name: Release
uses: cycjimmy/semantic-release-action@v3
with:
semantic_version: 16
env:
GITHUB_TOKEN: ${{ secrets.SEMANTIC_RELEASE_GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.SEMANTIC_RELEASE_NPM_TOKEN }}
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 16
- name: Install dependencies
run: npm ci
- name: Validate package-lock.json changes
run: make validate-no-uncommitted-package-lock-changes
- name: Lint
run: npm run lint
- name: Test
run: npm run test
- name: i18n_extract
run: npm run i18n_extract
- name: Coverage
uses: codecov/codecov-action@v2
- name: Build
run: npm run build
- name: Release
uses: cycjimmy/semantic-release-action@v2
with:
semantic_version: 16
env:
GITHUB_TOKEN: ${{ secrets.SEMANTIC_RELEASE_GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.SEMANTIC_RELEASE_NPM_TOKEN }}

View File

@@ -1,12 +0,0 @@
# This workflow runs when a comment is made on the ticket
# If the comment starts with "assign me" it assigns the author to the
# ticket (case insensitive)
name: Assign comment author to ticket if they say "assign me"
on:
issue_comment:
types: [created]
jobs:
self_assign_by_comment:
uses: openedx/.github/.github/workflows/self-assign-issue.yml@master

3
.gitignore vendored
View File

@@ -7,6 +7,3 @@ temp
src/i18n/transifex_input.json
module.config.js
.idea/
.vscode
src/i18n/messages

1
.nvmrc
View File

@@ -1 +0,0 @@
20

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,14 @@
transifex_resource = frontend-component-header
transifex_langs = "ar,fr,fr_CA,es_419,zh_CN"
transifex_utils = ./node_modules/.bin/transifex-utils.js
i18n = ./src/i18n
transifex_input = $(i18n)/transifex_input.json
tx_url1 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/translation/en/strings/
tx_url2 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/source/
# This directory must match .babelrc .
transifex_temp = ./temp/babel-plugin-formatjs
transifex_temp = ./temp/babel-plugin-react-intl
build:
rm -rf ./dist
@@ -14,7 +19,7 @@ build:
@rm -rf dist/__mocks__
requirements:
npm ci
npm install
i18n.extract:
# Pulling display strings from .jsx files into .json files...
@@ -32,6 +37,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/reactifex/bash_scripts/get_hashed_strings.sh $(tx_url1)
# Writing out comments to file...
$(transifex_utils) $(transifex_temp) --comments
# Pushing comments to Transifex...
./node_modules/reactifex/bash_scripts/put_comments.sh $(tx_url2)
# Pulls translations from Transifex.
pull_translations:
tx pull -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
Requirements
************
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>`_
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/edx/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.
@@ -46,14 +26,13 @@ Environment Variables
Defaults to "localhost" in development.
* ``LOGO_URL`` - The URL of the site's logo. This logo is displayed in the header.
* ``ORDER_HISTORY_URL`` - The URL of the order history page.
* ``ACCOUNT_PROFILE_URL`` - The URL of the account profile page.
* ``ACCOUNT_SETTINGS_URL`` - The URL of the account settings page.
* ``AUTHN_MINIMAL_HEADER`` - A boolean flag which hides the main menu, user menu, and logged-out
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 +40,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,24 +50,20 @@ 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>`_
* `An example of component and messages usage. <https://github.com/edx/frontend-template-application/blob/3355bb3a96232390e9056f35b06ffa8f105ed7ca/src/index.jsx#L21>`_
* `An example of SCSS file usage. <https://github.com/edx/frontend-template-application/blob/3cd5485bf387b8c479baf6b02bf59e3061dc3465/src/index.scss#L8>`_
***********
Development
===========
***********
Install dependencies::
npm ci
npm i
Start the development server::
@@ -122,63 +73,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 +84,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: [

48849
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",
@@ -24,56 +24,53 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/openedx/frontend-component-header.git"
"url": "git+https://github.com/edx/frontend-component-header.git"
},
"author": "edX",
"license": "AGPL-3.0",
"bugs": {
"url": "https://github.com/openedx/frontend-component-header/issues"
"url": "https://github.com/edx/frontend-component-header/issues"
},
"homepage": "https://github.com/openedx/frontend-component-header#readme",
"homepage": "https://github.com/edx/frontend-component-header#readme",
"devDependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/browserslist-config": "^1.1.1",
"@edx/frontend-platform": "8.1.2",
"@edx/reactifex": "^2.1.1",
"@openedx/frontend-build": "14.1.5",
"@openedx/paragon": "22.10.0",
"@testing-library/dom": "10.4.0",
"@testing-library/jest-dom": "5.17.0",
"@testing-library/react": "10.4.9",
"husky": "8.0.3",
"jest": "29.7.0",
"jest-chain": "1.1.6",
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
"@edx/frontend-build": "9.2.2",
"@edx/frontend-platform": "1.15.1",
"@edx/paragon": "19.20.0",
"codecov": "3.8.3",
"enzyme": "3.11.0",
"enzyme-adapter-react-16": "1.15.6",
"husky": "7.0.4",
"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-test-renderer": "17.0.2",
"redux": "4.2.1",
"redux-saga": "1.3.0"
"react": "16.14.0",
"react-dom": "16.14.0",
"react-redux": "7.2.8",
"react-router-dom": "5.3.1",
"react-test-renderer": "16.14.0",
"reactifex": "1.1.1",
"redux": "4.2.0",
"redux-saga": "1.1.3",
"@testing-library/dom": "7.31.2",
"@testing-library/jest-dom": "5.16.4",
"jest": "27.5.1",
"jest-chain": "1.1.5",
"@testing-library/react": "10.4.9"
},
"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",
"@fortawesome/react-fontawesome": "^0.2.0",
"@openedx/frontend-plugin-framework": "^1.3.0",
"axios-mock-adapter": "1.22.0",
"babel-polyfill": "6.26.0",
"classnames": "^2.5.1",
"jest-environment-jsdom": "^29.7.0",
"react-responsive": "8.2.0",
"react-transition-group": "4.4.5"
"react-transition-group": "4.4.2",
"@fortawesome/fontawesome-svg-core": "1.2.36",
"@fortawesome/free-brands-svg-icons": "5.15.4",
"@fortawesome/free-regular-svg-icons": "5.15.4",
"@fortawesome/free-solid-svg-icons": "5.15.4",
"@fortawesome/react-fontawesome": "^0.1.14"
},
"peerDependencies": {
"@edx/frontend-platform": "^7.0.0 || ^8.0.0",
"@openedx/paragon": ">= 21.5.7 < 23.0.0",
"@edx/frontend-platform": "^1.8.0",
"@edx/paragon": ">= 7.0.0 < 20.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": "^16.9.0",
"react-dom": "^16.9.0"
}
}

View File

@@ -22,11 +22,6 @@
"pin"
],
"automerge": true
},
{
"matchPackagePatterns": ["@edx", "@openedx"],
"matchUpdateTypes": ["minor", "patch"],
"automerge": true
}
],
"timezone": "America/New_York"

View File

@@ -3,12 +3,12 @@ import PropTypes from 'prop-types';
import { AvatarIcon } from './Icons';
const Avatar = ({
function Avatar({
size,
src,
alt,
className,
}) => {
}) {
const avatar = src ? (
<img className="d-block w-100 h-100" src={src} alt={alt} />
) : (
@@ -23,7 +23,7 @@ const Avatar = ({
{avatar}
</span>
);
};
}
Avatar.propTypes = {
src: PropTypes.string,

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,
}) => {
function 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.LMS_BASE_URL}/u/${authenticatedUser.username}`,
content: intl.formatMessage(messages['header.user.menu.profile']),
},
{
type: 'item',
href: `${config.LMS_BASE_URL}/account/settings`,
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,4 +1,3 @@
/* eslint-disable react/prop-types */
import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import TestRenderer from 'react-test-renderer';
@@ -7,31 +6,28 @@ import { Context as ResponsiveContext } from 'react-responsive';
import Header from './index';
const HeaderComponent = ({ width, contextValue }) => (
<ResponsiveContext.Provider value={width}>
<IntlProvider locale="en" messages={{}}>
<AppContext.Provider
value={contextValue}
>
<Header />
</AppContext.Provider>
</IntlProvider>
</ResponsiveContext.Provider>
);
describe('<Header />', () => {
it('renders correctly for anonymous desktop', () => {
const contextValue = {
authenticatedUser: null,
config: {
LMS_BASE_URL: process.env.LMS_BASE_URL,
SITE_NAME: process.env.SITE_NAME,
LOGIN_URL: process.env.LOGIN_URL,
LOGOUT_URL: process.env.LOGOUT_URL,
LOGO_URL: process.env.LOGO_URL,
},
};
const component = <HeaderComponent width={{ width: 1280 }} contextValue={contextValue} />;
const component = (
<ResponsiveContext.Provider value={{ width: 1280 }}>
<IntlProvider locale="en" messages={{}}>
<AppContext.Provider
value={{
authenticatedUser: null,
config: {
LMS_BASE_URL: process.env.LMS_BASE_URL,
SITE_NAME: process.env.SITE_NAME,
LOGIN_URL: process.env.LOGIN_URL,
LOGOUT_URL: process.env.LOGOUT_URL,
LOGO_URL: process.env.LOGO_URL,
},
}}
>
<Header />
</AppContext.Provider>
</IntlProvider>
</ResponsiveContext.Provider>
);
const wrapper = TestRenderer.create(component);
@@ -39,22 +35,31 @@ describe('<Header />', () => {
});
it('renders correctly for authenticated desktop', () => {
const contextValue = {
authenticatedUser: {
userId: 'abc123',
username: 'edX',
roles: [],
administrator: false,
},
config: {
LMS_BASE_URL: process.env.LMS_BASE_URL,
SITE_NAME: process.env.SITE_NAME,
LOGIN_URL: process.env.LOGIN_URL,
LOGOUT_URL: process.env.LOGOUT_URL,
LOGO_URL: process.env.LOGO_URL,
},
};
const component = <HeaderComponent width={{ width: 1280 }} contextValue={contextValue} />;
const component = (
<ResponsiveContext.Provider value={{ width: 1280 }}>
<IntlProvider locale="en" messages={{}}>
<AppContext.Provider
value={{
authenticatedUser: {
userId: 'abc123',
username: 'edX',
roles: [],
administrator: false,
},
config: {
LMS_BASE_URL: process.env.LMS_BASE_URL,
SITE_NAME: process.env.SITE_NAME,
LOGIN_URL: process.env.LOGIN_URL,
LOGOUT_URL: process.env.LOGOUT_URL,
LOGO_URL: process.env.LOGO_URL,
},
}}
>
<Header />
</AppContext.Provider>
</IntlProvider>
</ResponsiveContext.Provider>
);
const wrapper = TestRenderer.create(component);
@@ -62,17 +67,26 @@ describe('<Header />', () => {
});
it('renders correctly for anonymous mobile', () => {
const contextValue = {
authenticatedUser: null,
config: {
LMS_BASE_URL: process.env.LMS_BASE_URL,
SITE_NAME: process.env.SITE_NAME,
LOGIN_URL: process.env.LOGIN_URL,
LOGOUT_URL: process.env.LOGOUT_URL,
LOGO_URL: process.env.LOGO_URL,
},
};
const component = <HeaderComponent width={{ width: 500 }} contextValue={contextValue} />;
const component = (
<ResponsiveContext.Provider value={{ width: 500 }}>
<IntlProvider locale="en" messages={{}}>
<AppContext.Provider
value={{
authenticatedUser: null,
config: {
LMS_BASE_URL: process.env.LMS_BASE_URL,
SITE_NAME: process.env.SITE_NAME,
LOGIN_URL: process.env.LOGIN_URL,
LOGOUT_URL: process.env.LOGOUT_URL,
LOGO_URL: process.env.LOGO_URL,
},
}}
>
<Header />
</AppContext.Provider>
</IntlProvider>
</ResponsiveContext.Provider>
);
const wrapper = TestRenderer.create(component);
@@ -80,22 +94,31 @@ describe('<Header />', () => {
});
it('renders correctly for authenticated mobile', () => {
const contextValue = {
authenticatedUser: {
userId: 'abc123',
username: 'edX',
roles: [],
administrator: false,
},
config: {
LMS_BASE_URL: process.env.LMS_BASE_URL,
SITE_NAME: process.env.SITE_NAME,
LOGIN_URL: process.env.LOGIN_URL,
LOGOUT_URL: process.env.LOGOUT_URL,
LOGO_URL: process.env.LOGO_URL,
},
};
const component = <HeaderComponent width={{ width: 500 }} contextValue={contextValue} />;
const component = (
<ResponsiveContext.Provider value={{ width: 500 }}>
<IntlProvider locale="en" messages={{}}>
<AppContext.Provider
value={{
authenticatedUser: {
userId: 'abc123',
username: 'edX',
roles: [],
administrator: false,
},
config: {
LMS_BASE_URL: process.env.LMS_BASE_URL,
SITE_NAME: process.env.SITE_NAME,
LOGIN_URL: process.env.LOGIN_URL,
LOGOUT_URL: process.env.LOGOUT_URL,
LOGO_URL: process.env.LOGO_URL,
},
}}
>
<Header />
</AppContext.Provider>
</IntlProvider>
</ResponsiveContext.Provider>
);
const wrapper = TestRenderer.create(component);

View File

@@ -1,6 +1,6 @@
import React from 'react';
export const MenuIcon = (props) => (
export const MenuIcon = props => (
<svg
width="24px"
height="24px"
@@ -14,7 +14,7 @@ export const MenuIcon = (props) => (
</svg>
);
export const AvatarIcon = (props) => (
export const AvatarIcon = props => (
<svg
width="24px"
height="24px"
@@ -29,7 +29,7 @@ export const AvatarIcon = (props) => (
</svg>
);
export const CaretIcon = (props) => (
export const CaretIcon = props => (
<svg
width="16px"
height="16px"

View File

@@ -1,23 +1,35 @@
import React from 'react';
import PropTypes from 'prop-types';
const Logo = ({
function Logo({ src, alt, ...attributes }) {
return (
<img src={src} alt={alt} {...attributes} />
);
}
Logo.propTypes = {
src: PropTypes.string.isRequired,
alt: PropTypes.string.isRequired,
};
function LinkedLogo({
href,
src,
alt,
...attributes
}) => (
<a href={href} className="logo" {...attributes}>
<img className="d-block" src={src} alt={alt} />
</a>
);
}) {
return (
<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

@@ -2,10 +2,12 @@ import React from 'react';
import { CSSTransition } from 'react-transition-group';
import PropTypes from 'prop-types';
const MenuTrigger = ({ tag, className, ...attributes }) => React.createElement(tag, {
className: `menu-trigger ${className}`,
...attributes,
});
function MenuTrigger({ tag, className, ...attributes }) {
return React.createElement(tag, {
className: `menu-trigger ${className}`,
...attributes,
});
}
MenuTrigger.propTypes = {
tag: PropTypes.string,
className: PropTypes.string,
@@ -14,13 +16,14 @@ 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(' '),
...attributes,
});
function MenuContent({ tag, className, ...attributes }) {
return React.createElement(tag, {
className: ['menu-content', className].join(' '),
...attributes,
});
}
MenuContent.propTypes = {
tag: PropTypes.string,
className: PropTypes.string,

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,

99
src/StudioHeader.jsx Normal file
View File

@@ -0,0 +1,99 @@
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,
mergeConfig,
subscribe,
} from '@edx/frontend-platform';
import DesktopHeader from './DesktopHeader';
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');
});
function StudioHeader({ intl, mainMenu, appMenu }) {
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,
mainMenu,
userMenu,
appMenu,
loggedOutItems: [],
};
return <DesktopHeader {...props} />;
}
StudioHeader.propTypes = {
intl: intlShape.isRequired,
appMenu: PropTypes.shape(
{
content: PropTypes.string,
href: PropTypes.string,
menuItems: PropTypes.arrayOf(
PropTypes.shape({
type: PropTypes.string,
href: PropTypes.string,
content: PropTypes.string,
}),
),
},
),
mainMenu: PropTypes.arrayOf(
PropTypes.shape(
{
type: PropTypes.string,
href: PropTypes.string,
content: PropTypes.string,
},
),
),
};
StudioHeader.defaultProps = {
appMenu: null,
mainMenu: [],
};
export default injectIntl(StudioHeader);

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

@@ -0,0 +1,135 @@
import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import TestRenderer from 'react-test-renderer';
import { AppContext } from '@edx/frontend-platform/react';
import { StudioHeader } from './index';
describe('<StudioHeader />', () => {
it('renders correctly', () => {
const component = (
<IntlProvider locale="en" messages={{}}>
<AppContext.Provider
value={{
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,
},
}}
>
<StudioHeader />
</AppContext.Provider>
</IntlProvider>
);
const wrapper = TestRenderer.create(component);
expect(wrapper.toJSON()).toMatchSnapshot();
});
it('renders correctly with the optional app menu', () => {
const appMenu = {
content: 'App Menu',
menuItems: [
{
type: 'dropdown',
href: 'https://menu-href-url.org',
content: 'Content 1',
},
{
type: 'dropdown',
href: 'https://menu-href-url.org',
content: 'Content 2',
},
{
type: 'dropdown',
href: 'https://menu-href-url.org',
content: 'Content 3',
},
],
};
const component = (
<IntlProvider locale="en" messages={{}}>
<AppContext.Provider
value={{
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,
},
}}
>
<StudioHeader appMenu={appMenu} />
</AppContext.Provider>
</IntlProvider>
);
const wrapper = TestRenderer.create(component);
expect(wrapper.toJSON()).toMatchSnapshot();
});
it('renders correctly with the optional main menu', () => {
const mainMenu = [
{
type: 'dropdown',
href: 'https://menu-href-url.org',
content: 'Content 1',
},
{
type: 'dropdown',
href: 'https://menu-href-url.org',
content: 'Content 2',
},
{
type: 'dropdown',
href: 'https://menu-href-url.org',
content: 'Content 3',
},
];
const component = (
<IntlProvider locale="en" messages={{}}>
<AppContext.Provider
value={{
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,
},
}}
>
<StudioHeader mainMenu={mainMenu} />
</AppContext.Provider>
</IntlProvider>
);
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,425 @@
// 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"
/>
<nav
aria-label="Main"
className="nav main-nav"
/>
<nav
aria-label="Secondary"
className="nav secondary-menu-container align-items-center ml-auto"
>
<div
className="menu null"
onKeyDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<button
aria-expanded={false}
aria-haspopup="menu"
aria-label="Account menu for 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>
</header>
`;
exports[`<StudioHeader /> renders correctly with the optional app menu 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"
/>
<nav
aria-label="Main"
className="nav main-nav"
/>
<nav
aria-label="App"
className="nav app-nav"
>
<div
className="menu null"
onKeyDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<a
aria-expanded={false}
aria-haspopup="menu"
className="menu-trigger nav-link d-inline-flex align-items-center"
onClick={[Function]}
>
App Menu
<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>
</a>
</div>
</nav>
<nav
aria-label="Secondary"
className="nav secondary-menu-container align-items-center ml-auto"
>
<div
className="menu null"
onKeyDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<button
aria-expanded={false}
aria-haspopup="menu"
aria-label="Account menu for 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>
</header>
`;
exports[`<StudioHeader /> renders correctly with the optional main menu 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"
/>
<nav
aria-label="Main"
className="nav main-nav"
>
<div
className="menu nav-item"
onKeyDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<a
aria-expanded={false}
aria-haspopup="menu"
className="menu-trigger nav-link d-inline-flex align-items-center"
href="https://menu-href-url.org"
onClick={[Function]}
>
Content 1
<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>
</a>
</div>
<div
className="menu nav-item"
onKeyDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<a
aria-expanded={false}
aria-haspopup="menu"
className="menu-trigger nav-link d-inline-flex align-items-center"
href="https://menu-href-url.org"
onClick={[Function]}
>
Content 2
<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>
</a>
</div>
<div
className="menu nav-item"
onKeyDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<a
aria-expanded={false}
aria-haspopup="menu"
className="menu-trigger nav-link d-inline-flex align-items-center"
href="https://menu-href-url.org"
onClick={[Function]}
>
Content 3
<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>
</a>
</div>
</nav>
<nav
aria-label="Secondary"
className="nav secondary-menu-container align-items-center ml-auto"
>
<div
className="menu null"
onKeyDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<button
aria-expanded={false}
aria-haspopup="menu"
aria-label="Account menu for 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>
</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,34 @@
export default {};
import arMessages from './messages/ar.json';
import caMessages from './messages/ca.json';
import heMessages from './messages/he.json';
import idMessages from './messages/id.json';
import plMessages from './messages/pl.json';
import ruMessages from './messages/ru.json';
import thMessages from './messages/th.json';
import ukMessages from './messages/uk.json';
// no need to import en messages-- they are in the defaultMessage field
import es419Messages from './messages/es_419.json';
import frMessages from './messages/fr.json';
import kokrMessages from './messages/ko_KR.json';
import ptbrMessages from './messages/pt_BR.json';
import zhcnMessages from './messages/zh_CN.json';
const messages = {
ar: arMessages,
ca: caMessages,
he: heMessages,
id: idMessages,
pl: plMessages,
ru: ruMessages,
th: thMessages,
uk: ukMessages,
'es-419': es419Messages,
fr: frMessages,
'zh-cn': zhcnMessages,
'ko-kr': kokrMessages,
'pt-br': ptbrMessages,
};
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": "Studio Home",
"header.user.menu.studio.maintenance": "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": "App",
"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": "تسجيل الخروج"
}

View File

@@ -0,0 +1 @@
{}

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": "Studio Home",
"header.user.menu.studio.maintenance": "Maintenance",
"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": "App",
"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"
}

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,2 @@
{
}

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,2 @@
{
}

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1 @@
{}

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;
@@ -28,7 +27,7 @@ $white: #fff;
.learning-header {
min-width: 0;
.course-title-lockup {
min-width: 0;
@@ -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,29 @@ 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} />;
};
function AnonymousUserMenu({ intl }) {
return (
<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

@@ -3,52 +3,51 @@ import PropTypes from 'prop-types';
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,
},
];
function AuthenticatedUserDropdown({ intl, username }) {
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 mr-3" href={`${getConfig().SUPPORT_URL}`}>{intl.formatMessage(messages.help)}</a>
<Dropdown className="user-dropdown">
<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().LMS_BASE_URL}/u/${username}`}>
{intl.formatMessage(messages.profile)}
</Dropdown.Item>
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/account/settings`}>
{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>
</>
);
};
}
AuthenticatedUserDropdown.propTypes = {
intl: intlShape.isRequired,

View File

@@ -6,19 +6,35 @@ import { AppContext } from '@edx/frontend-platform/react';
import AnonymousUserMenu from './AnonymousUserMenu';
import AuthenticatedUserDropdown from './AuthenticatedUserDropdown';
import LogoSlot from '../plugin-slots/LogoSlot';
import CourseInfoSlot from '../plugin-slots/CourseInfoSlot';
import { courseInfoDataShape } from './LearningHeaderCourseInfo';
import messages from './messages';
import LearningHelpSlot from '../plugin-slots/LearningHelpSlot';
const LearningHeader = ({
function LinkedLogo({
href,
src,
alt,
...attributes
}) {
return (
<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,
};
function LearningHeader({
courseOrg, courseNumber, courseTitle, intl, showUserDropdown,
}) => {
}) {
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,29 +46,27 @@ 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}
/>
</>
)}
{showUserDropdown && !authenticatedUser && (
<AnonymousUserMenu />
<AnonymousUserMenu />
)}
</div>
</header>
);
};
}
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

@@ -12,7 +12,7 @@ describe('Header', () => {
it('displays user button', () => {
render(<Header />);
expect(screen.getByText(authenticatedUser.username)).toBeInTheDocument();
expect(screen.getByRole('button')).toHaveTextContent(authenticatedUser.username);
});
it('displays course data', () => {

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;
```

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