Compare commits

...

107 Commits

Author SHA1 Message Date
Andres Espinel
98ac95f347 chore: updated dependencies 2025-07-29 17:47:34 -05:00
Brian Smith
c00a63bc91 feat!: add design tokens support (#665)
BREAKING CHANGE: Pre-design-tokens theming is no longer supported.

Co-authored-by: Diana Olarte <diana.olarte@edunext.co>
2025-07-29 16:53:48 -05:00
Brian Smith
fe386e31ee feat: import FooterSlot from component package instead of slot package (#604) 2025-04-24 12:25:32 -04:00
Brian Smith
cb1de82f0a feat: standardize slot ids (#608) 2025-04-24 07:47:52 -04:00
renovate[bot]
2337843d54 fix(deps): update dependency @edx/frontend-component-header to v6.4.0 (#607)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-23 20:17:47 +00:00
renovate[bot]
70da0d38ed fix(deps): update dependency @edx/frontend-component-header to v6.3.0 (#606)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-21 10:26:20 +00:00
renovate[bot]
154a2583f6 fix(deps): update dependency @edx/frontend-component-footer to v14.6.0 (#605)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-21 07:27:36 +00:00
renovate[bot]
633050739e fix(deps): update dependency @edx/frontend-component-footer to v14.4.0 (#602)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-14 12:28:07 +00:00
renovate[bot]
61d24d29f1 chore(deps): update dependency @openedx/frontend-build to v14.5.0 (#601)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-14 07:11:40 +00:00
Ivo Branco
a210f23c9f fix: unenroll reasons translation
The unenroll reasons select wasn't being translated.
2025-04-09 10:17:21 -04:00
renovate[bot]
b16908842e fix(deps): update dependency @edx/frontend-platform to v8.3.4 (#596)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Deborah Kaplan <deborahgu@users.noreply.github.com>
2025-04-08 09:47:48 -04:00
renovate[bot]
b80cab7a66 chore(deps): update dependency @openedx/frontend-build to v14.4.2 (#595)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-08 09:42:55 -04:00
Brian Smith
6b8cd1f780 feat: upgrade to react 18 (#593) 2025-04-04 10:24:46 -04:00
Régis Behmo
78c5d73900 chore: remove husky 🪓🐶 (#594) 2025-04-03 09:17:58 -04:00
renovate[bot]
eb3fc9412d fix(deps): update dependency @openedx/paragon to v22.17.0 (#592)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-31 10:52:17 +00:00
renovate[bot]
3caf6fd67a chore(deps): update dependency @openedx/frontend-build to v14.4.1 (#591)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-31 06:45:28 +00:00
Brian Smith
d48cb3d9fc chore(deps): update @openedx dependencies to versions that support React 18 (#590) 2025-03-27 16:16:48 -04:00
Maxwell Frank
d3b4a7fc84 feat: remove upgrade refs course banner (#585) 2025-03-26 09:48:14 -04:00
Maxwell Frank
9e63777c5c fix: CourseBanner slot readme (#589) 2025-03-25 11:36:10 -04:00
renovate[bot]
cf2f3acc51 fix(deps): update dependency @openedx/frontend-slot-footer to v1.1.0 (#588)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 06:45:05 +00:00
Maxwell Frank
54f8bc86e3 fix: rename course banner slot (#586) 2025-03-21 11:08:32 -04:00
renovate[bot]
10961010ba fix(deps): update dependency @openedx/frontend-plugin-framework to v1.6.0 (#584)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-17 05:59:50 +00:00
Kira Miller
3c1b749395 fix: changing slot id 2025-03-11 08:39:12 -06:00
Kira Miller
845ee09bf2 fix: PR requests 2025-03-11 08:39:12 -06:00
Kira Miller
1efec09f44 fix: PR requests 2025-03-11 08:39:12 -06:00
Kira Miller
aa1cae5200 fix: pr requests 2025-03-11 08:39:12 -06:00
Kira Miller
77ab48c59f fix: renaming slot 2025-03-11 08:39:12 -06:00
Kira Miller
5d2b33abd3 feat: adding new plugin slot for an enterprise modal 2025-03-11 08:39:12 -06:00
renovate[bot]
dd4f61eec3 fix(deps): update dependency @edx/frontend-enterprise-hotjar to v7.2.0 (#581)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-10 12:27:57 +00:00
renovate[bot]
8f7580ec30 chore(deps): update dependency @openedx/frontend-build to v14.3.2 (#580)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-10 05:34:46 +00:00
Ankush Chudiwal
fbf24e42d3 fix: broken skip link in Learner Dashboard (#522)
Co-authored-by: Deborah Kaplan <deborahgu@users.noreply.github.com>
2025-03-03 14:52:33 -05:00
Jason Wesson
e764e9c502 Revert "feat: adding new plugin slot for an enterprise modal"
This reverts commit f110a0ade8.
2025-03-03 12:40:17 -06:00
Jason Wesson
13721f2770 Revert "fix: renaming slot"
This reverts commit 6a43918b56.
2025-03-03 12:40:17 -06:00
Jason Wesson
960647ce9f Revert "fix: pr requests"
This reverts commit 86fd29309a.
2025-03-03 12:40:17 -06:00
Jason Wesson
44c797854f Revert "fix: PR requests"
This reverts commit 57d09af61d.
2025-03-03 12:40:17 -06:00
Jason Wesson
3ea088e411 Revert "fix: PR requests"
This reverts commit 76783133da.
2025-03-03 12:40:17 -06:00
Jason Wesson
4a18c890c3 Revert "fix: changing slot id"
This reverts commit b26d4632c9.
2025-03-03 12:40:17 -06:00
renovate[bot]
e1c1c51704 chore(deps): update dependency @openedx/frontend-build to v14.3.1 (#578)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-03 10:39:48 +00:00
renovate[bot]
f83f3a1850 fix(deps): update dependency @openedx/paragon to v22.15.3 (#577)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-03 05:37:49 +00:00
Kira Miller
b26d4632c9 fix: changing slot id 2025-02-27 11:28:33 -06:00
Kira Miller
76783133da fix: PR requests 2025-02-27 11:28:33 -06:00
Kira Miller
57d09af61d fix: PR requests 2025-02-27 11:28:33 -06:00
Kira Miller
86fd29309a fix: pr requests 2025-02-27 11:28:33 -06:00
Kira Miller
6a43918b56 fix: renaming slot 2025-02-27 11:28:33 -06:00
Kira Miller
f110a0ade8 feat: adding new plugin slot for an enterprise modal 2025-02-27 11:28:33 -06:00
Feanil Patel
93bd883a01 Update catalog-info file for release data (#570) 2025-02-26 09:07:11 -05:00
salman2013
61375c9e95 chore: update catalog-info-file for release data and remove openedx.yaml file 2025-02-24 15:36:50 -05:00
renovate[bot]
c2f4be5063 fix(deps): update dependency @openedx/paragon to v22.15.2 (#574)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 11:48:06 +00:00
renovate[bot]
14bde7fc3f fix(deps): update dependency @edx/frontend-component-header to v5.8.3 (#573)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 07:03:02 +00:00
Maxim Beder
b5e2a94480 test: update test snapshots 2025-02-14 11:51:25 -08:00
Maxim Beder
edcf2fd756 fix: course image height on IOS Safari
Course thumbnails on IOS Safari stretch to the full height of the image,
instead of being limited by width and preserving aspect ratio. This
seems to be a IOS Safari specific behavior[1].

Learner dashboard MFE uses a custom implementation of CourseCardImage,
because the one in Paragon currently doesn't allow the image to be
clickable. Because of that, we are fixing this issue in this repo for
now, instead of fixing it in Paragon, until Paragon updates their
implementation and this repo is updated to use a newer version of
Paragon.

1: https://stackoverflow.com/a/44250830
2025-02-14 11:51:25 -08:00
Maxwell Frank
9228d017af feat: course banner slot (#559) 2025-02-12 14:13:31 -05:00
renovate[bot]
1104c58611 fix(deps): update dependency react-router-dom to v6.29.0 (#561)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-03 08:57:13 +00:00
renovate[bot]
3d7366ac1d fix(deps): update dependency @openedx/paragon to v22.15.1 (#560)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-03 05:15:00 +00:00
renovate[bot]
0f19ff9a02 fix(deps): update dependency @reduxjs/toolkit to v2.5.1 (#556)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-27 10:02:08 +00:00
renovate[bot]
a21caead92 fix(deps): update dependency @openedx/paragon to v22.14.0 (#557)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-27 05:56:20 +00:00
renovate[bot]
2b287c6332 chore(deps): update dependency @edx/browserslist-config to v1.5.0 (#552)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-20 09:40:34 +00:00
renovate[bot]
8b67abd304 fix(deps): update dependency react-router-dom to v6.28.2 (#551)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-20 05:03:34 +00:00
Maxwell Frank
abae82b507 fix: remove remaining UpgradeButton definition and tests (#548) 2025-01-14 14:36:38 -05:00
Maxwell Frank
777d3aa45c feat!: remove UpgradeButton (#536) 2025-01-13 13:50:50 -05:00
renovate[bot]
ce595d0e62 fix(deps): update dependency react-router-dom to v6.28.1 (#544)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-13 13:52:09 +00:00
renovate[bot]
0fd242eb74 fix(deps): update dependency core-js to v3.40.0 (#543)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-13 08:36:11 +00:00
renovate[bot]
d2215570da fix(deps): update dependency @edx/frontend-platform to v8.1.5 (#542)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-13 05:04:41 +00:00
paulbert
b6bef24ace refactor!: Remove ZendeskFab component
Deletes the ZendeskFab component and associated mock, removes react-zendesk package, and removes env variables for Zendesk
2025-01-08 14:07:21 -08:00
renovate[bot]
bb5a2aa3fd fix(deps): update dependency @reduxjs/toolkit to v2.5.0 (#540)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-30 09:27:09 +00:00
renovate[bot]
77d1ba93c3 fix(deps): update dependency @openedx/paragon to v22.13.0 (#539)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-30 06:39:30 +00:00
renovate[bot]
4aa786c595 fix(deps): update dependency @openedx/frontend-plugin-framework to v1.4.1 (#538)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-23 10:23:13 +00:00
renovate[bot]
a5ff2eceae chore(deps): update dependency @edx/browserslist-config to v1.4.0 (#537)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-23 06:34:26 +00:00
renovate[bot]
84b281aa51 fix(deps): update dependency @edx/frontend-platform to v8.1.3 (#534)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-16 10:29:28 +00:00
renovate[bot]
dc5c655314 fix(deps): update dependency @edx/frontend-component-header to v5.8.2 (#533)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-16 06:45:43 +00:00
Brian Smith
2140d8821d chore: add dev script to package.json (#530) 2024-12-13 12:29:02 -05:00
Deborah Kaplan
63860e95ce chore: removing send mail on failure gh action
This action frequently fails, and the current maintainers
(@openedx/2U-aperture) don't require it, as we both monitor the success
of the workflows and we have workflow failure email notifications
enabled. After discussion with Axim, we are removing the  action. It can
always be added back (and potentially debugged) later if another
maintainer would benefit from it.

FIXES: APER-3814
2024-12-09 12:57:47 -08:00
renovate[bot]
1474c4c546 chore(deps): update dependency jest-when to v3.7.0 (#527)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 11:45:54 +00:00
renovate[bot]
e2e51dc030 chore(deps): update dependency @openedx/frontend-build to v14.2.2 (#526)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 08:22:06 +00:00
Adolfo R. Brandes
604298eaca fix: Use browserslist-config
We were installing browserslist-config but not declaring it.  This had
the effect that webpack - and likely others - were not using it.
2024-12-06 11:08:11 -03:00
renovate[bot]
f9d13c4058 fix(deps): update dependency @edx/frontend-component-header to v5.8.1 (#518)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-02 11:07:31 +00:00
renovate[bot]
e1db6807ef fix(deps): update dependency @openedx/frontend-slot-footer to v1.0.7 (#517)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-02 07:43:53 +00:00
renovate[bot]
d8e1f82bdf fix(deps): update dependency @edx/frontend-enterprise-hotjar to v7 2024-11-26 15:49:09 -06:00
dependabot[bot]
c5a78e01f2 build(deps): bump dawidd6/action-send-mail from 3 to 4
Bumps [dawidd6/action-send-mail](https://github.com/dawidd6/action-send-mail) from 3 to 4.
- [Release notes](https://github.com/dawidd6/action-send-mail/releases)
- [Commits](https://github.com/dawidd6/action-send-mail/compare/v3...v4)

---
updated-dependencies:
- dependency-name: dawidd6/action-send-mail
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-26 15:43:43 -06:00
renovate[bot]
22e4b9facc chore(deps): update dependency @openedx/frontend-build to v14.2.0 (#514)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 18:36:14 +00:00
renovate[bot]
1ae555eac9 chore(deps): update dependency husky to v9.1.7 (#513)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-25 06:29:05 +00:00
Diana Olarte
a0e5f75f0b fix: apply feedback 2024-11-21 09:10:34 -05:00
Diana Olarte
2e101d5c23 fix: display programs tab only if it is configured 2024-11-21 09:10:34 -05:00
Deborah Kaplan
ce1848a5c3 Revert "fix: display programs only if the url is configured (#479)" (#504) 2024-11-18 14:45:16 -05:00
Deborah Kaplan
ee515ad666 Revert "fix: display programs only if the url is configured (#479)"
This reverts commit e8886c9d9d.
2024-11-18 14:37:40 -05:00
Deborah Kaplan
bc449a3c34 build(deps): bump codecov/codecov-action from 4 to 5 (#502) 2024-11-18 12:57:22 -05:00
dependabot[bot]
3012f64b4b build(deps): bump codecov/codecov-action from 4 to 5
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-18 16:47:36 +00:00
Diana Olarte
e8886c9d9d fix: display programs only if the url is configured (#479)
Removes the link of programs from the Header if the service is not configured.
2024-11-18 11:51:56 -03:00
renovate[bot]
a074459e03 fix(deps): update dependency react-intl to v6.8.9 (#501)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-18 11:02:36 +00:00
renovate[bot]
b87e12d2cb fix(deps): update dependency @edx/frontend-component-header to v5.7.2 (#500)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-18 06:15:08 +00:00
Emad Rad
bf2bc405d0 chore: npm publish action removed (#490) 2024-11-13 15:36:19 -05:00
Juan Carlos Iasenza (Aulasneo)
9fecc65680 chore: remove unused dependencies 2024-11-13 15:29:53 -05:00
Maxwell Frank
486a0232e3 fix: update husky (#493) 2024-11-13 09:25:16 -05:00
renovate[bot]
e68dc88d6c fix(deps): update dependency react-intl to v6.8.7 (#492)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-11 10:35:23 +00:00
renovate[bot]
f777eaabff fix(deps): update dependency @openedx/frontend-slot-footer to v1.0.6 (#491)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-11 07:42:48 +00:00
renovate[bot]
36080e7074 fix(deps): update dependency @edx/frontend-component-header to v5.7.1 2024-11-04 10:42:27 +00:00
renovate[bot]
bdeb7e1381 fix(deps): update dependency react-intl to v6.8.6 2024-11-04 06:09:44 +00:00
Deborah Kaplan
ecf7b56acf fix(deps): update dependency @reduxjs/toolkit to v2 (#482) 2024-11-01 12:29:05 -04:00
renovate[bot]
92a2ec1fb0 fix(deps): update dependency @reduxjs/toolkit to v2 2024-10-31 19:41:35 +00:00
Bilal Qamar
892262a107 test: Remove support for Node 18 (#436)
Co-authored-by: Jason Wesson <jsnwesson@gmail.com>
2024-10-31 15:39:36 -04:00
Deborah Kaplan
0e10a9b34b fix(deps): update dependency filesize to v10 (#484) 2024-10-30 11:58:49 -04:00
renovate[bot]
d872a57160 fix(deps): update dependency filesize to v10 2024-10-28 22:04:14 +00:00
Deborah Kaplan
0d38f107bd fix(deps): update dependency dompurify to v3 (#483) 2024-10-28 18:02:25 -04:00
renovate[bot]
1217e086c0 fix(deps): update dependency dompurify to v3 2024-10-28 12:48:57 +00:00
renovate[bot]
44e3d58e14 fix(deps): update dependency react-intl to v6.8.4 2024-10-28 11:24:24 +00:00
renovate[bot]
8b52cfc4d3 chore(deps): update dependency redux-mock-store to v1.5.5 2024-10-28 06:24:19 +00:00
Diana Olarte
c93d94035a fix: display SUPPORT_URL only if the url is configured 2024-10-25 12:18:49 -03:00
103 changed files with 7128 additions and 9865 deletions

5
.env
View File

@@ -32,7 +32,6 @@ ENTERPRISE_MARKETING_UTM_SOURCE=''
ENTERPRISE_MARKETING_UTM_CAMPAIGN=''
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=''
LEARNING_BASE_URL=''
ZENDESK_KEY=''
HOTJAR_APP_ID=''
HOTJAR_VERSION='6'
HOTJAR_DEBUG=''
@@ -41,3 +40,7 @@ ACCOUNT_PROFILE_URL=''
ENABLE_NOTICES=''
CAREER_LINK_URL=''
ENABLE_EDX_PERSONAL_DASHBOARD=false
ENABLE_PROGRAMS=false
NON_BROWSABLE_COURSES=false
# Fallback in local style files
PARAGON_THEME_URLS={}

View File

@@ -20,7 +20,7 @@ LMS_CLIENT_ID='login-service-client-id'
SEGMENT_KEY=''
FEATURE_FLAGS={}
MARKETING_SITE_BASE_URL='http://localhost:18000'
SUPPORT_URL='http://localhost:18000/support'
SUPPORT_URL=''
CONTACT_URL='http://localhost:18000/contact'
OPEN_SOURCE_URL='http://localhost:18000/openedx'
TERMS_OF_SERVICE_URL='http://localhost:18000/terms-of-service'
@@ -38,7 +38,6 @@ ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
LEARNING_BASE_URL='http://localhost:2000'
SESSION_COOKIE_DOMAIN='localhost'
ZENDESK_KEY=''
HOTJAR_APP_ID=''
HOTJAR_VERSION='6'
HOTJAR_DEBUG=''
@@ -47,3 +46,7 @@ ACCOUNT_PROFILE_URL='http://localhost:1995'
ENABLE_NOTICES=''
CAREER_LINK_URL=''
ENABLE_EDX_PERSONAL_DASHBOARD=false
ENABLE_PROGRAMS=false
NON_BROWSABLE_COURSES=false
# Fallback in local style files
PARAGON_THEME_URLS={}

View File

@@ -20,7 +20,7 @@ LMS_CLIENT_ID='login-service-client-id'
SEGMENT_KEY=''
FEATURE_FLAGS={}
MARKETING_SITE_BASE_URL='http://localhost:18000'
SUPPORT_URL='http://localhost:18000/support'
SUPPORT_URL=''
CONTACT_URL='http://localhost:18000/contact'
OPEN_SOURCE_URL='http://localhost:18000/openedx'
TERMS_OF_SERVICE_URL='http://localhost:18000/terms-of-service'
@@ -37,7 +37,6 @@ ENTERPRISE_MARKETING_UTM_SOURCE='example.com'
ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
LEARNING_BASE_URL='http://localhost:2000'
ZENDESK_KEY='test-zendesk-key'
HOTJAR_APP_ID='hot-jar-app-id'
HOTJAR_VERSION='6'
HOTJAR_DEBUG=''
@@ -46,3 +45,6 @@ ACCOUNT_PROFILE_URL='http://account-profile-url.test'
ENABLE_NOTICES=''
CAREER_LINK_URL=''
ENABLE_EDX_PERSONAL_DASHBOARD=true
ENABLE_PROGRAMS=false
NON_BROWSABLE_COURSES=false
PARAGON_THEME_URLS={}

View File

@@ -11,9 +11,6 @@ on:
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
node: [18, 20]
steps:
- name: Checkout
@@ -22,7 +19,7 @@ jobs:
- name: Setup Nodejs
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
node-version-file: '.nvmrc'
- name: Install dependencies
run: npm ci
@@ -40,24 +37,7 @@ jobs:
run: npm run build
- name: Run Coverage
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
- name: Send failure notification
if: ${{ failure() }}
uses: dawidd6/action-send-mail@v3
with:
server_address: email-smtp.us-east-1.amazonaws.com
server_port: 465
username: ${{ secrets.EDX_SMTP_USERNAME }}
password: ${{ secrets.EDX_SMTP_PASSWORD }}
subject: CI workflow failed in ${{github.repository}}
to: masters-grades@edx.org,aperture@2u-internal.opsgenie.net
from: github-actions <github-actions@edx.org>
nodemailerlog: true
nodemailerdebug: true
body: CI workflow in ${{github.repository}} failed!
For details see "github.com/${{ github.repository }}/actions/runs/${{ github.run_id
}}"

View File

@@ -1,35 +0,0 @@
name: Release CI
on:
push:
tags:
- "*"
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VER }}
- name: Install dependencies
run: npm ci
- name: Create Build
run: npm run build
- name: Release Package
env:
GITHUB_TOKEN: ${{ secrets.SEMANTIC_RELEASE_GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.SEMANTIC_RELEASE_NPM_TOKEN }}
run: npm semantic-release

View File

@@ -1,4 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm run lint

View File

@@ -1,27 +0,0 @@
{
"branch": "master",
"tagFormat": "v${version}",
"verifyConditions": [
"@semantic-release/npm",
{
"path": "@semantic-release/github",
"assets": {
"path": "dist/*"
}
}
],
"analyzeCommits": "@semantic-release/commit-analyzer",
"generateNotes": "@semantic-release/release-notes-generator",
"prepare": "@semantic-release/npm",
"publish": [
"@semantic-release/npm",
{
"path": "@semantic-release/github",
"assets": {
"path": "dist/*"
}
}
],
"success": [],
"fail": []
}

View File

@@ -17,6 +17,7 @@ metadata:
openedx.org/arch-interest-groups: ""
# This can be multiple comma-separated projects.
openedx.org/add-to-projects: "openedx:23"
openedx.org/release: "master"
spec:
type: 'service'
lifecycle: 'production'

View File

@@ -59,7 +59,6 @@ module.exports = {
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM: 'Footer',
LEARNING_BASE_URL: 'http://localhost:2000',
SESSION_COOKIE_DOMAIN: 'localhost',
ZENDESK_KEY: '',
HOTJAR_APP_ID: '',
HOTJAR_VERSION: 6,
HOTJAR_DEBUG: '',

View File

@@ -1,9 +0,0 @@
# This file describes this Open edX repo, as described in OEP-2:
# http://open-edx-proposals.readthedocs.io/en/latest/oeps/oep-0002.html#specification
tags:
- frontend-app
- masters
oeps:
oep-2: true # Repository metadata
openedx-release: {ref: master}

15257
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,9 @@
"type": "git",
"url": "git+https://github.com/edx/frontend-app-learner-dashboard.git"
},
"browserslist": [
"extends @edx/browserslist-config"
],
"scripts": {
"build": "fedx-scripts webpack",
"i18n_extract": "fedx-scripts formatjs extract",
@@ -13,11 +16,11 @@
"lint-fix": "fedx-scripts eslint --fix --ext .jsx,.js src/",
"semantic-release": "semantic-release",
"start": "fedx-scripts webpack-dev-server --progress",
"dev": "PUBLIC_PATH=/learner-dashboard/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
"test": "TZ=GMT fedx-scripts jest --coverage --passWithNoTests",
"quality": "npm run lint-fix && npm run test",
"watch-tests": "jest --watch",
"snapshot": "fedx-scripts jest --updateSnapshot",
"prepare": "husky install"
"snapshot": "fedx-scripts jest --updateSnapshot"
},
"author": "edX",
"license": "AGPL-3.0",
@@ -27,70 +30,58 @@
},
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/browserslist-config": "^1.1.0",
"@edx/frontend-component-header": "^5.6.0",
"@edx/frontend-enterprise-hotjar": "3.0.0",
"@edx/frontend-platform": "8.1.2",
"@edx/frontend-component-footer": "^14.6.0",
"@edx/frontend-component-header": "^6.2.0",
"@edx/frontend-enterprise-hotjar": "7.2.0",
"@edx/frontend-platform": "^8.3.1",
"@edx/openedx-atlas": "^0.6.0",
"@edx/react-unit-test-utils": "3.0.0",
"@edx/react-unit-test-utils": "^4.0.0",
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-brands-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/react-fontawesome": "^0.2.0",
"@openedx/frontend-plugin-framework": "^1.2.0",
"@openedx/frontend-slot-footer": "^1.0.2",
"@openedx/paragon": "^22.2.2",
"@redux-beacon/segment": "^1.1.0",
"@openedx/frontend-plugin-framework": "^1.7.0",
"@openedx/paragon": "^23.4.5",
"@redux-devtools/extension": "3.3.0",
"@reduxjs/toolkit": "^1.6.1",
"@testing-library/user-event": "^13.5.0",
"@reduxjs/toolkit": "^2.0.0",
"classnames": "^2.3.1",
"core-js": "3.38.1",
"dompurify": "^2.3.1",
"email-prop-type": "^3.0.1",
"file-saver": "^2.0.5",
"filesize": "^8.0.6",
"core-js": "3.40.0",
"filesize": "^10.0.0",
"font-awesome": "4.7.0",
"history": "5.3.0",
"html-react-parser": "^1.3.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "29.7.0",
"jest-when": "^3.6.0",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"prop-types": "15.8.1",
"query-string": "7.1.3",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-helmet": "^6.1.0",
"react-intl": "6.8.0",
"react-pdf": "^7.0.0",
"react-intl": "6.8.9",
"react-redux": "^7.2.4",
"react-router-dom": "6.27.0",
"react-router-dom": "6.29.0",
"react-share": "^4.4.0",
"react-zendesk": "^0.1.13",
"redux": "4.2.1",
"redux-beacon": "^2.1.0",
"redux-logger": "3.0.6",
"redux-thunk": "2.4.2",
"regenerator-runtime": "^0.14.0",
"reselect": "^4.0.0",
"universal-cookie": "^4.0.4",
"util": "^0.12.4",
"whatwg-fetch": "^3.6.2"
"util": "^0.12.4"
},
"devDependencies": {
"@edx/browserslist-config": "^1.3.0",
"@edx/reactifex": "^2.1.1",
"@openedx/frontend-build": "14.1.5",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.1.0",
"@openedx/frontend-build": "^14.3.3",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"copy-webpack-plugin": "^12.0.0",
"husky": "^9.0.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-expect-message": "^1.1.3",
"jest-when": "^3.6.0",
"react-dev-utils": "^12.0.0",
"react-test-renderer": "^17.0.2",
"redux-mock-store": "^1.5.4",
"semantic-release": "^20.1.3"
"react-test-renderer": "^18.3.1",
"redux-mock-store": "^1.5.4"
}
}

View File

@@ -6,7 +6,7 @@ import { logError } from '@edx/frontend-platform/logging';
import { initializeHotjar } from '@edx/frontend-enterprise-hotjar';
import { ErrorPage, AppContext } from '@edx/frontend-platform/react';
import FooterSlot from '@openedx/frontend-slot-footer';
import { FooterSlot } from '@edx/frontend-component-footer';
import { Alert } from '@openedx/paragon';
import { RequestKeys } from 'data/constants/requests';
@@ -80,7 +80,7 @@ export const App = () => {
<div>
<AppWrapper>
<LearnerDashboardHeader />
<main>
<main id="main">
{hasNetworkFailure
? (
<Alert variant="danger">

View File

@@ -1,13 +1,10 @@
// frontend-app-*/src/index.scss
@import "~@edx/brand/paragon/fonts";
@import "~@edx/brand/paragon/variables";
@import "~@openedx/paragon/scss/core/core";
@import "~@edx/brand/paragon/overrides";
@use "@openedx/paragon/styles/css/core/custom-media-breakpoints" as paragonCustomMediaBreakpoints;
$fa-font-path: "~font-awesome/fonts";
@import "~font-awesome/scss/font-awesome";
$input-focus-box-shadow: $input-box-shadow; // hack to get upgrade to paragon 4.0.0 to work
$input-focus-box-shadow: var(--pgn-elevation-form-input-base); // hack to get upgrade to paragon 4.0.0 to work
@import "~@edx/frontend-component-header/dist/index";
@import "~@edx/frontend-component-footer/dist/_footer";

View File

@@ -13,11 +13,10 @@ import AppWrapper from 'containers/WidgetContainers/AppWrapper';
import { App } from './App';
import messages from './messages';
jest.mock('@edx/frontend-component-footer', () => ({ FooterSlot: 'Footer' }));
jest.mock('@edx/frontend-component-footer', () => ({ FooterSlot: 'FooterSlot' }));
jest.mock('containers/Dashboard', () => 'Dashboard');
jest.mock('containers/LearnerDashboardHeader', () => 'LearnerDashboardHeader');
jest.mock('components/ZendeskFab', () => 'ZendeskFab');
jest.mock('containers/WidgetContainers/AppWrapper', () => 'AppWrapper');
jest.mock('data/redux', () => ({
selectors: 'redux.selectors',

View File

@@ -17,7 +17,9 @@ exports[`App router component component initialize failure snapshot 1`] = `
<div>
<AppWrapper>
<LearnerDashboardHeader />
<main>
<main
id="main"
>
<Alert
variant="danger"
>
@@ -49,7 +51,9 @@ exports[`App router component component no network failure snapshot 1`] = `
<div>
<AppWrapper>
<LearnerDashboardHeader />
<main>
<main
id="main"
>
<Dashboard />
</main>
</AppWrapper>
@@ -75,7 +79,9 @@ exports[`App router component component no network failure with optimizely proje
<div>
<AppWrapper>
<LearnerDashboardHeader />
<main>
<main
id="main"
>
<Dashboard />
</main>
</AppWrapper>
@@ -101,7 +107,9 @@ exports[`App router component component no network failure with optimizely url s
<div>
<AppWrapper>
<LearnerDashboardHeader />
<main>
<main
id="main"
>
<Dashboard />
</main>
</AppWrapper>
@@ -127,7 +135,9 @@ exports[`App router component component refresh failure snapshot 1`] = `
<div>
<AppWrapper>
<LearnerDashboardHeader />
<main>
<main
id="main"
>
<Alert
variant="danger"
>

View File

@@ -1,40 +1,43 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`app registry subscribe: APP_INIT_ERROR. snapshot: displays an ErrorPage to root element 1`] = `
<ErrorPage
message="test-error-message"
/>
<UNDEFINED>
<ErrorPage
message="test-error-message"
/>
</UNDEFINED>
`;
exports[`app registry subscribe: APP_READY. links App to root element 1`] = `
<AppProvider
store={
{
"redux": "store",
<UNDEFINED>
<AppProvider
store={
{
"redux": "store",
}
}
}
wrapWithRouter={true}
>
<NoticesWrapper>
<Routes>
<Route
element={
<PageWrap>
<App />
</PageWrap>
}
path="/"
/>
<Route
element={
<Navigate
replace={true}
to="/"
/>
}
path="*"
/>
</Routes>
</NoticesWrapper>
</AppProvider>
>
<NoticesWrapper>
<Routes>
<Route
element={
<PageWrap>
<App />
</PageWrap>
}
path="/"
/>
<Route
element={
<Navigate
replace={true}
to="/"
/>
}
path="*"
/>
</Routes>
</NoticesWrapper>
</AppProvider>
</UNDEFINED>
`;

View File

@@ -1,65 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ZendeskFab snapshot 1`] = `
<Zendesk
cookies={true}
defer={true}
webWidget={
{
"answerBot": {
"avatar": {
"name": {
"*": "edX Support",
},
"url": "https://edx-cdn.org/v3/prod/favicon.ico",
},
"contactOnlyAfterQuery": true,
"suppress": false,
"title": {
"*": "edX Support",
},
},
"chat": {
"departments": {
"enabled": [
"account settings",
"billing and payments",
"certificates",
"deadlines",
"errors and technical issues",
"other",
"proctoring",
],
},
"suppress": false,
},
"contactForm": {
"attachments": true,
"selectTicketForm": {
"*": "Please choose your request type:",
},
"ticketForms": [
{
"fields": [
{
"id": "description",
"prefill": {
"*": "",
},
},
],
"id": 360003368814,
"subject": false,
},
],
},
"contactOptions": {
"enabled": false,
},
"helpCenter": {
"originalArticleButton": true,
},
}
}
/>
`;

View File

@@ -1,56 +0,0 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import Zendesk from 'react-zendesk';
import messages from './messages';
const ZendeskFab = () => {
const { formatMessage } = useIntl();
const setting = {
cookies: true,
webWidget: {
contactOptions: {
enabled: false,
},
chat: {
suppress: false,
departments: {
enabled: ['account settings', 'billing and payments', 'certificates', 'deadlines', 'errors and technical issues', 'other', 'proctoring'],
},
},
contactForm: {
ticketForms: [
{
id: 360003368814,
subject: false,
fields: [{ id: 'description', prefill: { '*': '' } }],
},
],
selectTicketForm: {
'*': formatMessage(messages.selectTicketForm),
},
attachments: true,
},
helpCenter: {
originalArticleButton: true,
},
answerBot: {
suppress: false,
contactOnlyAfterQuery: true,
title: { '*': formatMessage(messages.supportTitle) },
avatar: {
url: 'https://edx-cdn.org/v3/prod/favicon.ico',
name: { '*': formatMessage(messages.supportTitle) },
},
},
},
};
return (
<Zendesk defer zendeskKey={getConfig().ZENDESK_KEY} {...setting} />
);
};
export default ZendeskFab;

View File

@@ -1,12 +0,0 @@
import { shallow } from '@edx/react-unit-test-utils';
import ZendeskFab from '.';
jest.mock('react-zendesk', () => 'Zendesk');
describe('ZendeskFab', () => {
test('snapshot', () => {
const wrapper = shallow(<ZendeskFab />);
expect(wrapper.snapshot).toMatchSnapshot();
});
});

View File

@@ -1,16 +0,0 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
supportTitle: {
id: 'zendesk.supportTitle',
description: 'Title for the support button',
defaultMessage: 'edX Support',
},
selectTicketForm: {
id: 'zendesk.selectTicketForm',
description: 'Select ticket form',
defaultMessage: 'Please choose your request type:',
},
});
export default messages;

View File

@@ -12,13 +12,13 @@ const configuration = {
// ACCESS_TOKEN_COOKIE_NAME: process.env.ACCESS_TOKEN_COOKIE_NAME,
LEARNING_BASE_URL: process.env.LEARNING_BASE_URL,
SESSION_COOKIE_DOMAIN: process.env.SESSION_COOKIE_DOMAIN || '',
ZENDESK_KEY: process.env.ZENDESK_KEY,
SUPPORT_URL: process.env.SUPPORT_URL || null,
ENABLE_NOTICES: process.env.ENABLE_NOTICES || null,
CAREER_LINK_URL: process.env.CAREER_LINK_URL || null,
LOGO_URL: process.env.LOGO_URL,
ENABLE_EDX_PERSONAL_DASHBOARD: process.env.ENABLE_EDX_PERSONAL_DASHBOARD === 'true',
SEARCH_CATALOG_URL: process.env.SEARCH_CATALOG_URL || null,
ENABLE_PROGRAMS: process.env.ENABLE_PROGRAMS === 'true',
};
const features = {};

View File

@@ -1,10 +1,8 @@
@import "@openedx/paragon/scss/core/core";
.course-card {
.card {
.pgn__card-wrapper-image-cap.vertical {
display: flex;
min-height: $card-image-vertical-max-height;
min-height: var(--pgn-size-card-image-vertical-max-height);
}
.pgn__card-image-cap {
border-bottom-left-radius: 0 !important;
@@ -53,11 +51,11 @@
> .alert {
border-radius: 0;
box-shadow: none;
padding: map-get($spacers, 3) map-get($spacers, 4);
padding: var(--pgn-spacing-spacer-3) var(--pgn-spacing-spacer-4);
&:last-of-type {
border-bottom-left-radius: $alert-border-radius;
border-bottom-right-radius: $alert-border-radius;
border-bottom-left-radius: var(--pgn-size-alert-border-radius);
border-bottom-right-radius: var(--pgn-size-alert-border-radius);
}
}

View File

@@ -27,7 +27,7 @@ reduxHooks.useCardCourseRunData.mockReturnValue({ homeUrl });
const execEdPath = (cardId) => `exec-ed-tracking-path=${cardId}`;
reduxHooks.useCardExecEdTrackingParam.mockImplementation(execEdPath);
reduxHooks.useTrackCourseEvent.mockImplementation(
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
(eventName, cardId, url) => ({ trackCourseEvent: { eventName, cardId, url } }),
);
describe('BeginCourseButton', () => {

View File

@@ -26,7 +26,7 @@ reduxHooks.useCardCourseRunData.mockReturnValue({ resumeUrl });
const execEdPath = (cardId) => `exec-ed-tracking-path=${cardId}`;
reduxHooks.useCardExecEdTrackingParam.mockImplementation(execEdPath);
reduxHooks.useTrackCourseEvent.mockImplementation(
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
(eventName, cardId, url) => ({ trackCourseEvent: { eventName, cardId, url } }),
);
let wrapper;

View File

@@ -1,45 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Locked } from '@openedx/paragon/icons';
import { useIntl } from '@edx/frontend-platform/i18n';
import track from 'tracking';
import { reduxHooks } from 'hooks';
import useActionDisabledState from '../hooks';
import ActionButton from './ActionButton';
import messages from './messages';
export const UpgradeButton = ({ cardId }) => {
const { formatMessage } = useIntl();
const { upgradeUrl } = reduxHooks.useCardCourseRunData(cardId);
const { disableUpgradeCourse } = useActionDisabledState(cardId);
const trackUpgradeClick = reduxHooks.useTrackCourseEvent(
track.course.upgradeClicked,
cardId,
upgradeUrl,
);
const enabledProps = {
as: 'a',
href: upgradeUrl,
onClick: trackUpgradeClick,
};
return (
<ActionButton
iconBefore={Locked}
variant="outline-primary"
disabled={disableUpgradeCourse}
{...!disableUpgradeCourse && enabledProps}
>
{formatMessage(messages.upgrade)}
</ActionButton>
);
};
UpgradeButton.propTypes = {
cardId: PropTypes.string.isRequired,
};
export default UpgradeButton;

View File

@@ -1,49 +0,0 @@
import { shallow } from '@edx/react-unit-test-utils';
import track from 'tracking';
import { reduxHooks } from 'hooks';
import useActionDisabledState from '../hooks';
import UpgradeButton from './UpgradeButton';
jest.mock('tracking', () => ({
course: {
upgradeClicked: jest.fn().mockName('segment.trackUpgradeClicked'),
},
}));
jest.mock('hooks', () => ({
reduxHooks: {
useCardCourseRunData: jest.fn(),
useTrackCourseEvent: jest.fn(
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
),
},
}));
jest.mock('../hooks', () => jest.fn(() => ({ disableUpgradeCourse: false })));
jest.mock('./ActionButton', () => 'ActionButton');
describe('UpgradeButton', () => {
const props = {
cardId: 'cardId',
};
const upgradeUrl = 'upgradeUrl';
reduxHooks.useCardCourseRunData.mockReturnValue({ upgradeUrl });
describe('snapshot', () => {
test('can upgrade', () => {
const wrapper = shallow(<UpgradeButton {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.instance.props.disabled).toEqual(false);
expect(wrapper.instance.props.onClick).toEqual(reduxHooks.useTrackCourseEvent(
track.course.upgradeClicked,
props.cardId,
upgradeUrl,
));
});
test('cannot upgrade', () => {
useActionDisabledState.mockReturnValueOnce({ disableUpgradeCourse: true });
const wrapper = shallow(<UpgradeButton {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.instance.props.disabled).toEqual(true);
});
});
});

View File

@@ -15,7 +15,7 @@ jest.mock('hooks', () => ({
reduxHooks: {
useCardCourseRunData: jest.fn(() => ({ homeUrl: 'homeUrl' })),
useTrackCourseEvent: jest.fn(
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
(eventName, cardId, url) => ({ trackCourseEvent: { eventName, cardId, url } }),
),
},
}));

View File

@@ -10,7 +10,7 @@ exports[`BeginCourseButton snapshot disabled snapshot 1`] = `
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"upgradeUrl": "home-urlexec-ed-tracking-path=cardId",
"url": "home-urlexec-ed-tracking-path=cardId",
},
}
}
@@ -29,7 +29,7 @@ exports[`BeginCourseButton snapshot enabled snapshot 1`] = `
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"upgradeUrl": "home-urlexec-ed-tracking-path=cardId",
"url": "home-urlexec-ed-tracking-path=cardId",
},
}
}

View File

@@ -10,7 +10,7 @@ exports[`ResumeButton snapshot disabled snapshot 1`] = `
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"upgradeUrl": "resume-urlexec-ed-tracking-path=cardId",
"url": "resume-urlexec-ed-tracking-path=cardId",
},
}
}
@@ -29,7 +29,7 @@ exports[`ResumeButton snapshot enabled snapshot 1`] = `
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"upgradeUrl": "resume-urlexec-ed-tracking-path=cardId",
"url": "resume-urlexec-ed-tracking-path=cardId",
},
}
}

View File

@@ -1,32 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UpgradeButton snapshot can upgrade 1`] = `
<ActionButton
as="a"
disabled={false}
href="upgradeUrl"
iconBefore={[MockFunction icons.Locked]}
onClick={
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.trackUpgradeClicked],
"upgradeUrl": "upgradeUrl",
},
}
}
variant="outline-primary"
>
Upgrade
</ActionButton>
`;
exports[`UpgradeButton snapshot cannot upgrade 1`] = `
<ActionButton
disabled={true}
iconBefore={[MockFunction icons.Locked]}
variant="outline-primary"
>
Upgrade
</ActionButton>
`;

View File

@@ -10,7 +10,7 @@ exports[`ViewCourseButton learner can view course 1`] = `
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"upgradeUrl": "homeUrl",
"url": "homeUrl",
},
}
}
@@ -29,7 +29,7 @@ exports[`ViewCourseButton learner cannot view course 1`] = `
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"upgradeUrl": "homeUrl",
"url": "homeUrl",
},
}
}

View File

@@ -3,7 +3,6 @@ import { shallow } from '@edx/react-unit-test-utils';
import { reduxHooks } from 'hooks';
import CourseCardActionSlot from 'plugin-slots/CourseCardActionSlot';
import UpgradeButton from './UpgradeButton';
import SelectSessionButton from './SelectSessionButton';
import BeginCourseButton from './BeginCourseButton';
import ResumeButton from './ResumeButton';
@@ -21,7 +20,6 @@ jest.mock('hooks', () => ({
}));
jest.mock('plugin-slots/CourseCardActionSlot', () => 'CustomActionButton');
jest.mock('./UpgradeButton', () => 'UpgradeButton');
jest.mock('./SelectSessionButton', () => 'SelectSessionButton');
jest.mock('./ViewCourseButton', () => 'ViewCourseButton');
jest.mock('./BeginCourseButton', () => 'BeginCourseButton');
@@ -59,19 +57,7 @@ describe('CourseCardActions', () => {
});
});
describe('output', () => {
describe('Exec Ed course', () => {
it('does not render upgrade button', () => {
mockHooks({ isExecEd2UCourse: true });
render();
expect(el.instance.findByType(UpgradeButton).length).toEqual(0);
});
});
describe('entitlement course', () => {
it('does not render upgrade button', () => {
mockHooks({ isEntitlement: true });
render();
expect(el.instance.findByType(UpgradeButton).length).toEqual(0);
});
it('renders ViewCourseButton if fulfilled', () => {
mockHooks({ isEntitlement: true, isFulfilled: true });
render();
@@ -83,22 +69,15 @@ describe('CourseCardActions', () => {
expect(el.instance.findByType(SelectSessionButton)[0].props.cardId).toEqual(cardId);
});
});
describe('verified course', () => {
it('does not render upgrade button', () => {
mockHooks({ isVerified: true });
render();
expect(el.instance.findByType(UpgradeButton).length).toEqual(0);
});
});
describe('not entitlement, verified, or exec ed', () => {
it('renders UpgradeButton and ViewCourseButton for archived courses', () => {
it('renders CourseCardActionSlot and ViewCourseButton for archived courses', () => {
mockHooks({ isArchived: true });
render();
expect(el.instance.findByType(CourseCardActionSlot)[0].props.cardId).toEqual(cardId);
expect(el.instance.findByType(ViewCourseButton)[0].props.cardId).toEqual(cardId);
});
describe('unstarted courses', () => {
it('renders UpgradeButton and BeginCourseButton', () => {
it('renders CourseCardActionSlot and BeginCourseButton', () => {
mockHooks();
render();
expect(el.instance.findByType(CourseCardActionSlot)[0].props.cardId).toEqual(cardId);
@@ -106,7 +85,7 @@ describe('CourseCardActions', () => {
});
});
describe('active courses (started, and not archived)', () => {
it('renders UpgradeButton and ResumeButton', () => {
it('renders CourseCardActionSlot and ResumeButton', () => {
mockHooks({ hasStarted: true });
render();
expect(el.instance.findByType(CourseCardActionSlot)[0].props.cardId).toEqual(cardId);

View File

@@ -1,11 +1,6 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
upgrade: {
id: 'learner-dash.courseCard.actions.upgrade',
description: 'Course card upgrade button text',
defaultMessage: 'Upgrade',
},
beginCourse: {
id: 'learner-dash.courseCard.actions.beginCourse',
description: 'Course card begin-course button text',

View File

@@ -12,7 +12,6 @@ export const CourseBanner = ({ cardId }) => {
const {
isVerified,
isAuditAccessExpired,
canUpgrade,
coursewareAccess = {},
} = reduxHooks.useCardEnrollmentData(cardId);
const courseRun = reduxHooks.useCardCourseRunData(cardId);
@@ -26,13 +25,7 @@ export const CourseBanner = ({ cardId }) => {
return (
<>
{isAuditAccessExpired
&& (canUpgrade ? (
<Banner>
{formatMessage(messages.auditAccessExpired)}
{' '}
{formatMessage(messages.upgradeToAccess)}
</Banner>
) : (
&& (
<Banner>
{formatMessage(messages.auditAccessExpired)}
{' '}
@@ -40,17 +33,7 @@ export const CourseBanner = ({ cardId }) => {
{formatMessage(messages.findAnotherCourse)}
</Hyperlink>
</Banner>
))}
{courseRun.isActive && !canUpgrade && (
<Banner>
{formatMessage(messages.upgradeDeadlinePassed)}
{' '}
<Hyperlink isInline destination={courseRun.marketingUrl || ''}>
{formatMessage(messages.exploreCourseDetails)}
</Hyperlink>
</Banner>
)}
)}
{(!isStaff && isTooEarly && courseRun.startDate) && (
<Banner>
@@ -59,6 +42,7 @@ export const CourseBanner = ({ cardId }) => {
})}
</Banner>
)}
{(!isStaff && hasUnmetPrerequisites) && (
<Banner>{formatMessage(messages.prerequisitesNotMet)}</Banner>
)}

View File

@@ -25,7 +25,6 @@ let el;
const enrollmentData = {
isVerified: false,
canUpgrade: false,
isAuditAccessExpired: false,
coursewareAccess: {
hasUnmetPrerequisites: false,
@@ -65,51 +64,18 @@ describe('CourseBanner', () => {
render({ enrollment: { isVerified: true } });
expect(el.isEmptyRender()).toEqual(true);
});
describe('audit access expired, can upgrade', () => {
beforeEach(() => {
render({ enrollment: { isAuditAccessExpired: true, canUpgrade: true } });
});
test('snapshot: (auditAccessExpired, upgradeToAccess)', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('messages: (auditAccessExpired, upgradeToAccess)', () => {
expect(el.instance.children[0].children[0].el).toContain(messages.auditAccessExpired.defaultMessage);
expect(el.instance.children[0].children[2].el).toContain(messages.upgradeToAccess.defaultMessage);
});
});
describe('audit access expired, cannot upgrade', () => {
describe('audit access expired', () => {
beforeEach(() => {
render({ enrollment: { isAuditAccessExpired: true } });
});
test('snapshot: (auditAccessExpired, findAnotherCourse hyperlink)', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('messages: (auditAccessExpired, upgradeToAccess)', () => {
test('messages: auditAccessExpired', () => {
expect(el.instance.children[0].children[0].el).toContain(messages.auditAccessExpired.defaultMessage);
expect(el.instance.findByType(Hyperlink)[0].children[0].el).toEqual(messages.findAnotherCourse.defaultMessage);
});
});
describe('course run active and cannot upgrade', () => {
beforeEach(() => {
render({ courseRun: { isActive: true } });
});
test('snapshot: (upgradseDeadlinePassed, exploreCourseDetails hyperlink)', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('messages: (upgradseDeadlinePassed, exploreCourseDetails hyperlink)', () => {
expect(el.instance.children[0].children[0].el).toContain(messages.upgradeDeadlinePassed.defaultMessage);
const link = el.instance.findByType(Hyperlink);
expect(link[0].children[0].el).toEqual(messages.exploreCourseDetails.defaultMessage);
expect(link[0].props.destination).toEqual(courseRunData.marketingUrl);
});
});
test('no display if audit access not expired and (course is not active or can upgrade)', () => {
render();
// isEmptyRender() isn't true because the minimal is <Fragment />
expect(el.instance.children).toEqual([]);
render({ enrollment: { canUpgrade: true }, courseRun: { isActive: true } });
expect(el.instance.children).toEqual([]);
});
describe('unmet prerequisites', () => {
beforeEach(() => {
render({ enrollment: { coursewareAccess: { hasUnmetPrerequisites: true } } });

View File

@@ -1,16 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CourseBanner audit access expired, can upgrade snapshot: (auditAccessExpired, upgradeToAccess) 1`] = `
<Fragment>
<Banner>
Your audit access to this course has expired.
Upgrade now to access your course again.
</Banner>
</Fragment>
`;
exports[`CourseBanner audit access expired, cannot upgrade snapshot: (auditAccessExpired, findAnotherCourse hyperlink) 1`] = `
exports[`CourseBanner audit access expired snapshot: (auditAccessExpired, findAnotherCourse hyperlink) 1`] = `
<Fragment>
<Banner>
Your audit access to this course has expired.
@@ -25,21 +15,6 @@ exports[`CourseBanner audit access expired, cannot upgrade snapshot: (auditAcces
</Fragment>
`;
exports[`CourseBanner course run active and cannot upgrade snapshot: (upgradseDeadlinePassed, exploreCourseDetails hyperlink) 1`] = `
<Fragment>
<Banner>
Your upgrade deadline for this course has passed. To upgrade, enroll in a session that is farther in the future.
<Hyperlink
destination="marketing-url"
isInline={true}
>
Explore course details.
</Hyperlink>
</Banner>
</Fragment>
`;
exports[`CourseBanner snapshot: stacking banners 1`] = `<Fragment />`;
exports[`CourseBanner staff snapshot: isStaff 1`] = `<Fragment />`;

View File

@@ -8,7 +8,7 @@ exports[`CourseCardBanners render with isEnrolled false 1`] = `
<RelatedProgramsBanner
cardId="test-card-id"
/>
<CourseBanner
<CourseBannerSlot
cardId="test-card-id"
/>
<EntitlementBanner
@@ -25,7 +25,7 @@ exports[`CourseCardBanners renders default CourseCardBanners 1`] = `
<RelatedProgramsBanner
cardId="test-card-id"
/>
<CourseBanner
<CourseBannerSlot
cardId="test-card-id"
/>
<EntitlementBanner

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { reduxHooks } from 'hooks';
import CourseBanner from './CourseBanner';
import CourseBannerSlot from 'plugin-slots/CourseBannerSlot';
import CertificateBanner from './CertificateBanner';
import CreditBanner from './CreditBanner';
import EntitlementBanner from './EntitlementBanner';
@@ -14,7 +14,7 @@ export const CourseCardBanners = ({ cardId }) => {
return (
<div className="course-card-banners" data-testid="CourseCardBanners">
<RelatedProgramsBanner cardId={cardId} />
<CourseBanner cardId={cardId} />
<CourseBannerSlot cardId={cardId} />
<EntitlementBanner cardId={cardId} />
{isEnrolled && <CertificateBanner cardId={cardId} />}
{isEnrolled && <CreditBanner cardId={cardId} />}

View File

@@ -6,26 +6,11 @@ const messages = defineMessages({
description: 'Audit access expiration banner message',
defaultMessage: 'Your audit access to this course has expired.',
},
upgradeToAccess: {
id: 'learner-dash.courseCard.banners.upgradeToAccess',
description: 'Upgrade prompt for audit-expired learners that can still upgrade',
defaultMessage: 'Upgrade now to access your course again.',
},
findAnotherCourse: {
id: 'learner-dash.courseCard.banners.findAnotherCourse',
description: 'Action prompt taking learners to course exploration',
defaultMessage: 'Find another course',
},
upgradeDeadlinePassed: {
id: 'learner-dash.courseCard.banners.upgradeDeadlinePassed',
description: 'Audit upgrade deadline passed banner message',
defaultMessage: 'Your upgrade deadline for this course has passed. To upgrade, enroll in a session that is farther in the future.',
},
exploreCourseDetails: {
id: 'learner-dash.courseCard.banners.exploreCourseDetails',
description: 'Action prompt taking learners to course details page',
defaultMessage: 'Explore course details.',
},
certRestricted: {
id: 'learner-dash.courseCard.banners.certificateRestricted',
description: 'Restricted certificate warning message',

View File

@@ -1,7 +1,3 @@
@import "~@edx/brand/paragon/variables";
@import "~@openedx/paragon/scss/core/core";
@import "~@edx/brand/paragon/overrides";
a.course-card-title {
color: $black;
color: var(--pgn-color-black);
}

View File

@@ -20,11 +20,13 @@ export const CourseCardImage = ({ cardId, orientation }) => {
const { isVerified } = reduxHooks.useCardEnrollmentData(cardId);
const { disableCourseTitle } = useActionDisabledState(cardId);
const handleImageClicked = reduxHooks.useTrackCourseEvent(courseImageClicked, cardId, homeUrl);
const wrapperClassName = `pgn__card-wrapper-image-cap overflow-visible ${orientation}`;
const wrapperClassName = `pgn__card-wrapper-image-cap d-inline-block overflow-visible ${orientation}`;
const image = (
<>
<img
className="pgn__card-image-cap show"
// w-100 is necessary for images on Safari, otherwise stretches full height of the image
// https://stackoverflow.com/a/44250830
className="pgn__card-image-cap w-100 show"
src={bannerImgSrc}
alt={formatMessage(messages.bannerAlt)}
/>

View File

@@ -18,8 +18,8 @@ jest.mock('hooks', () => ({
useCardCourseData: jest.fn(() => ({ bannerImgSrc: 'banner-img-src' })),
useCardCourseRunData: jest.fn(() => ({ homeUrl })),
useCardEnrollmentData: jest.fn(() => ({ isVerified: true })),
useTrackCourseEvent: jest.fn((eventName, cardId, upgradeUrl) => ({
trackCourseEvent: { eventName, cardId, upgradeUrl },
useTrackCourseEvent: jest.fn((eventName, cardId, url) => ({
trackCourseEvent: { eventName, cardId, url },
})),
},
}));

View File

@@ -17,8 +17,8 @@ jest.mock('hooks', () => ({
reduxHooks: {
useCardCourseData: jest.fn(() => ({ courseName: 'course-name' })),
useCardCourseRunData: jest.fn(() => ({ homeUrl })),
useTrackCourseEvent: jest.fn((eventName, cardId, upgradeUrl) => ({
trackCourseEvent: { eventName, cardId, upgradeUrl },
useTrackCourseEvent: jest.fn((eventName, cardId, url) => ({
trackCourseEvent: { eventName, cardId, url },
})),
},
}));

View File

@@ -2,14 +2,14 @@
exports[`CourseCardImage snapshot renders clickable link course Image 1`] = `
<a
className="pgn__card-wrapper-image-cap overflow-visible orientation"
className="pgn__card-wrapper-image-cap d-inline-block overflow-visible orientation"
href="home-url"
onClick={
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.courseImageClicked],
"upgradeUrl": "home-url",
"url": "home-url",
},
}
}
@@ -18,7 +18,7 @@ exports[`CourseCardImage snapshot renders clickable link course Image 1`] = `
<Fragment>
<img
alt="Course thumbnail"
className="pgn__card-image-cap show"
className="pgn__card-image-cap w-100 show"
src="banner-img-src"
/>
<span
@@ -43,12 +43,12 @@ exports[`CourseCardImage snapshot renders clickable link course Image 1`] = `
exports[`CourseCardImage snapshot renders disabled link 1`] = `
<div
className="pgn__card-wrapper-image-cap overflow-visible orientation"
className="pgn__card-wrapper-image-cap d-inline-block overflow-visible orientation"
>
<Fragment>
<img
alt="Course thumbnail"
className="pgn__card-image-cap show"
className="pgn__card-image-cap w-100 show"
src="banner-img-src"
/>
<span

View File

@@ -11,7 +11,7 @@ exports[`CourseCardTitle snapshot renders clickable link course title 1`] = `
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.courseTitleClicked],
"upgradeUrl": "home-url",
"url": "home-url",
},
}
}

View File

@@ -3,19 +3,18 @@ import { reduxHooks } from 'hooks';
export const useActionDisabledState = (cardId) => {
const { isMasquerading } = reduxHooks.useMasqueradeData();
const {
canUpgrade, hasAccess, isAudit, isAuditAccessExpired,
hasAccess, isAudit, isAuditAccessExpired,
} = reduxHooks.useCardEnrollmentData(cardId);
const {
isEntitlement, isFulfilled, canChange, hasSessions,
} = reduxHooks.useCardEntitlementData(cardId);
const { resumeUrl, homeUrl, upgradeUrl } = reduxHooks.useCardCourseRunData(cardId);
const { resumeUrl, homeUrl } = reduxHooks.useCardCourseRunData(cardId);
const disableBeginCourse = !homeUrl || (isMasquerading || !hasAccess || (isAudit && isAuditAccessExpired));
const disableResumeCourse = !resumeUrl || (isMasquerading || !hasAccess || (isAudit && isAuditAccessExpired));
const disableViewCourse = !hasAccess || (isAudit && isAuditAccessExpired);
const disableSelectSession = !isEntitlement || isMasquerading || !hasAccess || (!canChange || !hasSessions);
const disableUpgradeCourse = !upgradeUrl || (isMasquerading && !canUpgrade);
const disableCourseTitle = (isEntitlement && !isFulfilled) || disableViewCourse;
@@ -23,7 +22,6 @@ export const useActionDisabledState = (cardId) => {
disableBeginCourse,
disableResumeCourse,
disableViewCourse,
disableUpgradeCourse,
disableSelectSession,
disableCourseTitle,
};

View File

@@ -16,7 +16,6 @@ const cardId = 'my-test-course-number';
describe('useActionDisabledState', () => {
const defaultData = {
isMasquerading: false,
canUpgrade: false,
isEntitlement: false,
isFulfilled: false,
canChange: false,
@@ -26,12 +25,10 @@ describe('useActionDisabledState', () => {
isAuditAccessExpired: false,
resumeUrl: 'resume.url',
homeUrl: 'home.url',
upgradeUrl: 'upgrade.url',
};
const mockHooksData = (args) => {
const {
isMasquerading,
canUpgrade,
isEntitlement,
isFulfilled,
canChange,
@@ -41,11 +38,9 @@ describe('useActionDisabledState', () => {
isAuditAccessExpired,
resumeUrl,
homeUrl,
upgradeUrl,
} = { ...defaultData, ...args };
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading });
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({
canUpgrade,
hasAccess,
isAudit,
isAuditAccessExpired,
@@ -59,7 +54,6 @@ describe('useActionDisabledState', () => {
reduxHooks.useCardCourseRunData.mockReturnValueOnce({
resumeUrl,
homeUrl,
upgradeUrl,
});
};
@@ -121,21 +115,6 @@ describe('useActionDisabledState', () => {
testDisabled({ hasAccess: true }, false);
});
});
describe('disableUpgradeCourse', () => {
const testDisabled = (data, expected) => {
mockHooksData(data);
expect(runHook().disableUpgradeCourse).toBe(expected);
};
it('disable when upgradeUrl is invalid', () => {
testDisabled({ upgradeUrl: null }, true);
});
it('disable when isMasquerading is true and canUpgrade is false', () => {
testDisabled({ isMasquerading: true, canUpgrade: false }, true);
});
it('enable when all conditions are met', () => {
testDisabled({ canUpgrade: true }, false);
});
});
describe('disableSelectSession', () => {
const testDisabled = (data, expected) => {
mockHooksData(data);

View File

@@ -1,15 +1,13 @@
@import "@openedx/paragon/scss/core/core";
#no-courses-content-view {
border: 2px solid $light-400;
border: 2px solid var(--pgn-color-light-400);
flex-direction: column;
padding-bottom: map-get($spacers, 5);
padding-top: map-get($spacers, 5);
padding-bottom: var(--pgn-spacing-spacer-5);
padding-top: var(--pgn-spacing-spacer-5);
height: 100%;
& > * {
margin-top: map-get($spacers, 3);
margin-bottom: map-get($spacers, 3);
margin-top: var(--pgn-spacing-spacer-3);
margin-bottom: var(--pgn-spacing-spacer-3);
}
}

View File

@@ -1,5 +1,3 @@
@import "@openedx/paragon/scss/core/core";
.course-list-heading-container {
display: flex;
flex-direction: row;
@@ -8,8 +6,8 @@
}
.course-list-title {
font-size: $h2-font-size;
margin: map-get($spacers, 3) 0;
font-size: var(--pgn-typography-font-size-h2-base);
margin: var(--pgn-spacing-spacer-3) 0;
}
.course-list-loading {
@@ -24,7 +22,7 @@
align-self: center;
}
@include media-breakpoint-down(md) {
@media (--pgn-size-breakpoint-max-width-md) {
.course-list-heading-container {
flex-direction: column-reverse;
align-items: flex-start;
@@ -36,7 +34,7 @@
}
.course-list-title {
font-size: $h3-font-size;
margin: map-get($spacers, 2) 0;
font-size: var(--pgn-typography-font-size-h3-base);
margin: var(--pgn-spacing-spacer-2) 0;
}
}

View File

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Dashboard snapshots courses loaded, show select session modal, no available dashboards snapshot 1`] = `
exports[`Dashboard snapshots courses loaded, show select session modal snapshot 1`] = `
<div
className="d-flex flex-column p-2 pt-0"
id="dashboard-container"
@@ -11,6 +11,7 @@ exports[`Dashboard snapshots courses loaded, show select session modal, no avail
test-page-title
</h1>
<Fragment>
<DashboardModalSlot />
<SelectSessionModal />
</Fragment>
<div
@@ -43,7 +44,7 @@ exports[`Dashboard snapshots courses still loading snapshot 1`] = `
</div>
`;
exports[`Dashboard snapshots there are no courses, there ARE available dashboards snapshot 1`] = `
exports[`Dashboard snapshots there are no courses snapshot 1`] = `
<div
className="d-flex flex-column p-2 pt-0"
id="dashboard-container"
@@ -54,7 +55,7 @@ exports[`Dashboard snapshots there are no courses, there ARE available dashboard
test-page-title
</h1>
<Fragment>
<EnterpriseDashboardModal />
<DashboardModalSlot />
</Fragment>
<div
data-testid="dashboard-content"

View File

@@ -2,9 +2,9 @@ import React from 'react';
import { reduxHooks } from 'hooks';
import { RequestKeys } from 'data/constants/requests';
import EnterpriseDashboardModal from 'containers/EnterpriseDashboardModal';
import SelectSessionModal from 'containers/SelectSessionModal';
import CoursesPanel from 'containers/CoursesPanel';
import DashboardModalSlot from 'plugin-slots/DashboardModalSlot';
import LoadingView from './LoadingView';
import DashboardLayout from './DashboardLayout';
@@ -15,7 +15,6 @@ export const Dashboard = () => {
hooks.useInitializeDashboard();
const { pageTitle } = hooks.useDashboardMessages();
const hasCourses = reduxHooks.useHasCourses();
const hasAvailableDashboards = reduxHooks.useHasAvailableDashboards();
const initIsPending = reduxHooks.useRequestIsPending(RequestKeys.initialize);
const showSelectSessionModal = reduxHooks.useShowSelectSessionModal();
@@ -24,7 +23,7 @@ export const Dashboard = () => {
<h1 className="sr-only">{pageTitle}</h1>
{!initIsPending && (
<>
{hasAvailableDashboards && <EnterpriseDashboardModal />}
<DashboardModalSlot />
{(hasCourses && showSelectSessionModal) && <SelectSessionModal />}
</>
)}

View File

@@ -1,21 +1,27 @@
@import "@openedx/paragon/scss/core/core";
.course-list-column {
padding: 0 map-get($spacers, 4);
padding: 0 var(--pgn-spacing-spacer-4);
}
.sidebar-column {
padding: 0 map-get($spacers, 3) 0 map-get($spacers, 1);
padding: 0 var(--pgn-spacing-spacer-3) 0 var(--pgn-spacing-spacer-1);
&.not-collapsed {
padding-top: var(--pgn-spacing-spacer-2);
& >:first-child {
margin-top: var(--pgn-spacing-spacer-5-5);
}
}
}
@include media-breakpoint-down(lg) {
@media (--pgn-size-breakpoint-max-width-lg) {
.sidebar-column {
// grid are inheriting dir="ltr" from the body, so we need to override it
[dir=ltr] & {
padding: 0 map-get($spacers, 3);
padding: 0 var(--pgn-spacing-spacer-3);
}
[dir=rtl] & {
padding: 0 map-get($spacers, 3);
padding: 0 var(--pgn-spacing-spacer-3);
}
}
}
}

View File

@@ -2,7 +2,6 @@ import { shallow } from '@edx/react-unit-test-utils';
import { reduxHooks } from 'hooks';
import EnterpriseDashboardModal from 'containers/EnterpriseDashboardModal';
import SelectSessionModal from 'containers/SelectSessionModal';
import CoursesPanel from 'containers/CoursesPanel';
@@ -14,13 +13,12 @@ import Dashboard from '.';
jest.mock('hooks', () => ({
reduxHooks: {
useHasCourses: jest.fn(),
useHasAvailableDashboards: jest.fn(),
useShowSelectSessionModal: jest.fn(),
useRequestIsPending: jest.fn(),
},
}));
jest.mock('containers/EnterpriseDashboardModal', () => 'EnterpriseDashboardModal');
jest.mock('plugin-slots/DashboardModalSlot', () => 'DashboardModalSlot');
jest.mock('containers/CoursesPanel', () => 'CoursesPanel');
jest.mock('./LoadingView', () => 'LoadingView');
jest.mock('./DashboardLayout', () => 'DashboardLayout');
@@ -38,12 +36,10 @@ describe('Dashboard', () => {
});
const createWrapper = ({
hasCourses,
hasAvailableDashboards,
initIsPending,
showSelectSessionModal,
}) => {
reduxHooks.useHasCourses.mockReturnValueOnce(hasCourses);
reduxHooks.useHasAvailableDashboards.mockReturnValueOnce(hasAvailableDashboards);
reduxHooks.useRequestIsPending.mockReturnValueOnce(initIsPending);
reduxHooks.useShowSelectSessionModal.mockReturnValueOnce(showSelectSessionModal);
return shallow(<Dashboard />);
@@ -71,7 +67,6 @@ describe('Dashboard', () => {
const testView = ({
props,
content: [contentName, contentEl],
showEnterpriseModal,
showSelectSessionModal,
}) => {
beforeEach(() => { wrapper = createWrapper(props); });
@@ -80,10 +75,6 @@ describe('Dashboard', () => {
it(`renders ${contentName}`, () => {
testContent(contentEl);
});
it(`${renderString(showEnterpriseModal)} dashbaord modal`, () => {
expect(wrapper.instance.findByType(EnterpriseDashboardModal).length)
.toEqual(showEnterpriseModal ? 1 : 0);
});
it(`${renderString(showSelectSessionModal)} select session modal`, () => {
expect(wrapper.instance.findByType(SelectSessionModal).length).toEqual(showSelectSessionModal ? 1 : 0);
});
@@ -92,44 +83,38 @@ describe('Dashboard', () => {
testView({
props: {
hasCourses: false,
hasAvailableDashboards: false,
initIsPending: true,
showSelectSessionModal: false,
},
content: ['LoadingView', <LoadingView />],
showEnterpriseModal: false,
showSelectSessionModal: false,
});
});
describe('courses loaded, show select session modal, no available dashboards', () => {
describe('courses loaded, show select session modal', () => {
testView({
props: {
hasCourses: true,
hasAvailableDashboards: false,
initIsPending: false,
showSelectSessionModal: true,
},
content: ['LoadedView', (
<DashboardLayout><CoursesPanel /></DashboardLayout>
)],
showEnterpriseModal: false,
showSelectSessionModal: true,
});
});
describe('there are no courses, there ARE available dashboards', () => {
describe('there are no courses', () => {
testView({
props: {
hasCourses: false,
hasAvailableDashboards: true,
initIsPending: false,
showSelectSessionModal: false,
},
content: ['Dashboard layout with no courses sidebar and content', (
<DashboardLayout><CoursesPanel /></DashboardLayout>
)],
showEnterpriseModal: true,
showSelectSessionModal: false,
});
});

View File

@@ -1,42 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EnterpriseDashboard empty snapshot 1`] = `null`;
exports[`EnterpriseDashboard snapshot 1`] = `
<ModalDialog
hasCloseButton={false}
onClose={[MockFunction useEnterpriseDashboardHook.handleEscape]}
title=""
>
<div
className="bg-white p-3 rounded shadow"
style={
{
"textAlign": "start",
}
}
>
<h4>
You have access to the edX, Inc. dashboard
</h4>
<p>
To access the courses available to you through edX, Inc., visit the edX, Inc. dashboard now.
</p>
<ActionRow>
<Button
onClick={[MockFunction useEnterpriseDashboardHook.handleClose]}
variant="tertiary"
>
Dismiss
</Button>
<Button
href="/edx-dashboard"
onClick={[MockFunction useEnterpriseDashboardHook.handleCTAClick]}
type="a"
>
Go to dashboard
</Button>
</ActionRow>
</div>
</ModalDialog>
`;

View File

@@ -1,48 +0,0 @@
import React from 'react';
import { StrictDict } from 'utils';
import track from 'tracking';
import { reduxHooks } from 'hooks';
import * as module from './hooks';
export const state = StrictDict({
showModal: (val) => React.useState(val), // eslint-disable-line
});
const { modalOpened, modalClosed, modalCTAClicked } = track.enterpriseDashboard;
export const useEnterpriseDashboardHook = () => {
const [showModal, setShowModal] = module.state.showModal(true);
const dashboard = reduxHooks.useEnterpriseDashboardData();
const trackOpened = modalOpened(dashboard.enterpriseUUID);
const trackClose = modalClosed(dashboard.enterpriseUUID, 'Cancel button');
const trackEscape = modalClosed(dashboard.enterpriseUUID, 'Escape');
const handleCTAClick = modalCTAClicked(dashboard.enterpriseUUID, dashboard.url);
const handleClose = () => {
trackClose();
setShowModal(false);
};
const handleEscape = () => {
trackEscape();
setShowModal(false);
};
React.useEffect(() => {
if (dashboard && dashboard.label) {
trackOpened();
}
}, []); // eslint-disable-line
return {
showModal,
handleCTAClick,
handleClose,
handleEscape,
dashboard,
};
};
export default useEnterpriseDashboardHook;

View File

@@ -1,75 +0,0 @@
import { MockUseState } from 'testUtils';
import { reduxHooks } from 'hooks';
import track from 'tracking';
import * as hooks from './hooks';
jest.mock('hooks', () => ({
reduxHooks: {
useEnterpriseDashboardData: jest.fn(),
},
}));
jest.mock('tracking', () => {
const modalOpenedEvent = jest.fn();
const modalClosedEvent = jest.fn();
const modalCTAClickedEvent = jest.fn();
return {
__esModule: true,
default: {
enterpriseDashboard: {
modalOpenedEvent,
modalClosedEvent,
modalCTAClickedEvent,
modalOpened: jest.fn(() => modalOpenedEvent),
modalClosed: jest.fn(() => modalClosedEvent),
modalCTAClicked: jest.fn(() => modalCTAClickedEvent),
},
},
};
});
const state = new MockUseState(hooks);
const enterpriseDashboardData = { label: 'edX, Inc.', url: '/edx-dashboard' };
describe('EnterpriseDashboard hooks', () => {
reduxHooks.useEnterpriseDashboardData.mockReturnValue({ ...enterpriseDashboardData });
describe('state values', () => {
state.testGetter(state.keys.showModal);
});
describe('behavior', () => {
let out;
beforeEach(() => {
state.mock();
out = hooks.useEnterpriseDashboardHook();
});
afterEach(state.restore);
test('useEnterpriseDashboardHook to return dashboard data from redux hooks', () => {
expect(out.dashboard).toMatchObject(enterpriseDashboardData);
});
test('modal initializes to shown when rendered and closes on click', () => {
state.expectInitializedWith(state.keys.showModal, true);
out.handleClose();
expect(state.values.showModal).toEqual(false);
});
test('modal initializes to shown when rendered and closes on escape', () => {
state.expectInitializedWith(state.keys.showModal, true);
out.handleEscape();
expect(state.values.showModal).toEqual(false);
});
test('CTA click tracks modalCTAClicked', () => {
out.handleCTAClick();
expect(track.enterpriseDashboard.modalCTAClicked).toHaveBeenCalledWith(
enterpriseDashboardData.enterpriseUUID,
enterpriseDashboardData.url,
);
});
});
});

View File

@@ -1,60 +0,0 @@
import React from 'react';
// import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
ModalDialog, ActionRow, Button,
} from '@openedx/paragon';
import messages from './messages';
import useEnterpriseDashboardHook from './hooks';
export const EnterpriseDashboardModal = () => {
const { formatMessage } = useIntl();
const {
showModal,
handleClose,
handleCTAClick,
handleEscape,
dashboard,
} = useEnterpriseDashboardHook();
if (!dashboard || !dashboard.label) {
return null;
}
return (
<ModalDialog
isOpen={showModal}
onClose={handleEscape}
hasCloseButton={false}
title=""
>
<div
className="bg-white p-3 rounded shadow"
style={{ textAlign: 'start' }}
>
<h4>
{formatMessage(messages.enterpriseDialogHeader, {
label: dashboard.label,
})}
</h4>
<p>
{formatMessage(messages.enterpriseDialogBody, {
label: dashboard.label,
})}
</p>
<ActionRow>
<Button variant="tertiary" onClick={handleClose}>
{formatMessage(messages.enterpriseDialogDismissButton)}
</Button>
<Button type="a" href={dashboard.url} onClick={handleCTAClick}>
{formatMessage(messages.enterpriseDialogConfirmButton)}
</Button>
</ActionRow>
</div>
</ModalDialog>
);
};
EnterpriseDashboardModal.propTypes = {};
export default EnterpriseDashboardModal;

View File

@@ -1,29 +0,0 @@
import { shallow } from '@edx/react-unit-test-utils';
import EnterpriseDashboard from '.';
import useEnterpriseDashboardHook from './hooks';
jest.mock('./hooks', () => ({
__esModule: true,
default: jest.fn(),
}));
describe('EnterpriseDashboard', () => {
test('snapshot', () => {
const hookData = {
dashboard: { label: 'edX, Inc.', url: '/edx-dashboard' },
showDialog: false,
handleClose: jest.fn().mockName('useEnterpriseDashboardHook.handleClose'),
handleCTAClick: jest.fn().mockName('useEnterpriseDashboardHook.handleCTAClick'),
handleEscape: jest.fn().mockName('useEnterpriseDashboardHook.handleEscape'),
};
useEnterpriseDashboardHook.mockReturnValueOnce({ ...hookData });
const el = shallow(<EnterpriseDashboard />);
expect(el.snapshot).toMatchSnapshot();
});
test('empty snapshot', () => {
useEnterpriseDashboardHook.mockReturnValueOnce({});
const el = shallow(<EnterpriseDashboard />);
expect(el.snapshot).toMatchSnapshot();
});
});

View File

@@ -1,26 +0,0 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
enterpriseDialogHeader: {
id: 'leanerDashboard.enterpriseDialogHeader',
defaultMessage: 'You have access to the {label} dashboard',
description: 'title for enterpise dashboard dialog',
},
enterpriseDialogBody: {
id: 'leanerDashboard.enterpriseDialogBody',
defaultMessage: 'To access the courses available to you through {label}, visit the {label} dashboard now.',
description: 'Body text for enterpise dashboard dialog',
},
enterpriseDialogDismissButton: {
id: 'leanerDashboard.enterpriseDialogDismissButton',
defaultMessage: 'Dismiss',
description: 'Dismiss button to cancel visiting dashboard',
},
enterpriseDialogConfirmButton: {
id: 'leanerDashboard.enterpriseDialogConfirmButton',
defaultMessage: 'Go to dashboard',
description: 'Confirm button to go to the dashboard url',
},
});
export default messages;

View File

@@ -17,11 +17,11 @@ const getLearnerHeaderMenu = (
content: formatMessage(messages.course),
isActive: true,
},
{
...(getConfig().ENABLE_PROGRAMS ? [{
type: 'item',
href: `${urls.programsUrl()}`,
content: formatMessage(messages.program),
},
}] : []),
{
type: 'item',
href: `${urls.baseAppUrl(courseSearchUrl)}`,
@@ -32,11 +32,11 @@ const getLearnerHeaderMenu = (
},
],
secondaryMenu: [
{
...(getConfig().SUPPORT_URL ? [{
type: 'item',
href: `${getConfig().SUPPORT_URL}`,
content: formatMessage(messages.help),
},
}] : []),
],
userMenu: [
{
@@ -70,6 +70,7 @@ const getLearnerHeaderMenu = (
],
},
],
});
}
);
export default getLearnerHeaderMenu;

View File

@@ -12,11 +12,6 @@ exports[`LearnerDashboardHeader render 1`] = `
"isActive": true,
"type": "item",
},
{
"content": "Programs",
"href": "http://localhost:18000/dashboard/programs",
"type": "item",
},
{
"content": "Discover New",
"href": "http://localhost:18000/course-search-url",
@@ -25,15 +20,7 @@ exports[`LearnerDashboardHeader render 1`] = `
},
]
}
secondaryMenuItems={
[
{
"content": "Help",
"href": "http://localhost:18000/support",
"type": "item",
},
]
}
secondaryMenuItems={[]}
userMenuItems={
[
{

View File

@@ -56,7 +56,7 @@ describe('LearnerDashboardHeader hooks', () => {
username: 'test',
};
const learnerHomeHeaderMenu = useLearnerDashboardHeaderMenu({ courseSearchUrl, authenticatedUser });
expect(learnerHomeHeaderMenu.mainMenu.length).toBe(3);
expect(learnerHomeHeaderMenu.mainMenu.length).toBe(2);
});
});

View File

@@ -29,7 +29,19 @@ describe('LearnerDashboardHeader', () => {
expect(wrapper.instance.findByType('ConfirmEmailBanner')).toHaveLength(1);
expect(wrapper.instance.findByType('MasqueradeBar')).toHaveLength(1);
expect(wrapper.instance.findByType(Header)).toHaveLength(1);
wrapper.instance.findByType(Header)[0].props.mainMenuItems[2].onClick();
wrapper.instance.findByType(Header)[0].props.mainMenuItems[1].onClick();
expect(findCoursesNavClicked).toHaveBeenCalledWith(urls.baseAppUrl('/course-search-url'));
expect(wrapper.instance.findByType(Header)[0].props.secondaryMenuItems.length).toBe(0);
});
test('should display Help link if SUPPORT_URL is set', () => {
mergeConfig({ SUPPORT_URL: 'http://localhost:18000/support' });
const wrapper = shallow(<LearnerDashboardHeader />);
expect(wrapper.instance.findByType(Header)[0].props.secondaryMenuItems.length).toBe(1);
});
test('should display Programs link if it is enabled by configuration', () => {
mergeConfig({ ENABLE_PROGRAMS: true });
const wrapper = shallow(<LearnerDashboardHeader />);
expect(wrapper.instance.findByType(Header)[0].props.mainMenuItems.length).toBe(3);
});
});

View File

@@ -1,38 +1,36 @@
@import "@openedx/paragon/scss/core/core";
.masquerade-bar {
display: flex;
align-items: flex-start;
padding: map-get($spacers, 3);
margin-bottom: map-get($spacers, 2);
padding: var(--pgn-spacing-spacer-3);
margin-bottom: var(--pgn-spacing-spacer-2);
.masquerade-form-label {
padding: map-get($spacers, 2) map-get($spacers, 3);
padding: var(--pgn-spacing-spacer-2) var(--pgn-spacing-spacer-3);
display: flex;
align-items: center;
margin-bottom: 0;
white-space: nowrap;
&>.pgn__icon {
margin-right: map-get($spacers, 3);
margin-right: var(--pgn-spacing-spacer-3);
}
}
.masquerade-form-input {
margin-bottom: 0;
flex-grow: 1;
max-width: map-get($grid-breakpoints, 'md');
max-width: var(--pgn-size-breakpoint-md);
}
.masquerade-chip {
padding: map-get($spacers, 2) map-get($spacers, 3);
font-size: $font-size-base;
padding: var(--pgn-spacing-spacer-2) var(--pgn-spacing-spacer-3);
font-size: var(--pgn-typography-font-size-base);
}
}
@include media-breakpoint-down(md) {
@media (--pgn-size-breakpoint-max-width-md) {
.masquerade-bar {
margin: auto;
padding: map-get($spacers, 3) map-get($spacers, 4);
padding: var(--pgn-spacing-spacer-3) var(--pgn-spacing-spacer-4);
}
}
}

View File

@@ -1,5 +1,6 @@
/* eslint-disable quotes */
import { StrictDict } from 'utils';
import { defineMessages } from '@edx/frontend-platform/i18n';
export const reasonKeys = StrictDict({
prereqs: 'prereqs',
@@ -26,7 +27,7 @@ export const order = [
reasonKeys.easy,
];
const messages = StrictDict({
const messages = defineMessages({
[reasonKeys.prereqs]: {
id: 'learner-dash.unenrollConfirm.reasons.prereqs',
description: 'Unenroll reason option - missing prerequisites',

View File

@@ -10,10 +10,7 @@ export const numCourses = createSelector(
(courseData) => Object.keys(courseData).length,
);
export const hasCourses = createSelector([module.numCourses], (num) => num > 0);
export const hasAvailableDashboards = createSelector(
[simpleSelectors.enterpriseDashboard],
(data) => data !== null && data.isLearnerPortalEnabled === true,
);
export const showSelectSessionModal = createSelector(
[simpleSelectors.selectSessionModal],
(data) => data.cardId != null,
@@ -22,6 +19,5 @@ export const showSelectSessionModal = createSelector(
export default StrictDict({
numCourses,
hasCourses,
hasAvailableDashboards,
showSelectSessionModal,
});

View File

@@ -17,15 +17,6 @@ describe('basic app selectors', () => {
expect(cb(0)).toEqual(false);
});
});
describe('hasAvailableDashboards', () => {
it('returns true iff the enterpriseDashboard field is populated and learner portal is enabled', () => {
const { preSelectors, cb } = appSelectors.hasAvailableDashboards;
expect(preSelectors).toEqual([simpleSelectors.enterpriseDashboard]);
expect(cb({ isLearnerPortalEnabled: true })).toEqual(true);
expect(cb({ isLearnerPortalEnabled: false })).toEqual(false);
expect(cb(null)).toEqual(false);
});
});
describe('showSelectSessionModal', () => {
it('returns true if the selectSessionModal cardId is not null', () => {
const { preSelectors, cb } = appSelectors.showSelectSessionModal;

View File

@@ -52,7 +52,6 @@ export const courseCard = StrictDict({
homeUrl: courseRun.homeUrl,
marketingUrl: courseRun.marketingUrl,
upgradeUrl: courseRun.upgradeUrl,
progressUrl: baseAppUrl(courseRun.progressUrl),
resumeUrl: baseAppUrl(courseRun.resumeUrl), // resume will route this to learning mfe.

View File

@@ -156,7 +156,6 @@ describe('courseCard selectors module', () => {
homeUrl: 'test-home-url',
marketingUrl: 'test-marketing-url',
upgradeUrl: 'test-upgrade-url',
progressUrl: 'test-progress-url',
resumeUrl: 'test-resume-url',
@@ -181,10 +180,9 @@ describe('courseCard selectors module', () => {
it('passes minPassingGrade floored from float to a percentage value', () => {
expect(selected.minPassingGrade).toEqual(93);
});
it('passes [homeUrl, marketingUrl, upgradeUrl]', () => {
it('passes [homeUrl, marketingUrl]', () => {
expect(selected.homeUrl).toEqual(testData.homeUrl);
expect(selected.marketingUrl).toEqual(testData.marketingUrl);
expect(selected.upgradeUrl).toEqual(testData.upgradeUrl);
});
it('passes [progressUrl, unenrollUrl, resumeUrl], converted to baseAppUrl', () => {
expect(selected.progressUrl).toEqual(baseAppUrl(testData.progressUrl));

View File

@@ -36,12 +36,6 @@ describe('app simple selectors', () => {
expect(preSelectors).toEqual([appSelector]);
expect(cb(testState.app)).toEqual(testString);
});
test('enterpriseDashboard returns empty object if data returns null', () => {
testState = { app: { enterpriseDashboard: null } };
const { preSelectors, cb } = simpleSelectors.enterpriseDashboard;
expect(preSelectors).toEqual([appSelector]);
expect(cb(testState.app)).toEqual({});
});
describe('cardSimpleSelectors', () => {
keys = keyStore(cardSimpleSelectors);
test.each([

View File

@@ -18,7 +18,6 @@ export const useSocialShareSettings = () => useSelector(selectors.socialShareSet
/** global-level meta-selectors **/
export const useHasCourses = () => useSelector(selectors.hasCourses);
export const useHasAvailableDashboards = () => useSelector(selectors.hasAvailableDashboards);
export const useCurrentCourseList = (opts) => useSelector(
state => selectors.currentList(state, opts),
);

View File

@@ -50,12 +50,6 @@ export const logEvent = ({ eventName, data, courseId }) => post(urls.event(), {
event: JSON.stringify(data),
});
export const logUpgrade = ({ courseId }) => module.logEvent({
eventName: eventNames.upgradeButtonClickedEnrollment,
courseId,
data: { location: 'learner-dashboard' },
});
export const logShare = ({ courseId, site }) => module.logEvent({
eventName: eventNames.shareClicked,
courseId,
@@ -78,7 +72,6 @@ export default {
updateEntitlementEnrollment,
deleteEntitlementEnrollment,
logEvent,
logUpgrade,
logShare,
createCreditRequest,
};

View File

@@ -130,13 +130,6 @@ describe('lms api methods', () => {
beforeEach(() => {
jest.spyOn(api, moduleKeys.logEvent).mockImplementation(logEvent);
});
test('logUpgrade sends enrollment upgrade click event with learner dashboard location', () => {
expect(api.logUpgrade({ courseId })).toEqual(logEvent({
eventName: eventNames.upgradeButtonClickedEnrollment,
courseId,
data: { location: 'learner-dashboard' },
}));
});
test('logShare sends share clicke vent with course id, side and location', () => {
const site = 'test-site';
expect(api.logShare({ courseId, site })).toEqual(logEvent({

View File

@@ -779,9 +779,6 @@ export const compileCourseRunData = ({ courseName, ...data }, index) => {
courseProvider: getOption(providerOptions, index),
programs: getOption(programsOptions, index),
};
if (out.enrollment.canUpgrade) {
out.courseRun.upgradeUrl = 'test-upgrade-url';
}
return out;
};

View File

@@ -2,8 +2,8 @@
import 'core-js/stable';
import 'regenerator-runtime/runtime';
import React from 'react';
import ReactDOM from 'react-dom';
import React, { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import {
Route, Navigate, Routes,
} from 'react-router-dom';
@@ -30,23 +30,29 @@ import App from './App';
import NoticesWrapper from './components/NoticesWrapper';
subscribe(APP_READY, () => {
ReactDOM.render(
<AppProvider store={store}>
<NoticesWrapper>
<Routes>
<Route path="/" element={<PageWrap><App /></PageWrap>} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</NoticesWrapper>
</AppProvider>,
document.getElementById('root'),
const root = createRoot(document.getElementById('root'));
root.render(
<StrictMode>
<AppProvider store={store}>
<NoticesWrapper>
<Routes>
<Route path="/" element={<PageWrap><App /></PageWrap>} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</NoticesWrapper>
</AppProvider>
</StrictMode>,
);
});
subscribe(APP_INIT_ERROR, (error) => {
ReactDOM.render(
<ErrorPage message={error.message} />,
document.getElementById('root'),
const root = createRoot(document.getElementById('root'));
root.render(
<StrictMode>
<ErrorPage message={error.message} />
</StrictMode>,
);
});

View File

@@ -1,5 +1,3 @@
import { render } from 'react-dom';
import {
APP_INIT_ERROR,
APP_READY,
@@ -11,9 +9,20 @@ import {
import { configuration } from './config';
import * as app from '.';
jest.mock('react-dom', () => ({
render: jest.fn(),
}));
// These need to be var not let so they get hoisted
// and can be used by jest.mock (which is also hoisted)
var mockRender; // eslint-disable-line no-var
var mockCreateRoot; // eslint-disable-line no-var
jest.mock('react-dom/client', () => {
mockRender = jest.fn();
mockCreateRoot = jest.fn(() => ({
render: mockRender,
}));
return ({
createRoot: mockCreateRoot,
});
});
jest.mock('@edx/frontend-platform', () => ({
mergeConfig: jest.fn(),
@@ -32,7 +41,9 @@ describe('app registry', () => {
let getElement;
beforeEach(() => {
render.mockClear();
mockCreateRoot.mockClear();
mockRender.mockClear();
getElement = window.document.getElementById;
window.document.getElementById = jest.fn(id => ({ id }));
});
@@ -44,18 +55,16 @@ describe('app registry', () => {
const callArgs = subscribe.mock.calls[0];
expect(callArgs[0]).toEqual(APP_READY);
callArgs[1]();
const [rendered, target] = render.mock.calls[0];
const [rendered] = mockRender.mock.calls[0];
expect(rendered).toMatchSnapshot();
expect(target).toEqual(document.getElementById('root'));
});
test('subscribe: APP_INIT_ERROR. snapshot: displays an ErrorPage to root element', () => {
const callArgs = subscribe.mock.calls[1];
expect(callArgs[0]).toEqual(APP_INIT_ERROR);
const error = { message: 'test-error-message' };
callArgs[1](error);
const [rendered, target] = render.mock.calls[0];
const [rendered] = mockRender.mock.calls[0];
expect(rendered).toMatchSnapshot();
expect(target).toEqual(document.getElementById('root'));
});
test('initialize is called with requireAuthenticatedUser', () => {
expect(initialize).toHaveBeenCalledTimes(1);

View File

@@ -0,0 +1,48 @@
# Course Card Action Slot
### Slot ID: `org.openedx.frontend.learner_dashboard.course_card_banner.v1`
### Props:
* `cardId`
## Description
This slot is used for replacing or adding content for the `CourseBanner` component. This banner is rendered as a child of the `CourseCard`.
The default CourseBanner looks like this when audit access has expired for the course:
![Screenshot of the default CourseBanner when audit access has expired](./images/course_banner_slot_default.png)
## Example
The following `env.config.jsx` will render a custom implemenation of a CourseBanner under every `CourseCard`.
![Screenshot of custom banner added under CourseCard](./images/course_banner_slot_default.png)
```js
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
import { Alert } from '@openedx/paragon';
const config = {
pluginSlots: {
'org.openedx.frontend.learner_dashboard.course_card_banner.v1': {
keepDefault: false,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'org.openedx.frontend.learner_dashboard.course_card_banner.v1',
type: DIRECT_PLUGIN,
priority: 60,
RenderWidget: ({ cardId }) => (
<Alert variant="info" className="mb-0">
Course banner for course with {cardId}
</Alert>
),
},
},
],
},
},
}
export default config;
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

View File

@@ -0,0 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import CourseBanner from 'containers/CourseCard/components/CourseCardBanners/CourseBanner';
const CourseBannerSlot = ({ cardId }) => (
<PluginSlot
id="org.openedx.frontend.learner_dashboard.course_card_banner.v1"
pluginProps={{
cardId,
}}
>
<CourseBanner
cardId={cardId}
/>
</PluginSlot>
);
CourseBannerSlot.propTypes = {
cardId: PropTypes.string.isRequired,
};
export default CourseBannerSlot;

View File

@@ -1,6 +1,10 @@
# Course Card Action Slot
### Slot ID: `course_card_action_slot`
### Slot ID: `org.openedx.frontend.learner_dashboard.course_card_action.v1`
### Slot ID Aliases
* `course_card_action_slot`
### Props:
* `cardId`
@@ -20,7 +24,7 @@ import ActionButton from 'containers/CourseCard/components/CourseCardActions/Act
const config = {
pluginSlots: {
course_card_action_slot: {
'org.openedx.frontend.learner_dashboard.course_card_action.v1': {
keepDefault: false,
plugins: [
{

View File

@@ -2,27 +2,15 @@ import React from 'react';
import PropTypes from 'prop-types';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import { reduxHooks } from 'hooks';
import UpgradeButton from 'containers/CourseCard/components/CourseCardActions/UpgradeButton';
const CourseCardActionSlot = ({ cardId }) => {
const { isEntitlement } = reduxHooks.useCardEntitlementData(cardId);
const {
isVerified,
isExecEd2UCourse,
} = reduxHooks.useCardEnrollmentData(cardId);
return (
<PluginSlot
id="course_card_action_slot"
pluginProps={{
cardId,
}}
>
{!(isEntitlement || isVerified || isExecEd2UCourse) && <UpgradeButton cardId={cardId} />}
</PluginSlot>
);
};
const CourseCardActionSlot = ({ cardId }) => (
<PluginSlot
id="org.openedx.frontend.learner_dashboard.course_card_action.v1"
idAliases={['course_card_action_slot']}
pluginProps={{
cardId,
}}
/>
);
CourseCardActionSlot.propTypes = {
cardId: PropTypes.string.isRequired,

View File

@@ -1,6 +1,9 @@
# Course List Slot
### Slot ID: `course_list_slot`
### Slot ID: `org.openedx.frontend.learner_dashboard.course_list.v1`
### Slot ID Aliases
* `course_list_slot`
## Plugin Props
@@ -25,7 +28,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
const config = {
pluginSlots: {
course_list_slot: {
'org.openedx.frontend.learner_dashboard.course_list.v1': {
// Hide the default CourseList component
keepDefault: false,
plugins: [

View File

@@ -4,7 +4,11 @@ import { PluginSlot } from '@openedx/frontend-plugin-framework';
import { CourseList, courseListDataShape } from 'containers/CoursesPanel/CourseList';
export const CourseListSlot = ({ courseListData }) => (
<PluginSlot id="course_list_slot" pluginProps={{ courseListData }}>
<PluginSlot
id="org.openedx.frontend.learner_dashboard.course_list.v1"
idAliases={['course_list_slot']}
pluginProps={{ courseListData }}
>
<CourseList courseListData={courseListData} />
</PluginSlot>
);

View File

@@ -0,0 +1,41 @@
# Dashboard Modal Slot
### Slot ID: `org.openedx.frontend.learner_dashboard.dashboard_modal.v1`
### https://github.com/openedx/frontend-plugin-framework/blob/master/docs/decisions/0003-slot-naming-and-life-cycle.rst#1-naming-format
## Description
This slot is used for the modal on a dashboard.
The following `env.config.jsx` will render the modal.
## Example
Learner dashboard will show modal
![Screenshot of the dashboard modal](./images/dashboard_modal_slot.png)
```js
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
import { ModalDialog } from '@openedx/paragon';
const config = {
pluginSlots: {
org.openedx.frontend.learner_dashboard.dashboard_modal.v1: {
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'dashboard_modal',
type: DIRECT_PLUGIN,
priority: 60,
RenderWidget:
<ModalDialog title="Modal that appears on learner dashboard" />,
},
},
],
}
},
}
export default config;
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

View File

@@ -0,0 +1,8 @@
import React from 'react';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
const DashboardModalSlot = () => (
<PluginSlot id="org.openedx.frontend.learner_dashboard.dashboard_modal.v1" />
);
export default DashboardModalSlot;

View File

@@ -1,12 +1,15 @@
# Footer Slot
### Slot ID: `footer_slot`
### Slot ID: `org.openedx.frontend.layout.footer.v1`
### Slot ID Aliases
* `footer_slot`
## Description
This slot is used to replace/modify/hide the footer.
The implementation of the `FooterSlot` component lives in [the `frontend-slot-footer` repository](https://github.com/openedx/frontend-slot-footer/).
The implementation of the `FooterSlot` component lives in [the `frontend-component-footer` repository](https://github.com/openedx/frontend-component-footer/).
## Example
@@ -23,7 +26,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
const config = {
pluginSlots: {
footer_slot: {
'org.openedx.frontend.layout.footer.v1': {
plugins: [
{
// Hide the default footer

View File

@@ -1,6 +1,9 @@
# No Courses View Slot
### Slot ID: `no_courses_view_slot`
### Slot ID: `org.openedx.frontend.learner_dashboard.no_courses_view.v1`
### Slot ID Aliases
* `no_courses_view_slot`
## Description
@@ -21,7 +24,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
const config = {
pluginSlots: {
no_courses_view_slot: {
'org.openedx.frontend.learner_dashboard.no_courses_view.v1': {
// Hide the default NoCoursesView component
keepDefault: false,
plugins: [

View File

@@ -4,7 +4,10 @@ import { PluginSlot } from '@openedx/frontend-plugin-framework';
import NoCoursesView from 'containers/CoursesPanel/NoCoursesView';
export const NoCoursesViewSlot = () => (
<PluginSlot id="no_courses_view_slot">
<PluginSlot
id="org.openedx.frontend.learner_dashboard.no_courses_view.v1"
idAliases={['no_courses_view_slot']}
>
<NoCoursesView />
</PluginSlot>
);

View File

@@ -1,7 +1,8 @@
# `frontend-app-learner-dashboard` Plugin Slots
* [`course_card_action_slot`](./CourseCardActionSlot/)
* [`footer_slot`](./FooterSlot/)
* [`widget_sidebar_slot`](./WidgetSidebarSlot/)
* [`course_list_slot`](./CourseListSlot/)
* [`no_courses_view_slot`](./NoCoursesViewSlot/)
* [`org.openedx.frontend.learner_dashboard.course_card_action.v1`](./CourseCardActionSlot/)
* [`org.openedx.frontend.layout.footer.v1`](./FooterSlot/)
* [`org.openedx.frontend.learner_dashboard.widget_sidebar.v1`](./WidgetSidebarSlot/)
* [`org.openedx.frontend.learner_dashboard.course_list.v1`](./CourseListSlot/)
* [`org.openedx.frontend.learner_dashboard.no_courses_view.v1`](./NoCoursesViewSlot/)
* [`org.openedx.frontend.learner_dashboard.dashboard_modal.v1`](./DashboardModalSlot)

View File

@@ -1,6 +1,9 @@
# Widget Sidebar Slot
### Slot ID: `widget_sidebar_slot`
### Slot ID: `org.openedx.frontend.learner_dashboard.widget_sidebar.v1`
### Slot ID Aliases
* `widget_sidebar_slot`
## Description
@@ -21,7 +24,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
const config = {
pluginSlots: {
widget_sidebar_slot: {
'org.openedx.frontend.learner_dashboard.widget_sidebar.v1': {
// Hide the default LookingForChallenge component
keepDefault: false,
plugins: [

View File

@@ -2,7 +2,12 @@
exports[`WidgetSidebar snapshots 1`] = `
<PluginSlot
id="widget_sidebar_slot"
id="org.openedx.frontend.learner_dashboard.widget_sidebar.v1"
idAliases={
[
"widget_sidebar_slot",
]
}
>
<LookingForChallengeWidget />
</PluginSlot>

View File

@@ -5,7 +5,10 @@ import LookingForChallengeWidget from 'widgets/LookingForChallengeWidget';
// eslint-disable-next-line arrow-body-style
export const WidgetSidebarSlot = () => (
<PluginSlot id="widget_sidebar_slot">
<PluginSlot
id="org.openedx.frontend.learner_dashboard.widget_sidebar.v1"
idAliases={['widget_sidebar_slot']}
>
<LookingForChallengeWidget />
</PluginSlot>
);

View File

@@ -1,6 +1,5 @@
/* eslint-disable import/no-extraneous-dependencies */
import '@testing-library/jest-dom';
import '@testing-library/jest-dom/extend-expect';
jest.mock('react', () => ({
...jest.requireActual('react'),

View File

@@ -9,7 +9,6 @@ import {
within,
prettyDOM,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import {
initialize,
mergeConfig,

View File

@@ -2,7 +2,6 @@ import { StrictDict } from 'utils';
export const categories = StrictDict({
dashboard: 'dashboard',
upgrade: 'upgrade',
userEngagement: 'user-engagement',
searchButton: 'search_button',
credit: 'credit',
@@ -14,9 +13,6 @@ export const events = StrictDict({
courseImageClicked: 'courseImageClicked',
courseTitleClicked: 'courseTitleClicked',
courseOptionsDropdownClicked: 'courseOptionsDropdownClicked',
upgradeButtonClicked: 'upgradeButtonClicked',
upgradeButtonClickedEnrollment: 'upgradeButtonClickedEnrollment',
upgradeButtonClickedUpsell: 'upgradeButtonClickedUpsell',
shareClicked: 'shareClicked',
userSettingsChanged: 'userSettingsChanged',
newSession: 'newSession',
@@ -24,21 +20,13 @@ export const events = StrictDict({
leaveSession: 'leaveSession',
unenrollReason: 'unenrollReason',
entitlementUnenrollReason: 'entitlementUnenrollReason',
enterpriseDashboardModalOpened: 'enterpriseDashboardModalOpened',
enterpriseDashboardModalCTAClicked: 'enterpriseDashboardModalCTAClicked',
enterpriseDashboardModalClosed: 'enterpriseDashboardModalClosed',
});
const learnerPortal = 'edx.ui.enterprise.lms.dashboard.learner_portal_modal';
export const eventNames = StrictDict({
enterCourseClicked: 'edx.bi.dashboard.enter_course.clicked',
courseImageClicked: 'edx.bi.dashboard.course_image.clicked',
courseTitleClicked: 'edx.bi.dashboard.course_title.clicked',
courseOptionsDropdownClicked: 'edx.bi.dashboard.course_options_dropdown.clicked',
upgradeButtonClicked: 'edx.bi.dashboard.upgrade_button.clicked',
upgradeButtonClickedEnrollment: 'edx.course.enrollment.upgrade.clicked',
upgradeButtonClickedUpsell: 'edx.bi.ecommerce.upsell_links_clicked',
shareClicked: 'edx.course.share_clicked',
userSettingsChanged: 'edx.user.settings.changed',
newSession: 'course-dashboard.new-session',
@@ -46,9 +34,6 @@ export const eventNames = StrictDict({
leaveSession: 'course-dashboard.leave-session',
unenrollReason: 'unenrollment_reason.selected',
entitlementUnenrollReason: 'entitlement_unenrollment_reason.selected',
enterpriseDashboardModalOpened: `${learnerPortal}.opened`,
enterpriseDashboardModalCTAClicked: `${learnerPortal}.dashboard_cta.clicked`,
enterpriseDashboardModalClosed: `${learnerPortal}.closed`,
findCoursesClicked: 'edx.bi.dashboard.find_courses_button.clicked',
purchaseCredit: 'edx.bi.credit.clicked_purchase_credit',
filterClicked: 'course-dashboard.filter.clicked',

View File

@@ -1,7 +1,6 @@
import course from './trackers/course';
import credit from './trackers/credit';
import engagement from './trackers/engagement';
import enterpriseDashboard from './trackers/enterpriseDashboard';
import entitlements from './trackers/entitlements';
import socialShare from './trackers/socialShare';
import findCourses from './trackers/findCourses';
@@ -11,7 +10,6 @@ export default {
course,
credit,
engagement,
enterpriseDashboard,
entitlements,
socialShare,
findCourses,

View File

@@ -1,15 +1,7 @@
import api from 'data/services/lms/api';
import { createEventTracker, createLinkTracker } from 'data/services/segment/utils';
import { categories, eventNames } from '../constants';
import * as module from './course';
export const upsellOptions = {
linkName: 'course_dashboard_green',
linkType: 'button',
pageName: 'course_dashboard',
linkCategory: 'green_update',
};
// Utils/Helpers
/**
* Generate a segement event tracker for a given course event.
@@ -31,20 +23,6 @@ export const courseLinkTracker = (eventName) => (courseId, href) => (
createLinkTracker(module.courseEventTracker(eventName, courseId), href)
);
// Upgrade Events
/**
* There are currently multiple tracked api events for the upgrade event, with different targets.
* Goal here is to split out the tracked events for easier testing.
*/
export const upgradeButtonClicked = (courseId) => createEventTracker(
eventNames.upgradeButtonClicked,
{ category: categories.upgrade, label: courseId },
);
export const upgradeButtonClickedUpsell = (courseId) => createEventTracker(
eventNames.upgradeButtonClickedUpsell,
{ ...upsellOptions, courseId },
);
// Non-Link events
export const courseOptionsDropdownClicked = (courseId) => (
module.courseEventTracker(eventNames.courseOptionsDropdownClicked, courseId)
@@ -57,19 +35,10 @@ export const courseTitleClicked = (...args) => (
module.courseLinkTracker(eventNames.courseTitleClicked)(...args));
export const enterCourseClicked = (...args) => (
module.courseLinkTracker(eventNames.enterCourseClicked)(...args));
export const upgradeClicked = (courseId, href) => createLinkTracker(
() => {
module.upgradeButtonClicked(courseId)();
module.upgradeButtonClickedUpsell(courseId)();
api.logUpgrade({ courseId });
},
href,
);
export default {
courseImageClicked,
courseOptionsDropdownClicked,
courseTitleClicked,
enterCourseClicked,
upgradeClicked,
};

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