Compare commits

...

121 Commits

Author SHA1 Message Date
Adolfo R. Brandes
021f52b303 feat: remove upgrade button
This removes the upgrade button as default content in CourseCardActionSlot.

This is, for the moment, a Sumac-specific change.  The button is still
present in master, pending proper removal (including the component
definitions from the rest of the repository) prior to the U release by
the maintainers.  See:

https://github.com/openedx/frontend-app-learner-dashboard/issues/438
2024-12-06 15:55:31 -03:00
Adolfo R. Brandes
8e0e217402 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:23 -03:00
Diana Olarte
261448dee9 fix: display programs tab only if it is configured
fix: apply feedback
2024-12-02 07:50:28 -08:00
Diana Olarte
5ef5ed954c fix: display SUPPORT_URL only if the url is configured 2024-12-02 07:50:28 -08:00
Brian Smith
08d47dd9f1 feat(deps): update header to 5.6.0 (#485) 2024-10-22 19:19:30 -04:00
Jason Wesson
f250efb660 docs: improve the image example for course card slot 2024-10-21 13:01:43 -06:00
Jason Wesson
c144c04aee fix: modify tests for course card and import paths 2024-10-21 13:01:43 -06:00
Jason Wesson
0a52025a99 feat: add plugin slot for course card action 2024-10-21 13:01:43 -06:00
renovate[bot]
e4e02d4da2 fix(deps): update dependency @openedx/paragon to v22.9.0 2024-10-21 04:16:50 +00:00
Maxwell Frank
0408a54372 refactor: plugin slot implementation (#465) 2024-10-15 15:04:52 -04:00
renovate[bot]
134c741cf8 fix(deps): update dependency react-router-dom to v6.27.0 2024-10-14 11:06:00 +00:00
renovate[bot]
756e85f046 fix(deps): update dependency react-intl to v6.8.0 2024-10-14 06:53:38 +00:00
renovate[bot]
8b532aa49a fix(deps): update dependency @edx/frontend-platform to v8.1.2 2024-10-14 04:09:09 +00:00
renovate[bot]
cc544e4591 fix(deps): update dependency @openedx/paragon to v22.8.1 2024-10-07 09:26:22 +00:00
renovate[bot]
1bd6f71ac1 fix(deps): update dependency @edx/frontend-component-header to v5.5.0 2024-10-07 06:22:31 +00:00
renovate[bot]
8914c7f4cc fix(deps): update dependency dompurify to v2.5.7 2024-09-30 10:40:04 +00:00
renovate[bot]
636216c5d3 chore(deps): update dependency @openedx/frontend-build to v14.1.5 2024-09-30 06:25:59 +00:00
renovate[bot]
a174abbc09 fix(deps): update dependency react-router-dom to v6.26.2 2024-09-23 08:10:12 +00:00
renovate[bot]
5134f8f85b chore(deps): update dependency @openedx/frontend-build to v14.1.4 2024-09-23 05:01:59 +00:00
edX requirements bot
1007dc40fb chore: enable github action auto update in dependabot.yml (#457)
Co-authored-by: Maxwell Frank <92897870+MaxFrank13@users.noreply.github.com>
2024-09-17 09:13:17 -04:00
renovate[bot]
767596301a chore(deps): update dependency react-dev-utils to v12 (#443)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-16 13:38:42 -04:00
renovate[bot]
d76d13bcc2 fix(deps): update dependency @openedx/frontend-slot-footer to v1.0.5 2024-09-16 08:10:12 +00:00
renovate[bot]
bd495e98ee chore(deps): update dependency husky to v9.1.6 2024-09-16 04:43:31 +00:00
Jason Wesson
2f8ff3b517 test: update jest snapshots 2024-09-13 08:46:23 -07:00
Jason Wesson
629de04289 feat: remove Zendesk component from App 2024-09-13 08:46:23 -07:00
Justin Hynes
b4b3d0718d build: Switch to ubuntu-latest for builds (#454) 2024-09-09 14:29:47 -04:00
Feanil Patel
ed7a3ffdbc build: Switch to ubuntu-latest for builds
This code does not have any dependencies that are specific to any specific
version of ubuntu.  So instead of testing on a specific version and then needing
to do work to keep the versions up-to-date, we switch to the ubuntu-latest
target which should be sufficient for testing purposes.

This work is being done as a part of https://github.com/openedx/platform-roadmap/issues/377

closes https://github.com/openedx/frontend-app-learner-dashboard/issues/420
2024-09-09 10:00:38 -04:00
renovate[bot]
0cfebb6976 fix(deps): update dependency @openedx/frontend-plugin-framework to v1.3.0 2024-09-09 06:47:17 +00:00
renovate[bot]
48e2c72180 chore(deps): update dependency @openedx/frontend-build to v14.1.2 2024-09-09 05:07:35 +00:00
renovate[bot]
3ce54cfc4a fix(deps): update dependency core-js to v3.38.1 2024-09-02 06:50:10 +00:00
renovate[bot]
8969d011ff chore(deps): update dependency @openedx/frontend-build to v14.1.1 2024-09-02 04:21:13 +00:00
Deborah Kaplan
8fd6f2c7dc chore: removing an unnecessary import (#440) 2024-08-29 09:00:08 -04:00
Deborah Kaplan
a2041bfc11 chore: removing an unnecessary import
removing an unnecessary import

FIXES: APER-3600
2024-08-28 21:38:55 +00:00
Maxwell Frank
f836239ddb feat!: Remove RecommendationsPanel (#437) 2024-08-28 09:35:19 -04:00
renovate[bot]
00129bcee0 fix(deps): update dependency @openedx/frontend-slot-footer to v1.0.4 2024-08-26 08:48:16 +00:00
renovate[bot]
c714abd656 fix(deps): update dependency @edx/openedx-atlas to v0.6.2 2024-08-26 04:27:55 +00:00
Bilal Qamar
e6baa0787c build: Upgrade to Node 20 (#396) 2024-08-23 10:27:17 -04:00
Bilal Qamar
036e798637 test: Add Node 20 to CI matrix (#431)
Co-authored-by: Maxwell Frank <92897870+MaxFrank13@users.noreply.github.com>
2024-08-22 14:33:39 -04:00
renovate[bot]
db25a6c7e9 chore(deps): update dependency husky to v9 (#425)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-21 13:18:14 -04:00
Maxwell Frank
2d091895a8 chore: update merge config (#428) 2024-08-19 14:10:02 -04:00
renovate[bot]
5ea7c6cc0c chore(deps): update dependency @openedx/frontend-build to v14.1.0 2024-08-19 06:51:43 +00:00
renovate[bot]
72aa81f8dc fix(deps): update dependency react-router-dom to v6.26.1 2024-08-19 05:16:37 +00:00
Justin Hynes
afe386566b chore: remove unused dependency -- axios-mock-adapter (#422) 2024-08-15 09:51:09 -04:00
Justin Hynes
d5da8ba62f chore: remove unused dependency -- axios-mock-adapter
Similarly to #421, the `axios-mock-adapter` dependency is not being used. Renovate is trying to update this from v1.x to 2.x, this PR removes the dependency instead.
2024-08-14 20:17:39 +00:00
Justin Hynes
24a3a2de65 chore: remove unused dependency (#421) 2024-08-14 16:13:49 -04:00
Justin Hynes
40f8b0e960 chore: remove unused dependency
Renovate opened a PR to update `fetch-mock` to v11.x from v9.x, when I went to see where it was used... the only reference I found to this package was in the package.json file. It seems like it's not used anymore.
2024-08-14 20:06:47 +00:00
Justin Hynes
d1813d3dcd chore(deps): update dependency copy-webpack-plugin to v12 (#408) 2024-08-14 15:08:49 -04:00
renovate[bot]
d5b02fbbb0 chore(deps): update dependency copy-webpack-plugin to v12 2024-08-14 18:00:28 +00:00
Justin Hynes
b3620a7832 chore(deps): update actions/setup-node action to v4 (#407) 2024-08-14 13:59:17 -04:00
renovate[bot]
5829e25fed chore(deps): update actions/setup-node action to v4 2024-08-14 17:48:07 +00:00
Justin Hynes
aa041245af chore(deps): update actions/checkout action to v4 (#406) 2024-08-14 13:40:06 -04:00
renovate[bot]
17aa646856 chore(deps): update actions/checkout action to v4 2024-08-12 06:56:29 +00:00
Ahtesham Quraish
7ef5d5b034 fix: replace the header with openedx header
Description:
Replace the header with openedx header
van-1914
2024-08-12 11:55:44 +05:00
renovate[bot]
68a46ac023 fix(deps): update dependency @openedx/frontend-plugin-framework to v1.2.3 2024-08-12 06:40:36 +00:00
renovate[bot]
6310cc0452 chore(deps): update dependency @openedx/frontend-build to v14.0.15 2024-08-12 04:12:33 +00:00
renovate[bot]
9f52c61e4f fix(deps): update dependency react-router-dom to v6.26.0 2024-08-05 07:24:44 +00:00
renovate[bot]
6a62301c1c fix(deps): update dependency core-js to v3.38.0 2024-08-05 06:58:40 +00:00
renovate[bot]
e61eaa8264 fix(deps): update dependency @redux-devtools/extension to v3.3.0 2024-08-05 04:46:33 +00:00
Deborah Kaplan
c906ce0d3a fix(deps): replace dependency redux-devtools-extension with @redux-devtools/extension 3.0.0 (#314) 2024-07-30 14:58:34 -04:00
Deborah Kaplan
e9a1e3e40d chore: fixing jest
making mocked method match
2024-07-30 17:56:58 +00:00
Deborah Kaplan
f149f2c8cf chore: packkage and method name change
this renovate update changed the package. See:
https://github.com/reduxjs/redux-devtools/releases/tag/remotedev-redux-devtools-extension%403.0.0
2024-07-30 17:30:29 +00:00
renovate[bot]
3ee7c62119 fix(deps): replace dependency redux-devtools-extension with @redux-devtools/extension 3.0.0 2024-07-30 14:09:41 +00:00
Maxwell Frank
a5d1cb380d fix: remove Optimizely and surrounding components (#386) 2024-07-30 10:09:04 -04:00
renovate[bot]
e3b4e0956a fix(deps): update dependency regenerator-runtime to ^0.14.0 2024-07-29 13:48:49 +00:00
renovate[bot]
eb427f3772 fix(deps): update dependency redux-thunk to v2.4.2 2024-07-29 11:05:55 +00:00
renovate[bot]
a53c167558 fix(deps): update dependency @edx/frontend-platform to v8.1.1 2024-07-29 07:43:07 +00:00
renovate[bot]
61476422bb chore(deps): update dependency @openedx/frontend-build to v14.0.14 2024-07-29 04:30:28 +00:00
renovate[bot]
5b6c4004c7 fix(deps): update dependency redux to v4.2.1 2024-07-22 13:49:47 +00:00
renovate[bot]
d7bd32aae3 fix(deps): update dependency react-router-dom to v6.25.1 2024-07-22 12:00:05 +00:00
renovate[bot]
c14496ade9 fix(deps): update dependency react-intl to v6.6.8 2024-07-22 07:13:24 +00:00
renovate[bot]
4b117c7882 fix(deps): update dependency @openedx/paragon to v22.7.0 2024-07-22 04:58:26 +00:00
Deborah Kaplan
0162a62c56 feat: switching to 4 letter yaml extension (#388) 2024-07-17 08:46:03 -04:00
Deborah Kaplan
79ad701eca feat: switching to 4 letter yaml extension
it looks like the  processes which rely on catalog-info require the 4
letter extension.
2024-07-16 20:47:14 +00:00
renovate[bot]
3150c110a1 fix(deps): update dependency query-string to v7.1.3 2024-07-15 14:22:57 +00:00
renovate[bot]
9c3264c8a2 fix(deps): update dependency history to v5.3.0 2024-07-15 10:19:25 +00:00
renovate[bot]
95a35e17dc fix(deps): update dependency @openedx/paragon to v22.6.1 2024-07-15 06:15:11 +00:00
renovate[bot]
aa7296c3cd fix(deps): update dependency @openedx/frontend-slot-footer to v1.0.3 2024-07-15 04:43:33 +00:00
Jason Wesson
89ae34c874 feat: change course recommendations endpoint (#371) 2024-07-08 11:19:08 -06:00
Jason Wesson
62a9cb0045 Merge branch 'master' into jwesson/update-course-recommendations-endpoint 2024-07-08 11:15:28 -06:00
renovate[bot]
a96c8fc6ab fix(deps): update dependency @edx/frontend-platform to v8.1.0 2024-07-08 14:35:17 +00:00
renovate[bot]
c0ad27077f fix(deps): update dependency prop-types to v15.8.1 2024-07-08 11:10:15 +00:00
renovate[bot]
08ead35644 fix(deps): update dependency dompurify to v2.5.6 2024-07-08 06:58:48 +00:00
renovate[bot]
362bb8b3cf fix(deps): update dependency @openedx/frontend-plugin-framework to v1.2.2 2024-07-08 04:38:24 +00:00
Jason Wesson
20eebf2f28 feat: change course recommendations endpoint 2024-07-03 16:47:53 +00:00
renovate[bot]
7e8dad41ec fix(deps): update dependency dompurify to v2.5.5 2024-07-01 11:24:42 +00:00
renovate[bot]
f6af646b80 chore(deps): update dependency @openedx/frontend-build to v14.0.10 2024-07-01 07:43:02 +00:00
Justin Hynes
3b25d04752 perf: version check workflow for lockfile updated (#361) 2024-06-25 16:02:31 -04:00
Justin Hynes
31eafb30ba Merge branch 'master' into huniafatima/lockfile-workflow-update 2024-06-25 15:59:08 -04:00
Deborah Kaplan
1705926f52 feat: changing ownership for the recommendations widget (#366) 2024-06-20 16:16:32 -04:00
Deborah Kaplan
f1128d63d7 feat: changing ownership for the recommendations widget
* changing ownership to aperture for this widget  (which is DEPRd  from
  the open source code base, and aperture is scheduled to remove it
  at some point).

FIXES: APER-3497
2024-06-20 16:17:48 +00:00
Adolfo R. Brandes
c9866af227 build: Update codecov and use token
Update codecov to the latest version and start using the org-wide token for uploads.

See https://github.com/openedx/wg-frontend/issues/179
2024-06-17 12:16:53 -03:00
renovate[bot]
a2ccda7b30 fix(deps): update dependency @openedx/frontend-plugin-framework to v1.2.1 2024-06-17 10:40:26 +00:00
renovate[bot]
54cf4bb8fd chore(deps): update dependency @openedx/frontend-build to v14.0.9 2024-06-17 06:48:46 +00:00
Hunia Fatima
229436cddf perf: version check workflow for lockfile updated 2024-06-12 17:28:00 +05:00
Adolfo R. Brandes
e94dd56fb1 fix: Remove edX-specific reference
Remove edx-Specific reference from email confirmation banner.
2024-06-10 11:34:48 -03:00
renovate[bot]
3becef3468 fix(deps): update dependency @edx/openedx-atlas to v0.6.1 2024-06-10 10:22:25 +00:00
renovate[bot]
3570ead725 fix(deps): update dependency @edx/frontend-platform to v8.0.4 2024-06-10 07:19:45 +00:00
Bilal Qamar
17c5fd09f9 feat: updated frontend-build & frontend-platform major versions (#256) 2024-05-30 17:14:41 +05:00
Bilal Qamar
506f8f795f Merge branch master into bilalqamar95/jest-v29-upgrade 2024-05-30 17:09:44 +05:00
renovate[bot]
e73880b442 fix(deps): update dependency @fortawesome/react-fontawesome to v0.2.2 2024-05-27 04:46:08 +00:00
Bilal Qamar
08050f458e Merge branch 'master' into bilalqamar95/jest-v29-upgrade 2024-05-20 16:30:16 +05:00
renovate[bot]
75f19a28b7 fix(deps): update dependency core-js to v3.37.1 2024-05-20 11:12:50 +00:00
Bilal Qamar
ecc4c4c2e0 refactor: update package-lock 2024-05-20 16:08:06 +05:00
Bilal Qamar
28da100ca2 Merge branch master into bilalqamar95/jest-v29-upgrade 2024-05-20 16:07:50 +05:00
renovate[bot]
0fbac0715d fix(deps): update dependency @fortawesome/react-fontawesome to v0.2.1 2024-05-20 07:01:59 +00:00
Adolfo R. Brandes
49f9e6e424 feat: use frontend-plugin-framework to provide a FooterSlot (#345) 2024-05-17 13:05:30 -03:00
Brian Smith
17eff1da7b feat: use frontend-plugin-framework to provide a FooterSlot 2024-05-17 10:54:53 -04:00
Muhammad Abdullah Waheed
0fcaa64a6e fix: fixed snapshots 2024-05-17 16:54:08 +05:00
Bilal Qamar
506d3f655c refactor: updated version for frontend-plugin-framework 2024-05-17 16:35:59 +05:00
Bilal Qamar
37bb54d28e refactor: updated package-lock 2024-05-16 16:42:33 +05:00
Bilal Qamar
1d53ef6153 refactor: react-unit-test-util & hotjar major version upgrades, updated snapshots 2024-05-16 16:19:50 +05:00
Bilal Qamar
59d90f5dc8 Merge branch master into bilalqamar95/jest-v29-upgrade 2024-05-16 16:19:13 +05:00
renovate[bot]
61d881b1cd fix(deps): update dependency core-js to v3.37.0 2024-05-13 09:08:02 +00:00
renovate[bot]
77adb60167 fix(deps): update dependency classnames to v2.5.1 2024-05-13 06:08:29 +00:00
renovate[bot]
b2b199e0e2 fix(deps): update dependency @openedx/paragon to v22.4.0 2024-05-13 04:07:20 +00:00
Bilal Qamar
92f712d670 feat: updated build and platform major versions, along with edx packages 2024-04-24 14:10:11 +05:00
Bilal Qamar
11b7e48080 refactor: updated snapshots for failing tests 2024-04-04 16:33:13 +05:00
Bilal Qamar
a051d712dc refactor: updated frontend-build 2024-04-04 16:32:59 +05:00
Bilal Qamar
8c76b5c689 Merge branch 'master' into bilalqamar95/jest-v29-upgrade 2024-04-04 16:28:33 +05:00
Bilal Qamar
7fccd94d6b Merge branch 'master' into bilalqamar95/jest-v29-upgrade 2023-12-22 16:07:09 +05:00
Bilal Qamar
c405bc63ea chore: bumped jest to v29 2023-12-05 16:17:36 +05:00
166 changed files with 6070 additions and 20149 deletions

2
.env
View File

@@ -40,5 +40,5 @@ ACCOUNT_SETTINGS_URL=''
ACCOUNT_PROFILE_URL=''
ENABLE_NOTICES=''
CAREER_LINK_URL=''
OPTIMIZELY_FULL_STACK_SDK_KEY=''
ENABLE_EDX_PERSONAL_DASHBOARD=false
ENABLE_PROGRAMS=false

View File

@@ -8,7 +8,6 @@ LOGOUT_URL='http://localhost:18000/logout'
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
LOGO_POWERED_BY_OPEN_EDX_URL_SVG=https://edx-cdn.org/v3/stage/open-edx-tag.svg
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
@@ -21,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'
@@ -47,5 +46,5 @@ ACCOUNT_SETTINGS_URL='http://localhost:1997'
ACCOUNT_PROFILE_URL='http://localhost:1995'
ENABLE_NOTICES=''
CAREER_LINK_URL=''
OPTIMIZELY_FULL_STACK_SDK_KEY=''
ENABLE_EDX_PERSONAL_DASHBOARD=false
ENABLE_PROGRAMS=false

View File

@@ -8,7 +8,6 @@ LOGOUT_URL='http://localhost:18000/logout'
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
LOGO_POWERED_BY_OPEN_EDX_URL_SVG=https://edx-cdn.org/v3/stage/open-edx-tag.svg
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
@@ -21,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'
@@ -46,5 +45,5 @@ ACCOUNT_SETTINGS_URL='http://account-settings-url.test'
ACCOUNT_PROFILE_URL='http://account-profile-url.test'
ENABLE_NOTICES=''
CAREER_LINK_URL=''
OPTIMIZELY_FULL_STACK_SDK_KEY='SDK Key'
ENABLE_EDX_PERSONAL_DASHBOARD=true
ENABLE_PROGRAMS=false

7
.github/CODEOWNERS vendored
View File

@@ -1,8 +1 @@
# Root app is developed and owned by Aurora
* @openedx/2U-aperture
# WIDGETS and experiments are developed and owned by separate teams below
# Recommendations panel
/src/widgets/RecommendationsPanel @openedx/2U-vanguards
/src/widgets/LookingForChallengeWidget @openedx/2U-vanguards

7
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,7 @@
version: 2
updates:
# Adding new check for github-actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

View File

@@ -10,18 +10,19 @@ on:
jobs:
tests:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
strategy:
matrix:
node: [18, 20]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
uses: actions/checkout@v4
- name: Setup Nodejs
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VER }}
node-version: ${{ matrix.node }}
- name: Install dependencies
run: npm ci
@@ -39,7 +40,10 @@ jobs:
run: npm run build
- name: Run Coverage
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
- name: Send failure notification
if: ${{ failure() }}

View File

@@ -7,10 +7,10 @@ on:
jobs:
release:
name: Release
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -18,7 +18,7 @@ jobs:
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- name: Setup Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VER }}

2
.nvmrc
View File

@@ -1 +1 @@
18.20
20

View File

@@ -30,6 +30,12 @@ To start the MFE and enable the feature in LMS:
From there, simply load the configured address/port. You should be prompted to log into your LMS if you are not
already, and then redirected to your home page.
Plugins
-------
This MFE can be customized using `Frontend Plugin Framework <https://github.com/openedx/frontend-plugin-framework>`_.
The parts of this MFE that can be customized in that manner are documented `here </src/plugin-slots>`_.
Contributing
------------

View File

@@ -29,7 +29,6 @@ module.exports = {
LOGO_URL: 'https://edx-cdn.org/v3/default/logo.svg',
LOGO_TRADEMARK_URL: 'https://edx-cdn.org/v3/default/logo-trademark.svg',
LOGO_WHITE_URL: 'https://edx-cdn.org/v3/default/logo-white.svg',
LOGO_POWERED_BY_OPEN_EDX_URL_SVG: 'https://edx-cdn.org/v3/stage/open-edx-tag.svg',
FAVICON_URL: 'https://edx-cdn.org/v3/default/favicon.ico',
CSRF_TOKEN_API_PATH: '/csrf/api/v1/token',
REFRESH_ACCESS_TOKEN_ENDPOINT: 'http://localhost:18000/login_refresh',
@@ -70,6 +69,5 @@ module.exports = {
ACCOUNT_PROFILE_URL: 'http://localhost:1995',
ENABLE_NOTICES: '',
CAREER_LINK_URL: '',
OPTIMIZELY_FULL_STACK_SDK_KEY: '',
EXPERIMENT_08_23_VAN_PAINTED_DOOR: true,
};

20342
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",
@@ -27,70 +30,68 @@
},
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/browserslist-config": "^1.1.0",
"@edx/frontend-component-footer": "13.1.0",
"@edx/frontend-component-header": "^5.6.0",
"@edx/frontend-enterprise-hotjar": "3.0.0",
"@edx/frontend-platform": "7.1.4",
"@edx/frontend-platform": "8.1.2",
"@edx/openedx-atlas": "^0.6.0",
"@edx/react-unit-test-utils": "2.0.0",
"@edx/react-unit-test-utils": "3.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.0.2",
"@openedx/frontend-plugin-framework": "^1.2.0",
"@openedx/frontend-slot-footer": "^1.0.2",
"@openedx/paragon": "^22.2.2",
"@optimizely/react-sdk": "^2.9.2",
"@redux-beacon/segment": "^1.1.0",
"@redux-devtools/extension": "3.3.0",
"@reduxjs/toolkit": "^1.6.1",
"@testing-library/user-event": "^13.5.0",
"axios": "^0.28.0",
"classnames": "^2.3.1",
"core-js": "3.16.2",
"core-js": "3.38.1",
"dompurify": "^2.3.1",
"email-prop-type": "^3.0.1",
"file-saver": "^2.0.5",
"filesize": "^8.0.6",
"font-awesome": "4.7.0",
"history": "5.0.1",
"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.7.2",
"query-string": "7.0.1",
"prop-types": "15.8.1",
"query-string": "7.1.3",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-helmet": "^6.1.0",
"react-intl": "6.4.7",
"react-intl": "6.8.0",
"react-pdf": "^7.0.0",
"react-redux": "^7.2.4",
"react-router-dom": "6.15.0",
"react-router-dom": "6.27.0",
"react-share": "^4.4.0",
"react-zendesk": "^0.1.13",
"redux": "4.1.1",
"redux": "4.2.1",
"redux-beacon": "^2.1.0",
"redux-devtools-extension": "2.13.9",
"redux-logger": "3.0.6",
"redux-thunk": "2.3.0",
"regenerator-runtime": "^0.13.9",
"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"
},
"devDependencies": {
"@edx/browserslist-config": "^1.3.0",
"@edx/reactifex": "^2.1.1",
"@openedx/frontend-build": "13.1.4",
"@openedx/frontend-build": "14.1.5",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.1.0",
"axios-mock-adapter": "^1.20.0",
"copy-webpack-plugin": "^11.0.0",
"fetch-mock": "^9.11.0",
"husky": "^7.0.0",
"copy-webpack-plugin": "^12.0.0",
"husky": "^9.0.0",
"identity-obj-proxy": "^3.0.0",
"jest-expect-message": "^1.0.2",
"react-dev-utils": "^11.0.4",
"jest-expect-message": "^1.1.3",
"react-dev-utils": "^12.0.0",
"react-test-renderer": "^17.0.2",
"redux-mock-store": "^1.5.4",
"semantic-release": "^20.1.3"

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 Footer from '@edx/frontend-component-footer';
import FooterSlot from '@openedx/frontend-slot-footer';
import { Alert } from '@openedx/paragon';
import { RequestKeys } from 'data/constants/requests';
@@ -17,8 +17,6 @@ import {
} from 'data/redux';
import { reduxHooks } from 'hooks';
import Dashboard from 'containers/Dashboard';
import ZendeskFab from 'components/ZendeskFab';
import { ExperimentProvider } from 'ExperimentContext';
import track from 'tracking';
@@ -42,19 +40,6 @@ export const App = () => {
const { supportEmail } = reduxHooks.usePlatformSettingsData();
const loadData = reduxHooks.useLoadData();
const optimizelyScript = () => {
if (getConfig().OPTIMIZELY_URL) {
return <script src={getConfig().OPTIMIZELY_URL} />;
} if (getConfig().OPTIMIZELY_PROJECT_ID) {
return (
<script
src={`${getConfig().MARKETING_SITE_BASE_URL}/optimizelyjs/${getConfig().OPTIMIZELY_PROJECT_ID}.js`}
/>
);
}
return null;
};
React.useEffect(() => {
if (authenticatedUser?.administrator || getConfig().NODE_ENV === 'development') {
window.loadEmptyData = () => {
@@ -91,7 +76,6 @@ export const App = () => {
<Helmet>
<title>{formatMessage(messages.pageTitle)}</title>
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
{optimizelyScript()}
</Helmet>
<div>
<AppWrapper>
@@ -103,14 +87,11 @@ export const App = () => {
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
</Alert>
) : (
<ExperimentProvider>
<Dashboard />
</ExperimentProvider>
<Dashboard />
)}
</main>
</AppWrapper>
<Footer logo={getConfig().LOGO_POWERED_BY_OPEN_EDX_URL_SVG} />
<ZendeskFab />
<FooterSlot />
</div>
</>
);

View File

@@ -9,6 +9,7 @@ $fa-font-path: "~font-awesome/fonts";
$input-focus-box-shadow: $input-box-shadow; // 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";
.text-ellipsis {

View File

@@ -2,7 +2,6 @@ import React from 'react';
import { Helmet } from 'react-helmet';
import { shallow } from '@edx/react-unit-test-utils';
import Footer from '@edx/frontend-component-footer';
import { useIntl } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
@@ -11,18 +10,14 @@ import { reduxHooks } from 'hooks';
import Dashboard from 'containers/Dashboard';
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
import AppWrapper from 'containers/WidgetContainers/AppWrapper';
import { ExperimentProvider } from 'ExperimentContext';
import { App } from './App';
import messages from './messages';
jest.mock('@edx/frontend-component-footer', () => 'Footer');
jest.mock('@edx/frontend-component-footer', () => ({ FooterSlot: 'Footer' }));
jest.mock('containers/Dashboard', () => 'Dashboard');
jest.mock('containers/LearnerDashboardHeader', () => 'LearnerDashboardHeader');
jest.mock('components/ZendeskFab', () => 'ZendeskFab');
jest.mock('ExperimentContext', () => ({
ExperimentProvider: 'ExperimentProvider',
}));
jest.mock('containers/WidgetContainers/AppWrapper', () => 'AppWrapper');
jest.mock('data/redux', () => ({
selectors: 'redux.selectors',
@@ -38,8 +33,6 @@ jest.mock('hooks', () => ({
}));
jest.mock('data/store', () => 'data/store');
const logo = 'fakeLogo.png';
jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(() => ({})),
}));
@@ -66,9 +59,6 @@ describe('App router component', () => {
it('displays learner dashboard header', () => {
expect(el.instance.findByType(LearnerDashboardHeader).length).toEqual(1);
});
test('Footer logo drawn from env variable', () => {
expect(el.instance.findByType(Footer)[0].props.logo).toEqual(logo);
});
it('wraps the header and main components in an AppWrapper widget container', () => {
const container = el.instance.findByType(AppWrapper)[0];
expect(container.children[0].type).toEqual('LearnerDashboardHeader');
@@ -78,61 +68,58 @@ describe('App router component', () => {
describe('no network failure', () => {
beforeAll(() => {
reduxHooks.useRequestIsFailed.mockReturnValue(false);
getConfig.mockReturnValue({ LOGO_POWERED_BY_OPEN_EDX_URL_SVG: logo });
getConfig.mockReturnValue({});
el = shallow(<App />);
});
runBasicTests();
it('loads dashboard', () => {
const main = el.instance.findByType('main')[0];
expect(main.children.length).toEqual(1);
const expProvider = main.children[0];
expect(expProvider.type).toEqual('ExperimentProvider');
expect(expProvider.children.length).toEqual(1);
const dashboard = main.children[0];
expect(dashboard.type).toEqual('Dashboard');
expect(
expProvider.matches(shallow(<ExperimentProvider><Dashboard /></ExperimentProvider>)),
dashboard.matches(shallow(<Dashboard />)),
).toEqual(true);
});
});
describe('no network failure with optimizely url', () => {
beforeAll(() => {
reduxHooks.useRequestIsFailed.mockReturnValue(false);
getConfig.mockReturnValue({ LOGO_POWERED_BY_OPEN_EDX_URL_SVG: logo, OPTIMIZELY_URL: 'fake.url' });
getConfig.mockReturnValue({ OPTIMIZELY_URL: 'fake.url' });
el = shallow(<App />);
});
runBasicTests();
it('loads dashboard', () => {
const main = el.instance.findByType('main')[0];
expect(main.children.length).toEqual(1);
const expProvider = main.children[0];
expect(expProvider.type).toEqual('ExperimentProvider');
expect(expProvider.children.length).toEqual(1);
const dashboard = main.children[0];
expect(dashboard.type).toEqual('Dashboard');
expect(
expProvider.matches(shallow(<ExperimentProvider><Dashboard /></ExperimentProvider>)),
dashboard.matches(shallow(<Dashboard />)),
).toEqual(true);
});
});
describe('no network failure with optimizely project id', () => {
beforeAll(() => {
reduxHooks.useRequestIsFailed.mockReturnValue(false);
getConfig.mockReturnValue({ LOGO_POWERED_BY_OPEN_EDX_URL_SVG: logo, OPTIMIZELY_PROJECT_ID: 'fakeId' });
getConfig.mockReturnValue({ OPTIMIZELY_PROJECT_ID: 'fakeId' });
el = shallow(<App />);
});
runBasicTests();
it('loads dashboard', () => {
const main = el.instance.findByType('main')[0];
expect(main.children.length).toEqual(1);
const expProvider = main.children[0];
expect(expProvider.type).toEqual('ExperimentProvider');
expect(expProvider.children.length).toEqual(1);
const dashboard = main.children[0];
expect(dashboard.type).toEqual('Dashboard');
expect(
expProvider.matches(shallow(<ExperimentProvider><Dashboard /></ExperimentProvider>)),
dashboard.matches(shallow(<Dashboard />)),
).toEqual(true);
});
});
describe('initialize failure', () => {
beforeAll(() => {
reduxHooks.useRequestIsFailed.mockImplementation((key) => key === RequestKeys.initialize);
getConfig.mockReturnValue({ LOGO_POWERED_BY_OPEN_EDX_URL_SVG: logo });
getConfig.mockReturnValue({});
el = shallow(<App />);
});
runBasicTests();
@@ -150,7 +137,7 @@ describe('App router component', () => {
describe('refresh failure', () => {
beforeAll(() => {
reduxHooks.useRequestIsFailed.mockImplementation((key) => key === RequestKeys.refreshList);
getConfig.mockReturnValue({ LOGO_POWERED_BY_OPEN_EDX_URL_SVG: logo });
getConfig.mockReturnValue({});
el = shallow(<App />);
});
runBasicTests();

View File

@@ -1,64 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useWindowSize, breakpoints } from '@openedx/paragon';
import { StrictDict } from 'utils';
import api from 'widgets/ProductRecommendations/api';
import * as module from './ExperimentContext';
export const state = StrictDict({
experiment: (val) => React.useState(val), // eslint-disable-line
countryCode: (val) => React.useState(val), // eslint-disable-line
});
export const useCountryCode = (setCountryCode) => {
React.useEffect(() => {
api
.fetchRecommendationsContext()
.then((response) => {
setCountryCode(response.data.countryCode);
})
.catch(() => {
setCountryCode('');
});
/* eslint-disable */
}, []);
};
export const ExperimentContext = React.createContext();
export const ExperimentProvider = ({ children }) => {
const [countryCode, setCountryCode] = module.state.countryCode(null);
const [experiment, setExperiment] = module.state.experiment({
isExperimentActive: false,
inRecommendationsVariant: true,
});
module.useCountryCode(setCountryCode);
const { width } = useWindowSize();
const isMobile = width < breakpoints.small.minWidth;
const contextValue = React.useMemo(
() => ({
experiment,
countryCode,
setExperiment,
setCountryCode,
isMobile,
}),
[experiment, countryCode, setExperiment, setCountryCode, isMobile]
);
return (
<ExperimentContext.Provider value={contextValue}>
{children}
</ExperimentContext.Provider>
);
};
export const useExperimentContext = () => React.useContext(ExperimentContext);
ExperimentProvider.propTypes = {
children: PropTypes.node.isRequired,
};
export default { useCountryCode, useExperimentContext };

View File

@@ -1,122 +0,0 @@
import React from 'react';
import { waitFor, render } from '@testing-library/react';
import { useWindowSize } from '@openedx/paragon';
import api from 'widgets/ProductRecommendations/api';
import { MockUseState } from 'testUtils';
import * as experiment from 'ExperimentContext';
const state = new MockUseState(experiment);
jest.unmock('react');
jest.spyOn(React, 'useEffect').mockImplementation((cb, prereqs) => ({ useEffect: { cb, prereqs } }));
jest.mock('widgets/ProductRecommendations/api', () => ({
fetchRecommendationsContext: jest.fn(),
}));
describe('experiments context', () => {
beforeEach(() => {
jest.resetAllMocks();
});
describe('useCountryCode', () => {
describe('behaviour', () => {
describe('useEffect call', () => {
let calls;
let cb;
const setCountryCode = jest.fn();
const successfulFetch = { data: { countryCode: 'ZA' } };
beforeEach(() => {
experiment.useCountryCode(setCountryCode);
({ calls } = React.useEffect.mock);
[[cb]] = calls;
});
it('calls useEffect once', () => {
expect(calls.length).toEqual(1);
});
describe('successful fetch', () => {
it('sets the country code', async () => {
let resolveFn;
api.fetchRecommendationsContext.mockReturnValueOnce(
new Promise((resolve) => {
resolveFn = resolve;
}),
);
cb();
expect(api.fetchRecommendationsContext).toHaveBeenCalled();
expect(setCountryCode).not.toHaveBeenCalled();
resolveFn(successfulFetch);
await waitFor(() => {
expect(setCountryCode).toHaveBeenCalledWith(successfulFetch.data.countryCode);
});
});
});
describe('unsuccessful fetch', () => {
it('sets the country code to an empty string', async () => {
let rejectFn;
api.fetchRecommendationsContext.mockReturnValueOnce(
new Promise((resolve, reject) => {
rejectFn = reject;
}),
);
cb();
expect(api.fetchRecommendationsContext).toHaveBeenCalled();
expect(setCountryCode).not.toHaveBeenCalled();
rejectFn();
await waitFor(() => {
expect(setCountryCode).toHaveBeenCalledWith('');
});
});
});
});
});
});
describe('ExperimentProvider', () => {
const { ExperimentProvider } = experiment;
const TestComponent = () => {
const {
experiment: exp,
setExperiment,
countryCode,
setCountryCode,
isMobile,
} = experiment.useExperimentContext();
expect(exp.isExperimentActive).toBeFalsy();
expect(exp.inRecommendationsVariant).toBeTruthy();
expect(countryCode).toBeNull();
expect(isMobile).toBe(false);
expect(setExperiment).toBeDefined();
expect(setCountryCode).toBeDefined();
return (
<div />
);
};
it('allows access to child components with the context stateful values', () => {
const countryCodeSpy = jest.spyOn(experiment, 'useCountryCode').mockImplementationOnce(() => {});
useWindowSize.mockImplementationOnce(() => ({ width: 577, height: 943 }));
state.mock();
render(
<ExperimentProvider>
<TestComponent />
</ExperimentProvider>,
);
expect(countryCodeSpy).toHaveBeenCalledWith(state.setState.countryCode);
state.expectInitializedWith(state.keys.countryCode, null);
state.expectInitializedWith(state.keys.experiment, { isExperimentActive: false, inRecommendationsVariant: true });
});
});
});

View File

@@ -27,10 +27,7 @@ exports[`App router component component initialize failure snapshot 1`] = `
</Alert>
</main>
</AppWrapper>
<Footer
logo="fakeLogo.png"
/>
<ZendeskFab />
<FooterSlot />
</div>
</Fragment>
`;
@@ -53,15 +50,10 @@ exports[`App router component component no network failure snapshot 1`] = `
<AppWrapper>
<LearnerDashboardHeader />
<main>
<ExperimentProvider>
<Dashboard />
</ExperimentProvider>
<Dashboard />
</main>
</AppWrapper>
<Footer
logo="fakeLogo.png"
/>
<ZendeskFab />
<FooterSlot />
</div>
</Fragment>
`;
@@ -79,23 +71,15 @@ exports[`App router component component no network failure with optimizely proje
rel="shortcut icon"
type="image/x-icon"
/>
<script
src="undefined/optimizelyjs/fakeId.js"
/>
</HelmetWrapper>
<div>
<AppWrapper>
<LearnerDashboardHeader />
<main>
<ExperimentProvider>
<Dashboard />
</ExperimentProvider>
<Dashboard />
</main>
</AppWrapper>
<Footer
logo="fakeLogo.png"
/>
<ZendeskFab />
<FooterSlot />
</div>
</Fragment>
`;
@@ -113,23 +97,15 @@ exports[`App router component component no network failure with optimizely url s
rel="shortcut icon"
type="image/x-icon"
/>
<script
src="fake.url"
/>
</HelmetWrapper>
<div>
<AppWrapper>
<LearnerDashboardHeader />
<main>
<ExperimentProvider>
<Dashboard />
</ExperimentProvider>
<Dashboard />
</main>
</AppWrapper>
<Footer
logo="fakeLogo.png"
/>
<ZendeskFab />
<FooterSlot />
</div>
</Fragment>
`;
@@ -161,10 +137,7 @@ exports[`App router component component refresh failure snapshot 1`] = `
</Alert>
</main>
</AppWrapper>
<Footer
logo="fakeLogo.png"
/>
<ZendeskFab />
<FooterSlot />
</div>
</Fragment>
`;

View File

@@ -9,7 +9,7 @@ exports[`app registry subscribe: APP_INIT_ERROR. snapshot: displays an ErrorPag
exports[`app registry subscribe: APP_READY. links App to root element 1`] = `
<AppProvider
store={
Object {
{
"redux": "store",
}
}

View File

@@ -5,23 +5,23 @@ exports[`ZendeskFab snapshot 1`] = `
cookies={true}
defer={true}
webWidget={
Object {
"answerBot": Object {
"avatar": Object {
"name": Object {
{
"answerBot": {
"avatar": {
"name": {
"*": "edX Support",
},
"url": "https://edx-cdn.org/v3/prod/favicon.ico",
},
"contactOnlyAfterQuery": true,
"suppress": false,
"title": Object {
"title": {
"*": "edX Support",
},
},
"chat": Object {
"departments": Object {
"enabled": Array [
"chat": {
"departments": {
"enabled": [
"account settings",
"billing and payments",
"certificates",
@@ -33,17 +33,17 @@ exports[`ZendeskFab snapshot 1`] = `
},
"suppress": false,
},
"contactForm": Object {
"contactForm": {
"attachments": true,
"selectTicketForm": Object {
"selectTicketForm": {
"*": "Please choose your request type:",
},
"ticketForms": Array [
Object {
"fields": Array [
Object {
"ticketForms": [
{
"fields": [
{
"id": "description",
"prefill": Object {
"prefill": {
"*": "",
},
},
@@ -53,10 +53,10 @@ exports[`ZendeskFab snapshot 1`] = `
},
],
},
"contactOptions": Object {
"contactOptions": {
"enabled": false,
},
"helpCenter": Object {
"helpCenter": {
"originalArticleButton": true,
},
}

View File

@@ -18,6 +18,8 @@ const configuration = {
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

@@ -6,8 +6,8 @@ exports[`BeginCourseButton snapshot disabled snapshot 1`] = `
disabled={true}
href="#"
onClick={
Object {
"trackCourseEvent": Object {
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"upgradeUrl": "home-urlexec-ed-tracking-path=cardId",
@@ -25,8 +25,8 @@ exports[`BeginCourseButton snapshot enabled snapshot 1`] = `
disabled={false}
href="#"
onClick={
Object {
"trackCourseEvent": Object {
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"upgradeUrl": "home-urlexec-ed-tracking-path=cardId",

View File

@@ -6,8 +6,8 @@ exports[`ResumeButton snapshot disabled snapshot 1`] = `
disabled={true}
href="#"
onClick={
Object {
"trackCourseEvent": Object {
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"upgradeUrl": "resume-urlexec-ed-tracking-path=cardId",
@@ -25,8 +25,8 @@ exports[`ResumeButton snapshot enabled snapshot 1`] = `
disabled={false}
href="#"
onClick={
Object {
"trackCourseEvent": Object {
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"upgradeUrl": "resume-urlexec-ed-tracking-path=cardId",

View File

@@ -7,8 +7,8 @@ exports[`UpgradeButton snapshot can upgrade 1`] = `
href="upgradeUrl"
iconBefore={[MockFunction icons.Locked]}
onClick={
Object {
"trackCourseEvent": Object {
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.trackUpgradeClicked],
"upgradeUrl": "upgradeUrl",

View File

@@ -6,8 +6,8 @@ exports[`ViewCourseButton learner can view course 1`] = `
disabled={false}
href="#"
onClick={
Object {
"trackCourseEvent": Object {
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"upgradeUrl": "homeUrl",
@@ -25,8 +25,8 @@ exports[`ViewCourseButton learner cannot view course 1`] = `
disabled={true}
href="#"
onClick={
Object {
"trackCourseEvent": Object {
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"upgradeUrl": "homeUrl",

View File

@@ -5,7 +5,7 @@ import { ActionRow } from '@openedx/paragon';
import { reduxHooks } from 'hooks';
import UpgradeButton from './UpgradeButton';
import CourseCardActionSlot from 'plugin-slots/CourseCardActionSlot';
import SelectSessionButton from './SelectSessionButton';
import BeginCourseButton from './BeginCourseButton';
import ResumeButton from './ResumeButton';
@@ -14,15 +14,13 @@ import ViewCourseButton from './ViewCourseButton';
export const CourseCardActions = ({ cardId }) => {
const { isEntitlement, isFulfilled } = reduxHooks.useCardEntitlementData(cardId);
const {
isVerified,
hasStarted,
isExecEd2UCourse,
} = reduxHooks.useCardEnrollmentData(cardId);
const { isArchived } = reduxHooks.useCardCourseRunData(cardId);
return (
<ActionRow data-test-id="CourseCardActions">
{!(isEntitlement || isVerified || isExecEd2UCourse) && <UpgradeButton cardId={cardId} />}
<CourseCardActionSlot cardId={cardId} />
{isEntitlement && (isFulfilled
? <ViewCourseButton cardId={cardId} />
: <SelectSessionButton cardId={cardId} />

View File

@@ -2,6 +2,7 @@ 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';
@@ -19,6 +20,7 @@ jest.mock('hooks', () => ({
},
}));
jest.mock('plugin-slots/CourseCardActionSlot', () => 'CustomActionButton');
jest.mock('./UpgradeButton', () => 'UpgradeButton');
jest.mock('./SelectSessionButton', () => 'SelectSessionButton');
jest.mock('./ViewCourseButton', () => 'ViewCourseButton');
@@ -88,18 +90,18 @@ describe('CourseCardActions', () => {
expect(el.instance.findByType(UpgradeButton).length).toEqual(0);
});
});
describe('not entielement, verified, or exec ed', () => {
describe('not entitlement, verified, or exec ed', () => {
it('renders UpgradeButton and ViewCourseButton for archived courses', () => {
mockHooks({ isArchived: true });
render();
expect(el.instance.findByType(UpgradeButton)[0].props.cardId).toEqual(cardId);
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', () => {
mockHooks();
render();
expect(el.instance.findByType(UpgradeButton)[0].props.cardId).toEqual(cardId);
expect(el.instance.findByType(CourseCardActionSlot)[0].props.cardId).toEqual(cardId);
expect(el.instance.findByType(BeginCourseButton)[0].props.cardId).toEqual(cardId);
});
});
@@ -107,7 +109,7 @@ describe('CourseCardActions', () => {
it('renders UpgradeButton and ResumeButton', () => {
mockHooks({ hasStarted: true });
render();
expect(el.instance.findByType(UpgradeButton)[0].props.cardId).toEqual(cardId);
expect(el.instance.findByType(CourseCardActionSlot)[0].props.cardId).toEqual(cardId);
expect(el.instance.findByType(ResumeButton)[0].props.cardId).toEqual(cardId);
});
});

View File

@@ -10,14 +10,14 @@ exports[`CreditBanner component render with error state snapshot 1`] = `
>
<format-message-function
message={
Object {
{
"defaultMessage": "An error occurred with this transaction. For help, contact {supportEmailLink}.",
"description": "",
"id": "learner-dash.courseCard.banners.credit.error",
}
}
values={
Object {
{
"supportEmailLink": <MailtoLink
to="test-support-email"
>

View File

@@ -27,8 +27,8 @@ exports[`CreditContent component render with action snapshot 1`] = `
</ActionRow>
<CreditRequestForm
requestData={
Object {
"parameters": Object {
{
"parameters": {
"key1": "val1",
},
"url": "test-request-data-url",
@@ -48,8 +48,8 @@ exports[`CreditContent component render without action snapshot 1`] = `
</div>
<CreditRequestForm
requestData={
Object {
"parameters": Object {
{
"parameters": {
"key1": "val1",
},
"url": "test-request-data-url",

View File

@@ -13,12 +13,12 @@ exports[`RelatedProgramsBanner render with programs 1`] = `
</span>
<ProgramsList
programs={
Array [
Object {
[
{
"title": "Program 1",
"url": "http://example.com/program1",
},
Object {
{
"title": "Program 2",
"url": "http://example.com/program2",
},

View File

@@ -43,14 +43,14 @@ exports[`CertificateBanner snapshot is restricted and verified with billing emai
<format-message-function
message={
Object {
{
"defaultMessage": "If you would like a refund on your Certificate of Achievement, please contact our billing address {billingEmail}",
"description": "Message to learners to contact billing for certificate refunds",
"id": "learner-dash.courseCard.banners.certificateRefundContactBilling",
}
}
values={
Object {
{
"billingEmail": <MailtoLink
to="billing@email"
>
@@ -68,14 +68,14 @@ exports[`CertificateBanner snapshot is restricted and verified with support and
>
<format-message-function
message={
Object {
{
"defaultMessage": "Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {supportEmail}.",
"description": "Restricted certificate warning message",
"id": "learner-dash.courseCard.banners.certificateRestricted",
}
}
values={
Object {
{
"supportEmail": <MailtoLink
to="suport@email"
>
@@ -87,14 +87,14 @@ exports[`CertificateBanner snapshot is restricted and verified with support and
<format-message-function
message={
Object {
{
"defaultMessage": "If you would like a refund on your Certificate of Achievement, please contact our billing address {billingEmail}",
"description": "Message to learners to contact billing for certificate refunds",
"id": "learner-dash.courseCard.banners.certificateRefundContactBilling",
}
}
values={
Object {
{
"billingEmail": <MailtoLink
to="billing@email"
>
@@ -112,14 +112,14 @@ exports[`CertificateBanner snapshot is restricted and verified with support emai
>
<format-message-function
message={
Object {
{
"defaultMessage": "Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {supportEmail}.",
"description": "Restricted certificate warning message",
"id": "learner-dash.courseCard.banners.certificateRestricted",
}
}
values={
Object {
{
"supportEmail": <MailtoLink
to="suport@email"
>
@@ -147,14 +147,14 @@ exports[`CertificateBanner snapshot is restricted with support email 1`] = `
>
<format-message-function
message={
Object {
{
"defaultMessage": "Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {supportEmail}.",
"description": "Restricted certificate warning message",
"id": "learner-dash.courseCard.banners.certificateRestricted",
}
}
values={
Object {
{
"supportEmail": <MailtoLink
to="suport@email"
>

View File

@@ -4,14 +4,14 @@ exports[`EntitlementBanner snapshot: expiration warning 1`] = `
<Banner>
<format-message-function
message={
Object {
{
"defaultMessage": "You must {selectSessionButton} by {changeDeadline} to access the course.",
"description": "Entitlement course message when the entitlement is expiring soon.",
"id": "learner-dash.courseCard.banners.entitlementExpiringSoon",
}
}
values={
Object {
{
"changeDeadline": "11/11/2022",
"selectSessionButton": <Button
className="m-0 p-0"
@@ -33,14 +33,14 @@ exports[`EntitlementBanner snapshot: no sessions available 1`] = `
>
<format-message-function
message={
Object {
{
"defaultMessage": "There are no sessions available at the moment. The course team will create new sessions soon. If no sessions appear, please contact {emailLink} for information.",
"description": "Entitlement course message when no sessions are available",
"id": "learner-dash.courseCard.banners.entitlementUnavailable",
}
}
values={
Object {
{
"emailLink": <MailtoLink
to="test-support-email"
>

View File

@@ -17,7 +17,7 @@ exports[`CourseCardMenu render show dropdown hide unenroll item and disable emai
<SocialShareMenu
cardId="test-card-id"
emailSettings={
Object {
{
"hide": [MockFunction emailSettingHide],
"isVisible": false,
"show": [MockFunction emailSettingShow],
@@ -58,7 +58,7 @@ exports[`CourseCardMenu render show dropdown show unenroll and enable email snap
<SocialShareMenu
cardId="test-card-id"
emailSettings={
Object {
{
"hide": [MockFunction emailSettingHide],
"isVisible": false,
"show": [MockFunction emailSettingShow],

View File

@@ -5,8 +5,8 @@ exports[`CourseCardImage snapshot renders clickable link course Image 1`] = `
className="pgn__card-wrapper-image-cap overflow-visible orientation"
href="home-url"
onClick={
Object {
"trackCourseEvent": Object {
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.courseImageClicked],
"upgradeUrl": "home-url",

View File

@@ -7,8 +7,8 @@ exports[`CourseCardTitle snapshot renders clickable link course title 1`] = `
data-testid="CourseCardTitle"
href="home-url"
onClick={
Object {
"trackCourseEvent": Object {
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.courseTitleClicked],
"upgradeUrl": "home-url",

View File

@@ -28,7 +28,7 @@ exports[`CourseFilterControls is not mobile snapshot 1`] = `
>
<FilterForm
filters={
Array [
[
"test-filter",
]
}
@@ -84,7 +84,7 @@ exports[`CourseFilterControls mobile snapshot 1`] = `
>
<FilterForm
filters={
Array [
[
"test-filter",
]
}
@@ -144,7 +144,7 @@ exports[`CourseFilterControls no courses snapshot 1`] = `
>
<FilterForm
filters={
Array [
[
"test-filter",
]
}

View File

@@ -11,7 +11,7 @@ exports[`FilterForm snapshot renders 1`] = `
name="course-status-filters"
onChange={[MockFunction handleFilterChange]}
value={
Array [
[
"test-filter",
]
}

View File

@@ -9,9 +9,10 @@ import CourseCard from 'containers/CourseCard';
import { useIsCollapsed } from './hooks';
export const CourseList = ({
filterOptions, setPageNumber, numPages, showFilters, visibleList,
}) => {
export const CourseList = ({ courseListData }) => {
const {
filterOptions, setPageNumber, numPages, showFilters, visibleList,
} = courseListData;
const isCollapsed = useIsCollapsed();
return (
<>
@@ -38,14 +39,16 @@ export const CourseList = ({
);
};
CourseList.propTypes = {
export const courseListDataShape = PropTypes.shape({
showFilters: PropTypes.bool.isRequired,
// eslint-disable-next-line react/forbid-prop-types
visibleList: PropTypes.arrayOf(PropTypes.object).isRequired,
// eslint-disable-next-line react/forbid-prop-types
filterOptions: PropTypes.object.isRequired,
visibleList: PropTypes.arrayOf(PropTypes.shape()).isRequired,
filterOptions: PropTypes.shape().isRequired,
numPages: PropTypes.number.isRequired,
setPageNumber: PropTypes.func.isRequired,
});
CourseList.propTypes = {
courseListData: courseListDataShape,
};
export default CourseList;

View File

@@ -23,7 +23,7 @@ describe('CourseList', () => {
useIsCollapsed.mockReturnValue(false);
const createWrapper = (courseListData = defaultCourseListData) => (
shallow(<CourseList {...courseListData} />)
shallow(<CourseList courseListData={courseListData} />)
);
describe('no courses or filters', () => {

View File

@@ -18,11 +18,7 @@ exports[`CoursesPanel no courses snapshot 1`] = `
<CourseFilterControls />
</div>
</div>
<PluginSlot
id="no_courses_view"
>
<NoCoursesView />
</PluginSlot>
<NoCoursesViewSlot />
</div>
`;
@@ -44,16 +40,16 @@ exports[`CoursesPanel with courses snapshot 1`] = `
<CourseFilterControls />
</div>
</div>
<PluginSlot
id="course_list"
>
<CourseList
filterOptions={Object {}}
numPages={1}
setPageNumber={[MockFunction setPageNumber]}
showFilters={false}
visibleList={Array []}
/>
</PluginSlot>
<CourseListSlot
courseListData={
{
"filterOptions": {},
"numPages": 1,
"setPageNumber": [MockFunction setPageNumber],
"showFilters": false,
"visibleList": [],
}
}
/>
</div>
`;

View File

@@ -1,15 +1,13 @@
import React from 'react';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import { useIntl } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks';
import {
CourseFilterControls,
} from 'containers/CourseFilterControls';
import NoCoursesView from './NoCoursesView';
import CourseList from './CourseList';
import CourseListSlot from 'plugin-slots/CourseListSlot';
import NoCoursesViewSlot from 'plugin-slots/NoCoursesViewSlot';
import { useCourseListData } from './hooks';
@@ -34,19 +32,7 @@ export const CoursesPanel = () => {
<CourseFilterControls {...courseListData.filterOptions} />
</div>
</div>
{hasCourses ? (
<PluginSlot
id="course_list"
>
<CourseList {...courseListData} />
</PluginSlot>
) : (
<PluginSlot
id="no_courses_view"
>
<NoCoursesView />
</PluginSlot>
)}
{hasCourses ? <CourseListSlot courseListData={courseListData} /> : <NoCoursesViewSlot />}
</div>
);
};

View File

@@ -3,7 +3,8 @@ import PropTypes from 'prop-types';
import { Container, Col, Row } from '@openedx/paragon';
import WidgetFooter from 'containers/WidgetContainers/WidgetFooter';
import WidgetSidebarSlot from 'plugin-slots/WidgetSidebarSlot';
import hooks from './hooks';
export const columnConfig = {
@@ -23,11 +24,10 @@ export const columnConfig = {
},
};
export const DashboardLayout = ({ children, sidebar: Sidebar }) => {
export const DashboardLayout = ({ children }) => {
const {
isCollapsed,
sidebarShowing,
setSidebarShowing,
} = hooks.useDashboardLayoutData();
const courseListColumnProps = sidebarShowing
@@ -42,12 +42,7 @@ export const DashboardLayout = ({ children, sidebar: Sidebar }) => {
</Col>
<Col {...columnConfig.sidebar} className="sidebar-column">
{!isCollapsed && (<h2 className="course-list-title">&nbsp;</h2>)}
<Sidebar setSidebarShowing={setSidebarShowing} />
</Col>
</Row>
<Row>
<Col>
<WidgetFooter />
<WidgetSidebarSlot />
</Col>
</Row>
</Container>
@@ -55,7 +50,6 @@ export const DashboardLayout = ({ children, sidebar: Sidebar }) => {
};
DashboardLayout.propTypes = {
children: PropTypes.node.isRequired,
sidebar: PropTypes.func.isRequired,
};
export default DashboardLayout;

View File

@@ -16,17 +16,13 @@ const hookProps = {
};
hooks.useDashboardLayoutData.mockReturnValue(hookProps);
const props = {
sidebar: jest.fn(() => 'test-sidebar-content'),
};
const children = 'test-children';
let el;
describe('DashboardLayout', () => {
beforeEach(() => {
jest.clearAllMocks();
el = shallow(<DashboardLayout {...props}>{children}</DashboardLayout>);
el = shallow(<DashboardLayout>{children}</DashboardLayout>);
});
const testColumns = () => {
@@ -40,17 +36,13 @@ describe('DashboardLayout', () => {
const columns = el.instance.findByType(Row)[0].findByType(Col);
expect(columns[0].children).not.toHaveLength(0);
});
it('displays sidebar prop in second column', () => {
it('displays WidgetSidebarSlot in second column', () => {
const columns = el.instance.findByType(Row)[0].findByType(Col);
expect(columns[1].findByType(props.sidebar)).toHaveLength(1);
});
it('displays a footer in the second row', () => {
const columns = el.instance.findByType(Row)[1].findByType(Col);
expect(columns[0].children[0].type).toEqual('WidgetFooter');
expect(columns[1].findByType('WidgetSidebarSlot')).toHaveLength(1);
});
};
const testSidebarLayout = () => {
it('displays widthSidebar width for course list column', () => {
it('displays withSidebar width for course list column', () => {
const columns = el.instance.findByType(Row)[0].findByType(Col);
Object.keys(columnConfig.courseList.withSidebar).forEach(size => {
expect(columns[0].props[size]).toEqual(columnConfig.courseList.withSidebar[size]);

View File

@@ -9,13 +9,13 @@ exports[`DashboardLayout collapsed sidebar not showing snapshot 1`] = `
<Col
className="course-list-column"
lg={
Object {
{
"offset": 0,
"span": 12,
}
}
xl={
Object {
{
"offset": 0,
"span": 12,
}
@@ -26,26 +26,19 @@ exports[`DashboardLayout collapsed sidebar not showing snapshot 1`] = `
<Col
className="sidebar-column"
lg={
Object {
{
"offset": 0,
"span": 12,
}
}
xl={
Object {
{
"offset": 0,
"span": 4,
}
}
>
<mockConstructor
setSidebarShowing={[MockFunction hooks.setSidebarShowing]}
/>
</Col>
</Row>
<Row>
<Col>
<WidgetFooter />
<WidgetSidebarSlot />
</Col>
</Row>
</Container>
@@ -60,13 +53,13 @@ exports[`DashboardLayout collapsed sidebar showing snapshot 1`] = `
<Col
className="course-list-column"
lg={
Object {
{
"offset": 0,
"span": 12,
}
}
xl={
Object {
{
"offset": 0,
"span": 8,
}
@@ -77,26 +70,19 @@ exports[`DashboardLayout collapsed sidebar showing snapshot 1`] = `
<Col
className="sidebar-column"
lg={
Object {
{
"offset": 0,
"span": 12,
}
}
xl={
Object {
{
"offset": 0,
"span": 4,
}
}
>
<mockConstructor
setSidebarShowing={[MockFunction hooks.setSidebarShowing]}
/>
</Col>
</Row>
<Row>
<Col>
<WidgetFooter />
<WidgetSidebarSlot />
</Col>
</Row>
</Container>
@@ -111,13 +97,13 @@ exports[`DashboardLayout not collapsed sidebar not showing snapshot 1`] = `
<Col
className="course-list-column"
lg={
Object {
{
"offset": 0,
"span": 12,
}
}
xl={
Object {
{
"offset": 0,
"span": 12,
}
@@ -128,13 +114,13 @@ exports[`DashboardLayout not collapsed sidebar not showing snapshot 1`] = `
<Col
className="sidebar-column"
lg={
Object {
{
"offset": 0,
"span": 12,
}
}
xl={
Object {
{
"offset": 0,
"span": 4,
}
@@ -145,14 +131,7 @@ exports[`DashboardLayout not collapsed sidebar not showing snapshot 1`] = `
>
 
</h2>
<mockConstructor
setSidebarShowing={[MockFunction hooks.setSidebarShowing]}
/>
</Col>
</Row>
<Row>
<Col>
<WidgetFooter />
<WidgetSidebarSlot />
</Col>
</Row>
</Container>
@@ -167,13 +146,13 @@ exports[`DashboardLayout not collapsed sidebar showing snapshot 1`] = `
<Col
className="course-list-column"
lg={
Object {
{
"offset": 0,
"span": 12,
}
}
xl={
Object {
{
"offset": 0,
"span": 8,
}
@@ -184,13 +163,13 @@ exports[`DashboardLayout not collapsed sidebar showing snapshot 1`] = `
<Col
className="sidebar-column"
lg={
Object {
{
"offset": 0,
"span": 12,
}
}
xl={
Object {
{
"offset": 0,
"span": 4,
}
@@ -201,14 +180,7 @@ exports[`DashboardLayout not collapsed sidebar showing snapshot 1`] = `
>
 
</h2>
<mockConstructor
setSidebarShowing={[MockFunction hooks.setSidebarShowing]}
/>
</Col>
</Row>
<Row>
<Col>
<WidgetFooter />
<WidgetSidebarSlot />
</Col>
</Row>
</Container>

View File

@@ -17,9 +17,7 @@ exports[`Dashboard snapshots courses loaded, show select session modal, no avail
data-testid="dashboard-content"
id="dashboard-content"
>
<DashboardLayout
sidebar="LoadedWidgetSidebar"
>
<DashboardLayout>
<CoursesPanel />
</DashboardLayout>
</div>
@@ -62,9 +60,7 @@ exports[`Dashboard snapshots there are no courses, there ARE available dashboard
data-testid="dashboard-content"
id="dashboard-content"
>
<DashboardLayout
sidebar="NoCoursesWidgetSidebar"
>
<DashboardLayout>
<CoursesPanel />
</DashboardLayout>
</div>

View File

@@ -26,7 +26,8 @@ export const useDashboardMessages = () => {
export const useDashboardLayoutData = () => {
const { width } = useWindowSize();
const [sidebarShowing, setSidebarShowing] = module.state.sidebarShowing(false);
const [sidebarShowing, setSidebarShowing] = module.state.sidebarShowing(true);
return {
isDashboardCollapsed: width < breakpoints.large.maxWidth,
sidebarShowing,

View File

@@ -40,9 +40,9 @@ describe('CourseCard hooks', () => {
describe('useDashboardLayoutData', () => {
beforeEach(() => { state.mock(); });
describe('behavior', () => {
it('initializes sidebarShowing to default false value', () => {
it('initializes sidebarShowing to default true value', () => {
hooks.useDashboardLayoutData();
state.expectInitializedWith(state.keys.sidebarShowing, false);
state.expectInitializedWith(state.keys.sidebarShowing, true);
});
});
describe('output', () => {

View File

@@ -6,9 +6,6 @@ import EnterpriseDashboardModal from 'containers/EnterpriseDashboardModal';
import SelectSessionModal from 'containers/SelectSessionModal';
import CoursesPanel from 'containers/CoursesPanel';
import LoadedSidebar from 'containers/WidgetContainers/LoadedSidebar';
import NoCoursesSidebar from 'containers/WidgetContainers/NoCoursesSidebar';
import LoadingView from './LoadingView';
import DashboardLayout from './DashboardLayout';
import hooks from './hooks';
@@ -35,7 +32,7 @@ export const Dashboard = () => {
{initIsPending
? (<LoadingView />)
: (
<DashboardLayout sidebar={hasCourses ? LoadedSidebar : NoCoursesSidebar}>
<DashboardLayout>
<CoursesPanel />
</DashboardLayout>
)}

View File

@@ -6,9 +6,6 @@ import EnterpriseDashboardModal from 'containers/EnterpriseDashboardModal';
import SelectSessionModal from 'containers/SelectSessionModal';
import CoursesPanel from 'containers/CoursesPanel';
import LoadedWidgetSidebar from 'containers/WidgetContainers/LoadedSidebar';
import NoCoursesWidgetSidebar from 'containers/WidgetContainers/NoCoursesSidebar';
import DashboardLayout from './DashboardLayout';
import LoadingView from './LoadingView';
import hooks from './hooks';
@@ -25,8 +22,6 @@ jest.mock('hooks', () => ({
jest.mock('containers/EnterpriseDashboardModal', () => 'EnterpriseDashboardModal');
jest.mock('containers/CoursesPanel', () => 'CoursesPanel');
jest.mock('containers/WidgetContainers/LoadedSidebar', () => 'LoadedWidgetSidebar');
jest.mock('containers/WidgetContainers/NoCoursesSidebar', () => 'NoCoursesWidgetSidebar');
jest.mock('./LoadingView', () => 'LoadingView');
jest.mock('./DashboardLayout', () => 'DashboardLayout');
@@ -116,7 +111,7 @@ describe('Dashboard', () => {
showSelectSessionModal: true,
},
content: ['LoadedView', (
<DashboardLayout sidebar={LoadedWidgetSidebar}><CoursesPanel /></DashboardLayout>
<DashboardLayout><CoursesPanel /></DashboardLayout>
)],
showEnterpriseModal: false,
showSelectSessionModal: true,
@@ -132,7 +127,7 @@ describe('Dashboard', () => {
showSelectSessionModal: false,
},
content: ['Dashboard layout with no courses sidebar and content', (
<DashboardLayout sidebar={NoCoursesWidgetSidebar}><CoursesPanel /></DashboardLayout>
<DashboardLayout><CoursesPanel /></DashboardLayout>
)],
showEnterpriseModal: true,
showSelectSessionModal: false,

View File

@@ -10,7 +10,7 @@ exports[`EmailSettingsModal render snapshot: emails disabled, show: false 1`] =
<div
className="bg-white p-3 rounded shadow"
style={
Object {
{
"textAlign": "start",
}
}
@@ -54,7 +54,7 @@ exports[`EmailSettingsModal render snapshot: emails disabled, show: true 1`] = `
<div
className="bg-white p-3 rounded shadow"
style={
Object {
{
"textAlign": "start",
}
}
@@ -98,7 +98,7 @@ exports[`EmailSettingsModal render snapshot: emails enabled, show: true 1`] = `
<div
className="bg-white p-3 rounded shadow"
style={
Object {
{
"textAlign": "start",
}
}

View File

@@ -11,7 +11,7 @@ exports[`EnterpriseDashboard snapshot 1`] = `
<div
className="bg-white p-3 rounded shadow"
style={
Object {
{
"textAlign": "start",
}
}

View File

@@ -1,104 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import { Button, Badge } from '@openedx/paragon';
import urls from 'data/services/lms/urls';
import { reduxHooks } from 'hooks';
import { findCoursesNavDropdownClicked } from '../hooks';
import messages from '../messages';
export const CollapseMenuBody = ({ isOpen }) => {
const { formatMessage } = useIntl();
const { authenticatedUser } = React.useContext(AppContext);
const dashboard = reduxHooks.useEnterpriseDashboardData();
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
const exploreCoursesClick = findCoursesNavDropdownClicked(
urls.baseAppUrl(courseSearchUrl),
);
if (!isOpen) {
return null;
}
return (
<div className="d-flex flex-column shadow-sm nav-small-menu">
<Button as="a" href="/" variant="inverse-primary">
{formatMessage(messages.course)}
</Button>
<Button as="a" href={urls.programsUrl()} variant="inverse-primary">
{formatMessage(messages.program)}
</Button>
<Button
as="a"
href={urls.baseAppUrl(courseSearchUrl)}
variant="inverse-primary"
onClick={exploreCoursesClick}
>
{formatMessage(messages.discoverNew)}
</Button>
<Button as="a" href={getConfig().SUPPORT_URL} variant="inverse-primary">
{formatMessage(messages.help)}
</Button>
{authenticatedUser && (
<>
{!!dashboard && (
<Button as="a" href={dashboard.url} variant="inverse-primary">
{formatMessage(messages.dashboard)}
</Button>
)}
{!dashboard && getConfig().CAREER_LINK_URL && (
<Button href={`${getConfig().CAREER_LINK_URL}`}>
{formatMessage(messages.career)}
<Badge className="px-2 mx-2" variant="warning">
{formatMessage(messages.newAlert)}
</Badge>
</Button>
)}
<Button
as="a"
href={`${getConfig().LMS_BASE_URL}/u/${authenticatedUser.username}`}
variant="inverse-primary"
>
{formatMessage(messages.profile)}
</Button>
<Button
as="a"
href={`${getConfig().LMS_BASE_URL}/account/settings`}
variant="inverse-primary"
>
{formatMessage(messages.account)}
</Button>
{getConfig().ORDER_HISTORY_URL && (
<Button
as="a"
variant="inverse-primary"
href={getConfig().ORDER_HISTORY_URL}
>
{formatMessage(messages.orderHistory)}
</Button>
)}
<Button
as="a"
href={getConfig().LOGOUT_URL}
variant="inverse-primary"
>
{formatMessage(messages.signOut)}
</Button>
</>
)}
</div>
);
};
CollapseMenuBody.propTypes = {
isOpen: PropTypes.bool.isRequired,
};
export default CollapseMenuBody;

View File

@@ -1,48 +0,0 @@
import { shallow } from '@edx/react-unit-test-utils';
import { AppContext } from '@edx/frontend-platform/react';
import CollapseMenuBody from './CollapseMenuBody';
jest.mock('@edx/frontend-platform/react', () => ({
AppContext: {
authenticatedUser: {
username: 'username',
},
},
}));
jest.mock('hooks', () => ({
reduxHooks: {
useEnterpriseDashboardData: () => ({
url: 'url',
}),
usePlatformSettingsData: () => ({
courseSearchUrl: '/courseSearchUrl',
}),
},
}));
jest.mock('../hooks', () => ({
findCoursesNavDropdownClicked: (url) => jest.fn().mockName(`findCoursesNavDropdownClicked("${url}")`),
}));
describe('CollapseMenuBody', () => {
test('render', () => {
const wrapper = shallow(<CollapseMenuBody isOpen />);
expect(wrapper.snapshot).toMatchSnapshot();
});
test('render empty if not open', () => {
const wrapper = shallow(<CollapseMenuBody isOpen={false} />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.isEmptyRender()).toBe(true);
});
test('render unauthenticated', () => {
const { authenticatedUser } = AppContext;
AppContext.authenticatedUser = null;
const wrapper = shallow(<CollapseMenuBody isOpen />);
expect(wrapper.snapshot).toMatchSnapshot();
AppContext.authenticatedUser = authenticatedUser;
});
});

View File

@@ -1,105 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CollapseMenuBody render 1`] = `
<div
className="d-flex flex-column shadow-sm nav-small-menu"
>
<Button
as="a"
href="/"
variant="inverse-primary"
>
Courses
</Button>
<Button
as="a"
href="http://localhost:18000/dashboard/programs"
variant="inverse-primary"
>
Programs
</Button>
<Button
as="a"
href="http://localhost:18000/courseSearchUrl"
onClick={[MockFunction findCoursesNavDropdownClicked("http://localhost:18000/courseSearchUrl")]}
variant="inverse-primary"
>
Discover New
</Button>
<Button
as="a"
href="http://localhost:18000/support"
variant="inverse-primary"
>
Help
</Button>
<Fragment>
<Button
as="a"
href="url"
variant="inverse-primary"
>
Dashboard
</Button>
<Button
as="a"
href="http://localhost:18000/u/username"
variant="inverse-primary"
>
Profile
</Button>
<Button
as="a"
href="http://localhost:18000/account/settings"
variant="inverse-primary"
>
Account
</Button>
<Button
as="a"
href="http://localhost:18000/logout"
variant="inverse-primary"
>
Sign Out
</Button>
</Fragment>
</div>
`;
exports[`CollapseMenuBody render empty if not open 1`] = `null`;
exports[`CollapseMenuBody render unauthenticated 1`] = `
<div
className="d-flex flex-column shadow-sm nav-small-menu"
>
<Button
as="a"
href="/"
variant="inverse-primary"
>
Courses
</Button>
<Button
as="a"
href="http://localhost:18000/dashboard/programs"
variant="inverse-primary"
>
Programs
</Button>
<Button
as="a"
href="http://localhost:18000/courseSearchUrl"
onClick={[MockFunction findCoursesNavDropdownClicked("http://localhost:18000/courseSearchUrl")]}
variant="inverse-primary"
>
Discover New
</Button>
<Button
as="a"
href="http://localhost:18000/support"
variant="inverse-primary"
>
Help
</Button>
</div>
`;

View File

@@ -1,48 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CollapsedHeader render nothing if not collapsed 1`] = `false`;
exports[`CollapsedHeader renders 1`] = `
<Fragment>
<header
className="d-flex shadow-sm align-items-center learner-variant-header"
>
<IconButton
alt="Close"
className="p-4"
iconAs="Icon"
invertColors={true}
isActive={true}
onClick={[MockFunction toggleIsOpen]}
variant="primary"
/>
<mockConstructor />
</header>
<mockConstructor
isOpen={false}
/>
</Fragment>
`;
exports[`CollapsedHeader renders with isOpen true 1`] = `
<Fragment>
<header
className="d-flex shadow-sm align-items-center learner-variant-header"
>
<IconButton
alt="Menu"
className="p-4"
iconAs="Icon"
invertColors={true}
isActive={true}
onClick={[MockFunction toggleIsOpen]}
src={[MockFunction icons.Close]}
variant="primary"
/>
<mockConstructor />
</header>
<mockConstructor
isOpen={true}
/>
</Fragment>
`;

View File

@@ -1,47 +0,0 @@
import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { MenuIcon, Close } from '@openedx/paragon/icons';
import { IconButton, Icon } from '@openedx/paragon';
import { useLearnerDashboardHeaderData, useIsCollapsed } from '../hooks';
import CollapseMenuBody from './CollapseMenuBody';
import BrandLogo from '../BrandLogo';
import messages from '../messages';
export const CollapsedHeader = () => {
const { formatMessage } = useIntl();
const isCollapsed = useIsCollapsed();
const { isOpen, toggleIsOpen } = useLearnerDashboardHeaderData();
return (
isCollapsed && (
<>
<header className="d-flex shadow-sm align-items-center learner-variant-header">
<IconButton
invertColors
isActive
src={isOpen ? Close : MenuIcon}
iconAs={Icon}
alt={
isOpen
? formatMessage(messages.collapseMenuOpenAltText)
: formatMessage(messages.collapseMenuClosedAltText)
}
onClick={toggleIsOpen}
variant="primary"
className="p-4"
/>
<BrandLogo />
</header>
<CollapseMenuBody isOpen={isOpen} />
</>
)
);
};
CollapsedHeader.propTypes = {};
export default CollapsedHeader;

View File

@@ -1,38 +0,0 @@
import { shallow } from '@edx/react-unit-test-utils';
import CollapsedHeader from '.';
import { useLearnerDashboardHeaderData, useIsCollapsed } from '../hooks';
jest.mock('../BrandLogo', () => jest.fn(() => 'BrandLogo'));
jest.mock('./CollapseMenuBody', () => jest.fn(() => 'CollapseMenuBody'));
jest.mock('../hooks', () => ({
useIsCollapsed: jest.fn(() => true),
useLearnerDashboardHeaderData: jest.fn(() => ({
isOpen: false,
toggleIsOpen: jest.fn().mockName('toggleIsOpen'),
})),
}));
describe('CollapsedHeader', () => {
it('renders', () => {
const wrapper = shallow(<CollapsedHeader />);
expect(wrapper.snapshot).toMatchSnapshot();
});
it('render nothing if not collapsed', () => {
useIsCollapsed.mockReturnValueOnce(false);
const wrapper = shallow(<CollapsedHeader />);
expect(wrapper.snapshot).toMatchSnapshot();
});
it('renders with isOpen true', () => {
useLearnerDashboardHeaderData.mockReturnValueOnce({
isOpen: true,
toggleIsOpen: jest.fn().mockName('toggleIsOpen'),
});
const wrapper = shallow(<CollapsedHeader />);
expect(wrapper.snapshot).toMatchSnapshot();
});
});

View File

@@ -9,14 +9,14 @@ exports[`ConfirmEmailBanner snapshot Show on unverified 1`] = `
>
<format-message-function
message={
Object {
"defaultMessage": "Remember to confirm your email so that you can keep learning on edX! {confirmNowButton}.",
{
"defaultMessage": "Remember to confirm your email so that you can keep learning! {confirmNowButton}.",
"description": "Text for reminding user to confirm email",
"id": "leanerDashboard.confirmEmailTextReminderBanner",
}
}
values={
Object {
{
"confirmNowButton": <Button
className="confirm-email-now-button"
onClick={[MockFunction openConfirmModalButtonClick]}

View File

@@ -9,7 +9,7 @@ const messages = defineMessages({
confirmEmailTextReminderBanner: {
id: 'leanerDashboard.confirmEmailTextReminderBanner',
description: 'Text for reminding user to confirm email',
defaultMessage: 'Remember to confirm your email so that you can keep learning on edX! {confirmNowButton}.',
defaultMessage: 'Remember to confirm your email so that you can keep learning! {confirmNowButton}.',
},
verifiedConfirmEmailButton: {
id: 'leanerDashboard.verifiedConfirmEmailButton',

View File

@@ -1,78 +0,0 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import { AvatarButton, Dropdown, Badge } from '@openedx/paragon';
import { reduxHooks } from 'hooks';
import messages from '../messages';
export const AuthenticatedUserDropdown = () => {
const { formatMessage } = useIntl();
const { authenticatedUser } = React.useContext(AppContext);
const dashboard = reduxHooks.useEnterpriseDashboardData();
return (
authenticatedUser && (
<Dropdown className="user-dropdown pr4">
<Dropdown.Toggle
as={AvatarButton}
src={authenticatedUser.profileImage}
id="user"
variant="light"
className="p-4"
>
<span data-hj-suppress className="d-md-inline">
{authenticatedUser.username}
</span>
</Dropdown.Toggle>
<Dropdown.Menu className="dropdown-menu-right">
{ getConfig().ENABLE_EDX_PERSONAL_DASHBOARD && (
<>
<Dropdown.Header>{formatMessage(messages.dashboardSwitch)}</Dropdown.Header>
<Dropdown.Item as="a" href="/edx-dashboard" className="active">
{formatMessage(messages.dashboardPersonal)}
</Dropdown.Item>
{!!dashboard && (
<Dropdown.Item as="a" href={dashboard.url} key={dashboard.label}>
{dashboard.label} {formatMessage(messages.dashboard)}
</Dropdown.Item>
)}
<Dropdown.Divider />
</>
)}
{!dashboard && getConfig().CAREER_LINK_URL && (
<Dropdown.Item href={`${getConfig().CAREER_LINK_URL}`}>
{formatMessage(messages.career)}
<Badge className="px-2 mx-2" variant="warning">
{formatMessage(messages.newAlert)}
</Badge>
</Dropdown.Item>
)}
<Dropdown.Item href={`${getConfig().ACCOUNT_PROFILE_URL}/u/${authenticatedUser.username}`}>
{formatMessage(messages.profile)}
</Dropdown.Item>
<Dropdown.Item href={getConfig().ACCOUNT_SETTINGS_URL}>
{formatMessage(messages.account)}
</Dropdown.Item>
{getConfig().ORDER_HISTORY_URL && (
<Dropdown.Item href={getConfig().ORDER_HISTORY_URL}>
{formatMessage(messages.orderHistory)}
</Dropdown.Item>
)}
<Dropdown.Divider />
<Dropdown.Item href={getConfig().LOGOUT_URL}>
{formatMessage(messages.signOut)}
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
)
);
};
AuthenticatedUserDropdown.propTypes = {};
export default AuthenticatedUserDropdown;

View File

@@ -1,81 +0,0 @@
import { shallow } from '@edx/react-unit-test-utils';
import { reduxHooks } from 'hooks';
import { getConfig } from '@edx/frontend-platform';
import { AppContext } from '@edx/frontend-platform/react';
import { AuthenticatedUserDropdown } from './AuthenticatedUserDropdown';
import { useIsCollapsed } from '../hooks';
jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(),
}));
jest.mock('@edx/frontend-platform/react', () => ({
AppContext: {
authenticatedUser: {
profileImage: 'profileImage',
username: 'username',
},
},
}));
const COURSE_SEARCH_URL = 'test-course-search-url';
jest.mock('hooks', () => ({
reduxHooks: {
useEnterpriseDashboardData: jest.fn(),
usePlatformSettingsData: jest.fn(() => ({
courseSearchUrl: COURSE_SEARCH_URL,
})),
},
}));
jest.mock('../hooks', () => ({
useIsCollapsed: jest.fn(),
findCoursesNavDropdownClicked: (href) => jest.fn().mockName(`findCoursesNavDropdownClicked('${href}')`),
}));
jest.mock('data/services/lms/urls', () => ({
baseAppUrl: (url) => (url),
programsUrl: 'http://localhost:18000/dashboard/programs',
}));
const config = {
ACCOUNT_PROFILE_URL: 'http://account-profile-url.test',
ACCOUNT_SETTINGS_URL: 'http://account-settings-url.test',
LOGOUT_URL: 'http://logout-url.test',
ORDER_HISTORY_URL: 'http://order-history-url.test',
SUPPORT_URL: 'http://localhost:18000/support',
CAREER_LINK_URL: 'http://localhost:18000/career',
LMS_BASE_URL: 'http:/localhost:18000',
ENABLE_EDX_PERSONAL_DASHBOARD: true,
};
getConfig.mockReturnValue(config);
describe('AuthenticatedUserDropdown', () => {
const defaultDashboardData = {
label: 'label',
url: 'url',
};
describe('snapshots', () => {
test('no auth render empty', () => {
const { authenticatedUser } = AppContext;
AppContext.authenticatedUser = null;
const wrapper = shallow(<AuthenticatedUserDropdown />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.isEmptyRender()).toBe(true);
AppContext.authenticatedUser = authenticatedUser;
});
test('with enterprise dashboard', () => {
reduxHooks.useEnterpriseDashboardData.mockReturnValueOnce(defaultDashboardData);
useIsCollapsed.mockReturnValueOnce(true);
const wrapper = shallow(<AuthenticatedUserDropdown />);
expect(wrapper.snapshot).toMatchSnapshot();
});
test('without enterprise dashboard and expanded', () => {
reduxHooks.useEnterpriseDashboardData.mockReturnValueOnce(null);
useIsCollapsed.mockReturnValueOnce(false);
const wrapper = shallow(<AuthenticatedUserDropdown />);
expect(wrapper.snapshot).toMatchSnapshot();
});
});
});

View File

@@ -1,139 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AuthenticatedUserDropdown snapshots no auth render empty 1`] = `null`;
exports[`AuthenticatedUserDropdown snapshots with enterprise dashboard 1`] = `
<Dropdown
className="user-dropdown pr4"
>
<Dropdown.Toggle
className="p-4"
id="user"
src="profileImage"
variant="light"
>
<span
className="d-md-inline"
data-hj-suppress={true}
>
username
</span>
</Dropdown.Toggle>
<Dropdown.Menu
className="dropdown-menu-right"
>
<Fragment>
<Dropdown.Header>
SWITCH DASHBOARD
</Dropdown.Header>
<Dropdown.Item
as="a"
className="active"
href="/edx-dashboard"
>
Personal
</Dropdown.Item>
<Dropdown.Item
as="a"
href="url"
key="label"
>
label
Dashboard
</Dropdown.Item>
<Dropdown.Divider />
</Fragment>
<Dropdown.Item
href="http://account-profile-url.test/u/username"
>
Profile
</Dropdown.Item>
<Dropdown.Item
href="http://account-settings-url.test"
>
Account
</Dropdown.Item>
<Dropdown.Item
href="http://order-history-url.test"
>
Order History
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item
href="http://logout-url.test"
>
Sign Out
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
`;
exports[`AuthenticatedUserDropdown snapshots without enterprise dashboard and expanded 1`] = `
<Dropdown
className="user-dropdown pr4"
>
<Dropdown.Toggle
className="p-4"
id="user"
src="profileImage"
variant="light"
>
<span
className="d-md-inline"
data-hj-suppress={true}
>
username
</span>
</Dropdown.Toggle>
<Dropdown.Menu
className="dropdown-menu-right"
>
<Fragment>
<Dropdown.Header>
SWITCH DASHBOARD
</Dropdown.Header>
<Dropdown.Item
as="a"
className="active"
href="/edx-dashboard"
>
Personal
</Dropdown.Item>
<Dropdown.Divider />
</Fragment>
<Dropdown.Item
href="http://localhost:18000/career"
>
Career
<Badge
className="px-2 mx-2"
variant="warning"
>
New
</Badge>
</Dropdown.Item>
<Dropdown.Item
href="http://account-profile-url.test/u/username"
>
Profile
</Dropdown.Item>
<Dropdown.Item
href="http://account-settings-url.test"
>
Account
</Dropdown.Item>
<Dropdown.Item
href="http://order-history-url.test"
>
Order History
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item
href="http://logout-url.test"
>
Sign Out
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
`;

View File

@@ -1,52 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ExpandedHeader render 1`] = `
<header
className="d-flex shadow-sm align-items-center learner-variant-header pl-4"
>
<div
className="flex-grow-1 d-flex align-items-center"
>
<BrandLogo />
<Button
as="a"
className="p-4 course-link"
href="/"
variant="inverse-primary"
>
Courses
</Button>
<Button
as="a"
className="p-4"
href="programsUrl"
variant="inverse-primary"
>
Programs
</Button>
<Button
as="a"
className="p-4"
href="http://localhost:18000/courseSearchUrl"
onClick={[MockFunction findCoursesNavClicked("http://localhost:18000/courseSearchUrl")]}
variant="inverse-primary"
>
Discover New
</Button>
<span
className="flex-grow-1"
/>
<Button
as="a"
className="p-4"
href="http://localhost:18000/support"
variant="inverse-primary"
>
Help
</Button>
</div>
<AuthenticatedUserDropdown />
</header>
`;
exports[`ExpandedHeader render empty if collapsed 1`] = `null`;

View File

@@ -1,76 +0,0 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon';
import urls from 'data/services/lms/urls';
import { reduxHooks } from 'hooks';
import AuthenticatedUserDropdown from './AuthenticatedUserDropdown';
import { useIsCollapsed, findCoursesNavClicked } from '../hooks';
import messages from '../messages';
import BrandLogo from '../BrandLogo';
export const ExpandedHeader = () => {
const { formatMessage } = useIntl();
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
const isCollapsed = useIsCollapsed();
const exploreCoursesClick = findCoursesNavClicked(
urls.baseAppUrl(courseSearchUrl),
);
if (isCollapsed) {
return null;
}
return (
<header className="d-flex shadow-sm align-items-center learner-variant-header pl-4">
<div className="flex-grow-1 d-flex align-items-center">
<BrandLogo />
<Button
as="a"
href="/"
variant="inverse-primary"
className="p-4 course-link"
>
{formatMessage(messages.course)}
</Button>
<Button
as="a"
href={urls.programsUrl()}
variant="inverse-primary"
className="p-4"
>
{formatMessage(messages.program)}
</Button>
<Button
as="a"
href={urls.baseAppUrl(courseSearchUrl)}
variant="inverse-primary"
className="p-4"
onClick={exploreCoursesClick}
>
{formatMessage(messages.discoverNew)}
</Button>
<span className="flex-grow-1" />
<Button
as="a"
href={getConfig().SUPPORT_URL}
variant="inverse-primary"
className="p-4"
>
{formatMessage(messages.help)}
</Button>
</div>
<AuthenticatedUserDropdown />
</header>
);
};
ExpandedHeader.propTypes = {};
export default ExpandedHeader;

View File

@@ -1,41 +0,0 @@
import { shallow } from '@edx/react-unit-test-utils';
import ExpandedHeader from '.';
import { useIsCollapsed } from '../hooks';
jest.mock('data/services/lms/urls', () => ({
programsUrl: () => 'programsUrl',
baseAppUrl: url => (`http://localhost:18000${url}`),
}));
jest.mock('hooks', () => ({
reduxHooks: {
usePlatformSettingsData: () => ({
courseSearchUrl: '/courseSearchUrl',
}),
},
}));
jest.mock('../hooks', () => ({
useIsCollapsed: jest.fn(),
findCoursesNavClicked: (url) => jest.fn().mockName(`findCoursesNavClicked("${url}")`),
}));
jest.mock('./AuthenticatedUserDropdown', () => 'AuthenticatedUserDropdown');
jest.mock('../BrandLogo', () => 'BrandLogo');
describe('ExpandedHeader', () => {
test('render', () => {
useIsCollapsed.mockReturnValueOnce(false);
const wrapper = shallow(<ExpandedHeader />);
expect(wrapper.snapshot).toMatchSnapshot();
});
test('render empty if collapsed', () => {
useIsCollapsed.mockReturnValueOnce(true);
const wrapper = shallow(<ExpandedHeader />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.isEmptyRender()).toBe(true);
});
});

View File

@@ -0,0 +1,76 @@
import { getConfig } from '@edx/frontend-platform';
import urls from 'data/services/lms/urls';
import messages from './messages';
const getLearnerHeaderMenu = (
formatMessage,
courseSearchUrl,
authenticatedUser,
exploreCoursesClick,
) => ({
mainMenu: [
{
type: 'item',
href: '/',
content: formatMessage(messages.course),
isActive: true,
},
...(getConfig().ENABLE_PROGRAMS ? [{
type: 'item',
href: `${urls.programsUrl()}`,
content: formatMessage(messages.program),
}] : []),
{
type: 'item',
href: `${urls.baseAppUrl(courseSearchUrl)}`,
content: formatMessage(messages.discoverNew),
onClick: (e) => {
exploreCoursesClick(e);
},
},
],
secondaryMenu: [
...(getConfig().SUPPORT_URL ? [{
type: 'item',
href: `${getConfig().SUPPORT_URL}`,
content: formatMessage(messages.help),
}] : []),
],
userMenu: [
{
heading: '',
items: [
{
type: 'item',
href: `${getConfig().ACCOUNT_PROFILE_URL}/u/${authenticatedUser?.username}`,
content: formatMessage(messages.profile),
},
{
type: 'item',
href: `${getConfig().ACCOUNT_SETTINGS_URL}`,
content: formatMessage(messages.account),
},
...(getConfig().ORDER_HISTORY_URL ? [{
type: 'item',
href: getConfig().ORDER_HISTORY_URL,
content: formatMessage(messages.orderHistory),
}] : []),
],
},
{
heading: '',
items: [
{
type: 'item',
href: `${getConfig().LOGOUT_URL}`,
content: formatMessage(messages.signOut),
},
],
},
],
}
);
export default getLearnerHeaderMenu;

View File

@@ -3,8 +3,59 @@
exports[`LearnerDashboardHeader render 1`] = `
<Fragment>
<ConfirmEmailBanner />
<CollapsedHeader />
<ExpandedHeader />
<Header
mainMenuItems={
[
{
"content": "Courses",
"href": "/",
"isActive": true,
"type": "item",
},
{
"content": "Discover New",
"href": "http://localhost:18000/course-search-url",
"onClick": [Function],
"type": "item",
},
]
}
secondaryMenuItems={[]}
userMenuItems={
[
{
"heading": "",
"items": [
{
"content": "Profile",
"href": "http://account-profile-url.test/u/undefined",
"type": "item",
},
{
"content": "Account",
"href": "http://account-settings-url.test",
"type": "item",
},
{
"content": "Order History",
"href": "test-url",
"type": "item",
},
],
},
{
"heading": "",
"items": [
{
"content": "Sign Out",
"href": "http://localhost:18000/logout",
"type": "item",
},
],
},
]
}
/>
<MasqueradeBar />
</Fragment>
`;

View File

@@ -1,9 +1,12 @@
import React from 'react';
import { useWindowSize, breakpoints } from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import track from 'tracking';
import { StrictDict } from 'utils';
import { linkNames } from 'tracking/constants';
import getLearnerHeaderMenu from './LearnerDashboardMenu';
import * as module from './hooks';
export const state = StrictDict({
@@ -24,6 +27,13 @@ export const findCoursesNavDropdownClicked = (href) => track.findCourses.findCou
linkName: linkNames.learnerHomeNavDropdownExplore,
});
export const useLearnerDashboardHeaderMenu = ({
courseSearchUrl, authenticatedUser, exploreCoursesClick,
}) => {
const { formatMessage } = useIntl();
return getLearnerHeaderMenu(formatMessage, courseSearchUrl, authenticatedUser, exploreCoursesClick);
};
export const useLearnerDashboardHeaderData = () => {
const [isOpen, setIsOpen] = module.state.isOpen(false);
const toggleIsOpen = () => setIsOpen(!isOpen);
@@ -39,4 +49,5 @@ export default {
findCoursesNavClicked,
findCoursesNavDropdownClicked,
useLearnerDashboardHeaderData,
useLearnerDashboardHeaderMenu,
};

View File

@@ -13,6 +13,7 @@ const {
findCoursesNavClicked,
findCoursesNavDropdownClicked,
useLearnerDashboardHeaderData,
useLearnerDashboardHeaderMenu,
} = hooks;
jest.mock('tracking', () => ({
@@ -48,6 +49,17 @@ describe('LearnerDashboardHeader hooks', () => {
});
});
describe('getLearnerDashboardHeaderMenu', () => {
test('calls header menu data hook', () => {
const courseSearchUrl = '/courses';
const authenticatedUser = {
username: 'test',
};
const learnerHomeHeaderMenu = useLearnerDashboardHeaderMenu({ courseSearchUrl, authenticatedUser });
expect(learnerHomeHeaderMenu.mainMenu.length).toBe(2);
});
});
describe('findCoursesNavDropdownClicked', () => {
test('calls tracking with dropdown link name', () => {
findCoursesNavDropdownClicked(url);

View File

@@ -1,21 +1,43 @@
import React from 'react';
import MasqueradeBar from 'containers/MasqueradeBar';
import { AppContext } from '@edx/frontend-platform/react';
import Header from '@edx/frontend-component-header';
import { reduxHooks } from 'hooks';
import urls from 'data/services/lms/urls';
import ConfirmEmailBanner from './ConfirmEmailBanner';
import CollapsedHeader from './CollapsedHeader';
import ExpandedHeader from './ExpandedHeader';
import { useLearnerDashboardHeaderMenu, findCoursesNavClicked } from './hooks';
import './index.scss';
export const LearnerDashboardHeader = () => (
<>
<ConfirmEmailBanner />
<CollapsedHeader />
<ExpandedHeader />
<MasqueradeBar />
</>
);
export const LearnerDashboardHeader = () => {
const { authenticatedUser } = React.useContext(AppContext);
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
const exploreCoursesClick = () => {
findCoursesNavClicked(urls.baseAppUrl(courseSearchUrl));
};
const learnerHomeHeaderMenu = useLearnerDashboardHeaderMenu({
courseSearchUrl,
authenticatedUser,
exploreCoursesClick,
});
return (
<>
<ConfirmEmailBanner />
<Header
mainMenuItems={learnerHomeHeaderMenu.mainMenu}
secondaryMenuItems={learnerHomeHeaderMenu.secondaryMenu}
userMenuItems={learnerHomeHeaderMenu.userMenu}
/>
<MasqueradeBar />
</>
);
};
LearnerDashboardHeader.propTypes = {};

View File

@@ -1,18 +1,47 @@
import { mergeConfig } from '@edx/frontend-platform';
import { shallow } from '@edx/react-unit-test-utils';
import LearnerDashboardHeader from '.';
import Header from '@edx/frontend-component-header';
import urls from 'data/services/lms/urls';
import LearnerDashboardHeader from '.';
import { findCoursesNavClicked } from './hooks';
jest.mock('hooks', () => ({
reduxHooks: {
usePlatformSettingsData: jest.fn(() => ({
courseSearchUrl: '/course-search-url',
})),
},
}));
jest.mock('./hooks', () => ({
...jest.requireActual('./hooks'),
findCoursesNavClicked: jest.fn(),
}));
jest.mock('containers/MasqueradeBar', () => 'MasqueradeBar');
jest.mock('./CollapsedHeader', () => 'CollapsedHeader');
jest.mock('./ConfirmEmailBanner', () => 'ConfirmEmailBanner');
jest.mock('./ExpandedHeader', () => 'ExpandedHeader');
jest.mock('@edx/frontend-component-header', () => 'Header');
describe('LearnerDashboardHeader', () => {
test('render', () => {
mergeConfig({ ORDER_HISTORY_URL: 'test-url' });
const wrapper = shallow(<LearnerDashboardHeader />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.instance.findByType('ConfirmEmailBanner')).toHaveLength(1);
expect(wrapper.instance.findByType('MasqueradeBar')).toHaveLength(1);
expect(wrapper.instance.findByType('CollapsedHeader')).toHaveLength(1);
expect(wrapper.instance.findByType('ExpandedHeader')).toHaveLength(1);
expect(wrapper.instance.findByType(Header)).toHaveLength(1);
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

@@ -33,7 +33,7 @@ exports[`MasqueradeBar snapshot can masquerade 1`] = `
className="mr-3"
disabled={true}
labels={
Object {
{
"default": "Submit",
}
}
@@ -79,7 +79,7 @@ exports[`MasqueradeBar snapshot can masquerade with input 1`] = `
className="mr-3"
disabled={false}
labels={
Object {
{
"default": "Submit",
}
}
@@ -131,7 +131,7 @@ exports[`MasqueradeBar snapshot is masquerading failed with error 1`] = `
className="mr-3"
disabled={true}
labels={
Object {
{
"default": "Submit",
}
}
@@ -177,7 +177,7 @@ exports[`MasqueradeBar snapshot is masquerading pending 1`] = `
className="mr-3"
disabled={true}
labels={
Object {
{
"default": "Submit",
}
}

View File

@@ -37,8 +37,8 @@ exports[`RelatedProgramsModal snapshot: closed 1`] = `
>
<ProgramCard
data={
Object {
"programData": Object {
{
"programData": {
"dataFor": "program1",
},
"programUrl": "program-1-url",
@@ -53,8 +53,8 @@ exports[`RelatedProgramsModal snapshot: closed 1`] = `
>
<ProgramCard
data={
Object {
"programData": Object {
{
"programData": {
"dataFor": "program2",
},
"programUrl": "program-2-url",
@@ -69,8 +69,8 @@ exports[`RelatedProgramsModal snapshot: closed 1`] = `
>
<ProgramCard
data={
Object {
"programData": Object {
{
"programData": {
"dataFor": "program3",
},
"programUrl": "program-3-url",
@@ -121,8 +121,8 @@ exports[`RelatedProgramsModal snapshot: open 1`] = `
>
<ProgramCard
data={
Object {
"programData": Object {
{
"programData": {
"dataFor": "program1",
},
"programUrl": "program-1-url",
@@ -137,8 +137,8 @@ exports[`RelatedProgramsModal snapshot: open 1`] = `
>
<ProgramCard
data={
Object {
"programData": Object {
{
"programData": {
"dataFor": "program2",
},
"programUrl": "program-2-url",
@@ -153,8 +153,8 @@ exports[`RelatedProgramsModal snapshot: open 1`] = `
>
<ProgramCard
data={
Object {
"programData": Object {
{
"programData": {
"dataFor": "program3",
},
"programUrl": "program-3-url",

View File

@@ -7,7 +7,7 @@ exports[`RelatedProgramsModal ProgramCard snapshot 1`] = `
href="props.data.programUrl"
isClickable={true}
style={
Object {
{
"color": "white",
"width": "18rem",
}

View File

@@ -11,7 +11,7 @@ exports[`UnenrollConfirmModal component snapshot: modalStates.confirm 1`] = `
<div
className="bg-white p-3 rounded shadow"
style={
Object {
{
"textAlign": "start",
}
}
@@ -35,7 +35,7 @@ exports[`UnenrollConfirmModal component snapshot: modalStates.finished, reason g
<div
className="bg-white p-3 rounded shadow"
style={
Object {
{
"textAlign": "start",
}
}
@@ -59,7 +59,7 @@ exports[`UnenrollConfirmModal component snapshot: modalStates.finished, reason s
<div
className="bg-white p-3 rounded shadow"
style={
Object {
{
"textAlign": "start",
}
}
@@ -83,14 +83,14 @@ exports[`UnenrollConfirmModal component snapshot: modalStates.reason, should be
<div
className="bg-white p-3 rounded"
style={
Object {
{
"textAlign": "start",
}
}
>
<ReasonPane
reason={
Object {
{
"isSkipped": false,
"reasonProps": "other",
}

View File

@@ -1,15 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`WidgetSidebar snapshots default 1`] = `
<div
className="widget-sidebar"
>
<div
className="d-flex flex-column"
>
<PluginSlot
id="widget_sidebar_plugin_slot"
/>
</div>
</div>
`;

View File

@@ -1,29 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import hooks from 'widgets/ProductRecommendations/hooks';
export const WidgetSidebar = ({ setSidebarShowing }) => {
const { inRecommendationsVariant, isExperimentActive } = hooks.useShowRecommendationsFooter();
if (!inRecommendationsVariant && isExperimentActive) {
setSidebarShowing(true);
return (
<div className="widget-sidebar">
<div className="d-flex flex-column">
<PluginSlot id="widget_sidebar_plugin_slot" />
</div>
</div>
);
}
return null;
};
WidgetSidebar.propTypes = {
setSidebarShowing: PropTypes.func.isRequired,
};
export default WidgetSidebar;

View File

@@ -1,52 +0,0 @@
import { shallow } from '@edx/react-unit-test-utils';
import hooks from 'widgets/ProductRecommendations/hooks';
import { mockFooterRecommendationsHook } from 'widgets/ProductRecommendations/testData';
import WidgetSidebar from '.';
jest.mock('widgets/LookingForChallengeWidget', () => 'LookingForChallengeWidget');
jest.mock('widgets/ProductRecommendations/hooks', () => ({
useShowRecommendationsFooter: jest.fn(),
}));
jest.mock('@openedx/frontend-plugin-framework', () => ({
PluginSlot: 'PluginSlot',
}));
describe('WidgetSidebar', () => {
beforeEach(() => jest.resetAllMocks());
const props = {
setSidebarShowing: jest.fn(),
};
describe('snapshots', () => {
test('default', () => {
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
mockFooterRecommendationsHook.activeControl,
);
const wrapper = shallow(<WidgetSidebar {...props} />);
expect(props.setSidebarShowing).toHaveBeenCalledWith(true);
expect(wrapper.snapshot).toMatchSnapshot();
});
});
test('is hidden when the has the default values', () => {
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
mockFooterRecommendationsHook.default,
);
const wrapper = shallow(<WidgetSidebar {...props} />);
expect(props.setSidebarShowing).not.toHaveBeenCalled();
expect(wrapper.shallowWrapper).toBeNull();
});
test('is hidden when the has the treatment values', () => {
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
mockFooterRecommendationsHook.activeTreatment,
);
const wrapper = shallow(<WidgetSidebar {...props} />);
expect(props.setSidebarShowing).not.toHaveBeenCalled();
expect(wrapper.shallowWrapper).toBeNull();
});
});

View File

@@ -1,15 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`WidgetSidebar snapshots default 1`] = `
<div
className="widget-sidebar px-2"
>
<div
className="d-flex"
>
<PluginSlot
id="widget_sidebar_plugin_slot"
/>
</div>
</div>
`;

View File

@@ -1,29 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import hooks from 'widgets/ProductRecommendations/hooks';
export const WidgetSidebar = ({ setSidebarShowing }) => {
const { inRecommendationsVariant, isExperimentActive } = hooks.useShowRecommendationsFooter();
if (!inRecommendationsVariant && isExperimentActive) {
setSidebarShowing(true);
return (
<div className="widget-sidebar px-2">
<div className="d-flex">
<PluginSlot id="widget_sidebar_plugin_slot" />
</div>
</div>
);
}
return null;
};
WidgetSidebar.propTypes = {
setSidebarShowing: PropTypes.func.isRequired,
};
export default WidgetSidebar;

View File

@@ -1,52 +0,0 @@
import { shallow } from '@edx/react-unit-test-utils';
import hooks from 'widgets/ProductRecommendations/hooks';
import { mockFooterRecommendationsHook } from 'widgets/ProductRecommendations/testData';
import WidgetSidebar from '.';
jest.mock('widgets/LookingForChallengeWidget', () => 'LookingForChallengeWidget');
jest.mock('widgets/ProductRecommendations/hooks', () => ({
useShowRecommendationsFooter: jest.fn(),
}));
jest.mock('@openedx/frontend-plugin-framework', () => ({
PluginSlot: 'PluginSlot',
}));
describe('WidgetSidebar', () => {
beforeEach(() => jest.resetAllMocks());
const props = {
setSidebarShowing: jest.fn(),
};
describe('snapshots', () => {
test('default', () => {
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
mockFooterRecommendationsHook.activeControl,
);
const wrapper = shallow(<WidgetSidebar {...props} />);
expect(props.setSidebarShowing).toHaveBeenCalledWith(true);
expect(wrapper.snapshot).toMatchSnapshot();
});
});
test('is hidden when the has the default values', () => {
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
mockFooterRecommendationsHook.default,
);
const wrapper = shallow(<WidgetSidebar {...props} />);
expect(props.setSidebarShowing).not.toHaveBeenCalled();
expect(wrapper.shallowWrapper).toBeNull();
});
test('is hidden when the has the treatment values', () => {
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
mockFooterRecommendationsHook.activeTreatment,
);
const wrapper = shallow(<WidgetSidebar {...props} />);
expect(props.setSidebarShowing).not.toHaveBeenCalled();
expect(wrapper.shallowWrapper).toBeNull();
});
});

View File

@@ -1,9 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`WidgetFooter snapshots default 1`] = `
<div
className="widget-footer"
>
<ProductRecommendations />
</div>
`;

View File

@@ -1,21 +0,0 @@
import React from 'react';
import ProductRecommendations from 'widgets/ProductRecommendations';
import hooks from 'widgets/ProductRecommendations/hooks';
export const WidgetFooter = () => {
hooks.useActivateRecommendationsExperiment();
const { inRecommendationsVariant, isExperimentActive } = hooks.useShowRecommendationsFooter();
if (inRecommendationsVariant && isExperimentActive) {
return (
<div className="widget-footer">
<ProductRecommendations />
</div>
);
}
return null;
};
export default WidgetFooter;

View File

@@ -1,45 +0,0 @@
import { shallow } from '@edx/react-unit-test-utils';
import hooks from 'widgets/ProductRecommendations/hooks';
import { mockFooterRecommendationsHook } from 'widgets/ProductRecommendations/testData';
import WidgetFooter from '.';
jest.mock('widgets/LookingForChallengeWidget', () => 'LookingForChallengeWidget');
jest.mock('widgets/ProductRecommendations/hooks', () => ({
useActivateRecommendationsExperiment: jest.fn(),
useShowRecommendationsFooter: jest.fn(),
}));
describe('WidgetFooter', () => {
describe('snapshots', () => {
test('default', () => {
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
mockFooterRecommendationsHook.activeTreatment,
);
const wrapper = shallow(<WidgetFooter />);
expect(hooks.useActivateRecommendationsExperiment).toHaveBeenCalled();
expect(wrapper.snapshot).toMatchSnapshot();
});
});
test('is hidden when the experiment has the default values', () => {
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
mockFooterRecommendationsHook.default,
);
const wrapper = shallow(<WidgetFooter />);
expect(hooks.useActivateRecommendationsExperiment).toHaveBeenCalled();
expect(wrapper.shallowWrapper).toBeNull();
});
test('is hidden when the experiment has the control values', () => {
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
mockFooterRecommendationsHook.activeControl,
);
const wrapper = shallow(<WidgetFooter />);
expect(hooks.useActivateRecommendationsExperiment).toHaveBeenCalled();
expect(wrapper.shallowWrapper).toBeNull();
});
});

View File

@@ -1,6 +1,8 @@
import * as redux from 'redux';
import thunkMiddleware from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction';
import {
composeWithDevToolsLogOnlyInProduction,
} from '@redux-devtools/extension';
import { createLogger } from 'redux-logger';
import apiTestUtils from 'data/services/lms/fakeData/testUtils';
@@ -14,7 +16,7 @@ export const createStore = () => {
const store = redux.createStore(
reducer,
composeWithDevTools(redux.applyMiddleware(...middleware)),
composeWithDevToolsLogOnlyInProduction(redux.applyMiddleware(...middleware)),
);
/**

View File

@@ -1,6 +1,8 @@
import { applyMiddleware } from 'redux';
import thunkMiddleware from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction';
import {
composeWithDevToolsLogOnlyInProduction,
} from '@redux-devtools/extension';
import { createLogger } from 'redux-logger';
import rootReducer, { actions, selectors } from 'data/redux';
@@ -22,8 +24,8 @@ jest.mock('redux', () => ({
applyMiddleware: (...middleware) => ({ applied: middleware }),
createStore: (reducer, middleware) => ({ reducer, middleware }),
}));
jest.mock('redux-devtools-extension/logOnlyInProduction', () => ({
composeWithDevTools: (middleware) => ({ withDevTools: middleware }),
jest.mock('@redux-devtools/extension', () => ({
composeWithDevToolsLogOnlyInProduction: (middleware) => ({ withDevTools: middleware }),
}));
describe('store aggregator module', () => {
@@ -37,7 +39,7 @@ describe('store aggregator module', () => {
describe('middleware', () => {
it('exports thunk and logger middleware, composed and applied with dev tools', () => {
expect(createStore().middleware).toEqual(
composeWithDevTools(applyMiddleware(thunkMiddleware, createLogger())),
composeWithDevToolsLogOnlyInProduction(applyMiddleware(thunkMiddleware, createLogger())),
);
});
});

View File

@@ -0,0 +1,64 @@
# Course Card Action Slot
### Slot ID: `course_card_action_slot`
### Props:
* `cardId`
## Description
This slot is used for adding content in the Action buttons section of each Course Card.
## Example
The following `env.config.jsx` will render the `cardId` of the course as `<p>` elements in a `<div>`.
![Screenshot of Content added after the Sequence Container](./images/post_course_card_action.png)
```js
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
import ActionButton from 'containers/CourseCard/components/CourseCardActions/ActionButton';
const config = {
pluginSlots: {
course_card_action_slot: {
keepDefault: false,
plugins: [
{
// Insert Custom Button in Course Card
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_course_card_action',
priority: 60,
type: DIRECT_PLUGIN,
RenderWidget: ({cardId}) => (
<ActionButton
variant="outline-primary"
>
Custom Button
</ ActionButton>
),
},
},
{
// Insert Another Button in Course Card
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'another_custom_course_card_action',
priority: 70,
type: DIRECT_PLUGIN,
RenderWidget: ({cardId}) => (
<ActionButton
variant="outline-primary"
>
📚: {cardId}
</ ActionButton>
),
},
},
]
}
},
}
export default config;
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 KiB

View File

@@ -0,0 +1,18 @@
import React from 'react';
import PropTypes from 'prop-types';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
const CourseCardActionSlot = ({ cardId }) => (
<PluginSlot
id="course_card_action_slot"
pluginProps={{
cardId,
}}
/>
);
CourseCardActionSlot.propTypes = {
cardId: PropTypes.string.isRequired,
};
export default CourseCardActionSlot;

View File

@@ -0,0 +1,60 @@
# Course List Slot
### Slot ID: `course_list_slot`
## Plugin Props
* courseListData
## Description
This slot is used for replacing or adding content around the `CourseList` component. The `CourseListSlot` is only rendered if the learner has enrolled in at least one course.
## Example
The space will show the `CourseList` component by default. This can be disabled in the configuration with the `keepDefault` boolean.
![Screenshot of the CourseListSlot](./images/course_list_slot.png)
Setting the MFE's `env.config.jsx` to the following will replace the default experience with a list of course titles.
![Screenshot of a custom course list](./images/readme_custom_course_list.png)
```js
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const config = {
pluginSlots: {
course_list_slot: {
// Hide the default CourseList component
keepDefault: false,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_course_list',
type: DIRECT_PLUGIN,
priority: 60,
RenderWidget: ({ courseListData }) => {
// Extract the "visibleList"
const courses = courseListData.visibleList;
// Render a list of course names
return (
<div>
{courses.map(courseData => (
<p>
{courseData.course.courseName}
</p>
))}
</div>
)
},
},
},
],
},
},
}
export default config;
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -0,0 +1,16 @@
import React from 'react';
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 }}>
<CourseList courseListData={courseListData} />
</PluginSlot>
);
CourseListSlot.propTypes = {
courseListData: courseListDataShape,
};
export default CourseListSlot;

View File

@@ -0,0 +1,50 @@
# Footer Slot
### Slot ID: `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/).
## Example
The following `env.config.jsx` will replace the default footer.
![Screenshot of Default Footer](./images/default_footer.png)
with a simple custom footer
![Screenshot of Custom Footer](./images/custom_footer.png)
```jsx
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const config = {
pluginSlots: {
footer_slot: {
plugins: [
{
// Hide the default footer
op: PLUGIN_OPERATIONS.Hide,
widgetId: 'default_contents',
},
{
// Insert a custom footer
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_footer',
type: DIRECT_PLUGIN,
RenderWidget: () => (
<h1 style={{textAlign: 'center'}}>🦶</h1>
),
},
},
]
}
},
}
export default config;
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -0,0 +1,47 @@
# No Courses View Slot
### Slot ID: `no_courses_view_slot`
## Description
This slot is used for replacing or adding content around the `NoCoursesView` component. The `NoCoursesViewSlot` only renders if the learner has not yet enrolled in any courses.
## Example
The space will show the `NoCoursesView` by default. This can be disabled in the configuration with the `keepDefault` boolean.
![Screenshot of the no courses view](./images/no_courses_view_slot.png)
Setting the MFE's `env.config.jsx` to the following will replace the default experience with a custom call-to-action component.
![Screenshot of a custom no courses view](./images/readme_custom_no_courses_view.png)
```js
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const config = {
pluginSlots: {
no_courses_view_slot: {
// Hide the default NoCoursesView component
keepDefault: false,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_no_courses_CTA',
type: DIRECT_PLUGIN,
priority: 60,
RenderWidget: () => (
<h3>
Check out our catalog of courses and start learning today!
</h3>
),
},
},
],
},
},
}
export default config;
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

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