Compare commits

...

96 Commits

Author SHA1 Message Date
Maxim Beder
b5e2a94480 test: update test snapshots 2025-02-14 11:51:25 -08:00
Maxim Beder
edcf2fd756 fix: course image height on IOS Safari
Course thumbnails on IOS Safari stretch to the full height of the image,
instead of being limited by width and preserving aspect ratio. This
seems to be a IOS Safari specific behavior[1].

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

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

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

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

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

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

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

2
.env
View File

@@ -32,7 +32,6 @@ ENTERPRISE_MARKETING_UTM_SOURCE=''
ENTERPRISE_MARKETING_UTM_CAMPAIGN=''
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=''
LEARNING_BASE_URL=''
ZENDESK_KEY=''
HOTJAR_APP_ID=''
HOTJAR_VERSION='6'
HOTJAR_DEBUG=''
@@ -41,3 +40,4 @@ ACCOUNT_PROFILE_URL=''
ENABLE_NOTICES=''
CAREER_LINK_URL=''
ENABLE_EDX_PERSONAL_DASHBOARD=false
ENABLE_PROGRAMS=false

View File

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

View File

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

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,16 @@ on:
jobs:
tests:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- name: Setup Nodejs
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VER }}
node-version-file: '.nvmrc'
- name: Install dependencies
run: npm ci
@@ -39,24 +37,7 @@ jobs:
run: npm run build
- name: Run Coverage
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
- name: Send failure notification
if: ${{ failure() }}
uses: dawidd6/action-send-mail@v3
with:
server_address: email-smtp.us-east-1.amazonaws.com
server_port: 465
username: ${{ secrets.EDX_SMTP_USERNAME }}
password: ${{ secrets.EDX_SMTP_PASSWORD }}
subject: CI workflow failed in ${{github.repository}}
to: masters-grades@edx.org,aperture@2u-internal.opsgenie.net
from: github-actions <github-actions@edx.org>
nodemailerlog: true
nodemailerdebug: true
body: CI workflow in ${{github.repository}} failed!
For details see "github.com/${{ github.repository }}/actions/runs/${{ github.run_id
}}"

View File

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

View File

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

View File

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

2
.nvmrc
View File

@@ -1 +1 @@
18.20
20

View File

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

View File

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

10789
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -17,7 +17,6 @@ import {
} from 'data/redux';
import { reduxHooks } from 'hooks';
import Dashboard from 'containers/Dashboard';
import ZendeskFab from 'components/ZendeskFab';
import track from 'tracking';
@@ -93,7 +92,6 @@ export const App = () => {
</main>
</AppWrapper>
<FooterSlot />
<ZendeskFab />
</div>
</>
);

View File

@@ -17,7 +17,6 @@ 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('containers/WidgetContainers/AppWrapper', () => 'AppWrapper');
jest.mock('data/redux', () => ({
selectors: 'redux.selectors',

View File

@@ -28,7 +28,6 @@ exports[`App router component component initialize failure snapshot 1`] = `
</main>
</AppWrapper>
<FooterSlot />
<ZendeskFab />
</div>
</Fragment>
`;
@@ -55,7 +54,6 @@ exports[`App router component component no network failure snapshot 1`] = `
</main>
</AppWrapper>
<FooterSlot />
<ZendeskFab />
</div>
</Fragment>
`;
@@ -82,7 +80,6 @@ exports[`App router component component no network failure with optimizely proje
</main>
</AppWrapper>
<FooterSlot />
<ZendeskFab />
</div>
</Fragment>
`;
@@ -109,7 +106,6 @@ exports[`App router component component no network failure with optimizely url s
</main>
</AppWrapper>
<FooterSlot />
<ZendeskFab />
</div>
</Fragment>
`;
@@ -142,7 +138,6 @@ exports[`App router component component refresh failure snapshot 1`] = `
</main>
</AppWrapper>
<FooterSlot />
<ZendeskFab />
</div>
</Fragment>
`;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,7 +2,7 @@ import { shallow } from '@edx/react-unit-test-utils';
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';
@@ -19,7 +19,7 @@ jest.mock('hooks', () => ({
},
}));
jest.mock('./UpgradeButton', () => 'UpgradeButton');
jest.mock('plugin-slots/CourseCardActionSlot', () => 'CustomActionButton');
jest.mock('./SelectSessionButton', () => 'SelectSessionButton');
jest.mock('./ViewCourseButton', () => 'ViewCourseButton');
jest.mock('./BeginCourseButton', () => 'BeginCourseButton');
@@ -57,19 +57,7 @@ describe('CourseCardActions', () => {
});
});
describe('output', () => {
describe('Exec Ed course', () => {
it('does not render upgrade button', () => {
mockHooks({ isExecEd2UCourse: true });
render();
expect(el.instance.findByType(UpgradeButton).length).toEqual(0);
});
});
describe('entitlement course', () => {
it('does not render upgrade button', () => {
mockHooks({ isEntitlement: true });
render();
expect(el.instance.findByType(UpgradeButton).length).toEqual(0);
});
it('renders ViewCourseButton if fulfilled', () => {
mockHooks({ isEntitlement: true, isFulfilled: true });
render();
@@ -81,33 +69,26 @@ describe('CourseCardActions', () => {
expect(el.instance.findByType(SelectSessionButton)[0].props.cardId).toEqual(cardId);
});
});
describe('verified course', () => {
it('does not render upgrade button', () => {
mockHooks({ isVerified: true });
render();
expect(el.instance.findByType(UpgradeButton).length).toEqual(0);
});
});
describe('not entielement, verified, or exec ed', () => {
it('renders UpgradeButton and ViewCourseButton for archived courses', () => {
describe('not entitlement, verified, or exec ed', () => {
it('renders CourseCardActionSlot 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', () => {
it('renders CourseCardActionSlot 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);
});
});
describe('active courses (started, and not archived)', () => {
it('renders UpgradeButton and ResumeButton', () => {
it('renders CourseCardActionSlot and ResumeButton', () => {
mockHooks({ hasStarted: true });
render();
expect(el.instance.findByType(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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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={{}}
numPages={1}
setPageNumber={[MockFunction setPageNumber]}
showFilters={false}
visibleList={[]}
/>
</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,7 @@ import PropTypes from 'prop-types';
import { Container, Col, Row } from '@openedx/paragon';
import WidgetSidebar from '../WidgetContainers/WidgetSidebar';
import WidgetSidebarSlot from 'plugin-slots/WidgetSidebarSlot';
import hooks from './hooks';
@@ -42,7 +42,7 @@ export const DashboardLayout = ({ children }) => {
</Col>
<Col {...columnConfig.sidebar} className="sidebar-column">
{!isCollapsed && (<h2 className="course-list-title">&nbsp;</h2>)}
<WidgetSidebar />
<WidgetSidebarSlot />
</Col>
</Row>
</Container>

View File

@@ -36,9 +36,9 @@ describe('DashboardLayout', () => {
const columns = el.instance.findByType(Row)[0].findByType(Col);
expect(columns[0].children).not.toHaveLength(0);
});
it('displays WidgetSidebar in second column', () => {
it('displays WidgetSidebarSlot in second column', () => {
const columns = el.instance.findByType(Row)[0].findByType(Col);
expect(columns[1].findByType('WidgetSidebar')).toHaveLength(1);
expect(columns[1].findByType('WidgetSidebarSlot')).toHaveLength(1);
});
};
const testSidebarLayout = () => {

View File

@@ -38,7 +38,7 @@ exports[`DashboardLayout collapsed sidebar not showing snapshot 1`] = `
}
}
>
<WidgetSidebar />
<WidgetSidebarSlot />
</Col>
</Row>
</Container>
@@ -82,7 +82,7 @@ exports[`DashboardLayout collapsed sidebar showing snapshot 1`] = `
}
}
>
<WidgetSidebar />
<WidgetSidebarSlot />
</Col>
</Row>
</Container>
@@ -131,7 +131,7 @@ exports[`DashboardLayout not collapsed sidebar not showing snapshot 1`] = `
>
 
</h2>
<WidgetSidebar />
<WidgetSidebarSlot />
</Col>
</Row>
</Container>
@@ -180,7 +180,7 @@ exports[`DashboardLayout not collapsed sidebar showing snapshot 1`] = `
>
 
</h2>
<WidgetSidebar />
<WidgetSidebarSlot />
</Col>
</Row>
</Container>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,23 +0,0 @@
import React from 'react';
import classNames from 'classnames';
import { reduxHooks } from 'hooks';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
// eslint-disable-next-line arrow-body-style
export const WidgetSidebar = () => {
const hasCourses = reduxHooks.useHasCourses();
const widgetSidebarClassNames = classNames('widget-sidebar', { 'px-2': !hasCourses });
const innerWrapperClassNames = classNames('d-flex', { 'flex-column': hasCourses });
return (
<div className={widgetSidebarClassNames}>
<div className={innerWrapperClassNames}>
<PluginSlot id="widget_sidebar_plugin_slot" />
</div>
</div>
);
};
export default WidgetSidebar;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

View File

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

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

View File

@@ -0,0 +1,12 @@
import React from 'react';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import NoCoursesView from 'containers/CoursesPanel/NoCoursesView';
export const NoCoursesViewSlot = () => (
<PluginSlot id="no_courses_view_slot">
<NoCoursesView />
</PluginSlot>
);
export default NoCoursesViewSlot;

View File

@@ -1,3 +1,7 @@
# `frontend-app-learner-dashboard` Plugin Slots
* [`course_card_action_slot`](./CourseCardActionSlot/)
* [`footer_slot`](./FooterSlot/)
* [`widget_sidebar_slot`](./WidgetSidebarSlot/)
* [`course_list_slot`](./CourseListSlot/)
* [`no_courses_view_slot`](./NoCoursesViewSlot/)

View File

@@ -0,0 +1,58 @@
# Widget Sidebar Slot
### Slot ID: `widget_sidebar_slot`
## Description
This slot is used for adding content to the right-hand sidebar.
## Example
The space will show the `LookingForChallengeWidget` by default. This can be disabled in the configuration with the `keepDefault` boolean.
![Screenshot of the widget sidebar](./images/widget_sidebar_slot.png)
Setting the MFE's `env.config.jsx` to the following will replace the default experience with a custom sidebar component.
![Screenshot of a custom call-to-action in the sidebar](./images/readme_custom_sidebar.png)
```js
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const config = {
pluginSlots: {
widget_sidebar_slot: {
// Hide the default LookingForChallenge component
keepDefault: false,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_sidebar_panel',
type: DIRECT_PLUGIN,
priority: 60,
RenderWidget: () => (
<div>
<h3>
Sidebar Menu
</h3>
<p>
sidebar item #1
</p>
<p>
sidebar item #2
</p>
<p>
sidebar item #3
</p>
</div>
),
},
},
],
},
},
}
export default config;
```

View File

@@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`WidgetSidebar snapshots 1`] = `
<PluginSlot
id="widget_sidebar_slot"
>
<LookingForChallengeWidget />
</PluginSlot>
`;

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

View File

@@ -0,0 +1,13 @@
import React from 'react';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import LookingForChallengeWidget from 'widgets/LookingForChallengeWidget';
// eslint-disable-next-line arrow-body-style
export const WidgetSidebarSlot = () => (
<PluginSlot id="widget_sidebar_slot">
<LookingForChallengeWidget />
</PluginSlot>
);
export default WidgetSidebarSlot;

View File

@@ -1,6 +1,6 @@
import { shallow } from '@edx/react-unit-test-utils';
import WidgetSidebar from '.';
import WidgetSidebarSlot from '.';
jest.mock('widgets/LookingForChallengeWidget', () => 'LookingForChallengeWidget');
@@ -12,7 +12,7 @@ describe('WidgetSidebar', () => {
beforeEach(() => jest.resetAllMocks());
test('snapshots', () => {
const wrapper = shallow(<WidgetSidebar />);
const wrapper = shallow(<WidgetSidebarSlot />);
expect(wrapper.snapshot).toMatchSnapshot();
});
});

View File

@@ -9,7 +9,6 @@ import {
within,
prettyDOM,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import {
initialize,
mergeConfig,
@@ -42,7 +41,7 @@ jest.unmock('react-redux');
jest.unmock('reselect');
jest.unmock('hooks');
jest.mock('containers/WidgetContainers/WidgetSidebar', () => jest.fn(() => 'widget-sidebar'));
jest.mock('plugin-slots/WidgetSidebarSlot', () => jest.fn(() => 'widget-sidebar'));
jest.mock('components/NoticesWrapper', () => 'notices-wrapper');
jest.mock('@edx/frontend-platform', () => ({

View File

@@ -2,7 +2,6 @@ import { StrictDict } from 'utils';
export const categories = StrictDict({
dashboard: 'dashboard',
upgrade: 'upgrade',
userEngagement: 'user-engagement',
searchButton: 'search_button',
credit: 'credit',
@@ -14,9 +13,6 @@ export const events = StrictDict({
courseImageClicked: 'courseImageClicked',
courseTitleClicked: 'courseTitleClicked',
courseOptionsDropdownClicked: 'courseOptionsDropdownClicked',
upgradeButtonClicked: 'upgradeButtonClicked',
upgradeButtonClickedEnrollment: 'upgradeButtonClickedEnrollment',
upgradeButtonClickedUpsell: 'upgradeButtonClickedUpsell',
shareClicked: 'shareClicked',
userSettingsChanged: 'userSettingsChanged',
newSession: 'newSession',
@@ -36,9 +32,6 @@ export const eventNames = StrictDict({
courseImageClicked: 'edx.bi.dashboard.course_image.clicked',
courseTitleClicked: 'edx.bi.dashboard.course_title.clicked',
courseOptionsDropdownClicked: 'edx.bi.dashboard.course_options_dropdown.clicked',
upgradeButtonClicked: 'edx.bi.dashboard.upgrade_button.clicked',
upgradeButtonClickedEnrollment: 'edx.course.enrollment.upgrade.clicked',
upgradeButtonClickedUpsell: 'edx.bi.ecommerce.upsell_links_clicked',
shareClicked: 'edx.course.share_clicked',
userSettingsChanged: 'edx.user.settings.changed',
newSession: 'course-dashboard.new-session',

View File

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

View File

@@ -1,13 +1,8 @@
import { keyStore } from 'utils';
import api from 'data/services/lms/api';
import { createEventTracker, createLinkTracker } from 'data/services/segment/utils';
import { categories, eventNames } from '../constants';
import * as trackers from './course';
jest.mock('data/services/lms/api', () => ({
logUpgrade: jest.fn(),
}));
jest.mock('data/services/segment/utils', () => ({
createEventTracker: jest.fn(args => ({ createEventTracker: args })),
createLinkTracker: jest.fn((cb, href) => ({ createLinkTracker: { cb, href } })),
@@ -44,26 +39,6 @@ describe('course trackers', () => {
});
});
});
describe('Upgrade Events', () => {
describe('upgradeButtonClicked', () => {
it('creates an event tracker for upgradeButtonClicked event with category and label', () => {
expect(trackers.upgradeButtonClicked(courseId)).toEqual(createEventTracker(
eventNames.upgradeButtonClicked,
{ category: categories.upgrade, label: courseId },
));
});
});
describe('upgradeButtonClickedUpsell', () => {
it('creates an event tracker for upgradeButtonClickedUpsell eventwith upsellOptions', () => {
expect(trackers.upgradeButtonClickedUpsell(courseId)).toEqual(
createEventTracker(
eventNames.upgradeButtonClickedUpsell,
{ ...trackers.upsellOptions, courseId },
),
);
});
});
});
describe('Non-link events', () => {
describe('courseOptionsDropdownClicked', () => {
it('creates course event tracker for courseOptionsDropdownClicked event', () => {
@@ -101,25 +76,5 @@ describe('course trackers', () => {
);
});
});
describe('upgradeClicked', () => {
it('triggers upgrade actions and api.logUpgrade with courseId', () => {
const upgradeButtonClicked = jest.fn();
const upgradeButtonClickedUpsell = jest.fn();
const trackUpgradeButtonClicked = jest.fn(() => upgradeButtonClicked);
const trackUpgradeButtonClickedUpsell = jest.fn(() => upgradeButtonClickedUpsell);
jest.spyOn(trackers, moduleKeys.upgradeButtonClicked)
.mockImplementationOnce(trackUpgradeButtonClicked);
jest.spyOn(trackers, moduleKeys.upgradeButtonClickedUpsell)
.mockImplementationOnce(trackUpgradeButtonClickedUpsell);
const out = trackers.upgradeClicked(courseId, href).createLinkTracker;
expect(out.href).toEqual(href);
out.cb();
expect(trackUpgradeButtonClicked).toHaveBeenCalledWith(courseId);
expect(trackUpgradeButtonClickedUpsell).toHaveBeenCalledWith(courseId);
expect(upgradeButtonClicked).toHaveBeenCalledWith();
expect(upgradeButtonClickedUpsell).toHaveBeenCalledWith();
expect(api.logUpgrade).toHaveBeenCalledWith({ courseId });
});
});
});
});

View File

@@ -8,7 +8,7 @@ import { reduxHooks } from 'hooks';
import moreCoursesSVG from 'assets/more-courses-sidewidget.svg';
import { baseAppUrl } from 'data/services/lms/urls';
import track from '../RecommendationsPanel/track';
import { findCoursesWidgetClicked } from './track';
import messages from './messages';
import './index.scss';
@@ -17,6 +17,8 @@ export const arrowIcon = (<Icon className="mx-1" src={ArrowForward} />);
export const LookingForChallengeWidget = () => {
const { formatMessage } = useIntl();
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
const hyperlinkDestination = baseAppUrl(courseSearchUrl) || '';
return (
<Card orientation="horizontal" id="looking-for-challenge-widget">
<Card.ImageCap
@@ -30,8 +32,8 @@ export const LookingForChallengeWidget = () => {
<h5>
<Hyperlink
variant="brand"
destination={baseAppUrl(courseSearchUrl)}
onClick={track.findCoursesWidgetClicked(baseAppUrl(courseSearchUrl))}
destination={hyperlinkDestination}
onClick={findCoursesWidgetClicked(hyperlinkDestination)}
className="d-flex align-items-center"
>
{formatMessage(messages.findCoursesButton, { arrow: arrowIcon })}

View File

@@ -10,7 +10,7 @@ jest.mock('hooks', () => ({
},
}));
jest.mock('../RecommendationsPanel/track', () => ({
jest.mock('./track', () => ({
findCoursesWidgetClicked: (href) => jest.fn().mockName(`track.findCoursesWidgetClicked('${href}')`),
}));

View File

@@ -0,0 +1,15 @@
import { StrictDict } from 'utils';
import track from 'tracking';
export const linkNames = StrictDict({
findCoursesWidget: 'learner_home_widget_explore',
});
export const findCoursesWidgetClicked = (href) => track.findCourses.findCoursesClicked(href, {
linkName: linkNames.findCoursesWidget,
});
export default {
linkNames,
findCoursesWidgetClicked,
};

View File

@@ -1,66 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon';
import { Search } from '@openedx/paragon/icons';
import { baseAppUrl } from 'data/services/lms/urls';
import { reduxHooks } from 'hooks';
import track from './track';
import CourseCard from './components/CourseCard';
import messages from './messages';
import './index.scss';
export const LoadedView = ({
courses,
isControl,
}) => {
const { formatMessage } = useIntl();
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
return (
<div className="p-4 w-100 panel-background">
<h3 className="pb-2">{isControl === false
? formatMessage(messages.recommendationsHeading) : formatMessage(messages.popularCoursesHeading)}
</h3>
<div>
{courses.map((course) => (
<CourseCard
key={course.courseKey}
course={course}
isControl={isControl}
/>
))}
</div>
<div className="text-center explore-courses-btn">
<Button
variant="tertiary"
iconBefore={Search}
as="a"
href={baseAppUrl(courseSearchUrl)}
onClick={track.findCoursesWidgetClicked(baseAppUrl(courseSearchUrl))}
>
{formatMessage(messages.exploreCoursesButton)}
</Button>
</div>
</div>
);
};
LoadedView.defaultProps = {
isControl: true,
};
LoadedView.propTypes = {
courses: PropTypes.arrayOf(PropTypes.shape({
courseKey: PropTypes.string,
title: PropTypes.string,
logoImageUrl: PropTypes.string,
marketingUrl: PropTypes.string,
})).isRequired,
isControl: PropTypes.oneOf([true, false, null]),
};
export default LoadedView;

View File

@@ -1,42 +0,0 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import LoadedView from './LoadedView';
import mockData from './mockData';
import messages from './messages';
jest.mock('hooks', () => ({
reduxHooks: {
usePlatformSettingsData: () => ({
courseSearchUrl: '/course-search-url',
}),
},
}));
jest.mock('data/services/lms/urls', () => ({
baseAppUrl: (url) => (`http://localhost:18000${url}`),
}));
jest.mock('./track', () => ({
findCoursesWidgetClicked: (href) => jest.fn().mockName(`track.findCoursesWidgetClicked('${href}')`),
}));
jest.mock('./components/CourseCard', () => 'CourseCard');
describe('RecommendationsPanel LoadedView', () => {
const props = {
courses: mockData.courses,
isControl: null,
};
describe('RecommendationPanelLoadedView', () => {
test('without personalize recommendation', () => {
const el = shallow(<LoadedView {...props} />);
expect(el.snapshot).toMatchSnapshot();
expect(el.instance.findByType('h3')[0].children[0].el).toEqual(messages.popularCoursesHeading.defaultMessage);
});
test('with personalize recommendation', () => {
const el = shallow(<LoadedView {...props} isControl={false} />);
expect(el.snapshot).toMatchSnapshot();
expect(el.instance.findByType('h3')[0].children[0].el).toEqual(messages.recommendationsHeading.defaultMessage);
});
});
});

View File

@@ -1,19 +0,0 @@
import React from 'react';
import { Spinner } from '@openedx/paragon';
import { useDashboardMessages } from 'containers/Dashboard/hooks';
export const LoadingView = () => {
const { spinnerScreenReaderText } = useDashboardMessages();
return (
<div className="recommendations-loading w-100">
<Spinner
animation="border"
variant="light"
screenReaderText={spinnerScreenReaderText}
/>
</div>
);
};
export default LoadingView;

View File

@@ -1,19 +0,0 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { useDashboardMessages } from 'containers/Dashboard/hooks';
import LoadingView from './LoadingView';
jest.mock('./components/CourseCard', () => 'CourseCard');
jest.mock('containers/Dashboard/hooks', () => ({
useDashboardMessages: jest.fn(),
}));
const spinnerScreenReaderText = 'test-spinner-screen-reader-text';
useDashboardMessages.mockReturnValue(spinnerScreenReaderText);
describe('RecommendationsPanel LoadingView', () => {
test('snapshot', () => {
expect(shallow(<LoadingView />).snapshot).toMatchSnapshot();
});
});

View File

@@ -1,151 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RecommendationsPanel LoadedView RecommendationPanelLoadedView with personalize recommendation 1`] = `
<div
className="p-4 w-100 panel-background"
>
<h3
className="pb-2"
>
Recommendations for you
</h3>
<div>
<CourseCard
course={
{
"courseKey": "cs-1",
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
"marketingUrl": "www.edx/recommended-course",
"title": "Recommended course 1",
}
}
isControl={false}
key="cs-1"
/>
<CourseCard
course={
{
"courseKey": "cs-2",
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
"marketingUrl": "www.edx/recommended-course",
"title": "Recommended course 2 with a really really really long name for some reason",
}
}
isControl={false}
key="cs-2"
/>
<CourseCard
course={
{
"courseKey": "cs-3",
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
"marketingUrl": "www.edx/recommended-course",
"title": "Recommended course 3",
}
}
isControl={false}
key="cs-3"
/>
<CourseCard
course={
{
"courseKey": "cs-4",
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
"marketingUrl": "www.edx/recommended-course",
"title": "Recommended course 4",
}
}
isControl={false}
key="cs-4"
/>
</div>
<div
className="text-center explore-courses-btn"
>
<Button
as="a"
href="http://localhost:18000/course-search-url"
iconBefore={[MockFunction icons.Search]}
onClick={[MockFunction track.findCoursesWidgetClicked('http://localhost:18000/course-search-url')]}
variant="tertiary"
>
Explore courses
</Button>
</div>
</div>
`;
exports[`RecommendationsPanel LoadedView RecommendationPanelLoadedView without personalize recommendation 1`] = `
<div
className="p-4 w-100 panel-background"
>
<h3
className="pb-2"
>
Popular courses
</h3>
<div>
<CourseCard
course={
{
"courseKey": "cs-1",
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
"marketingUrl": "www.edx/recommended-course",
"title": "Recommended course 1",
}
}
isControl={null}
key="cs-1"
/>
<CourseCard
course={
{
"courseKey": "cs-2",
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
"marketingUrl": "www.edx/recommended-course",
"title": "Recommended course 2 with a really really really long name for some reason",
}
}
isControl={null}
key="cs-2"
/>
<CourseCard
course={
{
"courseKey": "cs-3",
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
"marketingUrl": "www.edx/recommended-course",
"title": "Recommended course 3",
}
}
isControl={null}
key="cs-3"
/>
<CourseCard
course={
{
"courseKey": "cs-4",
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
"marketingUrl": "www.edx/recommended-course",
"title": "Recommended course 4",
}
}
isControl={null}
key="cs-4"
/>
</div>
<div
className="text-center explore-courses-btn"
>
<Button
as="a"
href="http://localhost:18000/course-search-url"
iconBefore={[MockFunction icons.Search]}
onClick={[MockFunction track.findCoursesWidgetClicked('http://localhost:18000/course-search-url')]}
variant="tertiary"
>
Explore courses
</Button>
</div>
</div>
`;

View File

@@ -1,12 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RecommendationsPanel LoadingView snapshot 1`] = `
<div
className="recommendations-loading w-100"
>
<Spinner
animation="border"
variant="light"
/>
</div>
`;

View File

@@ -1,12 +0,0 @@
import { StrictDict } from 'utils';
import { get, stringifyUrl } from 'data/services/lms/utils';
import urls from 'data/services/lms/urls';
export const getFetchUrl = () => (`${urls.getApiUrl()}/edx_recommendations/learner_dashboard/course_recommendations/`);
export const apiKeys = StrictDict({ user: 'user' });
const fetchRecommendedCourses = () => get(stringifyUrl(getFetchUrl()));
export default {
fetchRecommendedCourses,
};

View File

@@ -1,17 +0,0 @@
import { get, stringifyUrl } from 'data/services/lms/utils';
import api, { getFetchUrl } from './api';
jest.mock('data/services/lms/utils', () => ({
stringifyUrl: (...args) => ({ stringifyUrl: args }),
get: (...args) => ({ get: args }),
}));
describe('recommendedCourses api', () => {
describe('fetchRecommendedCourses', () => {
it('calls get with the correct recommendation courses URL and user', () => {
expect(api.fetchRecommendedCourses()).toEqual(
get(stringifyUrl(getFetchUrl())),
);
});
});
});

View File

@@ -1,51 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Card, Hyperlink, Truncate } from '@openedx/paragon';
import { useIsCollapsed } from 'containers/CourseCard/hooks';
import useCourseCardData from './hooks';
import './index.scss';
export const CourseCard = ({ course, isControl }) => {
const isCollapsed = useIsCollapsed();
const { handleCourseClick } = useCourseCardData(course, isControl);
return (
<Hyperlink
destination={course?.marketingUrl}
className="card-link"
onClick={handleCourseClick}
>
<Card orientation={isCollapsed ? 'vertical' : 'horizontal'} className="p-3 mb-1 recommended-course-card">
<div className={isCollapsed ? '' : 'd-flex align-items-center'}>
<Card.ImageCap
src={course.logoImageUrl}
srcAlt={course.title}
/>
<Card.Body className="d-flex align-items-center">
<Card.Section className={isCollapsed ? 'pt-3' : 'pl-3'}>
<h4 className="text-info-500">
<Truncate lines={3}>
{course.title}
</Truncate>
</h4>
</Card.Section>
</Card.Body>
</div>
</Card>
</Hyperlink>
);
};
CourseCard.propTypes = {
course: PropTypes.shape({
courseKey: PropTypes.string,
title: PropTypes.string,
logoImageUrl: PropTypes.string,
marketingUrl: PropTypes.string,
}).isRequired,
isControl: PropTypes.oneOf([true, false, null]).isRequired,
};
export default CourseCard;

View File

@@ -1,17 +0,0 @@
import track from '../track';
import './index.scss';
export const useCourseCardData = (course, isControl) => {
const handleCourseClick = (e) => {
e.preventDefault();
track.recommendedCourseClicked(
course.courseKey,
isControl,
course?.marketingUrl,
)(e);
};
return { handleCourseClick };
};
export default useCourseCardData;

View File

@@ -1,33 +0,0 @@
@import "@openedx/paragon/scss/core/core";
.card-link{
display: block !important;
margin: 0.5rem 0 0.5rem 0 !important;
}
.recommended-course-card {
margin: 0.5rem 0 0.5rem 0 !important;
.pgn__card-wrapper-image-cap {
width: 7.188rem !important;
max-width: 7.188rem !important;
min-width: 7.188rem !important;
height: 4.125rem !important;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15), 0 1px 4px rgba(0, 0, 0, 0.15);
border-radius: 4px;
padding: 0.5rem;
.pgn__card-image-cap {
max-width: 100% !important;
max-height: 100% !important;
}
}
.pgn__card-section {
padding: 0 !important;
}
margin-top: 0.313rem;
}
.text-info-500 {
margin: 0 !important;
}

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