Compare commits

..

63 Commits

Author SHA1 Message Date
Deimer Morales
2b1c56e850 fix: include frontend component header translation (#793) [Ulmo backport] (#800) 2026-02-19 13:46:43 -05:00
dependabot[bot]
0db621b134 chore(deps): bump actions/setup-node from 5 to 6 (#737) 2025-10-23 13:58:00 -04:00
renovate[bot]
aaf2e36fe9 chore(deps): update dependency @reduxjs/toolkit to v2.9.1 (#736)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 15:56:50 +00:00
renovate[bot]
b527fbcfba chore(deps): update dependency @openedx/paragon to v23.14.9 (#735)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 05:13:03 +00:00
Feanil Patel
fa3f7b27cf fix: Run npm audit fix to update dependencies. (#734) 2025-10-17 13:23:06 -04:00
Feanil Patel
3b0c58f376 fix: Run npm audit fix to update dependencies. 2025-10-15 10:10:56 -04:00
renovate[bot]
f903392ca1 fix(deps): update dependency core-js to v3.46.0 (#733)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-13 08:39:42 +00:00
renovate[bot]
89032f09f4 chore(deps): update dependency @openedx/paragon to v23.14.8 (#732)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-13 04:46:26 +00:00
renovate[bot]
03f146cce9 chore(deps): update dependency @testing-library/jest-dom to v6.9.1 (#728)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-06 06:04:32 +00:00
renovate[bot]
7dd2bda86e chore(deps): update dependency @openedx/paragon to v23.14.4 (#724)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-02 02:33:18 -04:00
Feanil Patel
410f2f730f build: remove unused reactifex packages
Remove reactifex and/or @edx/reactifex packages from devDependencies
as they are no longer needed. Translation extraction functionality has
been verified to work correctly without these dependencies.

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-26 14:07:40 -03:00
PKulkoRaccoonGang
de13749443 test: Remove support for Node 20 2025-09-25 09:59:23 -03:00
dependabot[bot]
0e66b2031d chore(deps): bump actions/setup-node from 4 to 5 (#715)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-24 16:23:41 -04:00
Peter Kulko
697c9ff2c8 build: Upgrade to Node 24 (#702) 2025-09-24 16:15:55 -04:00
renovate[bot]
fdb5d2f68c fix(deps): update dependency @reduxjs/toolkit to v2.9.0 (#722)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 09:47:27 +00:00
renovate[bot]
7cf79f8931 chore(deps): update dependency @testing-library/jest-dom to v6.8.0 (#721)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 04:57:59 +00:00
Peter Kulko
73b7b7f5d0 test: Add Node 24 to CI matrix (#701) 2025-09-16 09:36:20 -04:00
renovate[bot]
516544c7cc fix(deps): update dependency @edx/frontend-platform to v8.5.1 (#719)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 21:06:02 +00:00
renovate[bot]
2134df5478 fix(deps): update dependency @edx/frontend-component-footer to v14.9.2 (#718)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 05:49:50 +00:00
renovate[bot]
5345d2eef2 fix(deps): update dependency @edx/frontend-component-header to v6.6.1 (#714)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 10:42:20 +00:00
renovate[bot]
d94ac8dd62 fix(deps): update dependency @edx/frontend-component-footer to v14.9.1 (#713)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 04:33:27 +00:00
Mubbshar Anwar
ff925b06f1 mubbsharanwar/unenrollment process improvement (#704)
Co-authored-by: Deborah Kaplan <deborahgu@users.noreply.github.com>
2025-09-03 14:39:49 -04:00
Samuel Allan
2696486e5b fix: update frontend-build to fix install issues (#712) 2025-09-02 13:23:36 -04:00
renovate[bot]
5854b00d08 fix(deps): update dependency core-js to v3.45.1 (#711)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 09:46:49 +00:00
renovate[bot]
13e9b1a85f fix(deps): update dependency @fortawesome/react-fontawesome to v0.2.6 (#710)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 04:58:44 +00:00
Maxwell Frank
8116956a4b feat: remove widgets in favor of plugins (#708) 2025-08-20 11:35:58 -04:00
Maxwell Frank
8d23e7585b chore: upgrade frontend-component-header 6.6.0 (#709) 2025-08-19 19:43:37 -04:00
Maxwell Frank
b63a40006e feat: add tsconfig.json (#707) 2025-08-19 17:24:18 -04:00
renovate[bot]
92243063b9 fix(deps): update dependency @openedx/paragon to v23.14.2 (#706)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-18 08:58:22 +00:00
renovate[bot]
a2673399aa chore(deps): update dependency copy-webpack-plugin to v13.0.1 (#705)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-18 05:46:31 +00:00
dependabot[bot]
cedcb4172d chore(deps): bump actions/checkout from 4 to 5 (#700)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-14 13:44:21 -04:00
renovate[bot]
9e4faf1569 fix(deps): update dependency @edx/frontend-platform to v8.5.0 (#698)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-11 08:59:39 +00:00
renovate[bot]
3a9acc981b fix(deps): update dependency core-js to v3.45.0 (#699)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-11 05:22:27 +00:00
renovate[bot]
e3c18698d8 fix(deps): update dependency @openedx/paragon to v23.14.1 (#695)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-04 05:45:04 +00:00
Kyle McCormick
eb38beedc3 chore: Delete CODEOWNERS (#691) 2025-07-31 16:08:12 -04:00
renovate[bot]
01faf5a30a fix(deps): update dependency @fortawesome/react-fontawesome to v0.2.3 (#690)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-28 12:15:42 +00:00
renovate[bot]
f4807614e2 chore(deps): update dependency @testing-library/jest-dom to v6.6.4 (#689)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-28 06:51:58 +00:00
renovate[bot]
ab448e52f2 chore(deps): update dependency copy-webpack-plugin to v13 (#688)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-21 12:09:13 -04:00
renovate[bot]
3383016176 fix(deps): update dependency @edx/frontend-component-header to v6.4.2 (#687)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-21 04:43:49 +00:00
renovate[bot]
054cd57d4b fix(deps): update dependency react-router-dom to v6.30.1 (#686)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-14 10:34:49 +00:00
renovate[bot]
0fc3cc4d53 fix(deps): update dependency core-js to v3.44.0 (#685)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-14 06:17:55 +00:00
renovate[bot]
e5c1244e59 fix(deps): update dependency @openedx/paragon to v23.14.0 (#683)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 14:10:26 +00:00
renovate[bot]
3a389e14e1 fix(deps): update dependency @edx/frontend-component-header to v6.4.1 (#682)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 08:00:29 +00:00
Diana Villalvazo
6de409d7cc test: Deprecate react-unit-test-utils 15/15 (#680) 2025-07-02 12:23:55 -04:00
Diana Villalvazo
517b8424b3 test: Deprecate react-unit-test-utils 13/15 (#678) 2025-07-02 12:18:50 -04:00
Diana Villalvazo
49e527d810 test: Deprecate react-unit-test-utils 14/15 (#672) 2025-07-01 13:16:20 -04:00
Diana Villalvazo
fdc58a671a test: Transform snapshots into rtl tests (#674) 2025-06-30 15:35:13 -04:00
Muhammad Noyan Aziz
90aa652ca6 feat: added a generic creditPurchase Url logic (#675)
Co-authored-by: Muhammad Noyan  Aziz <noyan.aziz@A006-01474.local>
2025-06-30 10:35:32 -04:00
renovate[bot]
05a08101a2 fix(deps): update dependency @openedx/paragon to v23.13.0 (#677)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 10:17:18 +00:00
renovate[bot]
cf94ea99b4 chore(deps): update dependency @openedx/frontend-build to v14.6.1 (#676)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 04:54:58 +00:00
Diana Villalvazo
7f1e509ecf test: Deprecate react-unit-test-utils 12/15 (#671) 2025-06-27 12:13:33 -04:00
Diana Villalvazo
31c5445722 test: Deprecate react-unit-test-utils 11/15 (#670) 2025-06-26 15:53:29 -04:00
Diana Villalvazo
2acf7fbd73 test: Deprecate react-unit-test-utils 10/15 (#658) 2025-06-25 08:22:38 -04:00
Diana Villalvazo
cae7b1bba0 test: Deprecate react-unit-test-utils 9/15 (#668) 2025-06-24 16:50:11 +00:00
Diana Villalvazo
6ae8180f99 test: Deprecate react-unit-test-utils 8/15 (#664) 2025-06-24 12:38:13 -04:00
Diana Villalvazo
ab0f139d75 test: Deprecate react-unit-test-utils 7/15 (#662) 2025-06-24 09:44:25 -04:00
Diana Villalvazo
8a0b9dca5d test: Deprecate react-unit-test-utils 6/15 (#660) 2025-06-23 14:22:21 -04:00
Brian Smith
2e59e24876 feat!: add design tokens support (#665)
BREAKING CHANGE: Pre-design-tokens theming is no longer supported.

Co-authored-by: Diana Olarte <diana.olarte@edunext.co>
2025-06-18 15:05:17 -04:00
Diana Villalvazo
75865290bf test: Deprecate react-unit-test-utils 5/15 (#659) 2025-06-17 20:32:59 -04:00
Diana Villalvazo
57fe161b16 test: Deprecate react-unit-test-utils 4/15 (#656) 2025-06-17 20:09:36 -04:00
Diana Villalvazo
5c4dfd5de3 test: Deprecate react-unit-test-utils 3/15 (#655) 2025-06-17 20:00:44 -04:00
Diana Villalvazo
b0daefa2bf test: Deprecate react-unit-test-utils 2/15 (#643) 2025-06-17 19:47:36 -04:00
Diana Villalvazo
42a21180c7 test: Deprecate react-unit-test-utils 1/15 (#640) 2025-06-17 12:52:56 -04:00
320 changed files with 9578 additions and 6581 deletions

10
.dockerignore Executable file
View File

@@ -0,0 +1,10 @@
node_modules
npm-debug.log
README.md
LICENSE
.babelrc
.eslintignore
.eslintrc.json
.gitignore
.npmignore
commitlint.config.js

46
.env Normal file
View File

@@ -0,0 +1,46 @@
NODE_ENV='production'
NODE_PATH=./src
BASE_URL=''
LMS_BASE_URL=''
ECOMMERCE_BASE_URL=''
LOGIN_URL=''
LOGOUT_URL=''
CSRF_TOKEN_API_PATH=''
REFRESH_ACCESS_TOKEN_ENDPOINT=''
DATA_API_BASE_URL=''
SEGMENT_KEY=''
FEATURE_FLAGS={}
ACCESS_TOKEN_COOKIE_NAME=''
NEW_RELIC_APP_ID=''
NEW_RELIC_LICENSE_KEY=''
SITE_NAME=''
MARKETING_SITE_BASE_URL=''
SUPPORT_URL=''
CONTACT_URL=''
OPEN_SOURCE_URL=''
TERMS_OF_SERVICE_URL=''
PRIVACY_POLICY_URL=''
FACEBOOK_URL=''
TWITTER_URL=''
YOU_TUBE_URL=''
LINKED_IN_URL=''
REDDIT_URL=''
APPLE_APP_STORE_URL=''
GOOGLE_PLAY_URL=''
ENTERPRISE_MARKETING_URL=''
ENTERPRISE_MARKETING_UTM_SOURCE=''
ENTERPRISE_MARKETING_UTM_CAMPAIGN=''
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=''
LEARNING_BASE_URL=''
HOTJAR_APP_ID=''
HOTJAR_VERSION='6'
HOTJAR_DEBUG=''
ACCOUNT_SETTINGS_URL=''
ACCOUNT_PROFILE_URL=''
ENABLE_NOTICES=''
CAREER_LINK_URL=''
ENABLE_EDX_PERSONAL_DASHBOARD=false
ENABLE_PROGRAMS=false
NON_BROWSABLE_COURSES=false
# Fallback in local style files
PARAGON_THEME_URLS={}

52
.env.development Normal file
View File

@@ -0,0 +1,52 @@
NODE_ENV='development'
PORT=1996
BASE_URL='localhost:1996'
LMS_BASE_URL='http://localhost:18000'
ECOMMERCE_BASE_URL='http://localhost:18130'
LOGIN_URL='http://localhost:18000/login'
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
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'
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
USER_INFO_COOKIE_NAME='edx-user-info'
SITE_NAME=localhost
DATA_API_BASE_URL='http://localhost:8000'
// LMS_CLIENT_ID should match the lms DOT client application id your LMS containe
LMS_CLIENT_ID='login-service-client-id'
SEGMENT_KEY=''
FEATURE_FLAGS={}
MARKETING_SITE_BASE_URL='http://localhost:18000'
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'
PRIVACY_POLICY_URL='http://localhost:18000/privacy-policy'
FACEBOOK_URL='https://www.facebook.com'
TWITTER_URL='https://twitter.com'
YOU_TUBE_URL='https://www.youtube.com'
LINKED_IN_URL='https://www.linkedin.com'
REDDIT_URL='https://www.reddit.com'
APPLE_APP_STORE_URL='https://www.apple.com/ios/app-store/'
GOOGLE_PLAY_URL='https://play.google.com/store'
ENTERPRISE_MARKETING_URL='http://example.com'
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'
SESSION_COOKIE_DOMAIN='localhost'
HOTJAR_APP_ID=''
HOTJAR_VERSION='6'
HOTJAR_DEBUG=''
ACCOUNT_SETTINGS_URL='http://localhost:1997'
ACCOUNT_PROFILE_URL='http://localhost:1995'
ENABLE_NOTICES=''
CAREER_LINK_URL=''
ENABLE_EDX_PERSONAL_DASHBOARD=false
ENABLE_PROGRAMS=false
NON_BROWSABLE_COURSES=false
# Fallback in local style files
PARAGON_THEME_URLS={}

50
.env.test Normal file
View File

@@ -0,0 +1,50 @@
NODE_ENV='test'
PORT=1996
BASE_URL='localhost:1996'
LMS_BASE_URL='http://localhost:18000'
ECOMMERCE_BASE_URL='http://localhost:18130'
LOGIN_URL='http://localhost:18000/login'
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
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'
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
USER_INFO_COOKIE_NAME='edx-user-info'
SITE_NAME=localhost
DATA_API_BASE_URL='http://localhost:8000'
// LMS_CLIENT_ID should match the lms DOT client application id your LMS containe
LMS_CLIENT_ID='login-service-client-id'
SEGMENT_KEY=''
FEATURE_FLAGS={}
MARKETING_SITE_BASE_URL='http://localhost:18000'
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'
PRIVACY_POLICY_URL='http://localhost:18000/privacy-policy'
FACEBOOK_URL='https://www.facebook.com'
TWITTER_URL='https://twitter.com'
YOU_TUBE_URL='https://www.youtube.com'
LINKED_IN_URL='https://www.linkedin.com'
REDDIT_URL='https://www.reddit.com'
APPLE_APP_STORE_URL='https://www.apple.com/ios/app-store/'
GOOGLE_PLAY_URL='https://play.google.com/store'
ENTERPRISE_MARKETING_URL='http://example.com'
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'
HOTJAR_APP_ID='hot-jar-app-id'
HOTJAR_VERSION='6'
HOTJAR_DEBUG=''
ACCOUNT_SETTINGS_URL='http://account-settings-url.test'
ACCOUNT_PROFILE_URL='http://account-profile-url.test'
ENABLE_NOTICES=''
CAREER_LINK_URL=''
ENABLE_EDX_PERSONAL_DASHBOARD=true
ENABLE_PROGRAMS=false
NON_BROWSABLE_COURSES=false
PARAGON_THEME_URLS={}

5
.eslintignore Executable file
View File

@@ -0,0 +1,5 @@
coverage/*
dist/
node_modules/
src/postcss.config.js
src/segment.js

22
.eslintrc.js Normal file
View File

@@ -0,0 +1,22 @@
const { createConfig } = require('@openedx/frontend-build');
const config = createConfig('eslint', {
rules: {
'import/no-named-as-default': 'off',
'import/no-named-as-default-member': 'off',
'import/no-self-import': 'off',
'import/no-import-module-exports': 'off',
'spaced-comment': ['error', 'always', { 'block': { 'exceptions': ['*'] } }],
},
});
config.settings = {
"import/resolver": {
node: {
paths: ["src", "node_modules"],
extensions: [".js", ".jsx"],
},
},
};
module.exports = config;

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
*.snap linguist-generated=false

1
.github/CODEOWNERS vendored
View File

@@ -1 +0,0 @@
* @openedx/2U-aperture

View File

@@ -14,10 +14,10 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Nodejs
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
@@ -29,6 +29,9 @@ jobs:
- name: Lint
run: npm run lint
- name: Type check
run: npm run types
- name: Test
run: npm run test

30
.gitignore vendored
View File

@@ -1,15 +1,29 @@
.DS_Store
.eslintcache
env.config.*
node_modules
npm-debug.log
coverage
module.config.js
dist/
/*.tgz
public/samples/
### i18n ###
src/i18n/transifex_input.json
### pyenv ###
.python-version
### Editors ###
.DS_Store
### Emacs ###
*~
/temp
/.vscode
*.swo
*.swp
### Development environments ###
.idea
.vscode
# Local package dependencies
module.config.js
### transifex ###
src/i18n/transifex_input.json
temp
src/i18n/messages

View File

@@ -1,6 +1,12 @@
__mocks__
.eslintignore
.eslintrc.json
.gitignore
docker-compose.yml
Dockerfile
Makefile
npm-debug.log
config
coverage
node_modules
*.test.js
*.test.jsx
*.test.ts
*.test.tsx
public

View File

@@ -24,19 +24,6 @@ test.npm.%: validate-no-uncommitted-package-lock-changes
requirements: ## install ci requirements
npm ci
clean:
rm -rf dist
build: clean
tsc --project tsconfig.build.json
tsc-alias -p tsconfig.build.json
find src -type f \( -name '*.scss' -o -name '*.png' -o -name '*.svg' \) -exec sh -c '\
for f in "$$@"; do \
d="dist/$${f#src/}"; \
mkdir -p "$$(dirname "$$d")"; \
cp "$$f" "$$d"; \
done' sh {} +
i18n.extract:
# Pulling display strings from .jsx files into .json files...
rm -rf $(transifex_temp)
@@ -58,11 +45,13 @@ pull_translations:
mkdir src/i18n/messages
cd src/i18n/messages \
&& atlas pull $(ATLAS_OPTIONS) \
translations/frontend-base/src/i18n/messages:frontend-base \
translations/frontend-platform/src/i18n/messages:frontend-platform \
translations/paragon/src/i18n/messages:paragon \
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
translations/frontend-app-learner-dashboard/src/i18n/messages:frontend-app-learner-dashboard
$(intl_imports) frontend-base paragon frontend-app-learner-dashboard
$(intl_imports) frontend-platform paragon frontend-component-header frontend-component-footer frontend-app-learner-dashboard
# This target is used by CI.
validate-no-uncommitted-package-lock-changes:

View File

@@ -18,7 +18,7 @@ frontend-app-learner-dashboard
The Learner Home app is a microfrontend (MFE) course listing experience for the Open edX Learning Management System
(LMS). This experience was designed to provide a clean and functional interface to allow learners to view all of their
open enrollments, as well as take relevant actions on those enrollments. It also serves as host to a number of exposed
"widget" containers to provide upsell and discovery widgets as sidebar components.
"widget" containers to provide upsell and discovery widgets as sidebar/footer components.
Quickstart
----------
@@ -30,10 +30,21 @@ 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.
Widgets
Plugins
-------
This MFE can be customized with widgets. The parts of this MFE that can be customized in that manner are documented
`here </src/slots>`_.
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
------------
Contributions are very welcome. Please read `So you want to contribute to Open edX <https://docs.openedx.org/en/latest/developers/quickstarts/so_you_want_to_contribute.html>`_ for details on how to get started as an Open edX contributor.
This project is currently accepting all types of contributions — bug fixes, security fixes, maintenance work, or new features.
However, if you intend to add a new feature, make sure it has gone through the `Product Review process <https://openedx.atlassian.net/wiki/spaces/COMM/pages/3875962884/How+to+submit+an+open+source+contribution+for+Product+Review>`_.
When proposing a change, create an issue in this repo to get the discussion started.
License
-------

10
app.d.ts vendored
View File

@@ -1,10 +0,0 @@
/// <reference types="@openedx/frontend-base" />
declare module 'site.config' {
export default SiteConfig;
}
declare module '*.svg' {
const content: string;
export default content;
}

View File

@@ -1,3 +0,0 @@
const { createConfig } = require('@openedx/frontend-base/tools');
module.exports = createConfig('babel');

View File

@@ -1,22 +0,0 @@
// @ts-check
const { createLintConfig } = require('@openedx/frontend-base/tools');
module.exports = createLintConfig(
{
files: [
'src/**/*',
'site.config.*',
],
},
{
ignores: [
'coverage/*',
'dist/*',
'documentation/*',
'node_modules/*',
'**/__mocks__/*',
'**/__snapshots__/*',
],
},
);

73
example.env.config.js Normal file
View File

@@ -0,0 +1,73 @@
/*
Learner Dashboard is now able to handle JS-based configuration!
For the time being, the `.env.*` files are still made available when cloning down this repo or pulling from
the master branch. To switch to using `env.config.js`, make a copy of `example.env.config.js` and configure as needed.
For testing with Jest Snapshot, there is a mock in `/src/setupTest.jsx` for `getConfig` that will need to be
uncommented.
Note: having both .env and env.config.js files will follow a predictable order, in which non-empty values in the
JS-based config will overwrite the .env environment variables.
frontend-platform's getConfig loads configuration in the following sequence:
- .env file config
- optional handlers (commonly used to merge MFE-specific config in via additional process.env variables)
- env.config.js file config
- runtime config
*/
module.exports = {
NODE_ENV: 'development',
NODE_PATH: './src',
PORT: 1996,
BASE_URL: 'localhost:1996',
LMS_BASE_URL: 'http://localhost:18000',
ECOMMERCE_BASE_URL: 'http://localhost:18130',
CREDIT_PURCHASE_URL: 'http://localhost:8140',
LOGIN_URL: 'http://localhost:18000/login',
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',
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',
ACCESS_TOKEN_COOKIE_NAME: 'edx-jwt-cookie-header-payload',
USER_INFO_COOKIE_NAME: 'edx-user-info',
SITE_NAME: 'localhost',
DATA_API_BASE_URL: 'http://localhost:8000',
// LMS_CLIENT_ID should match the lms DOT client application in your LMS container
LMS_CLIENT_ID: 'login-service-client-id',
SEGMENT_KEY: '',
FEATURE_FLAGS: {},
MARKETING_SITE_BASE_URL: 'http://localhost:18000',
SUPPORT_URL: 'http://localhost:18000/support',
CONTACT_URL: 'http://localhost:18000/contact',
OPEN_SOURCE_URL: 'http://localhost:18000/openedx',
TERMS_OF_SERVICE_URL: 'http://localhost:18000/terms-of-service',
PRIVACY_POLICY_URL: 'http://localhost:18000/privacy-policy',
FACEBOOK_URL: 'https://www.facebook.com',
TWITTER_URL: 'https://twitter.com',
YOU_TUBE_URL: 'https://www.youtube.com',
LINKED_IN_URL: 'https://www.linkedin.com',
REDDIT_URL: 'https://www.reddit.com',
APPLE_APP_STORE_URL: 'https://www.apple.com/ios/app-store/',
GOOGLE_PLAY_URL: 'https://play.google.com/store',
ENTERPRISE_MARKETING_URL: 'http://example.com',
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',
SESSION_COOKIE_DOMAIN: 'localhost',
HOTJAR_APP_ID: '',
HOTJAR_VERSION: 6,
HOTJAR_DEBUG: '',
NEW_RELIC_APP_ID: '',
NEW_RELIC_LICENSE_KEY: '',
ACCOUNT_SETTINGS_URL: 'http://localhost:1997',
ACCOUNT_PROFILE_URL: 'http://localhost:1995',
ENABLE_NOTICES: '',
CAREER_LINK_URL: '',
EXPERIMENT_08_23_VAN_PAINTED_DOOR: true,
};

View File

@@ -1,22 +1,18 @@
const { createConfig } = require('@openedx/frontend-base/tools');
const { createConfig } = require('@openedx/frontend-build');
module.exports = createConfig('test', {
module.exports = createConfig('jest', {
setupFilesAfterEnv: [
'jest-expect-message',
'<rootDir>/src/setupTest.jsx',
],
modulePaths: ['<rootDir>/src/'],
coveragePathIgnorePatterns: [
'src/segment.js',
'src/postcss.config.js',
'testUtils', // don't unit test jest mocking tools
'src/data/services/lms/fakeData', // don't unit test mock data
'src/test', // don't unit test integration test utils
'src/__mocks__',
],
moduleNameMapper: {
// Asset mocks
'\\.svg$': '<rootDir>/src/__mocks__/svg.js',
'\\.(jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/src/__mocks__/file.js',
},
testTimeout: 120000,
testEnvironment: 'jsdom',
});

10532
package-lock.json generated

File diff suppressed because it is too large Load Diff

86
package.json Normal file → Executable file
View File

@@ -1,69 +1,79 @@
{
"name": "@openedx/frontend-app-learner-dashboard",
"version": "1.0.0-alpha.6",
"name": "@edx/frontend-app-learner-dashboard",
"version": "0.0.1",
"description": "",
"repository": {
"type": "git",
"url": "git+https://github.com/edx/frontend-app-learner-dashboard.git"
},
"exports": {
".": "./dist/index.js",
"./app.scss": "./dist/app.scss"
},
"files": [
"/dist"
],
"browserslist": [
"extends @edx/browserslist-config"
],
"sideEffects": [
"*.css",
"*.scss"
],
"scripts": {
"build": "make build",
"clean": "make clean",
"dev": "PORT=1996 PUBLIC_PATH=/learner-dashboard openedx dev",
"i18n_extract": "openedx formatjs extract",
"lint": "openedx lint .",
"lint:fix": "openedx lint --fix .",
"prepack": "npm run build",
"test": "openedx test --coverage --passWithNoTests"
"build": "fedx-scripts webpack",
"i18n_extract": "fedx-scripts formatjs extract",
"lint": "fedx-scripts eslint --ext .jsx,.js src/",
"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",
"types": "tsc --noEmit"
},
"author": "Open edX",
"author": "edX",
"license": "AGPL-3.0",
"homepage": "https://github.com/openedx/frontend-app-learner-dashboard#readme",
"homepage": "",
"publishConfig": {
"access": "public"
},
"bugs": {
"url": "https://github.com/openedx/frontend-app-learner-dashboard/issues"
},
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.3",
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-component-footer": "^14.6.0",
"@edx/frontend-component-header": "^6.6.0",
"@edx/frontend-enterprise-hotjar": "7.2.0",
"@edx/frontend-platform": "^8.3.1",
"@edx/openedx-atlas": "^0.7.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.7.0",
"@openedx/paragon": "^23.4.5",
"@redux-devtools/extension": "3.3.0",
"@reduxjs/toolkit": "^2.0.0",
"classnames": "^2.3.1",
"core-js": "3.46.0",
"filesize": "^10.0.0",
"font-awesome": "4.7.0",
"history": "5.3.0",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"prop-types": "15.8.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-helmet": "^6.1.0",
"react-intl": "6.8.9",
"react-redux": "^7.2.4",
"react-router-dom": "6.30.1",
"react-share": "^4.4.0",
"redux": "4.2.1",
"redux-logger": "3.0.6",
"redux-thunk": "2.4.2",
"reselect": "^4.0.0"
"regenerator-runtime": "^0.14.0",
"reselect": "^4.0.0",
"universal-cookie": "^4.0.4",
"util": "^0.12.4"
},
"devDependencies": {
"@edx/browserslist-config": "^1.5.0",
"@edx/browserslist-config": "^1.3.0",
"@edx/typescript-config": "^1.1.0",
"@openedx/frontend-build": "^14.6.2",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.6.1",
"copy-webpack-plugin": "^13.0.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
@@ -71,20 +81,6 @@
"jest-when": "^3.6.0",
"react-dev-utils": "^12.0.0",
"react-test-renderer": "^18.3.1",
"redux-mock-store": "^1.5.4",
"tsc-alias": "^1.8.16"
},
"peerDependencies": {
"@openedx/frontend-base": "^1.0.0-alpha.13",
"@openedx/paragon": "^23",
"@tanstack/react-query": "^5",
"@types/react": "^18",
"@types/react-dom": "^18",
"react": "^18",
"react-dom": "^18",
"react-redux": "^8",
"react-router": "^6",
"react-router-dom": "^6",
"redux": "^4"
"redux-mock-store": "^1.5.4"
}
}

View File

@@ -1,7 +1,6 @@
<!doctype html>
<html lang="en-us" dir="ltr">
<head>
<title>Learner Dashboard Development Site></title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>

2
public/robots.txt Normal file
View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View File

@@ -1,40 +0,0 @@
import { EnvironmentTypes, SiteConfig, footerApp, headerApp, shellApp } from '@openedx/frontend-base';
import { learnerDashboardApp } from './src';
import './src/app.scss';
const siteConfig: SiteConfig = {
siteId: 'learner-dashboard-dev',
siteName: 'Learner Dashboard Dev',
baseUrl: 'http://apps.local.openedx.io:1996',
lmsBaseUrl: 'http://local.openedx.io:8000',
loginUrl: 'http://local.openedx.io:8000/login',
logoutUrl: 'http://local.openedx.io:8000/logout',
environment: EnvironmentTypes.DEVELOPMENT,
apps: [
shellApp,
headerApp,
footerApp,
learnerDashboardApp
],
externalRoutes: [
{
role: 'org.openedx.frontend.role.profile',
url: 'http://apps.local.openedx.io:1995/profile/'
},
{
role: 'org.openedx.frontend.role.account',
url: 'http://apps.local.openedx.io:1997/account/'
},
{
role: 'org.openedx.frontend.role.logout',
url: 'http://local.openedx.io:8000/logout'
},
],
accessTokenCookieName: 'edx-jwt-cookie-header-payload',
};
export default siteConfig;

View File

@@ -1,27 +0,0 @@
import { EnvironmentTypes, SiteConfig } from '@openedx/frontend-base';
import { appId } from './src/constants';
const siteConfig: SiteConfig = {
siteId: 'learner-dashboard-test-site',
siteName: 'Learner Dashboard Test Site',
baseUrl: 'http://localhost:1996',
lmsBaseUrl: 'http://localhost:8000',
loginUrl: 'http://localhost:8000/login',
logoutUrl: 'http://localhost:8000/logout',
environment: EnvironmentTypes.TEST,
apps: [{
appId,
config: {
ECOMMERCE_BASE_URL: 'http://localhost:18130',
FAVICON_URL: 'https://edx-cdn.org/v3/default/favicon.ico',
LEARNING_BASE_URL: 'http://localhost:2000',
},
}],
accessTokenCookieName: 'edx-jwt-cookie-header-payload',
segmentKey: '',
};
export default siteConfig;

100
src/App.jsx Executable file
View File

@@ -0,0 +1,100 @@
import React from 'react';
import { Helmet } from 'react-helmet';
import { useIntl } from '@edx/frontend-platform/i18n';
import { logError } from '@edx/frontend-platform/logging';
import { initializeHotjar } from '@edx/frontend-enterprise-hotjar';
import { ErrorPage, AppContext } from '@edx/frontend-platform/react';
import { FooterSlot } from '@edx/frontend-component-footer';
import { Alert } from '@openedx/paragon';
import { RequestKeys } from 'data/constants/requests';
import store from 'data/store';
import {
selectors,
actions,
} from 'data/redux';
import { reduxHooks } from 'hooks';
import Dashboard from 'containers/Dashboard';
import track from 'tracking';
import fakeData from 'data/services/lms/fakeData/courses';
import AppWrapper from 'containers/AppWrapper';
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
import { getConfig } from '@edx/frontend-platform';
import messages from './messages';
import './App.scss';
export const App = () => {
const { authenticatedUser } = React.useContext(AppContext);
const { formatMessage } = useIntl();
const isFailed = {
initialize: reduxHooks.useRequestIsFailed(RequestKeys.initialize),
refreshList: reduxHooks.useRequestIsFailed(RequestKeys.refreshList),
};
const hasNetworkFailure = isFailed.initialize || isFailed.refreshList;
const { supportEmail } = reduxHooks.usePlatformSettingsData();
const loadData = reduxHooks.useLoadData();
React.useEffect(() => {
if (authenticatedUser?.administrator || getConfig().NODE_ENV === 'development') {
window.loadEmptyData = () => {
loadData({ ...fakeData.globalData, courses: [] });
};
window.loadMockData = () => {
loadData({
...fakeData.globalData,
courses: [
...fakeData.courseRunData,
...fakeData.entitlementData,
],
});
};
window.store = store;
window.selectors = selectors;
window.actions = actions;
window.track = track;
}
if (getConfig().HOTJAR_APP_ID) {
try {
initializeHotjar({
hotjarId: getConfig().HOTJAR_APP_ID,
hotjarVersion: getConfig().HOTJAR_VERSION,
hotjarDebug: !!getConfig().HOTJAR_DEBUG,
});
} catch (error) {
logError(error);
}
}
}, [authenticatedUser, loadData]);
return (
<>
<Helmet>
<title>{formatMessage(messages.pageTitle)}</title>
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
</Helmet>
<div>
<AppWrapper>
<LearnerDashboardHeader />
<main id="main">
{hasNetworkFailure
? (
<Alert variant="danger">
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
</Alert>
) : (
<Dashboard />
)}
</main>
</AppWrapper>
<FooterSlot />
</div>
</>
);
};
export default App;

66
src/App.scss Executable file
View File

@@ -0,0 +1,66 @@
// frontend-app-*/src/index.scss
@use "@openedx/paragon/styles/css/core/custom-media-breakpoints" as paragonCustomMediaBreakpoints;
$fa-font-path: "~font-awesome/fonts";
@import "~font-awesome/scss/font-awesome";
$input-focus-box-shadow: var(--pgn-elevation-form-input-base); // hack to get upgrade to paragon 4.0.0 to work
@import "~@edx/frontend-component-header/dist/index";
@import "~@edx/frontend-component-footer/dist/_footer";
.text-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.alert.alert-info .alert-icon {
color: black;
}
#root {
// Removing a odd 1.5 scaling on checkboxes.:
input[type=checkbox] {
transform: none;
}
display: flex;
flex-direction: column;
min-height: 100vh;
main {
flex-grow: 1;
}
header {
flex: 0 0 auto;
.logo {
display: block;
box-sizing: content-box;
position: relative;
top: 0.1em;
height: 1.75rem;
margin-right: 1rem;
img {
display: block;
height: 100%;
}
}
}
footer {
flex: 0;
}
}
#paragon-portal-root {
.pgn__modal-layer {
.pgn__modal-close-container {
right: 1rem !important;
}
}
.confirm-modal .pgn__modal-body {
overflow: hidden;
}
}

135
src/App.test.jsx Normal file
View File

@@ -0,0 +1,135 @@
import { render, screen, waitFor } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
import { RequestKeys } from 'data/constants/requests';
import { reduxHooks } from 'hooks';
import { App } from './App';
import messages from './messages';
jest.mock('@edx/frontend-component-footer', () => ({
FooterSlot: jest.fn(() => <div>FooterSlot</div>),
}));
jest.mock('containers/Dashboard', () => jest.fn(() => <div>Dashboard</div>));
jest.mock('containers/LearnerDashboardHeader', () => jest.fn(() => <div>LearnerDashboardHeader</div>));
jest.mock('containers/AppWrapper', () => jest.fn(({ children }) => <div className="AppWrapper">{children}</div>));
jest.mock('data/redux', () => ({
selectors: 'redux.selectors',
actions: 'redux.actions',
thunkActions: 'redux.thunkActions',
}));
jest.mock('hooks', () => ({
reduxHooks: {
useRequestIsFailed: jest.fn(),
usePlatformSettingsData: jest.fn(),
useLoadData: jest.fn(),
},
}));
jest.mock('data/store', () => 'data/store');
jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(() => ({})),
}));
jest.mock('@edx/frontend-platform/react', () => ({
...jest.requireActual('@edx/frontend-platform/react'),
ErrorPage: () => 'ErrorPage',
}));
const loadData = jest.fn();
reduxHooks.useLoadData.mockReturnValue(loadData);
const supportEmail = 'test@support.com';
reduxHooks.usePlatformSettingsData.mockReturnValue({ supportEmail });
describe('App router component', () => {
describe('component', () => {
const runBasicTests = () => {
it('displays title in helmet component', async () => {
await waitFor(() => expect(document.title).toEqual(messages.pageTitle.defaultMessage));
});
it('displays learner dashboard header', () => {
const learnerDashboardHeader = screen.getByText('LearnerDashboardHeader');
expect(learnerDashboardHeader).toBeInTheDocument();
});
it('wraps the header and main components in an AppWrapper widget container', () => {
const appWrapper = screen.getByText('LearnerDashboardHeader').parentElement;
expect(appWrapper).toHaveClass('AppWrapper');
expect(appWrapper.children[1].id).toEqual('main');
});
it('displays footer slot', () => {
const footerSlot = screen.getByText('FooterSlot');
expect(footerSlot).toBeInTheDocument();
});
};
describe('no network failure', () => {
beforeEach(() => {
jest.clearAllMocks();
reduxHooks.useRequestIsFailed.mockReturnValue(false);
getConfig.mockReturnValue({});
render(<IntlProvider locale="en"><App /></IntlProvider>);
});
runBasicTests();
it('loads dashboard', () => {
const dashboard = screen.getByText('Dashboard');
expect(dashboard).toBeInTheDocument();
});
});
describe('no network failure with optimizely url', () => {
beforeEach(() => {
jest.clearAllMocks();
reduxHooks.useRequestIsFailed.mockReturnValue(false);
getConfig.mockReturnValue({ OPTIMIZELY_URL: 'fake.url' });
render(<IntlProvider locale="en"><App /></IntlProvider>);
});
runBasicTests();
it('loads dashboard', () => {
const dashboard = screen.getByText('Dashboard');
expect(dashboard).toBeInTheDocument();
});
});
describe('no network failure with optimizely project id', () => {
beforeEach(() => {
jest.clearAllMocks();
reduxHooks.useRequestIsFailed.mockReturnValue(false);
getConfig.mockReturnValue({ OPTIMIZELY_PROJECT_ID: 'fakeId' });
render(<IntlProvider locale="en"><App /></IntlProvider>);
});
runBasicTests();
it('loads dashboard', () => {
const dashboard = screen.getByText('Dashboard');
expect(dashboard).toBeInTheDocument();
});
});
describe('initialize failure', () => {
beforeEach(() => {
jest.clearAllMocks();
reduxHooks.useRequestIsFailed.mockImplementation((key) => key === RequestKeys.initialize);
getConfig.mockReturnValue({});
render(<IntlProvider locale="en" messages={messages}><App /></IntlProvider>);
});
runBasicTests();
it('loads error page', () => {
const alert = screen.getByRole('alert');
expect(alert).toBeInTheDocument();
const errorPage = screen.getByText('ErrorPage');
expect(errorPage).toBeInTheDocument();
});
});
describe('refresh failure', () => {
beforeEach(() => {
reduxHooks.useRequestIsFailed.mockImplementation((key) => key === RequestKeys.refreshList);
getConfig.mockReturnValue({});
render(<IntlProvider locale="en"><App /></IntlProvider>);
});
runBasicTests();
it('loads error page', () => {
const alert = screen.getByRole('alert');
expect(alert).toBeInTheDocument();
const errorPage = screen.getByText('ErrorPage');
expect(errorPage).toBeInTheDocument();
});
});
});
});

View File

@@ -1,20 +0,0 @@
import { Provider as ReduxProvider } from 'react-redux';
import { CurrentAppProvider, PageWrap } from '@openedx/frontend-base';
import { appId } from './constants';
import store from './data/store';
import Dashboard from './containers/Dashboard';
import './app.scss';
const Main = () => (
<CurrentAppProvider appId={appId}>
<ReduxProvider store={store}>
<PageWrap>
<Dashboard />
</PageWrap>
</ReduxProvider>
</CurrentAppProvider>
);
export default Main;

View File

@@ -1 +0,0 @@
module.exports = 'FileMock';

View File

@@ -1 +0,0 @@
module.exports = 'SvgURL';

View File

@@ -1,38 +0,0 @@
@use "@openedx/frontend-base/shell/app.scss";
$fa-font-path: "~font-awesome/fonts";
@import "~font-awesome/scss/font-awesome";
$input-focus-box-shadow: var(--pgn-elevation-form-input-base); // hack to get upgrade to paragon 4.0.0 to work
#learnerDashboardRoot {
main {
flex-grow: 1;
}
// Removing a odd 1.5 scaling on checkboxes.:
input[type=checkbox] {
transform: none;
}
.text-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.alert.alert-info .alert-icon {
color: black;
}
#paragon-portal-root {
.pgn__modal-layer {
.pgn__modal-close-container {
right: 1rem !important;
}
}
.confirm-modal .pgn__modal-body {
overflow: hidden;
}
}
}

View File

@@ -1,23 +0,0 @@
import { App } from '@openedx/frontend-base';
import { appId } from './constants';
import routes from './routes';
import providers from './providers';
import messages from './i18n';
import slots from './slots';
const app: App = {
appId,
routes,
providers,
messages,
slots,
config: {
LEARNING_BASE_URL: 'http://apps.local.openedx.io:2000',
ENABLE_PROGRAMS: false,
ECOMMERCE_BASE_URL: '',
ORDER_HISTORY_URL: '',
SUPPORT_URL: '',
}
};
export default app;

View File

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

15
src/assets/top_stripe.svg Normal file
View File

@@ -0,0 +1,15 @@
<svg width="1350" height="7" viewBox="0 0 1350 7" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_3302_26079)">
<rect width="1350" height="6.75" transform="translate(0 -0.375)" fill="#03C7E8"/>
<rect y="-0.375" width="585.562" height="6.75" fill="#D23228"/>
<path d="M549.281 -0.375H933.188L929.491 6.375H549.281V-0.375Z" fill="#002121"/>
<path d="M550.129 13.125L545.062 -10.5L555.188 -10.5L550.129 13.125Z" fill="#D23228"/>
<path d="M931.082 13.125L925.594 -6.28125L936.563 -6.28125L931.082 13.125Z" fill="#002121"/>
<path d="M0 -0.375H106.312L105.289 6.375H0V-0.375Z" fill="#921108"/>
</g>
<defs>
<clipPath id="clip0_3302_26079">
<rect width="1350" height="6.75" fill="white" transform="translate(0 -0.375)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 765 B

View File

@@ -0,0 +1,25 @@
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { logError, logInfo } from '@edx/frontend-platform/logging';
export const noticesUrl = `${getConfig().LMS_BASE_URL}/notices/api/v1/unacknowledged`;
export const getNotices = ({ onLoad, notFoundMessage }) => {
const authenticatedUser = getAuthenticatedUser();
const handleError = async (e) => {
// Error probably means that notices is not installed, which is fine.
const { customAttributes: { httpErrorStatus } } = e;
if (httpErrorStatus === 404) {
logInfo(`${e}. ${notFoundMessage}`);
} else {
logError(e);
}
};
if (authenticatedUser) {
return getAuthenticatedHttpClient().get(noticesUrl, {}).then(onLoad).catch(handleError);
}
return null;
};
export default { getNotices };

View File

@@ -0,0 +1,65 @@
import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { logError, logInfo } from '@edx/frontend-platform/logging';
import * as api from './api';
jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(() => ({
LMS_BASE_URL: 'test-lms-url',
})),
}));
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedHttpClient: jest.fn(),
getAuthenticatedUser: jest.fn(),
}));
jest.mock('@edx/frontend-platform/logging', () => ({
logError: jest.fn(),
logInfo: jest.fn(),
}));
const testData = 'test-data';
const successfulGet = () => Promise.resolve(testData);
const error404 = { customAttributes: { httpErrorStatus: 404 }, test: 'error' };
const error404Get = () => Promise.reject(error404);
const error500 = { customAttributes: { httpErrorStatus: 500 }, test: 'error' };
const error500Get = () => Promise.reject(error500);
const get = jest.fn().mockImplementation(successfulGet);
getAuthenticatedHttpClient.mockReturnValue({ get });
const authenticatedUser = { fake: 'user' };
getAuthenticatedUser.mockReturnValue(authenticatedUser);
const onLoad = jest.fn();
describe('getNotices api method', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('behavior', () => {
describe('not authenticated', () => {
it('does not fetch anything', () => {
getAuthenticatedUser.mockReturnValueOnce(null);
api.getNotices({ onLoad });
expect(get).not.toHaveBeenCalled();
});
});
describe('authenticated', () => {
it('fetches noticesUrl with onLoad behavior', async () => {
await api.getNotices({ onLoad });
expect(get).toHaveBeenCalledWith(api.noticesUrl, {});
expect(onLoad).toHaveBeenCalledWith(testData);
});
it('calls logInfo if fetch fails with 404', async () => {
get.mockImplementation(error404Get);
await api.getNotices({ onLoad });
expect(logInfo).toHaveBeenCalledWith(`${error404}. ${api.error404Message}`);
});
it('calls logError if fetch fails with non-404 error', async () => {
get.mockImplementation(error500Get);
await api.getNotices({ onLoad });
expect(logError).toHaveBeenCalledWith(error500);
});
});
});
});

View File

@@ -0,0 +1,40 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { StrictDict } from 'utils';
import { getNotices } from './api';
import * as module from './hooks';
import messages from './messages';
/**
* This component uses the platform-plugin-notices plugin to function.
* If the user has an unacknowledged notice, they will be rerouted off
* course home and onto a full-screen notice page. If the plugin is not
* installed, or there are no notices, we just passthrough this component.
*/
export const state = StrictDict({
isRedirected: (val) => React.useState(val), // eslint-disable-line
});
export const useNoticesWrapperData = () => {
const [isRedirected, setIsRedirected] = module.state.isRedirected();
const { formatMessage } = useIntl();
React.useEffect(() => {
if (getConfig().ENABLE_NOTICES) {
getNotices({
onLoad: (data) => {
if (data?.data?.results?.length > 0) {
setIsRedirected(true);
window.location.replace(`${data.data.results[0]}?next=${window.location.href}`);
}
},
notFoundMessage: formatMessage(messages.error404Message),
});
}
}, [setIsRedirected, formatMessage]);
return { isRedirected };
};
export default useNoticesWrapperData;

View File

@@ -0,0 +1,99 @@
import React from 'react';
import { MockUseState, formatMessage } from 'testUtils';
import { getConfig } from '@edx/frontend-platform';
import { getNotices } from './api';
import * as hooks from './hooks';
jest.mock('@edx/frontend-platform', () => ({ getConfig: jest.fn() }));
jest.mock('./api', () => ({ getNotices: jest.fn() }));
jest.mock('react', () => ({
...jest.requireActual('react'),
useEffect: jest.fn((cb, prereqs) => ({ useEffect: { cb, prereqs } })),
useContext: jest.fn(context => context),
}));
jest.mock('@edx/frontend-platform/i18n', () => {
const { formatMessage: fn } = jest.requireActual('testUtils');
return {
...jest.requireActual('@edx/frontend-platform/i18n'),
useIntl: () => ({
formatMessage: fn,
}),
};
});
getConfig.mockReturnValue({ ENABLE_NOTICES: true });
const state = new MockUseState(hooks);
let hook;
describe('NoticesWrapper hooks', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('state hooks', () => {
state.testGetter(state.keys.isRedirected);
});
describe('useNoticesWrapperData', () => {
beforeEach(() => {
state.mock();
});
describe('behavior', () => {
it('initializes state hooks', () => {
hooks.useNoticesWrapperData();
expect(hooks.state.isRedirected).toHaveBeenCalledWith();
});
describe('effects', () => {
it('does not call notices if not enabled', () => {
getConfig.mockReturnValueOnce({ ENABLE_NOTICES: false });
hooks.useNoticesWrapperData();
const [cb, prereqs] = React.useEffect.mock.calls[0];
expect(prereqs).toEqual([state.setState.isRedirected, formatMessage]);
cb();
expect(getNotices).not.toHaveBeenCalled();
});
describe('getNotices call (if enabled) onLoad behavior', () => {
it('does not redirect if there are no results', () => {
hooks.useNoticesWrapperData();
expect(React.useEffect).toHaveBeenCalled();
const [cb, prereqs] = React.useEffect.mock.calls[0];
expect(prereqs).toEqual([state.setState.isRedirected, formatMessage]);
cb();
expect(getNotices).toHaveBeenCalled();
const { onLoad } = getNotices.mock.calls[0][0];
onLoad({});
expect(state.setState.isRedirected).not.toHaveBeenCalled();
onLoad({ data: {} });
expect(state.setState.isRedirected).not.toHaveBeenCalled();
onLoad({ data: { results: [] } });
expect(state.setState.isRedirected).not.toHaveBeenCalled();
});
it('redirects and set isRedirected if results are returned', () => {
delete window.location;
window.location = { replace: jest.fn(), href: 'test-old-href' };
hooks.useNoticesWrapperData();
const [cb, prereqs] = React.useEffect.mock.calls[0];
expect(prereqs).toEqual([state.setState.isRedirected, formatMessage]);
cb();
expect(getNotices).toHaveBeenCalled();
const { onLoad } = getNotices.mock.calls[0][0];
const target = 'url-target';
onLoad({ data: { results: [target] } });
expect(state.setState.isRedirected).toHaveBeenCalledWith(true);
expect(window.location.replace).toHaveBeenCalledWith(
`${target}?next=${window.location.href}`,
);
});
});
});
});
describe('output', () => {
it('forwards isRedirected from state call', () => {
hook = hooks.useNoticesWrapperData();
expect(hook.isRedirected).toEqual(state.stateVals.isRedirected);
});
});
});
});

View File

@@ -0,0 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
import useNoticesWrapperData from './hooks';
/**
* This component uses the platform-plugin-notices plugin to function.
* If the user has an unacknowledged notice, they will be rerouted off
* course home and onto a full-screen notice page. If the plugin is not
* installed, or there are no notices, we just passthrough this component.
*/
const NoticesWrapper = ({ children }) => {
const { isRedirected } = useNoticesWrapperData();
return (
<div>
{isRedirected === true ? null : children}
</div>
);
};
NoticesWrapper.propTypes = {
children: PropTypes.node.isRequired,
};
export default NoticesWrapper;

View File

@@ -0,0 +1,36 @@
import { render, screen } from '@testing-library/react';
import useNoticesWrapperData from './hooks';
import NoticesWrapper from '.';
jest.mock('./hooks', () => jest.fn());
const hookProps = { isRedirected: false };
const children = [<b key={1}>some</b>, <i key={2}>children</i>];
describe('NoticesWrapper component', () => {
beforeEach(() => {
useNoticesWrapperData.mockClear();
});
describe('behavior', () => {
it('initializes hooks', () => {
useNoticesWrapperData.mockReturnValue(hookProps);
render(<NoticesWrapper>{children}</NoticesWrapper>);
expect(useNoticesWrapperData).toHaveBeenCalledWith();
});
});
describe('output', () => {
it('does not show children if redirected', () => {
useNoticesWrapperData.mockReturnValueOnce({ isRedirected: true });
render(<NoticesWrapper>{children}</NoticesWrapper>);
expect(screen.queryByText('some')).not.toBeInTheDocument();
expect(screen.queryByText('children')).not.toBeInTheDocument();
});
it('shows children if not redirected', () => {
useNoticesWrapperData.mockReturnValue(hookProps);
render(<NoticesWrapper>{children}</NoticesWrapper>);
expect(screen.getByText('some')).toBeInTheDocument();
expect(screen.getByText('children')).toBeInTheDocument();
});
});
});

View File

@@ -0,0 +1,11 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
error404Message: {
id: 'learner-dash.notices.error404Message',
defaultMessage: 'This probably happened because the notices plugin is not installed on platform.',
description: 'Error message when notices API returns 404',
},
});
export default messages;

28
src/config/index.js Normal file
View File

@@ -0,0 +1,28 @@
const configuration = {
// BASE_URL: process.env.BASE_URL,
LMS_BASE_URL: process.env.LMS_BASE_URL,
ECOMMERCE_BASE_URL: process.env.ECOMMERCE_BASE_URL,
CREDIT_PURCHASE_URL: process.env.CREDIT_PURCHASE_URL,
// LOGIN_URL: process.env.LOGIN_URL,
// LOGOUT_URL: process.env.LOGOUT_URL,
// CSRF_TOKEN_API_PATH: process.env.CSRF_TOKEN_API_PATH,
// REFRESH_ACCESS_TOKEN_ENDPOINT: process.env.REFRESH_ACCESS_TOKEN_ENDPOINT,
// DATA_API_BASE_URL: process.env.DATA_API_BASE_URL,
// SECURE_COOKIES: process.env.NODE_ENV !== 'development',
SEGMENT_KEY: process.env.SEGMENT_KEY,
// 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 || '',
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',
NON_BROWSABLE_COURSES: process.env.NON_BROWSABLE_COURSES === 'true',
};
const features = {};
export { configuration, features };

View File

@@ -1 +0,0 @@
export const appId = 'org.openedx.frontend.app.learnerDashboard';

View File

@@ -0,0 +1,13 @@
import PropTypes from 'prop-types';
export const AppWrapper = ({
children,
}) => children;
AppWrapper.propTypes = {
children: PropTypes.oneOfType([
PropTypes.node,
PropTypes.arrayOf(PropTypes.node),
]).isRequired,
};
export default AppWrapper;

View File

@@ -1,11 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@openedx/frontend-base';
import track from '../../../../tracking';
import { reduxHooks } from '../../../../hooks';
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';

View File

@@ -1,18 +1,18 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@openedx/frontend-base';
import { reduxHooks } from '@src/hooks';
import track from '@src/tracking';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks';
import track from 'tracking';
import useActionDisabledState from '../hooks';
import BeginCourseButton from './BeginCourseButton';
jest.mock('@src/tracking', () => ({
jest.mock('tracking', () => ({
course: {
enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'),
},
}));
jest.mock('@src/hooks', () => ({
jest.mock('hooks', () => ({
reduxHooks: {
useCardCourseRunData: jest.fn(),
useCardExecEdTrackingParam: jest.fn(),

View File

@@ -1,11 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@openedx/frontend-base';
import track from '../../../../tracking';
import { reduxHooks } from '../../../../hooks';
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';

View File

@@ -1,19 +1,19 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@openedx/frontend-base';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from '@src/hooks';
import track from '@src/tracking';
import { reduxHooks } from 'hooks';
import track from 'tracking';
import useActionDisabledState from '../hooks';
import ResumeButton from './ResumeButton';
jest.mock('@src/tracking', () => ({
jest.mock('tracking', () => ({
course: {
enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'),
},
}));
jest.mock('@src/hooks', () => ({
jest.mock('hooks', () => ({
reduxHooks: {
useCardCourseRunData: jest.fn(),
useCardExecEdTrackingParam: jest.fn(),

View File

@@ -1,10 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@openedx/frontend-base';
import { reduxHooks } from '../../../../hooks';
import { useIntl } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks';
import useActionDisabledState from '../hooks';
import ActionButton from './ActionButton';
import messages from './messages';

View File

@@ -1,13 +1,13 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@openedx/frontend-base';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from '@src/hooks';
import { reduxHooks } from 'hooks';
import useActionDisabledState from '../hooks';
import SelectSessionButton from './SelectSessionButton';
jest.mock('@src/hooks', () => ({
jest.mock('hooks', () => ({
reduxHooks: {
useUpdateSelectSessionModalCallback: jest.fn(),
},

View File

@@ -1,11 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@openedx/frontend-base';
import track from '../../../../tracking';
import { reduxHooks } from '../../../../hooks';
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';

View File

@@ -1,19 +1,19 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@openedx/frontend-base';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import track from '@src/tracking';
import { reduxHooks } from '@src/hooks';
import track from 'tracking';
import { reduxHooks } from 'hooks';
import useActionDisabledState from '../hooks';
import ViewCourseButton from './ViewCourseButton';
jest.mock('@src/tracking', () => ({
jest.mock('tracking', () => ({
course: {
enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'),
},
}));
jest.mock('@src/hooks', () => ({
jest.mock('hooks', () => ({
reduxHooks: {
useCardCourseRunData: jest.fn(() => ({ homeUrl: 'homeUrl' })),
useTrackCourseEvent: jest.fn(),

View File

@@ -3,9 +3,9 @@ import PropTypes from 'prop-types';
import { ActionRow } from '@openedx/paragon';
import { reduxHooks } from '../../../../hooks';
import CourseCardActionSlot from '../../../../slots/CourseCardActionSlot';
import { reduxHooks } from 'hooks';
import CourseCardActionSlot from 'plugin-slots/CourseCardActionSlot';
import SelectSessionButton from './SelectSessionButton';
import BeginCourseButton from './BeginCourseButton';
import ResumeButton from './ResumeButton';

View File

@@ -1,9 +1,9 @@
import { render, screen } from '@testing-library/react';
import { reduxHooks } from '@src/hooks';
import { reduxHooks } from 'hooks';
import CourseCardActions from '.';
jest.mock('@src/hooks', () => ({
jest.mock('hooks', () => ({
reduxHooks: {
useCardCourseRunData: jest.fn(),
useCardEnrollmentData: jest.fn(),
@@ -12,7 +12,7 @@ jest.mock('@src/hooks', () => ({
},
}));
jest.mock('@src/slots/CourseCardActionSlot', () => jest.fn(() => <div>CourseCardActionSlot</div>));
jest.mock('plugin-slots/CourseCardActionSlot', () => jest.fn(() => <div>CourseCardActionSlot</div>));
jest.mock('./SelectSessionButton', () => jest.fn(() => <div>SelectSessionButton</div>));
jest.mock('./ViewCourseButton', () => jest.fn(() => <div>ViewCourseButton</div>));
jest.mock('./BeginCourseButton', () => jest.fn(() => <div>BeginCourseButton</div>));

View File

@@ -1,4 +1,4 @@
import { defineMessages } from '@openedx/frontend-base';
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
beginCourse: {

View File

@@ -4,10 +4,10 @@ import PropTypes from 'prop-types';
import { MailtoLink, Hyperlink } from '@openedx/paragon';
import { CheckCircle } from '@openedx/paragon/icons';
import { useIntl } from '@openedx/frontend-base';
import { useIntl } from '@edx/frontend-platform/i18n';
import { utilHooks, reduxHooks } from '../../../../hooks';
import Banner from '../../../../components/Banner';
import { utilHooks, reduxHooks } from 'hooks';
import Banner from 'components/Banner';
import messages from './messages';
@@ -31,7 +31,7 @@ export const CertificateBanner = ({ cardId }) => {
if (certificate.isRestricted) {
return (
<Banner variant="danger">
{supportEmail ? formatMessage(messages.certRestricted, { supportEmail: emailLink(supportEmail) }) : formatMessage(messages.certRestrictedNoEmail)}
{ supportEmail ? formatMessage(messages.certRestricted, { supportEmail: emailLink(supportEmail) }) : formatMessage(messages.certRestrictedNoEmail)}
{isVerified && ' '}
{isVerified && (billingEmail ? formatMessage(messages.certRefundContactBilling, { billingEmail: emailLink(billingEmail) }) : formatMessage(messages.certRefundContactBillingNoEmail))}
</Banner>

View File

@@ -1,10 +1,10 @@
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from '@src/hooks';
import { reduxHooks } from 'hooks';
import CertificateBanner from './CertificateBanner';
jest.mock('@src/hooks', () => ({
jest.mock('hooks', () => ({
utilHooks: {
useFormatDate: jest.fn(() => date => date),
},

View File

@@ -2,11 +2,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Hyperlink } from '@openedx/paragon';
import { useIntl } from '@openedx/frontend-base';
import { utilHooks, reduxHooks } from '../../../../hooks';
import Banner from '../../../../components/Banner';
import { useIntl } from '@edx/frontend-platform/i18n';
import { utilHooks, reduxHooks } from 'hooks';
import Banner from 'components/Banner';
import messages from './messages';
export const CourseBanner = ({ cardId }) => {

View File

@@ -1,13 +1,13 @@
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from '@src/hooks';
import { formatMessage } from '@src/testUtils';
import { reduxHooks } from 'hooks';
import { formatMessage } from 'testUtils';
import { CourseBanner } from './CourseBanner';
import messages from './messages';
jest.mock('@src/hooks', () => ({
jest.mock('hooks', () => ({
utilHooks: {
useFormatDate: () => date => date,
},

View File

@@ -1,5 +1,6 @@
import { StrictDict } from '../../../../../utils';
import { reduxHooks } from '../../../../../hooks';
import { StrictDict } from 'utils';
import { reduxHooks } from 'hooks';
import ApprovedContent from './views/ApprovedContent';
import EligibleContent from './views/EligibleContent';
@@ -16,9 +17,7 @@ export const statusComponents = StrictDict({
export const useCreditBannerData = (cardId) => {
const credit = reduxHooks.useCardCreditData(cardId);
const { supportEmail } = reduxHooks.usePlatformSettingsData();
if (!credit.isEligible) {
return null;
}
if (!credit.isEligible) { return null; }
const { error, purchased, requestStatus } = credit;
let ContentComponent = EligibleContent;

View File

@@ -1,5 +1,5 @@
import { keyStore } from '@src/utils';
import { reduxHooks } from '@src/hooks';
import { keyStore } from 'utils';
import { reduxHooks } from 'hooks';
import ApprovedContent from './views/ApprovedContent';
import EligibleContent from './views/EligibleContent';
@@ -9,7 +9,7 @@ import RejectedContent from './views/RejectedContent';
import * as hooks from './hooks';
jest.mock('@src/hooks', () => ({
jest.mock('hooks', () => ({
reduxHooks: {
useCardCreditData: jest.fn(),
usePlatformSettingsData: jest.fn(),

View File

@@ -1,9 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@openedx/frontend-base';
import { useIntl } from '@edx/frontend-platform/i18n';
import Banner from '../../../../../components/Banner';
import Banner from 'components/Banner';
import { MailtoLink } from '@openedx/paragon';
import hooks from './hooks';

View File

@@ -1,5 +1,5 @@
import { screen, render } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import hooks from './hooks';
import { CreditBanner } from '.';

View File

@@ -1,14 +1,14 @@
import { defineMessages } from '@openedx/frontend-base';
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
error: {
id: 'learner-dash.courseCard.banners.credit.error',
description: 'Error message for credit transaction with support email link',
description: '',
defaultMessage: 'An error occurred with this transaction. For help, contact {supportEmailLink}.',
},
errorNoEmail: {
id: 'learner-dash.courseCard.banners.credit.errorNoEmail',
description: 'Error message for credit transaction without support email',
description: '',
defaultMessage: 'An error occurred with this transaction.',
},
});

View File

@@ -1,9 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useContext } from 'react';
import { useIntl } from '@openedx/frontend-base';
import MasqueradeUserContext from '../../../../../../data/contexts/MasqueradeUserContext';
import { reduxHooks } from '../../../../../../hooks';
import { useIntl } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks';
import CreditContent from './components/CreditContent';
import ProviderLink from './components/ProviderLink';
@@ -11,7 +11,7 @@ import messages from './messages';
export const ApprovedContent = ({ cardId }) => {
const { providerStatusUrl: href, providerName } = reduxHooks.useCardCreditData(cardId);
const { isMasquerading } = useContext(MasqueradeUserContext);
const { isMasquerading } = reduxHooks.useMasqueradeData();
const { formatMessage } = useIntl();
return (
<CreditContent

View File

@@ -1,14 +1,14 @@
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base';
import { formatMessage } from '@src/testUtils';
import { reduxHooks } from '@src/hooks';
import MasqueradeUserContext from '@src/data/contexts/MasqueradeUserContext';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils';
import { reduxHooks } from 'hooks';
import messages from './messages';
import ApprovedContent from './ApprovedContent';
jest.mock('@src/hooks', () => ({
jest.mock('hooks', () => ({
reduxHooks: {
useCardCreditData: jest.fn(),
useMasqueradeData: jest.fn(),
},
}));
@@ -18,19 +18,12 @@ const credit = {
providerName: 'test-credit-provider-name',
};
reduxHooks.useCardCreditData.mockReturnValue(credit);
const renderWithMasquerading = (isMasquerading = false) => render(
<IntlProvider locale="en">
<MasqueradeUserContext.Provider value={{ isMasquerading }}>
<ApprovedContent cardId={cardId} />
</MasqueradeUserContext.Provider>
</IntlProvider>
);
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
describe('ApprovedContent component', () => {
describe('hooks', () => {
it('initializes credit data with cardId', () => {
renderWithMasquerading();
render(<IntlProvider locale="en"><ApprovedContent cardId={cardId} /></IntlProvider>);
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
});
});
@@ -38,7 +31,7 @@ describe('ApprovedContent component', () => {
describe('rendered CreditContent component', () => {
beforeEach(() => {
jest.clearAllMocks();
renderWithMasquerading();
render(<IntlProvider locale="en"><ApprovedContent cardId={cardId} /></IntlProvider>);
});
it('action.message is formatted viewCredit message', () => {
const actionButton = screen.getByRole('link', { name: messages.viewCredit.defaultMessage });
@@ -63,7 +56,8 @@ describe('ApprovedContent component', () => {
});
describe('when masquerading', () => {
beforeEach(() => {
renderWithMasquerading(true);
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: true });
render(<IntlProvider locale="en"><ApprovedContent cardId={cardId} /></IntlProvider>);
});
it('disables the action button', () => {

View File

@@ -1,10 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@openedx/frontend-base';
import { useIntl } from '@edx/frontend-platform/i18n';
import { reduxHooks } from '../../../../../../hooks';
import track from '../../../../../../tracking';
import { reduxHooks } from 'hooks';
import track from 'tracking';
import CreditContent from './components/CreditContent';
import messages from './messages';

View File

@@ -1,21 +1,21 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@openedx/frontend-base';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from '@src/hooks';
import track from '@src/tracking';
import { reduxHooks } from 'hooks';
import track from 'tracking';
import messages from './messages';
import EligibleContent from './EligibleContent';
jest.mock('@src/hooks', () => ({
jest.mock('hooks', () => ({
reduxHooks: {
useCardCreditData: jest.fn(),
useCardCourseRunData: jest.fn(),
},
}));
jest.mock('@src/tracking', () => ({
jest.mock('tracking', () => ({
credit: {
purchase: jest.fn(),
},

View File

@@ -1,8 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useContext } from 'react';
import { useIntl } from '@openedx/frontend-base';
import MasqueradeUserContext from '../../../../../../data/contexts/MasqueradeUserContext';
import { useIntl } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks';
import CreditContent from './components/CreditContent';
import ProviderLink from './components/ProviderLink';
import hooks from './hooks';
@@ -12,7 +13,7 @@ import messages from './messages';
export const MustRequestContent = ({ cardId }) => {
const { formatMessage } = useIntl();
const { requestData, createCreditRequest } = hooks.useCreditRequestData(cardId);
const { isMasquerading } = useContext(MasqueradeUserContext);
const { isMasquerading } = reduxHooks.useMasqueradeData();
return (
<CreditContent
action={{

View File

@@ -1,9 +1,8 @@
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import userEvent from '@testing-library/user-event';
import { reduxHooks } from '@src/hooks';
import MasqueradeUserContext from '@src/data/contexts/MasqueradeUserContext';
import { reduxHooks } from 'hooks';
import messages from './messages';
import hooks from './hooks';
import MustRequestContent from './MustRequestContent';
@@ -12,8 +11,9 @@ jest.mock('./hooks', () => ({
useCreditRequestData: jest.fn(),
}));
jest.mock('@src/hooks', () => ({
jest.mock('hooks', () => ({
reduxHooks: {
useMasqueradeData: jest.fn(),
useCardCreditData: jest.fn(),
},
}));
@@ -31,11 +31,9 @@ const providerName = 'test-credit-provider-name';
const providerStatusUrl = 'test-credit-provider-status-url';
const createCreditRequest = jest.fn().mockName('createCreditRequest');
const renderMustRequestContent = (isMasquerading = false) => render(
const renderMustRequestContent = () => render(
<IntlProvider locale="en" messages={messages}>
<MasqueradeUserContext.Provider value={{ isMasquerading }}>
<MustRequestContent cardId={cardId} />
</MasqueradeUserContext.Provider>
<MustRequestContent cardId={cardId} />
</IntlProvider>,
);
@@ -46,6 +44,7 @@ describe('MustRequestContent component', () => {
requestData,
createCreditRequest,
});
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
reduxHooks.useCardCreditData.mockReturnValue({
providerName,
providerStatusUrl,
@@ -91,13 +90,14 @@ describe('MustRequestContent component', () => {
describe('when masquerading', () => {
beforeEach(() => {
renderMustRequestContent(true);
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: true });
renderMustRequestContent();
});
it('disables the request credit button', () => {
const button = screen.getByRole('button', { name: /request credit/i });
expect(button).toHaveAttribute('aria-disabled', 'true');
expect(button).toHaveClass('disabled');
expect(button).toHaveAttribute('aria-disabled', 'true');
});
});
});

View File

@@ -1,15 +1,15 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useContext } from 'react';
import { useIntl } from '@openedx/frontend-base';
import MasqueradeUserContext from '../../../../../../data/contexts/MasqueradeUserContext';
import { reduxHooks } from '../../../../../../hooks';
import { useIntl } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks';
import CreditContent from './components/CreditContent';
import messages from './messages';
export const PendingContent = ({ cardId }) => {
const { providerStatusUrl: href, providerName } = reduxHooks.useCardCreditData(cardId);
const { isMasquerading } = useContext(MasqueradeUserContext);
const { isMasquerading } = reduxHooks.useMasqueradeData();
const { formatMessage } = useIntl();
return (
<CreditContent

View File

@@ -1,14 +1,13 @@
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from '@src/hooks';
import MasqueradeUserContext from '@src/data/contexts/MasqueradeUserContext';
import { reduxHooks } from 'hooks';
import messages from './messages';
import PendingContent from './PendingContent';
jest.mock('@src/hooks', () => ({
reduxHooks: { useCardCreditData: jest.fn() },
jest.mock('hooks', () => ({
reduxHooks: { useCardCreditData: jest.fn(), useMasqueradeData: jest.fn() },
}));
const cardId = 'test-card-id';
@@ -18,12 +17,11 @@ reduxHooks.useCardCreditData.mockReturnValue({
providerName,
providerStatusUrl,
});
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
const renderPendingContent = (isMasquerading = false) => render(
const renderPendingContent = () => render(
<IntlProvider messages={{}} locale="en">
<MasqueradeUserContext.Provider value={{ isMasquerading }}>
<PendingContent cardId={cardId} />
</MasqueradeUserContext.Provider>
<PendingContent cardId={cardId} />
</IntlProvider>,
);
describe('PendingContent component', () => {
@@ -58,9 +56,9 @@ describe('PendingContent component', () => {
});
describe('when masqueradeData is true', () => {
it('disables the view details button', () => {
renderPendingContent(true);
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: true });
renderPendingContent();
const button = screen.getByRole('link', { name: messages.viewDetails.defaultMessage });
expect(button).toHaveAttribute('aria-disabled', 'true');
expect(button).toHaveClass('disabled');
});
});

View File

@@ -1,9 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@openedx/frontend-base';
import { useIntl } from '@edx/frontend-platform/i18n';
import { reduxHooks } from '../../../../../../hooks';
import { reduxHooks } from 'hooks';
import CreditContent from './components/CreditContent';
import ProviderLink from './components/ProviderLink';
import messages from './messages';

View File

@@ -1,10 +1,10 @@
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from '@src/hooks';
import { reduxHooks } from 'hooks';
import RejectedContent from './RejectedContent';
jest.mock('@src/hooks', () => ({
jest.mock('hooks', () => ({
reduxHooks: {
useCardCreditData: jest.fn(),
},

View File

@@ -1,6 +1,6 @@
import { render } from '@testing-library/react';
import { keyStore } from '@src/utils';
import { keyStore } from 'utils';
import useCreditRequestFormData from './hooks';
import CreditRequestForm from '.';

View File

@@ -2,7 +2,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { reduxHooks } from '../../../../../../../hooks';
import { reduxHooks } from 'hooks';
import { Hyperlink } from '@openedx/paragon';
export const ProviderLink = ({ cardId }) => {

View File

@@ -1,10 +1,10 @@
import { render, screen } from '@testing-library/react';
import { reduxHooks } from '@src/hooks';
import { IntlProvider } from '@openedx/frontend-base';
import { reduxHooks } from 'hooks';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import ProviderLink from './ProviderLink';
jest.mock('@src/hooks', () => ({
jest.mock('hooks', () => ({
reduxHooks: {
useCardCreditData: jest.fn(),
},

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { StrictDict } from '../../../../../../utils';
import { apiHooks } from '../../../../../../hooks';
import { StrictDict } from 'utils';
import { apiHooks } from 'hooks';
import * as module from './hooks';

View File

@@ -1,8 +1,8 @@
import { MockUseState } from '@src/testUtils';
import { apiHooks } from '@src/hooks';
import { MockUseState } from 'testUtils';
import { apiHooks } from 'hooks';
import * as hooks from './hooks';
jest.mock('@src/hooks', () => ({
jest.mock('hooks', () => ({
apiHooks: {
useCreateCreditRequest: jest.fn(),
},

View File

@@ -1,59 +1,59 @@
import { defineMessages } from '@openedx/frontend-base';
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
approved: {
id: 'learner-dash.courseCard.banners.credit.approved',
description: 'Message shown when credit request has been approved',
description: '',
defaultMessage: '{congratulations} {providerName} has approved your request for course credit. To see your course credit, visit the {linkToProviderSite} website.',
},
congratulations: {
id: 'learner-dash.courseCard.banners.credit.congratulations',
description: 'Congratulatory message for credit approval',
description: '',
defaultMessage: 'Congratulations!',
},
eligible: {
id: 'learner-dash.courseCard.banners.credit.eligible',
description: 'Message shown when user is eligible to purchase course credit',
description: '',
defaultMessage: 'You have completed this course and are eligible to purchase course credit. Select {getCredit} to get started.',
},
eligibleFromProvider: {
id: 'learner-dash.courseCard.banners.credit.eligibleFromProvider',
description: 'Message shown when user is eligible for credit from a specific provider',
description: '',
defaultMessage: 'You are now eligible for credit from {providerName}. Congratulations!',
},
getCredit: {
id: 'learner-dash.courseCard.banners.credit.getCredit',
description: 'Button text for initiating the credit process',
description: '',
defaultMessage: 'Get Credit',
},
mustRequest: {
id: 'learner-dash.courseCard.banners.credit.mustRequest',
description: 'Message shown after payment to instruct user to request credit',
description: '',
defaultMessage: 'Thank you for your payment. To receive course credit, you must request credit at the {linkToProviderSite} website. Select {requestCredit} to get started',
},
received: {
id: 'learner-dash.courseCard.banners.credit.received',
description: 'Message shown when credit request has been received',
description: '',
defaultMessage: '{providerName} has received your course credit request. We will update you when credit processing is complete.',
},
rejected: {
id: 'learner-dash.courseCard.banners.credit.rejected',
description: 'Message shown when credit request has been rejected',
description: '',
defaultMessage: '{providerName} did not approve your request for course credit. For more information, contact {linkToProviderSite} directly.',
},
requestCredit: {
id: 'learner-dash.courseCard.banners.credit.requestCredit',
description: 'Button text for requesting credit',
description: '',
defaultMessage: 'Request Credit',
},
viewCredit: {
id: 'learner-dash.courseCard.banners.credit.viewCredit',
description: 'Button text for viewing credit details',
description: '',
defaultMessage: 'View Credit',
},
viewDetails: {
id: 'learner-dash.courseCard.banners.credit.viewDetails',
description: 'Button text for viewing credit request details',
description: '',
defaultMessage: 'View Details',
},
});

View File

@@ -1,12 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@openedx/frontend-base';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button, MailtoLink } from '@openedx/paragon';
import { utilHooks, reduxHooks } from '../../../../hooks';
import Banner from '../../../../components/Banner';
import { utilHooks, reduxHooks } from 'hooks';
import Banner from 'components/Banner';
import messages from './messages';
export const EntitlementBanner = ({ cardId }) => {

View File

@@ -1,12 +1,12 @@
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base';
import { formatMessage } from '@src/testUtils';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils';
import { reduxHooks } from '@src/hooks';
import { reduxHooks } from 'hooks';
import EntitlementBanner from './EntitlementBanner';
import messages from './messages';
jest.mock('@src/hooks', () => ({
jest.mock('hooks', () => ({
utilHooks: {
useFormatDate: () => date => date,
},

View File

@@ -2,10 +2,10 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Program } from '@openedx/paragon/icons';
import { useIntl } from '@openedx/frontend-base';
import { useIntl } from '@edx/frontend-platform/i18n';
import { reduxHooks } from '../../../../../hooks';
import Banner from '../../../../../components/Banner';
import { reduxHooks } from 'hooks';
import Banner from 'components/Banner';
import ProgramList from './ProgramsList';
import messages from './messages';

View File

@@ -1,10 +1,10 @@
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from '@src/hooks';
import { reduxHooks } from 'hooks';
import RelatedProgramsBanner from '.';
jest.mock('@src/hooks', () => ({
jest.mock('hooks', () => ({
reduxHooks: {
useCardRelatedProgramsData: jest.fn(),
},

View File

@@ -1,4 +1,4 @@
import { defineMessages } from '@openedx/frontend-base';
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
relatedPrograms: {

View File

@@ -1,9 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import { reduxHooks } from '../../../../hooks';
import CourseBannerSlot from '../../../../slots/CourseBannerSlot';
import { reduxHooks } from 'hooks';
import CourseBannerSlot from 'plugin-slots/CourseBannerSlot';
import CertificateBanner from './CertificateBanner';
import CreditBanner from './CreditBanner';
import EntitlementBanner from './EntitlementBanner';

View File

@@ -1,8 +1,8 @@
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base';
import { MemoryRouter } from 'react-router-dom';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks';
import { reduxHooks } from '@src/hooks';
import CourseCardBanners from '.';
jest.mock('./CourseBanner', () => jest.fn(() => <div>CourseBanner</div>));
@@ -19,7 +19,7 @@ const mockedComponents = [
'RelatedProgramsBanner',
];
jest.mock('@src/hooks', () => ({
jest.mock('hooks', () => ({
reduxHooks: {
useCardEnrollmentData: jest.fn(() => ({ isEnrolled: true })),
},
@@ -30,14 +30,7 @@ describe('CourseCardBanners', () => {
cardId: 'test-card-id',
};
it('renders default CourseCardBanners', () => {
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isEnrolled: true });
render(
<MemoryRouter>
<IntlProvider locale="en">
<CourseCardBanners {...props} />
</IntlProvider>
</MemoryRouter>
);
render(<IntlProvider locale="en"><CourseCardBanners {...props} /></IntlProvider>);
mockedComponents.map((componentName) => {
const mockedComponent = screen.getByText(componentName);
return expect(mockedComponent).toBeInTheDocument();
@@ -45,13 +38,7 @@ describe('CourseCardBanners', () => {
});
it('render with isEnrolled false', () => {
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isEnrolled: false });
render(
<MemoryRouter>
<IntlProvider locale="en">
<CourseCardBanners {...props} />
</IntlProvider>
</MemoryRouter>
);
render(<IntlProvider locale="en"><CourseCardBanners {...props} /></IntlProvider>);
const mockedComponentsIfNotEnrolled = mockedComponents.slice(-2);
mockedComponentsIfNotEnrolled.map((componentName) => {
const mockedComponent = screen.getByText(componentName);

View File

@@ -1,4 +1,4 @@
import { defineMessages } from '@openedx/frontend-base';
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
auditAccessExpired: {

View File

@@ -1,5 +1,5 @@
import { useIntl } from '@openedx/frontend-base';
import { utilHooks, reduxHooks } from '../../../../hooks';
import { useIntl } from '@edx/frontend-platform/i18n';
import { utilHooks, reduxHooks } from 'hooks';
import * as hooks from './hooks';
import messages from './messages';
@@ -10,10 +10,8 @@ export const useAccessMessage = ({ cardId }) => {
const courseRun = reduxHooks.useCardCourseRunData(cardId);
const formatDate = utilHooks.useFormatDate();
if (!courseRun.isStarted) {
if (!courseRun.startDate && !courseRun.advertisedStart) {
return null;
}
const startDate = courseRun.advertisedStart ?? formatDate(courseRun.startDate);
if (!courseRun.startDate && !courseRun.advertisedStart) { return null; }
const startDate = courseRun.advertisedStart ? courseRun.advertisedStart : formatDate(courseRun.startDate);
return formatMessage(messages.courseStarts, { startDate });
}
if (enrollment.isEnrolled) {
@@ -29,9 +27,7 @@ export const useAccessMessage = ({ cardId }) => {
{ accessExpirationDate: formatDate(accessExpirationDate) },
);
}
if (!endDate) {
return null;
}
if (!endDate) { return null; }
return formatMessage(
isArchived ? messages.courseEnded : messages.courseEnds,
{ endDate: formatDate(endDate) },
@@ -53,7 +49,7 @@ export const useCardDetailsData = ({ cardId }) => {
const openSessionModal = reduxHooks.useUpdateSelectSessionModalCallback(cardId);
return {
providerName: providerName ?? formatMessage(messages.unknownProviderName),
providerName: providerName || formatMessage(messages.unknownProviderName),
accessMessage: hooks.useAccessMessage({ cardId }),
isEntitlement,
isFulfilled,

View File

@@ -1,11 +1,12 @@
import { useIntl } from '@openedx/frontend-base';
import { useIntl } from '@edx/frontend-platform/i18n';
import { keyStore } from 'utils';
import { utilHooks, reduxHooks } from 'hooks';
import { keyStore } from '@src/utils';
import { utilHooks, reduxHooks } from '@src/hooks';
import * as hooks from './hooks';
import messages from './messages';
jest.mock('@src/hooks', () => ({
jest.mock('hooks', () => ({
utilHooks: {
useFormatDate: jest.fn(),
},
@@ -19,10 +20,10 @@ jest.mock('@src/hooks', () => ({
},
}));
jest.mock('@openedx/frontend-base', () => {
const { formatMessage } = jest.requireActual('@src/testUtils');
jest.mock('@edx/frontend-platform/i18n', () => {
const { formatMessage } = jest.requireActual('testUtils');
return {
...jest.requireActual('@openedx/frontend-base'),
...jest.requireActual('@edx/frontend-platform/i18n'),
useIntl: () => ({
formatMessage,
}),
@@ -45,8 +46,9 @@ describe('CourseCardDetails hooks', () => {
});
describe('useCardDetailsData', () => {
const providerName = 'my-provider-name';
const providerData = {};
const providerData = {
name: 'my-provider-name',
};
const entitlementData = {
isEntitlement: false,
disableViewCourse: false,
@@ -76,10 +78,8 @@ describe('CourseCardDetails hooks', () => {
expect(out.accessMessage).toEqual(mockAccessMessage({ cardId }));
});
it('forwards provider name if it exists, else formatted unknown provider name', () => {
runHook({ provider: { name: providerName } });
expect(out.providerName).toEqual(providerName);
runHook({ provider: {} });
expect(out.providerName).toEqual(providerData.name);
runHook({ provider: { name: '' } });
expect(out.providerName).toEqual(formatMessage(messages.unknownProviderName));
});
it('forward changeOrLeaveSessionMessage', () => {

View File

@@ -1,4 +1,4 @@
import { defineMessages } from '@openedx/frontend-base';
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
accessExpired: {

View File

@@ -1,12 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@openedx/frontend-base';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Badge } from '@openedx/paragon';
import track from '../../../tracking';
import { reduxHooks } from '../../../hooks';
import verifiedRibbon from '../../../assets/verified-ribbon.png';
import track from 'tracking';
import { reduxHooks } from 'hooks';
import verifiedRibbon from 'assets/verified-ribbon.png';
import useActionDisabledState from './hooks';
import messages from '../messages';

View File

@@ -1,7 +1,7 @@
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base';
import { formatMessage } from '@src/testUtils';
import { reduxHooks } from '@src/hooks';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils';
import { reduxHooks } from 'hooks';
import useActionDisabledState from './hooks';
import { CourseCardImage } from './CourseCardImage';
import messages from '../messages';
@@ -9,7 +9,7 @@ import messages from '../messages';
const homeUrl = 'https://example.com';
const bannerImgSrc = 'banner-img-src.jpg';
jest.mock('@src/hooks', () => ({
jest.mock('hooks', () => ({
reduxHooks: {
useCardCourseData: jest.fn(() => ({ bannerImgSrc })),
useCardCourseRunData: jest.fn(() => ({ homeUrl })),

View File

@@ -1,13 +1,12 @@
import { useContext } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import * as ReactShare from 'react-share';
import { useIntl } from '@openedx/frontend-base';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Dropdown } from '@openedx/paragon';
import MasqueradeUserContext from '../../../../data/contexts/MasqueradeUserContext';
import track from '../../../../tracking';
import { reduxHooks } from '../../../../hooks';
import track from 'tracking';
import { reduxHooks } from 'hooks';
import messages from './messages';
@@ -21,7 +20,7 @@ export const SocialShareMenu = ({ cardId, emailSettings }) => {
const { courseName } = reduxHooks.useCardCourseData(cardId);
const { isEmailEnabled, isExecEd2UCourse } = reduxHooks.useCardEnrollmentData(cardId);
const { twitter, facebook } = reduxHooks.useCardSocialSettingsData(cardId);
const { isMasquerading } = useContext(MasqueradeUserContext);
const { isMasquerading } = reduxHooks.useMasqueradeData();
const handleTwitterShare = reduxHooks.useTrackCourseEvent(track.socialShare, cardId, 'twitter');
const handleFacebookShare = reduxHooks.useTrackCourseEvent(track.socialShare, cardId, 'facebook');

View File

@@ -1,20 +1,22 @@
import { IntlProvider } from '@openedx/frontend-base';
import { render, screen } from '@testing-library/react';
import { when } from 'jest-when';
import track from '@src/tracking';
import { reduxHooks } from '@src/hooks';
import MasqueradeUserContext from '@src/data/contexts/MasqueradeUserContext';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { render, screen } from '@testing-library/react';
import track from 'tracking';
import { reduxHooks } from 'hooks';
import { useEmailSettings } from './hooks';
import SocialShareMenu from './SocialShareMenu';
import messages from './messages';
jest.mock('@src/tracking', () => ({
jest.mock('tracking', () => ({
socialShare: 'test-social-share-key',
}));
jest.mock('@src/hooks', () => ({
jest.mock('hooks', () => ({
reduxHooks: {
useMasqueradeData: jest.fn(),
useCardCourseData: jest.fn(),
useCardEnrollmentData: jest.fn(),
useCardSocialSettingsData: jest.fn(),
@@ -63,6 +65,7 @@ const mockHooks = (returnVals = {}) => {
{ isCardHook: true },
);
mockHook(reduxHooks.useCardCourseData, { courseName }, { isCardHook: true });
mockHook(reduxHooks.useMasqueradeData, { isMasquerading: !!returnVals.isMasquerading });
mockHook(
reduxHooks.useCardSocialSettingsData,
{
@@ -73,13 +76,7 @@ const mockHooks = (returnVals = {}) => {
);
};
const renderComponent = (isMasquerading = false) => render(
<IntlProvider locale="en">
<MasqueradeUserContext.Provider value={{ isMasquerading }}>
<SocialShareMenu {...props} />
</MasqueradeUserContext.Provider>
</IntlProvider>,
);
const renderComponent = () => render(<IntlProvider locale="en"><SocialShareMenu {...props} /></IntlProvider>);
describe('SocialShareMenu', () => {
describe('behavior', () => {
@@ -94,6 +91,7 @@ describe('SocialShareMenu', () => {
when(reduxHooks.useCardEnrollmentData).expectCalledWith(props.cardId);
when(reduxHooks.useCardCourseData).expectCalledWith(props.cardId);
when(reduxHooks.useCardSocialSettingsData).expectCalledWith(props.cardId);
when(reduxHooks.useMasqueradeData).expectCalledWith();
when(reduxHooks.useTrackCourseEvent).expectCalledWith(track.socialShare, props.cardId, 'twitter');
when(reduxHooks.useTrackCourseEvent).expectCalledWith(track.socialShare, props.cardId, 'facebook');
});
@@ -118,9 +116,7 @@ describe('SocialShareMenu', () => {
if (isMasquerading) {
it('is disabled', () => {
const emailSettingsButton = screen.getByRole('button', { name: messages.emailSettings.defaultMessage });
expect(emailSettingsButton).toBeInTheDocument();
expect(emailSettingsButton).toHaveAttribute('aria-disabled', 'true');
expect(emailSettingsButton).toHaveClass('disabled');
});
} else {
it('is enabled', () => {
@@ -171,8 +167,8 @@ describe('SocialShareMenu', () => {
});
describe('masquerading', () => {
beforeEach(() => {
mockHooks({ isEmailEnabled: true });
renderComponent(true);
mockHooks({ isEmailEnabled: true, isMasquerading: true });
renderComponent();
});
testEmailSettingsDropdown(true);
});

View File

@@ -1,8 +1,7 @@
import track from 'tracking';
import { reduxHooks } from 'hooks';
import { useState } from 'react';
import { reduxHooks } from '../../../../hooks';
import track from '../../../../tracking';
import { StrictDict } from '../../../../utils';
import { StrictDict } from 'utils';
export const state = StrictDict({
isUnenrollConfirmVisible: (val) => useState(val), // eslint-disable-line
@@ -33,9 +32,7 @@ export const useHandleToggleDropdown = (cardId) => {
cardId,
);
return (isOpen) => {
if (isOpen) {
trackCourseEvent();
}
if (isOpen) { trackCourseEvent(); }
};
};

View File

@@ -1,10 +1,10 @@
import { reduxHooks } from '@src/hooks';
import track from '@src/tracking';
import { MockUseState } from '@src/testUtils';
import { reduxHooks } from 'hooks';
import track from 'tracking';
import { MockUseState } from 'testUtils';
import * as hooks from './hooks';
jest.mock('@src/hooks', () => ({
jest.mock('hooks', () => ({
reduxHooks: {
useCardCertificateData: jest.fn(),
useCardEnrollmentData: jest.fn(),
@@ -68,9 +68,7 @@ describe('CourseCardMenu hooks', () => {
});
describe('useHandleToggleDropdown', () => {
beforeEach(() => {
out = hooks.useHandleToggleDropdown(cardId);
});
beforeEach(() => { out = hooks.useHandleToggleDropdown(cardId); });
describe('behavior', () => {
it('initializes course event tracker with event name and card ID', () => {
expect(reduxHooks.useTrackCourseEvent).toHaveBeenCalledWith(

View File

@@ -1,15 +1,12 @@
import { useContext } from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@openedx/frontend-base';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Dropdown, Icon, IconButton } from '@openedx/paragon';
import { MoreVert } from '@openedx/paragon/icons';
import MasqueradeUserContext from '../../../../data/contexts/MasqueradeUserContext';
import EmailSettingsModal from '../../../../containers/EmailSettingsModal';
import UnenrollConfirmModal from '../../../../containers/UnenrollConfirmModal';
import { reduxHooks } from '../../../../hooks';
import EmailSettingsModal from 'containers/EmailSettingsModal';
import UnenrollConfirmModal from 'containers/UnenrollConfirmModal';
import { reduxHooks } from 'hooks';
import SocialShareMenu from './SocialShareMenu';
import {
useEmailSettings,
@@ -31,7 +28,7 @@ export const CourseCardMenu = ({ cardId }) => {
const unenrollModal = useUnenrollData();
const handleToggleDropdown = useHandleToggleDropdown(cardId);
const { shouldShowUnenrollItem, shouldShowDropdown } = useOptionVisibility(cardId);
const { isMasquerading } = useContext(MasqueradeUserContext);
const { isMasquerading } = reduxHooks.useMasqueradeData();
const { isEmailEnabled } = reduxHooks.useCardEnrollmentData(cardId);
if (!shouldShowDropdown) {

View File

@@ -1,21 +1,23 @@
import { when } from 'jest-when';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@openedx/frontend-base';
import { reduxHooks } from '@src/hooks';
import MasqueradeUserContext from '@src/data/contexts/MasqueradeUserContext';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks';
import * as hooks from './hooks';
import CourseCardMenu from '.';
import messages from './messages';
jest.mock('@src/hooks', () => ({
jest.mock('hooks', () => ({
reduxHooks: {
useMasqueradeData: jest.fn(),
useCardEnrollmentData: jest.fn(),
},
}));
jest.mock('./SocialShareMenu', () => jest.fn(() => <div>SocialShareMenu</div>));
jest.mock('@src/containers/EmailSettingsModal', () => jest.fn(() => <div>EmailSettingsModal</div>));
jest.mock('@src/containers/UnenrollConfirmModal', () => jest.fn(() => <div>UnenrollConfirmModal</div>));
jest.mock('containers/EmailSettingsModal', () => jest.fn(() => <div>EmailSettingsModal</div>));
jest.mock('containers/UnenrollConfirmModal', () => jest.fn(() => <div>UnenrollConfirmModal</div>));
jest.mock('./hooks', () => ({
useEmailSettings: jest.fn(),
useUnenrollData: jest.fn(),
@@ -67,6 +69,7 @@ const mockHooks = (returnVals = {}) => {
},
{ isCardHook: true },
);
mockHook(reduxHooks.useMasqueradeData, { isMasquerading: !!returnVals.isMasquerading });
mockHook(
reduxHooks.useCardEnrollmentData,
{ isEmailEnabled: !!returnVals.isEmailEnabled },
@@ -74,13 +77,7 @@ const mockHooks = (returnVals = {}) => {
);
};
const renderComponent = (isMasquerading = false) => render(
<IntlProvider locale="en">
<MasqueradeUserContext.Provider value={{ isMasquerading }}>
<CourseCardMenu {...props} />
</MasqueradeUserContext.Provider>
</IntlProvider>
);
const renderComponent = () => render(<IntlProvider locale="en"><CourseCardMenu {...props} /></IntlProvider>);
describe('CourseCardMenu', () => {
describe('hooks', () => {
@@ -95,6 +92,7 @@ describe('CourseCardMenu', () => {
when(hooks.useOptionVisibility).expectCalledWith(props.cardId);
});
it('initializes redux hook data ', () => {
when(reduxHooks.useMasqueradeData).expectCalledWith();
when(reduxHooks.useCardEnrollmentData).expectCalledWith(props.cardId);
});
});
@@ -155,13 +153,14 @@ describe('CourseCardMenu', () => {
});
describe('masquerading', () => {
it('renders but unenroll is disabled', async () => {
mockHooks({ ...hookProps });
renderComponent(true);
mockHooks({ ...hookProps, isMasquerading: true });
renderComponent();
const user = userEvent.setup();
const dropdown = screen.getByRole('button', { name: messages.dropdownAlt.defaultMessage });
expect(dropdown).toBeInTheDocument();
await user.click(dropdown);
const unenrollOption = screen.getByRole('button', { name: messages.unenroll.defaultMessage });
expect(unenrollOption).toBeInTheDocument();
expect(unenrollOption).toHaveAttribute('aria-disabled', 'true');

View File

@@ -1,4 +1,4 @@
import { defineMessages } from '@openedx/frontend-base';
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
unenroll: {

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