Compare commits

...

141 Commits

Author SHA1 Message Date
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
renovate[bot]
5e405da37e fix(deps): update dependency @openedx/frontend-plugin-framework to v1.4.1 (#558)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-02 07:19:05 +00:00
renovate[bot]
901f39f42c chore(deps): update dependency @openedx/frontend-build to v14.2.0 (#557)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-25 06:30:07 +00:00
Brian Smith
346a636b76 feat: add pluginProps to CourseInfoSlot (#550) 2024-11-18 14:22:32 -05:00
renovate[bot]
34dcc88880 chore(deps): update dependency @openedx/paragon to v22.10.0 (#554)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-18 05:02:52 +00:00
Kyle McCormick
a229c34535 fix: Remove Studio Maintenance link (#553)
This Studio Maintenance app has been broken for a long time,
so it is being removed from edx-platform:
https://github.com/openedx/edx-platform/pull/35852
2024-11-15 10:52:36 -05:00
renovate[bot]
5d7b4fecf4 chore(deps): update dependency react-router-dom to v6.28.0 (#548)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-11 07:42:32 +00:00
renovate[bot]
f04130a7c6 fix(deps): update dependency @openedx/frontend-plugin-framework to v1.4.0 2024-11-04 07:18:59 +00:00
Peter Kulko
cb7774b325 feat: improved SPA routes 2024-11-01 13:03:59 -03:00
Bilal Qamar
3e4eb21d8c test: Remove support for Node 18 (#536) 2024-10-31 16:05:16 -04:00
Brian Smith
a346dccd4c feat: add frontend-plugin-framework slots (#545)
Add the following `frontend-plugin-framework` slots:
* `logo_slot`
* `desktop_main_menu_slot`
* `desktop_secondary_menu_slot`
* `mobile_main_menu_slot`
* `course_info_slot`
* `learning_help_slot`
* `desktop_logged_out_items_slot`
* `mobile_logged_out_items_slot`
* `mobile_user_menu_slot`
* `desktop_user_menu_slot`
* `learning_user_menu_slot`
* `learning_logged_out_items_slot`
* `desktop_header_slot`
2024-10-22 12:18:11 -04:00
renovate[bot]
c64a201072 chore(deps): update dependency @openedx/paragon to v22.9.0 2024-10-21 04:45:01 +00:00
renovate[bot]
6496642643 chore(deps): update dependency react-router-dom to v6.27.0 2024-10-14 04:43:06 +00:00
renovate[bot]
a6c36654b4 chore(deps): update dependency @edx/frontend-platform to v8.1.2 2024-10-07 07:34:11 +00:00
Rômulo Penido
ae5253c822 feat: expose containerProps in StudioHeader [FC-0062] (#529) 2024-10-01 09:20:34 -04:00
renovate[bot]
e44001e945 chore(deps): update dependency @openedx/paragon to v22.8.1 2024-09-30 10:31:20 +00:00
renovate[bot]
e07cf665a4 chore(deps): update dependency @openedx/frontend-build to v14.1.5 2024-09-30 06:36:53 +00:00
Brian Smith
8213ee7460 feat: add frontend-plugin-framework LogoSlot (#528) 2024-09-26 16:07:41 -04:00
renovate[bot]
8a7d6eecdf chore(deps): update dependency react-router-dom to v6.26.2 2024-09-23 07:57:54 +00:00
renovate[bot]
a2497eeb22 chore(deps): update dependency @openedx/frontend-build to v14.1.4 2024-09-23 07:57:36 +00:00
Bilal Qamar
a703abad76 build: Upgrade to Node 20 (#535) 2024-09-19 17:50:06 -04:00
Bilal Qamar
3f4d987d12 test: Add Node 20 to CI matrix (#533) 2024-09-16 11:42:21 -04:00
renovate[bot]
45e551ea44 chore(deps): update dependency @openedx/frontend-build to v14.1.2 2024-09-09 06:27:07 +00:00
renovate[bot]
a0d7fd7cf2 chore(deps): update dependency @openedx/frontend-build to v14.1.1 2024-09-02 07:12:22 +00:00
renovate[bot]
97d1bdedfb chore(deps): update dependency @openedx/frontend-build to v14.1.0 2024-08-19 05:29:18 +00:00
renovate[bot]
4351a09c9f chore(deps): update dependency react-router-dom to v6.26.1 2024-08-19 05:29:07 +00:00
renovate[bot]
9647a74507 chore(deps): update dependency @openedx/frontend-build to v14.0.15 2024-08-12 06:28:28 +00:00
renovate[bot]
9cef77349f chore(deps): update dependency react-router-dom to v6.26.0 2024-08-05 04:27:40 +00:00
renovate[bot]
ad7c42bcf9 chore(deps): update dependency @testing-library/dom to v10.4.0 2024-07-29 07:46:13 +00:00
renovate[bot]
266386fe24 chore(deps): update dependency @openedx/frontend-build to v14.0.14 2024-07-29 04:31:36 +00:00
renovate[bot]
f42ee37e16 chore(deps): update dependency @edx/frontend-platform to v8.1.1 2024-07-29 04:31:28 +00:00
renovate[bot]
354f9fdc38 fix(deps): update font awesome to v6.6.0 2024-07-22 07:37:43 +00:00
renovate[bot]
85b07acfb5 chore(deps): update dependency react-router-dom to v6.25.1 2024-07-22 07:37:34 +00:00
renovate[bot]
3e647f7394 chore(deps): update dependency @testing-library/dom to v10.3.2 2024-07-22 07:37:20 +00:00
renovate[bot]
848b0f37b9 chore(deps): update dependency @openedx/paragon to v22.7.0 2024-07-22 04:22:28 +00:00
renovate[bot]
818b3800aa chore(deps): update dependency @edx/frontend-platform to v8.1.0 2024-07-15 07:42:56 +00:00
renovate[bot]
63e47bc45a chore(deps): update dependency react-router-dom to v6.24.1 2024-07-15 07:42:35 +00:00
renovate[bot]
7ba5371f69 chore(deps): update dependency react-router-dom to v6.24.0 2024-07-01 04:14:48 +00:00
renovate[bot]
9f0c286897 chore(deps): update dependency @testing-library/dom to v10.2.0 2024-07-01 04:14:36 +00:00
renovate[bot]
4cc5b91d6d chore(deps): update dependency @openedx/paragon to v22.6.1 2024-06-24 08:01:58 +00:00
renovate[bot]
3d75a72f0c chore(deps): update dependency @openedx/frontend-build to v14.0.10 2024-06-24 04:49:34 +00:00
Adolfo R. Brandes
0541dc194e build: Update codecov and use token
Update codecov to the latest version and start using the org-wide token for uploads.

See https://github.com/openedx/wg-frontend/issues/179
2024-06-17 11:59:43 -03:00
renovate[bot]
b92127fd12 chore(deps): update dependency @openedx/frontend-build to v14.0.9 2024-06-17 04:24:47 +00:00
renovate[bot]
b2b9f3fa00 chore(deps): update dependency @openedx/paragon to v22.6.0 2024-06-03 07:27:47 +00:00
renovate[bot]
b9b6282b4b chore(deps): update dependency @edx/frontend-platform to v8.0.4 2024-06-03 07:27:20 +00:00
renovate[bot]
8606585978 chore(deps): update dependency @testing-library/dom to v10.1.0 2024-05-27 07:11:48 +00:00
renovate[bot]
159072779f chore(deps): update dependency @openedx/paragon to v22.5.1 2024-05-27 04:16:29 +00:00
renovate[bot]
de843d330d fix(deps): update dependency @fortawesome/react-fontawesome to v0.2.2 2024-05-27 04:15:58 +00:00
renovate[bot]
d554de89ca fix(deps): update dependency @fortawesome/react-fontawesome to v0.2.1 2024-05-20 07:06:56 +00:00
renovate[bot]
72be96c230 chore(deps): update dependency @edx/frontend-platform to v8.0.3 2024-05-20 07:06:23 +00:00
renovate[bot]
564f34a7c6 chore(deps): update dependency react-router-dom to v6.23.1 2024-05-13 06:55:33 +00:00
renovate[bot]
ab15b3d2bf chore(deps): update dependency @edx/frontend-platform to v8.0.2 2024-05-13 06:55:03 +00:00
Ahtesham Quraish
59db41c61e fix: provide onClick feature
Provide onClick facility for all the kind of menus
VAN-1914
2024-05-07 15:55:57 +01:00
renovate[bot]
0415c00353 chore(deps): update dependency react-router-dom to v6.23.0 2024-04-29 04:22:05 +00:00
renovate[bot]
9d01c074e0 chore(deps): update dependency @openedx/paragon to v22.3.1 2024-04-29 04:21:31 +00:00
Bilal Qamar
83c5b0258f feat: Updated frontend-build to bump jest version to v29 (#369)
* refactor: updated frontend-build to bump jest version to v29

* refactor: resolved failing tests

* refactor: updated frontend-build & failing snapshots

* refactor: updated snapshots

* refactor: updated frontend-build to alpha, updated snapshots

* fix: fixed peer deps issues

* refactor: updated frontend-build to the version which has eslint alpha synced with master

* refactor: updated eslintrc

* refactor: updated ci workflow

* feat: updated frontend-build and frontend-platform major versions

* refactor: updated package-lock

* refactor: updated peerDependency as per suggestion

---------

Co-authored-by: Muhammad Abdullah Waheed <abdullah.waheed@arbisoft.com>
2024-04-23 13:57:10 +05:00
renovate[bot]
45246ad5ee chore(deps): update dependency @openedx/paragon to v22.2.2 2024-04-22 05:14:02 +00:00
renovate[bot]
487b2590bd chore(deps): update dependency @edx/frontend-platform to v7.1.4 2024-04-22 05:13:44 +00:00
Syed Sajjad Hussain Shah
6cab3f3f3e Merge pull request #466 from openedx/sajjad/VAN-1823-custom-header
feat: enable header to accept custom menus
2024-04-19 11:36:03 +05:00
renovate[bot]
e3c8ec027e chore(deps): update dependency @testing-library/dom to v10 2024-04-15 07:39:31 +00:00
renovate[bot]
1e899c1c48 chore(deps): update dependency react-router-dom to v6.22.3 2024-04-15 04:27:37 +00:00
renovate[bot]
370b193df3 fix(deps): update font awesome to v6.5.2 2024-04-08 06:51:52 +00:00
renovate[bot]
58c34abd66 chore(deps): update dependency @openedx/frontend-build to v13.1.4 2024-04-08 06:51:23 +00:00
Rômulo Penido
c9942c1552 feat: add search menu button to header (#474) 2024-04-03 14:36:39 -04:00
renovate[bot]
432dbb5e6b chore(deps): update dependency @openedx/paragon to v22.2.1 2024-04-01 07:23:00 +00:00
renovate[bot]
02748fab13 chore(deps): update dependency @edx/brand to v1.2.3 2024-04-01 07:22:25 +00:00
renovate[bot]
3a5506c646 chore(deps): update dependency @openedx/frontend-build to v13.1.0 2024-04-01 07:18:24 +00:00
renovate[bot]
10619ceb5e chore(deps): update dependency @openedx/paragon to v22.1.1 2024-03-25 15:49:02 +00:00
renovate[bot]
2694492a7c chore(deps): update dependency @edx/frontend-platform to v7.1.3 2024-03-25 13:34:02 +00:00
renovate[bot]
be7d0d97e4 chore(deps): update dependency @openedx/frontend-build to v13.0.29 2024-03-25 10:26:14 +00:00
Syed Sajjad Hussain Shah
e6aa4be4f6 fix: update docs 2024-03-25 12:49:25 +05:00
Syed Sajjad Hussain Shah
f2c236c828 fix: remove logo URL 2024-03-25 12:49:25 +05:00
Syed Sajjad Hussain Shah
db912e6dae feat: enable header to accept custom menus 2024-03-25 12:49:25 +05:00
Samir Sabri
abb08be08e feat!: remove Transifex calls for OEP-58 (#416) 2024-03-22 13:08:10 -04:00
Bilal Qamar
6c6ccc7f20 chore(deps): update frontend-platform to v7 (#469)
* chore(deps): update frontend-platform to v7

* refactor: updated eslintrc
2024-03-22 11:34:40 +05:00
Adolfo R. Brandes
2494ad2b57 build: start releasing alpha packages 2024-03-20 13:09:30 -03:00
Jenkins
3b2a2bfa95 chore(i18n): update translations 2024-01-28 15:31:28 -05:00
renovate[bot]
30b91791e3 chore(deps): update dependency react-router-dom to v6.21.3 2024-01-22 09:41:11 +00:00
Brian Smith
08592aeec7 fix: update paragon dep to support v22 (#460) 2024-01-19 11:30:44 -05:00
Brian Smith
c1d143ace2 fix: update frontend-platform peer dependency (#459)
This updates the `frontend-platform` peer dependency to require a version that has paragon in the `openedx` scope as a peer dependency, as opposed to the `edx` scope
2024-01-18 12:59:14 -05:00
Brian Smith
54a879aec2 chore!: move paragon to peer dependency using @openedx scope (#456)
BREAKING CHANGE: consuming applications must now provide `paragon` from the `@openedx` scope
2024-01-16 11:06:27 -05:00
renovate[bot]
b5b37f1d64 chore(deps): update dependency @testing-library/dom to v9.3.4 2024-01-15 13:19:09 +00:00
renovate[bot]
36be99ace0 chore(deps): update dependency @openedx/frontend-build to v13.0.27 2024-01-15 11:42:22 +00:00
dependabot[bot]
222bc19bd0 chore(deps): bump follow-redirects from 1.15.2 to 1.15.4 (#454)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.2 to 1.15.4.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.2...v1.15.4)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-11 17:14:11 +05:00
renovate[bot]
3d827e64ea chore(deps): update dependency redux-saga to v1.3.0 (#453)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-08 15:40:28 +05:00
renovate[bot]
8a247abd6a fix(deps): update dependency @openedx/paragon to v21.13.0 2024-01-01 12:25:49 +00:00
renovate[bot]
a6943fbaeb chore(deps): update dependency @edx/frontend-platform to v6.2.0 2024-01-01 10:17:53 +00:00
Mashal Malik
19292cd5b6 refactor: replace @edx/paragon with @openedx/paragon (#446)
* refactor: replace @edx/paragon with @openedx/paragon

* fix: replaced frontend-build to @openedx to fix jest issues

* refactor: removed old unused package

---------

Co-authored-by: Abdullah Waheed <abdullah.waheed@arbisoft.com>
2024-01-01 13:08:15 +05:00
Syed Ali Abbas Zaidi
b6374a5c05 feat: migrate enzyme with RTL (#439)
Co-authored-by: Abdullah Waheed <abdullah.waheed@arbisoft.com>
2023-12-25 16:23:33 +05:00
156 changed files with 16382 additions and 14911 deletions

View File

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

View File

@@ -14,18 +14,18 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- name: Setup Nodejs
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VER }}
node-version-file: '.nvmrc'
- name: Install dependencies
run: npm ci
- name: Validate package-lock.json changes
run: make validate-no-uncommitted-package-lock-changes
- name: Lint
run: npm run lint
- name: Type check
run: npm run types
- name: Test
run: npm run test
- name: Build
@@ -33,4 +33,7 @@ jobs:
- name: i18n_extract
run: npm run i18n_extract
- name: Coverage
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true

View File

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

View File

@@ -3,6 +3,7 @@ on:
push:
branches:
- master
- alpha
jobs:
release:
name: Release
@@ -24,12 +25,17 @@ jobs:
run: make validate-no-uncommitted-package-lock-changes
- name: Lint
run: npm run lint
- name: Type check
run: npm run types
- name: Test
run: npm run test
- name: i18n_extract
run: npm run i18n_extract
- name: Coverage
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
- name: Build
run: npm run build
- name: Release

1
.gitignore vendored
View File

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

2
.nvmrc
View File

@@ -1 +1 @@
18
20

View File

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

View File

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

View File

@@ -1,6 +1,3 @@
export TRANSIFEX_RESOURCE = frontend-component-header
transifex_langs = "ar,fr,es_419,zh_CN,pt,it,de,uk,ru,hi,fr_CA"
transifex_utils = ./node_modules/.bin/transifex-utils.js
i18n = ./src/i18n
transifex_input = $(i18n)/transifex_input.json
@@ -35,21 +32,6 @@ detect_changed_source_translations:
# Checking for changed translations...
git diff --exit-code $(i18n)
# Pushes translations to Transifex. You must run make extract_translations first.
push_translations:
# Pushing strings to Transifex...
tx push -s
# Fetching hashes from Transifex...
./node_modules/@edx/reactifex/bash_scripts/get_hashed_strings_v3.sh
# Writing out comments to file...
$(transifex_utils) $(transifex_temp) --comments --v3-scripts-path
# Pushing comments to Transifex...
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
# Pulls translations from Transifex.
pull_translations:
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
# This target is used by Travis.
validate-no-uncommitted-package-lock-changes:
# Checking for package-lock.json changes...

View File

@@ -95,6 +95,12 @@ This library has the following exports:
* ``messages``: Internationalization messages suitable for use with `@edx/frontend-platform/i18n <https://edx.github.io/frontend-platform/module-Internationalization.html>`_
* ``dist/index.scss``: A SASS file which contains style information for the component. It should be imported into the micro-frontend's own SCSS file.
Plugins
-------
This can be customized using `Frontend Plugin Framework <https://github.com/openedx/frontend-plugin-framework>`_.
The parts of this that can be customized in that manner are documented `here </src/plugin-slots>`_.
Examples
========

View File

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

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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

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

View File

@@ -1,5 +1,3 @@
import 'babel-polyfill';
import React from 'react';
import ReactDOM from 'react-dom';
import { initialize, getConfig, subscribe, APP_READY } from '@edx/frontend-platform';

View File

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

View File

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

View File

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

26523
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,18 +10,16 @@
"build": "make build",
"i18n_extract": "fedx-scripts formatjs extract",
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
"snapshot": "fedx-scripts jest --updateSnapshot",
"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": [
"/dist"
],
"husky": {
"hooks": {
"pre-commit": "npm run lint"
}
},
"repository": {
"type": "git",
"url": "git+https://github.com/openedx/frontend-component-header.git"
@@ -35,42 +33,42 @@
"devDependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/browserslist-config": "^1.1.1",
"@edx/frontend-build": "13.0.14",
"@edx/frontend-platform": "6.1.2",
"@edx/frontend-platform": "^8.3.1",
"@edx/reactifex": "^2.1.1",
"@testing-library/dom": "9.3.3",
"@openedx/frontend-build": "^14.3.2",
"@openedx/paragon": "^23.0.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "5.17.0",
"@testing-library/react": "10.4.9",
"@wojtekmaj/enzyme-adapter-react-17": "0.8.0",
"enzyme": "3.11.0",
"husky": "8.0.3",
"jest": "29.7.0",
"jest-chain": "1.1.6",
"@testing-library/react": "^16.2.0",
"jest": "30.0.5",
"jest-environment-jsdom": "^30.0.0",
"prop-types": "15.8.1",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-redux": "7.2.9",
"react-router-dom": "6.21.1",
"react-test-renderer": "17.0.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-redux": "^8.1.1",
"react-router-dom": "6.30.1",
"react-test-renderer": "^18.3.1",
"redux": "4.2.1",
"redux-saga": "1.2.3"
"redux-saga": "1.3.0"
},
"dependencies": {
"@edx/paragon": "21.5.6",
"@fortawesome/fontawesome-svg-core": "6.5.1",
"@fortawesome/free-brands-svg-icons": "6.5.1",
"@fortawesome/free-regular-svg-icons": "6.5.1",
"@fortawesome/free-solid-svg-icons": "6.5.1",
"@fortawesome/fontawesome-svg-core": "6.7.2",
"@fortawesome/free-brands-svg-icons": "6.7.2",
"@fortawesome/free-regular-svg-icons": "6.7.2",
"@fortawesome/free-solid-svg-icons": "6.7.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"axios-mock-adapter": "1.22.0",
"babel-polyfill": "6.26.0",
"@openedx/frontend-plugin-framework": "^1.7.0",
"classnames": "^2.5.1",
"react-responsive": "8.2.0",
"react-transition-group": "4.4.5"
},
"peerDependencies": {
"@edx/frontend-platform": "^4.0.0 || ^5.0.0 || ^6.0.0",
"@edx/frontend-platform": "^7.0.0 || ^8.0.0",
"@openedx/paragon": ">= 22.0.0 < 24.0.0",
"prop-types": "^15.5.10",
"react": "^16.9.0 || ^17.0.0",
"react-dom": "^16.9.0 || ^17.0.0"
"react": "^16.9.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.9.0 || ^17.0.0 || ^18.0.0",
"react-router-dom": "^6.14.2"
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,59 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Menu, MenuTrigger, MenuContent } from '../Menu';
import { CaretIcon } from '../Icons';
const DesktopHeaderMainOrSecondaryMenu = ({ menu }) => {
// Nodes are accepted as a prop
if (!Array.isArray(menu)) {
return menu;
}
return menu.map((menuItem) => {
const {
type,
href,
content,
submenuContent,
disabled,
isActive,
onClick,
} = menuItem;
if (type === 'item') {
return (
<a
key={`${type}-${content}`}
className={`nav-link${disabled ? ' disabled' : ''}${isActive ? ' active' : ''}`}
href={href}
onClick={onClick || null}
>
{content}
</a>
);
}
return (
<Menu key={`${type}-${content}`} tag="div" className="nav-item" respondToPointerEvents>
<MenuTrigger onClick={onClick || null} tag="a" className="nav-link d-inline-flex align-items-center" href={href}>
{content} <CaretIcon role="img" aria-hidden focusable="false" />
</MenuTrigger>
<MenuContent className="pin-left pin-right shadow py-2">
{submenuContent}
</MenuContent>
</Menu>
);
});
};
export const desktopHeaderMainOrSecondaryMenuDataShape = PropTypes.oneOfType([
PropTypes.node,
PropTypes.array,
]);
DesktopHeaderMainOrSecondaryMenu.propTypes = {
menu: desktopHeaderMainOrSecondaryMenuDataShape,
};
export default DesktopHeaderMainOrSecondaryMenu;

View File

@@ -0,0 +1,39 @@
import React from 'react';
import PropTypes from 'prop-types';
const DesktopHeaderUserMenu = ({ menu }) => menu.map((group, index) => (
// eslint-disable-next-line react/jsx-no-comment-textnodes,react/no-array-index-key
<React.Fragment key={index}>
{group.heading && <div className="dropdown-header" role="heading" aria-level="1">{group.heading}</div>}
{group.items.map(({
type, content, href, disabled, isActive, onClick,
}) => (
<a
className={`dropdown-${type}${isActive ? ' active' : ''}${disabled ? ' disabled' : ''}`}
key={`${type}-${content}`}
href={href}
onClick={onClick || null}
>
{content}
</a>
))}
{index < menu.length - 1 && <div className="dropdown-divider" role="separator" />}
</React.Fragment>
));
export const desktopUserMenuDataShape = PropTypes.arrayOf(PropTypes.shape({
heading: PropTypes.string,
items: PropTypes.arrayOf(PropTypes.shape({
type: PropTypes.oneOf(['item', 'menu']),
href: PropTypes.string,
content: PropTypes.string,
isActive: PropTypes.bool,
onClick: PropTypes.func,
})),
}));
DesktopHeaderUserMenu.propTypes = {
menu: desktopUserMenuDataShape,
};
export default DesktopHeaderUserMenu;

View File

@@ -0,0 +1,24 @@
import React from 'react';
import PropTypes from 'prop-types';
const DesktopLoggedOutItems = ({ items }) => items.map((item, i, arr) => (
<a
key={`${item.type}-${item.content}`}
className={i < arr.length - 1 ? 'btn mr-2 btn-link' : 'btn mr-2 btn-outline-primary'}
href={item.href}
>
{item.content}
</a>
));
export const desktopLoggedOutItemsDataShape = PropTypes.arrayOf(PropTypes.shape({
type: PropTypes.oneOf(['item', 'menu']),
href: PropTypes.string,
content: PropTypes.string,
}));
DesktopLoggedOutItems.propTypes = {
items: desktopLoggedOutItemsDataShape,
};
export default DesktopLoggedOutItems;

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,36 +0,0 @@
{
"header.links.courses": "पाठ्यक्रम",
"header.links.programs": "कार्यक्रमों",
"header.links.content.search": "नया खोजें",
"header.links.schools": "स्कूलों और भागीदारों",
"header.user.menu.dashboard": "डैशबोर्ड",
"header.user.menu.profile": "प्रोफ़ाइल",
"header.user.menu.account.settings": "खाता",
"header.user.menu.order.history": "ऑर्डर इतिहास",
"header.user.menu.logout": "लॉग आउट",
"header.user.menu.login": "लॉगिन",
"header.user.menu.register": "साइन अप",
"header.user.menu.studio.home": "स्टूडियो होम",
"header.user.menu.studio.maintenance": "अनुरक्षण करना",
"header.label.account.nav": "खाता",
"header.label.account.menu": "खाता मेनू",
"header.label.account.menu.for": "{username} के लिए खाता मेनू",
"header.label.main.nav": "मुख्य",
"header.label.main.menu": "मुख्य मेनू",
"header.label.main.header": "मुख्य",
"header.label.secondary.nav": "माध्यमिक",
"header.label.skip.nav": "मुख्य विषयवस्तु में जाएं",
"header.label.app.nav": "ऐप",
"general.register.sentenceCase": "रजिस्टर करें",
"general.signIn.sentenceCase": "साइन इन करें",
"header.menu.dashboard.label": "डैशबोर्ड",
"header.help.label": "मदद",
"header.menu.profile.label": "प्रोफ़ाइल",
"header.menu.account.label": "खाता",
"header.menu.orderHistory.label": "ऑर्डर इतिहास",
"header.navigation.skipNavLink": "मुख्य सामग्री पर जाएँ।",
"header.menu.signOut.label": "साइन आउट करें",
"header.user.menu.studio": "स्टूडियो होम",
"header.user.menu.maintenance": "अनुरक्षण करना",
"header.label.courseOutline": "स्टूडियो में पाठ्यक्रम की रूपरेखा पर वापस जाएँ"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,27 +3,25 @@ import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button } from '@edx/paragon';
import LearningLoggedOutItemsSlot from '../plugin-slots/LearningLoggedOutItemsSlot';
import genericMessages from '../generic/messages';
const AnonymousUserMenu = ({ intl }) => (
<div>
<Button
className="mr-3"
variant="outline-primary"
href={`${getConfig().LMS_BASE_URL}/register?next=${encodeURIComponent(global.location.href)}`}
>
{intl.formatMessage(genericMessages.registerSentenceCase)}
</Button>
<Button
variant="primary"
href={`${getLoginRedirectUrl(global.location.href)}`}
>
{intl.formatMessage(genericMessages.signInSentenceCase)}
</Button>
</div>
);
const AnonymousUserMenu = ({ intl }) => {
const buttonsInfo = [
{
message: intl.formatMessage(genericMessages.registerSentenceCase),
href: `${getConfig().LMS_BASE_URL}/register?next=${encodeURIComponent(global.location.href)}`,
},
{
message: intl.formatMessage(genericMessages.signInSentenceCase),
href: getLoginRedirectUrl(global.location.href),
variant: 'primary',
},
];
return <LearningLoggedOutItemsSlot buttonsInfo={buttonsInfo} />;
};
AnonymousUserMenu.propTypes = {
intl: intlShape.isRequired,

View File

@@ -1,50 +1,49 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faUserCircle } from '@fortawesome/free-solid-svg-icons';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Dropdown } from '@edx/paragon';
import { Dropdown } from '@openedx/paragon';
import LearningUserMenuToggleSlot from '../plugin-slots/LearningUserMenuToggleSlot';
import LearningUserMenuSlot from '../plugin-slots/LearningUserMenuSlot';
import messages from './messages';
const AuthenticatedUserDropdown = ({ intl, username }) => {
const dashboardMenuItem = (
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/dashboard`}>
{intl.formatMessage(messages.dashboard)}
</Dropdown.Item>
);
const dropdownItems = [
{
message: intl.formatMessage(messages.dashboard),
href: `${getConfig().LMS_BASE_URL}/dashboard`,
},
{
message: intl.formatMessage(messages.profile),
href: `${getConfig().ACCOUNT_PROFILE_URL}/u/${username}`,
},
{
message: intl.formatMessage(messages.account),
href: getConfig().ACCOUNT_SETTINGS_URL,
},
...(getConfig().ORDER_HISTORY_URL ? [{
message: intl.formatMessage(messages.orderHistory),
href: getConfig().ORDER_HISTORY_URL,
}] : []),
{
message: intl.formatMessage(messages.signOut),
href: getConfig().LOGOUT_URL,
},
];
return (
<>
<a className="text-gray-700" href={`${getConfig().SUPPORT_URL}`}>{intl.formatMessage(messages.help)}</a>
<Dropdown className="user-dropdown ml-3">
<Dropdown.Toggle variant="outline-primary">
<FontAwesomeIcon icon={faUserCircle} className="d-md-none" size="lg" />
<span data-hj-suppress className="d-none d-md-inline">
{username}
</span>
</Dropdown.Toggle>
<Dropdown.Menu className="dropdown-menu-right">
{dashboardMenuItem}
<Dropdown.Item href={`${getConfig().ACCOUNT_PROFILE_URL}/u/${username}`}>
{intl.formatMessage(messages.profile)}
</Dropdown.Item>
<Dropdown.Item href={getConfig().ACCOUNT_SETTINGS_URL}>
{intl.formatMessage(messages.account)}
</Dropdown.Item>
{ getConfig().ORDER_HISTORY_URL && (
<Dropdown.Item href={getConfig().ORDER_HISTORY_URL}>
{intl.formatMessage(messages.orderHistory)}
</Dropdown.Item>
)}
<Dropdown.Item href={getConfig().LOGOUT_URL}>
{intl.formatMessage(messages.signOut)}
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
</>
<Dropdown className="user-dropdown ml-3">
<Dropdown.Toggle variant="outline-primary" aria-label={intl.formatMessage(messages.userOptionsDropdownLabel)}>
<LearningUserMenuToggleSlot label={username} icon={faUserCircle} />
</Dropdown.Toggle>
<Dropdown.Menu className="dropdown-menu-right">
<LearningUserMenuSlot items={dropdownItems} />
</Dropdown.Menu>
</Dropdown>
);
};

View File

@@ -6,24 +6,11 @@ import { AppContext } from '@edx/frontend-platform/react';
import AnonymousUserMenu from './AnonymousUserMenu';
import AuthenticatedUserDropdown from './AuthenticatedUserDropdown';
import LogoSlot from '../plugin-slots/LogoSlot';
import CourseInfoSlot from '../plugin-slots/CourseInfoSlot';
import { courseInfoDataShape } from './LearningHeaderCourseInfo';
import messages from './messages';
const LinkedLogo = ({
href,
src,
alt,
...attributes
}) => (
<a href={href} {...attributes}>
<img className="d-block" src={src} alt={alt} />
</a>
);
LinkedLogo.propTypes = {
href: PropTypes.string.isRequired,
src: PropTypes.string.isRequired,
alt: PropTypes.string.isRequired,
};
import LearningHelpSlot from '../plugin-slots/LearningHelpSlot';
const LearningHeader = ({
courseOrg, courseNumber, courseTitle, intl, showUserDropdown,
@@ -31,8 +18,7 @@ const LearningHeader = ({
const { authenticatedUser } = useContext(AppContext);
const headerLogo = (
<LinkedLogo
className="logo"
<LogoSlot
href={`${getConfig().LMS_BASE_URL}/dashboard`}
src={getConfig().LOGO_URL}
alt={getConfig().SITE_NAME}
@@ -44,14 +30,16 @@ const LearningHeader = ({
<a className="sr-only sr-only-focusable" href="#main-content">{intl.formatMessage(messages.skipNavLink)}</a>
<div className="container-xl py-2 d-flex align-items-center">
{headerLogo}
<div className="flex-grow-1 course-title-lockup" style={{ lineHeight: 1 }}>
<span className="d-block small m-0">{courseOrg} {courseNumber}</span>
<span className="d-block m-0 font-weight-bold course-title">{courseTitle}</span>
<div className="flex-grow-1 course-title-lockup d-flex" style={{ lineHeight: 1 }}>
<CourseInfoSlot courseOrg={courseOrg} courseNumber={courseNumber} courseTitle={courseTitle} />
</div>
{showUserDropdown && authenticatedUser && (
<AuthenticatedUserDropdown
username={authenticatedUser.username}
/>
<>
<LearningHelpSlot />
<AuthenticatedUserDropdown
username={authenticatedUser.username}
/>
</>
)}
{showUserDropdown && !authenticatedUser && (
<AnonymousUserMenu />
@@ -62,9 +50,9 @@ const LearningHeader = ({
};
LearningHeader.propTypes = {
courseOrg: PropTypes.string,
courseNumber: PropTypes.string,
courseTitle: PropTypes.string,
courseOrg: courseInfoDataShape.courseOrg,
courseNumber: courseInfoDataShape.courseNumber,
courseTitle: courseInfoDataShape.courseTitle,
intl: intlShape.isRequired,
showUserDropdown: PropTypes.bool,
};

View File

@@ -0,0 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
const LearningHeaderCourseInfo = ({
courseOrg,
courseNumber,
courseTitle,
}) => (
<div style={{ minWidth: 0 }}>
<span className="d-block small m-0">{courseOrg} {courseNumber}</span>
<span className="d-block m-0 font-weight-bold course-title">{courseTitle}</span>
</div>
);
export const courseInfoDataShape = {
courseOrg: PropTypes.string,
courseNumber: PropTypes.string,
courseTitle: PropTypes.string,
};
LearningHeaderCourseInfo.propTypes = courseInfoDataShape;
export default LearningHeaderCourseInfo;

View File

@@ -0,0 +1,14 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import messages from './messages';
const LearningHeaderHelpLink = () => {
const intl = useIntl();
return (
<a className="text-gray-700" href={`${getConfig().SUPPORT_URL}`}>{intl.formatMessage(messages.help)}</a>
);
};
export default LearningHeaderHelpLink;

View File

@@ -0,0 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Dropdown } from '@openedx/paragon';
const LearningHeaderUserMenuItems = ({ items }) => items.map((item) => (
<Dropdown.Item href={item.href}>
{item.message}
</Dropdown.Item>
));
export const learningHeaderUserMenuDataShape = {
items: PropTypes.arrayOf(PropTypes.shape({
message: PropTypes.string,
href: PropTypes.string,
})),
};
LearningHeaderUserMenuItems.propTypes = learningHeaderUserMenuDataShape;
export default LearningHeaderUserMenuItems;

View File

@@ -0,0 +1,26 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button } from '@openedx/paragon';
const LearningLoggedOutButtons = ({ buttonsInfo }) => buttonsInfo.map(buttonInfo => (
<Button
className="ml-3"
variant={buttonInfo.variant ?? 'outline-primary'}
href={buttonInfo.href}
>
{buttonInfo.message}
</Button>
));
export const learningHeaderLoggedOutItemsDataShape = {
buttonsInfo: PropTypes.arrayOf(PropTypes.shape({
message: PropTypes.string,
href: PropTypes.string,
variant: PropTypes.string,
})),
};
LearningLoggedOutButtons.propTypes = learningHeaderLoggedOutItemsDataShape;
export default LearningLoggedOutButtons;

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

View File

@@ -4,81 +4,45 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
// Local Components
import { Menu, MenuTrigger, MenuContent } from './Menu';
import Avatar from './Avatar';
import { LinkedLogo, Logo } from './Logo';
import MobileUserMenuToggleSlot from '../plugin-slots/MobileUserMenuToggleSlot';
import { Menu, MenuTrigger, MenuContent } from '../Menu';
import LogoSlot from '../plugin-slots/LogoSlot';
import MobileLoggedOutItemsSlot from '../plugin-slots/MobileLoggedOutItemsSlot';
import { mobileHeaderLoggedOutItemsDataShape } from './MobileLoggedOutItems';
import MobileMainMenuSlot from '../plugin-slots/MobileMainMenuSlot';
import { mobileHeaderMainMenuDataShape } from './MobileHeaderMainMenu';
import MobileUserMenuSlot from '../plugin-slots/MobileUserMenuSlot';
import { mobileHeaderUserMenuDataShape } from './MobileHeaderUserMenu';
// i18n
import messages from './Header.messages';
import messages from '../Header.messages';
// Assets
import { MenuIcon } from './Icons';
import { MenuIcon } from '../Icons';
class MobileHeader extends React.Component {
constructor(props) { // eslint-disable-line no-useless-constructor
constructor(props) { // eslint-disable-line @typescript-eslint/no-useless-constructor
super(props);
}
renderMainMenu() {
const { mainMenu } = this.props;
// Nodes are accepted as a prop
if (!Array.isArray(mainMenu)) {
return mainMenu;
}
return mainMenu.map((menuItem) => {
const {
type,
href,
content,
submenuContent,
} = menuItem;
if (type === 'item') {
return (
<a key={`${type}-${content}`} className="nav-link" href={href}>
{content}
</a>
);
}
return (
<Menu key={`${type}-${content}`} tag="div" className="nav-item">
<MenuTrigger tag="a" role="button" tabIndex="0" className="nav-link">
{content}
</MenuTrigger>
<MenuContent className="position-static pin-left pin-right py-2">
{submenuContent}
</MenuContent>
</Menu>
);
});
const { mainMenu, secondaryMenu } = this.props;
return <MobileMainMenuSlot menu={[...mainMenu, ...secondaryMenu]} />;
}
renderUserMenuItems() {
const { userMenu } = this.props;
return userMenu.map(({ type, href, content }) => (
<li className="nav-item" key={`${type}-${content}`}>
<a className="nav-link" href={href}>{content}</a>
</li>
));
return <MobileUserMenuSlot menu={userMenu} />;
}
renderLoggedOutItems() {
const { loggedOutItems } = this.props;
return <MobileLoggedOutItemsSlot items={loggedOutItems} />;
}
return loggedOutItems.map(({ type, href, content }, i, arr) => (
<li className="nav-item px-3 my-2" key={`${type}-${content}`}>
<a
className={i < arr.length - 1 ? 'btn btn-block btn-outline-primary' : 'btn btn-block btn-primary'}
href={href}
>
{content}
</a>
</li>
));
renderUserMenuToggle() {
const { avatar, username } = this.props;
return <MobileUserMenuToggleSlot avatar={avatar} label={username} />;
}
render() {
@@ -87,8 +51,6 @@ class MobileHeader extends React.Component {
logoAltText,
logoDestination,
loggedIn,
avatar,
username,
stickyOnMobile,
intl,
mainMenu,
@@ -128,7 +90,7 @@ class MobileHeader extends React.Component {
</div>
) : null}
<div className={`w-100 d-flex ${logoClasses}`}>
{ logoDestination === null ? <Logo className="logo" src={logo} alt={logoAltText} /> : <LinkedLogo className="logo" {...logoProps} itemType="http://schema.org/Organization" />}
<LogoSlot {...logoProps} itemType="http://schema.org/Organization" />
</div>
{userMenu.length > 0 || loggedOutItems.length > 0 ? (
<div className="w-100 d-flex justify-content-end align-items-center">
@@ -139,7 +101,7 @@ class MobileHeader extends React.Component {
aria-label={intl.formatMessage(messages['header.label.account.menu'])}
title={intl.formatMessage(messages['header.label.account.menu'])}
>
<Avatar size="1.5rem" src={avatar} alt={username} />
{this.renderUserMenuToggle()}
</MenuTrigger>
<MenuContent tag="ul" className="nav flex-column pin-left pin-right border-top shadow py-2">
{loggedIn ? this.renderUserMenuItems() : this.renderLoggedOutItems()}
@@ -152,22 +114,11 @@ class MobileHeader extends React.Component {
}
}
MobileHeader.propTypes = {
mainMenu: PropTypes.oneOfType([
PropTypes.node,
PropTypes.array,
]),
userMenu: PropTypes.arrayOf(PropTypes.shape({
type: PropTypes.oneOf(['item', 'menu']),
href: PropTypes.string,
content: PropTypes.string,
})),
loggedOutItems: PropTypes.arrayOf(PropTypes.shape({
type: PropTypes.oneOf(['item', 'menu']),
href: PropTypes.string,
content: PropTypes.string,
})),
export const mobileHeaderDataShape = {
mainMenu: mobileHeaderMainMenuDataShape,
secondaryMenu: mobileHeaderMainMenuDataShape,
userMenu: mobileHeaderUserMenuDataShape,
loggedOutItems: mobileHeaderLoggedOutItemsDataShape,
logo: PropTypes.string,
logoAltText: PropTypes.string,
logoDestination: PropTypes.string,
@@ -175,6 +126,20 @@ MobileHeader.propTypes = {
username: PropTypes.string,
loggedIn: PropTypes.bool,
stickyOnMobile: PropTypes.bool,
};
MobileHeader.propTypes = {
mainMenu: mobileHeaderDataShape.mainMenu,
secondaryMenu: mobileHeaderDataShape.secondaryMenu,
userMenu: mobileHeaderDataShape.userMenu,
loggedOutItems: mobileHeaderDataShape.loggedOutItems,
logo: mobileHeaderDataShape.logo,
logoAltText: mobileHeaderDataShape.logoAltText,
logoDestination: mobileHeaderDataShape.logoDestination,
avatar: mobileHeaderDataShape.avatar,
username: mobileHeaderDataShape.username,
loggedIn: mobileHeaderDataShape.loggedIn,
stickyOnMobile: mobileHeaderDataShape.stickyOnMobile,
// i18n
intl: intlShape.isRequired,
@@ -182,6 +147,7 @@ MobileHeader.propTypes = {
MobileHeader.defaultProps = {
mainMenu: [],
secondaryMenu: [],
userMenu: [],
loggedOutItems: [],
logo: null,

View File

@@ -0,0 +1,58 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Menu, MenuTrigger, MenuContent } from '../Menu';
const MobileHeaderMainMenu = ({ menu }) => {
// Nodes are accepted as a prop
if (!Array.isArray(menu)) {
return menu;
}
return menu.map((menuItem) => {
const {
type,
href,
content,
submenuContent,
disabled,
isActive,
onClick,
} = menuItem;
if (type === 'item') {
return (
<a
key={`${type}-${content}`}
className={`nav-link${disabled ? ' disabled' : ''}${isActive ? ' active' : ''}`}
href={href}
onClick={onClick || null}
>
{content}
</a>
);
}
return (
<Menu key={`${type}-${content}`} tag="div" className="nav-item">
<MenuTrigger onClick={onClick || null} tag="a" role="button" tabIndex="0" className="nav-link">
{content}
</MenuTrigger>
<MenuContent className="position-static pin-left pin-right py-2">
{submenuContent}
</MenuContent>
</Menu>
);
});
};
export const mobileHeaderMainMenuDataShape = PropTypes.oneOfType([
PropTypes.node,
PropTypes.array,
]);
MobileHeaderMainMenu.propTypes = {
menu: mobileHeaderMainMenuDataShape,
};
export default MobileHeaderMainMenu;

View File

@@ -0,0 +1,35 @@
import React from 'react';
import PropTypes from 'prop-types';
const MobileHeaderUserMenu = ({ menu }) => menu.map((group) => (
group.items.map(({
type, content, href, disabled, isActive, onClick,
}) => (
<li className="nav-item" key={`${type}-${content}`}>
<a
className={`nav-link${isActive ? ' active' : ''}${disabled ? ' disabled' : ''}`}
href={href}
onClick={onClick || null}
>
{content}
</a>
</li>
))
));
export const mobileHeaderUserMenuDataShape = PropTypes.arrayOf(PropTypes.shape({
heading: PropTypes.string,
items: PropTypes.arrayOf(PropTypes.shape({
type: PropTypes.oneOf(['item', 'menu']),
href: PropTypes.string,
content: PropTypes.string,
isActive: PropTypes.bool,
onClick: PropTypes.func,
})),
}));
MobileHeaderUserMenu.propTypes = {
menu: mobileHeaderUserMenuDataShape,
};
export default MobileHeaderUserMenu;

View File

@@ -0,0 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
const MobileLoggedOutItems = ({ items }) => items.map(({ type, href, content }, i, arr) => (
<li className="nav-item px-3 my-2" key={`${type}-${content}`}>
<a
className={i < arr.length - 1 ? 'btn btn-block btn-outline-primary' : 'btn btn-block btn-primary'}
href={href}
>
{content}
</a>
</li>
));
export const mobileHeaderLoggedOutItemsDataShape = PropTypes.arrayOf(PropTypes.shape({
type: PropTypes.oneOf(['item', 'menu']),
href: PropTypes.string,
content: PropTypes.string,
}));
MobileLoggedOutItems.propTypes = {
menu: mobileHeaderLoggedOutItemsDataShape,
};
export default MobileLoggedOutItems;

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

@@ -0,0 +1,128 @@
# Course Info Slot
### Slot ID: `org.openedx.frontend.layout.header_learning_course_info.v1`
### Slot ID Aliases
* `course_info_slot`
## Description
This slot is used to replace/modify/hide the course info.
## Examples
### Replace Course Title
The following `env.config.jsx` will replace the course title.
![Screenshot of replaced course title](./images/replace_course_title.png)
```jsx
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const replaceCourseTitle = ( widget ) => {
widget.content.courseTitle = "Custom Course Title";
return widget;
};
const config = {
pluginSlots: {
'org.openedx.frontend.layout.header_learning_course_info.v1': {
keepDefault: true,
plugins: [
{
op: PLUGIN_OPERATIONS.Modify,
widgetId: 'default_contents',
fn: replaceCourseTitle,
},
]
},
},
}
export default config;
```
### Replace Course Info with Custom Component
The following `env.config.jsx` will replace the course info entirely (in this case with a centered 🗺️ `h1`)
![Screenshot of replaced course info with custom component](./images/replace_course_info_with_custom_component.png)
```jsx
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const config = {
pluginSlots: {
'org.openedx.frontend.layout.header_learning_course_info.v1': {
keepDefault: false,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_course_info_component',
type: DIRECT_PLUGIN,
RenderWidget: () => (
<h1 style={{textAlign: 'center'}}>🗺</h1>
),
},
},
]
}
},
}
export default config;
```
### Add Custom Components before and after Course Info
The following `env.config.jsx` will place custom components before and after the course info (in this case centered `h1`s with 🌜 and 🌛).
![Screenshot of added custom components before and after course info](./images/add_custom_components_before_and_after_course_info.png)
```jsx
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const config = {
pluginSlots: {
'org.openedx.frontend.layout.header_learning_course_info.v1': {
keepDefault: true,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_before_course_info_component',
type: DIRECT_PLUGIN,
priority: 10,
RenderWidget: () => (
<h3 style={{
marginTop: 'auto',
marginBottom: 'auto',
marginRight: '0.5rem',
}}>🌜</h3>
),
},
},
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_after_course_info_component',
type: DIRECT_PLUGIN,
priority: 90,
RenderWidget: () => (
<h3 style={{
marginTop: 'auto',
marginBottom: 'auto',
marginLeft: '0.5rem',
}}>🌛</h3>
),
},
},
]
},
},
}
export default config;
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,34 @@
import React from 'react';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import LearningHeaderCourseInfo, { courseInfoDataShape } from '../../learning-header/LearningHeaderCourseInfo';
const CourseInfoSlot = ({
courseOrg,
courseNumber,
courseTitle,
...attributes
}) => (
<PluginSlot
id="org.openedx.frontend.layout.header_learning_course_info.v1"
idAliases={['course_info_slot']}
slotOptions={{
mergeProps: true,
}}
pluginProps={{
courseOrg,
courseNumber,
courseTitle,
}}
>
<LearningHeaderCourseInfo
courseOrg={courseOrg}
courseNumber={courseNumber}
courseTitle={courseTitle}
{...attributes}
/>
</PluginSlot>
);
CourseInfoSlot.propTypes = courseInfoDataShape;
export default CourseInfoSlot;

View File

@@ -0,0 +1,44 @@
# Desktop Header Slot
### Slot ID: `org.openedx.frontend.layout.header_desktop.v1`
### Slot ID Aliases
* `desktop_header_slot`
## Description
This slot is used to replace/modify/hide the entire desktop header.
## Examples
### Custom Component
The following `env.config.jsx` will replace the desktop header entirely (in this case with a centered 🗺️ `h1`)
![Screenshot of custom component](./images/desktop_header_custom_component.png)
```jsx
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const config = {
pluginSlots: {
'org.openedx.frontend.layout.header_desktop.v1': {
keepDefault: false,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_desktop_header_component',
type: DIRECT_PLUGIN,
RenderWidget: () => (
<h1 style={{textAlign: 'center'}}>🗺</h1>
),
},
},
]
}
},
}
export default config;
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -0,0 +1,21 @@
import React from 'react';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import DesktopHeader, { desktopHeaderDataShape } from '../../desktop-header/DesktopHeader';
const DesktopHeaderSlot = ({
props,
}) => (
<PluginSlot
id="org.openedx.frontend.layout.header_desktop.v1"
idAliases={['desktop_header_slot']}
slotOptions={{
mergeProps: true,
}}
>
<DesktopHeader {...props} />
</PluginSlot>
);
DesktopHeaderSlot.propTypes = desktopHeaderDataShape;
export default DesktopHeaderSlot;

View File

@@ -0,0 +1,137 @@
# 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
This slot is used to replace/modify/hide the items shown on desktop when the user is logged out.
## Examples
### Modify Items
The following `env.config.jsx` will modify the items shown on desktop when the user is logged out.
![Screenshot of modified items](./images/desktop_logged_out_items_modify_items.png)
```jsx
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const modifyLoggedOutItems = ( widget ) => {
widget.content.items = [
{
type: 'item',
href: 'https://openedx.org/',
content: 'openedx.org',
},
{
type: 'item',
href: 'https://docs.openedx.org/en/latest/',
content: 'Documentation',
},
{
type: 'item',
href: 'https://discuss.openedx.org/',
content: 'Forums',
}
];
return widget;
};
const config = {
pluginSlots: {
'org.openedx.frontend.layout.header_desktop_logged_out_items.v1': {
keepDefault: true,
plugins: [
{
op: PLUGIN_OPERATIONS.Modify,
widgetId: 'default_contents',
fn: modifyLoggedOutItems,
},
]
},
},
}
export default config;
```
### Replace with Custom Component
The following `env.config.jsx` will replace the items shown on desktop when the user is logged out entirely (in this case with a centered 🗺️ `h1`)
![Screenshot of custom component](./images/desktop_logged_out_items_custom_component.png)
```jsx
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const config = {
pluginSlots: {
'org.openedx.frontend.layout.header_desktop_logged_out_items.v1': {
keepDefault: false,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_logged_out_items_component',
type: DIRECT_PLUGIN,
RenderWidget: () => (
<h1 style={{textAlign: 'center'}}>🗺</h1>
),
},
},
]
},
},
}
export default config;
```
### Add Custom Components before and after
The following `env.config.jsx` will place custom components before and after the items shown on desktop when the user is logged out (in this case centered `h1`s with 🌜 and 🌛).
![Screenshot of custom components before and after](./images/desktop_logged_out_items_custom_components_before_after.png)
```jsx
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const config = {
pluginSlots: {
'org.openedx.frontend.layout.header_desktop_logged_out_items.v1': {
keepDefault: true,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_before_logged_out_items_component',
type: DIRECT_PLUGIN,
priority: 10,
RenderWidget: () => (
<h1 style={{textAlign: 'center'}}>🌜</h1>
),
},
},
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_after_logged_out_items_component',
type: DIRECT_PLUGIN,
priority: 90,
RenderWidget: () => (
<h1 style={{textAlign: 'center'}}>🌛</h1>
),
},
},
]
},
},
}
export default config;
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,23 @@
import React from 'react';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import DesktopLoggedOutItems, { desktopLoggedOutItemsDataShape } from '../../desktop-header/DesktopLoggedOutItems';
const DesktopLoggedOutItemsSlot = ({
items,
}) => (
<PluginSlot
id="org.openedx.frontend.layout.header_desktop_logged_out_items.v1"
idAliases={['desktop_logged_out_items_slot']}
slotOptions={{
mergeProps: true,
}}
>
<DesktopLoggedOutItems items={items} />
</PluginSlot>
);
DesktopLoggedOutItemsSlot.propTypes = {
items: desktopLoggedOutItemsDataShape,
};
export default DesktopLoggedOutItemsSlot;

View File

@@ -0,0 +1,137 @@
# Desktop Main Menu Slot
### Slot ID: `org.openedx.frontend.layout.header_desktop_main_menu.v1`
### Slot ID Aliases
* `desktop_main_menu_slot`
## Description
This slot is used to replace/modify/hide the desktop main menu.
## Examples
### Modify Items
The following `env.config.jsx` will modify the items in the desktop main menu.
![Screenshot of modified items](./images/desktop_main_menu_modify_items.png)
```jsx
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const modifyMainMenu = ( widget ) => {
widget.content.menu = [
{
type: 'item',
href: 'https://openedx.org/',
content: 'openedx.org',
},
{
type: 'item',
href: 'https://docs.openedx.org/en/latest/',
content: 'Documentation',
},
{
type: 'item',
href: 'https://discuss.openedx.org/',
content: 'Forums',
}
];
return widget;
};
const config = {
pluginSlots: {
'org.openedx.frontend.layout.header_desktop_main_menu.v1': {
keepDefault: true,
plugins: [
{
op: PLUGIN_OPERATIONS.Modify,
widgetId: 'default_contents',
fn: modifyMainMenu,
},
]
},
},
}
export default config;
```
### Replace Menu with Custom Component
The following `env.config.jsx` will replace the desktop main menu entirely (in this case with a centered 🗺️ `h1`)
![Screenshot of custom component](./images/desktop_main_menu_custom_component.png)
```jsx
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const config = {
pluginSlots: {
'org.openedx.frontend.layout.header_desktop_main_menu.v1': {
keepDefault: false,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_main_menu_component',
type: DIRECT_PLUGIN,
RenderWidget: () => (
<h1 style={{textAlign: 'center'}}>🗺</h1>
),
},
},
]
},
},
}
export default config;
```
### Add Custom Components before and after Menu
The following `env.config.jsx` will place custom components before and after the desktop main menu (in this case centered `h1`s with 🌜 and 🌛).
![Screenshot of custom components before and after](./images/desktop_main_menu_custom_components_before_after.png)
```jsx
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const config = {
pluginSlots: {
'org.openedx.frontend.layout.header_desktop_main_menu.v1': {
keepDefault: true,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_before_main_menu_component',
type: DIRECT_PLUGIN,
priority: 10,
RenderWidget: () => (
<h1 style={{textAlign: 'center'}}>🌜</h1>
),
},
},
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_after_main_menu_component',
type: DIRECT_PLUGIN,
priority: 90,
RenderWidget: () => (
<h1 style={{textAlign: 'center'}}>🌛</h1>
),
},
},
]
},
},
}
export default config;
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,23 @@
import React from 'react';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import DesktopHeaderMainOrSecondaryMenu, { desktopHeaderMainOrSecondaryMenuDataShape } from '../../desktop-header/DesktopHeaderMainOrSecondaryMenu';
const DesktopMainMenuSlot = ({
menu,
}) => (
<PluginSlot
id="org.openedx.frontend.layout.header_desktop_main_menu.v1"
idAliases={['desktop_main_menu_slot']}
slotOptions={{
mergeProps: true,
}}
>
<DesktopHeaderMainOrSecondaryMenu menu={menu} />
</PluginSlot>
);
DesktopMainMenuSlot.propTypes = {
menu: desktopHeaderMainOrSecondaryMenuDataShape,
};
export default DesktopMainMenuSlot;

View File

@@ -0,0 +1,132 @@
# Desktop Secondary Menu Slot
### Slot ID: `org.openedx.frontend.layout.header_desktop_secondary_menu.v1`
### Slot ID Aliases
* `desktop_secondary_menu_slot`
## Description
This slot is used to replace/modify/hide the desktop secondary menu.
## Examples
### Modify Items
The following `env.config.jsx` will modify the items in the desktop secondary menu.
![Screenshot of modified items](./images/desktop_secondary_menu_modify_items.png)
```jsx
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const modifySecondaryMenu = ( widget ) => {
widget.content.menu = [
{
type: 'item',
href: 'https://www.youtube.com/c/openedx',
content: 'Open edX on YouTube',
},
{
type: 'item',
href: 'https://github.com/openedx/',
content: 'Open edX on GitHub',
}
];
return widget;
};
const config = {
pluginSlots: {
'org.openedx.frontend.layout.header_desktop_secondary_menu.v1': {
keepDefault: true,
plugins: [
{
op: PLUGIN_OPERATIONS.Modify,
widgetId: 'default_contents',
fn: modifySecondaryMenu,
},
]
},
},
}
export default config;
```
### Replace Menu with Custom Component
The following `env.config.jsx` will replace the desktop secondary menu entirely (in this case with a centered 🗺️ `h1`)
![Screenshot of custom component](./images/desktop_secondary_menu_custom_component.png)
```jsx
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const config = {
pluginSlots: {
'org.openedx.frontend.layout.header_desktop_secondary_menu.v1': {
keepDefault: false,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_secondary_menu_component',
type: DIRECT_PLUGIN,
RenderWidget: () => (
<h1 style={{textAlign: 'center'}}>🗺</h1>
),
},
},
]
},
},
}
export default config;
```
### Add Custom Components before and after Menu
The following `env.config.jsx` will place custom components before and after the desktop secondary menu (in this case centered `h1`s with 🌜 and 🌛).
![Screenshot of custom components before and after](./images/desktop_secondary_menu_custom_components_before_after.png)
```jsx
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const config = {
pluginSlots: {
'org.openedx.frontend.layout.header_desktop_secondary_menu.v1': {
keepDefault: true,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_before_secondary_menu_component',
type: DIRECT_PLUGIN,
priority: 10,
RenderWidget: () => (
<h1 style={{textAlign: 'center'}}>🌜</h1>
),
},
},
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_after_secondary_menu_component',
type: DIRECT_PLUGIN,
priority: 90,
RenderWidget: () => (
<h1 style={{textAlign: 'center'}}>🌛</h1>
),
},
},
]
},
},
}
export default config;
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,23 @@
import React from 'react';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import DesktopHeaderMainOrSecondaryMenu, { desktopHeaderMainOrSecondaryMenuDataShape } from '../../desktop-header/DesktopHeaderMainOrSecondaryMenu';
const DesktopSecondaryMenuSlot = ({
menu,
}) => (
<PluginSlot
id="org.openedx.frontend.layout.header_desktop_secondary_menu.v1"
idAliases={['desktop_secondary_menu_slot']}
slotOptions={{
mergeProps: true,
}}
>
<DesktopHeaderMainOrSecondaryMenu menu={menu} />
</PluginSlot>
);
DesktopSecondaryMenuSlot.propTypes = {
menu: desktopHeaderMainOrSecondaryMenuDataShape,
};
export default DesktopSecondaryMenuSlot;

View File

@@ -0,0 +1,144 @@
# Desktop User Menu Slot
### Slot ID: `org.openedx.frontend.layout.header_desktop_user_menu.v1`
### Slot ID Aliases
* `desktop_user_menu_slot`
## Description
This slot is used to replace/modify/hide the desktop user menu.
## Examples
### Modify Items
The following `env.config.jsx` will modify the items in the desktop user menu.
![Screenshot of modified items](./images/desktop_user_menu_modify_items.png)
```jsx
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const modifyUserMenu = ( widget ) => {
widget.content.menu = [
{
items: [
{
type: 'item',
href: 'https://openedx.org/',
content: 'openedx.org',
},
{
type: 'item',
href: 'https://docs.openedx.org/en/latest/',
content: 'Documentation',
},
]
},
{
items: [
{
type: 'item',
href: 'https://discuss.openedx.org/',
content: 'Forums',
}
]
}
];
return widget;
};
const config = {
pluginSlots: {
'org.openedx.frontend.layout.header_desktop_user_menu.v1': {
keepDefault: true,
plugins: [
{
op: PLUGIN_OPERATIONS.Modify,
widgetId: 'default_contents',
fn: modifyUserMenu,
},
]
},
},
}
export default config;
```
### Replace Menu with Custom Component
The following `env.config.jsx` will replace the desktop user menu entirely (in this case with a centered 🗺️ `h1`)
![Screenshot of custom component](./images/desktop_user_menu_custom_component.png)
```jsx
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const config = {
pluginSlots: {
'org.openedx.frontend.layout.header_desktop_user_menu.v1': {
keepDefault: false,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_user_menu_component',
type: DIRECT_PLUGIN,
RenderWidget: () => (
<h1 style={{textAlign: 'center'}}>🗺</h1>
),
},
},
]
},
},
}
export default config;
```
### Add Custom Components before and after Menu
The following `env.config.jsx` will place custom components before and after the desktop user menu (in this case centered `h1`s with 🌞 and 🌚).
![Screenshot of custom components before and after](./images/desktop_user_menu_custom_components_before_after.png)
```jsx
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const config = {
pluginSlots: {
'org.openedx.frontend.layout.header_desktop_user_menu.v1': {
keepDefault: true,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_before_user_menu_component',
type: DIRECT_PLUGIN,
priority: 10,
RenderWidget: () => (
<h1 style={{textAlign: 'center'}}>🌞</h1>
),
},
},
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_after_user_menu_component',
type: DIRECT_PLUGIN,
priority: 90,
RenderWidget: () => (
<h1 style={{textAlign: 'center'}}>🌚</h1>
),
},
},
]
},
},
}
export default config;
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,23 @@
import React from 'react';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import DesktopHeaderUserMenu, { desktopUserMenuDataShape } from '../../desktop-header/DesktopHeaderUserMenu';
const DesktopUserMenuSlot = ({
menu,
}) => (
<PluginSlot
id="org.openedx.frontend.layout.header_desktop_user_menu.v1"
idAliases={['desktop_user_menu_slot']}
slotOptions={{
mergeProps: true,
}}
>
<DesktopHeaderUserMenu menu={menu} />
</PluginSlot>
);
DesktopUserMenuSlot.propTypes = {
menu: desktopUserMenuDataShape,
};
export default DesktopUserMenuSlot;

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

@@ -0,0 +1,44 @@
# Learning Help Slot
### Slot ID: `org.openedx.frontend.layout.header_learning_help.v1`
### Slot ID Aliases
* `learning_help_slot`
## Description
This slot is used to replace/modify/hide the learning help link.
## Examples
### Custom Component
The following `env.config.jsx` will replace the help link entirely (in this case with a centered 🗺️ `h1`)
![Screenshot of replaced learning help with custom component](./images/learning_help_custom_component.png)
```jsx
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const config = {
pluginSlots: {
'org.openedx.frontend.layout.header_learning_help.v1': {
keepDefault: false,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_learning_help_component',
type: DIRECT_PLUGIN,
RenderWidget: () => (
<h1 style={{textAlign: 'center'}}>🗺</h1>
),
},
},
]
}
},
}
export default config;
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

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

View File

@@ -0,0 +1,135 @@
# 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
This slot is used to replace/modify/hide the items shown on the learning header when the user is logged out.
## Examples
### Modify Items
The following `env.config.jsx` will modify the items shown on the learning header when the user is logged out.
![Screenshot of modified items](./images/learning_logged_out_items_modified_items.png)
```jsx
import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const modifyLoggedOutItems = ( widget ) => {
widget.content.buttonsInfo = [
{
href: 'https://docs.openedx.org/en/latest/',
message: 'Documentation',
},
{
href: 'https://discuss.openedx.org/',
message: 'Forums',
},
{
href: 'https://openedx.org/',
message: 'openedx.org',
variant: 'primary',
},
];
return widget;
};
const config = {
pluginSlots: {
'org.openedx.frontend.layout.header_learning_logged_out_items.v1': {
keepDefault: true,
plugins: [
{
op: PLUGIN_OPERATIONS.Modify,
widgetId: 'default_contents',
fn: modifyLoggedOutItems,
},
]
},
},
}
export default config;
```
### Replace with Custom Component
The following `env.config.jsx` will replace the items shown in the learning header when the user is logged out entirely (in this case with a centered 🗺️ `h1`)
![Screenshot of replaced with custom component](./images/learning_logged_out_items_custom_component.png)
```jsx
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const config = {
pluginSlots: {
'org.openedx.frontend.layout.header_learning_logged_out_items.v1': {
keepDefault: false,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_logged_out_items_component',
type: DIRECT_PLUGIN,
RenderWidget: () => (
<h1 style={{textAlign: 'center'}}>🗺</h1>
),
},
},
]
},
},
}
export default config;
```
### Add Custom Components before and after
The following `env.config.jsx` will place custom components before and after the items shown in the learning header when the user is logged out (in this case centered `h1`s with 🌜 and 🌛).
![Screenshot of added custom components before and after](./images/learning_logged_out_items_custom_components_before_after.png)
```jsx
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const config = {
pluginSlots: {
'org.openedx.frontend.layout.header_learning_logged_out_items.v1': {
keepDefault: true,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_before_logged_out_items_component',
type: DIRECT_PLUGIN,
priority: 10,
RenderWidget: () => (
<h1 style={{textAlign: 'center'}}>🌜</h1>
),
},
},
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_after_logged_out_items_component',
type: DIRECT_PLUGIN,
priority: 90,
RenderWidget: () => (
<h1 style={{textAlign: 'center'}}>🌛</h1>
),
},
},
]
},
},
}
export default config;
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

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