Compare commits

..

79 Commits

Author SHA1 Message Date
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
Peter Kulko
7970561181 feat: added support Paragon design tokens (#351)
Co-authored-by: Diana Catalina Olarte <diana.olarte@edunext.co>
2025-03-18 12:41:04 -04:00
Brian Smith
8d46de8fe3 feat!: remove Paragon 21 support (#578)
BREAKING CHANGE: consumers must now use Paragon 22
2025-03-17 16:00:03 -04:00
renovate[bot]
8341f17d46 chore(deps): update dependency @openedx/paragon to v22.16.0 (#577)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-17 05:55:14 +00:00
renovate[bot]
d7c3e5a687 chore(deps): update dependency @edx/frontend-platform to v8.3.1 (#576)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-10 05:59:56 +00:00
renovate[bot]
07b1c5bde1 chore(deps): update dependency @openedx/paragon to v22.15.3 (#572)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-03 05:29:00 +00:00
renovate[bot]
5512faa9b0 chore(deps): update dependency @edx/frontend-platform to v8.2.1 (#570)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 05:38:39 +00:00
Kyle McCormick
48c49fe0b2 revert: fix: Remove Studio Maintenance link (#565)
This reverts commit a229c34535.

We are temporarily re-introducing the Maintenance link, as the Maintenance
Announcements tool is still in use, as discussed on:
https://github.com/openedx/edx-platform/pull/35852

For more details, see the related edx-platform revert:
https://github.com/openedx/edx-platform/pull/36107

In the future, this will be re-removed:
https://github.com/openedx/edx-platform/issues/36263
2025-02-19 14:25:06 -05:00
renovate[bot]
8c7778218b chore(deps): update dependency @edx/browserslist-config to v1.5.0 (#569)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-17 06:14:04 +00:00
Feanil Patel
0dedbbd589 docs: Document the owner and drop the old metadata file.
This is going to be changing soon with the module federation work so I
think it makes sense to be maintained by the committers-frontend group.

I'm also cleaning up the old openedx.yaml file which is obsolete and out
of date while I'm adding the new metadata that should be up-to-date.
2025-02-14 16:55:15 -03:00
renovate[bot]
ef0b101fea chore(deps): update dependency @edx/frontend-platform to v8.1.5 (#566)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-20 08:48:36 +00:00
renovate[bot]
edb22316b8 chore(deps): update dependency @openedx/paragon to v22.13.0 (#564)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-30 06:16:38 +00:00
renovate[bot]
227a97afa1 chore(deps): update dependency @edx/browserslist-config to v1.4.0 (#563)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-30 05:11:57 +00:00
renovate[bot]
d01486e5f7 chore(deps): update dependency react-router-dom to v6.28.1 (#562)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-23 06:17:00 +00:00
renovate[bot]
a58f1eaf19 chore(deps): update dependency @edx/frontend-platform to v8.1.3 (#561)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-16 06:24:09 +00:00
Brian Smith
a5024c3fde fix: move overflow: hidden to address mixed-decls warning (#549)
https://sass-lang.com/documentation/breaking-changes/mixed-decls/
2024-12-09 12:20:38 -05:00
renovate[bot]
d7be18e717 chore(deps): update dependency @openedx/frontend-build to v14.2.2 (#559)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 06:44:08 +00:00
97 changed files with 16199 additions and 4440 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@v5
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

@@ -10,13 +10,13 @@ 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 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@v5
with: with:
node-version: ${{ env.NODE_VER }} node-version: ${{ env.NODE_VER }}
- name: Install dependencies - name: Install dependencies
@@ -25,6 +25,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

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>`_.

14
catalog-info.yaml Normal file
View File

@@ -0,0 +1,14 @@
# This file records information about this repo. Its use is described in OEP-55:
# https://open-edx-proposals.readthedocs.io/en/latest/processes/oep-0055-proc-project-maintainers.html
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: "frontend-component-header"
description: "A generic header for the Open edX micro-frontend applications."
annotations:
openedx.org/arch-interest-groups: ""
spec:
owner: group:committers-frontend
type: "library"
lifecycle: "production"

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

View File

@@ -1,6 +1,4 @@
@import "@edx/brand/paragon/fonts"; @use "@openedx/paragon/dist/core.min.css" as paragonCore;
@import "@edx/brand/paragon/variables"; @use "@openedx/paragon/dist/light.min.css" as paragonLight;
@import "@openedx/paragon/scss/core/core";
@import "@edx/brand/paragon/overrides";
@import "@edx/frontend-component-header/index"; @import "@edx/frontend-component-header/index";

View File

@@ -1,8 +0,0 @@
# openedx.yaml
---
owner: edx/fedx-team
tags:
- library
- component
- react

18718
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"
@@ -35,45 +33,41 @@
"devDependencies": { "devDependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2", "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/browserslist-config": "^1.1.1", "@edx/browserslist-config": "^1.1.1",
"@edx/frontend-platform": "8.1.2", "@edx/frontend-platform": "^8.3.1",
"@edx/reactifex": "^2.1.1", "@openedx/frontend-build": "^14.3.2",
"@openedx/frontend-build": "14.2.0", "@openedx/paragon": "^23.0.0",
"@openedx/paragon": "22.10.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.0", "react-router-dom": "6.30.1",
"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.3.0",
"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.3.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"
}, },
"peerDependencies": { "peerDependencies": {
"@edx/frontend-platform": "^7.0.0 || ^8.0.0", "@edx/frontend-platform": "^7.0.0 || ^8.0.0",
"@openedx/paragon": ">= 21.5.7 < 23.0.0", "@openedx/paragon": ">= 22.0.0 < 24.0.0",
"prop-types": "^15.5.10", "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

@@ -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,45 +1,45 @@
.menu { .menu {
position: relative; position: relative;
} }
.menu-content { .menu-content {
position: absolute; position: absolute;
top: 100%; top: 100%;
z-index: 10; z-index: 10;
background: #fff; background: var(--pgn-color-white, #fff);
min-width: 10rem; min-width: 10rem;
&.pin-left { &.pin-left {
left: 0; left: 0;
} }
&.pin-right { &.pin-right {
right: 0; right: 0;
} }
} }
.menu-dropdown-enter { .menu-dropdown-enter {
opacity: 0; opacity: 0;
transform-origin: 75% 0; transform-origin: 75% 0;
transform: scale3d(0.8, 0.8, 1); transform: scale3d(0.8, 0.8, 1);
} }
.menu-dropdown-enter-active { .menu-dropdown-enter-active {
transform-origin: 75% 0; transform-origin: 75% 0;
transition: all 250ms cubic-bezier(0.4, 0, 0.2, 1); transition: all 250ms cubic-bezier(0.4, 0, 0.2, 1);
transform: scale3d(1, 1, 1); transform: scale3d(1, 1, 1);
opacity: 1; opacity: 1;
} }
.menu-dropdown-enter-done {
}
.menu-dropdown-exit { .menu-dropdown-exit {
transform-origin: 75% 0; transform-origin: 75% 0;
transform: scale3d(1, 1, 1); transform: scale3d(1, 1, 1);
opacity: 1; opacity: 1;
} }
.menu-dropdown-exit-active { .menu-dropdown-exit-active {
transform-origin: 75% 0; transform-origin: 75% 0;
transform: scale3d(0.8, 0.8, 1); transform: scale3d(0.8, 0.8, 1);
transition: all 250ms cubic-bezier(0.8, 0, 0.6, 1); transition: all 250ms cubic-bezier(0.8, 0, 0.6, 1);
opacity: 0; opacity: 0;
} }
.menu-dropdown-exit-done {
}

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,61 +20,42 @@ 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,
}
renderMainMenu() {
const { mainMenu } = this.props;
return <DesktopMainMenuSlot menu={mainMenu} />;
}
renderSecondaryMenu() {
const { secondaryMenu } = this.props;
return <DesktopSecondaryMenuSlot menu={secondaryMenu} />;
}
renderUserMenu() {
const {
userMenu, userMenu,
loggedOutItems,
logo,
logoAltText,
logoDestination,
avatar, avatar,
username, username,
intl, loggedIn,
} = this.props; }) => {
const intl = useIntl();
return ( const renderMainMenu = () => <DesktopMainMenuSlot menu={mainMenu} />;
const renderSecondaryMenu = () => <DesktopSecondaryMenuSlot menu={secondaryMenu} />;
const renderUserMenu = () => (
<Menu transitionClassName="menu-dropdown" transitionTimeout={250}> <Menu transitionClassName="menu-dropdown" transitionTimeout={250}>
<MenuTrigger <MenuTrigger
tag="button" tag="button"
aria-label={intl.formatMessage(messages['header.label.account.menu.for'], { username })} 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" 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" /> <DesktopUserMenuToggleSlot avatar={avatar} label={username} />
{username} <CaretIcon role="img" aria-hidden focusable="false" />
</MenuTrigger> </MenuTrigger>
<MenuContent className="mb-0 dropdown-menu show dropdown-menu-right pin-right shadow py-2"> <MenuContent className="mb-0 dropdown-menu show dropdown-menu-right pin-right shadow py-2">
<DesktopUserMenuSlot menu={userMenu} /> <DesktopUserMenuSlot menu={userMenu} />
</MenuContent> </MenuContent>
</Menu> </Menu>
); );
}
renderLoggedOutItems() { const renderLoggedOutItems = () => <DesktopLoggedOutItemsSlot items={loggedOutItems} />;
const { loggedOutItems } = this.props;
return <DesktopLoggedOutItemsSlot items={loggedOutItems} />;
}
render() {
const {
logo,
logoAltText,
logoDestination,
loggedIn,
intl,
} = this.props;
const logoProps = { src: logo, alt: logoAltText, href: logoDestination }; const logoProps = { src: logo, alt: logoAltText, href: logoDestination };
const logoClasses = getConfig().AUTHN_MINIMAL_HEADER ? 'mw-100' : null; const logoClasses = getConfig().AUTHN_MINIMAL_HEADER ? 'mw-100' : null;
@@ -87,7 +69,7 @@ class DesktopHeader extends React.Component {
aria-label={intl.formatMessage(messages['header.label.main.nav'])} aria-label={intl.formatMessage(messages['header.label.main.nav'])}
className="nav main-nav" className="nav main-nav"
> >
{this.renderMainMenu()} {renderMainMenu()}
</nav> </nav>
<nav <nav
aria-label={intl.formatMessage(messages['header.label.secondary.nav'])} aria-label={intl.formatMessage(messages['header.label.secondary.nav'])}
@@ -96,17 +78,16 @@ class DesktopHeader extends React.Component {
{loggedIn {loggedIn
? ( ? (
<> <>
{this.renderSecondaryMenu()} {renderSecondaryMenu()}
{this.renderUserMenu()} {renderUserMenu()}
</> </>
) : this.renderLoggedOutItems()} ) : renderLoggedOutItems()}
</nav> </nav>
</div> </div>
</div> </div>
</header> </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

@@ -1,6 +1,9 @@
$spacer: 1rem; $spacer: 1rem;
$blue: #007db8; $blue: #007db8;
$white: #fff; $white: #fff;
$component-active-bg: #0A3055FF !default;
$component-active-color: $white !default;
$rounded-pill: 50rem !default;
@import './Menu/menu.scss'; @import './Menu/menu.scss';
@import './studio-header/StudioHeader.scss'; @import './studio-header/StudioHeader.scss';
@@ -21,8 +24,9 @@ $white: #fff;
padding: .75rem; padding: .75rem;
justify-content: center; justify-content: center;
align-items:center; align-items:center;
&:hover, &:focus { &:hover, &:focus {
background: rgba(0,0,0,.1); background: rgba(0, 0, 0, .1);
} }
} }
@@ -36,17 +40,12 @@ $white: #fff;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
padding-bottom: 0.1rem; padding-bottom: calc(var(--pgn-spacing-spacer-base, $spacer)* 0.1);
} }
} }
.user-dropdown { .user-dropdown .btn {
.btn {
height: 3rem; height: 3rem;
// @media (max-width: -1 + map-get($grid-breakpoints, "sm")) {
// padding: 0 0.5rem;
// }
}
} }
} }
@@ -63,6 +62,7 @@ $white: #fff;
text-decoration: none; text-decoration: none;
cursor: pointer; cursor: pointer;
} }
img { img {
height: 1.5rem; height: 1.5rem;
} }
@@ -70,19 +70,22 @@ $white: #fff;
.site-header-desktop { .site-header-desktop {
box-shadow: 0 1px 0 0 rgba(0,0,0,.1); box-shadow: 0 1px 0 0 rgba(0, 0, 0, .1);
background: $white; background: var(--pgn-color-white, $white);
.nav-link { .nav-link {
text-decoration: none; text-decoration: none;
} }
.logo { .logo {
display: block; display: block;
box-sizing: content-box; box-sizing: content-box;
position: relative; position: relative;
top: -.05em; top: -.05em;
height: 1.75rem; height: 1.75rem;
padding: 1rem 0; padding: var(--pgn-spacing-spacer-base, $spacer) 0;
margin-right: 1rem; margin-right: var(--pgn-spacing-spacer-base, $spacer);
img { img {
display: block; display: block;
height: 100%; height: 100%;
@@ -93,38 +96,42 @@ $white: #fff;
.nav-link:focus, .nav-link:focus,
.nav-link.active, .nav-link.active,
.expanded .nav-link { .expanded .nav-link {
background: $component-active-bg; background: var(--pgn-color-bg-active, $component-active-bg);
color: $component-active-color; color: var(--pgn-color-active, $component-active-color);
} }
} }
.main-nav { .main-nav {
.nav-link { .nav-link {
padding: 1.125rem 1rem; padding: 1.125rem var(--pgn-spacing-spacer-base, $spacer);
text-decoration: none; text-decoration: none;
font-weight: 500; font-weight: 500;
letter-spacing: .01em; letter-spacing: .01em;
} }
.nav-link:hover, .nav-link:hover,
.nav-link:focus, .nav-link:focus,
.nav-link.active, .nav-link.active,
.expanded .nav-link { .expanded .nav-link {
background: $component-active-bg; background: var(--pgn-color-bg-active, $component-active-bg);
color: $component-active-color; color: var(--pgn-color-active, $component-active-color);
} }
.menu { .menu {
position: static; position: static;
.menu-content { .menu-content {
border-top: solid 2px $component-active-bg; border-top: solid 2px var(--pgn-color-bg-active);
left: 0; left: 0;
right: 0; right: 0;
box-shadow: 0 1px 2px rgba(0,0,0,.25); box-shadow: var(--pgn-elevation-box-shadow-down-1, 0 1px 2px rgba(0,0,0,.25));
border-bottom-left-radius: 2px; border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px; border-bottom-right-radius: 2px;
padding: 1rem; padding: var(--pgn-spacing-spacer-base, $spacer);
} }
} }
} }
.search-input { .search-input {
border-radius: $rounded-pill; border-radius: var(--pgn-size-rounded-pill, $rounded-pill);
} }
} }

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,40 +20,29 @@ 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,
renderMainMenu() {
const { mainMenu, secondaryMenu } = this.props;
return <MobileMainMenuSlot menu={[...mainMenu, ...secondaryMenu]} />;
}
renderUserMenuItems() {
const { userMenu } = this.props;
return <MobileUserMenuSlot menu={userMenu} />;
}
renderLoggedOutItems() {
const { loggedOutItems } = this.props;
return <MobileLoggedOutItemsSlot items={loggedOutItems} />;
}
render() {
const {
logo, logo,
logoAltText, logoAltText,
logoDestination, logoDestination,
loggedIn,
avatar, avatar,
username, username,
loggedIn,
stickyOnMobile, stickyOnMobile,
intl, }) => {
mainMenu, const intl = useIntl();
userMenu,
loggedOutItems, const renderMainMenu = () => <MobileMainMenuSlot menu={[...mainMenu, ...secondaryMenu]} />;
} = this.props;
const renderUserMenuItems = () => <MobileUserMenuSlot menu={userMenu} />;
const renderLoggedOutItems = () => <MobileLoggedOutItemsSlot items={loggedOutItems} />;
const renderUserMenuToggle = () => <MobileUserMenuToggleSlot avatar={avatar} label={username} />;
const logoProps = { src: logo, alt: logoAltText, href: logoDestination }; const logoProps = { src: logo, alt: logoAltText, href: logoDestination };
const stickyClassName = stickyOnMobile ? 'sticky-top' : ''; const stickyClassName = stickyOnMobile ? 'sticky-top' : '';
const logoClasses = getConfig().AUTHN_MINIMAL_HEADER ? 'justify-content-left pl-3' : 'justify-content-center'; const logoClasses = getConfig().AUTHN_MINIMAL_HEADER ? 'justify-content-left pl-3' : 'justify-content-center';
@@ -81,7 +70,7 @@ class MobileHeader extends React.Component {
aria-label={intl.formatMessage(messages['header.label.main.nav'])} aria-label={intl.formatMessage(messages['header.label.main.nav'])}
className="nav flex-column pin-left pin-right border-top shadow py-2" className="nav flex-column pin-left pin-right border-top shadow py-2"
> >
{this.renderMainMenu()} {renderMainMenu()}
</MenuContent> </MenuContent>
</Menu> </Menu>
</div> </div>
@@ -98,18 +87,17 @@ class MobileHeader extends React.Component {
aria-label={intl.formatMessage(messages['header.label.account.menu'])} aria-label={intl.formatMessage(messages['header.label.account.menu'])}
title={intl.formatMessage(messages['header.label.account.menu'])} title={intl.formatMessage(messages['header.label.account.menu'])}
> >
<Avatar size="1.5rem" src={avatar} alt={username} /> {renderUserMenuToggle()}
</MenuTrigger> </MenuTrigger>
<MenuContent tag="ul" className="nav flex-column pin-left pin-right border-top shadow py-2"> <MenuContent tag="ul" className="nav flex-column pin-left pin-right border-top shadow py-2">
{loggedIn ? this.renderUserMenuItems() : this.renderLoggedOutItems()} {loggedIn ? renderUserMenuItems() : renderLoggedOutItems()}
</MenuContent> </MenuContent>
</Menu> </Menu>
</div> </div>
) : null} ) : null}
</header> </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: [
{ {

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

@@ -7,10 +7,10 @@ $white: #FFFFFF;
height: 3.75rem; height: 3.75rem;
box-shadow: 0 1px 0 0 rgb(0 0 0 / .1); box-shadow: 0 1px 0 0 rgb(0 0 0 / .1);
background: $white; background: var(--pgn-color-white, $white);
.btn-outline-primary { .btn-outline-primary {
border-color: $white; border-color: var(--pgn-color-white, $white);
} }
.logo { .logo {
@@ -19,8 +19,8 @@ $white: #FFFFFF;
position: relative; position: relative;
top: -.05em; top: -.05em;
height: 1.75rem; height: 1.75rem;
padding: $spacer 0; padding: var(--pgn-spacing-spacer-base, $spacer) 0;
margin-right: $spacer; margin-right: var(--pgn-spacing-spacer-base, $spacer);
img { img {
display: block; display: block;
@@ -29,17 +29,17 @@ $white: #FFFFFF;
} }
.course-title-lockup { .course-title-lockup {
overflow: hidden;
@media only screen and (min-width: 769px) { @media only screen and (min-width: 769px) {
padding: .5rem; padding: .5rem;
padding-right: $spacer; padding-right: var(--pgn-spacing-spacer-base, $spacer);
border-right: 1px solid #E5E5E5; border-right: 1px solid #E5E5E5;
width: 70%; width: 70%;
} }
overflow: hidden;
span { span {
color: #333333; color: var(--pgn-color-gray-800, #333333);
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;

View File

@@ -25,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: {
@@ -54,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',
@@ -71,7 +71,7 @@ const props = {
}, },
], ],
outlineLink: 'tEsTLInK', outlineLink: 'tEsTLInK',
searchButtonAction: null, searchButtonAction: undefined,
isNewHomePage: true, isNewHomePage: true,
}; };
@@ -142,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();
}); });

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;

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"]
}

View File

@@ -2,7 +2,9 @@ const path = require('path');
const { createConfig } = require('@openedx/frontend-build'); const { createConfig } = require('@openedx/frontend-build');
module.exports = createConfig('webpack-dev', { module.exports = createConfig('webpack-dev', {
entry: path.resolve(__dirname, 'example'), entry: {
app: path.resolve(__dirname, 'example'),
},
output: { output: {
path: path.resolve(__dirname, 'example/dist'), path: path.resolve(__dirname, 'example/dist'),
publicPath: '/', publicPath: '/',