Compare commits
128 Commits
open-relea
...
jwesson/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3663efb283 | ||
|
|
9ef5840700 | ||
|
|
3c8c92ab92 | ||
|
|
7488fe55f0 | ||
|
|
fe6c726306 | ||
|
|
8c3d62c12b | ||
|
|
66794acf17 | ||
|
|
2f3f3bcd8b | ||
|
|
dcab4f1b75 | ||
|
|
66fdd79bdf | ||
|
|
0ea9f6d193 | ||
|
|
fee6a26366 | ||
|
|
1301fddbc9 | ||
|
|
5dcd596ac9 | ||
|
|
a5522faa42 | ||
|
|
3542c38472 | ||
|
|
14c03d8461 | ||
|
|
5562d8a339 | ||
|
|
a9194261c8 | ||
|
|
11a7512fea | ||
|
|
be620a80fa | ||
|
|
1d9eb08e59 | ||
|
|
05e9626e57 | ||
|
|
fe386e31ee | ||
|
|
cb1de82f0a | ||
|
|
2337843d54 | ||
|
|
70da0d38ed | ||
|
|
154a2583f6 | ||
|
|
633050739e | ||
|
|
61d24d29f1 | ||
|
|
a210f23c9f | ||
|
|
b16908842e | ||
|
|
b80cab7a66 | ||
|
|
6b8cd1f780 | ||
|
|
78c5d73900 | ||
|
|
eb3fc9412d | ||
|
|
3caf6fd67a | ||
|
|
d48cb3d9fc | ||
|
|
d3b4a7fc84 | ||
|
|
9e63777c5c | ||
|
|
cf2f3acc51 | ||
|
|
54f8bc86e3 | ||
|
|
10961010ba | ||
|
|
3c1b749395 | ||
|
|
845ee09bf2 | ||
|
|
1efec09f44 | ||
|
|
aa1cae5200 | ||
|
|
77ab48c59f | ||
|
|
5d2b33abd3 | ||
|
|
dd4f61eec3 | ||
|
|
8f7580ec30 | ||
|
|
fbf24e42d3 | ||
|
|
e764e9c502 | ||
|
|
13721f2770 | ||
|
|
960647ce9f | ||
|
|
44c797854f | ||
|
|
3ea088e411 | ||
|
|
4a18c890c3 | ||
|
|
e1c1c51704 | ||
|
|
f83f3a1850 | ||
|
|
b26d4632c9 | ||
|
|
76783133da | ||
|
|
57d09af61d | ||
|
|
86fd29309a | ||
|
|
6a43918b56 | ||
|
|
f110a0ade8 | ||
|
|
93bd883a01 | ||
|
|
61375c9e95 | ||
|
|
c2f4be5063 | ||
|
|
14bde7fc3f | ||
|
|
b5e2a94480 | ||
|
|
edcf2fd756 | ||
|
|
9228d017af | ||
|
|
1104c58611 | ||
|
|
3d7366ac1d | ||
|
|
0f19ff9a02 | ||
|
|
a21caead92 | ||
|
|
2b287c6332 | ||
|
|
8b67abd304 | ||
|
|
abae82b507 | ||
|
|
777d3aa45c | ||
|
|
ce595d0e62 | ||
|
|
0fd242eb74 | ||
|
|
d2215570da | ||
|
|
b6bef24ace | ||
|
|
bb5a2aa3fd | ||
|
|
77d1ba93c3 | ||
|
|
4aa786c595 | ||
|
|
a5ff2eceae | ||
|
|
84b281aa51 | ||
|
|
dc5c655314 | ||
|
|
2140d8821d | ||
|
|
63860e95ce | ||
|
|
1474c4c546 | ||
|
|
e2e51dc030 | ||
|
|
604298eaca | ||
|
|
f9d13c4058 | ||
|
|
e1db6807ef | ||
|
|
d8e1f82bdf | ||
|
|
c5a78e01f2 | ||
|
|
22e4b9facc | ||
|
|
1ae555eac9 | ||
|
|
a0e5f75f0b | ||
|
|
2e101d5c23 | ||
|
|
ce1848a5c3 | ||
|
|
ee515ad666 | ||
|
|
bc449a3c34 | ||
|
|
3012f64b4b | ||
|
|
e8886c9d9d | ||
|
|
a074459e03 | ||
|
|
b87e12d2cb | ||
|
|
bf2bc405d0 | ||
|
|
9fecc65680 | ||
|
|
486a0232e3 | ||
|
|
e68dc88d6c | ||
|
|
f777eaabff | ||
|
|
36080e7074 | ||
|
|
bdeb7e1381 | ||
|
|
ecf7b56acf | ||
|
|
92a2ec1fb0 | ||
|
|
892262a107 | ||
|
|
0e10a9b34b | ||
|
|
d872a57160 | ||
|
|
0d38f107bd | ||
|
|
1217e086c0 | ||
|
|
44e3d58e14 | ||
|
|
8b52cfc4d3 | ||
|
|
c93d94035a |
3
.env
3
.env
@@ -32,7 +32,6 @@ ENTERPRISE_MARKETING_UTM_SOURCE=''
|
|||||||
ENTERPRISE_MARKETING_UTM_CAMPAIGN=''
|
ENTERPRISE_MARKETING_UTM_CAMPAIGN=''
|
||||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=''
|
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=''
|
||||||
LEARNING_BASE_URL=''
|
LEARNING_BASE_URL=''
|
||||||
ZENDESK_KEY=''
|
|
||||||
HOTJAR_APP_ID=''
|
HOTJAR_APP_ID=''
|
||||||
HOTJAR_VERSION='6'
|
HOTJAR_VERSION='6'
|
||||||
HOTJAR_DEBUG=''
|
HOTJAR_DEBUG=''
|
||||||
@@ -41,3 +40,5 @@ ACCOUNT_PROFILE_URL=''
|
|||||||
ENABLE_NOTICES=''
|
ENABLE_NOTICES=''
|
||||||
CAREER_LINK_URL=''
|
CAREER_LINK_URL=''
|
||||||
ENABLE_EDX_PERSONAL_DASHBOARD=false
|
ENABLE_EDX_PERSONAL_DASHBOARD=false
|
||||||
|
ENABLE_PROGRAMS=false
|
||||||
|
NON_BROWSABLE_COURSES=false
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ LMS_CLIENT_ID='login-service-client-id'
|
|||||||
SEGMENT_KEY=''
|
SEGMENT_KEY=''
|
||||||
FEATURE_FLAGS={}
|
FEATURE_FLAGS={}
|
||||||
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
||||||
SUPPORT_URL='http://localhost:18000/support'
|
SUPPORT_URL=''
|
||||||
CONTACT_URL='http://localhost:18000/contact'
|
CONTACT_URL='http://localhost:18000/contact'
|
||||||
OPEN_SOURCE_URL='http://localhost:18000/openedx'
|
OPEN_SOURCE_URL='http://localhost:18000/openedx'
|
||||||
TERMS_OF_SERVICE_URL='http://localhost:18000/terms-of-service'
|
TERMS_OF_SERVICE_URL='http://localhost:18000/terms-of-service'
|
||||||
@@ -38,7 +38,6 @@ ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
|
|||||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
|
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
|
||||||
LEARNING_BASE_URL='http://localhost:2000'
|
LEARNING_BASE_URL='http://localhost:2000'
|
||||||
SESSION_COOKIE_DOMAIN='localhost'
|
SESSION_COOKIE_DOMAIN='localhost'
|
||||||
ZENDESK_KEY=''
|
|
||||||
HOTJAR_APP_ID=''
|
HOTJAR_APP_ID=''
|
||||||
HOTJAR_VERSION='6'
|
HOTJAR_VERSION='6'
|
||||||
HOTJAR_DEBUG=''
|
HOTJAR_DEBUG=''
|
||||||
@@ -47,3 +46,5 @@ ACCOUNT_PROFILE_URL='http://localhost:1995'
|
|||||||
ENABLE_NOTICES=''
|
ENABLE_NOTICES=''
|
||||||
CAREER_LINK_URL=''
|
CAREER_LINK_URL=''
|
||||||
ENABLE_EDX_PERSONAL_DASHBOARD=false
|
ENABLE_EDX_PERSONAL_DASHBOARD=false
|
||||||
|
ENABLE_PROGRAMS=false
|
||||||
|
NON_BROWSABLE_COURSES=false
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ LMS_CLIENT_ID='login-service-client-id'
|
|||||||
SEGMENT_KEY=''
|
SEGMENT_KEY=''
|
||||||
FEATURE_FLAGS={}
|
FEATURE_FLAGS={}
|
||||||
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
||||||
SUPPORT_URL='http://localhost:18000/support'
|
SUPPORT_URL=''
|
||||||
CONTACT_URL='http://localhost:18000/contact'
|
CONTACT_URL='http://localhost:18000/contact'
|
||||||
OPEN_SOURCE_URL='http://localhost:18000/openedx'
|
OPEN_SOURCE_URL='http://localhost:18000/openedx'
|
||||||
TERMS_OF_SERVICE_URL='http://localhost:18000/terms-of-service'
|
TERMS_OF_SERVICE_URL='http://localhost:18000/terms-of-service'
|
||||||
@@ -37,7 +37,6 @@ ENTERPRISE_MARKETING_UTM_SOURCE='example.com'
|
|||||||
ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
|
ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
|
||||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
|
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
|
||||||
LEARNING_BASE_URL='http://localhost:2000'
|
LEARNING_BASE_URL='http://localhost:2000'
|
||||||
ZENDESK_KEY='test-zendesk-key'
|
|
||||||
HOTJAR_APP_ID='hot-jar-app-id'
|
HOTJAR_APP_ID='hot-jar-app-id'
|
||||||
HOTJAR_VERSION='6'
|
HOTJAR_VERSION='6'
|
||||||
HOTJAR_DEBUG=''
|
HOTJAR_DEBUG=''
|
||||||
@@ -46,3 +45,5 @@ ACCOUNT_PROFILE_URL='http://account-profile-url.test'
|
|||||||
ENABLE_NOTICES=''
|
ENABLE_NOTICES=''
|
||||||
CAREER_LINK_URL=''
|
CAREER_LINK_URL=''
|
||||||
ENABLE_EDX_PERSONAL_DASHBOARD=true
|
ENABLE_EDX_PERSONAL_DASHBOARD=true
|
||||||
|
ENABLE_PROGRAMS=false
|
||||||
|
NON_BROWSABLE_COURSES=false
|
||||||
|
|||||||
24
.github/workflows/ci.yml
vendored
24
.github/workflows/ci.yml
vendored
@@ -11,9 +11,6 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node: [18, 20]
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -22,7 +19,7 @@ jobs:
|
|||||||
- name: Setup Nodejs
|
- name: Setup Nodejs
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node }}
|
node-version-file: '.nvmrc'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
@@ -40,24 +37,7 @@ jobs:
|
|||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: Run Coverage
|
- name: Run Coverage
|
||||||
uses: codecov/codecov-action@v4
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
|
|
||||||
- name: Send failure notification
|
|
||||||
if: ${{ failure() }}
|
|
||||||
uses: dawidd6/action-send-mail@v3
|
|
||||||
with:
|
|
||||||
server_address: email-smtp.us-east-1.amazonaws.com
|
|
||||||
server_port: 465
|
|
||||||
username: ${{ secrets.EDX_SMTP_USERNAME }}
|
|
||||||
password: ${{ secrets.EDX_SMTP_PASSWORD }}
|
|
||||||
subject: CI workflow failed in ${{github.repository}}
|
|
||||||
to: masters-grades@edx.org,aperture@2u-internal.opsgenie.net
|
|
||||||
from: github-actions <github-actions@edx.org>
|
|
||||||
nodemailerlog: true
|
|
||||||
nodemailerdebug: true
|
|
||||||
body: CI workflow in ${{github.repository}} failed!
|
|
||||||
For details see "github.com/${{ github.repository }}/actions/runs/${{ github.run_id
|
|
||||||
}}"
|
|
||||||
|
|||||||
35
.github/workflows/npm-publish.yml
vendored
35
.github/workflows/npm-publish.yml
vendored
@@ -1,35 +0,0 @@
|
|||||||
name: Release CI
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "*"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
name: Release
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup Nodejs Env
|
|
||||||
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: ${{ env.NODE_VER }}
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Create Build
|
|
||||||
run: npm run build
|
|
||||||
|
|
||||||
- name: Release Package
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.SEMANTIC_RELEASE_GITHUB_TOKEN }}
|
|
||||||
NPM_TOKEN: ${{ secrets.SEMANTIC_RELEASE_NPM_TOKEN }}
|
|
||||||
run: npm semantic-release
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
|
||||||
|
|
||||||
npm run lint
|
|
||||||
27
.releaserc
27
.releaserc
@@ -1,27 +0,0 @@
|
|||||||
{
|
|
||||||
"branch": "master",
|
|
||||||
"tagFormat": "v${version}",
|
|
||||||
"verifyConditions": [
|
|
||||||
"@semantic-release/npm",
|
|
||||||
{
|
|
||||||
"path": "@semantic-release/github",
|
|
||||||
"assets": {
|
|
||||||
"path": "dist/*"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"analyzeCommits": "@semantic-release/commit-analyzer",
|
|
||||||
"generateNotes": "@semantic-release/release-notes-generator",
|
|
||||||
"prepare": "@semantic-release/npm",
|
|
||||||
"publish": [
|
|
||||||
"@semantic-release/npm",
|
|
||||||
{
|
|
||||||
"path": "@semantic-release/github",
|
|
||||||
"assets": {
|
|
||||||
"path": "dist/*"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"success": [],
|
|
||||||
"fail": []
|
|
||||||
}
|
|
||||||
@@ -17,6 +17,7 @@ metadata:
|
|||||||
openedx.org/arch-interest-groups: ""
|
openedx.org/arch-interest-groups: ""
|
||||||
# This can be multiple comma-separated projects.
|
# This can be multiple comma-separated projects.
|
||||||
openedx.org/add-to-projects: "openedx:23"
|
openedx.org/add-to-projects: "openedx:23"
|
||||||
|
openedx.org/release: "master"
|
||||||
spec:
|
spec:
|
||||||
type: 'service'
|
type: 'service'
|
||||||
lifecycle: 'production'
|
lifecycle: 'production'
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ module.exports = {
|
|||||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM: 'Footer',
|
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM: 'Footer',
|
||||||
LEARNING_BASE_URL: 'http://localhost:2000',
|
LEARNING_BASE_URL: 'http://localhost:2000',
|
||||||
SESSION_COOKIE_DOMAIN: 'localhost',
|
SESSION_COOKIE_DOMAIN: 'localhost',
|
||||||
ZENDESK_KEY: '',
|
|
||||||
HOTJAR_APP_ID: '',
|
HOTJAR_APP_ID: '',
|
||||||
HOTJAR_VERSION: 6,
|
HOTJAR_VERSION: 6,
|
||||||
HOTJAR_DEBUG: '',
|
HOTJAR_DEBUG: '',
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
# This file describes this Open edX repo, as described in OEP-2:
|
|
||||||
# http://open-edx-proposals.readthedocs.io/en/latest/oeps/oep-0002.html#specification
|
|
||||||
|
|
||||||
tags:
|
|
||||||
- frontend-app
|
|
||||||
- masters
|
|
||||||
oeps:
|
|
||||||
oep-2: true # Repository metadata
|
|
||||||
openedx-release: {ref: master}
|
|
||||||
13990
package-lock.json
generated
13990
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
70
package.json
70
package.json
@@ -6,6 +6,9 @@
|
|||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/edx/frontend-app-learner-dashboard.git"
|
"url": "git+https://github.com/edx/frontend-app-learner-dashboard.git"
|
||||||
},
|
},
|
||||||
|
"browserslist": [
|
||||||
|
"extends @edx/browserslist-config"
|
||||||
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "fedx-scripts webpack",
|
"build": "fedx-scripts webpack",
|
||||||
"i18n_extract": "fedx-scripts formatjs extract",
|
"i18n_extract": "fedx-scripts formatjs extract",
|
||||||
@@ -13,11 +16,11 @@
|
|||||||
"lint-fix": "fedx-scripts eslint --fix --ext .jsx,.js src/",
|
"lint-fix": "fedx-scripts eslint --fix --ext .jsx,.js src/",
|
||||||
"semantic-release": "semantic-release",
|
"semantic-release": "semantic-release",
|
||||||
"start": "fedx-scripts webpack-dev-server --progress",
|
"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",
|
"test": "TZ=GMT fedx-scripts jest --coverage --passWithNoTests",
|
||||||
"quality": "npm run lint-fix && npm run test",
|
"quality": "npm run lint-fix && npm run test",
|
||||||
"watch-tests": "jest --watch",
|
"watch-tests": "jest --watch",
|
||||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
"snapshot": "fedx-scripts jest --updateSnapshot"
|
||||||
"prepare": "husky install"
|
|
||||||
},
|
},
|
||||||
"author": "edX",
|
"author": "edX",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
@@ -27,70 +30,57 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||||
"@edx/browserslist-config": "^1.1.0",
|
"@edx/frontend-component-footer": "^14.6.0",
|
||||||
"@edx/frontend-component-header": "^5.6.0",
|
"@edx/frontend-component-header": "^6.2.0",
|
||||||
"@edx/frontend-enterprise-hotjar": "3.0.0",
|
"@edx/frontend-enterprise-hotjar": "7.2.0",
|
||||||
"@edx/frontend-platform": "8.1.2",
|
"@edx/frontend-platform": "^8.3.1",
|
||||||
"@edx/openedx-atlas": "^0.6.0",
|
"@edx/openedx-atlas": "^0.7.0",
|
||||||
"@edx/react-unit-test-utils": "3.0.0",
|
"@edx/react-unit-test-utils": "^4.0.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
||||||
"@fortawesome/free-brands-svg-icons": "^5.15.4",
|
"@fortawesome/free-brands-svg-icons": "^5.15.4",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@openedx/frontend-plugin-framework": "^1.2.0",
|
"@openedx/frontend-plugin-framework": "^1.7.0",
|
||||||
"@openedx/frontend-slot-footer": "^1.0.2",
|
"@openedx/paragon": "^22.16.0",
|
||||||
"@openedx/paragon": "^22.2.2",
|
|
||||||
"@redux-beacon/segment": "^1.1.0",
|
|
||||||
"@redux-devtools/extension": "3.3.0",
|
"@redux-devtools/extension": "3.3.0",
|
||||||
"@reduxjs/toolkit": "^1.6.1",
|
"@reduxjs/toolkit": "^2.0.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"core-js": "3.38.1",
|
"core-js": "3.42.0",
|
||||||
"dompurify": "^2.3.1",
|
"filesize": "^10.0.0",
|
||||||
"email-prop-type": "^3.0.1",
|
|
||||||
"file-saver": "^2.0.5",
|
|
||||||
"filesize": "^8.0.6",
|
|
||||||
"font-awesome": "4.7.0",
|
"font-awesome": "4.7.0",
|
||||||
"history": "5.3.0",
|
"history": "5.3.0",
|
||||||
"html-react-parser": "^1.3.0",
|
|
||||||
"jest": "^29.7.0",
|
|
||||||
"jest-environment-jsdom": "29.7.0",
|
|
||||||
"jest-when": "^3.6.0",
|
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"prop-types": "15.8.1",
|
"prop-types": "15.8.1",
|
||||||
"query-string": "7.1.3",
|
"react": "^18.3.1",
|
||||||
"react": "^17.0.2",
|
"react-dom": "^18.3.1",
|
||||||
"react-dom": "^17.0.2",
|
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-intl": "6.8.0",
|
"react-intl": "6.8.9",
|
||||||
"react-pdf": "^7.0.0",
|
|
||||||
"react-redux": "^7.2.4",
|
"react-redux": "^7.2.4",
|
||||||
"react-router-dom": "6.27.0",
|
"react-router-dom": "6.29.0",
|
||||||
"react-share": "^4.4.0",
|
"react-share": "^4.4.0",
|
||||||
"react-zendesk": "^0.1.13",
|
|
||||||
"redux": "4.2.1",
|
"redux": "4.2.1",
|
||||||
"redux-beacon": "^2.1.0",
|
|
||||||
"redux-logger": "3.0.6",
|
"redux-logger": "3.0.6",
|
||||||
"redux-thunk": "2.4.2",
|
"redux-thunk": "2.4.2",
|
||||||
"regenerator-runtime": "^0.14.0",
|
"regenerator-runtime": "^0.14.0",
|
||||||
"reselect": "^4.0.0",
|
"reselect": "^4.0.0",
|
||||||
"universal-cookie": "^4.0.4",
|
"universal-cookie": "^4.0.4",
|
||||||
"util": "^0.12.4",
|
"util": "^0.12.4"
|
||||||
"whatwg-fetch": "^3.6.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@edx/browserslist-config": "^1.3.0",
|
||||||
"@edx/reactifex": "^2.1.1",
|
"@edx/reactifex": "^2.1.1",
|
||||||
"@openedx/frontend-build": "14.1.5",
|
"@openedx/frontend-build": "^14.3.3",
|
||||||
"@testing-library/jest-dom": "^5.14.1",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^12.1.0",
|
"@testing-library/react": "^16.2.0",
|
||||||
"copy-webpack-plugin": "^12.0.0",
|
"copy-webpack-plugin": "^12.0.0",
|
||||||
"husky": "^9.0.0",
|
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"jest-expect-message": "^1.1.3",
|
"jest-expect-message": "^1.1.3",
|
||||||
|
"jest-when": "^3.6.0",
|
||||||
"react-dev-utils": "^12.0.0",
|
"react-dev-utils": "^12.0.0",
|
||||||
"react-test-renderer": "^17.0.2",
|
"react-test-renderer": "^18.3.1",
|
||||||
"redux-mock-store": "^1.5.4",
|
"redux-mock-store": "^1.5.4"
|
||||||
"semantic-release": "^20.1.3"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { logError } from '@edx/frontend-platform/logging';
|
|||||||
import { initializeHotjar } from '@edx/frontend-enterprise-hotjar';
|
import { initializeHotjar } from '@edx/frontend-enterprise-hotjar';
|
||||||
|
|
||||||
import { ErrorPage, AppContext } from '@edx/frontend-platform/react';
|
import { ErrorPage, AppContext } from '@edx/frontend-platform/react';
|
||||||
import FooterSlot from '@openedx/frontend-slot-footer';
|
import { FooterSlot } from '@edx/frontend-component-footer';
|
||||||
import { Alert } from '@openedx/paragon';
|
import { Alert } from '@openedx/paragon';
|
||||||
|
|
||||||
import { RequestKeys } from 'data/constants/requests';
|
import { RequestKeys } from 'data/constants/requests';
|
||||||
@@ -80,7 +80,7 @@ export const App = () => {
|
|||||||
<div>
|
<div>
|
||||||
<AppWrapper>
|
<AppWrapper>
|
||||||
<LearnerDashboardHeader />
|
<LearnerDashboardHeader />
|
||||||
<main>
|
<main id="main">
|
||||||
{hasNetworkFailure
|
{hasNetworkFailure
|
||||||
? (
|
? (
|
||||||
<Alert variant="danger">
|
<Alert variant="danger">
|
||||||
|
|||||||
@@ -13,11 +13,10 @@ import AppWrapper from 'containers/WidgetContainers/AppWrapper';
|
|||||||
import { App } from './App';
|
import { App } from './App';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
jest.mock('@edx/frontend-component-footer', () => ({ FooterSlot: 'Footer' }));
|
jest.mock('@edx/frontend-component-footer', () => ({ FooterSlot: 'FooterSlot' }));
|
||||||
|
|
||||||
jest.mock('containers/Dashboard', () => 'Dashboard');
|
jest.mock('containers/Dashboard', () => 'Dashboard');
|
||||||
jest.mock('containers/LearnerDashboardHeader', () => 'LearnerDashboardHeader');
|
jest.mock('containers/LearnerDashboardHeader', () => 'LearnerDashboardHeader');
|
||||||
jest.mock('components/ZendeskFab', () => 'ZendeskFab');
|
|
||||||
jest.mock('containers/WidgetContainers/AppWrapper', () => 'AppWrapper');
|
jest.mock('containers/WidgetContainers/AppWrapper', () => 'AppWrapper');
|
||||||
jest.mock('data/redux', () => ({
|
jest.mock('data/redux', () => ({
|
||||||
selectors: 'redux.selectors',
|
selectors: 'redux.selectors',
|
||||||
@@ -75,11 +74,9 @@ describe('App router component', () => {
|
|||||||
it('loads dashboard', () => {
|
it('loads dashboard', () => {
|
||||||
const main = el.instance.findByType('main')[0];
|
const main = el.instance.findByType('main')[0];
|
||||||
expect(main.children.length).toEqual(1);
|
expect(main.children.length).toEqual(1);
|
||||||
const dashboard = main.children[0];
|
const dashboard = main.children[0].el;
|
||||||
expect(dashboard.type).toEqual('Dashboard');
|
expect(dashboard.type).toEqual('Dashboard');
|
||||||
expect(
|
expect(dashboard).toEqual(shallow(<Dashboard />));
|
||||||
dashboard.matches(shallow(<Dashboard />)),
|
|
||||||
).toEqual(true);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('no network failure with optimizely url', () => {
|
describe('no network failure with optimizely url', () => {
|
||||||
@@ -92,11 +89,9 @@ describe('App router component', () => {
|
|||||||
it('loads dashboard', () => {
|
it('loads dashboard', () => {
|
||||||
const main = el.instance.findByType('main')[0];
|
const main = el.instance.findByType('main')[0];
|
||||||
expect(main.children.length).toEqual(1);
|
expect(main.children.length).toEqual(1);
|
||||||
const dashboard = main.children[0];
|
const dashboard = main.children[0].el;
|
||||||
expect(dashboard.type).toEqual('Dashboard');
|
expect(dashboard.type).toEqual('Dashboard');
|
||||||
expect(
|
expect(dashboard).toEqual(shallow(<Dashboard />));
|
||||||
dashboard.matches(shallow(<Dashboard />)),
|
|
||||||
).toEqual(true);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('no network failure with optimizely project id', () => {
|
describe('no network failure with optimizely project id', () => {
|
||||||
@@ -109,11 +104,9 @@ describe('App router component', () => {
|
|||||||
it('loads dashboard', () => {
|
it('loads dashboard', () => {
|
||||||
const main = el.instance.findByType('main')[0];
|
const main = el.instance.findByType('main')[0];
|
||||||
expect(main.children.length).toEqual(1);
|
expect(main.children.length).toEqual(1);
|
||||||
const dashboard = main.children[0];
|
const dashboard = main.children[0].el;
|
||||||
expect(dashboard.type).toEqual('Dashboard');
|
expect(dashboard.type).toEqual('Dashboard');
|
||||||
expect(
|
expect(dashboard).toEqual(shallow(<Dashboard />));
|
||||||
dashboard.matches(shallow(<Dashboard />)),
|
|
||||||
).toEqual(true);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('initialize failure', () => {
|
describe('initialize failure', () => {
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ exports[`App router component component initialize failure snapshot 1`] = `
|
|||||||
<div>
|
<div>
|
||||||
<AppWrapper>
|
<AppWrapper>
|
||||||
<LearnerDashboardHeader />
|
<LearnerDashboardHeader />
|
||||||
<main>
|
<main
|
||||||
|
id="main"
|
||||||
|
>
|
||||||
<Alert
|
<Alert
|
||||||
variant="danger"
|
variant="danger"
|
||||||
>
|
>
|
||||||
@@ -49,7 +51,9 @@ exports[`App router component component no network failure snapshot 1`] = `
|
|||||||
<div>
|
<div>
|
||||||
<AppWrapper>
|
<AppWrapper>
|
||||||
<LearnerDashboardHeader />
|
<LearnerDashboardHeader />
|
||||||
<main>
|
<main
|
||||||
|
id="main"
|
||||||
|
>
|
||||||
<Dashboard />
|
<Dashboard />
|
||||||
</main>
|
</main>
|
||||||
</AppWrapper>
|
</AppWrapper>
|
||||||
@@ -75,7 +79,9 @@ exports[`App router component component no network failure with optimizely proje
|
|||||||
<div>
|
<div>
|
||||||
<AppWrapper>
|
<AppWrapper>
|
||||||
<LearnerDashboardHeader />
|
<LearnerDashboardHeader />
|
||||||
<main>
|
<main
|
||||||
|
id="main"
|
||||||
|
>
|
||||||
<Dashboard />
|
<Dashboard />
|
||||||
</main>
|
</main>
|
||||||
</AppWrapper>
|
</AppWrapper>
|
||||||
@@ -101,7 +107,9 @@ exports[`App router component component no network failure with optimizely url s
|
|||||||
<div>
|
<div>
|
||||||
<AppWrapper>
|
<AppWrapper>
|
||||||
<LearnerDashboardHeader />
|
<LearnerDashboardHeader />
|
||||||
<main>
|
<main
|
||||||
|
id="main"
|
||||||
|
>
|
||||||
<Dashboard />
|
<Dashboard />
|
||||||
</main>
|
</main>
|
||||||
</AppWrapper>
|
</AppWrapper>
|
||||||
@@ -127,7 +135,9 @@ exports[`App router component component refresh failure snapshot 1`] = `
|
|||||||
<div>
|
<div>
|
||||||
<AppWrapper>
|
<AppWrapper>
|
||||||
<LearnerDashboardHeader />
|
<LearnerDashboardHeader />
|
||||||
<main>
|
<main
|
||||||
|
id="main"
|
||||||
|
>
|
||||||
<Alert
|
<Alert
|
||||||
variant="danger"
|
variant="danger"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`app registry subscribe: APP_INIT_ERROR. snapshot: displays an ErrorPage to root element 1`] = `
|
exports[`app registry subscribe: APP_INIT_ERROR. snapshot: displays an ErrorPage to root element 1`] = `
|
||||||
|
<UNDEFINED>
|
||||||
<ErrorPage
|
<ErrorPage
|
||||||
message="test-error-message"
|
message="test-error-message"
|
||||||
/>
|
/>
|
||||||
|
</UNDEFINED>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`app registry subscribe: APP_READY. links App to root element 1`] = `
|
exports[`app registry subscribe: APP_READY. links App to root element 1`] = `
|
||||||
|
<UNDEFINED>
|
||||||
<AppProvider
|
<AppProvider
|
||||||
store={
|
store={
|
||||||
{
|
{
|
||||||
"redux": "store",
|
"redux": "store",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
wrapWithRouter={true}
|
|
||||||
>
|
>
|
||||||
<NoticesWrapper>
|
<NoticesWrapper>
|
||||||
<Routes>
|
<Routes>
|
||||||
@@ -37,4 +39,5 @@ exports[`app registry subscribe: APP_READY. links App to root element 1`] = `
|
|||||||
</Routes>
|
</Routes>
|
||||||
</NoticesWrapper>
|
</NoticesWrapper>
|
||||||
</AppProvider>
|
</AppProvider>
|
||||||
|
</UNDEFINED>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -3,16 +3,15 @@ import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-
|
|||||||
import { logError, logInfo } from '@edx/frontend-platform/logging';
|
import { logError, logInfo } from '@edx/frontend-platform/logging';
|
||||||
|
|
||||||
export const noticesUrl = `${getConfig().LMS_BASE_URL}/notices/api/v1/unacknowledged`;
|
export const noticesUrl = `${getConfig().LMS_BASE_URL}/notices/api/v1/unacknowledged`;
|
||||||
export const error404Message = 'This probably happened because the notices plugin is not installed on platform.';
|
|
||||||
|
|
||||||
export const getNotices = ({ onLoad }) => {
|
export const getNotices = ({ onLoad, notFoundMessage }) => {
|
||||||
const authenticatedUser = getAuthenticatedUser();
|
const authenticatedUser = getAuthenticatedUser();
|
||||||
|
|
||||||
const handleError = async (e) => {
|
const handleError = async (e) => {
|
||||||
// Error probably means that notices is not installed, which is fine.
|
// Error probably means that notices is not installed, which is fine.
|
||||||
const { customAttributes: { httpErrorStatus } } = e;
|
const { customAttributes: { httpErrorStatus } } = e;
|
||||||
if (httpErrorStatus === 404) {
|
if (httpErrorStatus === 404) {
|
||||||
logInfo(`${e}. ${error404Message}`);
|
logInfo(`${e}. ${notFoundMessage}`);
|
||||||
} else {
|
} else {
|
||||||
logError(e);
|
logError(e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { StrictDict } from 'utils';
|
import { StrictDict } from 'utils';
|
||||||
import { getNotices } from './api';
|
import { getNotices } from './api';
|
||||||
import * as module from './hooks';
|
import * as module from './hooks';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component uses the platform-plugin-notices plugin to function.
|
* This component uses the platform-plugin-notices plugin to function.
|
||||||
@@ -17,6 +19,8 @@ export const state = StrictDict({
|
|||||||
|
|
||||||
export const useNoticesWrapperData = () => {
|
export const useNoticesWrapperData = () => {
|
||||||
const [isRedirected, setIsRedirected] = module.state.isRedirected();
|
const [isRedirected, setIsRedirected] = module.state.isRedirected();
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (getConfig().ENABLE_NOTICES) {
|
if (getConfig().ENABLE_NOTICES) {
|
||||||
getNotices({
|
getNotices({
|
||||||
@@ -26,9 +30,10 @@ export const useNoticesWrapperData = () => {
|
|||||||
window.location.replace(`${data.data.results[0]}?next=${window.location.href}`);
|
window.location.replace(`${data.data.results[0]}?next=${window.location.href}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
notFoundMessage: formatMessage(messages.error404Message),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [setIsRedirected]);
|
}, [setIsRedirected, formatMessage]);
|
||||||
return { isRedirected };
|
return { isRedirected };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ import * as hooks from './hooks';
|
|||||||
|
|
||||||
jest.mock('@edx/frontend-platform', () => ({ getConfig: jest.fn() }));
|
jest.mock('@edx/frontend-platform', () => ({ getConfig: jest.fn() }));
|
||||||
jest.mock('./api', () => ({ getNotices: jest.fn() }));
|
jest.mock('./api', () => ({ getNotices: jest.fn() }));
|
||||||
|
const mockFormatMessage = jest.fn(message => message.defaultMessage || 'translated-string');
|
||||||
|
jest.mock('react-intl', () => ({
|
||||||
|
useIntl: () => ({
|
||||||
|
formatMessage: mockFormatMessage,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
getConfig.mockReturnValue({ ENABLE_NOTICES: true });
|
getConfig.mockReturnValue({ ENABLE_NOTICES: true });
|
||||||
const state = new MockUseState(hooks);
|
const state = new MockUseState(hooks);
|
||||||
@@ -34,7 +40,7 @@ describe('NoticesWrapper hooks', () => {
|
|||||||
getConfig.mockReturnValueOnce({ ENABLE_NOTICES: false });
|
getConfig.mockReturnValueOnce({ ENABLE_NOTICES: false });
|
||||||
hooks.useNoticesWrapperData();
|
hooks.useNoticesWrapperData();
|
||||||
const [cb, prereqs] = React.useEffect.mock.calls[0];
|
const [cb, prereqs] = React.useEffect.mock.calls[0];
|
||||||
expect(prereqs).toEqual([state.setState.isRedirected]);
|
expect(prereqs).toEqual([state.setState.isRedirected, mockFormatMessage]);
|
||||||
cb();
|
cb();
|
||||||
expect(getNotices).not.toHaveBeenCalled();
|
expect(getNotices).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -43,7 +49,7 @@ describe('NoticesWrapper hooks', () => {
|
|||||||
hooks.useNoticesWrapperData();
|
hooks.useNoticesWrapperData();
|
||||||
expect(React.useEffect).toHaveBeenCalled();
|
expect(React.useEffect).toHaveBeenCalled();
|
||||||
const [cb, prereqs] = React.useEffect.mock.calls[0];
|
const [cb, prereqs] = React.useEffect.mock.calls[0];
|
||||||
expect(prereqs).toEqual([state.setState.isRedirected]);
|
expect(prereqs).toEqual([state.setState.isRedirected, mockFormatMessage]);
|
||||||
cb();
|
cb();
|
||||||
expect(getNotices).toHaveBeenCalled();
|
expect(getNotices).toHaveBeenCalled();
|
||||||
const { onLoad } = getNotices.mock.calls[0][0];
|
const { onLoad } = getNotices.mock.calls[0][0];
|
||||||
@@ -59,7 +65,7 @@ describe('NoticesWrapper hooks', () => {
|
|||||||
window.location = { replace: jest.fn(), href: 'test-old-href' };
|
window.location = { replace: jest.fn(), href: 'test-old-href' };
|
||||||
hooks.useNoticesWrapperData();
|
hooks.useNoticesWrapperData();
|
||||||
const [cb, prereqs] = React.useEffect.mock.calls[0];
|
const [cb, prereqs] = React.useEffect.mock.calls[0];
|
||||||
expect(prereqs).toEqual([state.setState.isRedirected]);
|
expect(prereqs).toEqual([state.setState.isRedirected, mockFormatMessage]);
|
||||||
cb();
|
cb();
|
||||||
expect(getNotices).toHaveBeenCalled();
|
expect(getNotices).toHaveBeenCalled();
|
||||||
const { onLoad } = getNotices.mock.calls[0][0];
|
const { onLoad } = getNotices.mock.calls[0][0];
|
||||||
|
|||||||
11
src/components/NoticesWrapper/messages.js
Normal file
11
src/components/NoticesWrapper/messages.js
Normal 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;
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`ZendeskFab snapshot 1`] = `
|
|
||||||
<Zendesk
|
|
||||||
cookies={true}
|
|
||||||
defer={true}
|
|
||||||
webWidget={
|
|
||||||
{
|
|
||||||
"answerBot": {
|
|
||||||
"avatar": {
|
|
||||||
"name": {
|
|
||||||
"*": "edX Support",
|
|
||||||
},
|
|
||||||
"url": "https://edx-cdn.org/v3/prod/favicon.ico",
|
|
||||||
},
|
|
||||||
"contactOnlyAfterQuery": true,
|
|
||||||
"suppress": false,
|
|
||||||
"title": {
|
|
||||||
"*": "edX Support",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"chat": {
|
|
||||||
"departments": {
|
|
||||||
"enabled": [
|
|
||||||
"account settings",
|
|
||||||
"billing and payments",
|
|
||||||
"certificates",
|
|
||||||
"deadlines",
|
|
||||||
"errors and technical issues",
|
|
||||||
"other",
|
|
||||||
"proctoring",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"suppress": false,
|
|
||||||
},
|
|
||||||
"contactForm": {
|
|
||||||
"attachments": true,
|
|
||||||
"selectTicketForm": {
|
|
||||||
"*": "Please choose your request type:",
|
|
||||||
},
|
|
||||||
"ticketForms": [
|
|
||||||
{
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"id": "description",
|
|
||||||
"prefill": {
|
|
||||||
"*": "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"id": 360003368814,
|
|
||||||
"subject": false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"contactOptions": {
|
|
||||||
"enabled": false,
|
|
||||||
},
|
|
||||||
"helpCenter": {
|
|
||||||
"originalArticleButton": true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
`;
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
|
||||||
|
|
||||||
import Zendesk from 'react-zendesk';
|
|
||||||
import messages from './messages';
|
|
||||||
|
|
||||||
const ZendeskFab = () => {
|
|
||||||
const { formatMessage } = useIntl();
|
|
||||||
const setting = {
|
|
||||||
cookies: true,
|
|
||||||
webWidget: {
|
|
||||||
contactOptions: {
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
chat: {
|
|
||||||
suppress: false,
|
|
||||||
departments: {
|
|
||||||
enabled: ['account settings', 'billing and payments', 'certificates', 'deadlines', 'errors and technical issues', 'other', 'proctoring'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
contactForm: {
|
|
||||||
ticketForms: [
|
|
||||||
{
|
|
||||||
id: 360003368814,
|
|
||||||
subject: false,
|
|
||||||
fields: [{ id: 'description', prefill: { '*': '' } }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
selectTicketForm: {
|
|
||||||
'*': formatMessage(messages.selectTicketForm),
|
|
||||||
},
|
|
||||||
attachments: true,
|
|
||||||
},
|
|
||||||
helpCenter: {
|
|
||||||
originalArticleButton: true,
|
|
||||||
},
|
|
||||||
answerBot: {
|
|
||||||
suppress: false,
|
|
||||||
contactOnlyAfterQuery: true,
|
|
||||||
title: { '*': formatMessage(messages.supportTitle) },
|
|
||||||
avatar: {
|
|
||||||
url: 'https://edx-cdn.org/v3/prod/favicon.ico',
|
|
||||||
name: { '*': formatMessage(messages.supportTitle) },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Zendesk defer zendeskKey={getConfig().ZENDESK_KEY} {...setting} />
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ZendeskFab;
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { shallow } from '@edx/react-unit-test-utils';
|
|
||||||
|
|
||||||
import ZendeskFab from '.';
|
|
||||||
|
|
||||||
jest.mock('react-zendesk', () => 'Zendesk');
|
|
||||||
|
|
||||||
describe('ZendeskFab', () => {
|
|
||||||
test('snapshot', () => {
|
|
||||||
const wrapper = shallow(<ZendeskFab />);
|
|
||||||
expect(wrapper.snapshot).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
supportTitle: {
|
|
||||||
id: 'zendesk.supportTitle',
|
|
||||||
description: 'Title for the support button',
|
|
||||||
defaultMessage: 'edX Support',
|
|
||||||
},
|
|
||||||
selectTicketForm: {
|
|
||||||
id: 'zendesk.selectTicketForm',
|
|
||||||
description: 'Select ticket form',
|
|
||||||
defaultMessage: 'Please choose your request type:',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default messages;
|
|
||||||
@@ -12,13 +12,14 @@ const configuration = {
|
|||||||
// ACCESS_TOKEN_COOKIE_NAME: process.env.ACCESS_TOKEN_COOKIE_NAME,
|
// ACCESS_TOKEN_COOKIE_NAME: process.env.ACCESS_TOKEN_COOKIE_NAME,
|
||||||
LEARNING_BASE_URL: process.env.LEARNING_BASE_URL,
|
LEARNING_BASE_URL: process.env.LEARNING_BASE_URL,
|
||||||
SESSION_COOKIE_DOMAIN: process.env.SESSION_COOKIE_DOMAIN || '',
|
SESSION_COOKIE_DOMAIN: process.env.SESSION_COOKIE_DOMAIN || '',
|
||||||
ZENDESK_KEY: process.env.ZENDESK_KEY,
|
|
||||||
SUPPORT_URL: process.env.SUPPORT_URL || null,
|
SUPPORT_URL: process.env.SUPPORT_URL || null,
|
||||||
ENABLE_NOTICES: process.env.ENABLE_NOTICES || null,
|
ENABLE_NOTICES: process.env.ENABLE_NOTICES || null,
|
||||||
CAREER_LINK_URL: process.env.CAREER_LINK_URL || null,
|
CAREER_LINK_URL: process.env.CAREER_LINK_URL || null,
|
||||||
LOGO_URL: process.env.LOGO_URL,
|
LOGO_URL: process.env.LOGO_URL,
|
||||||
ENABLE_EDX_PERSONAL_DASHBOARD: process.env.ENABLE_EDX_PERSONAL_DASHBOARD === 'true',
|
ENABLE_EDX_PERSONAL_DASHBOARD: process.env.ENABLE_EDX_PERSONAL_DASHBOARD === 'true',
|
||||||
SEARCH_CATALOG_URL: process.env.SEARCH_CATALOG_URL || null,
|
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 = {};
|
const features = {};
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ reduxHooks.useCardCourseRunData.mockReturnValue({ homeUrl });
|
|||||||
const execEdPath = (cardId) => `exec-ed-tracking-path=${cardId}`;
|
const execEdPath = (cardId) => `exec-ed-tracking-path=${cardId}`;
|
||||||
reduxHooks.useCardExecEdTrackingParam.mockImplementation(execEdPath);
|
reduxHooks.useCardExecEdTrackingParam.mockImplementation(execEdPath);
|
||||||
reduxHooks.useTrackCourseEvent.mockImplementation(
|
reduxHooks.useTrackCourseEvent.mockImplementation(
|
||||||
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
|
(eventName, cardId, url) => ({ trackCourseEvent: { eventName, cardId, url } }),
|
||||||
);
|
);
|
||||||
|
|
||||||
describe('BeginCourseButton', () => {
|
describe('BeginCourseButton', () => {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ reduxHooks.useCardCourseRunData.mockReturnValue({ resumeUrl });
|
|||||||
const execEdPath = (cardId) => `exec-ed-tracking-path=${cardId}`;
|
const execEdPath = (cardId) => `exec-ed-tracking-path=${cardId}`;
|
||||||
reduxHooks.useCardExecEdTrackingParam.mockImplementation(execEdPath);
|
reduxHooks.useCardExecEdTrackingParam.mockImplementation(execEdPath);
|
||||||
reduxHooks.useTrackCourseEvent.mockImplementation(
|
reduxHooks.useTrackCourseEvent.mockImplementation(
|
||||||
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
|
(eventName, cardId, url) => ({ trackCourseEvent: { eventName, cardId, url } }),
|
||||||
);
|
);
|
||||||
|
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { Locked } from '@openedx/paragon/icons';
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
|
||||||
|
|
||||||
import track from 'tracking';
|
|
||||||
import { reduxHooks } from 'hooks';
|
|
||||||
import useActionDisabledState from '../hooks';
|
|
||||||
|
|
||||||
import ActionButton from './ActionButton';
|
|
||||||
import messages from './messages';
|
|
||||||
|
|
||||||
export const UpgradeButton = ({ cardId }) => {
|
|
||||||
const { formatMessage } = useIntl();
|
|
||||||
|
|
||||||
const { upgradeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
|
||||||
const { disableUpgradeCourse } = useActionDisabledState(cardId);
|
|
||||||
|
|
||||||
const trackUpgradeClick = reduxHooks.useTrackCourseEvent(
|
|
||||||
track.course.upgradeClicked,
|
|
||||||
cardId,
|
|
||||||
upgradeUrl,
|
|
||||||
);
|
|
||||||
|
|
||||||
const enabledProps = {
|
|
||||||
as: 'a',
|
|
||||||
href: upgradeUrl,
|
|
||||||
onClick: trackUpgradeClick,
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<ActionButton
|
|
||||||
iconBefore={Locked}
|
|
||||||
variant="outline-primary"
|
|
||||||
disabled={disableUpgradeCourse}
|
|
||||||
{...!disableUpgradeCourse && enabledProps}
|
|
||||||
>
|
|
||||||
{formatMessage(messages.upgrade)}
|
|
||||||
</ActionButton>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
UpgradeButton.propTypes = {
|
|
||||||
cardId: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
export default UpgradeButton;
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import { shallow } from '@edx/react-unit-test-utils';
|
|
||||||
|
|
||||||
import track from 'tracking';
|
|
||||||
import { reduxHooks } from 'hooks';
|
|
||||||
import useActionDisabledState from '../hooks';
|
|
||||||
import UpgradeButton from './UpgradeButton';
|
|
||||||
|
|
||||||
jest.mock('tracking', () => ({
|
|
||||||
course: {
|
|
||||||
upgradeClicked: jest.fn().mockName('segment.trackUpgradeClicked'),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
|
||||||
reduxHooks: {
|
|
||||||
useCardCourseRunData: jest.fn(),
|
|
||||||
useTrackCourseEvent: jest.fn(
|
|
||||||
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
jest.mock('../hooks', () => jest.fn(() => ({ disableUpgradeCourse: false })));
|
|
||||||
jest.mock('./ActionButton', () => 'ActionButton');
|
|
||||||
|
|
||||||
describe('UpgradeButton', () => {
|
|
||||||
const props = {
|
|
||||||
cardId: 'cardId',
|
|
||||||
};
|
|
||||||
const upgradeUrl = 'upgradeUrl';
|
|
||||||
reduxHooks.useCardCourseRunData.mockReturnValue({ upgradeUrl });
|
|
||||||
describe('snapshot', () => {
|
|
||||||
test('can upgrade', () => {
|
|
||||||
const wrapper = shallow(<UpgradeButton {...props} />);
|
|
||||||
expect(wrapper.snapshot).toMatchSnapshot();
|
|
||||||
expect(wrapper.instance.props.disabled).toEqual(false);
|
|
||||||
expect(wrapper.instance.props.onClick).toEqual(reduxHooks.useTrackCourseEvent(
|
|
||||||
track.course.upgradeClicked,
|
|
||||||
props.cardId,
|
|
||||||
upgradeUrl,
|
|
||||||
));
|
|
||||||
});
|
|
||||||
test('cannot upgrade', () => {
|
|
||||||
useActionDisabledState.mockReturnValueOnce({ disableUpgradeCourse: true });
|
|
||||||
const wrapper = shallow(<UpgradeButton {...props} />);
|
|
||||||
expect(wrapper.snapshot).toMatchSnapshot();
|
|
||||||
expect(wrapper.instance.props.disabled).toEqual(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -15,7 +15,7 @@ jest.mock('hooks', () => ({
|
|||||||
reduxHooks: {
|
reduxHooks: {
|
||||||
useCardCourseRunData: jest.fn(() => ({ homeUrl: 'homeUrl' })),
|
useCardCourseRunData: jest.fn(() => ({ homeUrl: 'homeUrl' })),
|
||||||
useTrackCourseEvent: jest.fn(
|
useTrackCourseEvent: jest.fn(
|
||||||
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
|
(eventName, cardId, url) => ({ trackCourseEvent: { eventName, cardId, url } }),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ exports[`BeginCourseButton snapshot disabled snapshot 1`] = `
|
|||||||
"trackCourseEvent": {
|
"trackCourseEvent": {
|
||||||
"cardId": "cardId",
|
"cardId": "cardId",
|
||||||
"eventName": [MockFunction segment.enterCourseClicked],
|
"eventName": [MockFunction segment.enterCourseClicked],
|
||||||
"upgradeUrl": "home-urlexec-ed-tracking-path=cardId",
|
"url": "home-urlexec-ed-tracking-path=cardId",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@ exports[`BeginCourseButton snapshot enabled snapshot 1`] = `
|
|||||||
"trackCourseEvent": {
|
"trackCourseEvent": {
|
||||||
"cardId": "cardId",
|
"cardId": "cardId",
|
||||||
"eventName": [MockFunction segment.enterCourseClicked],
|
"eventName": [MockFunction segment.enterCourseClicked],
|
||||||
"upgradeUrl": "home-urlexec-ed-tracking-path=cardId",
|
"url": "home-urlexec-ed-tracking-path=cardId",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ exports[`ResumeButton snapshot disabled snapshot 1`] = `
|
|||||||
"trackCourseEvent": {
|
"trackCourseEvent": {
|
||||||
"cardId": "cardId",
|
"cardId": "cardId",
|
||||||
"eventName": [MockFunction segment.enterCourseClicked],
|
"eventName": [MockFunction segment.enterCourseClicked],
|
||||||
"upgradeUrl": "resume-urlexec-ed-tracking-path=cardId",
|
"url": "resume-urlexec-ed-tracking-path=cardId",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@ exports[`ResumeButton snapshot enabled snapshot 1`] = `
|
|||||||
"trackCourseEvent": {
|
"trackCourseEvent": {
|
||||||
"cardId": "cardId",
|
"cardId": "cardId",
|
||||||
"eventName": [MockFunction segment.enterCourseClicked],
|
"eventName": [MockFunction segment.enterCourseClicked],
|
||||||
"upgradeUrl": "resume-urlexec-ed-tracking-path=cardId",
|
"url": "resume-urlexec-ed-tracking-path=cardId",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`UpgradeButton snapshot can upgrade 1`] = `
|
|
||||||
<ActionButton
|
|
||||||
as="a"
|
|
||||||
disabled={false}
|
|
||||||
href="upgradeUrl"
|
|
||||||
iconBefore={[MockFunction icons.Locked]}
|
|
||||||
onClick={
|
|
||||||
{
|
|
||||||
"trackCourseEvent": {
|
|
||||||
"cardId": "cardId",
|
|
||||||
"eventName": [MockFunction segment.trackUpgradeClicked],
|
|
||||||
"upgradeUrl": "upgradeUrl",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
variant="outline-primary"
|
|
||||||
>
|
|
||||||
Upgrade
|
|
||||||
</ActionButton>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`UpgradeButton snapshot cannot upgrade 1`] = `
|
|
||||||
<ActionButton
|
|
||||||
disabled={true}
|
|
||||||
iconBefore={[MockFunction icons.Locked]}
|
|
||||||
variant="outline-primary"
|
|
||||||
>
|
|
||||||
Upgrade
|
|
||||||
</ActionButton>
|
|
||||||
`;
|
|
||||||
@@ -10,7 +10,7 @@ exports[`ViewCourseButton learner can view course 1`] = `
|
|||||||
"trackCourseEvent": {
|
"trackCourseEvent": {
|
||||||
"cardId": "cardId",
|
"cardId": "cardId",
|
||||||
"eventName": [MockFunction segment.enterCourseClicked],
|
"eventName": [MockFunction segment.enterCourseClicked],
|
||||||
"upgradeUrl": "homeUrl",
|
"url": "homeUrl",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@ exports[`ViewCourseButton learner cannot view course 1`] = `
|
|||||||
"trackCourseEvent": {
|
"trackCourseEvent": {
|
||||||
"cardId": "cardId",
|
"cardId": "cardId",
|
||||||
"eventName": [MockFunction segment.enterCourseClicked],
|
"eventName": [MockFunction segment.enterCourseClicked],
|
||||||
"upgradeUrl": "homeUrl",
|
"url": "homeUrl",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { shallow } from '@edx/react-unit-test-utils';
|
|||||||
import { reduxHooks } from 'hooks';
|
import { reduxHooks } from 'hooks';
|
||||||
|
|
||||||
import CourseCardActionSlot from 'plugin-slots/CourseCardActionSlot';
|
import CourseCardActionSlot from 'plugin-slots/CourseCardActionSlot';
|
||||||
import UpgradeButton from './UpgradeButton';
|
|
||||||
import SelectSessionButton from './SelectSessionButton';
|
import SelectSessionButton from './SelectSessionButton';
|
||||||
import BeginCourseButton from './BeginCourseButton';
|
import BeginCourseButton from './BeginCourseButton';
|
||||||
import ResumeButton from './ResumeButton';
|
import ResumeButton from './ResumeButton';
|
||||||
@@ -21,7 +20,6 @@ jest.mock('hooks', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('plugin-slots/CourseCardActionSlot', () => 'CustomActionButton');
|
jest.mock('plugin-slots/CourseCardActionSlot', () => 'CustomActionButton');
|
||||||
jest.mock('./UpgradeButton', () => 'UpgradeButton');
|
|
||||||
jest.mock('./SelectSessionButton', () => 'SelectSessionButton');
|
jest.mock('./SelectSessionButton', () => 'SelectSessionButton');
|
||||||
jest.mock('./ViewCourseButton', () => 'ViewCourseButton');
|
jest.mock('./ViewCourseButton', () => 'ViewCourseButton');
|
||||||
jest.mock('./BeginCourseButton', () => 'BeginCourseButton');
|
jest.mock('./BeginCourseButton', () => 'BeginCourseButton');
|
||||||
@@ -59,19 +57,7 @@ describe('CourseCardActions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('output', () => {
|
describe('output', () => {
|
||||||
describe('Exec Ed course', () => {
|
|
||||||
it('does not render upgrade button', () => {
|
|
||||||
mockHooks({ isExecEd2UCourse: true });
|
|
||||||
render();
|
|
||||||
expect(el.instance.findByType(UpgradeButton).length).toEqual(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('entitlement course', () => {
|
describe('entitlement course', () => {
|
||||||
it('does not render upgrade button', () => {
|
|
||||||
mockHooks({ isEntitlement: true });
|
|
||||||
render();
|
|
||||||
expect(el.instance.findByType(UpgradeButton).length).toEqual(0);
|
|
||||||
});
|
|
||||||
it('renders ViewCourseButton if fulfilled', () => {
|
it('renders ViewCourseButton if fulfilled', () => {
|
||||||
mockHooks({ isEntitlement: true, isFulfilled: true });
|
mockHooks({ isEntitlement: true, isFulfilled: true });
|
||||||
render();
|
render();
|
||||||
@@ -83,22 +69,15 @@ describe('CourseCardActions', () => {
|
|||||||
expect(el.instance.findByType(SelectSessionButton)[0].props.cardId).toEqual(cardId);
|
expect(el.instance.findByType(SelectSessionButton)[0].props.cardId).toEqual(cardId);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('verified course', () => {
|
|
||||||
it('does not render upgrade button', () => {
|
|
||||||
mockHooks({ isVerified: true });
|
|
||||||
render();
|
|
||||||
expect(el.instance.findByType(UpgradeButton).length).toEqual(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('not entitlement, verified, or exec ed', () => {
|
describe('not entitlement, verified, or exec ed', () => {
|
||||||
it('renders UpgradeButton and ViewCourseButton for archived courses', () => {
|
it('renders CourseCardActionSlot and ViewCourseButton for archived courses', () => {
|
||||||
mockHooks({ isArchived: true });
|
mockHooks({ isArchived: true });
|
||||||
render();
|
render();
|
||||||
expect(el.instance.findByType(CourseCardActionSlot)[0].props.cardId).toEqual(cardId);
|
expect(el.instance.findByType(CourseCardActionSlot)[0].props.cardId).toEqual(cardId);
|
||||||
expect(el.instance.findByType(ViewCourseButton)[0].props.cardId).toEqual(cardId);
|
expect(el.instance.findByType(ViewCourseButton)[0].props.cardId).toEqual(cardId);
|
||||||
});
|
});
|
||||||
describe('unstarted courses', () => {
|
describe('unstarted courses', () => {
|
||||||
it('renders UpgradeButton and BeginCourseButton', () => {
|
it('renders CourseCardActionSlot and BeginCourseButton', () => {
|
||||||
mockHooks();
|
mockHooks();
|
||||||
render();
|
render();
|
||||||
expect(el.instance.findByType(CourseCardActionSlot)[0].props.cardId).toEqual(cardId);
|
expect(el.instance.findByType(CourseCardActionSlot)[0].props.cardId).toEqual(cardId);
|
||||||
@@ -106,7 +85,7 @@ describe('CourseCardActions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('active courses (started, and not archived)', () => {
|
describe('active courses (started, and not archived)', () => {
|
||||||
it('renders UpgradeButton and ResumeButton', () => {
|
it('renders CourseCardActionSlot and ResumeButton', () => {
|
||||||
mockHooks({ hasStarted: true });
|
mockHooks({ hasStarted: true });
|
||||||
render();
|
render();
|
||||||
expect(el.instance.findByType(CourseCardActionSlot)[0].props.cardId).toEqual(cardId);
|
expect(el.instance.findByType(CourseCardActionSlot)[0].props.cardId).toEqual(cardId);
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
upgrade: {
|
|
||||||
id: 'learner-dash.courseCard.actions.upgrade',
|
|
||||||
description: 'Course card upgrade button text',
|
|
||||||
defaultMessage: 'Upgrade',
|
|
||||||
},
|
|
||||||
beginCourse: {
|
beginCourse: {
|
||||||
id: 'learner-dash.courseCard.actions.beginCourse',
|
id: 'learner-dash.courseCard.actions.beginCourse',
|
||||||
description: 'Course card begin-course button text',
|
description: 'Course card begin-course button text',
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ export const CourseBanner = ({ cardId }) => {
|
|||||||
const {
|
const {
|
||||||
isVerified,
|
isVerified,
|
||||||
isAuditAccessExpired,
|
isAuditAccessExpired,
|
||||||
canUpgrade,
|
|
||||||
coursewareAccess = {},
|
coursewareAccess = {},
|
||||||
} = reduxHooks.useCardEnrollmentData(cardId);
|
} = reduxHooks.useCardEnrollmentData(cardId);
|
||||||
const courseRun = reduxHooks.useCardCourseRunData(cardId);
|
const courseRun = reduxHooks.useCardCourseRunData(cardId);
|
||||||
@@ -26,13 +25,7 @@ export const CourseBanner = ({ cardId }) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isAuditAccessExpired
|
{isAuditAccessExpired
|
||||||
&& (canUpgrade ? (
|
&& (
|
||||||
<Banner>
|
|
||||||
{formatMessage(messages.auditAccessExpired)}
|
|
||||||
{' '}
|
|
||||||
{formatMessage(messages.upgradeToAccess)}
|
|
||||||
</Banner>
|
|
||||||
) : (
|
|
||||||
<Banner>
|
<Banner>
|
||||||
{formatMessage(messages.auditAccessExpired)}
|
{formatMessage(messages.auditAccessExpired)}
|
||||||
{' '}
|
{' '}
|
||||||
@@ -40,16 +33,6 @@ export const CourseBanner = ({ cardId }) => {
|
|||||||
{formatMessage(messages.findAnotherCourse)}
|
{formatMessage(messages.findAnotherCourse)}
|
||||||
</Hyperlink>
|
</Hyperlink>
|
||||||
</Banner>
|
</Banner>
|
||||||
))}
|
|
||||||
|
|
||||||
{courseRun.isActive && !canUpgrade && (
|
|
||||||
<Banner>
|
|
||||||
{formatMessage(messages.upgradeDeadlinePassed)}
|
|
||||||
{' '}
|
|
||||||
<Hyperlink isInline destination={courseRun.marketingUrl || ''}>
|
|
||||||
{formatMessage(messages.exploreCourseDetails)}
|
|
||||||
</Hyperlink>
|
|
||||||
</Banner>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(!isStaff && isTooEarly && courseRun.startDate) && (
|
{(!isStaff && isTooEarly && courseRun.startDate) && (
|
||||||
@@ -59,6 +42,7 @@ export const CourseBanner = ({ cardId }) => {
|
|||||||
})}
|
})}
|
||||||
</Banner>
|
</Banner>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(!isStaff && hasUnmetPrerequisites) && (
|
{(!isStaff && hasUnmetPrerequisites) && (
|
||||||
<Banner>{formatMessage(messages.prerequisitesNotMet)}</Banner>
|
<Banner>{formatMessage(messages.prerequisitesNotMet)}</Banner>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ let el;
|
|||||||
|
|
||||||
const enrollmentData = {
|
const enrollmentData = {
|
||||||
isVerified: false,
|
isVerified: false,
|
||||||
canUpgrade: false,
|
|
||||||
isAuditAccessExpired: false,
|
isAuditAccessExpired: false,
|
||||||
coursewareAccess: {
|
coursewareAccess: {
|
||||||
hasUnmetPrerequisites: false,
|
hasUnmetPrerequisites: false,
|
||||||
@@ -65,51 +64,18 @@ describe('CourseBanner', () => {
|
|||||||
render({ enrollment: { isVerified: true } });
|
render({ enrollment: { isVerified: true } });
|
||||||
expect(el.isEmptyRender()).toEqual(true);
|
expect(el.isEmptyRender()).toEqual(true);
|
||||||
});
|
});
|
||||||
describe('audit access expired, can upgrade', () => {
|
describe('audit access expired', () => {
|
||||||
beforeEach(() => {
|
|
||||||
render({ enrollment: { isAuditAccessExpired: true, canUpgrade: true } });
|
|
||||||
});
|
|
||||||
test('snapshot: (auditAccessExpired, upgradeToAccess)', () => {
|
|
||||||
expect(el.snapshot).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
test('messages: (auditAccessExpired, upgradeToAccess)', () => {
|
|
||||||
expect(el.instance.children[0].children[0].el).toContain(messages.auditAccessExpired.defaultMessage);
|
|
||||||
expect(el.instance.children[0].children[2].el).toContain(messages.upgradeToAccess.defaultMessage);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('audit access expired, cannot upgrade', () => {
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
render({ enrollment: { isAuditAccessExpired: true } });
|
render({ enrollment: { isAuditAccessExpired: true } });
|
||||||
});
|
});
|
||||||
test('snapshot: (auditAccessExpired, findAnotherCourse hyperlink)', () => {
|
test('snapshot: (auditAccessExpired, findAnotherCourse hyperlink)', () => {
|
||||||
expect(el.snapshot).toMatchSnapshot();
|
expect(el.snapshot).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
test('messages: (auditAccessExpired, upgradeToAccess)', () => {
|
test('messages: auditAccessExpired', () => {
|
||||||
expect(el.instance.children[0].children[0].el).toContain(messages.auditAccessExpired.defaultMessage);
|
expect(el.instance.children[0].children[0].el).toContain(messages.auditAccessExpired.defaultMessage);
|
||||||
expect(el.instance.findByType(Hyperlink)[0].children[0].el).toEqual(messages.findAnotherCourse.defaultMessage);
|
expect(el.instance.findByType(Hyperlink)[0].children[0].el).toEqual(messages.findAnotherCourse.defaultMessage);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('course run active and cannot upgrade', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
render({ courseRun: { isActive: true } });
|
|
||||||
});
|
|
||||||
test('snapshot: (upgradseDeadlinePassed, exploreCourseDetails hyperlink)', () => {
|
|
||||||
expect(el.snapshot).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
test('messages: (upgradseDeadlinePassed, exploreCourseDetails hyperlink)', () => {
|
|
||||||
expect(el.instance.children[0].children[0].el).toContain(messages.upgradeDeadlinePassed.defaultMessage);
|
|
||||||
const link = el.instance.findByType(Hyperlink);
|
|
||||||
expect(link[0].children[0].el).toEqual(messages.exploreCourseDetails.defaultMessage);
|
|
||||||
expect(link[0].props.destination).toEqual(courseRunData.marketingUrl);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
test('no display if audit access not expired and (course is not active or can upgrade)', () => {
|
|
||||||
render();
|
|
||||||
// isEmptyRender() isn't true because the minimal is <Fragment />
|
|
||||||
expect(el.instance.children).toEqual([]);
|
|
||||||
render({ enrollment: { canUpgrade: true }, courseRun: { isActive: true } });
|
|
||||||
expect(el.instance.children).toEqual([]);
|
|
||||||
});
|
|
||||||
describe('unmet prerequisites', () => {
|
describe('unmet prerequisites', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
render({ enrollment: { coursewareAccess: { hasUnmetPrerequisites: true } } });
|
render({ enrollment: { coursewareAccess: { hasUnmetPrerequisites: true } } });
|
||||||
|
|||||||
@@ -1,16 +1,6 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`CourseBanner audit access expired, can upgrade snapshot: (auditAccessExpired, upgradeToAccess) 1`] = `
|
exports[`CourseBanner audit access expired snapshot: (auditAccessExpired, findAnotherCourse hyperlink) 1`] = `
|
||||||
<Fragment>
|
|
||||||
<Banner>
|
|
||||||
Your audit access to this course has expired.
|
|
||||||
|
|
||||||
Upgrade now to access your course again.
|
|
||||||
</Banner>
|
|
||||||
</Fragment>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`CourseBanner audit access expired, cannot upgrade snapshot: (auditAccessExpired, findAnotherCourse hyperlink) 1`] = `
|
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Banner>
|
<Banner>
|
||||||
Your audit access to this course has expired.
|
Your audit access to this course has expired.
|
||||||
@@ -25,21 +15,6 @@ exports[`CourseBanner audit access expired, cannot upgrade snapshot: (auditAcces
|
|||||||
</Fragment>
|
</Fragment>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`CourseBanner course run active and cannot upgrade snapshot: (upgradseDeadlinePassed, exploreCourseDetails hyperlink) 1`] = `
|
|
||||||
<Fragment>
|
|
||||||
<Banner>
|
|
||||||
Your upgrade deadline for this course has passed. To upgrade, enroll in a session that is farther in the future.
|
|
||||||
|
|
||||||
<Hyperlink
|
|
||||||
destination="marketing-url"
|
|
||||||
isInline={true}
|
|
||||||
>
|
|
||||||
Explore course details.
|
|
||||||
</Hyperlink>
|
|
||||||
</Banner>
|
|
||||||
</Fragment>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`CourseBanner snapshot: stacking banners 1`] = `<Fragment />`;
|
exports[`CourseBanner snapshot: stacking banners 1`] = `<Fragment />`;
|
||||||
|
|
||||||
exports[`CourseBanner staff snapshot: isStaff 1`] = `<Fragment />`;
|
exports[`CourseBanner staff snapshot: isStaff 1`] = `<Fragment />`;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ exports[`CourseCardBanners render with isEnrolled false 1`] = `
|
|||||||
<RelatedProgramsBanner
|
<RelatedProgramsBanner
|
||||||
cardId="test-card-id"
|
cardId="test-card-id"
|
||||||
/>
|
/>
|
||||||
<CourseBanner
|
<CourseBannerSlot
|
||||||
cardId="test-card-id"
|
cardId="test-card-id"
|
||||||
/>
|
/>
|
||||||
<EntitlementBanner
|
<EntitlementBanner
|
||||||
@@ -25,7 +25,7 @@ exports[`CourseCardBanners renders default CourseCardBanners 1`] = `
|
|||||||
<RelatedProgramsBanner
|
<RelatedProgramsBanner
|
||||||
cardId="test-card-id"
|
cardId="test-card-id"
|
||||||
/>
|
/>
|
||||||
<CourseBanner
|
<CourseBannerSlot
|
||||||
cardId="test-card-id"
|
cardId="test-card-id"
|
||||||
/>
|
/>
|
||||||
<EntitlementBanner
|
<EntitlementBanner
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
|||||||
|
|
||||||
import { reduxHooks } from 'hooks';
|
import { reduxHooks } from 'hooks';
|
||||||
|
|
||||||
import CourseBanner from './CourseBanner';
|
import CourseBannerSlot from 'plugin-slots/CourseBannerSlot';
|
||||||
import CertificateBanner from './CertificateBanner';
|
import CertificateBanner from './CertificateBanner';
|
||||||
import CreditBanner from './CreditBanner';
|
import CreditBanner from './CreditBanner';
|
||||||
import EntitlementBanner from './EntitlementBanner';
|
import EntitlementBanner from './EntitlementBanner';
|
||||||
@@ -14,7 +14,7 @@ export const CourseCardBanners = ({ cardId }) => {
|
|||||||
return (
|
return (
|
||||||
<div className="course-card-banners" data-testid="CourseCardBanners">
|
<div className="course-card-banners" data-testid="CourseCardBanners">
|
||||||
<RelatedProgramsBanner cardId={cardId} />
|
<RelatedProgramsBanner cardId={cardId} />
|
||||||
<CourseBanner cardId={cardId} />
|
<CourseBannerSlot cardId={cardId} />
|
||||||
<EntitlementBanner cardId={cardId} />
|
<EntitlementBanner cardId={cardId} />
|
||||||
{isEnrolled && <CertificateBanner cardId={cardId} />}
|
{isEnrolled && <CertificateBanner cardId={cardId} />}
|
||||||
{isEnrolled && <CreditBanner cardId={cardId} />}
|
{isEnrolled && <CreditBanner cardId={cardId} />}
|
||||||
|
|||||||
@@ -6,26 +6,11 @@ const messages = defineMessages({
|
|||||||
description: 'Audit access expiration banner message',
|
description: 'Audit access expiration banner message',
|
||||||
defaultMessage: 'Your audit access to this course has expired.',
|
defaultMessage: 'Your audit access to this course has expired.',
|
||||||
},
|
},
|
||||||
upgradeToAccess: {
|
|
||||||
id: 'learner-dash.courseCard.banners.upgradeToAccess',
|
|
||||||
description: 'Upgrade prompt for audit-expired learners that can still upgrade',
|
|
||||||
defaultMessage: 'Upgrade now to access your course again.',
|
|
||||||
},
|
|
||||||
findAnotherCourse: {
|
findAnotherCourse: {
|
||||||
id: 'learner-dash.courseCard.banners.findAnotherCourse',
|
id: 'learner-dash.courseCard.banners.findAnotherCourse',
|
||||||
description: 'Action prompt taking learners to course exploration',
|
description: 'Action prompt taking learners to course exploration',
|
||||||
defaultMessage: 'Find another course',
|
defaultMessage: 'Find another course',
|
||||||
},
|
},
|
||||||
upgradeDeadlinePassed: {
|
|
||||||
id: 'learner-dash.courseCard.banners.upgradeDeadlinePassed',
|
|
||||||
description: 'Audit upgrade deadline passed banner message',
|
|
||||||
defaultMessage: 'Your upgrade deadline for this course has passed. To upgrade, enroll in a session that is farther in the future.',
|
|
||||||
},
|
|
||||||
exploreCourseDetails: {
|
|
||||||
id: 'learner-dash.courseCard.banners.exploreCourseDetails',
|
|
||||||
description: 'Action prompt taking learners to course details page',
|
|
||||||
defaultMessage: 'Explore course details.',
|
|
||||||
},
|
|
||||||
certRestricted: {
|
certRestricted: {
|
||||||
id: 'learner-dash.courseCard.banners.certificateRestricted',
|
id: 'learner-dash.courseCard.banners.certificateRestricted',
|
||||||
description: 'Restricted certificate warning message',
|
description: 'Restricted certificate warning message',
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ export const useAccessMessage = ({ cardId }) => {
|
|||||||
const courseRun = reduxHooks.useCardCourseRunData(cardId);
|
const courseRun = reduxHooks.useCardCourseRunData(cardId);
|
||||||
const formatDate = utilHooks.useFormatDate();
|
const formatDate = utilHooks.useFormatDate();
|
||||||
if (!courseRun.isStarted) {
|
if (!courseRun.isStarted) {
|
||||||
if (!courseRun.startDate) { return null; }
|
if (!courseRun.startDate && !courseRun.advertisedStart) { return null; }
|
||||||
const startDate = formatDate(courseRun.startDate);
|
const startDate = courseRun.advertisedStart ? courseRun.advertisedStart : formatDate(courseRun.startDate);
|
||||||
return formatMessage(messages.courseStarts, { startDate });
|
return formatMessage(messages.courseStarts, { startDate });
|
||||||
}
|
}
|
||||||
if (enrollment.isEnrolled) {
|
if (enrollment.isEnrolled) {
|
||||||
|
|||||||
@@ -20,11 +20,13 @@ export const CourseCardImage = ({ cardId, orientation }) => {
|
|||||||
const { isVerified } = reduxHooks.useCardEnrollmentData(cardId);
|
const { isVerified } = reduxHooks.useCardEnrollmentData(cardId);
|
||||||
const { disableCourseTitle } = useActionDisabledState(cardId);
|
const { disableCourseTitle } = useActionDisabledState(cardId);
|
||||||
const handleImageClicked = reduxHooks.useTrackCourseEvent(courseImageClicked, cardId, homeUrl);
|
const handleImageClicked = reduxHooks.useTrackCourseEvent(courseImageClicked, cardId, homeUrl);
|
||||||
const wrapperClassName = `pgn__card-wrapper-image-cap overflow-visible ${orientation}`;
|
const wrapperClassName = `pgn__card-wrapper-image-cap d-inline-block overflow-visible ${orientation}`;
|
||||||
const image = (
|
const image = (
|
||||||
<>
|
<>
|
||||||
<img
|
<img
|
||||||
className="pgn__card-image-cap show"
|
// w-100 is necessary for images on Safari, otherwise stretches full height of the image
|
||||||
|
// https://stackoverflow.com/a/44250830
|
||||||
|
className="pgn__card-image-cap w-100 show"
|
||||||
src={bannerImgSrc}
|
src={bannerImgSrc}
|
||||||
alt={formatMessage(messages.bannerAlt)}
|
alt={formatMessage(messages.bannerAlt)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ jest.mock('hooks', () => ({
|
|||||||
useCardCourseData: jest.fn(() => ({ bannerImgSrc: 'banner-img-src' })),
|
useCardCourseData: jest.fn(() => ({ bannerImgSrc: 'banner-img-src' })),
|
||||||
useCardCourseRunData: jest.fn(() => ({ homeUrl })),
|
useCardCourseRunData: jest.fn(() => ({ homeUrl })),
|
||||||
useCardEnrollmentData: jest.fn(() => ({ isVerified: true })),
|
useCardEnrollmentData: jest.fn(() => ({ isVerified: true })),
|
||||||
useTrackCourseEvent: jest.fn((eventName, cardId, upgradeUrl) => ({
|
useTrackCourseEvent: jest.fn((eventName, cardId, url) => ({
|
||||||
trackCourseEvent: { eventName, cardId, upgradeUrl },
|
trackCourseEvent: { eventName, cardId, url },
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ jest.mock('hooks', () => ({
|
|||||||
reduxHooks: {
|
reduxHooks: {
|
||||||
useCardCourseData: jest.fn(() => ({ courseName: 'course-name' })),
|
useCardCourseData: jest.fn(() => ({ courseName: 'course-name' })),
|
||||||
useCardCourseRunData: jest.fn(() => ({ homeUrl })),
|
useCardCourseRunData: jest.fn(() => ({ homeUrl })),
|
||||||
useTrackCourseEvent: jest.fn((eventName, cardId, upgradeUrl) => ({
|
useTrackCourseEvent: jest.fn((eventName, cardId, url) => ({
|
||||||
trackCourseEvent: { eventName, cardId, upgradeUrl },
|
trackCourseEvent: { eventName, cardId, url },
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
|
|
||||||
exports[`CourseCardImage snapshot renders clickable link course Image 1`] = `
|
exports[`CourseCardImage snapshot renders clickable link course Image 1`] = `
|
||||||
<a
|
<a
|
||||||
className="pgn__card-wrapper-image-cap overflow-visible orientation"
|
className="pgn__card-wrapper-image-cap d-inline-block overflow-visible orientation"
|
||||||
href="home-url"
|
href="home-url"
|
||||||
onClick={
|
onClick={
|
||||||
{
|
{
|
||||||
"trackCourseEvent": {
|
"trackCourseEvent": {
|
||||||
"cardId": "cardId",
|
"cardId": "cardId",
|
||||||
"eventName": [MockFunction segment.courseImageClicked],
|
"eventName": [MockFunction segment.courseImageClicked],
|
||||||
"upgradeUrl": "home-url",
|
"url": "home-url",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@ exports[`CourseCardImage snapshot renders clickable link course Image 1`] = `
|
|||||||
<Fragment>
|
<Fragment>
|
||||||
<img
|
<img
|
||||||
alt="Course thumbnail"
|
alt="Course thumbnail"
|
||||||
className="pgn__card-image-cap show"
|
className="pgn__card-image-cap w-100 show"
|
||||||
src="banner-img-src"
|
src="banner-img-src"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -43,12 +43,12 @@ exports[`CourseCardImage snapshot renders clickable link course Image 1`] = `
|
|||||||
|
|
||||||
exports[`CourseCardImage snapshot renders disabled link 1`] = `
|
exports[`CourseCardImage snapshot renders disabled link 1`] = `
|
||||||
<div
|
<div
|
||||||
className="pgn__card-wrapper-image-cap overflow-visible orientation"
|
className="pgn__card-wrapper-image-cap d-inline-block overflow-visible orientation"
|
||||||
>
|
>
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<img
|
<img
|
||||||
alt="Course thumbnail"
|
alt="Course thumbnail"
|
||||||
className="pgn__card-image-cap show"
|
className="pgn__card-image-cap w-100 show"
|
||||||
src="banner-img-src"
|
src="banner-img-src"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ exports[`CourseCardTitle snapshot renders clickable link course title 1`] = `
|
|||||||
"trackCourseEvent": {
|
"trackCourseEvent": {
|
||||||
"cardId": "cardId",
|
"cardId": "cardId",
|
||||||
"eventName": [MockFunction segment.courseTitleClicked],
|
"eventName": [MockFunction segment.courseTitleClicked],
|
||||||
"upgradeUrl": "home-url",
|
"url": "home-url",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,19 +3,18 @@ import { reduxHooks } from 'hooks';
|
|||||||
export const useActionDisabledState = (cardId) => {
|
export const useActionDisabledState = (cardId) => {
|
||||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||||
const {
|
const {
|
||||||
canUpgrade, hasAccess, isAudit, isAuditAccessExpired,
|
hasAccess, isAudit, isAuditAccessExpired,
|
||||||
} = reduxHooks.useCardEnrollmentData(cardId);
|
} = reduxHooks.useCardEnrollmentData(cardId);
|
||||||
const {
|
const {
|
||||||
isEntitlement, isFulfilled, canChange, hasSessions,
|
isEntitlement, isFulfilled, canChange, hasSessions,
|
||||||
} = reduxHooks.useCardEntitlementData(cardId);
|
} = reduxHooks.useCardEntitlementData(cardId);
|
||||||
|
|
||||||
const { resumeUrl, homeUrl, upgradeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
const { resumeUrl, homeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||||
|
|
||||||
const disableBeginCourse = !homeUrl || (isMasquerading || !hasAccess || (isAudit && isAuditAccessExpired));
|
const disableBeginCourse = !homeUrl || (isMasquerading || !hasAccess || (isAudit && isAuditAccessExpired));
|
||||||
const disableResumeCourse = !resumeUrl || (isMasquerading || !hasAccess || (isAudit && isAuditAccessExpired));
|
const disableResumeCourse = !resumeUrl || (isMasquerading || !hasAccess || (isAudit && isAuditAccessExpired));
|
||||||
const disableViewCourse = !hasAccess || (isAudit && isAuditAccessExpired);
|
const disableViewCourse = !hasAccess || (isAudit && isAuditAccessExpired);
|
||||||
const disableSelectSession = !isEntitlement || isMasquerading || !hasAccess || (!canChange || !hasSessions);
|
const disableSelectSession = !isEntitlement || isMasquerading || !hasAccess || (!canChange || !hasSessions);
|
||||||
const disableUpgradeCourse = !upgradeUrl || (isMasquerading && !canUpgrade);
|
|
||||||
|
|
||||||
const disableCourseTitle = (isEntitlement && !isFulfilled) || disableViewCourse;
|
const disableCourseTitle = (isEntitlement && !isFulfilled) || disableViewCourse;
|
||||||
|
|
||||||
@@ -23,7 +22,6 @@ export const useActionDisabledState = (cardId) => {
|
|||||||
disableBeginCourse,
|
disableBeginCourse,
|
||||||
disableResumeCourse,
|
disableResumeCourse,
|
||||||
disableViewCourse,
|
disableViewCourse,
|
||||||
disableUpgradeCourse,
|
|
||||||
disableSelectSession,
|
disableSelectSession,
|
||||||
disableCourseTitle,
|
disableCourseTitle,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ const cardId = 'my-test-course-number';
|
|||||||
describe('useActionDisabledState', () => {
|
describe('useActionDisabledState', () => {
|
||||||
const defaultData = {
|
const defaultData = {
|
||||||
isMasquerading: false,
|
isMasquerading: false,
|
||||||
canUpgrade: false,
|
|
||||||
isEntitlement: false,
|
isEntitlement: false,
|
||||||
isFulfilled: false,
|
isFulfilled: false,
|
||||||
canChange: false,
|
canChange: false,
|
||||||
@@ -26,12 +25,10 @@ describe('useActionDisabledState', () => {
|
|||||||
isAuditAccessExpired: false,
|
isAuditAccessExpired: false,
|
||||||
resumeUrl: 'resume.url',
|
resumeUrl: 'resume.url',
|
||||||
homeUrl: 'home.url',
|
homeUrl: 'home.url',
|
||||||
upgradeUrl: 'upgrade.url',
|
|
||||||
};
|
};
|
||||||
const mockHooksData = (args) => {
|
const mockHooksData = (args) => {
|
||||||
const {
|
const {
|
||||||
isMasquerading,
|
isMasquerading,
|
||||||
canUpgrade,
|
|
||||||
isEntitlement,
|
isEntitlement,
|
||||||
isFulfilled,
|
isFulfilled,
|
||||||
canChange,
|
canChange,
|
||||||
@@ -41,11 +38,9 @@ describe('useActionDisabledState', () => {
|
|||||||
isAuditAccessExpired,
|
isAuditAccessExpired,
|
||||||
resumeUrl,
|
resumeUrl,
|
||||||
homeUrl,
|
homeUrl,
|
||||||
upgradeUrl,
|
|
||||||
} = { ...defaultData, ...args };
|
} = { ...defaultData, ...args };
|
||||||
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading });
|
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading });
|
||||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({
|
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({
|
||||||
canUpgrade,
|
|
||||||
hasAccess,
|
hasAccess,
|
||||||
isAudit,
|
isAudit,
|
||||||
isAuditAccessExpired,
|
isAuditAccessExpired,
|
||||||
@@ -59,7 +54,6 @@ describe('useActionDisabledState', () => {
|
|||||||
reduxHooks.useCardCourseRunData.mockReturnValueOnce({
|
reduxHooks.useCardCourseRunData.mockReturnValueOnce({
|
||||||
resumeUrl,
|
resumeUrl,
|
||||||
homeUrl,
|
homeUrl,
|
||||||
upgradeUrl,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -121,21 +115,6 @@ describe('useActionDisabledState', () => {
|
|||||||
testDisabled({ hasAccess: true }, false);
|
testDisabled({ hasAccess: true }, false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('disableUpgradeCourse', () => {
|
|
||||||
const testDisabled = (data, expected) => {
|
|
||||||
mockHooksData(data);
|
|
||||||
expect(runHook().disableUpgradeCourse).toBe(expected);
|
|
||||||
};
|
|
||||||
it('disable when upgradeUrl is invalid', () => {
|
|
||||||
testDisabled({ upgradeUrl: null }, true);
|
|
||||||
});
|
|
||||||
it('disable when isMasquerading is true and canUpgrade is false', () => {
|
|
||||||
testDisabled({ isMasquerading: true, canUpgrade: false }, true);
|
|
||||||
});
|
|
||||||
it('enable when all conditions are met', () => {
|
|
||||||
testDisabled({ canUpgrade: true }, false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('disableSelectSession', () => {
|
describe('disableSelectSession', () => {
|
||||||
const testDisabled = (data, expected) => {
|
const testDisabled = (data, expected) => {
|
||||||
mockHooksData(data);
|
mockHooksData(data);
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ exports[`NoCoursesView snapshot 1`] = `
|
|||||||
alt="No Courses view banner"
|
alt="No Courses view banner"
|
||||||
src="icon/mock/path"
|
src="icon/mock/path"
|
||||||
/>
|
/>
|
||||||
<h1>
|
<h3
|
||||||
|
className="h1"
|
||||||
|
>
|
||||||
Looking for a new challenge?
|
Looking for a new challenge?
|
||||||
</h1>
|
</h3>
|
||||||
<p>
|
<p>
|
||||||
Explore our courses to add them to your dashboard.
|
Explore our courses to add them to your dashboard.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ export const NoCoursesView = () => {
|
|||||||
className="d-flex align-items-center justify-content-center mb-4.5"
|
className="d-flex align-items-center justify-content-center mb-4.5"
|
||||||
>
|
>
|
||||||
<Image src={emptyCourseSVG} alt={formatMessage(messages.bannerAlt)} />
|
<Image src={emptyCourseSVG} alt={formatMessage(messages.bannerAlt)} />
|
||||||
<h1>
|
<h3 className="h1">
|
||||||
{formatMessage(messages.lookingForChallengePrompt)}
|
{formatMessage(messages.lookingForChallengePrompt)}
|
||||||
</h1>
|
</h3>
|
||||||
<p>
|
<p>
|
||||||
{formatMessage(messages.exploreCoursesPrompt)}
|
{formatMessage(messages.exploreCoursesPrompt)}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import queryString from 'query-string';
|
|
||||||
|
|
||||||
import { ListPageSize, SortKeys } from 'data/constants/app';
|
import { ListPageSize, SortKeys } from 'data/constants/app';
|
||||||
import { reduxHooks } from 'hooks';
|
import { reduxHooks } from 'hooks';
|
||||||
import { StrictDict } from 'utils';
|
import { StrictDict } from 'utils';
|
||||||
@@ -27,12 +25,13 @@ export const useCourseListData = () => {
|
|||||||
|
|
||||||
const [sortBy, setSortBy] = module.state.sortBy(SortKeys.enrolled);
|
const [sortBy, setSortBy] = module.state.sortBy(SortKeys.enrolled);
|
||||||
|
|
||||||
const querySearch = queryString.parse(window.location.search, { parseNumbers: true });
|
const querySearch = new URLSearchParams(window.location.search);
|
||||||
|
const disablePagination = querySearch.get('disable_pagination');
|
||||||
|
|
||||||
const { numPages, visibleList } = reduxHooks.useCurrentCourseList({
|
const { numPages, visibleList } = reduxHooks.useCurrentCourseList({
|
||||||
sortBy,
|
sortBy,
|
||||||
filters,
|
filters,
|
||||||
pageSize: querySearch?.disable_pagination === 1 ? 0 : ListPageSize,
|
pageSize: Number(disablePagination) === 1 ? 0 : ListPageSize,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleRemoveFilter = (filter) => () => removeFilter(filter);
|
const handleRemoveFilter = (filter) => () => removeFilter(filter);
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import queryString from 'query-string';
|
|
||||||
|
|
||||||
import { MockUseState } from 'testUtils';
|
import { MockUseState } from 'testUtils';
|
||||||
import { reduxHooks } from 'hooks';
|
import { reduxHooks } from 'hooks';
|
||||||
import { ListPageSize, SortKeys } from 'data/constants/app';
|
import { ListPageSize, SortKeys } from 'data/constants/app';
|
||||||
@@ -15,8 +13,10 @@ jest.mock('hooks', () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('query-string', () => ({
|
const mockGet = jest.fn(() => ({}));
|
||||||
parse: jest.fn(() => ({})),
|
|
||||||
|
global.URLSearchParams = jest.fn().mockImplementation(() => ({
|
||||||
|
get: mockGet,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const state = new MockUseState(hooks);
|
const state = new MockUseState(hooks);
|
||||||
@@ -67,7 +67,7 @@ describe('CourseList hooks', () => {
|
|||||||
it('loads current course list with page size 0 if/when there is query param disable_pagination=1', () => {
|
it('loads current course list with page size 0 if/when there is query param disable_pagination=1', () => {
|
||||||
state.mock();
|
state.mock();
|
||||||
state.mockVal(state.keys.sortBy, testSortBy);
|
state.mockVal(state.keys.sortBy, testSortBy);
|
||||||
queryString.parse.mockReturnValueOnce({ disable_pagination: 1 });
|
mockGet.mockReturnValueOnce('1');
|
||||||
out = hooks.useCourseListData();
|
out = hooks.useCourseListData();
|
||||||
expect(reduxHooks.useCurrentCourseList).toHaveBeenCalledWith({
|
expect(reduxHooks.useCurrentCourseList).toHaveBeenCalledWith({
|
||||||
sortBy: testSortBy,
|
sortBy: testSortBy,
|
||||||
|
|||||||
@@ -40,8 +40,7 @@ export const DashboardLayout = ({ children }) => {
|
|||||||
<Col {...courseListColumnProps} className="course-list-column">
|
<Col {...courseListColumnProps} className="course-list-column">
|
||||||
{children}
|
{children}
|
||||||
</Col>
|
</Col>
|
||||||
<Col {...columnConfig.sidebar} className="sidebar-column">
|
<Col {...columnConfig.sidebar} className={['sidebar-column', !isCollapsed && 'not-collapsed']}>
|
||||||
{!isCollapsed && (<h2 className="course-list-title"> </h2>)}
|
|
||||||
<WidgetSidebarSlot />
|
<WidgetSidebarSlot />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|||||||
@@ -84,10 +84,9 @@ describe('DashboardLayout', () => {
|
|||||||
|
|
||||||
describe('not collapsed', () => {
|
describe('not collapsed', () => {
|
||||||
const testWidgetSpacing = () => {
|
const testWidgetSpacing = () => {
|
||||||
it('shows a blank (nbsp) h2 spacer component above widget sidebar', () => {
|
it('shows not-collapsed class on widget sidebar', () => {
|
||||||
const columns = el.instance.findByType(Col);
|
const columns = el.instance.findByType(Col);
|
||||||
// nonbreaking space equivalent
|
expect(columns[1].props.className).toContain('not-collapsed');
|
||||||
expect(columns[1].findByType('h2')[0].children[0].el).toEqual('\xA0');
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
describe('sidebar showing', () => {
|
describe('sidebar showing', () => {
|
||||||
|
|||||||
@@ -24,7 +24,12 @@ exports[`DashboardLayout collapsed sidebar not showing snapshot 1`] = `
|
|||||||
test-children
|
test-children
|
||||||
</Col>
|
</Col>
|
||||||
<Col
|
<Col
|
||||||
className="sidebar-column"
|
className={
|
||||||
|
[
|
||||||
|
"sidebar-column",
|
||||||
|
false,
|
||||||
|
]
|
||||||
|
}
|
||||||
lg={
|
lg={
|
||||||
{
|
{
|
||||||
"offset": 0,
|
"offset": 0,
|
||||||
@@ -68,7 +73,12 @@ exports[`DashboardLayout collapsed sidebar showing snapshot 1`] = `
|
|||||||
test-children
|
test-children
|
||||||
</Col>
|
</Col>
|
||||||
<Col
|
<Col
|
||||||
className="sidebar-column"
|
className={
|
||||||
|
[
|
||||||
|
"sidebar-column",
|
||||||
|
false,
|
||||||
|
]
|
||||||
|
}
|
||||||
lg={
|
lg={
|
||||||
{
|
{
|
||||||
"offset": 0,
|
"offset": 0,
|
||||||
@@ -112,7 +122,12 @@ exports[`DashboardLayout not collapsed sidebar not showing snapshot 1`] = `
|
|||||||
test-children
|
test-children
|
||||||
</Col>
|
</Col>
|
||||||
<Col
|
<Col
|
||||||
className="sidebar-column"
|
className={
|
||||||
|
[
|
||||||
|
"sidebar-column",
|
||||||
|
"not-collapsed",
|
||||||
|
]
|
||||||
|
}
|
||||||
lg={
|
lg={
|
||||||
{
|
{
|
||||||
"offset": 0,
|
"offset": 0,
|
||||||
@@ -126,11 +141,6 @@ exports[`DashboardLayout not collapsed sidebar not showing snapshot 1`] = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<h2
|
|
||||||
className="course-list-title"
|
|
||||||
>
|
|
||||||
|
|
||||||
</h2>
|
|
||||||
<WidgetSidebarSlot />
|
<WidgetSidebarSlot />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
@@ -161,7 +171,12 @@ exports[`DashboardLayout not collapsed sidebar showing snapshot 1`] = `
|
|||||||
test-children
|
test-children
|
||||||
</Col>
|
</Col>
|
||||||
<Col
|
<Col
|
||||||
className="sidebar-column"
|
className={
|
||||||
|
[
|
||||||
|
"sidebar-column",
|
||||||
|
"not-collapsed",
|
||||||
|
]
|
||||||
|
}
|
||||||
lg={
|
lg={
|
||||||
{
|
{
|
||||||
"offset": 0,
|
"offset": 0,
|
||||||
@@ -175,11 +190,6 @@ exports[`DashboardLayout not collapsed sidebar showing snapshot 1`] = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<h2
|
|
||||||
className="course-list-title"
|
|
||||||
>
|
|
||||||
|
|
||||||
</h2>
|
|
||||||
<WidgetSidebarSlot />
|
<WidgetSidebarSlot />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`Dashboard snapshots courses loaded, show select session modal, no available dashboards snapshot 1`] = `
|
exports[`Dashboard snapshots courses loaded, show select session modal snapshot 1`] = `
|
||||||
<div
|
<div
|
||||||
className="d-flex flex-column p-2 pt-0"
|
className="d-flex flex-column p-2 pt-0"
|
||||||
id="dashboard-container"
|
id="dashboard-container"
|
||||||
@@ -11,6 +11,7 @@ exports[`Dashboard snapshots courses loaded, show select session modal, no avail
|
|||||||
test-page-title
|
test-page-title
|
||||||
</h1>
|
</h1>
|
||||||
<Fragment>
|
<Fragment>
|
||||||
|
<DashboardModalSlot />
|
||||||
<SelectSessionModal />
|
<SelectSessionModal />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
<div
|
<div
|
||||||
@@ -43,7 +44,7 @@ exports[`Dashboard snapshots courses still loading snapshot 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Dashboard snapshots there are no courses, there ARE available dashboards snapshot 1`] = `
|
exports[`Dashboard snapshots there are no courses snapshot 1`] = `
|
||||||
<div
|
<div
|
||||||
className="d-flex flex-column p-2 pt-0"
|
className="d-flex flex-column p-2 pt-0"
|
||||||
id="dashboard-container"
|
id="dashboard-container"
|
||||||
@@ -54,7 +55,7 @@ exports[`Dashboard snapshots there are no courses, there ARE available dashboard
|
|||||||
test-page-title
|
test-page-title
|
||||||
</h1>
|
</h1>
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<EnterpriseDashboardModal />
|
<DashboardModalSlot />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
<div
|
<div
|
||||||
data-testid="dashboard-content"
|
data-testid="dashboard-content"
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import React from 'react';
|
|||||||
|
|
||||||
import { reduxHooks } from 'hooks';
|
import { reduxHooks } from 'hooks';
|
||||||
import { RequestKeys } from 'data/constants/requests';
|
import { RequestKeys } from 'data/constants/requests';
|
||||||
import EnterpriseDashboardModal from 'containers/EnterpriseDashboardModal';
|
|
||||||
import SelectSessionModal from 'containers/SelectSessionModal';
|
import SelectSessionModal from 'containers/SelectSessionModal';
|
||||||
import CoursesPanel from 'containers/CoursesPanel';
|
import CoursesPanel from 'containers/CoursesPanel';
|
||||||
|
import DashboardModalSlot from 'plugin-slots/DashboardModalSlot';
|
||||||
|
|
||||||
import LoadingView from './LoadingView';
|
import LoadingView from './LoadingView';
|
||||||
import DashboardLayout from './DashboardLayout';
|
import DashboardLayout from './DashboardLayout';
|
||||||
@@ -15,7 +15,6 @@ export const Dashboard = () => {
|
|||||||
hooks.useInitializeDashboard();
|
hooks.useInitializeDashboard();
|
||||||
const { pageTitle } = hooks.useDashboardMessages();
|
const { pageTitle } = hooks.useDashboardMessages();
|
||||||
const hasCourses = reduxHooks.useHasCourses();
|
const hasCourses = reduxHooks.useHasCourses();
|
||||||
const hasAvailableDashboards = reduxHooks.useHasAvailableDashboards();
|
|
||||||
const initIsPending = reduxHooks.useRequestIsPending(RequestKeys.initialize);
|
const initIsPending = reduxHooks.useRequestIsPending(RequestKeys.initialize);
|
||||||
const showSelectSessionModal = reduxHooks.useShowSelectSessionModal();
|
const showSelectSessionModal = reduxHooks.useShowSelectSessionModal();
|
||||||
|
|
||||||
@@ -24,7 +23,7 @@ export const Dashboard = () => {
|
|||||||
<h1 className="sr-only">{pageTitle}</h1>
|
<h1 className="sr-only">{pageTitle}</h1>
|
||||||
{!initIsPending && (
|
{!initIsPending && (
|
||||||
<>
|
<>
|
||||||
{hasAvailableDashboards && <EnterpriseDashboardModal />}
|
<DashboardModalSlot />
|
||||||
{(hasCourses && showSelectSessionModal) && <SelectSessionModal />}
|
{(hasCourses && showSelectSessionModal) && <SelectSessionModal />}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -6,6 +6,14 @@
|
|||||||
|
|
||||||
.sidebar-column {
|
.sidebar-column {
|
||||||
padding: 0 map-get($spacers, 3) 0 map-get($spacers, 1);
|
padding: 0 map-get($spacers, 3) 0 map-get($spacers, 1);
|
||||||
|
|
||||||
|
&.not-collapsed {
|
||||||
|
padding-top: map-get($spacers, 2);
|
||||||
|
|
||||||
|
& >:first-child {
|
||||||
|
margin-top: map-get($spacers, 5\.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@include media-breakpoint-down(lg) {
|
@include media-breakpoint-down(lg) {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { shallow } from '@edx/react-unit-test-utils';
|
|||||||
|
|
||||||
import { reduxHooks } from 'hooks';
|
import { reduxHooks } from 'hooks';
|
||||||
|
|
||||||
import EnterpriseDashboardModal from 'containers/EnterpriseDashboardModal';
|
|
||||||
import SelectSessionModal from 'containers/SelectSessionModal';
|
import SelectSessionModal from 'containers/SelectSessionModal';
|
||||||
import CoursesPanel from 'containers/CoursesPanel';
|
import CoursesPanel from 'containers/CoursesPanel';
|
||||||
|
|
||||||
@@ -14,13 +13,12 @@ import Dashboard from '.';
|
|||||||
jest.mock('hooks', () => ({
|
jest.mock('hooks', () => ({
|
||||||
reduxHooks: {
|
reduxHooks: {
|
||||||
useHasCourses: jest.fn(),
|
useHasCourses: jest.fn(),
|
||||||
useHasAvailableDashboards: jest.fn(),
|
|
||||||
useShowSelectSessionModal: jest.fn(),
|
useShowSelectSessionModal: jest.fn(),
|
||||||
useRequestIsPending: jest.fn(),
|
useRequestIsPending: jest.fn(),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('containers/EnterpriseDashboardModal', () => 'EnterpriseDashboardModal');
|
jest.mock('plugin-slots/DashboardModalSlot', () => 'DashboardModalSlot');
|
||||||
jest.mock('containers/CoursesPanel', () => 'CoursesPanel');
|
jest.mock('containers/CoursesPanel', () => 'CoursesPanel');
|
||||||
jest.mock('./LoadingView', () => 'LoadingView');
|
jest.mock('./LoadingView', () => 'LoadingView');
|
||||||
jest.mock('./DashboardLayout', () => 'DashboardLayout');
|
jest.mock('./DashboardLayout', () => 'DashboardLayout');
|
||||||
@@ -38,12 +36,10 @@ describe('Dashboard', () => {
|
|||||||
});
|
});
|
||||||
const createWrapper = ({
|
const createWrapper = ({
|
||||||
hasCourses,
|
hasCourses,
|
||||||
hasAvailableDashboards,
|
|
||||||
initIsPending,
|
initIsPending,
|
||||||
showSelectSessionModal,
|
showSelectSessionModal,
|
||||||
}) => {
|
}) => {
|
||||||
reduxHooks.useHasCourses.mockReturnValueOnce(hasCourses);
|
reduxHooks.useHasCourses.mockReturnValueOnce(hasCourses);
|
||||||
reduxHooks.useHasAvailableDashboards.mockReturnValueOnce(hasAvailableDashboards);
|
|
||||||
reduxHooks.useRequestIsPending.mockReturnValueOnce(initIsPending);
|
reduxHooks.useRequestIsPending.mockReturnValueOnce(initIsPending);
|
||||||
reduxHooks.useShowSelectSessionModal.mockReturnValueOnce(showSelectSessionModal);
|
reduxHooks.useShowSelectSessionModal.mockReturnValueOnce(showSelectSessionModal);
|
||||||
return shallow(<Dashboard />);
|
return shallow(<Dashboard />);
|
||||||
@@ -71,7 +67,6 @@ describe('Dashboard', () => {
|
|||||||
const testView = ({
|
const testView = ({
|
||||||
props,
|
props,
|
||||||
content: [contentName, contentEl],
|
content: [contentName, contentEl],
|
||||||
showEnterpriseModal,
|
|
||||||
showSelectSessionModal,
|
showSelectSessionModal,
|
||||||
}) => {
|
}) => {
|
||||||
beforeEach(() => { wrapper = createWrapper(props); });
|
beforeEach(() => { wrapper = createWrapper(props); });
|
||||||
@@ -80,10 +75,6 @@ describe('Dashboard', () => {
|
|||||||
it(`renders ${contentName}`, () => {
|
it(`renders ${contentName}`, () => {
|
||||||
testContent(contentEl);
|
testContent(contentEl);
|
||||||
});
|
});
|
||||||
it(`${renderString(showEnterpriseModal)} dashbaord modal`, () => {
|
|
||||||
expect(wrapper.instance.findByType(EnterpriseDashboardModal).length)
|
|
||||||
.toEqual(showEnterpriseModal ? 1 : 0);
|
|
||||||
});
|
|
||||||
it(`${renderString(showSelectSessionModal)} select session modal`, () => {
|
it(`${renderString(showSelectSessionModal)} select session modal`, () => {
|
||||||
expect(wrapper.instance.findByType(SelectSessionModal).length).toEqual(showSelectSessionModal ? 1 : 0);
|
expect(wrapper.instance.findByType(SelectSessionModal).length).toEqual(showSelectSessionModal ? 1 : 0);
|
||||||
});
|
});
|
||||||
@@ -92,44 +83,38 @@ describe('Dashboard', () => {
|
|||||||
testView({
|
testView({
|
||||||
props: {
|
props: {
|
||||||
hasCourses: false,
|
hasCourses: false,
|
||||||
hasAvailableDashboards: false,
|
|
||||||
initIsPending: true,
|
initIsPending: true,
|
||||||
showSelectSessionModal: false,
|
showSelectSessionModal: false,
|
||||||
},
|
},
|
||||||
content: ['LoadingView', <LoadingView />],
|
content: ['LoadingView', <LoadingView />],
|
||||||
showEnterpriseModal: false,
|
|
||||||
showSelectSessionModal: false,
|
showSelectSessionModal: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('courses loaded, show select session modal, no available dashboards', () => {
|
describe('courses loaded, show select session modal', () => {
|
||||||
testView({
|
testView({
|
||||||
props: {
|
props: {
|
||||||
hasCourses: true,
|
hasCourses: true,
|
||||||
hasAvailableDashboards: false,
|
|
||||||
initIsPending: false,
|
initIsPending: false,
|
||||||
showSelectSessionModal: true,
|
showSelectSessionModal: true,
|
||||||
},
|
},
|
||||||
content: ['LoadedView', (
|
content: ['LoadedView', (
|
||||||
<DashboardLayout><CoursesPanel /></DashboardLayout>
|
<DashboardLayout><CoursesPanel /></DashboardLayout>
|
||||||
)],
|
)],
|
||||||
showEnterpriseModal: false,
|
|
||||||
showSelectSessionModal: true,
|
showSelectSessionModal: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('there are no courses, there ARE available dashboards', () => {
|
describe('there are no courses', () => {
|
||||||
testView({
|
testView({
|
||||||
props: {
|
props: {
|
||||||
hasCourses: false,
|
hasCourses: false,
|
||||||
hasAvailableDashboards: true,
|
|
||||||
initIsPending: false,
|
initIsPending: false,
|
||||||
showSelectSessionModal: false,
|
showSelectSessionModal: false,
|
||||||
},
|
},
|
||||||
content: ['Dashboard layout with no courses sidebar and content', (
|
content: ['Dashboard layout with no courses sidebar and content', (
|
||||||
<DashboardLayout><CoursesPanel /></DashboardLayout>
|
<DashboardLayout><CoursesPanel /></DashboardLayout>
|
||||||
)],
|
)],
|
||||||
showEnterpriseModal: true,
|
|
||||||
showSelectSessionModal: false,
|
showSelectSessionModal: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`EnterpriseDashboard empty snapshot 1`] = `null`;
|
|
||||||
|
|
||||||
exports[`EnterpriseDashboard snapshot 1`] = `
|
|
||||||
<ModalDialog
|
|
||||||
hasCloseButton={false}
|
|
||||||
onClose={[MockFunction useEnterpriseDashboardHook.handleEscape]}
|
|
||||||
title=""
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="bg-white p-3 rounded shadow"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"textAlign": "start",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<h4>
|
|
||||||
You have access to the edX, Inc. dashboard
|
|
||||||
</h4>
|
|
||||||
<p>
|
|
||||||
To access the courses available to you through edX, Inc., visit the edX, Inc. dashboard now.
|
|
||||||
</p>
|
|
||||||
<ActionRow>
|
|
||||||
<Button
|
|
||||||
onClick={[MockFunction useEnterpriseDashboardHook.handleClose]}
|
|
||||||
variant="tertiary"
|
|
||||||
>
|
|
||||||
Dismiss
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
href="/edx-dashboard"
|
|
||||||
onClick={[MockFunction useEnterpriseDashboardHook.handleCTAClick]}
|
|
||||||
type="a"
|
|
||||||
>
|
|
||||||
Go to dashboard
|
|
||||||
</Button>
|
|
||||||
</ActionRow>
|
|
||||||
</div>
|
|
||||||
</ModalDialog>
|
|
||||||
`;
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { StrictDict } from 'utils';
|
|
||||||
import track from 'tracking';
|
|
||||||
import { reduxHooks } from 'hooks';
|
|
||||||
|
|
||||||
import * as module from './hooks';
|
|
||||||
|
|
||||||
export const state = StrictDict({
|
|
||||||
showModal: (val) => React.useState(val), // eslint-disable-line
|
|
||||||
});
|
|
||||||
|
|
||||||
const { modalOpened, modalClosed, modalCTAClicked } = track.enterpriseDashboard;
|
|
||||||
|
|
||||||
export const useEnterpriseDashboardHook = () => {
|
|
||||||
const [showModal, setShowModal] = module.state.showModal(true);
|
|
||||||
const dashboard = reduxHooks.useEnterpriseDashboardData();
|
|
||||||
|
|
||||||
const trackOpened = modalOpened(dashboard.enterpriseUUID);
|
|
||||||
const trackClose = modalClosed(dashboard.enterpriseUUID, 'Cancel button');
|
|
||||||
const trackEscape = modalClosed(dashboard.enterpriseUUID, 'Escape');
|
|
||||||
|
|
||||||
const handleCTAClick = modalCTAClicked(dashboard.enterpriseUUID, dashboard.url);
|
|
||||||
const handleClose = () => {
|
|
||||||
trackClose();
|
|
||||||
setShowModal(false);
|
|
||||||
};
|
|
||||||
const handleEscape = () => {
|
|
||||||
trackEscape();
|
|
||||||
setShowModal(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (dashboard && dashboard.label) {
|
|
||||||
trackOpened();
|
|
||||||
}
|
|
||||||
}, []); // eslint-disable-line
|
|
||||||
|
|
||||||
return {
|
|
||||||
showModal,
|
|
||||||
handleCTAClick,
|
|
||||||
handleClose,
|
|
||||||
handleEscape,
|
|
||||||
dashboard,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useEnterpriseDashboardHook;
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
import { MockUseState } from 'testUtils';
|
|
||||||
import { reduxHooks } from 'hooks';
|
|
||||||
import track from 'tracking';
|
|
||||||
|
|
||||||
import * as hooks from './hooks';
|
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
|
||||||
reduxHooks: {
|
|
||||||
useEnterpriseDashboardData: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
jest.mock('tracking', () => {
|
|
||||||
const modalOpenedEvent = jest.fn();
|
|
||||||
const modalClosedEvent = jest.fn();
|
|
||||||
const modalCTAClickedEvent = jest.fn();
|
|
||||||
return {
|
|
||||||
__esModule: true,
|
|
||||||
default: {
|
|
||||||
enterpriseDashboard: {
|
|
||||||
modalOpenedEvent,
|
|
||||||
modalClosedEvent,
|
|
||||||
modalCTAClickedEvent,
|
|
||||||
modalOpened: jest.fn(() => modalOpenedEvent),
|
|
||||||
modalClosed: jest.fn(() => modalClosedEvent),
|
|
||||||
modalCTAClicked: jest.fn(() => modalCTAClickedEvent),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const state = new MockUseState(hooks);
|
|
||||||
|
|
||||||
const enterpriseDashboardData = { label: 'edX, Inc.', url: '/edx-dashboard' };
|
|
||||||
|
|
||||||
describe('EnterpriseDashboard hooks', () => {
|
|
||||||
reduxHooks.useEnterpriseDashboardData.mockReturnValue({ ...enterpriseDashboardData });
|
|
||||||
|
|
||||||
describe('state values', () => {
|
|
||||||
state.testGetter(state.keys.showModal);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('behavior', () => {
|
|
||||||
let out;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
state.mock();
|
|
||||||
out = hooks.useEnterpriseDashboardHook();
|
|
||||||
});
|
|
||||||
afterEach(state.restore);
|
|
||||||
|
|
||||||
test('useEnterpriseDashboardHook to return dashboard data from redux hooks', () => {
|
|
||||||
expect(out.dashboard).toMatchObject(enterpriseDashboardData);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('modal initializes to shown when rendered and closes on click', () => {
|
|
||||||
state.expectInitializedWith(state.keys.showModal, true);
|
|
||||||
out.handleClose();
|
|
||||||
expect(state.values.showModal).toEqual(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('modal initializes to shown when rendered and closes on escape', () => {
|
|
||||||
state.expectInitializedWith(state.keys.showModal, true);
|
|
||||||
out.handleEscape();
|
|
||||||
expect(state.values.showModal).toEqual(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('CTA click tracks modalCTAClicked', () => {
|
|
||||||
out.handleCTAClick();
|
|
||||||
expect(track.enterpriseDashboard.modalCTAClicked).toHaveBeenCalledWith(
|
|
||||||
enterpriseDashboardData.enterpriseUUID,
|
|
||||||
enterpriseDashboardData.url,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
// import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
|
||||||
import {
|
|
||||||
ModalDialog, ActionRow, Button,
|
|
||||||
} from '@openedx/paragon';
|
|
||||||
|
|
||||||
import messages from './messages';
|
|
||||||
import useEnterpriseDashboardHook from './hooks';
|
|
||||||
|
|
||||||
export const EnterpriseDashboardModal = () => {
|
|
||||||
const { formatMessage } = useIntl();
|
|
||||||
const {
|
|
||||||
showModal,
|
|
||||||
handleClose,
|
|
||||||
handleCTAClick,
|
|
||||||
handleEscape,
|
|
||||||
dashboard,
|
|
||||||
} = useEnterpriseDashboardHook();
|
|
||||||
if (!dashboard || !dashboard.label) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<ModalDialog
|
|
||||||
isOpen={showModal}
|
|
||||||
onClose={handleEscape}
|
|
||||||
hasCloseButton={false}
|
|
||||||
title=""
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="bg-white p-3 rounded shadow"
|
|
||||||
style={{ textAlign: 'start' }}
|
|
||||||
>
|
|
||||||
<h4>
|
|
||||||
{formatMessage(messages.enterpriseDialogHeader, {
|
|
||||||
label: dashboard.label,
|
|
||||||
})}
|
|
||||||
</h4>
|
|
||||||
<p>
|
|
||||||
{formatMessage(messages.enterpriseDialogBody, {
|
|
||||||
label: dashboard.label,
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
<ActionRow>
|
|
||||||
<Button variant="tertiary" onClick={handleClose}>
|
|
||||||
{formatMessage(messages.enterpriseDialogDismissButton)}
|
|
||||||
</Button>
|
|
||||||
<Button type="a" href={dashboard.url} onClick={handleCTAClick}>
|
|
||||||
{formatMessage(messages.enterpriseDialogConfirmButton)}
|
|
||||||
</Button>
|
|
||||||
</ActionRow>
|
|
||||||
</div>
|
|
||||||
</ModalDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
EnterpriseDashboardModal.propTypes = {};
|
|
||||||
|
|
||||||
export default EnterpriseDashboardModal;
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { shallow } from '@edx/react-unit-test-utils';
|
|
||||||
import EnterpriseDashboard from '.';
|
|
||||||
|
|
||||||
import useEnterpriseDashboardHook from './hooks';
|
|
||||||
|
|
||||||
jest.mock('./hooks', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
default: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('EnterpriseDashboard', () => {
|
|
||||||
test('snapshot', () => {
|
|
||||||
const hookData = {
|
|
||||||
dashboard: { label: 'edX, Inc.', url: '/edx-dashboard' },
|
|
||||||
showDialog: false,
|
|
||||||
handleClose: jest.fn().mockName('useEnterpriseDashboardHook.handleClose'),
|
|
||||||
handleCTAClick: jest.fn().mockName('useEnterpriseDashboardHook.handleCTAClick'),
|
|
||||||
handleEscape: jest.fn().mockName('useEnterpriseDashboardHook.handleEscape'),
|
|
||||||
};
|
|
||||||
useEnterpriseDashboardHook.mockReturnValueOnce({ ...hookData });
|
|
||||||
const el = shallow(<EnterpriseDashboard />);
|
|
||||||
expect(el.snapshot).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
test('empty snapshot', () => {
|
|
||||||
useEnterpriseDashboardHook.mockReturnValueOnce({});
|
|
||||||
const el = shallow(<EnterpriseDashboard />);
|
|
||||||
expect(el.snapshot).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
enterpriseDialogHeader: {
|
|
||||||
id: 'leanerDashboard.enterpriseDialogHeader',
|
|
||||||
defaultMessage: 'You have access to the {label} dashboard',
|
|
||||||
description: 'title for enterpise dashboard dialog',
|
|
||||||
},
|
|
||||||
enterpriseDialogBody: {
|
|
||||||
id: 'leanerDashboard.enterpriseDialogBody',
|
|
||||||
defaultMessage: 'To access the courses available to you through {label}, visit the {label} dashboard now.',
|
|
||||||
description: 'Body text for enterpise dashboard dialog',
|
|
||||||
},
|
|
||||||
enterpriseDialogDismissButton: {
|
|
||||||
id: 'leanerDashboard.enterpriseDialogDismissButton',
|
|
||||||
defaultMessage: 'Dismiss',
|
|
||||||
description: 'Dismiss button to cancel visiting dashboard',
|
|
||||||
},
|
|
||||||
enterpriseDialogConfirmButton: {
|
|
||||||
id: 'leanerDashboard.enterpriseDialogConfirmButton',
|
|
||||||
defaultMessage: 'Go to dashboard',
|
|
||||||
description: 'Confirm button to go to the dashboard url',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default messages;
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
|
||||||
|
|
||||||
import { reduxHooks } from 'hooks';
|
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import messages from './messages';
|
|
||||||
|
|
||||||
export const BrandLogo = () => {
|
|
||||||
const { formatMessage } = useIntl();
|
|
||||||
const dashboard = reduxHooks.useEnterpriseDashboardData();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<a href={dashboard?.url || '/'} className="mx-auto">
|
|
||||||
<img
|
|
||||||
className="logo py-3"
|
|
||||||
src={getConfig().LOGO_URL}
|
|
||||||
alt={formatMessage(messages.logoAltText)}
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
BrandLogo.propTypes = {};
|
|
||||||
|
|
||||||
export default BrandLogo;
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { shallow } from '@edx/react-unit-test-utils';
|
|
||||||
|
|
||||||
import { reduxHooks } from 'hooks';
|
|
||||||
import BrandLogo from './BrandLogo';
|
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
|
||||||
reduxHooks: {
|
|
||||||
useEnterpriseDashboardData: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('BrandLogo', () => {
|
|
||||||
test('dashboard defined', () => {
|
|
||||||
reduxHooks.useEnterpriseDashboardData.mockReturnValueOnce({
|
|
||||||
url: 'url',
|
|
||||||
});
|
|
||||||
const wrapper = shallow(<BrandLogo />);
|
|
||||||
expect(wrapper.snapshot).toMatchSnapshot();
|
|
||||||
expect(wrapper.instance.findByType('a')[0].props.href).toEqual('url');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('dashboard undefined', () => {
|
|
||||||
reduxHooks.useEnterpriseDashboardData.mockReturnValueOnce(null);
|
|
||||||
const wrapper = shallow(<BrandLogo />);
|
|
||||||
expect(wrapper.snapshot).toMatchSnapshot();
|
|
||||||
expect(wrapper.instance.findByType('a')[0].props.href).toEqual('/');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -55,11 +55,11 @@ exports[`ConfirmEmailBanner snapshot Show on unverified 1`] = `
|
|||||||
onClose={[MockFunction closeConfirmModal]}
|
onClose={[MockFunction closeConfirmModal]}
|
||||||
title=""
|
title=""
|
||||||
>
|
>
|
||||||
<h1
|
<h2
|
||||||
className="text-center p-3"
|
className="text-center p-3 h1"
|
||||||
>
|
>
|
||||||
Confirm your email
|
Confirm your email
|
||||||
</h1>
|
</h2>
|
||||||
<p
|
<p
|
||||||
className="text-center"
|
className="text-center"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export const ConfirmEmailBanner = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<h1 className="text-center p-3">{formatMessage(messages.confirmEmailModalHeader)}</h1>
|
<h2 className="text-center p-3 h1">{formatMessage(messages.confirmEmailModalHeader)}</h2>
|
||||||
<p className="text-center">{formatMessage(messages.confirmEmailModalBody)}</p>
|
<p className="text-center">{formatMessage(messages.confirmEmailModalBody)}</p>
|
||||||
</MarketingModal>
|
</MarketingModal>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
|
|
||||||
import urls from 'data/services/lms/urls';
|
|
||||||
|
|
||||||
import messages from './messages';
|
|
||||||
|
|
||||||
const getLearnerHeaderMenu = (
|
|
||||||
formatMessage,
|
|
||||||
courseSearchUrl,
|
|
||||||
authenticatedUser,
|
|
||||||
exploreCoursesClick,
|
|
||||||
) => ({
|
|
||||||
mainMenu: [
|
|
||||||
{
|
|
||||||
type: 'item',
|
|
||||||
href: '/',
|
|
||||||
content: formatMessage(messages.course),
|
|
||||||
isActive: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'item',
|
|
||||||
href: `${urls.programsUrl()}`,
|
|
||||||
content: formatMessage(messages.program),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'item',
|
|
||||||
href: `${urls.baseAppUrl(courseSearchUrl)}`,
|
|
||||||
content: formatMessage(messages.discoverNew),
|
|
||||||
onClick: (e) => {
|
|
||||||
exploreCoursesClick(e);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
secondaryMenu: [
|
|
||||||
{
|
|
||||||
type: 'item',
|
|
||||||
href: `${getConfig().SUPPORT_URL}`,
|
|
||||||
content: formatMessage(messages.help),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
userMenu: [
|
|
||||||
{
|
|
||||||
heading: '',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
type: 'item',
|
|
||||||
href: `${getConfig().ACCOUNT_PROFILE_URL}/u/${authenticatedUser?.username}`,
|
|
||||||
content: formatMessage(messages.profile),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'item',
|
|
||||||
href: `${getConfig().ACCOUNT_SETTINGS_URL}`,
|
|
||||||
content: formatMessage(messages.account),
|
|
||||||
},
|
|
||||||
...(getConfig().ORDER_HISTORY_URL ? [{
|
|
||||||
type: 'item',
|
|
||||||
href: getConfig().ORDER_HISTORY_URL,
|
|
||||||
content: formatMessage(messages.orderHistory),
|
|
||||||
}] : []),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
heading: '',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
type: 'item',
|
|
||||||
href: `${getConfig().LOGOUT_URL}`,
|
|
||||||
content: formatMessage(messages.signOut),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
export default getLearnerHeaderMenu;
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`BrandLogo dashboard defined 1`] = `
|
|
||||||
<a
|
|
||||||
className="mx-auto"
|
|
||||||
href="url"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
alt="edX, Inc. Dashboard"
|
|
||||||
className="logo py-3"
|
|
||||||
src="https://edx-cdn.org/v3/default/logo.svg"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`BrandLogo dashboard undefined 1`] = `
|
|
||||||
<a
|
|
||||||
className="mx-auto"
|
|
||||||
href="/"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
alt="edX, Inc. Dashboard"
|
|
||||||
className="logo py-3"
|
|
||||||
src="https://edx-cdn.org/v3/default/logo.svg"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
`;
|
|
||||||
@@ -3,72 +3,7 @@
|
|||||||
exports[`LearnerDashboardHeader render 1`] = `
|
exports[`LearnerDashboardHeader render 1`] = `
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<ConfirmEmailBanner />
|
<ConfirmEmailBanner />
|
||||||
<Header
|
<Header />
|
||||||
mainMenuItems={
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"content": "Courses",
|
|
||||||
"href": "/",
|
|
||||||
"isActive": true,
|
|
||||||
"type": "item",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"content": "Programs",
|
|
||||||
"href": "http://localhost:18000/dashboard/programs",
|
|
||||||
"type": "item",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"content": "Discover New",
|
|
||||||
"href": "http://localhost:18000/course-search-url",
|
|
||||||
"onClick": [Function],
|
|
||||||
"type": "item",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
secondaryMenuItems={
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"content": "Help",
|
|
||||||
"href": "http://localhost:18000/support",
|
|
||||||
"type": "item",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
userMenuItems={
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"heading": "",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"content": "Profile",
|
|
||||||
"href": "http://account-profile-url.test/u/undefined",
|
|
||||||
"type": "item",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"content": "Account",
|
|
||||||
"href": "http://account-settings-url.test",
|
|
||||||
"type": "item",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"content": "Order History",
|
|
||||||
"href": "test-url",
|
|
||||||
"type": "item",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"heading": "",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"content": "Sign Out",
|
|
||||||
"href": "http://localhost:18000/logout",
|
|
||||||
"type": "item",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<MasqueradeBar />
|
<MasqueradeBar />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { useWindowSize, breakpoints } from '@openedx/paragon';
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
|
||||||
import track from 'tracking';
|
|
||||||
import { StrictDict } from 'utils';
|
|
||||||
import { linkNames } from 'tracking/constants';
|
|
||||||
|
|
||||||
import getLearnerHeaderMenu from './LearnerDashboardMenu';
|
|
||||||
|
|
||||||
import * as module from './hooks';
|
|
||||||
|
|
||||||
export const state = StrictDict({
|
|
||||||
isOpen: (val) => React.useState(val), // eslint-disable-line
|
|
||||||
});
|
|
||||||
|
|
||||||
export const useIsCollapsed = () => {
|
|
||||||
const { width } = useWindowSize();
|
|
||||||
const isCollapsed = React.useMemo(() => (width <= breakpoints.large.minWidth), [width]);
|
|
||||||
return isCollapsed;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findCoursesNavClicked = (href) => track.findCourses.findCoursesClicked(href, {
|
|
||||||
linkName: linkNames.learnerHomeNavExplore,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const findCoursesNavDropdownClicked = (href) => track.findCourses.findCoursesClicked(href, {
|
|
||||||
linkName: linkNames.learnerHomeNavDropdownExplore,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const useLearnerDashboardHeaderMenu = ({
|
|
||||||
courseSearchUrl, authenticatedUser, exploreCoursesClick,
|
|
||||||
}) => {
|
|
||||||
const { formatMessage } = useIntl();
|
|
||||||
return getLearnerHeaderMenu(formatMessage, courseSearchUrl, authenticatedUser, exploreCoursesClick);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useLearnerDashboardHeaderData = () => {
|
|
||||||
const [isOpen, setIsOpen] = module.state.isOpen(false);
|
|
||||||
const toggleIsOpen = () => setIsOpen(!isOpen);
|
|
||||||
|
|
||||||
return {
|
|
||||||
isOpen,
|
|
||||||
toggleIsOpen,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
useIsCollapsed,
|
|
||||||
findCoursesNavClicked,
|
|
||||||
findCoursesNavDropdownClicked,
|
|
||||||
useLearnerDashboardHeaderData,
|
|
||||||
useLearnerDashboardHeaderMenu,
|
|
||||||
};
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
import { useWindowSize, breakpoints } from '@openedx/paragon';
|
|
||||||
import track from 'tracking';
|
|
||||||
import { linkNames } from 'tracking/constants';
|
|
||||||
|
|
||||||
import { MockUseState } from 'testUtils';
|
|
||||||
|
|
||||||
import * as hooks from './hooks';
|
|
||||||
|
|
||||||
const state = new MockUseState(hooks);
|
|
||||||
|
|
||||||
const {
|
|
||||||
useIsCollapsed,
|
|
||||||
findCoursesNavClicked,
|
|
||||||
findCoursesNavDropdownClicked,
|
|
||||||
useLearnerDashboardHeaderData,
|
|
||||||
useLearnerDashboardHeaderMenu,
|
|
||||||
} = hooks;
|
|
||||||
|
|
||||||
jest.mock('tracking', () => ({
|
|
||||||
findCourses: {
|
|
||||||
findCoursesClicked: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const url = 'http://example.com';
|
|
||||||
|
|
||||||
describe('LearnerDashboardHeader hooks', () => {
|
|
||||||
describe('state values', () => {
|
|
||||||
state.testGetter(state.keys.isOpen);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('useIsCollapsed', () => {
|
|
||||||
test('large screen is not collapsed', () => {
|
|
||||||
useWindowSize.mockReturnValueOnce({ width: breakpoints.large.minWidth + 1 });
|
|
||||||
expect(useIsCollapsed()).toEqual(false);
|
|
||||||
});
|
|
||||||
test('small screen is collapsed', () => {
|
|
||||||
useWindowSize.mockReturnValueOnce({ width: breakpoints.large.minWidth - 1 });
|
|
||||||
expect(useIsCollapsed()).toEqual(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('findCoursesNavClicked', () => {
|
|
||||||
test('calls tracking with nav link name', () => {
|
|
||||||
findCoursesNavClicked(url);
|
|
||||||
expect(track.findCourses.findCoursesClicked).toHaveBeenCalledWith(url, {
|
|
||||||
linkName: linkNames.learnerHomeNavExplore,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getLearnerDashboardHeaderMenu', () => {
|
|
||||||
test('calls header menu data hook', () => {
|
|
||||||
const courseSearchUrl = '/courses';
|
|
||||||
const authenticatedUser = {
|
|
||||||
username: 'test',
|
|
||||||
};
|
|
||||||
const learnerHomeHeaderMenu = useLearnerDashboardHeaderMenu({ courseSearchUrl, authenticatedUser });
|
|
||||||
expect(learnerHomeHeaderMenu.mainMenu.length).toBe(3);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('findCoursesNavDropdownClicked', () => {
|
|
||||||
test('calls tracking with dropdown link name', () => {
|
|
||||||
findCoursesNavDropdownClicked(url);
|
|
||||||
expect(track.findCourses.findCoursesClicked).toHaveBeenCalledWith(url, {
|
|
||||||
linkName: linkNames.learnerHomeNavDropdownExplore,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('useLearnerDashboardHeaderData', () => {
|
|
||||||
test('default state', () => {
|
|
||||||
state.mock();
|
|
||||||
const out = useLearnerDashboardHeaderData();
|
|
||||||
state.expectInitializedWith(state.keys.isOpen, false);
|
|
||||||
out.toggleIsOpen();
|
|
||||||
expect(state.values.isOpen).toEqual(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,43 +1,16 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import MasqueradeBar from 'containers/MasqueradeBar';
|
|
||||||
import { AppContext } from '@edx/frontend-platform/react';
|
|
||||||
import Header from '@edx/frontend-component-header';
|
import Header from '@edx/frontend-component-header';
|
||||||
import { reduxHooks } from 'hooks';
|
import MasqueradeBar from 'containers/MasqueradeBar';
|
||||||
import urls from 'data/services/lms/urls';
|
|
||||||
|
|
||||||
import ConfirmEmailBanner from './ConfirmEmailBanner';
|
import ConfirmEmailBanner from './ConfirmEmailBanner';
|
||||||
|
|
||||||
import { useLearnerDashboardHeaderMenu, findCoursesNavClicked } from './hooks';
|
export const LearnerDashboardHeader = () => (
|
||||||
|
|
||||||
import './index.scss';
|
|
||||||
|
|
||||||
export const LearnerDashboardHeader = () => {
|
|
||||||
const { authenticatedUser } = React.useContext(AppContext);
|
|
||||||
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
|
|
||||||
|
|
||||||
const exploreCoursesClick = () => {
|
|
||||||
findCoursesNavClicked(urls.baseAppUrl(courseSearchUrl));
|
|
||||||
};
|
|
||||||
|
|
||||||
const learnerHomeHeaderMenu = useLearnerDashboardHeaderMenu({
|
|
||||||
courseSearchUrl,
|
|
||||||
authenticatedUser,
|
|
||||||
exploreCoursesClick,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
<>
|
||||||
<ConfirmEmailBanner />
|
<ConfirmEmailBanner />
|
||||||
<Header
|
<Header />
|
||||||
mainMenuItems={learnerHomeHeaderMenu.mainMenu}
|
|
||||||
secondaryMenuItems={learnerHomeHeaderMenu.secondaryMenu}
|
|
||||||
userMenuItems={learnerHomeHeaderMenu.userMenu}
|
|
||||||
/>
|
|
||||||
<MasqueradeBar />
|
<MasqueradeBar />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
LearnerDashboardHeader.propTypes = {};
|
LearnerDashboardHeader.propTypes = {};
|
||||||
|
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
.dropdown-menu-collapse {
|
|
||||||
width: 100vw;
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.learner-variant-header {
|
|
||||||
a {
|
|
||||||
// needed to make the link not resize the header
|
|
||||||
border-bottom: 2px solid transparent;
|
|
||||||
}
|
|
||||||
.course-link {
|
|
||||||
border-bottom: 2px solid !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.course-link:hover {
|
|
||||||
border-bottom: inherit !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-small-menu {
|
|
||||||
> * {
|
|
||||||
justify-content: flex-start !important;
|
|
||||||
|
|
||||||
border-radius: 0 !important;
|
|
||||||
border-top: 1px solid #ddd !important;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: '\00BB';
|
|
||||||
padding-left: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
// copy from legacy dashboard
|
|
||||||
height: 40px;
|
|
||||||
}
|
|
||||||
@@ -1,35 +1,18 @@
|
|||||||
import { mergeConfig } from '@edx/frontend-platform';
|
|
||||||
import { shallow } from '@edx/react-unit-test-utils';
|
import { shallow } from '@edx/react-unit-test-utils';
|
||||||
import Header from '@edx/frontend-component-header';
|
import Header from '@edx/frontend-component-header';
|
||||||
|
|
||||||
import urls from 'data/services/lms/urls';
|
|
||||||
import LearnerDashboardHeader from '.';
|
import LearnerDashboardHeader from '.';
|
||||||
import { findCoursesNavClicked } from './hooks';
|
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
|
||||||
reduxHooks: {
|
|
||||||
usePlatformSettingsData: jest.fn(() => ({
|
|
||||||
courseSearchUrl: '/course-search-url',
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
jest.mock('./hooks', () => ({
|
|
||||||
...jest.requireActual('./hooks'),
|
|
||||||
findCoursesNavClicked: jest.fn(),
|
|
||||||
}));
|
|
||||||
jest.mock('containers/MasqueradeBar', () => 'MasqueradeBar');
|
jest.mock('containers/MasqueradeBar', () => 'MasqueradeBar');
|
||||||
jest.mock('./ConfirmEmailBanner', () => 'ConfirmEmailBanner');
|
jest.mock('./ConfirmEmailBanner', () => 'ConfirmEmailBanner');
|
||||||
jest.mock('@edx/frontend-component-header', () => 'Header');
|
jest.mock('@edx/frontend-component-header', () => 'Header');
|
||||||
|
|
||||||
describe('LearnerDashboardHeader', () => {
|
describe('LearnerDashboardHeader', () => {
|
||||||
test('render', () => {
|
test('render', () => {
|
||||||
mergeConfig({ ORDER_HISTORY_URL: 'test-url' });
|
|
||||||
const wrapper = shallow(<LearnerDashboardHeader />);
|
const wrapper = shallow(<LearnerDashboardHeader />);
|
||||||
expect(wrapper.snapshot).toMatchSnapshot();
|
expect(wrapper.snapshot).toMatchSnapshot();
|
||||||
expect(wrapper.instance.findByType('ConfirmEmailBanner')).toHaveLength(1);
|
expect(wrapper.instance.findByType('ConfirmEmailBanner')).toHaveLength(1);
|
||||||
expect(wrapper.instance.findByType('MasqueradeBar')).toHaveLength(1);
|
expect(wrapper.instance.findByType('MasqueradeBar')).toHaveLength(1);
|
||||||
expect(wrapper.instance.findByType(Header)).toHaveLength(1);
|
expect(wrapper.instance.findByType(Header)).toHaveLength(1);
|
||||||
wrapper.instance.findByType(Header)[0].props.mainMenuItems[2].onClick();
|
|
||||||
expect(findCoursesNavClicked).toHaveBeenCalledWith(urls.baseAppUrl('/course-search-url'));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
dashboard: {
|
|
||||||
id: 'learnerVariantDashboard.menu.dashboard.label',
|
|
||||||
defaultMessage: 'Dashboard',
|
|
||||||
description: 'The text for the user menu Dashboard navigation link.',
|
|
||||||
},
|
|
||||||
dashboardPersonal: {
|
|
||||||
id: 'learnerVariantDashboard.menu.dashboardPersonal.label',
|
|
||||||
defaultMessage: 'Personal',
|
|
||||||
description: 'Link to personal dashboard in user menu',
|
|
||||||
},
|
|
||||||
dashboardSwitch: {
|
|
||||||
id: 'learnerVariantDashboard.menu.dashboardSwitch.label',
|
|
||||||
defaultMessage: 'SWITCH DASHBOARD',
|
|
||||||
description: 'Switch Dashboard header in the user menu',
|
|
||||||
},
|
|
||||||
help: {
|
|
||||||
id: 'learnerVariantDashboard.help.label',
|
|
||||||
defaultMessage: 'Help',
|
|
||||||
description: 'The text for the link to the Help Center',
|
|
||||||
},
|
|
||||||
profile: {
|
|
||||||
id: 'learnerVariantDashboard.menu.profile.label',
|
|
||||||
defaultMessage: 'Profile',
|
|
||||||
description: 'The text for the user menu Profile navigation link.',
|
|
||||||
},
|
|
||||||
viewPrograms: {
|
|
||||||
id: 'learnerVariantDashboard.menu.viewPrograms.label',
|
|
||||||
defaultMessage: 'View Programs',
|
|
||||||
description: 'The text for the user menu View Programs navigation link.',
|
|
||||||
},
|
|
||||||
account: {
|
|
||||||
id: 'learnerVariantDashboard.menu.account.label',
|
|
||||||
defaultMessage: 'Account',
|
|
||||||
description: 'The text for the user menu Account navigation link.',
|
|
||||||
},
|
|
||||||
orderHistory: {
|
|
||||||
id: 'learnerVariantDashboard.menu.orderHistory.label',
|
|
||||||
defaultMessage: 'Order History',
|
|
||||||
description: 'The text for the user menu Order History navigation link.',
|
|
||||||
},
|
|
||||||
signOut: {
|
|
||||||
id: 'learnerVariantDashboard.menu.signOut.label',
|
|
||||||
defaultMessage: 'Sign Out',
|
|
||||||
description: 'The label for the user menu Sign Out action.',
|
|
||||||
},
|
|
||||||
course: {
|
|
||||||
id: 'learnerVariantDashboard.course',
|
|
||||||
defaultMessage: 'Courses',
|
|
||||||
description: 'Header link for switching to dashboard page.',
|
|
||||||
},
|
|
||||||
program: {
|
|
||||||
id: 'learnerVariantDashboard.program',
|
|
||||||
defaultMessage: 'Programs',
|
|
||||||
description: 'Header link for switching to program page.',
|
|
||||||
},
|
|
||||||
discoverNew: {
|
|
||||||
id: 'learnerVariantDashboard.discoverNew',
|
|
||||||
defaultMessage: 'Discover New',
|
|
||||||
description: 'Header link for switching to discover page.',
|
|
||||||
},
|
|
||||||
logoAltText: {
|
|
||||||
id: 'learnerVariantDashboard.logoAltText',
|
|
||||||
defaultMessage: 'edX, Inc. Dashboard',
|
|
||||||
description: 'Alt text for the edX logo.',
|
|
||||||
},
|
|
||||||
collapseMenuOpenAltText: {
|
|
||||||
id: 'learnerVariantDashboard.collapseMenuOpenAltText',
|
|
||||||
defaultMessage: 'Menu',
|
|
||||||
description: 'Alt text for the collapse menu icon when the menu is open.',
|
|
||||||
},
|
|
||||||
collapseMenuClosedAltText: {
|
|
||||||
id: 'learnerVariantDashboard.collapseMenuClosedAltText',
|
|
||||||
defaultMessage: 'Close',
|
|
||||||
description: 'Alt text for the collapse menu icon when the menu is closed.',
|
|
||||||
},
|
|
||||||
career: {
|
|
||||||
id: 'leanerDashboard.menu.career.label',
|
|
||||||
defaultMessage: 'Career',
|
|
||||||
description: 'The text for the user menu Career navigation link.',
|
|
||||||
},
|
|
||||||
newAlert: {
|
|
||||||
id: 'header.menu.new.label',
|
|
||||||
defaultMessage: 'New',
|
|
||||||
description: 'The text announcing that an item in the user menu is New',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default messages;
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
/* eslint-disable quotes */
|
/* eslint-disable quotes */
|
||||||
import { StrictDict } from 'utils';
|
import { StrictDict } from 'utils';
|
||||||
|
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
export const reasonKeys = StrictDict({
|
export const reasonKeys = StrictDict({
|
||||||
prereqs: 'prereqs',
|
prereqs: 'prereqs',
|
||||||
@@ -26,7 +27,7 @@ export const order = [
|
|||||||
reasonKeys.easy,
|
reasonKeys.easy,
|
||||||
];
|
];
|
||||||
|
|
||||||
const messages = StrictDict({
|
const messages = defineMessages({
|
||||||
[reasonKeys.prereqs]: {
|
[reasonKeys.prereqs]: {
|
||||||
id: 'learner-dash.unenrollConfirm.reasons.prereqs',
|
id: 'learner-dash.unenrollConfirm.reasons.prereqs',
|
||||||
description: 'Unenroll reason option - missing prerequisites',
|
description: 'Unenroll reason option - missing prerequisites',
|
||||||
|
|||||||
@@ -10,10 +10,7 @@ export const numCourses = createSelector(
|
|||||||
(courseData) => Object.keys(courseData).length,
|
(courseData) => Object.keys(courseData).length,
|
||||||
);
|
);
|
||||||
export const hasCourses = createSelector([module.numCourses], (num) => num > 0);
|
export const hasCourses = createSelector([module.numCourses], (num) => num > 0);
|
||||||
export const hasAvailableDashboards = createSelector(
|
|
||||||
[simpleSelectors.enterpriseDashboard],
|
|
||||||
(data) => data !== null && data.isLearnerPortalEnabled === true,
|
|
||||||
);
|
|
||||||
export const showSelectSessionModal = createSelector(
|
export const showSelectSessionModal = createSelector(
|
||||||
[simpleSelectors.selectSessionModal],
|
[simpleSelectors.selectSessionModal],
|
||||||
(data) => data.cardId != null,
|
(data) => data.cardId != null,
|
||||||
@@ -22,6 +19,5 @@ export const showSelectSessionModal = createSelector(
|
|||||||
export default StrictDict({
|
export default StrictDict({
|
||||||
numCourses,
|
numCourses,
|
||||||
hasCourses,
|
hasCourses,
|
||||||
hasAvailableDashboards,
|
|
||||||
showSelectSessionModal,
|
showSelectSessionModal,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,15 +17,6 @@ describe('basic app selectors', () => {
|
|||||||
expect(cb(0)).toEqual(false);
|
expect(cb(0)).toEqual(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('hasAvailableDashboards', () => {
|
|
||||||
it('returns true iff the enterpriseDashboard field is populated and learner portal is enabled', () => {
|
|
||||||
const { preSelectors, cb } = appSelectors.hasAvailableDashboards;
|
|
||||||
expect(preSelectors).toEqual([simpleSelectors.enterpriseDashboard]);
|
|
||||||
expect(cb({ isLearnerPortalEnabled: true })).toEqual(true);
|
|
||||||
expect(cb({ isLearnerPortalEnabled: false })).toEqual(false);
|
|
||||||
expect(cb(null)).toEqual(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('showSelectSessionModal', () => {
|
describe('showSelectSessionModal', () => {
|
||||||
it('returns true if the selectSessionModal cardId is not null', () => {
|
it('returns true if the selectSessionModal cardId is not null', () => {
|
||||||
const { preSelectors, cb } = appSelectors.showSelectSessionModal;
|
const { preSelectors, cb } = appSelectors.showSelectSessionModal;
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export const courseCard = StrictDict({
|
|||||||
(courseRun) => (courseRun === null ? {} : {
|
(courseRun) => (courseRun === null ? {} : {
|
||||||
endDate: module.loadDateVal(courseRun.endDate),
|
endDate: module.loadDateVal(courseRun.endDate),
|
||||||
startDate: module.loadDateVal(courseRun.startDate),
|
startDate: module.loadDateVal(courseRun.startDate),
|
||||||
|
advertisedStart: courseRun.advertisedStart,
|
||||||
|
|
||||||
courseId: courseRun.courseId,
|
courseId: courseRun.courseId,
|
||||||
isArchived: courseRun.isArchived,
|
isArchived: courseRun.isArchived,
|
||||||
@@ -52,7 +53,6 @@ export const courseCard = StrictDict({
|
|||||||
|
|
||||||
homeUrl: courseRun.homeUrl,
|
homeUrl: courseRun.homeUrl,
|
||||||
marketingUrl: courseRun.marketingUrl,
|
marketingUrl: courseRun.marketingUrl,
|
||||||
upgradeUrl: courseRun.upgradeUrl,
|
|
||||||
|
|
||||||
progressUrl: baseAppUrl(courseRun.progressUrl),
|
progressUrl: baseAppUrl(courseRun.progressUrl),
|
||||||
resumeUrl: baseAppUrl(courseRun.resumeUrl), // resume will route this to learning mfe.
|
resumeUrl: baseAppUrl(courseRun.resumeUrl), // resume will route this to learning mfe.
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ describe('courseCard selectors module', () => {
|
|||||||
loadSelector(courseCard.courseRun, {
|
loadSelector(courseCard.courseRun, {
|
||||||
endDate: '3000-10-20',
|
endDate: '3000-10-20',
|
||||||
startDate: '2000-10-20',
|
startDate: '2000-10-20',
|
||||||
|
advertisedStart: 'Mid June',
|
||||||
|
|
||||||
courseId: 'test-course-id',
|
courseId: 'test-course-id',
|
||||||
isArchived: 'test-is-archived',
|
isArchived: 'test-is-archived',
|
||||||
@@ -156,7 +157,6 @@ describe('courseCard selectors module', () => {
|
|||||||
|
|
||||||
homeUrl: 'test-home-url',
|
homeUrl: 'test-home-url',
|
||||||
marketingUrl: 'test-marketing-url',
|
marketingUrl: 'test-marketing-url',
|
||||||
upgradeUrl: 'test-upgrade-url',
|
|
||||||
|
|
||||||
progressUrl: 'test-progress-url',
|
progressUrl: 'test-progress-url',
|
||||||
resumeUrl: 'test-resume-url',
|
resumeUrl: 'test-resume-url',
|
||||||
@@ -173,6 +173,9 @@ describe('courseCard selectors module', () => {
|
|||||||
expect(selected.endDate).toEqual(new Date(testData.endDate));
|
expect(selected.endDate).toEqual(new Date(testData.endDate));
|
||||||
expect(selected.startDate).toEqual(new Date(testData.startDate));
|
expect(selected.startDate).toEqual(new Date(testData.startDate));
|
||||||
});
|
});
|
||||||
|
it('passes advertised start date', () => {
|
||||||
|
expect(selected.advertisedStart).toEqual(testData.advertisedStart);
|
||||||
|
});
|
||||||
it('passes [courseId, isArchived, isStarted]', () => {
|
it('passes [courseId, isArchived, isStarted]', () => {
|
||||||
expect(selected.courseId).toEqual(testData.courseId);
|
expect(selected.courseId).toEqual(testData.courseId);
|
||||||
expect(selected.isArchived).toEqual(testData.isArchived);
|
expect(selected.isArchived).toEqual(testData.isArchived);
|
||||||
@@ -181,10 +184,9 @@ describe('courseCard selectors module', () => {
|
|||||||
it('passes minPassingGrade floored from float to a percentage value', () => {
|
it('passes minPassingGrade floored from float to a percentage value', () => {
|
||||||
expect(selected.minPassingGrade).toEqual(93);
|
expect(selected.minPassingGrade).toEqual(93);
|
||||||
});
|
});
|
||||||
it('passes [homeUrl, marketingUrl, upgradeUrl]', () => {
|
it('passes [homeUrl, marketingUrl]', () => {
|
||||||
expect(selected.homeUrl).toEqual(testData.homeUrl);
|
expect(selected.homeUrl).toEqual(testData.homeUrl);
|
||||||
expect(selected.marketingUrl).toEqual(testData.marketingUrl);
|
expect(selected.marketingUrl).toEqual(testData.marketingUrl);
|
||||||
expect(selected.upgradeUrl).toEqual(testData.upgradeUrl);
|
|
||||||
});
|
});
|
||||||
it('passes [progressUrl, unenrollUrl, resumeUrl], converted to baseAppUrl', () => {
|
it('passes [progressUrl, unenrollUrl, resumeUrl], converted to baseAppUrl', () => {
|
||||||
expect(selected.progressUrl).toEqual(baseAppUrl(testData.progressUrl));
|
expect(selected.progressUrl).toEqual(baseAppUrl(testData.progressUrl));
|
||||||
|
|||||||
@@ -36,12 +36,6 @@ describe('app simple selectors', () => {
|
|||||||
expect(preSelectors).toEqual([appSelector]);
|
expect(preSelectors).toEqual([appSelector]);
|
||||||
expect(cb(testState.app)).toEqual(testString);
|
expect(cb(testState.app)).toEqual(testString);
|
||||||
});
|
});
|
||||||
test('enterpriseDashboard returns empty object if data returns null', () => {
|
|
||||||
testState = { app: { enterpriseDashboard: null } };
|
|
||||||
const { preSelectors, cb } = simpleSelectors.enterpriseDashboard;
|
|
||||||
expect(preSelectors).toEqual([appSelector]);
|
|
||||||
expect(cb(testState.app)).toEqual({});
|
|
||||||
});
|
|
||||||
describe('cardSimpleSelectors', () => {
|
describe('cardSimpleSelectors', () => {
|
||||||
keys = keyStore(cardSimpleSelectors);
|
keys = keyStore(cardSimpleSelectors);
|
||||||
test.each([
|
test.each([
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ export const useSocialShareSettings = () => useSelector(selectors.socialShareSet
|
|||||||
|
|
||||||
/** global-level meta-selectors **/
|
/** global-level meta-selectors **/
|
||||||
export const useHasCourses = () => useSelector(selectors.hasCourses);
|
export const useHasCourses = () => useSelector(selectors.hasCourses);
|
||||||
export const useHasAvailableDashboards = () => useSelector(selectors.hasAvailableDashboards);
|
|
||||||
export const useCurrentCourseList = (opts) => useSelector(
|
export const useCurrentCourseList = (opts) => useSelector(
|
||||||
state => selectors.currentList(state, opts),
|
state => selectors.currentList(state, opts),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -47,5 +47,16 @@ describe('requests reducer', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('clearRequest', () => {
|
||||||
|
it('cleanup status and error', () => {
|
||||||
|
expect(reducer(
|
||||||
|
testingState,
|
||||||
|
actions.clearRequest({ requestKey: testKey, error: testValue }),
|
||||||
|
)).toEqual({
|
||||||
|
...testingState,
|
||||||
|
[testKey]: {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -50,12 +50,6 @@ export const logEvent = ({ eventName, data, courseId }) => post(urls.event(), {
|
|||||||
event: JSON.stringify(data),
|
event: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const logUpgrade = ({ courseId }) => module.logEvent({
|
|
||||||
eventName: eventNames.upgradeButtonClickedEnrollment,
|
|
||||||
courseId,
|
|
||||||
data: { location: 'learner-dashboard' },
|
|
||||||
});
|
|
||||||
|
|
||||||
export const logShare = ({ courseId, site }) => module.logEvent({
|
export const logShare = ({ courseId, site }) => module.logEvent({
|
||||||
eventName: eventNames.shareClicked,
|
eventName: eventNames.shareClicked,
|
||||||
courseId,
|
courseId,
|
||||||
@@ -78,7 +72,6 @@ export default {
|
|||||||
updateEntitlementEnrollment,
|
updateEntitlementEnrollment,
|
||||||
deleteEntitlementEnrollment,
|
deleteEntitlementEnrollment,
|
||||||
logEvent,
|
logEvent,
|
||||||
logUpgrade,
|
|
||||||
logShare,
|
logShare,
|
||||||
createCreditRequest,
|
createCreditRequest,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -130,13 +130,6 @@ describe('lms api methods', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.spyOn(api, moduleKeys.logEvent).mockImplementation(logEvent);
|
jest.spyOn(api, moduleKeys.logEvent).mockImplementation(logEvent);
|
||||||
});
|
});
|
||||||
test('logUpgrade sends enrollment upgrade click event with learner dashboard location', () => {
|
|
||||||
expect(api.logUpgrade({ courseId })).toEqual(logEvent({
|
|
||||||
eventName: eventNames.upgradeButtonClickedEnrollment,
|
|
||||||
courseId,
|
|
||||||
data: { location: 'learner-dashboard' },
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
test('logShare sends share clicke vent with course id, side and location', () => {
|
test('logShare sends share clicke vent with course id, side and location', () => {
|
||||||
const site = 'test-site';
|
const site = 'test-site';
|
||||||
expect(api.logShare({ courseId, site })).toEqual(logEvent({
|
expect(api.logShare({ courseId, site })).toEqual(logEvent({
|
||||||
|
|||||||
@@ -779,9 +779,6 @@ export const compileCourseRunData = ({ courseName, ...data }, index) => {
|
|||||||
courseProvider: getOption(providerOptions, index),
|
courseProvider: getOption(providerOptions, index),
|
||||||
programs: getOption(programsOptions, index),
|
programs: getOption(programsOptions, index),
|
||||||
};
|
};
|
||||||
if (out.enrollment.canUpgrade) {
|
|
||||||
out.courseRun.upgradeUrl = 'test-upgrade-url';
|
|
||||||
}
|
|
||||||
return out;
|
return out;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,34 @@
|
|||||||
import queryString from 'query-string';
|
|
||||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* stringify(query, existingQuery)
|
||||||
|
* simple wrapper to convert an object to a query string
|
||||||
|
* @param {object} query - object to convert
|
||||||
|
* @param {string} existingQuery - existing query string
|
||||||
|
* @returns {string} - query string
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const stringify = (query, existingQuery = '') => {
|
||||||
|
const searchParams = new URLSearchParams(existingQuery);
|
||||||
|
|
||||||
|
Object.entries(query).forEach(([key, value]) => {
|
||||||
|
if (value === undefined || value === null || value === '') {
|
||||||
|
searchParams.delete(key);
|
||||||
|
} else if (Array.isArray(value)) {
|
||||||
|
searchParams.delete(key);
|
||||||
|
value.forEach((val) => {
|
||||||
|
if (val !== undefined && val !== null && val !== '') {
|
||||||
|
searchParams.append(key, val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
searchParams.set(key, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return searchParams.toString();
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get(url)
|
* get(url)
|
||||||
* simple wrapper providing an authenticated Http client get action
|
* simple wrapper providing an authenticated Http client get action
|
||||||
@@ -10,21 +38,23 @@ export const get = (...args) => getAuthenticatedHttpClient().get(...args);
|
|||||||
/**
|
/**
|
||||||
* post(url, data)
|
* post(url, data)
|
||||||
* simple wrapper providing an authenticated Http client post action
|
* simple wrapper providing an authenticated Http client post action
|
||||||
* queryString.stringify is used to convert the object to query string with = and &
|
* stringify is used to convert the object to query string with = and &
|
||||||
* @param {string} url - target url
|
* @param {string} url - target url
|
||||||
* @param {object|string} body - post payload
|
* @param {object|string} body - post payload
|
||||||
*/
|
*/
|
||||||
export const post = (url, body) => getAuthenticatedHttpClient().post(url, queryString.stringify(body));
|
export const post = (url, body = {}) => getAuthenticatedHttpClient().post(url, stringify(body));
|
||||||
|
|
||||||
export const client = getAuthenticatedHttpClient;
|
export const client = getAuthenticatedHttpClient;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* stringifyUrl(url, query)
|
* stringifyUrl(url, query)
|
||||||
* simple wrapper around queryString.stringifyUrl that sets skip behavior
|
* simple wrapper to convert a url and query object to a full url
|
||||||
* @param {string} url - base url string
|
* @param {string} url - base url string
|
||||||
* @param {object} query - query parameters
|
* @param {object} query - query parameters
|
||||||
|
* @returns {string} - full url
|
||||||
*/
|
*/
|
||||||
export const stringifyUrl = (url, query) => queryString.stringifyUrl(
|
export const stringifyUrl = (url, query) => {
|
||||||
{ url, query },
|
const [baseUrl, existingQuery = ''] = url.split('?');
|
||||||
{ skipNull: true, skipEmptyString: true },
|
const queryString = stringify(query, existingQuery);
|
||||||
);
|
return queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import queryString from 'query-string';
|
|
||||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
import * as utils from './utils';
|
import * as utils from './utils';
|
||||||
|
|
||||||
jest.mock('query-string', () => ({
|
|
||||||
stringifyUrl: jest.fn((url, options) => ({ url, options })),
|
|
||||||
stringify: jest.fn((data) => data),
|
|
||||||
}));
|
|
||||||
jest.mock('@edx/frontend-platform/auth', () => ({
|
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||||
getAuthenticatedHttpClient: jest.fn(),
|
getAuthenticatedHttpClient: jest.fn(),
|
||||||
}));
|
}));
|
||||||
@@ -20,28 +15,25 @@ describe('lms service utils', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('post', () => {
|
describe('post', () => {
|
||||||
it('forwards arguments to authenticatedHttpClient().post', () => {
|
it('forwards arguments to authenticatedHttpClient().post, removes undefined attributes and appends array values', () => {
|
||||||
const post = jest.fn((...args) => ({ post: args }));
|
const post = jest.fn((...args) => ({ post: args }));
|
||||||
getAuthenticatedHttpClient.mockReturnValue({ post });
|
getAuthenticatedHttpClient.mockReturnValue({ post });
|
||||||
const url = 'some url';
|
const url = 'some url';
|
||||||
const body = {
|
const body = {
|
||||||
some: 'body',
|
some: 'body',
|
||||||
for: 'the',
|
for: undefined,
|
||||||
test: 'yay',
|
test: 'yay',
|
||||||
|
array: ['one', 'two', undefined],
|
||||||
};
|
};
|
||||||
const expectedUrl = utils.post(url, body);
|
const expectedUrl = utils.post(url, body);
|
||||||
expect(queryString.stringify).toHaveBeenCalledWith(body);
|
expect(expectedUrl).toEqual(post(url, 'some=body&test=yay&array=one&array=two'));
|
||||||
expect(expectedUrl).toEqual(post(url, body));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('stringifyUrl', () => {
|
describe('stringifyUrl', () => {
|
||||||
it('forwards url and query to stringifyUrl with options to skip null and ""', () => {
|
it('forwards url and query to stringifyUrl skipping null and ""', () => {
|
||||||
const url = 'here.com';
|
const url = 'here.com';
|
||||||
const query = { some: 'set', of: 'queryParams' };
|
const query = { some: 'set', of: 'queryParams' };
|
||||||
const options = { skipNull: true, skipEmptyString: true };
|
expect(utils.stringifyUrl(url, query)).toEqual('here.com?some=set&of=queryParams');
|
||||||
expect(utils.stringifyUrl(url, query)).toEqual(
|
|
||||||
queryString.stringifyUrl({ url, query }, options),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
import 'core-js/stable';
|
import 'core-js/stable';
|
||||||
import 'regenerator-runtime/runtime';
|
import 'regenerator-runtime/runtime';
|
||||||
|
|
||||||
import React from 'react';
|
import React, { StrictMode } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import { createRoot } from 'react-dom/client';
|
||||||
import {
|
import {
|
||||||
Route, Navigate, Routes,
|
Route, Navigate, Routes,
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
@@ -30,7 +30,10 @@ import App from './App';
|
|||||||
import NoticesWrapper from './components/NoticesWrapper';
|
import NoticesWrapper from './components/NoticesWrapper';
|
||||||
|
|
||||||
subscribe(APP_READY, () => {
|
subscribe(APP_READY, () => {
|
||||||
ReactDOM.render(
|
const root = createRoot(document.getElementById('root'));
|
||||||
|
|
||||||
|
root.render(
|
||||||
|
<StrictMode>
|
||||||
<AppProvider store={store}>
|
<AppProvider store={store}>
|
||||||
<NoticesWrapper>
|
<NoticesWrapper>
|
||||||
<Routes>
|
<Routes>
|
||||||
@@ -38,15 +41,18 @@ subscribe(APP_READY, () => {
|
|||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</NoticesWrapper>
|
</NoticesWrapper>
|
||||||
</AppProvider>,
|
</AppProvider>
|
||||||
document.getElementById('root'),
|
</StrictMode>,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
subscribe(APP_INIT_ERROR, (error) => {
|
subscribe(APP_INIT_ERROR, (error) => {
|
||||||
ReactDOM.render(
|
const root = createRoot(document.getElementById('root'));
|
||||||
<ErrorPage message={error.message} />,
|
|
||||||
document.getElementById('root'),
|
root.render(
|
||||||
|
<StrictMode>
|
||||||
|
<ErrorPage message={error.message} />
|
||||||
|
</StrictMode>,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { render } from 'react-dom';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
APP_INIT_ERROR,
|
APP_INIT_ERROR,
|
||||||
APP_READY,
|
APP_READY,
|
||||||
@@ -11,10 +9,21 @@ import {
|
|||||||
import { configuration } from './config';
|
import { configuration } from './config';
|
||||||
import * as app from '.';
|
import * as app from '.';
|
||||||
|
|
||||||
jest.mock('react-dom', () => ({
|
// These need to be var not let so they get hoisted
|
||||||
render: jest.fn(),
|
// and can be used by jest.mock (which is also hoisted)
|
||||||
|
var mockRender; // eslint-disable-line no-var
|
||||||
|
var mockCreateRoot; // eslint-disable-line no-var
|
||||||
|
jest.mock('react-dom/client', () => {
|
||||||
|
mockRender = jest.fn();
|
||||||
|
mockCreateRoot = jest.fn(() => ({
|
||||||
|
render: mockRender,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
return ({
|
||||||
|
createRoot: mockCreateRoot,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
jest.mock('@edx/frontend-platform', () => ({
|
jest.mock('@edx/frontend-platform', () => ({
|
||||||
mergeConfig: jest.fn(),
|
mergeConfig: jest.fn(),
|
||||||
ensureConfig: jest.fn(),
|
ensureConfig: jest.fn(),
|
||||||
@@ -32,7 +41,9 @@ describe('app registry', () => {
|
|||||||
let getElement;
|
let getElement;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
render.mockClear();
|
mockCreateRoot.mockClear();
|
||||||
|
mockRender.mockClear();
|
||||||
|
|
||||||
getElement = window.document.getElementById;
|
getElement = window.document.getElementById;
|
||||||
window.document.getElementById = jest.fn(id => ({ id }));
|
window.document.getElementById = jest.fn(id => ({ id }));
|
||||||
});
|
});
|
||||||
@@ -44,18 +55,16 @@ describe('app registry', () => {
|
|||||||
const callArgs = subscribe.mock.calls[0];
|
const callArgs = subscribe.mock.calls[0];
|
||||||
expect(callArgs[0]).toEqual(APP_READY);
|
expect(callArgs[0]).toEqual(APP_READY);
|
||||||
callArgs[1]();
|
callArgs[1]();
|
||||||
const [rendered, target] = render.mock.calls[0];
|
const [rendered] = mockRender.mock.calls[0];
|
||||||
expect(rendered).toMatchSnapshot();
|
expect(rendered).toMatchSnapshot();
|
||||||
expect(target).toEqual(document.getElementById('root'));
|
|
||||||
});
|
});
|
||||||
test('subscribe: APP_INIT_ERROR. snapshot: displays an ErrorPage to root element', () => {
|
test('subscribe: APP_INIT_ERROR. snapshot: displays an ErrorPage to root element', () => {
|
||||||
const callArgs = subscribe.mock.calls[1];
|
const callArgs = subscribe.mock.calls[1];
|
||||||
expect(callArgs[0]).toEqual(APP_INIT_ERROR);
|
expect(callArgs[0]).toEqual(APP_INIT_ERROR);
|
||||||
const error = { message: 'test-error-message' };
|
const error = { message: 'test-error-message' };
|
||||||
callArgs[1](error);
|
callArgs[1](error);
|
||||||
const [rendered, target] = render.mock.calls[0];
|
const [rendered] = mockRender.mock.calls[0];
|
||||||
expect(rendered).toMatchSnapshot();
|
expect(rendered).toMatchSnapshot();
|
||||||
expect(target).toEqual(document.getElementById('root'));
|
|
||||||
});
|
});
|
||||||
test('initialize is called with requireAuthenticatedUser', () => {
|
test('initialize is called with requireAuthenticatedUser', () => {
|
||||||
expect(initialize).toHaveBeenCalledTimes(1);
|
expect(initialize).toHaveBeenCalledTimes(1);
|
||||||
|
|||||||
48
src/plugin-slots/CourseBannerSlot/README.md
Normal file
48
src/plugin-slots/CourseBannerSlot/README.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# Course Card Action Slot
|
||||||
|
|
||||||
|
### Slot ID: `org.openedx.frontend.learner_dashboard.course_card_banner.v1`
|
||||||
|
### Props:
|
||||||
|
* `cardId`
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
This slot is used for replacing or adding content for the `CourseBanner` component. This banner is rendered as a child of the `CourseCard`.
|
||||||
|
|
||||||
|
The default CourseBanner looks like this when audit access has expired for the course:
|
||||||
|

|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
The following `env.config.jsx` will render a custom implemenation of a CourseBanner under every `CourseCard`.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```js
|
||||||
|
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
import { Alert } from '@openedx/paragon';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
'org.openedx.frontend.learner_dashboard.course_card_banner.v1': {
|
||||||
|
keepDefault: false,
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Insert,
|
||||||
|
widget: {
|
||||||
|
id: 'org.openedx.frontend.learner_dashboard.course_card_banner.v1',
|
||||||
|
type: DIRECT_PLUGIN,
|
||||||
|
priority: 60,
|
||||||
|
RenderWidget: ({ cardId }) => (
|
||||||
|
<Alert variant="info" className="mb-0">
|
||||||
|
Course banner for course with {cardId}
|
||||||
|
</Alert>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 90 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 87 KiB |
23
src/plugin-slots/CourseBannerSlot/index.jsx
Normal file
23
src/plugin-slots/CourseBannerSlot/index.jsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||||
|
import CourseBanner from 'containers/CourseCard/components/CourseCardBanners/CourseBanner';
|
||||||
|
|
||||||
|
const CourseBannerSlot = ({ cardId }) => (
|
||||||
|
<PluginSlot
|
||||||
|
id="org.openedx.frontend.learner_dashboard.course_card_banner.v1"
|
||||||
|
pluginProps={{
|
||||||
|
cardId,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CourseBanner
|
||||||
|
cardId={cardId}
|
||||||
|
/>
|
||||||
|
</PluginSlot>
|
||||||
|
);
|
||||||
|
|
||||||
|
CourseBannerSlot.propTypes = {
|
||||||
|
cardId: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CourseBannerSlot;
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
# Course Card Action Slot
|
# Course Card Action Slot
|
||||||
|
|
||||||
### Slot ID: `course_card_action_slot`
|
### Slot ID: `org.openedx.frontend.learner_dashboard.course_card_action.v1`
|
||||||
|
|
||||||
|
### Slot ID Aliases
|
||||||
|
* `course_card_action_slot`
|
||||||
|
|
||||||
### Props:
|
### Props:
|
||||||
* `cardId`
|
* `cardId`
|
||||||
|
|
||||||
@@ -20,7 +24,7 @@ import ActionButton from 'containers/CourseCard/components/CourseCardActions/Act
|
|||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
pluginSlots: {
|
pluginSlots: {
|
||||||
course_card_action_slot: {
|
'org.openedx.frontend.learner_dashboard.course_card_action.v1': {
|
||||||
keepDefault: false,
|
keepDefault: false,
|
||||||
plugins: [
|
plugins: [
|
||||||
{
|
{
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user