Compare commits

...

73 Commits

Author SHA1 Message Date
renovate[bot]
85881f5857 chore(deps): update actions/setup-node action to v6 2026-01-27 14:15:01 +00:00
Brian Smith
af6fe9fcdb chore(ci): update semantic release workflow to use OIDC (#660) 2026-01-27 09:14:11 -05:00
renovate[bot]
1118dca6c8 chore(deps): update dependency @openedx/paragon to v23.18.2 (#661)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-22 06:40:46 +00:00
renovate[bot]
59ac43b1cc chore(deps): update dependency ts-jest to v29.4.6 (#659)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-15 05:54:58 +00:00
renovate[bot]
aaa849a697 chore(deps): update dependency @openedx/paragon to v23.18.1 (#658)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 08:59:11 +00:00
renovate[bot]
840b042606 chore(deps): update dependency @openedx/paragon to v23.18.0 (#657)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 05:03:19 +00:00
renovate[bot]
93d62e35e8 chore(deps): update dependency react-router-dom to v6.30.2 (#656)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-17 06:10:50 +00:00
renovate[bot]
0c305651b0 chore(deps): update dependency @openedx/paragon to v23.16.0 (#654)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-10 06:36:21 +00:00
renovate[bot]
dfad1edcb9 chore(deps): update dependency redux-saga to v1.4.2 (#652)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-03 07:02:31 +00:00
renovate[bot]
af46ef756e chore(deps): update dependency @openedx/paragon to v23.15.2 (#651)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-03 07:02:10 +00:00
Farhaan Bukhsh
c86165180f feat: Moves search button into a slot (#644)
Signed-off-by: Farhaan Bukhsh <farhaan@opencraft.com>
2025-10-27 10:51:45 -04:00
renovate[bot]
97063850c6 chore(deps): update dependency @openedx/paragon to v23.15.0 (#650)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-27 04:33:28 +00:00
renovate[bot]
af252d2d3f chore(deps): update dependency @edx/frontend-platform to v8.5.2 (#649)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-27 04:33:12 +00:00
renovate[bot]
9e850c3229 chore(deps): update dependency @openedx/paragon to v23.14.9 (#647)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 05:52:53 +00:00
Braden MacDonald
413454b6e6 refactor: update propTypes -> TypeScript in Studio Header (#643) 2025-10-14 09:43:33 -07:00
renovate[bot]
ed7cc8a36e chore(deps): update dependency ts-jest to v29.4.5 (#646)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-13 04:27:12 +00:00
renovate[bot]
a8a4ad59ed chore(deps): update dependency @openedx/paragon to v23.14.8 (#645)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-13 04:27:04 +00:00
Kyle McCormick
9f2b2a4026 feat: Remove Studio Maintenance link (empty commit for release CI)
This was already merged in
6d424edbd5,
but we are pushing this empty commit with BREAKING CHANGE message
in order to trigger the release of a new semantic version.

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

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

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

* refactor: code refactoring

* feat: added logoDestination props for Header component

* refactor: after rebase

* refactor: updated tests

* refactor: after review

* refactor: removed logoDestination prop

* docs: Custom Marketing links mobile example

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

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

BREAKING CHANGE: release breaking change (typescript) as breaking
2025-09-24 13:51:11 -04:00
renovate[bot]
dbec796ceb chore(deps): update jest monorepo (#633)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 06:06:34 +00:00
bydawen
4a797a59cc test: Add Node 24 to CI matrix (#611) 2025-09-16 10:35:41 -04:00
renovate[bot]
a8a7348605 chore(deps): update dependency @edx/frontend-platform to v8.5.1 (#632)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 04:56:33 +00:00
renovate[bot]
1ff9ecaf15 chore(deps): update dependency @openedx/frontend-build to v14.6.2 (#631)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 05:27:56 +00:00
Jacobo Dominguez
8f67fdba68 refactor: replacement of injectIntl (#625) 2025-08-29 13:05:16 -04:00
renovate[bot]
43684dce91 fix(deps): update dependency @fortawesome/react-fontawesome to v0.2.6 (#630)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 06:16:18 +00:00
renovate[bot]
3ddfbab1ef chore(deps): update dependency @openedx/paragon to v23.14.2 (#628)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-18 04:53:34 +00:00
renovate[bot]
030758e078 chore(deps): update dependency @edx/frontend-platform to v8.5.0 (#627)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-11 04:25:30 +00:00
renovate[bot]
c56b945f0d chore(deps): update dependency @openedx/paragon to v23.14.1 (#626)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-11 04:25:16 +00:00
Fox Piacenti
69750674c3 feat: User menu trigger plugin slots. (#618) 2025-08-01 09:58:35 -04:00
renovate[bot]
813cbb3156 fix(deps): update dependency @fortawesome/react-fontawesome to v0.2.3 (#624)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-28 06:35:01 +00:00
renovate[bot]
20aaa4f2e2 chore(deps): update jest monorepo to v30.0.5 (#623)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-28 06:34:40 +00:00
Braden MacDonald
4dfb1b3053 fix: remove unused jest-chain, babel-polyfill, axios-mock-adapter (#603) 2025-07-21 10:38:01 -07:00
Braden MacDonald
171a770235 feat: enable the use of TypeScript in this repo (#604)
* feat: enable Typescript in this repo

* refactor: rename studio-header files to .ts[x]

* chore: fix minor type warnings

* chore: add types for frontend-platform

* chore: fix type issues

* chore: update name of suppressed lint check
2025-07-21 10:24:52 -07:00
renovate[bot]
f47c1ed1e6 fix(deps): update jest monorepo to v30 (#620)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-14 06:41:21 +00:00
renovate[bot]
c63da3051b chore(deps): update dependency @openedx/paragon to v23.14.0 (#616)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 06:16:34 +00:00
renovate[bot]
04b35786d4 fix(deps): update font awesome to v6.7.2 (#610)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 12:47:21 +00:00
renovate[bot]
657e9c0190 chore(deps): update dependency @openedx/frontend-build to v14.6.1 (#609)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 12:47:03 +00:00
renovate[bot]
2874c9603f chore(deps): update dependency @openedx/paragon to v23.13.0 (#614)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 06:47:01 +00:00
renovate[bot]
ec5381ea17 chore(deps): update dependency @openedx/paragon to v23.12.2 (#606)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-16 05:48:54 +00:00
renovate[bot]
d5ac171a5b chore(deps): update dependency @edx/frontend-platform to v8.4.0 (#605)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-16 05:48:36 +00:00
renovate[bot]
3be690b34b chore(deps): update dependency @openedx/paragon to v23.10.1 (#602)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-09 05:36:02 +00:00
renovate[bot]
441e1542ad chore(deps): update dependency @edx/frontend-platform to v8.3.9 (#601)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-09 05:35:41 +00:00
renovate[bot]
c1db3d409e chore(deps): update dependency @openedx/paragon to v23.10.0 (#600)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-02 06:36:18 +00:00
renovate[bot]
0c343cfdf0 chore(deps): update dependency @edx/frontend-platform to v8.3.8 (#599)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-02 06:36:03 +00:00
renovate[bot]
f740d0107e chore(deps): update dependency react-router-dom to v6.30.1 (#597)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-26 06:57:40 +00:00
renovate[bot]
98bc20a282 chore(deps): update dependency @edx/frontend-platform to v8.3.7 (#596)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-26 06:57:30 +00:00
renovate[bot]
ca15863c82 chore(deps): update dependency react-router-dom to v6.30.0 (#595)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 06:08:20 +00:00
renovate[bot]
ff9cb1b238 chore(deps): update dependency @openedx/paragon to v23.6.0 (#594)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 06:08:14 +00:00
renovate[bot]
67967156f4 chore(deps): update dependency @edx/frontend-platform to v8.3.6 (#592)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-12 07:13:35 +00:00
renovate[bot]
e4720ff6b0 chore(deps): update dependency @openedx/paragon to v23.5.1 (#593)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-12 07:12:12 +00:00
renovate[bot]
df704ce6d7 chore(deps): update dependency @openedx/frontend-build to v14.6.0 (#591)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-05 07:44:35 +00:00
renovate[bot]
a2dc80ffb8 chore(deps): update dependency @edx/frontend-platform to v8.3.5 (#590)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-05 07:44:23 +00:00
Brian Smith
95efe7fedd feat: standardize slot ids (#589) 2025-04-23 15:45:16 -04:00
renovate[bot]
8df7d928dd chore(deps): update dependency @openedx/paragon to v23.4.5 (#588)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-21 05:55:17 +00:00
renovate[bot]
5e77a47708 chore(deps): update dependency @edx/frontend-platform to v8.3.4 (#587)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-21 05:54:58 +00:00
Régis Behmo
8ef3a27a62 chore: remove husky 🪓🐶
We remove husky, which is triggering pre-push git hooks, including
running "npm lint". This is causing failures when building Docker
images, because "npm clean-install --omit=dev" automatically triggers "npm
prepare", which attemps to run "husky". But husky is not listed in the
build dependencies, only in devDependencies. As a consequence, package
installation is failing with the following error:

        14.13 > @edx/frontend-app-ora-grading@0.0.1 prepare
        14.13 > husky install
        14.13
        14.15 sh: 1: husky: not found

Similar to: https://github.com/openedx/frontend-app-learning/pull/1622
2025-04-14 11:03:03 -03:00
vladislavkeblysh
e0841996d0 feat: added laber for header dropdown btn 2025-04-07 19:30:59 -07:00
renovate[bot]
90f2e2540e chore(deps): update dependency @openedx/paragon to v23.4.3 (#583)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-31 06:00:52 +00:00
renovate[bot]
6a02c517b9 chore(deps): update dependency @openedx/paragon to v23.4.2 (#582)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 06:00:25 +00:00
renovate[bot]
5076d55314 chore(deps): update dependency @edx/frontend-platform to v8.3.3 (#581)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 06:00:17 +00:00
Brian Smith
2f3b9b87ca feat: add react 18 support (#580) 2025-03-21 13:40:42 -04:00
90 changed files with 12119 additions and 3567 deletions

View File

@@ -11,11 +11,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Nodejs - name: Setup Nodejs
uses: actions/setup-node@v4 uses: actions/setup-node@v6
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
- name: Install dependencies - name: Install dependencies
@@ -24,6 +24,8 @@ jobs:
run: make validate-no-uncommitted-package-lock-changes run: make validate-no-uncommitted-package-lock-changes
- name: Lint - name: Lint
run: npm run lint run: npm run lint
- name: Type check
run: npm run types
- name: Test - name: Test
run: npm run test run: npm run test
- name: Build - name: Build

View File

@@ -4,19 +4,26 @@ on:
branches: branches:
- master - master
- alpha - alpha
permissions:
contents: read # for checkout
jobs: jobs:
release: release:
name: Release name: Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: write # to be able to publish a GitHub release
issues: write # to be able to comment on released issues
pull-requests: write # to be able to comment on released pull requests
id-token: write # to enable use of OIDC for trusted publishing and npm provenance
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Nodejs Env - name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v6
with: with:
node-version: ${{ env.NODE_VER }} node-version: ${{ env.NODE_VER }}
- name: Install dependencies - name: Install dependencies
@@ -25,6 +32,8 @@ jobs:
run: make validate-no-uncommitted-package-lock-changes run: make validate-no-uncommitted-package-lock-changes
- name: Lint - name: Lint
run: npm run lint run: npm run lint
- name: Type check
run: npm run types
- name: Test - name: Test
run: npm run test run: npm run test
- name: i18n_extract - name: i18n_extract
@@ -37,9 +46,6 @@ jobs:
- name: Build - name: Build
run: npm run build run: npm run build
- name: Release - name: Release
uses: cycjimmy/semantic-release-action@v3
with:
semantic_version: 16
env: env:
GITHUB_TOKEN: ${{ secrets.SEMANTIC_RELEASE_GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.SEMANTIC_RELEASE_GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.SEMANTIC_RELEASE_NPM_TOKEN }} run: npx semantic-release@25

2
.nvmrc
View File

@@ -1 +1 @@
20 24

View File

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

13840
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,18 +10,16 @@
"build": "make build", "build": "make build",
"i18n_extract": "fedx-scripts formatjs extract", "i18n_extract": "fedx-scripts formatjs extract",
"lint": "fedx-scripts eslint --ext .js --ext .jsx .", "lint": "fedx-scripts eslint --ext .js --ext .jsx .",
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
"snapshot": "fedx-scripts jest --updateSnapshot", "snapshot": "fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress", "start": "fedx-scripts webpack-dev-server --progress",
"test": "fedx-scripts jest --coverage" "test": "fedx-scripts jest --coverage",
"test:dev": "fedx-scripts jest --watchAll",
"types": "tsc --noEmit"
}, },
"files": [ "files": [
"/dist" "/dist"
], ],
"husky": {
"hooks": {
"pre-commit": "npm run lint"
}
},
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/openedx/frontend-component-header.git" "url": "git+https://github.com/openedx/frontend-component-header.git"
@@ -36,35 +34,31 @@
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2", "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/browserslist-config": "^1.1.1", "@edx/browserslist-config": "^1.1.1",
"@edx/frontend-platform": "^8.3.1", "@edx/frontend-platform": "^8.3.1",
"@edx/reactifex": "^2.1.1", "@openedx/frontend-build": "^14.3.2",
"@openedx/frontend-build": "14.2.2",
"@openedx/paragon": "^23.0.0", "@openedx/paragon": "^23.0.0",
"@testing-library/dom": "10.4.0", "@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "5.17.0", "@testing-library/jest-dom": "5.17.0",
"@testing-library/react": "10.4.9", "@testing-library/react": "^16.2.0",
"husky": "8.0.3", "jest": "30.2.0",
"jest": "29.7.0", "jest-environment-jsdom": "^30.0.0",
"jest-chain": "1.1.6",
"prop-types": "15.8.1", "prop-types": "15.8.1",
"react": "17.0.2", "react": "^18.3.1",
"react-dom": "17.0.2", "react-dom": "^18.3.1",
"react-redux": "7.2.9", "react-redux": "^8.1.1",
"react-router-dom": "6.28.1", "react-router-dom": "6.30.2",
"react-test-renderer": "17.0.2", "react-test-renderer": "^18.3.1",
"redux": "4.2.1", "redux": "4.2.1",
"redux-saga": "1.3.0" "redux-saga": "1.4.2",
"ts-jest": "^29.4.4"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "6.6.0", "@fortawesome/fontawesome-svg-core": "6.7.2",
"@fortawesome/free-brands-svg-icons": "6.6.0", "@fortawesome/free-brands-svg-icons": "6.7.2",
"@fortawesome/free-regular-svg-icons": "6.6.0", "@fortawesome/free-regular-svg-icons": "6.7.2",
"@fortawesome/free-solid-svg-icons": "6.6.0", "@fortawesome/free-solid-svg-icons": "6.7.2",
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@openedx/frontend-plugin-framework": "^1.6.0", "@openedx/frontend-plugin-framework": "^1.7.0",
"axios-mock-adapter": "1.22.0",
"babel-polyfill": "6.26.0",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"jest-environment-jsdom": "^29.7.0",
"react-responsive": "8.2.0", "react-responsive": "8.2.0",
"react-transition-group": "4.4.5" "react-transition-group": "4.4.5"
}, },
@@ -72,8 +66,8 @@
"@edx/frontend-platform": "^7.0.0 || ^8.0.0", "@edx/frontend-platform": "^7.0.0 || ^8.0.0",
"@openedx/paragon": ">= 22.0.0 < 24.0.0", "@openedx/paragon": ">= 22.0.0 < 24.0.0",
"prop-types": "^15.5.10", "prop-types": "^15.5.10",
"react": "^16.9.0 || ^17.0.0", "react": "^16.9.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.9.0 || ^17.0.0", "react-dom": "^16.9.0 || ^17.0.0 || ^18.0.0",
"react-router-dom": "^6.14.2" "react-router-dom": "^6.14.2"
} }
} }

View File

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

View File

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

View File

@@ -33,6 +33,7 @@ describe('<Header />', () => {
}; };
const component = <HeaderComponent width={{ width: 1280 }} contextValue={contextValue} />; const component = <HeaderComponent width={{ width: 1280 }} contextValue={contextValue} />;
// FIXME: react-test-renderer is deprecated. Convert to @testing-library/react.
const wrapper = TestRenderer.create(component); const wrapper = TestRenderer.create(component);
expect(wrapper.toJSON()).toMatchSnapshot(); expect(wrapper.toJSON()).toMatchSnapshot();
@@ -56,6 +57,7 @@ describe('<Header />', () => {
}; };
const component = <HeaderComponent width={{ width: 1280 }} contextValue={contextValue} />; const component = <HeaderComponent width={{ width: 1280 }} contextValue={contextValue} />;
// FIXME: react-test-renderer is deprecated. Convert to @testing-library/react.
const wrapper = TestRenderer.create(component); const wrapper = TestRenderer.create(component);
expect(wrapper.toJSON()).toMatchSnapshot(); expect(wrapper.toJSON()).toMatchSnapshot();
@@ -74,6 +76,7 @@ describe('<Header />', () => {
}; };
const component = <HeaderComponent width={{ width: 500 }} contextValue={contextValue} />; const component = <HeaderComponent width={{ width: 500 }} contextValue={contextValue} />;
// FIXME: react-test-renderer is deprecated. Convert to @testing-library/react.
const wrapper = TestRenderer.create(component); const wrapper = TestRenderer.create(component);
expect(wrapper.toJSON()).toMatchSnapshot(); expect(wrapper.toJSON()).toMatchSnapshot();

View File

@@ -1,11 +1,12 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
// Local Components // Local Components
import DesktopUserMenuToggleSlot
from '../plugin-slots/DesktopUserMenuToggleSlot';
import { Menu, MenuTrigger, MenuContent } from '../Menu'; import { Menu, MenuTrigger, MenuContent } from '../Menu';
import Avatar from '../Avatar';
import LogoSlot from '../plugin-slots/LogoSlot'; import LogoSlot from '../plugin-slots/LogoSlot';
import DesktopLoggedOutItemsSlot from '../plugin-slots/DesktopLoggedOutItemsSlot'; import DesktopLoggedOutItemsSlot from '../plugin-slots/DesktopLoggedOutItemsSlot';
import { desktopLoggedOutItemsDataShape } from './DesktopLoggedOutItems'; import { desktopLoggedOutItemsDataShape } from './DesktopLoggedOutItems';
@@ -19,94 +20,74 @@ import { desktopUserMenuDataShape } from './DesktopHeaderUserMenu';
import messages from '../Header.messages'; import messages from '../Header.messages';
// Assets // Assets
import { CaretIcon } from '../Icons';
class DesktopHeader extends React.Component { const DesktopHeader = ({
constructor(props) { // eslint-disable-line no-useless-constructor mainMenu,
super(props); secondaryMenu,
} userMenu,
loggedOutItems,
logo,
logoAltText,
logoDestination,
avatar,
username,
loggedIn,
}) => {
const intl = useIntl();
renderMainMenu() { const renderMainMenu = () => <DesktopMainMenuSlot menu={mainMenu} />;
const { mainMenu } = this.props;
return <DesktopMainMenuSlot menu={mainMenu} />;
}
renderSecondaryMenu() { const renderSecondaryMenu = () => <DesktopSecondaryMenuSlot menu={secondaryMenu} />;
const { secondaryMenu } = this.props;
return <DesktopSecondaryMenuSlot menu={secondaryMenu} />;
}
renderUserMenu() { const renderUserMenu = () => (
const { <Menu transitionClassName="menu-dropdown" transitionTimeout={250}>
userMenu, <MenuTrigger
avatar, tag="button"
username, aria-label={intl.formatMessage(messages['header.label.account.menu.for'], { username })}
intl, className="btn btn-outline-primary d-inline-flex align-items-center pl-2 pr-3"
} = this.props; >
<DesktopUserMenuToggleSlot avatar={avatar} label={username} />
</MenuTrigger>
<MenuContent className="mb-0 dropdown-menu show dropdown-menu-right pin-right shadow py-2">
<DesktopUserMenuSlot menu={userMenu} />
</MenuContent>
</Menu>
);
return ( const renderLoggedOutItems = () => <DesktopLoggedOutItemsSlot items={loggedOutItems} />;
<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 logoProps = { src: logo, alt: logoAltText, href: logoDestination };
const { loggedOutItems } = this.props; const logoClasses = getConfig().AUTHN_MINIMAL_HEADER ? 'mw-100' : null;
return <DesktopLoggedOutItemsSlot items={loggedOutItems} />;
}
render() { return (
const { <header className="site-header-desktop">
logo, <a className="nav-skip sr-only sr-only-focusable" href="#main">{intl.formatMessage(messages['header.label.skip.nav'])}</a>
logoAltText, <div className={`container-fluid ${logoClasses}`}>
logoDestination, <div className="nav-container position-relative d-flex align-items-center">
loggedIn, <LogoSlot {...logoProps} />
intl, <nav
} = this.props; aria-label={intl.formatMessage(messages['header.label.main.nav'])}
const logoProps = { src: logo, alt: logoAltText, href: logoDestination }; className="nav main-nav"
const logoClasses = getConfig().AUTHN_MINIMAL_HEADER ? 'mw-100' : null; >
{renderMainMenu()}
return ( </nav>
<header className="site-header-desktop"> <nav
<a className="nav-skip sr-only sr-only-focusable" href="#main">{intl.formatMessage(messages['header.label.skip.nav'])}</a> aria-label={intl.formatMessage(messages['header.label.secondary.nav'])}
<div className={`container-fluid ${logoClasses}`}> className="nav secondary-menu-container align-items-center ml-auto"
<div className="nav-container position-relative d-flex align-items-center"> >
<LogoSlot {...logoProps} /> {loggedIn
<nav ? (
aria-label={intl.formatMessage(messages['header.label.main.nav'])} <>
className="nav main-nav" {renderSecondaryMenu()}
> {renderUserMenu()}
{this.renderMainMenu()} </>
</nav> ) : renderLoggedOutItems()}
<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> </div>
</header> </div>
); </header>
} );
} };
export const desktopHeaderDataShape = { export const desktopHeaderDataShape = {
mainMenu: desktopHeaderMainOrSecondaryMenuDataShape, mainMenu: desktopHeaderMainOrSecondaryMenuDataShape,
@@ -123,18 +104,15 @@ export const desktopHeaderDataShape = {
DesktopHeader.propTypes = { DesktopHeader.propTypes = {
mainMenu: desktopHeaderDataShape.mainMenu, mainMenu: desktopHeaderDataShape.mainMenu,
secondaryMenu: desktopHeaderDataShape.secondaryMenumainMenu, secondaryMenu: desktopHeaderDataShape.secondaryMenu,
userMenu: desktopHeaderDataShape.userMenumainMenu, userMenu: desktopHeaderDataShape.userMenu,
loggedOutItems: desktopHeaderDataShape.loggedOutItemsmainMenu, loggedOutItems: desktopHeaderDataShape.loggedOutItems,
logo: desktopHeaderDataShape.logomainMenu, logo: desktopHeaderDataShape.logo,
logoAltText: desktopHeaderDataShape.logoAltTextmainMenu, logoAltText: desktopHeaderDataShape.logoAltText,
logoDestination: desktopHeaderDataShape.logoDestinationmainMenu, logoDestination: desktopHeaderDataShape.logoDestination,
avatar: desktopHeaderDataShape.avatarmainMenu, avatar: desktopHeaderDataShape.avatar,
username: desktopHeaderDataShape.usernamemainMenu, username: desktopHeaderDataShape.username,
loggedIn: desktopHeaderDataShape.loggedInmainMenu, loggedIn: desktopHeaderDataShape.loggedIn,
// i18n
intl: intlShape.isRequired,
}; };
DesktopHeader.defaultProps = { DesktopHeader.defaultProps = {
@@ -150,4 +128,4 @@ DesktopHeader.defaultProps = {
loggedIn: false, loggedIn: false,
}; };
export default injectIntl(DesktopHeader); export default DesktopHeader;

View File

@@ -0,0 +1,20 @@
import React from 'react';
import PropTypes from 'prop-types';
import { CaretIcon } from '../Icons';
import Avatar from '../Avatar';
const DesktopUserMenuToggle = ({ avatar, label }) => (
<>
<Avatar size="1.5em" src={avatar} alt="" className="mr-2" />
{label} <CaretIcon role="img" aria-hidden focusable="false" />
</>
);
export const DesktopUserMenuTogglePropTypes = {
avatar: PropTypes.string,
label: PropTypes.string,
};
DesktopUserMenuToggle.propTypes = DesktopUserMenuTogglePropTypes;
export default DesktopUserMenuToggle;

41
src/frontend-platform.d.ts vendored Normal file
View File

@@ -0,0 +1,41 @@
// frontend-platform currently doesn't provide types... do it ourselves for i18n module at least.
// We can remove this in the future when we migrate to frontend-shell, or when frontend-platform gets types
// (whichever comes first).
declare module '@edx/frontend-platform/i18n' {
// eslint-disable-next-line import/no-extraneous-dependencies
import { injectIntl as _injectIntl } from 'react-intl';
/** @deprecated Use useIntl() hook instead. */
export const injectIntl: typeof _injectIntl;
/** @deprecated Use useIntl() hook instead. */
export const intlShape: any;
// eslint-disable-next-line import/no-extraneous-dependencies
export {
createIntl,
FormattedDate,
FormattedTime,
FormattedRelativeTime,
FormattedNumber,
FormattedPlural,
FormattedMessage,
defineMessages,
IntlProvider,
useIntl,
} from 'react-intl';
// Other exports from the i18n module:
export const configure: any;
export const getPrimaryLanguageSubtag: (code: string) => string;
export const getLocale: (locale?: string) => string;
export const getMessages: any;
export const isRtl: (locale?: string) => boolean;
export const handleRtl: any;
export const mergeMessages: any;
export const LOCALE_CHANGED: any;
export const LOCALE_TOPIC: any;
export const getCountryList: any;
export const getCountryMessages: any;
export const getLanguageList: any;
export const getLanguageMessages: any;
}

View File

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

View File

@@ -1,17 +1,18 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faUserCircle } from '@fortawesome/free-solid-svg-icons'; import { faUserCircle } from '@fortawesome/free-solid-svg-icons';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
import { Dropdown } from '@openedx/paragon'; import { Dropdown } from '@openedx/paragon';
import LearningUserMenuToggleSlot from '../plugin-slots/LearningUserMenuToggleSlot';
import LearningUserMenuSlot from '../plugin-slots/LearningUserMenuSlot'; import LearningUserMenuSlot from '../plugin-slots/LearningUserMenuSlot';
import messages from './messages'; import messages from './messages';
const AuthenticatedUserDropdown = ({ intl, username }) => { const AuthenticatedUserDropdown = ({ username }) => {
const intl = useIntl();
const dropdownItems = [ const dropdownItems = [
{ {
message: intl.formatMessage(messages.dashboard), message: intl.formatMessage(messages.dashboard),
@@ -37,11 +38,8 @@ const AuthenticatedUserDropdown = ({ intl, username }) => {
return ( return (
<Dropdown className="user-dropdown ml-3"> <Dropdown className="user-dropdown ml-3">
<Dropdown.Toggle variant="outline-primary"> <Dropdown.Toggle variant="outline-primary" aria-label={intl.formatMessage(messages.userOptionsDropdownLabel)}>
<FontAwesomeIcon icon={faUserCircle} className="d-md-none" size="lg" /> <LearningUserMenuToggleSlot label={username} icon={faUserCircle} />
<span data-hj-suppress className="d-none d-md-inline">
{username}
</span>
</Dropdown.Toggle> </Dropdown.Toggle>
<Dropdown.Menu className="dropdown-menu-right"> <Dropdown.Menu className="dropdown-menu-right">
<LearningUserMenuSlot items={dropdownItems} /> <LearningUserMenuSlot items={dropdownItems} />
@@ -51,8 +49,7 @@ const AuthenticatedUserDropdown = ({ intl, username }) => {
}; };
AuthenticatedUserDropdown.propTypes = { AuthenticatedUserDropdown.propTypes = {
intl: intlShape.isRequired,
username: PropTypes.string.isRequired, username: PropTypes.string.isRequired,
}; };
export default injectIntl(AuthenticatedUserDropdown); export default AuthenticatedUserDropdown;

View File

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

View File

@@ -0,0 +1,28 @@
import React from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import PropTypes from 'prop-types';
const LearningUserMenuToggle = ({
label,
icon,
}) => (
<>
<FontAwesomeIcon icon={icon} className="d-md-none" size="lg" />
<span data-hj-suppress className="d-none d-md-inline">
{label}
</span>
</>
);
export const LearningUserMenuTogglePropTypes = {
label: PropTypes.string.isRequired,
// Full shape available by examining @fortawesome/fontawesome-common-types/index.d.ts.
icon: PropTypes.shape({
prefix: PropTypes.string.isRequired,
iconName: PropTypes.string.isRequired,
}).isRequired,
};
LearningUserMenuToggle.propTypes = LearningUserMenuTogglePropTypes;
export default LearningUserMenuToggle;

View File

@@ -36,6 +36,11 @@ const messages = defineMessages({
defaultMessage: 'Sign Out', defaultMessage: 'Sign Out',
description: 'The label for the user menu Sign Out action.', description: 'The label for the user menu Sign Out action.',
}, },
userOptionsDropdownLabel: {
id: 'header.menu.aria-label',
defaultMessage: 'User Options',
description: 'The aria-label for the user options dropdown.',
},
}); });
export default messages; export default messages;

View File

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

View File

@@ -0,0 +1,14 @@
import React from 'react';
import PropTypes from 'prop-types';
import Avatar from '../Avatar';
const MobileUserMenuToggle = ({ avatar, username }) => <Avatar size="1.5rem" src={avatar} alt={username} />;
export const MobileUserMenuTogglePropTypes = {
avatar: PropTypes.string,
username: PropTypes.string,
};
MobileUserMenuToggle.propTypes = MobileUserMenuTogglePropTypes;
export default MobileUserMenuToggle;

View File

@@ -1,6 +1,9 @@
# Course Info Slot # Course Info Slot
### Slot ID: `course_info_slot` ### Slot ID: `org.openedx.frontend.layout.header_learning_course_info.v1`
### Slot ID Aliases
* `course_info_slot`
## Description ## Description
@@ -24,7 +27,7 @@ const replaceCourseTitle = ( widget ) => {
const config = { const config = {
pluginSlots: { pluginSlots: {
course_info_slot: { 'org.openedx.frontend.layout.header_learning_course_info.v1': {
keepDefault: true, keepDefault: true,
plugins: [ plugins: [
{ {
@@ -51,7 +54,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
const config = { const config = {
pluginSlots: { pluginSlots: {
course_info_slot: { 'org.openedx.frontend.layout.header_learning_course_info.v1': {
keepDefault: false, keepDefault: false,
plugins: [ plugins: [
{ {
@@ -83,7 +86,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
const config = { const config = {
pluginSlots: { pluginSlots: {
course_info_slot: { 'org.openedx.frontend.layout.header_learning_course_info.v1': {
keepDefault: true, keepDefault: true,
plugins: [ plugins: [
{ {

View File

@@ -9,7 +9,8 @@ const CourseInfoSlot = ({
...attributes ...attributes
}) => ( }) => (
<PluginSlot <PluginSlot
id="course_info_slot" id="org.openedx.frontend.layout.header_learning_course_info.v1"
idAliases={['course_info_slot']}
slotOptions={{ slotOptions={{
mergeProps: true, mergeProps: true,
}} }}

View File

@@ -1,6 +1,9 @@
# Desktop Header Slot # Desktop Header Slot
### Slot ID: `desktop_header_slot` ### Slot ID: `org.openedx.frontend.layout.header_desktop.v1`
### Slot ID Aliases
* `desktop_header_slot`
## Description ## Description
@@ -19,7 +22,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
const config = { const config = {
pluginSlots: { pluginSlots: {
desktop_header_slot: { 'org.openedx.frontend.layout.header_desktop.v1': {
keepDefault: false, keepDefault: false,
plugins: [ plugins: [
{ {
@@ -38,4 +41,4 @@ const config = {
} }
export default config; export default config;
``` ```

View File

@@ -6,7 +6,8 @@ const DesktopHeaderSlot = ({
props, props,
}) => ( }) => (
<PluginSlot <PluginSlot
id="desktop_header_slot" id="org.openedx.frontend.layout.header_desktop.v1"
idAliases={['desktop_header_slot']}
slotOptions={{ slotOptions={{
mergeProps: true, mergeProps: true,
}} }}

View File

@@ -1,6 +1,9 @@
# Desktop Logged Out Items Slot # Desktop Logged Out Items Slot
### Slot ID: `desktop_logged_out_items_slot` ### Slot ID: `org.openedx.frontend.layout.header_desktop_logged_out_items.v1`
### Slot ID Aliases
* `desktop_logged_out_items_slot`
## Description ## Description
@@ -40,7 +43,7 @@ const modifyLoggedOutItems = ( widget ) => {
const config = { const config = {
pluginSlots: { pluginSlots: {
desktop_logged_out_items_slot: { 'org.openedx.frontend.layout.header_desktop_logged_out_items.v1': {
keepDefault: true, keepDefault: true,
plugins: [ plugins: [
{ {
@@ -67,7 +70,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
const config = { const config = {
pluginSlots: { pluginSlots: {
desktop_logged_out_items_slot: { 'org.openedx.frontend.layout.header_desktop_logged_out_items.v1': {
keepDefault: false, keepDefault: false,
plugins: [ plugins: [
{ {
@@ -99,7 +102,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
const config = { const config = {
pluginSlots: { pluginSlots: {
desktop_logged_out_items_slot: { 'org.openedx.frontend.layout.header_desktop_logged_out_items.v1': {
keepDefault: true, keepDefault: true,
plugins: [ plugins: [
{ {

View File

@@ -6,7 +6,8 @@ const DesktopLoggedOutItemsSlot = ({
items, items,
}) => ( }) => (
<PluginSlot <PluginSlot
id="desktop_logged_out_items_slot" id="org.openedx.frontend.layout.header_desktop_logged_out_items.v1"
idAliases={['desktop_logged_out_items_slot']}
slotOptions={{ slotOptions={{
mergeProps: true, mergeProps: true,
}} }}

View File

@@ -1,6 +1,9 @@
# Desktop Main Menu Slot # Desktop Main Menu Slot
### Slot ID: `desktop_main_menu_slot` ### Slot ID: `org.openedx.frontend.layout.header_desktop_main_menu.v1`
### Slot ID Aliases
* `desktop_main_menu_slot`
## Description ## Description
@@ -10,9 +13,11 @@ This slot is used to replace/modify/hide the desktop main menu.
### Modify Items ### Modify Items
The following `env.config.jsx` will modify the items in the desktop main menu. #### Replace All Items
![Screenshot of modified items](./images/desktop_main_menu_modify_items.png) The following `env.config.jsx` will replace all items in the desktop main menu.
![Screenshot of modified items](./images/desktop_main_menu_replace_all_items.png)
```jsx ```jsx
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework'; import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
@@ -40,7 +45,59 @@ const modifyMainMenu = ( widget ) => {
const config = { const config = {
pluginSlots: { pluginSlots: {
desktop_main_menu_slot: { 'org.openedx.frontend.layout.header_desktop_main_menu.v1': {
keepDefault: true,
plugins: [
{
op: PLUGIN_OPERATIONS.Modify,
widgetId: 'default_contents',
fn: modifyMainMenu,
},
]
},
},
}
export default config;
```
#### Add Items
The following `env.config.jsx` will add items in the desktop main menu.
![Screenshot of custom marketing links](./images/desktop_main_menu_add_items.png)
```jsx
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const modifyMainMenu = (widget) => {
const existingMenu = widget.RenderWidget.props.menu || [];
const newMarketingLinks = [
{
type: 'item',
href: 'https://example.com/how-it-works',
content: 'How it works',
},
{
type: 'item',
href: 'https://example.com/courses',
content: 'Courses',
},
{
type: 'item',
href: 'https://example.com/schools',
content: 'Schools',
}
];
widget.content.menu = [...existingMenu, ...newMarketingLinks];
return widget;
};
const config = {
pluginSlots: {
'org.openedx.frontend.layout.header_desktop_main_menu.v1': {
keepDefault: true, keepDefault: true,
plugins: [ plugins: [
{ {
@@ -67,7 +124,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
const config = { const config = {
pluginSlots: { pluginSlots: {
desktop_main_menu_slot: { 'org.openedx.frontend.layout.header_desktop_main_menu.v1': {
keepDefault: false, keepDefault: false,
plugins: [ plugins: [
{ {
@@ -99,7 +156,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
const config = { const config = {
pluginSlots: { pluginSlots: {
desktop_main_menu_slot: { 'org.openedx.frontend.layout.header_desktop_main_menu.v1': {
keepDefault: true, keepDefault: true,
plugins: [ plugins: [
{ {
@@ -131,4 +188,3 @@ const config = {
export default config; export default config;
``` ```

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -6,7 +6,8 @@ const DesktopMainMenuSlot = ({
menu, menu,
}) => ( }) => (
<PluginSlot <PluginSlot
id="desktop_main_menu_slot" id="org.openedx.frontend.layout.header_desktop_main_menu.v1"
idAliases={['desktop_main_menu_slot']}
slotOptions={{ slotOptions={{
mergeProps: true, mergeProps: true,
}} }}

View File

@@ -1,6 +1,9 @@
# Desktop Secondary Menu Slot # Desktop Secondary Menu Slot
### Slot ID: `desktop_secondary_menu_slot` ### Slot ID: `org.openedx.frontend.layout.header_desktop_secondary_menu.v1`
### Slot ID Aliases
* `desktop_secondary_menu_slot`
## Description ## Description
@@ -35,7 +38,7 @@ const modifySecondaryMenu = ( widget ) => {
const config = { const config = {
pluginSlots: { pluginSlots: {
desktop_secondary_menu_slot: { 'org.openedx.frontend.layout.header_desktop_secondary_menu.v1': {
keepDefault: true, keepDefault: true,
plugins: [ plugins: [
{ {
@@ -62,7 +65,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
const config = { const config = {
pluginSlots: { pluginSlots: {
desktop_secondary_menu_slot: { 'org.openedx.frontend.layout.header_desktop_secondary_menu.v1': {
keepDefault: false, keepDefault: false,
plugins: [ plugins: [
{ {
@@ -94,7 +97,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
const config = { const config = {
pluginSlots: { pluginSlots: {
desktop_secondary_menu_slot: { 'org.openedx.frontend.layout.header_desktop_secondary_menu.v1': {
keepDefault: true, keepDefault: true,
plugins: [ plugins: [
{ {

View File

@@ -6,7 +6,8 @@ const DesktopSecondaryMenuSlot = ({
menu, menu,
}) => ( }) => (
<PluginSlot <PluginSlot
id="desktop_secondary_menu_slot" id="org.openedx.frontend.layout.header_desktop_secondary_menu.v1"
idAliases={['desktop_secondary_menu_slot']}
slotOptions={{ slotOptions={{
mergeProps: true, mergeProps: true,
}} }}

View File

@@ -1,6 +1,9 @@
# Desktop User Menu Slot # Desktop User Menu Slot
### Slot ID: `desktop_user_menu_slot` ### Slot ID: `org.openedx.frontend.layout.header_desktop_user_menu.v1`
### Slot ID Aliases
* `desktop_user_menu_slot`
## Description ## Description
@@ -48,7 +51,7 @@ const modifyUserMenu = ( widget ) => {
const config = { const config = {
pluginSlots: { pluginSlots: {
desktop_user_menu_slot: { 'org.openedx.frontend.layout.header_desktop_user_menu.v1': {
keepDefault: true, keepDefault: true,
plugins: [ plugins: [
{ {
@@ -75,7 +78,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
const config = { const config = {
pluginSlots: { pluginSlots: {
desktop_user_menu_slot: { 'org.openedx.frontend.layout.header_desktop_user_menu.v1': {
keepDefault: false, keepDefault: false,
plugins: [ plugins: [
{ {
@@ -107,7 +110,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
const config = { const config = {
pluginSlots: { pluginSlots: {
desktop_user_menu_slot: { 'org.openedx.frontend.layout.header_desktop_user_menu.v1': {
keepDefault: true, keepDefault: true,
plugins: [ plugins: [
{ {

View File

@@ -6,7 +6,8 @@ const DesktopUserMenuSlot = ({
menu, menu,
}) => ( }) => (
<PluginSlot <PluginSlot
id="desktop_user_menu_slot" id="org.openedx.frontend.layout.header_desktop_user_menu.v1"
idAliases={['desktop_user_menu_slot']}
slotOptions={{ slotOptions={{
mergeProps: true, mergeProps: true,
}} }}

View File

@@ -0,0 +1,74 @@
# Desktop User Menu Toggle Slot
### Slot ID: `org.openedx.frontend.layout.header_desktop_user_menu_toggle.v1`
## Description
This slot is used to replace/modify/hide the contents of the user menu toggle button on desktop sized screens.
## Examples
### Modify Label Text
The following `env.config.jsx` will modify the label text to be something more generic:
![Screenshot of modified label](./images/desktop_user_menu_modified_toggle.png)
```jsx
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
import { faHouse } from '@fortawesome/free-solid-svg-icons';
const modifyUserMenuToggle = ( widget ) => {
widget.content.label = "My Profile";
return widget;
};
const config = {
pluginSlots: {
'org.openedx.frontend.layout.header_desktop_user_menu_toggle.v1': {
keepDefault: true,
plugins: [
{
op: PLUGIN_OPERATIONS.Modify,
widgetId: 'default_contents',
fn: modifyUserMenuToggle,
},
]
},
},
}
export default config;
```
### Replace Menu toggle contents with Custom Component
The following `env.config.jsx` will replace the contents of the learning user menu toggle button entirely (in this case with an emoji)
![Screenshot of replaced with custom component](./images/desktop_user_menu_custom_component.png)
```jsx
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const config = {
pluginSlots: {
'org.openedx.frontend.layout.header_desktop_user_menu_toggle.v1': {
keepDefault: false,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_desktop_user_menu_toggle',
type: DIRECT_PLUGIN,
RenderWidget: () => (
<span>🦊</span>
),
},
},
]
},
},
}
export default config;
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@@ -0,0 +1,21 @@
import React from 'react';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import DesktopUserMenuToggle, { DesktopUserMenuTogglePropTypes } from '../../desktop-header/DesktopUserMenuToggle';
const DesktopUserMenuToggleSlot = ({
avatar,
label,
}) => (
<PluginSlot
id="org.openedx.frontend.layout.header_desktop_user_menu_toggle.v1"
slotOptions={{
mergeProps: true,
}}
>
<DesktopUserMenuToggle avatar={avatar} label={label} />
</PluginSlot>
);
DesktopUserMenuToggleSlot.propTypes = DesktopUserMenuTogglePropTypes;
export default DesktopUserMenuToggleSlot;

View File

@@ -1,6 +1,9 @@
# Learning Help Slot # Learning Help Slot
### Slot ID: `learning_help_slot` ### Slot ID: `org.openedx.frontend.layout.header_learning_help.v1`
### Slot ID Aliases
* `learning_help_slot`
## Description ## Description
@@ -19,7 +22,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
const config = { const config = {
pluginSlots: { pluginSlots: {
learning_help_slot: { 'org.openedx.frontend.layout.header_learning_help.v1': {
keepDefault: false, keepDefault: false,
plugins: [ plugins: [
{ {

View File

@@ -3,7 +3,7 @@ import { PluginSlot } from '@openedx/frontend-plugin-framework';
import LearningHeaderHelpLink from '../../learning-header/LearningHeaderHelpLink'; import LearningHeaderHelpLink from '../../learning-header/LearningHeaderHelpLink';
const LearningHelpSlot = () => ( const LearningHelpSlot = () => (
<PluginSlot id="learning_help_slot"> <PluginSlot id="org.openedx.frontend.layout.header_learning_help.v1" idAliases={['learning_help_slot']}>
<LearningHeaderHelpLink /> <LearningHeaderHelpLink />
</PluginSlot> </PluginSlot>
); );

View File

@@ -1,6 +1,9 @@
# Learning Logged Out Items Slot # Learning Logged Out Items Slot
### Slot ID: `learning_logged_out_items_slot` ### Slot ID: `org.openedx.frontend.layout.header_learning_logged_out_items.v1`
### Slot ID Aliases
* `learning_logged_out_items_slot`
## Description ## Description
@@ -38,7 +41,7 @@ const modifyLoggedOutItems = ( widget ) => {
const config = { const config = {
pluginSlots: { pluginSlots: {
learning_logged_out_items_slot: { 'org.openedx.frontend.layout.header_learning_logged_out_items.v1': {
keepDefault: true, keepDefault: true,
plugins: [ plugins: [
{ {
@@ -65,7 +68,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
const config = { const config = {
pluginSlots: { pluginSlots: {
learning_logged_out_items_slot: { 'org.openedx.frontend.layout.header_learning_logged_out_items.v1': {
keepDefault: false, keepDefault: false,
plugins: [ plugins: [
{ {
@@ -97,7 +100,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
const config = { const config = {
pluginSlots: { pluginSlots: {
learning_logged_out_items_slot: { 'org.openedx.frontend.layout.header_learning_logged_out_items.v1': {
keepDefault: true, keepDefault: true,
plugins: [ plugins: [
{ {

View File

@@ -6,7 +6,8 @@ const LearningLoggedOutItemsSlot = ({
buttonsInfo, buttonsInfo,
}) => ( }) => (
<PluginSlot <PluginSlot
id="learning_logged_out_items_slot" id="org.openedx.frontend.layout.header_learning_logged_out_items.v1"
idAliases={['learning_logged_out_items_slot']}
slotOptions={{ slotOptions={{
mergeProps: true, mergeProps: true,
}} }}

View File

@@ -1,6 +1,9 @@
# Learning User Menu Slot # Learning User Menu Slot
### Slot ID: `learning_user_menu_slot` ### Slot ID: `org.openedx.frontend.layout.header_learning_user_menu.v1`
### Slot ID Aliases
* `learning_user_menu_slot`
## Description ## Description
@@ -37,7 +40,7 @@ const modifyUserMenu = ( widget ) => {
const config = { const config = {
pluginSlots: { pluginSlots: {
learning_user_menu_slot: { 'org.openedx.frontend.layout.header_learning_user_menu.v1': {
keepDefault: true, keepDefault: true,
plugins: [ plugins: [
{ {
@@ -64,7 +67,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
const config = { const config = {
pluginSlots: { pluginSlots: {
learning_user_menu_slot: { 'org.openedx.frontend.layout.header_learning_user_menu.v1': {
keepDefault: false, keepDefault: false,
plugins: [ plugins: [
{ {
@@ -96,7 +99,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
const config = { const config = {
pluginSlots: { pluginSlots: {
learning_user_menu_slot: { 'org.openedx.frontend.layout.header_learning_user_menu.v1': {
keepDefault: true, keepDefault: true,
plugins: [ plugins: [
{ {

View File

@@ -6,7 +6,8 @@ const LearningUserMenuSlot = ({
items, items,
}) => ( }) => (
<PluginSlot <PluginSlot
id="learning_user_menu_slot" id="org.openedx.frontend.layout.header_learning_user_menu.v1"
idAliases={['learning_user_menu_slot']}
slotOptions={{ slotOptions={{
mergeProps: true, mergeProps: true,
}} }}

View File

@@ -0,0 +1,74 @@
# Learning User Menu Toggle Slot
### Slot ID: `org.openedx.frontend.layout.header_learning_user_menu_toggle.v1`
## Description
This slot is used to replace/modify/hide the contents of the learning user menu toggle button.
## Examples
### Modify Icon
The following `env.config.jsx` will modify the icon for the learning user menu toggle button. **Note:** The icon is only shown on mobile screens.
![Screenshot of modified items](./images/learning_user_menu_toggle_modified_items.png)
```jsx
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
import { faHouse } from '@fortawesome/free-solid-svg-icons';
const modifyUserMenuToggle = ( widget ) => {
widget.content.icon = faHouse;
return widget;
};
const config = {
pluginSlots: {
'org.openedx.frontend.layout.header_learning_user_menu_toggle.v1': {
keepDefault: true,
plugins: [
{
op: PLUGIN_OPERATIONS.Modify,
widgetId: 'default_contents',
fn: modifyUserMenuToggle,
},
]
},
},
}
export default config;
```
### Replace Menu toggle contents with Custom Component
The following `env.config.jsx` will replace the contents of the learning user menu toggle button's contents entirely (in this case with an emoji)
![Screenshot of replaced with custom component](./images/learning_user_menu_toggle_custom_component.png)
```jsx
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const config = {
pluginSlots: {
'org.openedx.frontend.layout.header_learning_user_menu_toggle.v1': {
keepDefault: false,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_learning_user_menu_toggle',
type: DIRECT_PLUGIN,
RenderWidget: () => (
<span>🦊</span>
),
},
},
]
},
},
}
export default config;
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,22 @@
import React from 'react';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import LearningUserMenuToggle, {
LearningUserMenuTogglePropTypes,
} from '../../learning-header/LearningUserMenuToggle';
const LearningUserMenuToggleSlot = ({
label, icon,
}) => (
<PluginSlot
id="org.openedx.frontend.layout.header_learning_user_menu_toggle.v1"
slotOptions={{
mergeProps: true,
}}
>
<LearningUserMenuToggle label={label} icon={icon} />
</PluginSlot>
);
LearningUserMenuToggleSlot.propTypes = LearningUserMenuTogglePropTypes;
export default LearningUserMenuToggleSlot;

View File

@@ -1,6 +1,9 @@
# Logo Slot # Logo Slot
### Slot ID: `logo_slot` ### Slot ID: `org.openedx.frontend.layout.header_logo.v1`
### Slot ID Aliases
* `logo_slot`
## Description ## Description
@@ -22,7 +25,7 @@ const modifyLogoHref = ( widget ) => {
const config = { const config = {
pluginSlots: { pluginSlots: {
logo_slot: { 'org.openedx.frontend.layout.header_logo.v1': {
keepDefault: true, keepDefault: true,
plugins: [ plugins: [
{ {
@@ -47,7 +50,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
const config = { const config = {
pluginSlots: { pluginSlots: {
logo_slot: { 'org.openedx.frontend.layout.header_logo.v1': {
keepDefault: false, keepDefault: false,
plugins: [ plugins: [
{ {

View File

@@ -6,7 +6,8 @@ const LogoSlot = ({
href, src, alt, ...attributes href, src, alt, ...attributes
}) => ( }) => (
<PluginSlot <PluginSlot
id="logo_slot" id="org.openedx.frontend.layout.header_logo.v1"
idAliases={['logo_slot']}
slotOptions={{ slotOptions={{
mergeProps: true, mergeProps: true,
}} }}

View File

@@ -1,6 +1,9 @@
# Mobile Header Slot # Mobile Header Slot
### Slot ID: `mobile_header_slot` ### Slot ID: `org.openedx.frontend.layout.header_mobile.v1`
### Slot ID Aliases
* `mobile_header_slot`
## Description ## Description
@@ -19,7 +22,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
const config = { const config = {
pluginSlots: { pluginSlots: {
mobile_header_slot: { 'org.openedx.frontend.layout.header_mobile.v1': {
keepDefault: false, keepDefault: false,
plugins: [ plugins: [
{ {

View File

@@ -6,7 +6,8 @@ const MobileHeaderSlot = ({
props, props,
}) => ( }) => (
<PluginSlot <PluginSlot
id="mobile_header_slot" id="org.openedx.frontend.layout.header_mobile.v1"
idAliases={['mobile_header_slot']}
slotOptions={{ slotOptions={{
mergeProps: true, mergeProps: true,
}} }}

View File

@@ -1,6 +1,9 @@
# Mobile Logged Out Items Slot # Mobile Logged Out Items Slot
### Slot ID: `mobile_logged_out_items_slot` ### Slot ID: `org.openedx.frontend.layout.header_mobile_logged_out_items.v1`
### Slot ID Aliases
* `mobile_logged_out_items_slot`
## Description ## Description
@@ -40,7 +43,7 @@ const modifyLoggedOutItems = ( widget ) => {
const config = { const config = {
pluginSlots: { pluginSlots: {
mobile_logged_out_items_slot: { 'org.openedx.frontend.layout.header_mobile_logged_out_items.v1': {
keepDefault: true, keepDefault: true,
plugins: [ plugins: [
{ {
@@ -67,7 +70,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
const config = { const config = {
pluginSlots: { pluginSlots: {
mobile_logged_out_items_slot: { 'org.openedx.frontend.layout.header_mobile_logged_out_items.v1': {
keepDefault: false, keepDefault: false,
plugins: [ plugins: [
{ {
@@ -99,7 +102,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
const config = { const config = {
pluginSlots: { pluginSlots: {
mobile_logged_out_items_slot: { 'org.openedx.frontend.layout.header_mobile_logged_out_items.v1': {
keepDefault: true, keepDefault: true,
plugins: [ plugins: [
{ {
@@ -131,4 +134,3 @@ const config = {
export default config; export default config;
``` ```

View File

@@ -6,7 +6,8 @@ const MobileLoggedOutItemsSlot = ({
items, items,
}) => ( }) => (
<PluginSlot <PluginSlot
id="mobile_logged_out_items_slot" id="org.openedx.frontend.layout.header_mobile_logged_out_items.v1"
idAliases={['mobile_logged_out_items_slot']}
slotOptions={{ slotOptions={{
mergeProps: true, mergeProps: true,
}} }}

View File

@@ -1,6 +1,9 @@
# Mobile Main Menu Slot # Mobile Main Menu Slot
### Slot ID: `mobile_main_menu_slot` ### Slot ID: `org.openedx.frontend.layout.header_mobile_main_menu.v1`
### Slot ID Aliases
* `mobile_main_menu_slot`
## Description ## Description
@@ -10,9 +13,11 @@ This slot is used to replace/modify/hide the mobile main menu.
### Modify Items ### Modify Items
The following `env.config.jsx` will modify the items in the mobile main menu. #### Replace All Items
![Screenshot of modified items](./images/mobile_main_menu_modify_items.png) The following `env.config.jsx` will replace all items in the mobile main menu.
![Screenshot of modified items](./images/mobile_main_menu_replace_all_items.png)
```jsx ```jsx
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework'; import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
@@ -40,7 +45,59 @@ const modifyMainMenu = ( widget ) => {
const config = { const config = {
pluginSlots: { pluginSlots: {
mobile_main_menu_slot: { 'org.openedx.frontend.layout.header_mobile_main_menu.v1': {
keepDefault: true,
plugins: [
{
op: PLUGIN_OPERATIONS.Modify,
widgetId: 'default_contents',
fn: modifyMainMenu,
},
]
},
},
}
export default config;
```
#### Add Items
The following `env.config.jsx` will add items in the mobile main menu.
![Screenshot of custom marketing links](./images/mobile_main_menu_add_items.png)
```jsx
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const modifyMainMenu = (widget) => {
const existingMenu = widget.RenderWidget.props.menu || [];
const newMarketingLinks = [
{
type: 'item',
href: 'https://example.com/how-it-works',
content: 'How it works',
},
{
type: 'item',
href: 'https://example.com/courses',
content: 'Courses',
},
{
type: 'item',
href: 'https://example.com/schools',
content: 'Schools',
}
];
widget.content.menu = [...existingMenu, ...newMarketingLinks];
return widget;
};
const config = {
pluginSlots: {
'org.openedx.frontend.layout.header_mobile_main_menu.v1': {
keepDefault: true, keepDefault: true,
plugins: [ plugins: [
{ {
@@ -67,7 +124,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
const config = { const config = {
pluginSlots: { pluginSlots: {
mobile_main_menu_slot: { 'org.openedx.frontend.layout.header_mobile_main_menu.v1': {
keepDefault: false, keepDefault: false,
plugins: [ plugins: [
{ {
@@ -99,7 +156,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
const config = { const config = {
pluginSlots: { pluginSlots: {
mobile_main_menu_slot: { 'org.openedx.frontend.layout.header_mobile_main_menu.v1': {
keepDefault: true, keepDefault: true,
plugins: [ plugins: [
{ {
@@ -131,4 +188,3 @@ const config = {
export default config; export default config;
``` ```

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -6,7 +6,8 @@ const MobileMainMenuSlot = ({
menu, menu,
}) => ( }) => (
<PluginSlot <PluginSlot
id="mobile_main_menu_slot" id="org.openedx.frontend.layout.header_mobile_main_menu.v1"
idAliases={['mobile_main_menu_slot']}
slotOptions={{ slotOptions={{
mergeProps: true, mergeProps: true,
}} }}

View File

@@ -1,6 +1,9 @@
# Mobile User Menu Slot # Mobile User Menu Slot
### Slot ID: `mobile_user_menu_slot` ### Slot ID: `org.openedx.frontend.layout.header_mobile_user_menu.v1`
### Slot ID Aliases
* `mobile_user_menu_slot`
## Description ## Description
@@ -48,7 +51,7 @@ const modifyUserMenu = ( widget ) => {
const config = { const config = {
pluginSlots: { pluginSlots: {
mobile_user_menu_slot: { 'org.openedx.frontend.layout.header_mobile_user_menu.v1': {
keepDefault: true, keepDefault: true,
plugins: [ plugins: [
{ {
@@ -75,7 +78,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
const config = { const config = {
pluginSlots: { pluginSlots: {
mobile_user_menu_slot: { 'org.openedx.frontend.layout.header_mobile_user_menu.v1': {
keepDefault: false, keepDefault: false,
plugins: [ plugins: [
{ {
@@ -107,7 +110,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
const config = { const config = {
pluginSlots: { pluginSlots: {
mobile_user_menu_slot: { 'org.openedx.frontend.layout.header_mobile_user_menu.v1': {
keepDefault: true, keepDefault: true,
plugins: [ plugins: [
{ {
@@ -139,4 +142,3 @@ const config = {
export default config; export default config;
``` ```

View File

@@ -6,7 +6,8 @@ const MobileUserMenuSlot = ({
menu, menu,
}) => ( }) => (
<PluginSlot <PluginSlot
id="mobile_user_menu_slot" id="org.openedx.frontend.layout.header_mobile_user_menu.v1"
idAliases={['mobile_user_menu_slot']}
slotOptions={{ slotOptions={{
mergeProps: true, mergeProps: true,
}} }}

View File

@@ -0,0 +1,74 @@
# Mobile User Menu Toggle Slot
### Slot ID: `org.openedx.frontend.layout.header_mobile_user_menu_trigger.v1`
## Description
This slot is used to replace/modify/hide the contents of the user menu toggle button on mobile screens.
## Examples
### Modify Avatar
The following `env.config.jsx` will modify the icon for the user menu toggle button on mobile.
![Screenshot of modified items](./images/mobile_user_menu_toggle_modified_items.png)
```jsx
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const modifyUserMenuToggle = ( widget ) => {
// Shows a dummy image with the resolution marker '30x30'.
widget.content.avatar = "https://dummyimage.com/30x30"
return widget;
};
const config = {
pluginSlots: {
'org.openedx.frontend.layout.header_mobile_user_menu_trigger.v1': {
keepDefault: true,
plugins: [
{
op: PLUGIN_OPERATIONS.Modify,
widgetId: 'default_contents',
fn: modifyUserMenuToggle,
},
]
},
},
}
export default config;
```
### Replace Menu toggle contents with Custom Component
The following `env.config.jsx` will replace the contents of the user menu toggle button's contents entirely (in this case with an emoji).
![Screenshot of replaced with custom component](./images/mobile_user_menu_toggle_custom_component.png)
```jsx
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const config = {
pluginSlots: {
'org.openedx.frontend.layout.header_mobile_user_menu_trigger.v1': {
keepDefault: false,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_mobile_user_menu_toggle',
type: DIRECT_PLUGIN,
RenderWidget: () => (
<span>🦊</span>
),
},
},
]
},
},
}
export default config;
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,23 @@
import React from 'react';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import MobileUserMenuToggle, {
MobileUserMenuTogglePropTypes,
} from '../../mobile-header/MobileUserMenuToggle';
const MobileUserMenuToggleSlot = ({
avatar,
label,
}) => (
<PluginSlot
id="org.openedx.frontend.layout.header_mobile_user_menu_trigger.v1"
slotOptions={{
mergeProps: true,
}}
>
<MobileUserMenuToggle avatar={avatar} label={label} />
</PluginSlot>
);
MobileUserMenuToggleSlot.propTypes = MobileUserMenuTogglePropTypes;
export default MobileUserMenuToggleSlot;

View File

@@ -1,15 +1,29 @@
# `frontend-component-header` Plugin Slots # `frontend-component-header` Plugin Slots
* [`logo_slot`](./LogoSlot/) ### Shared
* [`desktop_main_menu_slot`](./DesktopMainMenuSlot/) * [`org.openedx.frontend.layout.header_logo.v1`](./LogoSlot/)
* [`desktop_secondary_menu_slot`](./DesktopSecondaryMenuSlot/)
* [`mobile_main_menu_slot`](./MobileMainMenuSlot/) ### Desktop Header
* [`course_info_slot`](./CourseInfoSlot/) * [`org.openedx.frontend.layout.header_desktop.v1`](./DesktopHeaderSlot/)
* [`learning_help_slot`](./LearningHelpSlot/) * [`org.openedx.frontend.layout.header_desktop_logged_out_items.v1`](./DesktopLoggedOutItemsSlot/)
* [`desktop_logged_out_items_slot`](./DesktopLoggedOutItemsSlot/) * [`org.openedx.frontend.layout.header_desktop_main_menu.v1`](./DesktopMainMenuSlot/)
* [`mobile_logged_out_items_slot`](./MobileLoggedOutItemsSlot/) * [`org.openedx.frontend.layout.header_desktop_secondary_menu.v1`](./DesktopSecondaryMenuSlot/)
* [`mobile_user_menu_slot`](./MobileUserMenuSlot/) * [`org.openedx.frontend.layout.header_desktop_user_menu.v1`](./DesktopUserMenuSlot/)
* [`desktop_user_menu_slot`](./DesktopUserMenuSlot/) * [`org.openedx.frontend.layout.header_desktop_user_menu_toggle.v1`](./DesktopUserMenuToggleSlot/)
* [`learning_user_menu_slot`](./LearningUserMenuSlot/)
* [`learning_logged_out_items_slot`](./LearningLoggedOutItemsSlot/) ### Learning Header
* [`desktop_header_slot`](./DesktopHeaderSlot/) * [`org.openedx.frontend.layout.header_learning_course_info.v1`](./CourseInfoSlot/)
* [`org.openedx.frontend.layout.header_learning_help.v1`](./LearningHelpSlot/)
* [`org.openedx.frontend.layout.header_learning_logged_out_items.v1`](./LearningLoggedOutItemsSlot/)
* [`org.openedx.frontend.layout.header_learning_user_menu.v1`](./LearningUserMenuSlot/)
* [`org.openedx.frontend.layout.header_learning_user_menu.v1`](./LearningUserMenuSlot/)
### Mobile Header
* [`org.openedx.frontend.layout.header_mobile.v1`](./MobileHeaderSlot/)
* [`org.openedx.frontend.layout.header_mobile_logged_out_items.v1`](./MobileLoggedOutItemsSlot/)
* [`org.openedx.frontend.layout.header_mobile_main_menu.v1`](./MobileMainMenuSlot/)
* [`org.openedx.frontend.layout.header_mobile_user_menu.v1`](./MobileUserMenuSlot/)
* [`org.openedx.frontend.layout.header_mobile_user_menu_trigger.v1`](./MobileUserMenuToggleSlot/)
### Studio Header
* [`org.openedx.frontend.layout.studio_header_search_button_slot.v1`](./StudioHeaderSearchButtonSlot/)

View File

@@ -0,0 +1,89 @@
# Studio Header Search Button Slot
### Slot ID: `org.openedx.frontend.layout.studio_header_search_button_slot.v1`
## Description
This slot is used to replace/modify/hide the search button in the studio header.
## Examples
### Replace search with custom component
The following `env.config.jsx` will replace the search button entirely (in this case with a custom emoji icon):
![Search button being replaced](./images/studio_header_search_button_replace.png)
```jsx
import {
DIRECT_PLUGIN,
PLUGIN_OPERATIONS,
} from "@openedx/frontend-plugin-framework";
const config = {
pluginSlots: {
"org.openedx.frontend.layout.studio_header_search_button_slot.v1": {
keepDefault: false,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: "custom_notification_tray",
type: DIRECT_PLUGIN,
RenderWidget: () => <span>🔔</span>,
},
},
],
},
},
};
export default config;
```
### Add custom component before and after search button
The following `env.config.jsx` will insert emoji after and before the search button
![Add custom component before and after search button](./images/studio_header_search_button_before_after.png)
```jsx
import {
DIRECT_PLUGIN,
PLUGIN_OPERATIONS,
} from "@openedx/frontend-plugin-framework";
const config = {
pluginSlots: {
"org.openedx.frontend.layout.studio_header_search_button_slot.v1": {
keepDefault: true,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
priority: 10,
id: 'custom_notification_tray_before',
type: DIRECT_PLUGIN,
RenderWidget: () => <span>🔔</span>,
},
},
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
priority: 90,
id: 'custom_notification_tray_after',
type: DIRECT_PLUGIN,
RenderWidget: () => <span>🔕</span>,
},
},
],
},
};
export default config;
```
## API
- **Slot ID:** `org.openedx.frontend.layout.studio_header_search_button_slot.v1`
- **Component:** Uses the [PluginSlot](https://github.com/openedx/frontend-plugin-framework#pluginslot) from the Open edX Frontend Plugin Framework for plugin injection.

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,34 @@
import React from 'react';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import { Nav, IconButton, Icon } from '@openedx/paragon';
import { Search } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import messages from '../../studio-header/messages';
const StudioHeaderSearchButtonSlot = ({ searchButtonAction }) => {
const intl = useIntl();
return (
<PluginSlot
id="org.openedx.frontend.layout.studio_header_search_button_slot.v1"
>
{searchButtonAction && (
<Nav>
<IconButton
src={Search}
iconAs={Icon}
onClick={searchButtonAction}
aria-label={intl.formatMessage(messages['header.label.search.nav'])}
alt={intl.formatMessage(messages['header.label.search.nav'])}
/>
</Nav>
)}
</PluginSlot>
);
};
StudioHeaderSearchButtonSlot.propTypes = {
searchButtonAction: PropTypes.func,
};
export default StudioHeaderSearchButtonSlot;

View File

@@ -4,8 +4,6 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import '@testing-library/jest-dom/extend-expect'; import '@testing-library/jest-dom/extend-expect';
import 'babel-polyfill';
import 'jest-chain';
import { getConfig, mergeConfig } from '@edx/frontend-platform'; import { getConfig, mergeConfig } from '@edx/frontend-platform';
import { configure as configureLogging } from '@edx/frontend-platform/logging'; import { configure as configureLogging } from '@edx/frontend-platform/logging';
import { configure as configureI18n } from '@edx/frontend-platform/i18n'; import { configure as configureI18n } from '@edx/frontend-platform/i18n';

View File

@@ -34,7 +34,7 @@ describe('BrandNav Component', () => {
it('displays a link that navigates to studioBaseUrl', () => { it('displays a link that navigates to studioBaseUrl', () => {
render(<RootWrapper />); render(<RootWrapper />);
const link = screen.getByRole('link'); const link = screen.getByRole('link') as HTMLAnchorElement;
expect(link.href).toBe(studioBaseUrl); expect(link.href).toBe(studioBaseUrl);
}); });
}); });

View File

@@ -1,8 +1,13 @@
import React from 'react'; import React, { type FunctionComponent } from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
const BrandNav = ({ interface Props {
studioBaseUrl: string;
logo: string;
logoAltText: string;
}
const BrandNav: FunctionComponent<Props> = ({
studioBaseUrl, studioBaseUrl,
logo, logo,
logoAltText, logoAltText,
@@ -16,10 +21,4 @@ const BrandNav = ({
</Link> </Link>
); );
BrandNav.propTypes = {
studioBaseUrl: PropTypes.string.isRequired,
logo: PropTypes.string.isRequired,
logoAltText: PropTypes.string.isRequired,
};
export default BrandNav; export default BrandNav;

View File

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

View File

@@ -16,7 +16,7 @@ const mockProps = {
const RootWrapper = (props) => ( const RootWrapper = (props) => (
<MemoryRouter> <MemoryRouter>
<IntlProvider locale="en" messages={messages}> <IntlProvider locale="en" messages={{}}>
<CourseLockUp {...props} /> <CourseLockUp {...props} />
</IntlProvider> </IntlProvider>
</MemoryRouter> </MemoryRouter>
@@ -52,7 +52,8 @@ describe('CourseLockUp Component', () => {
it('navigates to an absolute URL when clicked', () => { it('navigates to an absolute URL when clicked', () => {
render(<RootWrapper {...mockProps} />); render(<RootWrapper {...mockProps} />);
const link = screen.getByTestId('course-lock-up-block'); // FIXME: don't use testId - https://testing-library.com/docs/queries/about#priority
const link = screen.getByTestId('course-lock-up-block') as HTMLAnchorElement;
expect(link.href).toBe(mockProps.outlineLink); expect(link.href).toBe(mockProps.outlineLink);
}); });
}); });

View File

@@ -0,0 +1,48 @@
import React, { type FunctionComponent } from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
OverlayTrigger,
Tooltip,
} from '@openedx/paragon';
import { Link } from 'react-router-dom';
import messages from './messages';
interface Props {
outlineLink?: string;
org?: string;
number?: string;
title?: string;
}
const CourseLockUp: FunctionComponent<Props> = ({
outlineLink = '',
org = '',
number = '',
title = '',
}) => {
const intl = useIntl();
return (
<OverlayTrigger
placement="bottom"
overlay={(
<Tooltip id="course-lock-up">
{title}
</Tooltip>
)}
>
<Link
className="course-title-lockup mr-2"
to={outlineLink}
aria-label={intl.formatMessage(messages['header.label.courseOutline'])}
data-testid="course-lock-up-block"
>
<span className="d-block small m-0 text-gray-800" data-testid="course-org-number">{org} {number}</span>
<span className="d-block m-0 font-weight-bold text-gray-800" data-testid="course-title">{title}</span>
</Link>
</OverlayTrigger>
);
};
export default CourseLockUp;

View File

@@ -35,7 +35,7 @@ const defaultProps = {
const RootWrapper = (props) => ( const RootWrapper = (props) => (
<MemoryRouter> <MemoryRouter>
<IntlProvider locale="en" messages={messages}> <IntlProvider locale="en" messages={{}}>
<HeaderBody {...props} /> <HeaderBody {...props} />
</IntlProvider> </IntlProvider>
</MemoryRouter> </MemoryRouter>

View File

@@ -1,23 +1,45 @@
import React from 'react'; import React, { type ReactNode, type ComponentProps } from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import classNames from 'classnames'; import classNames from 'classnames';
import { import {
ActionRow, ActionRow,
Button, Button,
Container, Container,
Icon,
IconButton,
Nav, Nav,
Row, Row,
} from '@openedx/paragon'; } from '@openedx/paragon';
import { Close, MenuIcon, Search } from '@openedx/paragon/icons'; import { Close, MenuIcon } from '@openedx/paragon/icons';
import CourseLockUp from './CourseLockUp'; import CourseLockUp from './CourseLockUp';
import UserMenu from './UserMenu'; import UserMenu from './UserMenu';
import BrandNav from './BrandNav'; import BrandNav from './BrandNav';
import NavDropdownMenu from './NavDropdownMenu'; import NavDropdownMenu from './NavDropdownMenu';
import messages from './messages'; import StudioHeaderSearchButtonSlot from '../plugin-slots/StudioHeaderSearchButtonSlot';
export interface HeaderBodyProps {
studioBaseUrl: string;
logoutUrl: string;
setModalPopupTarget?: ((instance: HTMLButtonElement | null) => void) | null;
toggleModalPopup?: React.MouseEventHandler<HTMLButtonElement>;
isModalPopupOpen?: boolean;
number?: string;
org?: string;
title: string;
logo: string;
logoAltText: string;
authenticatedUserAvatar?: string;
username?: string;
isAdmin?: boolean;
isMobile?: boolean;
isHiddenMainMenu?: boolean;
mainMenuDropdowns?: {
id: string;
buttonTitle: ReactNode;
items: { title: ReactNode; href: string; }[];
}[];
outlineLink?: string;
searchButtonAction?: React.MouseEventHandler<HTMLButtonElement>;
containerProps?: Omit<ComponentProps<typeof Container>, 'children'>;
}
const HeaderBody = ({ const HeaderBody = ({
logo, logo,
@@ -31,16 +53,15 @@ const HeaderBody = ({
logoutUrl, logoutUrl,
authenticatedUserAvatar, authenticatedUserAvatar,
isMobile, isMobile,
setModalPopupTarget, setModalPopupTarget = null,
toggleModalPopup, toggleModalPopup,
isModalPopupOpen, isModalPopupOpen = false,
isHiddenMainMenu, isHiddenMainMenu = false,
mainMenuDropdowns, mainMenuDropdowns = [],
outlineLink, outlineLink,
searchButtonAction, searchButtonAction,
containerProps, containerProps = {},
}) => { }: HeaderBodyProps) => {
const intl = useIntl();
const renderBrandNav = ( const renderBrandNav = (
<BrandNav <BrandNav
@@ -52,7 +73,7 @@ const HeaderBody = ({
/> />
); );
const { className: containerClassName, ...restContainerProps } = containerProps || {}; const { className: containerClassName, ...restContainerProps } = containerProps;
return ( return (
<Container <Container
@@ -116,17 +137,9 @@ const HeaderBody = ({
</> </>
)} )}
<ActionRow.Spacer /> <ActionRow.Spacer />
{searchButtonAction && ( <StudioHeaderSearchButtonSlot
<Nav> searchButtonAction={searchButtonAction}
<IconButton />
src={Search}
iconAs={Icon}
onClick={searchButtonAction}
aria-label={intl.formatMessage(messages['header.label.search.nav'])}
alt={intl.formatMessage(messages['header.label.search.nav'])}
/>
</Nav>
)}
<Nav> <Nav>
<UserMenu <UserMenu
{...{ {...{
@@ -135,6 +148,7 @@ const HeaderBody = ({
logoutUrl, logoutUrl,
authenticatedUserAvatar, authenticatedUserAvatar,
isAdmin, isAdmin,
isMobile,
}} }}
/> />
</Nav> </Nav>
@@ -143,53 +157,4 @@ const HeaderBody = ({
); );
}; };
HeaderBody.propTypes = {
studioBaseUrl: PropTypes.string.isRequired,
logoutUrl: PropTypes.string.isRequired,
setModalPopupTarget: PropTypes.func,
toggleModalPopup: PropTypes.func,
isModalPopupOpen: PropTypes.bool,
number: PropTypes.string,
org: PropTypes.string,
title: PropTypes.string,
logo: PropTypes.string,
logoAltText: PropTypes.string,
authenticatedUserAvatar: PropTypes.string,
username: PropTypes.string,
isAdmin: PropTypes.bool,
isMobile: PropTypes.bool,
isHiddenMainMenu: PropTypes.bool,
mainMenuDropdowns: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
buttonTitle: PropTypes.node,
items: PropTypes.arrayOf(PropTypes.shape({
href: PropTypes.string,
title: PropTypes.node,
})),
})),
outlineLink: PropTypes.string,
searchButtonAction: PropTypes.func,
containerProps: PropTypes.shape(Container.propTypes),
};
HeaderBody.defaultProps = {
setModalPopupTarget: null,
toggleModalPopup: null,
isModalPopupOpen: false,
logo: null,
logoAltText: null,
number: '',
org: '',
title: '',
authenticatedUserAvatar: null,
username: null,
isAdmin: false,
isMobile: false,
isHiddenMainMenu: false,
mainMenuDropdowns: [],
outlineLink: null,
searchButtonAction: null,
containerProps: {},
};
export default HeaderBody; export default HeaderBody;

View File

@@ -1,73 +0,0 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { useToggle, ModalPopup } from '@openedx/paragon';
import HeaderBody from './HeaderBody';
import MobileMenu from './MobileMenu';
const MobileHeader = ({
mainMenuDropdowns,
...props
}) => {
const [isOpen, , close, toggle] = useToggle(false);
const [target, setTarget] = useState(null);
return (
<>
<HeaderBody
{...props}
isMobile
setModalPopupTarget={setTarget}
toggleModalPopup={toggle}
isModalPopupOpen={isOpen}
/>
<ModalPopup
hasArrow
placement="bottom"
positionRef={target}
isOpen={isOpen}
onClose={close}
onEscapeKey={close}
className="mobile-menu-container"
>
<MobileMenu {...{ mainMenuDropdowns }} />
</ModalPopup>
</>
);
};
MobileHeader.propTypes = {
studioBaseUrl: PropTypes.string.isRequired,
logoutUrl: PropTypes.string.isRequired,
number: PropTypes.string,
org: PropTypes.string,
title: PropTypes.string,
logo: PropTypes.string,
logoAltText: PropTypes.string,
authenticatedUserAvatar: PropTypes.string,
username: PropTypes.string,
isAdmin: PropTypes.bool,
mainMenuDropdowns: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
buttonTitle: PropTypes.node,
items: PropTypes.arrayOf(PropTypes.shape({
href: PropTypes.string,
title: PropTypes.node,
})),
})),
outlineLink: PropTypes.string,
};
MobileHeader.defaultProps = {
logo: null,
logoAltText: null,
number: null,
org: null,
title: null,
authenticatedUserAvatar: null,
username: null,
isAdmin: false,
mainMenuDropdowns: [],
outlineLink: null,
};
export default MobileHeader;

View File

@@ -0,0 +1,52 @@
import React, { type FunctionComponent, useState } from 'react';
import { useToggle, ModalPopup } from '@openedx/paragon';
import HeaderBody, { type HeaderBodyProps } from './HeaderBody';
import MobileMenu from './MobileMenu';
type Props = Pick<HeaderBodyProps,
| 'studioBaseUrl'
| 'logoutUrl'
| 'number'
| 'org'
| 'title'
| 'logo'
| 'logoAltText'
| 'authenticatedUserAvatar'
| 'username'
| 'isAdmin'
| 'mainMenuDropdowns'
| 'outlineLink'
>;
const MobileHeader: FunctionComponent<Props> = ({
mainMenuDropdowns,
...props
}) => {
const [isOpen, , close, toggle] = useToggle(false);
const [target, setTarget] = useState<HTMLButtonElement | null>(null);
return (
<>
<HeaderBody
{...props}
isMobile
setModalPopupTarget={setTarget}
toggleModalPopup={toggle}
isModalPopupOpen={isOpen}
/>
<ModalPopup
hasArrow
placement="bottom"
positionRef={target}
isOpen={isOpen}
onClose={close}
onEscapeKey={close}
className="mobile-menu-container"
>
<MobileMenu {...{ mainMenuDropdowns }} />
</ModalPopup>
</>
);
};
export default MobileHeader;

View File

@@ -1,16 +1,21 @@
import React from 'react'; import React, { type ReactNode } from 'react';
import PropTypes from 'prop-types';
import { import {
Dropdown, Dropdown,
DropdownButton, DropdownButton,
} from '@openedx/paragon'; } from '@openedx/paragon';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
interface Props {
id: string;
buttonTitle: ReactNode;
items: { title: ReactNode; href: string; }[];
}
const NavDropdownMenu = ({ const NavDropdownMenu = ({
id, id,
buttonTitle, buttonTitle,
items, items,
}) => ( }: Props) => (
<DropdownButton <DropdownButton
id={id} id={id}
title={buttonTitle} title={buttonTitle}
@@ -30,13 +35,4 @@ const NavDropdownMenu = ({
</DropdownButton> </DropdownButton>
); );
NavDropdownMenu.propTypes = {
id: PropTypes.string.isRequired,
buttonTitle: PropTypes.node.isRequired,
items: PropTypes.arrayOf(PropTypes.shape({
href: PropTypes.string.isRequired,
title: PropTypes.node.isRequired,
})).isRequired,
};
export default NavDropdownMenu; export default NavDropdownMenu;

View File

@@ -12,7 +12,6 @@ import { Context as ResponsiveContext } from 'react-responsive';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import StudioHeader from './StudioHeader'; import StudioHeader from './StudioHeader';
import messages from './messages';
const authenticatedUser = { const authenticatedUser = {
userId: 3, userId: 3,
@@ -26,7 +25,7 @@ let screenWidth = 1280;
const RootWrapper = ({ const RootWrapper = ({
...props ...props
}) => { }: React.ComponentProps<typeof StudioHeader>) => {
const appContextValue = useMemo(() => ({ const appContextValue = useMemo(() => ({
authenticatedUser: currentUser, authenticatedUser: currentUser,
config: { config: {
@@ -55,7 +54,7 @@ const RootWrapper = ({
); );
}; };
const props = { const props: React.ComponentProps<typeof StudioHeader> = {
number: '123', number: '123',
org: 'Ed', org: 'Ed',
title: 'test', title: 'test',
@@ -72,7 +71,7 @@ const props = {
}, },
], ],
outlineLink: 'tEsTLInK', outlineLink: 'tEsTLInK',
searchButtonAction: null, searchButtonAction: undefined,
isNewHomePage: true, isNewHomePage: true,
}; };
@@ -115,16 +114,6 @@ describe('Header', () => {
expect(dropdownOption).toBeVisible(); expect(dropdownOption).toBeVisible();
}); });
it('maintenance should not be in user menu', async () => {
currentUser = { ...authenticatedUser, administrator: false };
const { getAllByRole, queryByText } = render(<RootWrapper {...props} />);
const userMenu = getAllByRole('button')[1];
await waitFor(() => fireEvent.click(userMenu));
const maintenanceButton = queryByText(messages['header.user.menu.maintenance'].defaultMessage);
expect(maintenanceButton).toBeNull();
});
it('user menu should use avatar icon', async () => { it('user menu should use avatar icon', async () => {
currentUser = { ...authenticatedUser, avatar: null }; currentUser = { ...authenticatedUser, avatar: null };
const { getByTestId } = render(<RootWrapper {...props} />); const { getByTestId } = render(<RootWrapper {...props} />);
@@ -153,7 +142,7 @@ describe('Header', () => {
}); });
it('should not show search button', async () => { it('should not show search button', async () => {
const testProps = { ...props, searchButtonAction: null }; const testProps = { ...props, searchButtonAction: undefined };
const { queryByRole } = render(<RootWrapper {...testProps} />); const { queryByRole } = render(<RootWrapper {...testProps} />);
expect(queryByRole('button', { name: 'Search content' })).not.toBeInTheDocument(); expect(queryByRole('button', { name: 'Search content' })).not.toBeInTheDocument();
}); });
@@ -186,15 +175,6 @@ describe('Header', () => {
expect(desktopMenu).toBeNull(); expect(desktopMenu).toBeNull();
}); });
it('maintenance should be in user menu', async () => {
const { getAllByRole, getByText } = render(<RootWrapper {...props} />);
const userMenu = getAllByRole('button')[1];
await waitFor(() => fireEvent.click(userMenu));
const maintenanceButton = getByText(messages['header.user.menu.maintenance'].defaultMessage);
expect(maintenanceButton).toBeVisible();
});
it('user menu should use avatar image', async () => { it('user menu should use avatar image', async () => {
const { getByTestId } = render(<RootWrapper {...props} />); const { getByTestId } = render(<RootWrapper {...props} />);
const avatarImage = getByTestId('avatar-image'); const avatarImage = getByTestId('avatar-image');

View File

@@ -1,11 +1,10 @@
import React, { useContext } from 'react'; import React, { type FunctionComponent, useContext } from 'react';
import PropTypes from 'prop-types';
import Responsive from 'react-responsive'; import Responsive from 'react-responsive';
import { AppContext } from '@edx/frontend-platform/react'; import { AppContext } from '@edx/frontend-platform/react';
import { ensureConfig } from '@edx/frontend-platform'; import { ensureConfig } from '@edx/frontend-platform';
import MobileHeader from './MobileHeader'; import MobileHeader from './MobileHeader';
import HeaderBody from './HeaderBody'; import HeaderBody, { HeaderBodyProps } from './HeaderBody';
ensureConfig([ ensureConfig([
'STUDIO_BASE_URL', 'STUDIO_BASE_URL',
@@ -15,10 +14,31 @@ ensureConfig([
'LOGO_URL', 'LOGO_URL',
], 'Studio Header component'); ], 'Studio Header component');
const StudioHeader = ({ type Props = Pick<HeaderBodyProps,
number, org, title, containerProps, isHiddenMainMenu, mainMenuDropdowns, | 'number'
outlineLink, searchButtonAction, isNewHomePage, | 'org'
| 'title'
| 'containerProps'
| 'isHiddenMainMenu'
| 'mainMenuDropdowns'
| 'outlineLink'
| 'searchButtonAction'
> & {
isNewHomePage: boolean;
};
const StudioHeader: FunctionComponent<Props> = ({
number,
org,
title,
containerProps,
isHiddenMainMenu,
mainMenuDropdowns,
outlineLink,
searchButtonAction,
isNewHomePage,
}) => { }) => {
// @ts-expect-error - frontend-platform doesn't yet have type information :/
const { authenticatedUser, config } = useContext(AppContext); const { authenticatedUser, config } = useContext(AppContext);
const props = { const props = {
logo: config.LOGO_URL, logo: config.LOGO_URL,
@@ -51,33 +71,4 @@ const StudioHeader = ({
); );
}; };
StudioHeader.propTypes = {
number: PropTypes.string,
org: PropTypes.string,
title: PropTypes.string.isRequired,
containerProps: HeaderBody.propTypes.containerProps,
isHiddenMainMenu: PropTypes.bool,
mainMenuDropdowns: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
buttonTitle: PropTypes.node,
items: PropTypes.arrayOf(PropTypes.shape({
href: PropTypes.string,
title: PropTypes.node,
})),
})),
outlineLink: PropTypes.string,
searchButtonAction: PropTypes.func,
isNewHomePage: PropTypes.bool.isRequired,
};
StudioHeader.defaultProps = {
number: '',
org: '',
containerProps: {},
isHiddenMainMenu: false,
mainMenuDropdowns: [],
outlineLink: null,
searchButtonAction: null,
};
export default StudioHeader; export default StudioHeader;

View File

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

View File

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

View File

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

12
tsconfig.json Normal file
View File

@@ -0,0 +1,12 @@
{
"extends": "@edx/typescript-config",
"compilerOptions": {
"noEmit": true,
"baseUrl": "./src",
"paths": {
"*": ["*"]
}
},
"include": ["*.js", ".eslintrc.js", "src/**/*", "plugins/**/*"],
"exclude": ["dist", "node_modules"]
}