Compare commits
96 Commits
mfrank/rem
...
hunia/upgr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
08d47dd9f1 | ||
|
|
f250efb660 | ||
|
|
c144c04aee | ||
|
|
0a52025a99 | ||
|
|
e4e02d4da2 | ||
|
|
0408a54372 | ||
|
|
134c741cf8 | ||
|
|
756e85f046 | ||
|
|
8b532aa49a | ||
|
|
cc544e4591 | ||
|
|
1bd6f71ac1 | ||
|
|
8914c7f4cc | ||
|
|
636216c5d3 | ||
|
|
a174abbc09 | ||
|
|
5134f8f85b | ||
|
|
1007dc40fb | ||
|
|
767596301a | ||
|
|
d76d13bcc2 | ||
|
|
bd495e98ee | ||
|
|
2f8ff3b517 | ||
|
|
629de04289 | ||
|
|
b4b3d0718d | ||
|
|
ed7a3ffdbc | ||
|
|
0cfebb6976 | ||
|
|
48e2c72180 | ||
|
|
3ce54cfc4a | ||
|
|
8969d011ff | ||
|
|
8fd6f2c7dc | ||
|
|
a2041bfc11 | ||
|
|
f836239ddb | ||
|
|
00129bcee0 | ||
|
|
c714abd656 | ||
|
|
e6baa0787c | ||
|
|
036e798637 | ||
|
|
db25a6c7e9 | ||
|
|
2d091895a8 | ||
|
|
5ea7c6cc0c | ||
|
|
72aa81f8dc |
2
.env
@@ -32,7 +32,6 @@ ENTERPRISE_MARKETING_UTM_SOURCE=''
|
||||
ENTERPRISE_MARKETING_UTM_CAMPAIGN=''
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=''
|
||||
LEARNING_BASE_URL=''
|
||||
ZENDESK_KEY=''
|
||||
HOTJAR_APP_ID=''
|
||||
HOTJAR_VERSION='6'
|
||||
HOTJAR_DEBUG=''
|
||||
@@ -41,3 +40,4 @@ ACCOUNT_PROFILE_URL=''
|
||||
ENABLE_NOTICES=''
|
||||
CAREER_LINK_URL=''
|
||||
ENABLE_EDX_PERSONAL_DASHBOARD=false
|
||||
ENABLE_PROGRAMS=false
|
||||
|
||||
@@ -20,7 +20,7 @@ LMS_CLIENT_ID='login-service-client-id'
|
||||
SEGMENT_KEY=''
|
||||
FEATURE_FLAGS={}
|
||||
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
||||
SUPPORT_URL='http://localhost:18000/support'
|
||||
SUPPORT_URL=''
|
||||
CONTACT_URL='http://localhost:18000/contact'
|
||||
OPEN_SOURCE_URL='http://localhost:18000/openedx'
|
||||
TERMS_OF_SERVICE_URL='http://localhost:18000/terms-of-service'
|
||||
@@ -38,7 +38,6 @@ ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
|
||||
LEARNING_BASE_URL='http://localhost:2000'
|
||||
SESSION_COOKIE_DOMAIN='localhost'
|
||||
ZENDESK_KEY=''
|
||||
HOTJAR_APP_ID=''
|
||||
HOTJAR_VERSION='6'
|
||||
HOTJAR_DEBUG=''
|
||||
@@ -47,3 +46,4 @@ ACCOUNT_PROFILE_URL='http://localhost:1995'
|
||||
ENABLE_NOTICES=''
|
||||
CAREER_LINK_URL=''
|
||||
ENABLE_EDX_PERSONAL_DASHBOARD=false
|
||||
ENABLE_PROGRAMS=false
|
||||
|
||||
@@ -20,7 +20,7 @@ LMS_CLIENT_ID='login-service-client-id'
|
||||
SEGMENT_KEY=''
|
||||
FEATURE_FLAGS={}
|
||||
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
||||
SUPPORT_URL='http://localhost:18000/support'
|
||||
SUPPORT_URL=''
|
||||
CONTACT_URL='http://localhost:18000/contact'
|
||||
OPEN_SOURCE_URL='http://localhost:18000/openedx'
|
||||
TERMS_OF_SERVICE_URL='http://localhost:18000/terms-of-service'
|
||||
@@ -37,7 +37,6 @@ ENTERPRISE_MARKETING_UTM_SOURCE='example.com'
|
||||
ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
|
||||
LEARNING_BASE_URL='http://localhost:2000'
|
||||
ZENDESK_KEY='test-zendesk-key'
|
||||
HOTJAR_APP_ID='hot-jar-app-id'
|
||||
HOTJAR_VERSION='6'
|
||||
HOTJAR_DEBUG=''
|
||||
@@ -46,3 +45,4 @@ ACCOUNT_PROFILE_URL='http://account-profile-url.test'
|
||||
ENABLE_NOTICES=''
|
||||
CAREER_LINK_URL=''
|
||||
ENABLE_EDX_PERSONAL_DASHBOARD=true
|
||||
ENABLE_PROGRAMS=false
|
||||
|
||||
7
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
version: 2
|
||||
updates:
|
||||
# Adding new check for github-actions
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
27
.github/workflows/ci.yml
vendored
@@ -10,18 +10,16 @@ on:
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Nodejs Env
|
||||
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup Nodejs
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VER }}
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
@@ -39,24 +37,7 @@ jobs:
|
||||
run: npm run build
|
||||
|
||||
- name: Run Coverage
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
|
||||
- name: Send failure notification
|
||||
if: ${{ failure() }}
|
||||
uses: dawidd6/action-send-mail@v3
|
||||
with:
|
||||
server_address: email-smtp.us-east-1.amazonaws.com
|
||||
server_port: 465
|
||||
username: ${{ secrets.EDX_SMTP_USERNAME }}
|
||||
password: ${{ secrets.EDX_SMTP_PASSWORD }}
|
||||
subject: CI workflow failed in ${{github.repository}}
|
||||
to: masters-grades@edx.org,aperture@2u-internal.opsgenie.net
|
||||
from: github-actions <github-actions@edx.org>
|
||||
nodemailerlog: true
|
||||
nodemailerdebug: true
|
||||
body: CI workflow in ${{github.repository}} failed!
|
||||
For details see "github.com/${{ github.repository }}/actions/runs/${{ github.run_id
|
||||
}}"
|
||||
|
||||
2
.github/workflows/lockfileversion-check.yml
vendored
@@ -10,4 +10,4 @@ on:
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
uses: openedx/.github/.github/workflows/lockfile-check.yml@master
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
|
||||
|
||||
35
.github/workflows/npm-publish.yml
vendored
@@ -1,35 +0,0 @@
|
||||
name: Release CI
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Nodejs Env
|
||||
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VER }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Create Build
|
||||
run: npm run build
|
||||
|
||||
- name: Release Package
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.SEMANTIC_RELEASE_GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.SEMANTIC_RELEASE_NPM_TOKEN }}
|
||||
run: npm semantic-release
|
||||
@@ -1,4 +1 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npm run lint
|
||||
|
||||
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": []
|
||||
}
|
||||
@@ -59,7 +59,6 @@ module.exports = {
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM: 'Footer',
|
||||
LEARNING_BASE_URL: 'http://localhost:2000',
|
||||
SESSION_COOKIE_DOMAIN: 'localhost',
|
||||
ZENDESK_KEY: '',
|
||||
HOTJAR_APP_ID: '',
|
||||
HOTJAR_VERSION: 6,
|
||||
HOTJAR_DEBUG: '',
|
||||
|
||||
10789
package-lock.json
generated
52
package.json
@@ -6,6 +6,9 @@
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/edx/frontend-app-learner-dashboard.git"
|
||||
},
|
||||
"browserslist": [
|
||||
"extends @edx/browserslist-config"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "fedx-scripts webpack",
|
||||
"i18n_extract": "fedx-scripts formatjs extract",
|
||||
@@ -13,11 +16,12 @@
|
||||
"lint-fix": "fedx-scripts eslint --fix --ext .jsx,.js src/",
|
||||
"semantic-release": "semantic-release",
|
||||
"start": "fedx-scripts webpack-dev-server --progress",
|
||||
"dev": "PUBLIC_PATH=/learner-dashboard/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
|
||||
"test": "TZ=GMT fedx-scripts jest --coverage --passWithNoTests",
|
||||
"quality": "npm run lint-fix && npm run test",
|
||||
"watch-tests": "jest --watch",
|
||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
||||
"prepare": "husky install"
|
||||
"prepare": "husky"
|
||||
},
|
||||
"author": "edX",
|
||||
"license": "AGPL-3.0",
|
||||
@@ -27,10 +31,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||
"@edx/browserslist-config": "^1.1.0",
|
||||
"@edx/frontend-component-header": "^5.3.1",
|
||||
"@edx/frontend-enterprise-hotjar": "3.0.0",
|
||||
"@edx/frontend-platform": "8.1.1",
|
||||
"@edx/frontend-component-header": "^5.6.0",
|
||||
"@edx/frontend-enterprise-hotjar": "7.1.0",
|
||||
"@edx/frontend-platform": "8.1.5",
|
||||
"@edx/openedx-atlas": "^0.6.0",
|
||||
"@edx/react-unit-test-utils": "3.0.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
||||
@@ -40,23 +43,13 @@
|
||||
"@openedx/frontend-plugin-framework": "^1.2.0",
|
||||
"@openedx/frontend-slot-footer": "^1.0.2",
|
||||
"@openedx/paragon": "^22.2.2",
|
||||
"@redux-beacon/segment": "^1.1.0",
|
||||
"@redux-devtools/extension": "3.3.0",
|
||||
"@reduxjs/toolkit": "^1.6.1",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"axios": "^0.28.0",
|
||||
"@reduxjs/toolkit": "^2.0.0",
|
||||
"classnames": "^2.3.1",
|
||||
"core-js": "3.38.0",
|
||||
"dompurify": "^2.3.1",
|
||||
"email-prop-type": "^3.0.1",
|
||||
"file-saver": "^2.0.5",
|
||||
"filesize": "^8.0.6",
|
||||
"core-js": "3.40.0",
|
||||
"filesize": "^10.0.0",
|
||||
"font-awesome": "4.7.0",
|
||||
"history": "5.3.0",
|
||||
"html-react-parser": "^1.3.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"jest-when": "^3.6.0",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"prop-types": "15.8.1",
|
||||
@@ -64,34 +57,33 @@
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-intl": "6.6.8",
|
||||
"react-pdf": "^7.0.0",
|
||||
"react-intl": "6.8.9",
|
||||
"react-redux": "^7.2.4",
|
||||
"react-router-dom": "6.26.0",
|
||||
"react-router-dom": "6.29.0",
|
||||
"react-share": "^4.4.0",
|
||||
"react-zendesk": "^0.1.13",
|
||||
"redux": "4.2.1",
|
||||
"redux-beacon": "^2.1.0",
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-thunk": "2.4.2",
|
||||
"regenerator-runtime": "^0.14.0",
|
||||
"reselect": "^4.0.0",
|
||||
"universal-cookie": "^4.0.4",
|
||||
"util": "^0.12.4",
|
||||
"whatwg-fetch": "^3.6.2"
|
||||
"util": "^0.12.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/browserslist-config": "^1.3.0",
|
||||
"@edx/reactifex": "^2.1.1",
|
||||
"@openedx/frontend-build": "14.0.15",
|
||||
"@openedx/frontend-build": "14.2.2",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^12.1.0",
|
||||
"copy-webpack-plugin": "^12.0.0",
|
||||
"husky": "^7.0.0",
|
||||
"husky": "^9.0.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-expect-message": "^1.1.3",
|
||||
"react-dev-utils": "^11.0.4",
|
||||
"jest-when": "^3.6.0",
|
||||
"react-dev-utils": "^12.0.0",
|
||||
"react-test-renderer": "^17.0.2",
|
||||
"redux-mock-store": "^1.5.4",
|
||||
"semantic-release": "^20.1.3"
|
||||
"redux-mock-store": "^1.5.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
} from 'data/redux';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import Dashboard from 'containers/Dashboard';
|
||||
import ZendeskFab from 'components/ZendeskFab';
|
||||
|
||||
import track from 'tracking';
|
||||
|
||||
@@ -93,7 +92,6 @@ export const App = () => {
|
||||
</main>
|
||||
</AppWrapper>
|
||||
<FooterSlot />
|
||||
<ZendeskFab />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -17,7 +17,6 @@ jest.mock('@edx/frontend-component-footer', () => ({ FooterSlot: 'Footer' }));
|
||||
|
||||
jest.mock('containers/Dashboard', () => 'Dashboard');
|
||||
jest.mock('containers/LearnerDashboardHeader', () => 'LearnerDashboardHeader');
|
||||
jest.mock('components/ZendeskFab', () => 'ZendeskFab');
|
||||
jest.mock('containers/WidgetContainers/AppWrapper', () => 'AppWrapper');
|
||||
jest.mock('data/redux', () => ({
|
||||
selectors: 'redux.selectors',
|
||||
|
||||
@@ -28,7 +28,6 @@ exports[`App router component component initialize failure snapshot 1`] = `
|
||||
</main>
|
||||
</AppWrapper>
|
||||
<FooterSlot />
|
||||
<ZendeskFab />
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
@@ -55,7 +54,6 @@ exports[`App router component component no network failure snapshot 1`] = `
|
||||
</main>
|
||||
</AppWrapper>
|
||||
<FooterSlot />
|
||||
<ZendeskFab />
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
@@ -82,7 +80,6 @@ exports[`App router component component no network failure with optimizely proje
|
||||
</main>
|
||||
</AppWrapper>
|
||||
<FooterSlot />
|
||||
<ZendeskFab />
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
@@ -109,7 +106,6 @@ exports[`App router component component no network failure with optimizely url s
|
||||
</main>
|
||||
</AppWrapper>
|
||||
<FooterSlot />
|
||||
<ZendeskFab />
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
@@ -142,7 +138,6 @@ exports[`App router component component refresh failure snapshot 1`] = `
|
||||
</main>
|
||||
</AppWrapper>
|
||||
<FooterSlot />
|
||||
<ZendeskFab />
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
@@ -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,12 +12,13 @@ const configuration = {
|
||||
// ACCESS_TOKEN_COOKIE_NAME: process.env.ACCESS_TOKEN_COOKIE_NAME,
|
||||
LEARNING_BASE_URL: process.env.LEARNING_BASE_URL,
|
||||
SESSION_COOKIE_DOMAIN: process.env.SESSION_COOKIE_DOMAIN || '',
|
||||
ZENDESK_KEY: process.env.ZENDESK_KEY,
|
||||
SUPPORT_URL: process.env.SUPPORT_URL || null,
|
||||
ENABLE_NOTICES: process.env.ENABLE_NOTICES || null,
|
||||
CAREER_LINK_URL: process.env.CAREER_LINK_URL || null,
|
||||
LOGO_URL: process.env.LOGO_URL,
|
||||
ENABLE_EDX_PERSONAL_DASHBOARD: process.env.ENABLE_EDX_PERSONAL_DASHBOARD === 'true',
|
||||
SEARCH_CATALOG_URL: process.env.SEARCH_CATALOG_URL || null,
|
||||
ENABLE_PROGRAMS: process.env.ENABLE_PROGRAMS === 'true',
|
||||
};
|
||||
|
||||
const features = {};
|
||||
|
||||
@@ -27,7 +27,7 @@ reduxHooks.useCardCourseRunData.mockReturnValue({ homeUrl });
|
||||
const execEdPath = (cardId) => `exec-ed-tracking-path=${cardId}`;
|
||||
reduxHooks.useCardExecEdTrackingParam.mockImplementation(execEdPath);
|
||||
reduxHooks.useTrackCourseEvent.mockImplementation(
|
||||
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
|
||||
(eventName, cardId, url) => ({ trackCourseEvent: { eventName, cardId, url } }),
|
||||
);
|
||||
|
||||
describe('BeginCourseButton', () => {
|
||||
|
||||
@@ -26,7 +26,7 @@ reduxHooks.useCardCourseRunData.mockReturnValue({ resumeUrl });
|
||||
const execEdPath = (cardId) => `exec-ed-tracking-path=${cardId}`;
|
||||
reduxHooks.useCardExecEdTrackingParam.mockImplementation(execEdPath);
|
||||
reduxHooks.useTrackCourseEvent.mockImplementation(
|
||||
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
|
||||
(eventName, cardId, url) => ({ trackCourseEvent: { eventName, cardId, url } }),
|
||||
);
|
||||
|
||||
let wrapper;
|
||||
|
||||
@@ -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: {
|
||||
useCardCourseRunData: jest.fn(() => ({ homeUrl: 'homeUrl' })),
|
||||
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": {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.enterCourseClicked],
|
||||
"upgradeUrl": "home-urlexec-ed-tracking-path=cardId",
|
||||
"url": "home-urlexec-ed-tracking-path=cardId",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ exports[`BeginCourseButton snapshot enabled snapshot 1`] = `
|
||||
"trackCourseEvent": {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.enterCourseClicked],
|
||||
"upgradeUrl": "home-urlexec-ed-tracking-path=cardId",
|
||||
"url": "home-urlexec-ed-tracking-path=cardId",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ exports[`ResumeButton snapshot disabled snapshot 1`] = `
|
||||
"trackCourseEvent": {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.enterCourseClicked],
|
||||
"upgradeUrl": "resume-urlexec-ed-tracking-path=cardId",
|
||||
"url": "resume-urlexec-ed-tracking-path=cardId",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ exports[`ResumeButton snapshot enabled snapshot 1`] = `
|
||||
"trackCourseEvent": {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.enterCourseClicked],
|
||||
"upgradeUrl": "resume-urlexec-ed-tracking-path=cardId",
|
||||
"url": "resume-urlexec-ed-tracking-path=cardId",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.enterCourseClicked],
|
||||
"upgradeUrl": "homeUrl",
|
||||
"url": "homeUrl",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ exports[`ViewCourseButton learner cannot view course 1`] = `
|
||||
"trackCourseEvent": {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.enterCourseClicked],
|
||||
"upgradeUrl": "homeUrl",
|
||||
"url": "homeUrl",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ActionRow } from '@openedx/paragon';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import UpgradeButton from './UpgradeButton';
|
||||
import CourseCardActionSlot from 'plugin-slots/CourseCardActionSlot';
|
||||
import SelectSessionButton from './SelectSessionButton';
|
||||
import BeginCourseButton from './BeginCourseButton';
|
||||
import ResumeButton from './ResumeButton';
|
||||
@@ -14,15 +14,13 @@ import ViewCourseButton from './ViewCourseButton';
|
||||
export const CourseCardActions = ({ cardId }) => {
|
||||
const { isEntitlement, isFulfilled } = reduxHooks.useCardEntitlementData(cardId);
|
||||
const {
|
||||
isVerified,
|
||||
hasStarted,
|
||||
isExecEd2UCourse,
|
||||
} = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const { isArchived } = reduxHooks.useCardCourseRunData(cardId);
|
||||
|
||||
return (
|
||||
<ActionRow data-test-id="CourseCardActions">
|
||||
{!(isEntitlement || isVerified || isExecEd2UCourse) && <UpgradeButton cardId={cardId} />}
|
||||
<CourseCardActionSlot cardId={cardId} />
|
||||
{isEntitlement && (isFulfilled
|
||||
? <ViewCourseButton cardId={cardId} />
|
||||
: <SelectSessionButton cardId={cardId} />
|
||||
|
||||
@@ -2,7 +2,7 @@ import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import UpgradeButton from './UpgradeButton';
|
||||
import CourseCardActionSlot from 'plugin-slots/CourseCardActionSlot';
|
||||
import SelectSessionButton from './SelectSessionButton';
|
||||
import BeginCourseButton from './BeginCourseButton';
|
||||
import ResumeButton from './ResumeButton';
|
||||
@@ -19,7 +19,7 @@ jest.mock('hooks', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('./UpgradeButton', () => 'UpgradeButton');
|
||||
jest.mock('plugin-slots/CourseCardActionSlot', () => 'CustomActionButton');
|
||||
jest.mock('./SelectSessionButton', () => 'SelectSessionButton');
|
||||
jest.mock('./ViewCourseButton', () => 'ViewCourseButton');
|
||||
jest.mock('./BeginCourseButton', () => 'BeginCourseButton');
|
||||
@@ -57,19 +57,7 @@ describe('CourseCardActions', () => {
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
describe('Exec Ed course', () => {
|
||||
it('does not render upgrade button', () => {
|
||||
mockHooks({ isExecEd2UCourse: true });
|
||||
render();
|
||||
expect(el.instance.findByType(UpgradeButton).length).toEqual(0);
|
||||
});
|
||||
});
|
||||
describe('entitlement course', () => {
|
||||
it('does not render upgrade button', () => {
|
||||
mockHooks({ isEntitlement: true });
|
||||
render();
|
||||
expect(el.instance.findByType(UpgradeButton).length).toEqual(0);
|
||||
});
|
||||
it('renders ViewCourseButton if fulfilled', () => {
|
||||
mockHooks({ isEntitlement: true, isFulfilled: true });
|
||||
render();
|
||||
@@ -81,33 +69,26 @@ describe('CourseCardActions', () => {
|
||||
expect(el.instance.findByType(SelectSessionButton)[0].props.cardId).toEqual(cardId);
|
||||
});
|
||||
});
|
||||
describe('verified course', () => {
|
||||
it('does not render upgrade button', () => {
|
||||
mockHooks({ isVerified: true });
|
||||
render();
|
||||
expect(el.instance.findByType(UpgradeButton).length).toEqual(0);
|
||||
});
|
||||
});
|
||||
describe('not entielement, verified, or exec ed', () => {
|
||||
it('renders UpgradeButton and ViewCourseButton for archived courses', () => {
|
||||
describe('not entitlement, verified, or exec ed', () => {
|
||||
it('renders CourseCardActionSlot and ViewCourseButton for archived courses', () => {
|
||||
mockHooks({ isArchived: true });
|
||||
render();
|
||||
expect(el.instance.findByType(UpgradeButton)[0].props.cardId).toEqual(cardId);
|
||||
expect(el.instance.findByType(CourseCardActionSlot)[0].props.cardId).toEqual(cardId);
|
||||
expect(el.instance.findByType(ViewCourseButton)[0].props.cardId).toEqual(cardId);
|
||||
});
|
||||
describe('unstarted courses', () => {
|
||||
it('renders UpgradeButton and BeginCourseButton', () => {
|
||||
it('renders CourseCardActionSlot and BeginCourseButton', () => {
|
||||
mockHooks();
|
||||
render();
|
||||
expect(el.instance.findByType(UpgradeButton)[0].props.cardId).toEqual(cardId);
|
||||
expect(el.instance.findByType(CourseCardActionSlot)[0].props.cardId).toEqual(cardId);
|
||||
expect(el.instance.findByType(BeginCourseButton)[0].props.cardId).toEqual(cardId);
|
||||
});
|
||||
});
|
||||
describe('active courses (started, and not archived)', () => {
|
||||
it('renders UpgradeButton and ResumeButton', () => {
|
||||
it('renders CourseCardActionSlot and ResumeButton', () => {
|
||||
mockHooks({ hasStarted: true });
|
||||
render();
|
||||
expect(el.instance.findByType(UpgradeButton)[0].props.cardId).toEqual(cardId);
|
||||
expect(el.instance.findByType(CourseCardActionSlot)[0].props.cardId).toEqual(cardId);
|
||||
expect(el.instance.findByType(ResumeButton)[0].props.cardId).toEqual(cardId);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
upgrade: {
|
||||
id: 'learner-dash.courseCard.actions.upgrade',
|
||||
description: 'Course card upgrade button text',
|
||||
defaultMessage: 'Upgrade',
|
||||
},
|
||||
beginCourse: {
|
||||
id: 'learner-dash.courseCard.actions.beginCourse',
|
||||
description: 'Course card begin-course button text',
|
||||
|
||||
@@ -8,7 +8,7 @@ exports[`CourseCardBanners render with isEnrolled false 1`] = `
|
||||
<RelatedProgramsBanner
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
<CourseBanner
|
||||
<CourseBannerSlot
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
<EntitlementBanner
|
||||
@@ -25,7 +25,7 @@ exports[`CourseCardBanners renders default CourseCardBanners 1`] = `
|
||||
<RelatedProgramsBanner
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
<CourseBanner
|
||||
<CourseBannerSlot
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
<EntitlementBanner
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import CourseBanner from './CourseBanner';
|
||||
import CourseBannerSlot from 'plugin-slots/CourseBannerSlot';
|
||||
import CertificateBanner from './CertificateBanner';
|
||||
import CreditBanner from './CreditBanner';
|
||||
import EntitlementBanner from './EntitlementBanner';
|
||||
@@ -14,7 +14,7 @@ export const CourseCardBanners = ({ cardId }) => {
|
||||
return (
|
||||
<div className="course-card-banners" data-testid="CourseCardBanners">
|
||||
<RelatedProgramsBanner cardId={cardId} />
|
||||
<CourseBanner cardId={cardId} />
|
||||
<CourseBannerSlot cardId={cardId} />
|
||||
<EntitlementBanner cardId={cardId} />
|
||||
{isEnrolled && <CertificateBanner cardId={cardId} />}
|
||||
{isEnrolled && <CreditBanner cardId={cardId} />}
|
||||
|
||||
@@ -20,11 +20,13 @@ export const CourseCardImage = ({ cardId, orientation }) => {
|
||||
const { isVerified } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const { disableCourseTitle } = useActionDisabledState(cardId);
|
||||
const handleImageClicked = reduxHooks.useTrackCourseEvent(courseImageClicked, cardId, homeUrl);
|
||||
const wrapperClassName = `pgn__card-wrapper-image-cap overflow-visible ${orientation}`;
|
||||
const wrapperClassName = `pgn__card-wrapper-image-cap d-inline-block overflow-visible ${orientation}`;
|
||||
const image = (
|
||||
<>
|
||||
<img
|
||||
className="pgn__card-image-cap show"
|
||||
// w-100 is necessary for images on Safari, otherwise stretches full height of the image
|
||||
// https://stackoverflow.com/a/44250830
|
||||
className="pgn__card-image-cap w-100 show"
|
||||
src={bannerImgSrc}
|
||||
alt={formatMessage(messages.bannerAlt)}
|
||||
/>
|
||||
|
||||
@@ -18,8 +18,8 @@ jest.mock('hooks', () => ({
|
||||
useCardCourseData: jest.fn(() => ({ bannerImgSrc: 'banner-img-src' })),
|
||||
useCardCourseRunData: jest.fn(() => ({ homeUrl })),
|
||||
useCardEnrollmentData: jest.fn(() => ({ isVerified: true })),
|
||||
useTrackCourseEvent: jest.fn((eventName, cardId, upgradeUrl) => ({
|
||||
trackCourseEvent: { eventName, cardId, upgradeUrl },
|
||||
useTrackCourseEvent: jest.fn((eventName, cardId, url) => ({
|
||||
trackCourseEvent: { eventName, cardId, url },
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -17,8 +17,8 @@ jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCourseData: jest.fn(() => ({ courseName: 'course-name' })),
|
||||
useCardCourseRunData: jest.fn(() => ({ homeUrl })),
|
||||
useTrackCourseEvent: jest.fn((eventName, cardId, upgradeUrl) => ({
|
||||
trackCourseEvent: { eventName, cardId, upgradeUrl },
|
||||
useTrackCourseEvent: jest.fn((eventName, cardId, url) => ({
|
||||
trackCourseEvent: { eventName, cardId, url },
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
exports[`CourseCardImage snapshot renders clickable link course Image 1`] = `
|
||||
<a
|
||||
className="pgn__card-wrapper-image-cap overflow-visible orientation"
|
||||
className="pgn__card-wrapper-image-cap d-inline-block overflow-visible orientation"
|
||||
href="home-url"
|
||||
onClick={
|
||||
{
|
||||
"trackCourseEvent": {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.courseImageClicked],
|
||||
"upgradeUrl": "home-url",
|
||||
"url": "home-url",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ exports[`CourseCardImage snapshot renders clickable link course Image 1`] = `
|
||||
<Fragment>
|
||||
<img
|
||||
alt="Course thumbnail"
|
||||
className="pgn__card-image-cap show"
|
||||
className="pgn__card-image-cap w-100 show"
|
||||
src="banner-img-src"
|
||||
/>
|
||||
<span
|
||||
@@ -43,12 +43,12 @@ exports[`CourseCardImage snapshot renders clickable link course Image 1`] = `
|
||||
|
||||
exports[`CourseCardImage snapshot renders disabled link 1`] = `
|
||||
<div
|
||||
className="pgn__card-wrapper-image-cap overflow-visible orientation"
|
||||
className="pgn__card-wrapper-image-cap d-inline-block overflow-visible orientation"
|
||||
>
|
||||
<Fragment>
|
||||
<img
|
||||
alt="Course thumbnail"
|
||||
className="pgn__card-image-cap show"
|
||||
className="pgn__card-image-cap w-100 show"
|
||||
src="banner-img-src"
|
||||
/>
|
||||
<span
|
||||
|
||||
@@ -11,7 +11,7 @@ exports[`CourseCardTitle snapshot renders clickable link course title 1`] = `
|
||||
"trackCourseEvent": {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.courseTitleClicked],
|
||||
"upgradeUrl": "home-url",
|
||||
"url": "home-url",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,19 +3,18 @@ import { reduxHooks } from 'hooks';
|
||||
export const useActionDisabledState = (cardId) => {
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const {
|
||||
canUpgrade, hasAccess, isAudit, isAuditAccessExpired,
|
||||
hasAccess, isAudit, isAuditAccessExpired,
|
||||
} = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const {
|
||||
isEntitlement, isFulfilled, canChange, hasSessions,
|
||||
} = reduxHooks.useCardEntitlementData(cardId);
|
||||
|
||||
const { resumeUrl, homeUrl, upgradeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||
const { resumeUrl, homeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||
|
||||
const disableBeginCourse = !homeUrl || (isMasquerading || !hasAccess || (isAudit && isAuditAccessExpired));
|
||||
const disableResumeCourse = !resumeUrl || (isMasquerading || !hasAccess || (isAudit && isAuditAccessExpired));
|
||||
const disableViewCourse = !hasAccess || (isAudit && isAuditAccessExpired);
|
||||
const disableSelectSession = !isEntitlement || isMasquerading || !hasAccess || (!canChange || !hasSessions);
|
||||
const disableUpgradeCourse = !upgradeUrl || (isMasquerading && !canUpgrade);
|
||||
|
||||
const disableCourseTitle = (isEntitlement && !isFulfilled) || disableViewCourse;
|
||||
|
||||
@@ -23,7 +22,6 @@ export const useActionDisabledState = (cardId) => {
|
||||
disableBeginCourse,
|
||||
disableResumeCourse,
|
||||
disableViewCourse,
|
||||
disableUpgradeCourse,
|
||||
disableSelectSession,
|
||||
disableCourseTitle,
|
||||
};
|
||||
|
||||
@@ -16,7 +16,6 @@ const cardId = 'my-test-course-number';
|
||||
describe('useActionDisabledState', () => {
|
||||
const defaultData = {
|
||||
isMasquerading: false,
|
||||
canUpgrade: false,
|
||||
isEntitlement: false,
|
||||
isFulfilled: false,
|
||||
canChange: false,
|
||||
@@ -26,12 +25,10 @@ describe('useActionDisabledState', () => {
|
||||
isAuditAccessExpired: false,
|
||||
resumeUrl: 'resume.url',
|
||||
homeUrl: 'home.url',
|
||||
upgradeUrl: 'upgrade.url',
|
||||
};
|
||||
const mockHooksData = (args) => {
|
||||
const {
|
||||
isMasquerading,
|
||||
canUpgrade,
|
||||
isEntitlement,
|
||||
isFulfilled,
|
||||
canChange,
|
||||
@@ -41,11 +38,9 @@ describe('useActionDisabledState', () => {
|
||||
isAuditAccessExpired,
|
||||
resumeUrl,
|
||||
homeUrl,
|
||||
upgradeUrl,
|
||||
} = { ...defaultData, ...args };
|
||||
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading });
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({
|
||||
canUpgrade,
|
||||
hasAccess,
|
||||
isAudit,
|
||||
isAuditAccessExpired,
|
||||
@@ -59,7 +54,6 @@ describe('useActionDisabledState', () => {
|
||||
reduxHooks.useCardCourseRunData.mockReturnValueOnce({
|
||||
resumeUrl,
|
||||
homeUrl,
|
||||
upgradeUrl,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -121,21 +115,6 @@ describe('useActionDisabledState', () => {
|
||||
testDisabled({ hasAccess: true }, false);
|
||||
});
|
||||
});
|
||||
describe('disableUpgradeCourse', () => {
|
||||
const testDisabled = (data, expected) => {
|
||||
mockHooksData(data);
|
||||
expect(runHook().disableUpgradeCourse).toBe(expected);
|
||||
};
|
||||
it('disable when upgradeUrl is invalid', () => {
|
||||
testDisabled({ upgradeUrl: null }, true);
|
||||
});
|
||||
it('disable when isMasquerading is true and canUpgrade is false', () => {
|
||||
testDisabled({ isMasquerading: true, canUpgrade: false }, true);
|
||||
});
|
||||
it('enable when all conditions are met', () => {
|
||||
testDisabled({ canUpgrade: true }, false);
|
||||
});
|
||||
});
|
||||
describe('disableSelectSession', () => {
|
||||
const testDisabled = (data, expected) => {
|
||||
mockHooksData(data);
|
||||
|
||||
@@ -9,9 +9,10 @@ import CourseCard from 'containers/CourseCard';
|
||||
|
||||
import { useIsCollapsed } from './hooks';
|
||||
|
||||
export const CourseList = ({
|
||||
filterOptions, setPageNumber, numPages, showFilters, visibleList,
|
||||
}) => {
|
||||
export const CourseList = ({ courseListData }) => {
|
||||
const {
|
||||
filterOptions, setPageNumber, numPages, showFilters, visibleList,
|
||||
} = courseListData;
|
||||
const isCollapsed = useIsCollapsed();
|
||||
return (
|
||||
<>
|
||||
@@ -38,14 +39,16 @@ export const CourseList = ({
|
||||
);
|
||||
};
|
||||
|
||||
CourseList.propTypes = {
|
||||
export const courseListDataShape = PropTypes.shape({
|
||||
showFilters: PropTypes.bool.isRequired,
|
||||
// eslint-disable-next-line react/forbid-prop-types
|
||||
visibleList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
// eslint-disable-next-line react/forbid-prop-types
|
||||
filterOptions: PropTypes.object.isRequired,
|
||||
visibleList: PropTypes.arrayOf(PropTypes.shape()).isRequired,
|
||||
filterOptions: PropTypes.shape().isRequired,
|
||||
numPages: PropTypes.number.isRequired,
|
||||
setPageNumber: PropTypes.func.isRequired,
|
||||
});
|
||||
|
||||
CourseList.propTypes = {
|
||||
courseListData: courseListDataShape,
|
||||
};
|
||||
|
||||
export default CourseList;
|
||||
|
||||
@@ -23,7 +23,7 @@ describe('CourseList', () => {
|
||||
useIsCollapsed.mockReturnValue(false);
|
||||
|
||||
const createWrapper = (courseListData = defaultCourseListData) => (
|
||||
shallow(<CourseList {...courseListData} />)
|
||||
shallow(<CourseList courseListData={courseListData} />)
|
||||
);
|
||||
|
||||
describe('no courses or filters', () => {
|
||||
|
||||
@@ -18,11 +18,7 @@ exports[`CoursesPanel no courses snapshot 1`] = `
|
||||
<CourseFilterControls />
|
||||
</div>
|
||||
</div>
|
||||
<PluginSlot
|
||||
id="no_courses_view"
|
||||
>
|
||||
<NoCoursesView />
|
||||
</PluginSlot>
|
||||
<NoCoursesViewSlot />
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -44,16 +40,16 @@ exports[`CoursesPanel with courses snapshot 1`] = `
|
||||
<CourseFilterControls />
|
||||
</div>
|
||||
</div>
|
||||
<PluginSlot
|
||||
id="course_list"
|
||||
>
|
||||
<CourseList
|
||||
filterOptions={{}}
|
||||
numPages={1}
|
||||
setPageNumber={[MockFunction setPageNumber]}
|
||||
showFilters={false}
|
||||
visibleList={[]}
|
||||
/>
|
||||
</PluginSlot>
|
||||
<CourseListSlot
|
||||
courseListData={
|
||||
{
|
||||
"filterOptions": {},
|
||||
"numPages": 1,
|
||||
"setPageNumber": [MockFunction setPageNumber],
|
||||
"showFilters": false,
|
||||
"visibleList": [],
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import {
|
||||
CourseFilterControls,
|
||||
} from 'containers/CourseFilterControls';
|
||||
import NoCoursesView from './NoCoursesView';
|
||||
|
||||
import CourseList from './CourseList';
|
||||
import CourseListSlot from 'plugin-slots/CourseListSlot';
|
||||
import NoCoursesViewSlot from 'plugin-slots/NoCoursesViewSlot';
|
||||
|
||||
import { useCourseListData } from './hooks';
|
||||
|
||||
@@ -34,19 +32,7 @@ export const CoursesPanel = () => {
|
||||
<CourseFilterControls {...courseListData.filterOptions} />
|
||||
</div>
|
||||
</div>
|
||||
{hasCourses ? (
|
||||
<PluginSlot
|
||||
id="course_list"
|
||||
>
|
||||
<CourseList {...courseListData} />
|
||||
</PluginSlot>
|
||||
) : (
|
||||
<PluginSlot
|
||||
id="no_courses_view"
|
||||
>
|
||||
<NoCoursesView />
|
||||
</PluginSlot>
|
||||
)}
|
||||
{hasCourses ? <CourseListSlot courseListData={courseListData} /> : <NoCoursesViewSlot />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { Container, Col, Row } from '@openedx/paragon';
|
||||
|
||||
import WidgetSidebar from '../WidgetContainers/WidgetSidebar';
|
||||
import WidgetSidebarSlot from 'plugin-slots/WidgetSidebarSlot';
|
||||
|
||||
import hooks from './hooks';
|
||||
|
||||
@@ -42,7 +42,7 @@ export const DashboardLayout = ({ children }) => {
|
||||
</Col>
|
||||
<Col {...columnConfig.sidebar} className="sidebar-column">
|
||||
{!isCollapsed && (<h2 className="course-list-title"> </h2>)}
|
||||
<WidgetSidebar />
|
||||
<WidgetSidebarSlot />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
|
||||
@@ -36,9 +36,9 @@ describe('DashboardLayout', () => {
|
||||
const columns = el.instance.findByType(Row)[0].findByType(Col);
|
||||
expect(columns[0].children).not.toHaveLength(0);
|
||||
});
|
||||
it('displays WidgetSidebar in second column', () => {
|
||||
it('displays WidgetSidebarSlot in second column', () => {
|
||||
const columns = el.instance.findByType(Row)[0].findByType(Col);
|
||||
expect(columns[1].findByType('WidgetSidebar')).toHaveLength(1);
|
||||
expect(columns[1].findByType('WidgetSidebarSlot')).toHaveLength(1);
|
||||
});
|
||||
};
|
||||
const testSidebarLayout = () => {
|
||||
|
||||
@@ -38,7 +38,7 @@ exports[`DashboardLayout collapsed sidebar not showing snapshot 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
<WidgetSidebar />
|
||||
<WidgetSidebarSlot />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
@@ -82,7 +82,7 @@ exports[`DashboardLayout collapsed sidebar showing snapshot 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
<WidgetSidebar />
|
||||
<WidgetSidebarSlot />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
@@ -131,7 +131,7 @@ exports[`DashboardLayout not collapsed sidebar not showing snapshot 1`] = `
|
||||
>
|
||||
|
||||
</h2>
|
||||
<WidgetSidebar />
|
||||
<WidgetSidebarSlot />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
@@ -180,7 +180,7 @@ exports[`DashboardLayout not collapsed sidebar showing snapshot 1`] = `
|
||||
>
|
||||
|
||||
</h2>
|
||||
<WidgetSidebar />
|
||||
<WidgetSidebarSlot />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
|
||||
@@ -17,11 +17,11 @@ const getLearnerHeaderMenu = (
|
||||
content: formatMessage(messages.course),
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
...(getConfig().ENABLE_PROGRAMS ? [{
|
||||
type: 'item',
|
||||
href: `${urls.programsUrl()}`,
|
||||
content: formatMessage(messages.program),
|
||||
},
|
||||
}] : []),
|
||||
{
|
||||
type: 'item',
|
||||
href: `${urls.baseAppUrl(courseSearchUrl)}`,
|
||||
@@ -32,11 +32,11 @@ const getLearnerHeaderMenu = (
|
||||
},
|
||||
],
|
||||
secondaryMenu: [
|
||||
{
|
||||
...(getConfig().SUPPORT_URL ? [{
|
||||
type: 'item',
|
||||
href: `${getConfig().SUPPORT_URL}`,
|
||||
content: formatMessage(messages.help),
|
||||
},
|
||||
}] : []),
|
||||
],
|
||||
userMenu: [
|
||||
{
|
||||
@@ -70,6 +70,7 @@ const getLearnerHeaderMenu = (
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export default getLearnerHeaderMenu;
|
||||
|
||||
@@ -12,11 +12,6 @@ exports[`LearnerDashboardHeader render 1`] = `
|
||||
"isActive": true,
|
||||
"type": "item",
|
||||
},
|
||||
{
|
||||
"content": "Programs",
|
||||
"href": "http://localhost:18000/dashboard/programs",
|
||||
"type": "item",
|
||||
},
|
||||
{
|
||||
"content": "Discover New",
|
||||
"href": "http://localhost:18000/course-search-url",
|
||||
@@ -25,15 +20,7 @@ exports[`LearnerDashboardHeader render 1`] = `
|
||||
},
|
||||
]
|
||||
}
|
||||
secondaryMenuItems={
|
||||
[
|
||||
{
|
||||
"content": "Help",
|
||||
"href": "http://localhost:18000/support",
|
||||
"type": "item",
|
||||
},
|
||||
]
|
||||
}
|
||||
secondaryMenuItems={[]}
|
||||
userMenuItems={
|
||||
[
|
||||
{
|
||||
|
||||
@@ -56,7 +56,7 @@ describe('LearnerDashboardHeader hooks', () => {
|
||||
username: 'test',
|
||||
};
|
||||
const learnerHomeHeaderMenu = useLearnerDashboardHeaderMenu({ courseSearchUrl, authenticatedUser });
|
||||
expect(learnerHomeHeaderMenu.mainMenu.length).toBe(3);
|
||||
expect(learnerHomeHeaderMenu.mainMenu.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -29,7 +29,19 @@ describe('LearnerDashboardHeader', () => {
|
||||
expect(wrapper.instance.findByType('ConfirmEmailBanner')).toHaveLength(1);
|
||||
expect(wrapper.instance.findByType('MasqueradeBar')).toHaveLength(1);
|
||||
expect(wrapper.instance.findByType(Header)).toHaveLength(1);
|
||||
wrapper.instance.findByType(Header)[0].props.mainMenuItems[2].onClick();
|
||||
wrapper.instance.findByType(Header)[0].props.mainMenuItems[1].onClick();
|
||||
expect(findCoursesNavClicked).toHaveBeenCalledWith(urls.baseAppUrl('/course-search-url'));
|
||||
expect(wrapper.instance.findByType(Header)[0].props.secondaryMenuItems.length).toBe(0);
|
||||
});
|
||||
|
||||
test('should display Help link if SUPPORT_URL is set', () => {
|
||||
mergeConfig({ SUPPORT_URL: 'http://localhost:18000/support' });
|
||||
const wrapper = shallow(<LearnerDashboardHeader />);
|
||||
expect(wrapper.instance.findByType(Header)[0].props.secondaryMenuItems.length).toBe(1);
|
||||
});
|
||||
test('should display Programs link if it is enabled by configuration', () => {
|
||||
mergeConfig({ ENABLE_PROGRAMS: true });
|
||||
const wrapper = shallow(<LearnerDashboardHeader />);
|
||||
expect(wrapper.instance.findByType(Header)[0].props.mainMenuItems.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`WidgetSidebar snapshots 1`] = `
|
||||
<div
|
||||
className="widget-sidebar px-2"
|
||||
>
|
||||
<div
|
||||
className="d-flex"
|
||||
>
|
||||
<PluginSlot
|
||||
id="widget_sidebar_plugin_slot"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,23 +0,0 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
// eslint-disable-next-line arrow-body-style
|
||||
export const WidgetSidebar = () => {
|
||||
const hasCourses = reduxHooks.useHasCourses();
|
||||
|
||||
const widgetSidebarClassNames = classNames('widget-sidebar', { 'px-2': !hasCourses });
|
||||
const innerWrapperClassNames = classNames('d-flex', { 'flex-column': hasCourses });
|
||||
|
||||
return (
|
||||
<div className={widgetSidebarClassNames}>
|
||||
<div className={innerWrapperClassNames}>
|
||||
<PluginSlot id="widget_sidebar_plugin_slot" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WidgetSidebar;
|
||||
@@ -52,7 +52,6 @@ export const courseCard = StrictDict({
|
||||
|
||||
homeUrl: courseRun.homeUrl,
|
||||
marketingUrl: courseRun.marketingUrl,
|
||||
upgradeUrl: courseRun.upgradeUrl,
|
||||
|
||||
progressUrl: baseAppUrl(courseRun.progressUrl),
|
||||
resumeUrl: baseAppUrl(courseRun.resumeUrl), // resume will route this to learning mfe.
|
||||
|
||||
@@ -156,7 +156,6 @@ describe('courseCard selectors module', () => {
|
||||
|
||||
homeUrl: 'test-home-url',
|
||||
marketingUrl: 'test-marketing-url',
|
||||
upgradeUrl: 'test-upgrade-url',
|
||||
|
||||
progressUrl: 'test-progress-url',
|
||||
resumeUrl: 'test-resume-url',
|
||||
@@ -181,10 +180,9 @@ describe('courseCard selectors module', () => {
|
||||
it('passes minPassingGrade floored from float to a percentage value', () => {
|
||||
expect(selected.minPassingGrade).toEqual(93);
|
||||
});
|
||||
it('passes [homeUrl, marketingUrl, upgradeUrl]', () => {
|
||||
it('passes [homeUrl, marketingUrl]', () => {
|
||||
expect(selected.homeUrl).toEqual(testData.homeUrl);
|
||||
expect(selected.marketingUrl).toEqual(testData.marketingUrl);
|
||||
expect(selected.upgradeUrl).toEqual(testData.upgradeUrl);
|
||||
});
|
||||
it('passes [progressUrl, unenrollUrl, resumeUrl], converted to baseAppUrl', () => {
|
||||
expect(selected.progressUrl).toEqual(baseAppUrl(testData.progressUrl));
|
||||
|
||||
@@ -50,12 +50,6 @@ export const logEvent = ({ eventName, data, courseId }) => post(urls.event(), {
|
||||
event: JSON.stringify(data),
|
||||
});
|
||||
|
||||
export const logUpgrade = ({ courseId }) => module.logEvent({
|
||||
eventName: eventNames.upgradeButtonClickedEnrollment,
|
||||
courseId,
|
||||
data: { location: 'learner-dashboard' },
|
||||
});
|
||||
|
||||
export const logShare = ({ courseId, site }) => module.logEvent({
|
||||
eventName: eventNames.shareClicked,
|
||||
courseId,
|
||||
@@ -78,7 +72,6 @@ export default {
|
||||
updateEntitlementEnrollment,
|
||||
deleteEntitlementEnrollment,
|
||||
logEvent,
|
||||
logUpgrade,
|
||||
logShare,
|
||||
createCreditRequest,
|
||||
};
|
||||
|
||||
@@ -130,13 +130,6 @@ describe('lms api methods', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(api, moduleKeys.logEvent).mockImplementation(logEvent);
|
||||
});
|
||||
test('logUpgrade sends enrollment upgrade click event with learner dashboard location', () => {
|
||||
expect(api.logUpgrade({ courseId })).toEqual(logEvent({
|
||||
eventName: eventNames.upgradeButtonClickedEnrollment,
|
||||
courseId,
|
||||
data: { location: 'learner-dashboard' },
|
||||
}));
|
||||
});
|
||||
test('logShare sends share clicke vent with course id, side and location', () => {
|
||||
const site = 'test-site';
|
||||
expect(api.logShare({ courseId, site })).toEqual(logEvent({
|
||||
|
||||
@@ -779,9 +779,6 @@ export const compileCourseRunData = ({ courseName, ...data }, index) => {
|
||||
courseProvider: getOption(providerOptions, index),
|
||||
programs: getOption(programsOptions, index),
|
||||
};
|
||||
if (out.enrollment.canUpgrade) {
|
||||
out.courseRun.upgradeUrl = 'test-upgrade-url';
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
|
||||
47
src/plugin-slots/CourseBannerSlot/README.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Course Card Action Slot
|
||||
|
||||
### Slot ID: `course_banner_slot`
|
||||
### Props:
|
||||
* `cardId`
|
||||
|
||||
## Description
|
||||
|
||||
This slot is used for replacing or adding content for the `CourseBanner` component. This banner is rendered as a child of the `CourseCard`.
|
||||
|
||||
The default CourseBanner looks like this when audit access has expired for the course:
|
||||

|
||||
|
||||
## 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';
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
course_banner_slot: {
|
||||
keepDefault: false,
|
||||
plugins: [
|
||||
{
|
||||
op: PLUGIN_OPERATIONS.Insert,
|
||||
widget: {
|
||||
id: 'custom_course_banner',
|
||||
type: DIRECT_PLUGIN,
|
||||
priority: 60,
|
||||
RenderWidget: ({ cardId }) => (
|
||||
<Alert variant="info" className="mb-0">
|
||||
Course banner for course with {cardId}
|
||||
</Alert>
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 87 KiB |
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="course_banner_slot"
|
||||
pluginProps={{
|
||||
cardId,
|
||||
}}
|
||||
>
|
||||
<CourseBanner
|
||||
cardId={cardId}
|
||||
/>
|
||||
</PluginSlot>
|
||||
);
|
||||
|
||||
CourseBannerSlot.propTypes = {
|
||||
cardId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default CourseBannerSlot;
|
||||
64
src/plugin-slots/CourseCardActionSlot/README.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Course Card Action Slot
|
||||
|
||||
### Slot ID: `course_card_action_slot`
|
||||
### Props:
|
||||
* `cardId`
|
||||
|
||||
## Description
|
||||
|
||||
This slot is used for adding content in the Action buttons section of each Course Card.
|
||||
|
||||
## Example
|
||||
|
||||
The following `env.config.jsx` will render the `cardId` of the course as `<p>` elements in a `<div>`.
|
||||
|
||||

|
||||
|
||||
```js
|
||||
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||
import ActionButton from 'containers/CourseCard/components/CourseCardActions/ActionButton';
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
course_card_action_slot: {
|
||||
keepDefault: false,
|
||||
plugins: [
|
||||
{
|
||||
// Insert Custom Button in Course Card
|
||||
op: PLUGIN_OPERATIONS.Insert,
|
||||
widget: {
|
||||
id: 'custom_course_card_action',
|
||||
priority: 60,
|
||||
type: DIRECT_PLUGIN,
|
||||
RenderWidget: ({cardId}) => (
|
||||
<ActionButton
|
||||
variant="outline-primary"
|
||||
>
|
||||
Custom Button
|
||||
</ ActionButton>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
// Insert Another Button in Course Card
|
||||
op: PLUGIN_OPERATIONS.Insert,
|
||||
widget: {
|
||||
id: 'another_custom_course_card_action',
|
||||
priority: 70,
|
||||
type: DIRECT_PLUGIN,
|
||||
RenderWidget: ({cardId}) => (
|
||||
<ActionButton
|
||||
variant="outline-primary"
|
||||
>
|
||||
📚: {cardId}
|
||||
</ ActionButton>
|
||||
),
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
After Width: | Height: | Size: 603 KiB |
18
src/plugin-slots/CourseCardActionSlot/index.jsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
const CourseCardActionSlot = ({ cardId }) => (
|
||||
<PluginSlot
|
||||
id="course_card_action_slot"
|
||||
pluginProps={{
|
||||
cardId,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
CourseCardActionSlot.propTypes = {
|
||||
cardId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default CourseCardActionSlot;
|
||||
60
src/plugin-slots/CourseListSlot/README.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Course List Slot
|
||||
|
||||
### Slot ID: `course_list_slot`
|
||||
|
||||
## Plugin Props
|
||||
|
||||
* courseListData
|
||||
|
||||
## Description
|
||||
|
||||
This slot is used for replacing or adding content around the `CourseList` component. The `CourseListSlot` is only rendered if the learner has enrolled in at least one course.
|
||||
|
||||
## Example
|
||||
|
||||
The space will show the `CourseList` component by default. This can be disabled in the configuration with the `keepDefault` boolean.
|
||||
|
||||

|
||||
|
||||
Setting the MFE's `env.config.jsx` to the following will replace the default experience with a list of course titles.
|
||||
|
||||

|
||||
|
||||
```js
|
||||
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
course_list_slot: {
|
||||
// Hide the default CourseList component
|
||||
keepDefault: false,
|
||||
plugins: [
|
||||
{
|
||||
op: PLUGIN_OPERATIONS.Insert,
|
||||
widget: {
|
||||
id: 'custom_course_list',
|
||||
type: DIRECT_PLUGIN,
|
||||
priority: 60,
|
||||
RenderWidget: ({ courseListData }) => {
|
||||
// Extract the "visibleList"
|
||||
const courses = courseListData.visibleList;
|
||||
// Render a list of course names
|
||||
return (
|
||||
<div>
|
||||
{courses.map(courseData => (
|
||||
<p>
|
||||
{courseData.course.courseName}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default config;
|
||||
```
|
||||
BIN
src/plugin-slots/CourseListSlot/images/course_list_slot.png
Normal file
|
After Width: | Height: | Size: 226 KiB |
|
After Width: | Height: | Size: 21 KiB |
16
src/plugin-slots/CourseListSlot/index.jsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
import { CourseList, courseListDataShape } from 'containers/CoursesPanel/CourseList';
|
||||
|
||||
export const CourseListSlot = ({ courseListData }) => (
|
||||
<PluginSlot id="course_list_slot" pluginProps={{ courseListData }}>
|
||||
<CourseList courseListData={courseListData} />
|
||||
</PluginSlot>
|
||||
);
|
||||
|
||||
CourseListSlot.propTypes = {
|
||||
courseListData: courseListDataShape,
|
||||
};
|
||||
|
||||
export default CourseListSlot;
|
||||
47
src/plugin-slots/NoCoursesViewSlot/README.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# No Courses View Slot
|
||||
|
||||
### Slot ID: `no_courses_view_slot`
|
||||
|
||||
## Description
|
||||
|
||||
This slot is used for replacing or adding content around the `NoCoursesView` component. The `NoCoursesViewSlot` only renders if the learner has not yet enrolled in any courses.
|
||||
|
||||
## Example
|
||||
|
||||
The space will show the `NoCoursesView` by default. This can be disabled in the configuration with the `keepDefault` boolean.
|
||||
|
||||

|
||||
|
||||
Setting the MFE's `env.config.jsx` to the following will replace the default experience with a custom call-to-action component.
|
||||
|
||||

|
||||
|
||||
```js
|
||||
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
no_courses_view_slot: {
|
||||
// Hide the default NoCoursesView component
|
||||
keepDefault: false,
|
||||
plugins: [
|
||||
{
|
||||
op: PLUGIN_OPERATIONS.Insert,
|
||||
widget: {
|
||||
id: 'custom_no_courses_CTA',
|
||||
type: DIRECT_PLUGIN,
|
||||
priority: 60,
|
||||
RenderWidget: () => (
|
||||
<h3>
|
||||
Check out our catalog of courses and start learning today!
|
||||
</h3>
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 24 KiB |
12
src/plugin-slots/NoCoursesViewSlot/index.jsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
import NoCoursesView from 'containers/CoursesPanel/NoCoursesView';
|
||||
|
||||
export const NoCoursesViewSlot = () => (
|
||||
<PluginSlot id="no_courses_view_slot">
|
||||
<NoCoursesView />
|
||||
</PluginSlot>
|
||||
);
|
||||
|
||||
export default NoCoursesViewSlot;
|
||||
@@ -1,3 +1,7 @@
|
||||
# `frontend-app-learner-dashboard` Plugin Slots
|
||||
|
||||
* [`course_card_action_slot`](./CourseCardActionSlot/)
|
||||
* [`footer_slot`](./FooterSlot/)
|
||||
* [`widget_sidebar_slot`](./WidgetSidebarSlot/)
|
||||
* [`course_list_slot`](./CourseListSlot/)
|
||||
* [`no_courses_view_slot`](./NoCoursesViewSlot/)
|
||||
58
src/plugin-slots/WidgetSidebarSlot/README.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Widget Sidebar Slot
|
||||
|
||||
### Slot ID: `widget_sidebar_slot`
|
||||
|
||||
## Description
|
||||
|
||||
This slot is used for adding content to the right-hand sidebar.
|
||||
|
||||
## Example
|
||||
|
||||
The space will show the `LookingForChallengeWidget` by default. This can be disabled in the configuration with the `keepDefault` boolean.
|
||||
|
||||

|
||||
|
||||
Setting the MFE's `env.config.jsx` to the following will replace the default experience with a custom sidebar component.
|
||||
|
||||

|
||||
|
||||
```js
|
||||
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
widget_sidebar_slot: {
|
||||
// Hide the default LookingForChallenge component
|
||||
keepDefault: false,
|
||||
plugins: [
|
||||
{
|
||||
op: PLUGIN_OPERATIONS.Insert,
|
||||
widget: {
|
||||
id: 'custom_sidebar_panel',
|
||||
type: DIRECT_PLUGIN,
|
||||
priority: 60,
|
||||
RenderWidget: () => (
|
||||
<div>
|
||||
<h3>
|
||||
Sidebar Menu
|
||||
</h3>
|
||||
<p>
|
||||
sidebar item #1
|
||||
</p>
|
||||
<p>
|
||||
sidebar item #2
|
||||
</p>
|
||||
<p>
|
||||
sidebar item #3
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default config;
|
||||
```
|
||||
@@ -0,0 +1,9 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`WidgetSidebar snapshots 1`] = `
|
||||
<PluginSlot
|
||||
id="widget_sidebar_slot"
|
||||
>
|
||||
<LookingForChallengeWidget />
|
||||
</PluginSlot>
|
||||
`;
|
||||
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 217 KiB |
13
src/plugin-slots/WidgetSidebarSlot/index.jsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
import LookingForChallengeWidget from 'widgets/LookingForChallengeWidget';
|
||||
|
||||
// eslint-disable-next-line arrow-body-style
|
||||
export const WidgetSidebarSlot = () => (
|
||||
<PluginSlot id="widget_sidebar_slot">
|
||||
<LookingForChallengeWidget />
|
||||
</PluginSlot>
|
||||
);
|
||||
|
||||
export default WidgetSidebarSlot;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import WidgetSidebar from '.';
|
||||
import WidgetSidebarSlot from '.';
|
||||
|
||||
jest.mock('widgets/LookingForChallengeWidget', () => 'LookingForChallengeWidget');
|
||||
|
||||
@@ -12,7 +12,7 @@ describe('WidgetSidebar', () => {
|
||||
beforeEach(() => jest.resetAllMocks());
|
||||
|
||||
test('snapshots', () => {
|
||||
const wrapper = shallow(<WidgetSidebar />);
|
||||
const wrapper = shallow(<WidgetSidebarSlot />);
|
||||
expect(wrapper.snapshot).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
within,
|
||||
prettyDOM,
|
||||
} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import {
|
||||
initialize,
|
||||
mergeConfig,
|
||||
@@ -42,7 +41,7 @@ jest.unmock('react-redux');
|
||||
jest.unmock('reselect');
|
||||
jest.unmock('hooks');
|
||||
|
||||
jest.mock('containers/WidgetContainers/WidgetSidebar', () => jest.fn(() => 'widget-sidebar'));
|
||||
jest.mock('plugin-slots/WidgetSidebarSlot', () => jest.fn(() => 'widget-sidebar'));
|
||||
jest.mock('components/NoticesWrapper', () => 'notices-wrapper');
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
|
||||
@@ -2,7 +2,6 @@ import { StrictDict } from 'utils';
|
||||
|
||||
export const categories = StrictDict({
|
||||
dashboard: 'dashboard',
|
||||
upgrade: 'upgrade',
|
||||
userEngagement: 'user-engagement',
|
||||
searchButton: 'search_button',
|
||||
credit: 'credit',
|
||||
@@ -14,9 +13,6 @@ export const events = StrictDict({
|
||||
courseImageClicked: 'courseImageClicked',
|
||||
courseTitleClicked: 'courseTitleClicked',
|
||||
courseOptionsDropdownClicked: 'courseOptionsDropdownClicked',
|
||||
upgradeButtonClicked: 'upgradeButtonClicked',
|
||||
upgradeButtonClickedEnrollment: 'upgradeButtonClickedEnrollment',
|
||||
upgradeButtonClickedUpsell: 'upgradeButtonClickedUpsell',
|
||||
shareClicked: 'shareClicked',
|
||||
userSettingsChanged: 'userSettingsChanged',
|
||||
newSession: 'newSession',
|
||||
@@ -36,9 +32,6 @@ export const eventNames = StrictDict({
|
||||
courseImageClicked: 'edx.bi.dashboard.course_image.clicked',
|
||||
courseTitleClicked: 'edx.bi.dashboard.course_title.clicked',
|
||||
courseOptionsDropdownClicked: 'edx.bi.dashboard.course_options_dropdown.clicked',
|
||||
upgradeButtonClicked: 'edx.bi.dashboard.upgrade_button.clicked',
|
||||
upgradeButtonClickedEnrollment: 'edx.course.enrollment.upgrade.clicked',
|
||||
upgradeButtonClickedUpsell: 'edx.bi.ecommerce.upsell_links_clicked',
|
||||
shareClicked: 'edx.course.share_clicked',
|
||||
userSettingsChanged: 'edx.user.settings.changed',
|
||||
newSession: 'course-dashboard.new-session',
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
import api from 'data/services/lms/api';
|
||||
import { createEventTracker, createLinkTracker } from 'data/services/segment/utils';
|
||||
import { categories, eventNames } from '../constants';
|
||||
import * as module from './course';
|
||||
|
||||
export const upsellOptions = {
|
||||
linkName: 'course_dashboard_green',
|
||||
linkType: 'button',
|
||||
pageName: 'course_dashboard',
|
||||
linkCategory: 'green_update',
|
||||
};
|
||||
|
||||
// Utils/Helpers
|
||||
/**
|
||||
* Generate a segement event tracker for a given course event.
|
||||
@@ -31,20 +23,6 @@ export const courseLinkTracker = (eventName) => (courseId, href) => (
|
||||
createLinkTracker(module.courseEventTracker(eventName, courseId), href)
|
||||
);
|
||||
|
||||
// Upgrade Events
|
||||
/**
|
||||
* There are currently multiple tracked api events for the upgrade event, with different targets.
|
||||
* Goal here is to split out the tracked events for easier testing.
|
||||
*/
|
||||
export const upgradeButtonClicked = (courseId) => createEventTracker(
|
||||
eventNames.upgradeButtonClicked,
|
||||
{ category: categories.upgrade, label: courseId },
|
||||
);
|
||||
export const upgradeButtonClickedUpsell = (courseId) => createEventTracker(
|
||||
eventNames.upgradeButtonClickedUpsell,
|
||||
{ ...upsellOptions, courseId },
|
||||
);
|
||||
|
||||
// Non-Link events
|
||||
export const courseOptionsDropdownClicked = (courseId) => (
|
||||
module.courseEventTracker(eventNames.courseOptionsDropdownClicked, courseId)
|
||||
@@ -57,19 +35,10 @@ export const courseTitleClicked = (...args) => (
|
||||
module.courseLinkTracker(eventNames.courseTitleClicked)(...args));
|
||||
export const enterCourseClicked = (...args) => (
|
||||
module.courseLinkTracker(eventNames.enterCourseClicked)(...args));
|
||||
export const upgradeClicked = (courseId, href) => createLinkTracker(
|
||||
() => {
|
||||
module.upgradeButtonClicked(courseId)();
|
||||
module.upgradeButtonClickedUpsell(courseId)();
|
||||
api.logUpgrade({ courseId });
|
||||
},
|
||||
href,
|
||||
);
|
||||
|
||||
export default {
|
||||
courseImageClicked,
|
||||
courseOptionsDropdownClicked,
|
||||
courseTitleClicked,
|
||||
enterCourseClicked,
|
||||
upgradeClicked,
|
||||
};
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import { keyStore } from 'utils';
|
||||
import api from 'data/services/lms/api';
|
||||
import { createEventTracker, createLinkTracker } from 'data/services/segment/utils';
|
||||
import { categories, eventNames } from '../constants';
|
||||
import * as trackers from './course';
|
||||
|
||||
jest.mock('data/services/lms/api', () => ({
|
||||
logUpgrade: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('data/services/segment/utils', () => ({
|
||||
createEventTracker: jest.fn(args => ({ createEventTracker: args })),
|
||||
createLinkTracker: jest.fn((cb, href) => ({ createLinkTracker: { cb, href } })),
|
||||
@@ -44,26 +39,6 @@ describe('course trackers', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('Upgrade Events', () => {
|
||||
describe('upgradeButtonClicked', () => {
|
||||
it('creates an event tracker for upgradeButtonClicked event with category and label', () => {
|
||||
expect(trackers.upgradeButtonClicked(courseId)).toEqual(createEventTracker(
|
||||
eventNames.upgradeButtonClicked,
|
||||
{ category: categories.upgrade, label: courseId },
|
||||
));
|
||||
});
|
||||
});
|
||||
describe('upgradeButtonClickedUpsell', () => {
|
||||
it('creates an event tracker for upgradeButtonClickedUpsell eventwith upsellOptions', () => {
|
||||
expect(trackers.upgradeButtonClickedUpsell(courseId)).toEqual(
|
||||
createEventTracker(
|
||||
eventNames.upgradeButtonClickedUpsell,
|
||||
{ ...trackers.upsellOptions, courseId },
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('Non-link events', () => {
|
||||
describe('courseOptionsDropdownClicked', () => {
|
||||
it('creates course event tracker for courseOptionsDropdownClicked event', () => {
|
||||
@@ -101,25 +76,5 @@ describe('course trackers', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('upgradeClicked', () => {
|
||||
it('triggers upgrade actions and api.logUpgrade with courseId', () => {
|
||||
const upgradeButtonClicked = jest.fn();
|
||||
const upgradeButtonClickedUpsell = jest.fn();
|
||||
const trackUpgradeButtonClicked = jest.fn(() => upgradeButtonClicked);
|
||||
const trackUpgradeButtonClickedUpsell = jest.fn(() => upgradeButtonClickedUpsell);
|
||||
jest.spyOn(trackers, moduleKeys.upgradeButtonClicked)
|
||||
.mockImplementationOnce(trackUpgradeButtonClicked);
|
||||
jest.spyOn(trackers, moduleKeys.upgradeButtonClickedUpsell)
|
||||
.mockImplementationOnce(trackUpgradeButtonClickedUpsell);
|
||||
const out = trackers.upgradeClicked(courseId, href).createLinkTracker;
|
||||
expect(out.href).toEqual(href);
|
||||
out.cb();
|
||||
expect(trackUpgradeButtonClicked).toHaveBeenCalledWith(courseId);
|
||||
expect(trackUpgradeButtonClickedUpsell).toHaveBeenCalledWith(courseId);
|
||||
expect(upgradeButtonClicked).toHaveBeenCalledWith();
|
||||
expect(upgradeButtonClickedUpsell).toHaveBeenCalledWith();
|
||||
expect(api.logUpgrade).toHaveBeenCalledWith({ courseId });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ import { reduxHooks } from 'hooks';
|
||||
import moreCoursesSVG from 'assets/more-courses-sidewidget.svg';
|
||||
import { baseAppUrl } from 'data/services/lms/urls';
|
||||
|
||||
import track from '../RecommendationsPanel/track';
|
||||
import { findCoursesWidgetClicked } from './track';
|
||||
import messages from './messages';
|
||||
import './index.scss';
|
||||
|
||||
@@ -17,6 +17,8 @@ export const arrowIcon = (<Icon className="mx-1" src={ArrowForward} />);
|
||||
export const LookingForChallengeWidget = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
|
||||
const hyperlinkDestination = baseAppUrl(courseSearchUrl) || '';
|
||||
|
||||
return (
|
||||
<Card orientation="horizontal" id="looking-for-challenge-widget">
|
||||
<Card.ImageCap
|
||||
@@ -30,8 +32,8 @@ export const LookingForChallengeWidget = () => {
|
||||
<h5>
|
||||
<Hyperlink
|
||||
variant="brand"
|
||||
destination={baseAppUrl(courseSearchUrl)}
|
||||
onClick={track.findCoursesWidgetClicked(baseAppUrl(courseSearchUrl))}
|
||||
destination={hyperlinkDestination}
|
||||
onClick={findCoursesWidgetClicked(hyperlinkDestination)}
|
||||
className="d-flex align-items-center"
|
||||
>
|
||||
{formatMessage(messages.findCoursesButton, { arrow: arrowIcon })}
|
||||
|
||||
@@ -10,7 +10,7 @@ jest.mock('hooks', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../RecommendationsPanel/track', () => ({
|
||||
jest.mock('./track', () => ({
|
||||
findCoursesWidgetClicked: (href) => jest.fn().mockName(`track.findCoursesWidgetClicked('${href}')`),
|
||||
}));
|
||||
|
||||
|
||||
15
src/widgets/LookingForChallengeWidget/track.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { StrictDict } from 'utils';
|
||||
import track from 'tracking';
|
||||
|
||||
export const linkNames = StrictDict({
|
||||
findCoursesWidget: 'learner_home_widget_explore',
|
||||
});
|
||||
|
||||
export const findCoursesWidgetClicked = (href) => track.findCourses.findCoursesClicked(href, {
|
||||
linkName: linkNames.findCoursesWidget,
|
||||
});
|
||||
|
||||
export default {
|
||||
linkNames,
|
||||
findCoursesWidgetClicked,
|
||||
};
|
||||
@@ -1,66 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@openedx/paragon';
|
||||
import { Search } from '@openedx/paragon/icons';
|
||||
import { baseAppUrl } from 'data/services/lms/urls';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import track from './track';
|
||||
import CourseCard from './components/CourseCard';
|
||||
import messages from './messages';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
export const LoadedView = ({
|
||||
courses,
|
||||
isControl,
|
||||
}) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
|
||||
|
||||
return (
|
||||
<div className="p-4 w-100 panel-background">
|
||||
<h3 className="pb-2">{isControl === false
|
||||
? formatMessage(messages.recommendationsHeading) : formatMessage(messages.popularCoursesHeading)}
|
||||
</h3>
|
||||
<div>
|
||||
{courses.map((course) => (
|
||||
<CourseCard
|
||||
key={course.courseKey}
|
||||
course={course}
|
||||
isControl={isControl}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-center explore-courses-btn">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
iconBefore={Search}
|
||||
as="a"
|
||||
href={baseAppUrl(courseSearchUrl)}
|
||||
onClick={track.findCoursesWidgetClicked(baseAppUrl(courseSearchUrl))}
|
||||
>
|
||||
{formatMessage(messages.exploreCoursesButton)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
LoadedView.defaultProps = {
|
||||
isControl: true,
|
||||
};
|
||||
|
||||
LoadedView.propTypes = {
|
||||
courses: PropTypes.arrayOf(PropTypes.shape({
|
||||
courseKey: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
logoImageUrl: PropTypes.string,
|
||||
marketingUrl: PropTypes.string,
|
||||
})).isRequired,
|
||||
isControl: PropTypes.oneOf([true, false, null]),
|
||||
};
|
||||
|
||||
export default LoadedView;
|
||||
@@ -1,42 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import LoadedView from './LoadedView';
|
||||
import mockData from './mockData';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
usePlatformSettingsData: () => ({
|
||||
courseSearchUrl: '/course-search-url',
|
||||
}),
|
||||
},
|
||||
}));
|
||||
jest.mock('data/services/lms/urls', () => ({
|
||||
baseAppUrl: (url) => (`http://localhost:18000${url}`),
|
||||
}));
|
||||
jest.mock('./track', () => ({
|
||||
findCoursesWidgetClicked: (href) => jest.fn().mockName(`track.findCoursesWidgetClicked('${href}')`),
|
||||
}));
|
||||
jest.mock('./components/CourseCard', () => 'CourseCard');
|
||||
|
||||
describe('RecommendationsPanel LoadedView', () => {
|
||||
const props = {
|
||||
courses: mockData.courses,
|
||||
isControl: null,
|
||||
};
|
||||
|
||||
describe('RecommendationPanelLoadedView', () => {
|
||||
test('without personalize recommendation', () => {
|
||||
const el = shallow(<LoadedView {...props} />);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
expect(el.instance.findByType('h3')[0].children[0].el).toEqual(messages.popularCoursesHeading.defaultMessage);
|
||||
});
|
||||
|
||||
test('with personalize recommendation', () => {
|
||||
const el = shallow(<LoadedView {...props} isControl={false} />);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
expect(el.instance.findByType('h3')[0].children[0].el).toEqual(messages.recommendationsHeading.defaultMessage);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Spinner } from '@openedx/paragon';
|
||||
|
||||
import { useDashboardMessages } from 'containers/Dashboard/hooks';
|
||||
|
||||
export const LoadingView = () => {
|
||||
const { spinnerScreenReaderText } = useDashboardMessages();
|
||||
return (
|
||||
<div className="recommendations-loading w-100">
|
||||
<Spinner
|
||||
animation="border"
|
||||
variant="light"
|
||||
screenReaderText={spinnerScreenReaderText}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoadingView;
|
||||
@@ -1,19 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { useDashboardMessages } from 'containers/Dashboard/hooks';
|
||||
import LoadingView from './LoadingView';
|
||||
|
||||
jest.mock('./components/CourseCard', () => 'CourseCard');
|
||||
jest.mock('containers/Dashboard/hooks', () => ({
|
||||
useDashboardMessages: jest.fn(),
|
||||
}));
|
||||
|
||||
const spinnerScreenReaderText = 'test-spinner-screen-reader-text';
|
||||
useDashboardMessages.mockReturnValue(spinnerScreenReaderText);
|
||||
|
||||
describe('RecommendationsPanel LoadingView', () => {
|
||||
test('snapshot', () => {
|
||||
expect(shallow(<LoadingView />).snapshot).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,151 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`RecommendationsPanel LoadedView RecommendationPanelLoadedView with personalize recommendation 1`] = `
|
||||
<div
|
||||
className="p-4 w-100 panel-background"
|
||||
>
|
||||
<h3
|
||||
className="pb-2"
|
||||
>
|
||||
Recommendations for you
|
||||
</h3>
|
||||
<div>
|
||||
<CourseCard
|
||||
course={
|
||||
{
|
||||
"courseKey": "cs-1",
|
||||
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
|
||||
"marketingUrl": "www.edx/recommended-course",
|
||||
"title": "Recommended course 1",
|
||||
}
|
||||
}
|
||||
isControl={false}
|
||||
key="cs-1"
|
||||
/>
|
||||
<CourseCard
|
||||
course={
|
||||
{
|
||||
"courseKey": "cs-2",
|
||||
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
|
||||
"marketingUrl": "www.edx/recommended-course",
|
||||
"title": "Recommended course 2 with a really really really long name for some reason",
|
||||
}
|
||||
}
|
||||
isControl={false}
|
||||
key="cs-2"
|
||||
/>
|
||||
<CourseCard
|
||||
course={
|
||||
{
|
||||
"courseKey": "cs-3",
|
||||
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
|
||||
"marketingUrl": "www.edx/recommended-course",
|
||||
"title": "Recommended course 3",
|
||||
}
|
||||
}
|
||||
isControl={false}
|
||||
key="cs-3"
|
||||
/>
|
||||
<CourseCard
|
||||
course={
|
||||
{
|
||||
"courseKey": "cs-4",
|
||||
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
|
||||
"marketingUrl": "www.edx/recommended-course",
|
||||
"title": "Recommended course 4",
|
||||
}
|
||||
}
|
||||
isControl={false}
|
||||
key="cs-4"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="text-center explore-courses-btn"
|
||||
>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/course-search-url"
|
||||
iconBefore={[MockFunction icons.Search]}
|
||||
onClick={[MockFunction track.findCoursesWidgetClicked('http://localhost:18000/course-search-url')]}
|
||||
variant="tertiary"
|
||||
>
|
||||
Explore courses
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`RecommendationsPanel LoadedView RecommendationPanelLoadedView without personalize recommendation 1`] = `
|
||||
<div
|
||||
className="p-4 w-100 panel-background"
|
||||
>
|
||||
<h3
|
||||
className="pb-2"
|
||||
>
|
||||
Popular courses
|
||||
</h3>
|
||||
<div>
|
||||
<CourseCard
|
||||
course={
|
||||
{
|
||||
"courseKey": "cs-1",
|
||||
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
|
||||
"marketingUrl": "www.edx/recommended-course",
|
||||
"title": "Recommended course 1",
|
||||
}
|
||||
}
|
||||
isControl={null}
|
||||
key="cs-1"
|
||||
/>
|
||||
<CourseCard
|
||||
course={
|
||||
{
|
||||
"courseKey": "cs-2",
|
||||
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
|
||||
"marketingUrl": "www.edx/recommended-course",
|
||||
"title": "Recommended course 2 with a really really really long name for some reason",
|
||||
}
|
||||
}
|
||||
isControl={null}
|
||||
key="cs-2"
|
||||
/>
|
||||
<CourseCard
|
||||
course={
|
||||
{
|
||||
"courseKey": "cs-3",
|
||||
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
|
||||
"marketingUrl": "www.edx/recommended-course",
|
||||
"title": "Recommended course 3",
|
||||
}
|
||||
}
|
||||
isControl={null}
|
||||
key="cs-3"
|
||||
/>
|
||||
<CourseCard
|
||||
course={
|
||||
{
|
||||
"courseKey": "cs-4",
|
||||
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
|
||||
"marketingUrl": "www.edx/recommended-course",
|
||||
"title": "Recommended course 4",
|
||||
}
|
||||
}
|
||||
isControl={null}
|
||||
key="cs-4"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="text-center explore-courses-btn"
|
||||
>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/course-search-url"
|
||||
iconBefore={[MockFunction icons.Search]}
|
||||
onClick={[MockFunction track.findCoursesWidgetClicked('http://localhost:18000/course-search-url')]}
|
||||
variant="tertiary"
|
||||
>
|
||||
Explore courses
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,12 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`RecommendationsPanel LoadingView snapshot 1`] = `
|
||||
<div
|
||||
className="recommendations-loading w-100"
|
||||
>
|
||||
<Spinner
|
||||
animation="border"
|
||||
variant="light"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,12 +0,0 @@
|
||||
import { StrictDict } from 'utils';
|
||||
import { get, stringifyUrl } from 'data/services/lms/utils';
|
||||
import urls from 'data/services/lms/urls';
|
||||
|
||||
export const getFetchUrl = () => (`${urls.getApiUrl()}/edx_recommendations/learner_dashboard/course_recommendations/`);
|
||||
export const apiKeys = StrictDict({ user: 'user' });
|
||||
|
||||
const fetchRecommendedCourses = () => get(stringifyUrl(getFetchUrl()));
|
||||
|
||||
export default {
|
||||
fetchRecommendedCourses,
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
import { get, stringifyUrl } from 'data/services/lms/utils';
|
||||
import api, { getFetchUrl } from './api';
|
||||
|
||||
jest.mock('data/services/lms/utils', () => ({
|
||||
stringifyUrl: (...args) => ({ stringifyUrl: args }),
|
||||
get: (...args) => ({ get: args }),
|
||||
}));
|
||||
|
||||
describe('recommendedCourses api', () => {
|
||||
describe('fetchRecommendedCourses', () => {
|
||||
it('calls get with the correct recommendation courses URL and user', () => {
|
||||
expect(api.fetchRecommendedCourses()).toEqual(
|
||||
get(stringifyUrl(getFetchUrl())),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,51 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Card, Hyperlink, Truncate } from '@openedx/paragon';
|
||||
|
||||
import { useIsCollapsed } from 'containers/CourseCard/hooks';
|
||||
import useCourseCardData from './hooks';
|
||||
import './index.scss';
|
||||
|
||||
export const CourseCard = ({ course, isControl }) => {
|
||||
const isCollapsed = useIsCollapsed();
|
||||
const { handleCourseClick } = useCourseCardData(course, isControl);
|
||||
|
||||
return (
|
||||
<Hyperlink
|
||||
destination={course?.marketingUrl}
|
||||
className="card-link"
|
||||
onClick={handleCourseClick}
|
||||
>
|
||||
<Card orientation={isCollapsed ? 'vertical' : 'horizontal'} className="p-3 mb-1 recommended-course-card">
|
||||
<div className={isCollapsed ? '' : 'd-flex align-items-center'}>
|
||||
<Card.ImageCap
|
||||
src={course.logoImageUrl}
|
||||
srcAlt={course.title}
|
||||
/>
|
||||
<Card.Body className="d-flex align-items-center">
|
||||
<Card.Section className={isCollapsed ? 'pt-3' : 'pl-3'}>
|
||||
<h4 className="text-info-500">
|
||||
<Truncate lines={3}>
|
||||
{course.title}
|
||||
</Truncate>
|
||||
</h4>
|
||||
</Card.Section>
|
||||
</Card.Body>
|
||||
</div>
|
||||
</Card>
|
||||
</Hyperlink>
|
||||
);
|
||||
};
|
||||
|
||||
CourseCard.propTypes = {
|
||||
course: PropTypes.shape({
|
||||
courseKey: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
logoImageUrl: PropTypes.string,
|
||||
marketingUrl: PropTypes.string,
|
||||
}).isRequired,
|
||||
isControl: PropTypes.oneOf([true, false, null]).isRequired,
|
||||
};
|
||||
|
||||
export default CourseCard;
|
||||
@@ -1,17 +0,0 @@
|
||||
import track from '../track';
|
||||
import './index.scss';
|
||||
|
||||
export const useCourseCardData = (course, isControl) => {
|
||||
const handleCourseClick = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
track.recommendedCourseClicked(
|
||||
course.courseKey,
|
||||
isControl,
|
||||
course?.marketingUrl,
|
||||
)(e);
|
||||
};
|
||||
return { handleCourseClick };
|
||||
};
|
||||
|
||||
export default useCourseCardData;
|
||||
@@ -1,33 +0,0 @@
|
||||
@import "@openedx/paragon/scss/core/core";
|
||||
|
||||
.card-link{
|
||||
display: block !important;
|
||||
margin: 0.5rem 0 0.5rem 0 !important;
|
||||
}
|
||||
|
||||
.recommended-course-card {
|
||||
margin: 0.5rem 0 0.5rem 0 !important;
|
||||
|
||||
.pgn__card-wrapper-image-cap {
|
||||
width: 7.188rem !important;
|
||||
max-width: 7.188rem !important;
|
||||
min-width: 7.188rem !important;
|
||||
height: 4.125rem !important;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15), 0 1px 4px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 4px;
|
||||
padding: 0.5rem;
|
||||
|
||||
.pgn__card-image-cap {
|
||||
max-width: 100% !important;
|
||||
max-height: 100% !important;
|
||||
}
|
||||
}
|
||||
.pgn__card-section {
|
||||
padding: 0 !important;
|
||||
}
|
||||
margin-top: 0.313rem;
|
||||
}
|
||||
|
||||
.text-info-500 {
|
||||
margin: 0 !important;
|
||||
}
|
||||