Compare commits
39 Commits
release/te
...
v1.0.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f4bf0a13e | ||
|
|
6bed0308bd | ||
|
|
057b925589 | ||
|
|
6202f7bb54 | ||
|
|
d0c27f4377 | ||
|
|
b2c6ec2dc9 | ||
|
|
8175d7e2f6 | ||
|
|
d0051d0a7d | ||
|
|
c621d581bd | ||
|
|
268ccc864d | ||
|
|
2045854099 | ||
|
|
9b439d7d74 | ||
|
|
b8f4d49a55 | ||
|
|
1d29810f6c | ||
|
|
89559a4987 | ||
|
|
c4205e9400 | ||
|
|
086c0638f1 | ||
|
|
9ef5840700 | ||
|
|
3c8c92ab92 | ||
|
|
7488fe55f0 | ||
|
|
fe6c726306 | ||
|
|
8c3d62c12b | ||
|
|
66794acf17 | ||
|
|
2f3f3bcd8b | ||
|
|
dcab4f1b75 | ||
|
|
66fdd79bdf | ||
|
|
0ea9f6d193 | ||
|
|
fee6a26366 | ||
|
|
1301fddbc9 | ||
|
|
5dcd596ac9 | ||
|
|
a5522faa42 | ||
|
|
3542c38472 | ||
|
|
14c03d8461 | ||
|
|
5562d8a339 | ||
|
|
a9194261c8 | ||
|
|
11a7512fea | ||
|
|
be620a80fa | ||
|
|
1d9eb08e59 | ||
|
|
05e9626e57 |
@@ -1,10 +0,0 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
README.md
|
||||
LICENSE
|
||||
.babelrc
|
||||
.eslintignore
|
||||
.eslintrc.json
|
||||
.gitignore
|
||||
.npmignore
|
||||
commitlint.config.js
|
||||
43
.env
43
.env
@@ -1,43 +0,0 @@
|
||||
NODE_ENV='production'
|
||||
NODE_PATH=./src
|
||||
BASE_URL=''
|
||||
LMS_BASE_URL=''
|
||||
ECOMMERCE_BASE_URL=''
|
||||
LOGIN_URL=''
|
||||
LOGOUT_URL=''
|
||||
CSRF_TOKEN_API_PATH=''
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT=''
|
||||
DATA_API_BASE_URL=''
|
||||
SEGMENT_KEY=''
|
||||
FEATURE_FLAGS={}
|
||||
ACCESS_TOKEN_COOKIE_NAME=''
|
||||
NEW_RELIC_APP_ID=''
|
||||
NEW_RELIC_LICENSE_KEY=''
|
||||
SITE_NAME=''
|
||||
MARKETING_SITE_BASE_URL=''
|
||||
SUPPORT_URL=''
|
||||
CONTACT_URL=''
|
||||
OPEN_SOURCE_URL=''
|
||||
TERMS_OF_SERVICE_URL=''
|
||||
PRIVACY_POLICY_URL=''
|
||||
FACEBOOK_URL=''
|
||||
TWITTER_URL=''
|
||||
YOU_TUBE_URL=''
|
||||
LINKED_IN_URL=''
|
||||
REDDIT_URL=''
|
||||
APPLE_APP_STORE_URL=''
|
||||
GOOGLE_PLAY_URL=''
|
||||
ENTERPRISE_MARKETING_URL=''
|
||||
ENTERPRISE_MARKETING_UTM_SOURCE=''
|
||||
ENTERPRISE_MARKETING_UTM_CAMPAIGN=''
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=''
|
||||
LEARNING_BASE_URL=''
|
||||
HOTJAR_APP_ID=''
|
||||
HOTJAR_VERSION='6'
|
||||
HOTJAR_DEBUG=''
|
||||
ACCOUNT_SETTINGS_URL=''
|
||||
ACCOUNT_PROFILE_URL=''
|
||||
ENABLE_NOTICES=''
|
||||
CAREER_LINK_URL=''
|
||||
ENABLE_EDX_PERSONAL_DASHBOARD=false
|
||||
ENABLE_PROGRAMS=false
|
||||
@@ -1,49 +0,0 @@
|
||||
NODE_ENV='development'
|
||||
PORT=1996
|
||||
BASE_URL='localhost:1996'
|
||||
LMS_BASE_URL='http://localhost:18000'
|
||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||
LOGIN_URL='http://localhost:18000/login'
|
||||
LOGOUT_URL='http://localhost:18000/logout'
|
||||
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
|
||||
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
|
||||
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
|
||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
SITE_NAME=localhost
|
||||
DATA_API_BASE_URL='http://localhost:8000'
|
||||
// LMS_CLIENT_ID should match the lms DOT client application id your LMS containe
|
||||
LMS_CLIENT_ID='login-service-client-id'
|
||||
SEGMENT_KEY=''
|
||||
FEATURE_FLAGS={}
|
||||
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
||||
SUPPORT_URL=''
|
||||
CONTACT_URL='http://localhost:18000/contact'
|
||||
OPEN_SOURCE_URL='http://localhost:18000/openedx'
|
||||
TERMS_OF_SERVICE_URL='http://localhost:18000/terms-of-service'
|
||||
PRIVACY_POLICY_URL='http://localhost:18000/privacy-policy'
|
||||
FACEBOOK_URL='https://www.facebook.com'
|
||||
TWITTER_URL='https://twitter.com'
|
||||
YOU_TUBE_URL='https://www.youtube.com'
|
||||
LINKED_IN_URL='https://www.linkedin.com'
|
||||
REDDIT_URL='https://www.reddit.com'
|
||||
APPLE_APP_STORE_URL='https://www.apple.com/ios/app-store/'
|
||||
GOOGLE_PLAY_URL='https://play.google.com/store'
|
||||
ENTERPRISE_MARKETING_URL='http://example.com'
|
||||
ENTERPRISE_MARKETING_UTM_SOURCE='example.com'
|
||||
ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
|
||||
LEARNING_BASE_URL='http://localhost:2000'
|
||||
SESSION_COOKIE_DOMAIN='localhost'
|
||||
HOTJAR_APP_ID=''
|
||||
HOTJAR_VERSION='6'
|
||||
HOTJAR_DEBUG=''
|
||||
ACCOUNT_SETTINGS_URL='http://localhost:1997'
|
||||
ACCOUNT_PROFILE_URL='http://localhost:1995'
|
||||
ENABLE_NOTICES=''
|
||||
CAREER_LINK_URL=''
|
||||
ENABLE_EDX_PERSONAL_DASHBOARD=false
|
||||
ENABLE_PROGRAMS=false
|
||||
48
.env.test
48
.env.test
@@ -1,48 +0,0 @@
|
||||
NODE_ENV='test'
|
||||
PORT=1996
|
||||
BASE_URL='localhost:1996'
|
||||
LMS_BASE_URL='http://localhost:18000'
|
||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||
LOGIN_URL='http://localhost:18000/login'
|
||||
LOGOUT_URL='http://localhost:18000/logout'
|
||||
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
|
||||
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
|
||||
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
|
||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
SITE_NAME=localhost
|
||||
DATA_API_BASE_URL='http://localhost:8000'
|
||||
// LMS_CLIENT_ID should match the lms DOT client application id your LMS containe
|
||||
LMS_CLIENT_ID='login-service-client-id'
|
||||
SEGMENT_KEY=''
|
||||
FEATURE_FLAGS={}
|
||||
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
||||
SUPPORT_URL=''
|
||||
CONTACT_URL='http://localhost:18000/contact'
|
||||
OPEN_SOURCE_URL='http://localhost:18000/openedx'
|
||||
TERMS_OF_SERVICE_URL='http://localhost:18000/terms-of-service'
|
||||
PRIVACY_POLICY_URL='http://localhost:18000/privacy-policy'
|
||||
FACEBOOK_URL='https://www.facebook.com'
|
||||
TWITTER_URL='https://twitter.com'
|
||||
YOU_TUBE_URL='https://www.youtube.com'
|
||||
LINKED_IN_URL='https://www.linkedin.com'
|
||||
REDDIT_URL='https://www.reddit.com'
|
||||
APPLE_APP_STORE_URL='https://www.apple.com/ios/app-store/'
|
||||
GOOGLE_PLAY_URL='https://play.google.com/store'
|
||||
ENTERPRISE_MARKETING_URL='http://example.com'
|
||||
ENTERPRISE_MARKETING_UTM_SOURCE='example.com'
|
||||
ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
|
||||
LEARNING_BASE_URL='http://localhost:2000'
|
||||
HOTJAR_APP_ID='hot-jar-app-id'
|
||||
HOTJAR_VERSION='6'
|
||||
HOTJAR_DEBUG=''
|
||||
ACCOUNT_SETTINGS_URL='http://account-settings-url.test'
|
||||
ACCOUNT_PROFILE_URL='http://account-profile-url.test'
|
||||
ENABLE_NOTICES=''
|
||||
CAREER_LINK_URL=''
|
||||
ENABLE_EDX_PERSONAL_DASHBOARD=true
|
||||
ENABLE_PROGRAMS=false
|
||||
@@ -1,5 +0,0 @@
|
||||
coverage/*
|
||||
dist/
|
||||
node_modules/
|
||||
src/postcss.config.js
|
||||
src/segment.js
|
||||
22
.eslintrc.js
22
.eslintrc.js
@@ -1,22 +0,0 @@
|
||||
const { createConfig } = require('@openedx/frontend-build');
|
||||
|
||||
const config = createConfig('eslint', {
|
||||
rules: {
|
||||
'import/no-named-as-default': 'off',
|
||||
'import/no-named-as-default-member': 'off',
|
||||
'import/no-self-import': 'off',
|
||||
'import/no-import-module-exports': 'off',
|
||||
'spaced-comment': ['error', 'always', { 'block': { 'exceptions': ['*'] } }],
|
||||
},
|
||||
});
|
||||
|
||||
config.settings = {
|
||||
"import/resolver": {
|
||||
node: {
|
||||
paths: ["src", "node_modules"],
|
||||
extensions: [".js", ".jsx"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1 +0,0 @@
|
||||
*.snap linguist-generated=false
|
||||
32
.gitignore
vendored
32
.gitignore
vendored
@@ -1,29 +1,15 @@
|
||||
.DS_Store
|
||||
.eslintcache
|
||||
env.config.*
|
||||
node_modules
|
||||
npm-debug.log
|
||||
coverage
|
||||
|
||||
dist/
|
||||
public/samples/
|
||||
|
||||
### pyenv ###
|
||||
.python-version
|
||||
|
||||
### Emacs ###
|
||||
*~
|
||||
*.swo
|
||||
*.swp
|
||||
|
||||
### Development environments ###
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
# Local package dependencies
|
||||
module.config.js
|
||||
dist/
|
||||
/*.tgz
|
||||
|
||||
### transifex ###
|
||||
### i18n ###
|
||||
src/i18n/transifex_input.json
|
||||
temp
|
||||
src/i18n/messages
|
||||
|
||||
### Editors ###
|
||||
.DS_Store
|
||||
*~
|
||||
/temp
|
||||
/.vscode
|
||||
|
||||
16
.npmignore
16
.npmignore
@@ -1,12 +1,6 @@
|
||||
.eslintignore
|
||||
.eslintrc.json
|
||||
.gitignore
|
||||
docker-compose.yml
|
||||
Dockerfile
|
||||
Makefile
|
||||
npm-debug.log
|
||||
|
||||
config
|
||||
coverage
|
||||
__mocks__
|
||||
node_modules
|
||||
public
|
||||
*.test.js
|
||||
*.test.jsx
|
||||
*.test.ts
|
||||
*.test.tsx
|
||||
|
||||
5
Makefile
5
Makefile
@@ -45,12 +45,11 @@ pull_translations:
|
||||
mkdir src/i18n/messages
|
||||
cd src/i18n/messages \
|
||||
&& atlas pull $(ATLAS_OPTIONS) \
|
||||
translations/frontend-platform/src/i18n/messages:frontend-platform \
|
||||
translations/frontend-base/src/i18n/messages:frontend-base \
|
||||
translations/paragon/src/i18n/messages:paragon \
|
||||
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
|
||||
translations/frontend-app-learner-dashboard/src/i18n/messages:frontend-app-learner-dashboard
|
||||
|
||||
$(intl_imports) frontend-platform paragon frontend-component-footer frontend-app-learner-dashboard
|
||||
$(intl_imports) frontend-base paragon frontend-app-learner-dashboard
|
||||
|
||||
# This target is used by CI.
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
|
||||
29
README.rst
29
README.rst
@@ -18,7 +18,7 @@ frontend-app-learner-dashboard
|
||||
The Learner Home app is a microfrontend (MFE) course listing experience for the Open edX Learning Management System
|
||||
(LMS). This experience was designed to provide a clean and functional interface to allow learners to view all of their
|
||||
open enrollments, as well as take relevant actions on those enrollments. It also serves as host to a number of exposed
|
||||
"widget" containers to provide upsell and discovery widgets as sidebar/footer components.
|
||||
"widget" containers to provide upsell and discovery widgets as sidebar components.
|
||||
|
||||
Quickstart
|
||||
----------
|
||||
@@ -30,31 +30,10 @@ To start the MFE and enable the feature in LMS:
|
||||
From there, simply load the configured address/port. You should be prompted to log into your LMS if you are not
|
||||
already, and then redirected to your home page.
|
||||
|
||||
Plugins
|
||||
Widgets
|
||||
-------
|
||||
This MFE can be customized using `Frontend Plugin Framework <https://github.com/openedx/frontend-plugin-framework>`_.
|
||||
|
||||
The parts of this MFE that can be customized in that manner are documented `here </src/plugin-slots>`_.
|
||||
|
||||
Contributing
|
||||
------------
|
||||
|
||||
A core goal of this app is to provide a clean experimentation interface. To promote this end, we have provided a
|
||||
silo'ed code directory at ``src/widgets`` in which contributors should add their custom widget components. In order to
|
||||
ensure our ability to maintain the code stability of the app, the code for these widgets should be strictly contained
|
||||
within the bounds of that directory.
|
||||
|
||||
Once written, the widgets can be configured into one of our widget containers at ``src/containers/WidgetContainers``.
|
||||
This can include conditional logic, as well as Optimizely triggers. It is important to note that our integration tests
|
||||
will isolate and ignore these containers, and thus testing your widget is the response of the creator/maintainer of the
|
||||
widget itself.
|
||||
|
||||
Some guidelines for writing widgets:
|
||||
|
||||
* Code for the widget should be strictly confined to the ``src/widgets`` directory.
|
||||
* You can load data from the redux store, but should not add or modify fields in that structure.
|
||||
* Network events should be managed in component hooks, though can use our ``data/constants/requests:requestStates`` for
|
||||
ease of tracking the request states.
|
||||
This MFE can be customized with widgets. The parts of this MFE that can be customized in that manner are documented
|
||||
`here </src/slots>`_.
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
10
app.d.ts
vendored
Normal file
10
app.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
/// <reference types="@openedx/frontend-base" />
|
||||
|
||||
declare module 'site.config' {
|
||||
export default SiteConfig;
|
||||
}
|
||||
|
||||
declare module '*.svg' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
3
babel.config.js
Normal file
3
babel.config.js
Normal file
@@ -0,0 +1,3 @@
|
||||
const { createConfig } = require('@openedx/frontend-base/config');
|
||||
|
||||
module.exports = createConfig('babel');
|
||||
22
eslint.config.js
Normal file
22
eslint.config.js
Normal file
@@ -0,0 +1,22 @@
|
||||
// @ts-check
|
||||
|
||||
const { createLintConfig } = require('@openedx/frontend-base/config');
|
||||
|
||||
module.exports = createLintConfig(
|
||||
{
|
||||
files: [
|
||||
'src/**/*',
|
||||
'site.config.*',
|
||||
],
|
||||
},
|
||||
{
|
||||
ignores: [
|
||||
'coverage/*',
|
||||
'dist/*',
|
||||
'documentation/*',
|
||||
'node_modules/*',
|
||||
'**/__mocks__/*',
|
||||
'**/__snapshots__/*',
|
||||
],
|
||||
},
|
||||
);
|
||||
@@ -1,72 +0,0 @@
|
||||
/*
|
||||
Learner Dashboard is now able to handle JS-based configuration!
|
||||
|
||||
For the time being, the `.env.*` files are still made available when cloning down this repo or pulling from
|
||||
the master branch. To switch to using `env.config.js`, make a copy of `example.env.config.js` and configure as needed.
|
||||
|
||||
For testing with Jest Snapshot, there is a mock in `/src/setupTest.jsx` for `getConfig` that will need to be
|
||||
uncommented.
|
||||
|
||||
Note: having both .env and env.config.js files will follow a predictable order, in which non-empty values in the
|
||||
JS-based config will overwrite the .env environment variables.
|
||||
|
||||
frontend-platform's getConfig loads configuration in the following sequence:
|
||||
- .env file config
|
||||
- optional handlers (commonly used to merge MFE-specific config in via additional process.env variables)
|
||||
- env.config.js file config
|
||||
- runtime config
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
NODE_ENV: 'development',
|
||||
NODE_PATH: './src',
|
||||
PORT: 1996,
|
||||
BASE_URL: 'localhost:1996',
|
||||
LMS_BASE_URL: 'http://localhost:18000',
|
||||
ECOMMERCE_BASE_URL: 'http://localhost:18130',
|
||||
LOGIN_URL: 'http://localhost:18000/login',
|
||||
LOGOUT_URL: 'http://localhost:18000/logout',
|
||||
LOGO_URL: 'https://edx-cdn.org/v3/default/logo.svg',
|
||||
LOGO_TRADEMARK_URL: 'https://edx-cdn.org/v3/default/logo-trademark.svg',
|
||||
LOGO_WHITE_URL: 'https://edx-cdn.org/v3/default/logo-white.svg',
|
||||
FAVICON_URL: 'https://edx-cdn.org/v3/default/favicon.ico',
|
||||
CSRF_TOKEN_API_PATH: '/csrf/api/v1/token',
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT: 'http://localhost:18000/login_refresh',
|
||||
ACCESS_TOKEN_COOKIE_NAME: 'edx-jwt-cookie-header-payload',
|
||||
USER_INFO_COOKIE_NAME: 'edx-user-info',
|
||||
SITE_NAME: 'localhost',
|
||||
DATA_API_BASE_URL: 'http://localhost:8000',
|
||||
// LMS_CLIENT_ID should match the lms DOT client application in your LMS container
|
||||
LMS_CLIENT_ID: 'login-service-client-id',
|
||||
SEGMENT_KEY: '',
|
||||
FEATURE_FLAGS: {},
|
||||
MARKETING_SITE_BASE_URL: 'http://localhost:18000',
|
||||
SUPPORT_URL: 'http://localhost:18000/support',
|
||||
CONTACT_URL: 'http://localhost:18000/contact',
|
||||
OPEN_SOURCE_URL: 'http://localhost:18000/openedx',
|
||||
TERMS_OF_SERVICE_URL: 'http://localhost:18000/terms-of-service',
|
||||
PRIVACY_POLICY_URL: 'http://localhost:18000/privacy-policy',
|
||||
FACEBOOK_URL: 'https://www.facebook.com',
|
||||
TWITTER_URL: 'https://twitter.com',
|
||||
YOU_TUBE_URL: 'https://www.youtube.com',
|
||||
LINKED_IN_URL: 'https://www.linkedin.com',
|
||||
REDDIT_URL: 'https://www.reddit.com',
|
||||
APPLE_APP_STORE_URL: 'https://www.apple.com/ios/app-store/',
|
||||
GOOGLE_PLAY_URL: 'https://play.google.com/store',
|
||||
ENTERPRISE_MARKETING_URL: 'http://example.com',
|
||||
ENTERPRISE_MARKETING_UTM_SOURCE: 'example.com',
|
||||
ENTERPRISE_MARKETING_UTM_CAMPAIGN: 'example.com Referral',
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM: 'Footer',
|
||||
LEARNING_BASE_URL: 'http://localhost:2000',
|
||||
SESSION_COOKIE_DOMAIN: 'localhost',
|
||||
HOTJAR_APP_ID: '',
|
||||
HOTJAR_VERSION: 6,
|
||||
HOTJAR_DEBUG: '',
|
||||
NEW_RELIC_APP_ID: '',
|
||||
NEW_RELIC_LICENSE_KEY: '',
|
||||
ACCOUNT_SETTINGS_URL: 'http://localhost:1997',
|
||||
ACCOUNT_PROFILE_URL: 'http://localhost:1995',
|
||||
ENABLE_NOTICES: '',
|
||||
CAREER_LINK_URL: '',
|
||||
EXPERIMENT_08_23_VAN_PAINTED_DOOR: true,
|
||||
};
|
||||
@@ -1,18 +1,21 @@
|
||||
const { createConfig } = require('@openedx/frontend-build');
|
||||
const { createConfig } = require('@openedx/frontend-base/config');
|
||||
|
||||
module.exports = createConfig('jest', {
|
||||
module.exports = createConfig('test', {
|
||||
setupFilesAfterEnv: [
|
||||
'jest-expect-message',
|
||||
'<rootDir>/src/setupTest.jsx',
|
||||
],
|
||||
modulePaths: ['<rootDir>/src/'],
|
||||
coveragePathIgnorePatterns: [
|
||||
'src/segment.js',
|
||||
'src/postcss.config.js',
|
||||
'testUtils', // don't unit test jest mocking tools
|
||||
'src/data/services/lms/fakeData', // don't unit test mock data
|
||||
'src/test', // don't unit test integration test utils
|
||||
'src/__mocks__',
|
||||
],
|
||||
moduleNameMapper: {
|
||||
'\\.svg$': '<rootDir>/src/__mocks__/svg.js',
|
||||
'\\.(jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/src/__mocks__/file.js',
|
||||
},
|
||||
testTimeout: 120000,
|
||||
testEnvironment: 'jsdom',
|
||||
});
|
||||
|
||||
10292
package-lock.json
generated
10292
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
83
package.json
Executable file → Normal file
83
package.json
Executable file → Normal file
@@ -1,80 +1,64 @@
|
||||
{
|
||||
"name": "@edx/frontend-app-learner-dashboard",
|
||||
"version": "0.0.1",
|
||||
"name": "@openedx/frontend-app-learner-dashboard",
|
||||
"version": "1.0.0-alpha.1",
|
||||
"description": "",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/edx/frontend-app-learner-dashboard.git"
|
||||
},
|
||||
"main": "src/index.ts",
|
||||
"files": [
|
||||
"/src"
|
||||
],
|
||||
"browserslist": [
|
||||
"extends @edx/browserslist-config"
|
||||
],
|
||||
"sideEffects": [
|
||||
"*.css",
|
||||
"*.scss"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "fedx-scripts webpack",
|
||||
"i18n_extract": "fedx-scripts formatjs extract",
|
||||
"lint": "fedx-scripts eslint --ext .jsx,.js src/",
|
||||
"lint-fix": "fedx-scripts eslint --fix --ext .jsx,.js src/",
|
||||
"semantic-release": "semantic-release",
|
||||
"start": "fedx-scripts webpack-dev-server --progress",
|
||||
"dev": "PUBLIC_PATH=/learner-dashboard/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
|
||||
"test": "TZ=GMT fedx-scripts jest --coverage --passWithNoTests",
|
||||
"quality": "npm run lint-fix && npm run test",
|
||||
"watch-tests": "jest --watch",
|
||||
"snapshot": "fedx-scripts jest --updateSnapshot"
|
||||
"dev": "PORT=1996 PUBLIC_PATH=/learner-dashboard openedx dev",
|
||||
"i18n_extract": "openedx formatjs extract",
|
||||
"lint": "openedx lint .",
|
||||
"lint:fix": "openedx lint --fix .",
|
||||
"snapshot": "openedx test --updateSnapshot",
|
||||
"test": "openedx test --coverage --passWithNoTests"
|
||||
},
|
||||
"author": "edX",
|
||||
"author": "Open edX",
|
||||
"license": "AGPL-3.0",
|
||||
"homepage": "",
|
||||
"homepage": "https://github.com/openedx/frontend-app-learner-dashboard#readme",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/openedx/frontend-app-learner-dashboard/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||
"@edx/frontend-component-footer": "^14.6.0",
|
||||
"@edx/frontend-component-header": "^6.2.0",
|
||||
"@edx/frontend-enterprise-hotjar": "7.2.0",
|
||||
"@edx/frontend-platform": "^8.3.1",
|
||||
"@edx/openedx-atlas": "^0.6.0",
|
||||
"@edx/react-unit-test-utils": "^4.0.0",
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.3",
|
||||
"@edx/openedx-atlas": "^0.7.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@openedx/frontend-plugin-framework": "^1.7.0",
|
||||
"@openedx/paragon": "^22.16.0",
|
||||
"@redux-devtools/extension": "3.3.0",
|
||||
"@reduxjs/toolkit": "^2.0.0",
|
||||
"classnames": "^2.3.1",
|
||||
"core-js": "3.40.0",
|
||||
"filesize": "^10.0.0",
|
||||
"font-awesome": "4.7.0",
|
||||
"history": "5.3.0",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"prop-types": "15.8.1",
|
||||
"query-string": "7.1.3",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-intl": "6.8.9",
|
||||
"react-redux": "^7.2.4",
|
||||
"react-router-dom": "6.29.0",
|
||||
"react-share": "^4.4.0",
|
||||
"redux": "4.2.1",
|
||||
"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"
|
||||
"reselect": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/browserslist-config": "^1.3.0",
|
||||
"@edx/reactifex": "^2.1.1",
|
||||
"@openedx/frontend-build": "^14.3.3",
|
||||
"@edx/browserslist-config": "^1.5.0",
|
||||
"@edx/react-unit-test-utils": "^4.0.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"copy-webpack-plugin": "^12.0.0",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
@@ -83,5 +67,18 @@
|
||||
"react-dev-utils": "^12.0.0",
|
||||
"react-test-renderer": "^18.3.1",
|
||||
"redux-mock-store": "^1.5.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@openedx/frontend-base": "^1.0.0-alpha.1",
|
||||
"@openedx/paragon": "^22",
|
||||
"@tanstack/react-query": "^5",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-redux": "^8",
|
||||
"react-router": "^6",
|
||||
"react-router-dom": "^6",
|
||||
"redux": "^4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<!doctype html>
|
||||
<html lang="en-us" dir="ltr">
|
||||
<head>
|
||||
<title>Learner Dashboard Development Site></title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
41
site.config.dev.tsx
Normal file
41
site.config.dev.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { EnvironmentTypes, SiteConfig, footerApp, headerApp, shellApp } from '@openedx/frontend-base';
|
||||
|
||||
import { learnerDashboardApp } from './src';
|
||||
|
||||
import './src/app.scss';
|
||||
|
||||
const siteConfig: SiteConfig = {
|
||||
siteId: 'learner-dashboard-dev',
|
||||
siteName: 'Learner Dashboard Dev',
|
||||
baseUrl: 'http://apps.local.openedx.io:1996',
|
||||
lmsBaseUrl: 'http://local.openedx.io:8000',
|
||||
loginUrl: 'http://local.openedx.io:8000/login',
|
||||
logoutUrl: 'http://local.openedx.io:8000/logout',
|
||||
|
||||
environment: EnvironmentTypes.DEVELOPMENT,
|
||||
basename: '/learner-dashboard',
|
||||
apps: [
|
||||
shellApp,
|
||||
headerApp,
|
||||
footerApp,
|
||||
learnerDashboardApp
|
||||
],
|
||||
externalRoutes: [
|
||||
{
|
||||
role: 'profile',
|
||||
url: 'http://apps.local.openedx.io:1995/profile/'
|
||||
},
|
||||
{
|
||||
role: 'account',
|
||||
url: 'http://apps.local.openedx.io:1997/account/'
|
||||
},
|
||||
{
|
||||
role: 'logout',
|
||||
url: 'http://local.openedx.io:8000/logout'
|
||||
},
|
||||
],
|
||||
|
||||
accessTokenCookieName: 'edx-jwt-cookie-header-payload',
|
||||
};
|
||||
|
||||
export default siteConfig;
|
||||
28
site.config.test.tsx
Normal file
28
site.config.test.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { EnvironmentTypes, SiteConfig } from '@openedx/frontend-base';
|
||||
|
||||
import { appId } from './src/constants';
|
||||
|
||||
const siteConfig: SiteConfig = {
|
||||
siteId: 'learner-dashboard-test-site',
|
||||
siteName: 'Learner Dashboard Test Site',
|
||||
baseUrl: 'http://localhost:1996',
|
||||
lmsBaseUrl: 'http://localhost:8000',
|
||||
loginUrl: 'http://localhost:8000/login',
|
||||
logoutUrl: 'http://localhost:8000/logout',
|
||||
|
||||
environment: EnvironmentTypes.TEST,
|
||||
basename: '/learner-dashboard',
|
||||
apps: [{
|
||||
appId,
|
||||
config: {
|
||||
ECOMMERCE_BASE_URL: 'http://localhost:18130',
|
||||
FAVICON_URL: 'https://edx-cdn.org/v3/default/favicon.ico',
|
||||
LEARNING_BASE_URL: 'http://localhost:2000',
|
||||
},
|
||||
}],
|
||||
|
||||
accessTokenCookieName: 'edx-jwt-cookie-header-payload',
|
||||
segmentKey: '',
|
||||
};
|
||||
|
||||
export default siteConfig;
|
||||
100
src/App.jsx
100
src/App.jsx
@@ -1,100 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
import { initializeHotjar } from '@edx/frontend-enterprise-hotjar';
|
||||
|
||||
import { ErrorPage, AppContext } from '@edx/frontend-platform/react';
|
||||
import { FooterSlot } from '@edx/frontend-component-footer';
|
||||
import { Alert } from '@openedx/paragon';
|
||||
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
import store from 'data/store';
|
||||
import {
|
||||
selectors,
|
||||
actions,
|
||||
} from 'data/redux';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import Dashboard from 'containers/Dashboard';
|
||||
|
||||
import track from 'tracking';
|
||||
|
||||
import fakeData from 'data/services/lms/fakeData/courses';
|
||||
|
||||
import AppWrapper from 'containers/WidgetContainers/AppWrapper';
|
||||
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import messages from './messages';
|
||||
import './App.scss';
|
||||
|
||||
export const App = () => {
|
||||
const { authenticatedUser } = React.useContext(AppContext);
|
||||
const { formatMessage } = useIntl();
|
||||
const isFailed = {
|
||||
initialize: reduxHooks.useRequestIsFailed(RequestKeys.initialize),
|
||||
refreshList: reduxHooks.useRequestIsFailed(RequestKeys.refreshList),
|
||||
};
|
||||
const hasNetworkFailure = isFailed.initialize || isFailed.refreshList;
|
||||
const { supportEmail } = reduxHooks.usePlatformSettingsData();
|
||||
const loadData = reduxHooks.useLoadData();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (authenticatedUser?.administrator || getConfig().NODE_ENV === 'development') {
|
||||
window.loadEmptyData = () => {
|
||||
loadData({ ...fakeData.globalData, courses: [] });
|
||||
};
|
||||
window.loadMockData = () => {
|
||||
loadData({
|
||||
...fakeData.globalData,
|
||||
courses: [
|
||||
...fakeData.courseRunData,
|
||||
...fakeData.entitlementData,
|
||||
],
|
||||
});
|
||||
};
|
||||
window.store = store;
|
||||
window.selectors = selectors;
|
||||
window.actions = actions;
|
||||
window.track = track;
|
||||
}
|
||||
if (getConfig().HOTJAR_APP_ID) {
|
||||
try {
|
||||
initializeHotjar({
|
||||
hotjarId: getConfig().HOTJAR_APP_ID,
|
||||
hotjarVersion: getConfig().HOTJAR_VERSION,
|
||||
hotjarDebug: !!getConfig().HOTJAR_DEBUG,
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
}
|
||||
}
|
||||
}, [authenticatedUser, loadData]);
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{formatMessage(messages.pageTitle)}</title>
|
||||
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
|
||||
</Helmet>
|
||||
<div>
|
||||
<AppWrapper>
|
||||
<LearnerDashboardHeader />
|
||||
<main id="main">
|
||||
{hasNetworkFailure
|
||||
? (
|
||||
<Alert variant="danger">
|
||||
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
|
||||
</Alert>
|
||||
) : (
|
||||
<Dashboard />
|
||||
)}
|
||||
</main>
|
||||
</AppWrapper>
|
||||
<FooterSlot />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
69
src/App.scss
69
src/App.scss
@@ -1,69 +0,0 @@
|
||||
// frontend-app-*/src/index.scss
|
||||
@import "~@edx/brand/paragon/fonts";
|
||||
@import "~@edx/brand/paragon/variables";
|
||||
@import "~@openedx/paragon/scss/core/core";
|
||||
@import "~@edx/brand/paragon/overrides";
|
||||
|
||||
$fa-font-path: "~font-awesome/fonts";
|
||||
@import "~font-awesome/scss/font-awesome";
|
||||
|
||||
$input-focus-box-shadow: $input-box-shadow; // hack to get upgrade to paragon 4.0.0 to work
|
||||
|
||||
@import "~@edx/frontend-component-header/dist/index";
|
||||
@import "~@edx/frontend-component-footer/dist/_footer";
|
||||
|
||||
.text-ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.alert.alert-info .alert-icon {
|
||||
color: black;
|
||||
}
|
||||
|
||||
#root {
|
||||
// Removing a odd 1.5 scaling on checkboxes.:
|
||||
input[type=checkbox] {
|
||||
transform: none;
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
|
||||
main {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
header {
|
||||
flex: 0 0 auto;
|
||||
|
||||
.logo {
|
||||
display: block;
|
||||
box-sizing: content-box;
|
||||
position: relative;
|
||||
top: 0.1em;
|
||||
height: 1.75rem;
|
||||
margin-right: 1rem;
|
||||
img {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
flex: 0;
|
||||
}
|
||||
}
|
||||
|
||||
#paragon-portal-root {
|
||||
.pgn__modal-layer {
|
||||
.pgn__modal-close-container {
|
||||
right: 1rem !important;
|
||||
}
|
||||
}
|
||||
.confirm-modal .pgn__modal-body {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
155
src/App.test.jsx
155
src/App.test.jsx
@@ -1,155 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import Dashboard from 'containers/Dashboard';
|
||||
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
|
||||
import AppWrapper from 'containers/WidgetContainers/AppWrapper';
|
||||
import { App } from './App';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('@edx/frontend-component-footer', () => ({ FooterSlot: 'FooterSlot' }));
|
||||
|
||||
jest.mock('containers/Dashboard', () => 'Dashboard');
|
||||
jest.mock('containers/LearnerDashboardHeader', () => 'LearnerDashboardHeader');
|
||||
jest.mock('containers/WidgetContainers/AppWrapper', () => 'AppWrapper');
|
||||
jest.mock('data/redux', () => ({
|
||||
selectors: 'redux.selectors',
|
||||
actions: 'redux.actions',
|
||||
thunkActions: 'redux.thunkActions',
|
||||
}));
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useRequestIsFailed: jest.fn(),
|
||||
usePlatformSettingsData: jest.fn(),
|
||||
useLoadData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
jest.mock('data/store', () => 'data/store');
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
getConfig: jest.fn(() => ({})),
|
||||
}));
|
||||
|
||||
const loadData = jest.fn();
|
||||
reduxHooks.useLoadData.mockReturnValue(loadData);
|
||||
|
||||
let el;
|
||||
|
||||
const supportEmail = 'test-support-url';
|
||||
reduxHooks.usePlatformSettingsData.mockReturnValue({ supportEmail });
|
||||
|
||||
describe('App router component', () => {
|
||||
const { formatMessage } = useIntl();
|
||||
describe('component', () => {
|
||||
const runBasicTests = () => {
|
||||
test('snapshot', () => { expect(el.snapshot).toMatchSnapshot(); });
|
||||
it('displays title in helmet component', () => {
|
||||
const control = el.instance
|
||||
.findByType(Helmet)[0]
|
||||
.findByType('title')[0];
|
||||
expect(control.children[0].el).toEqual(formatMessage(messages.pageTitle));
|
||||
});
|
||||
it('displays learner dashboard header', () => {
|
||||
expect(el.instance.findByType(LearnerDashboardHeader).length).toEqual(1);
|
||||
});
|
||||
it('wraps the header and main components in an AppWrapper widget container', () => {
|
||||
const container = el.instance.findByType(AppWrapper)[0];
|
||||
expect(container.children[0].type).toEqual('LearnerDashboardHeader');
|
||||
expect(container.children[1].type).toEqual('main');
|
||||
});
|
||||
};
|
||||
describe('no network failure', () => {
|
||||
beforeAll(() => {
|
||||
reduxHooks.useRequestIsFailed.mockReturnValue(false);
|
||||
getConfig.mockReturnValue({});
|
||||
el = shallow(<App />);
|
||||
});
|
||||
runBasicTests();
|
||||
it('loads dashboard', () => {
|
||||
const main = el.instance.findByType('main')[0];
|
||||
expect(main.children.length).toEqual(1);
|
||||
const dashboard = main.children[0];
|
||||
expect(dashboard.type).toEqual('Dashboard');
|
||||
expect(
|
||||
dashboard.matches(shallow(<Dashboard />)),
|
||||
).toEqual(true);
|
||||
});
|
||||
});
|
||||
describe('no network failure with optimizely url', () => {
|
||||
beforeAll(() => {
|
||||
reduxHooks.useRequestIsFailed.mockReturnValue(false);
|
||||
getConfig.mockReturnValue({ OPTIMIZELY_URL: 'fake.url' });
|
||||
el = shallow(<App />);
|
||||
});
|
||||
runBasicTests();
|
||||
it('loads dashboard', () => {
|
||||
const main = el.instance.findByType('main')[0];
|
||||
expect(main.children.length).toEqual(1);
|
||||
const dashboard = main.children[0];
|
||||
expect(dashboard.type).toEqual('Dashboard');
|
||||
expect(
|
||||
dashboard.matches(shallow(<Dashboard />)),
|
||||
).toEqual(true);
|
||||
});
|
||||
});
|
||||
describe('no network failure with optimizely project id', () => {
|
||||
beforeAll(() => {
|
||||
reduxHooks.useRequestIsFailed.mockReturnValue(false);
|
||||
getConfig.mockReturnValue({ OPTIMIZELY_PROJECT_ID: 'fakeId' });
|
||||
el = shallow(<App />);
|
||||
});
|
||||
runBasicTests();
|
||||
it('loads dashboard', () => {
|
||||
const main = el.instance.findByType('main')[0];
|
||||
expect(main.children.length).toEqual(1);
|
||||
const dashboard = main.children[0];
|
||||
expect(dashboard.type).toEqual('Dashboard');
|
||||
expect(
|
||||
dashboard.matches(shallow(<Dashboard />)),
|
||||
).toEqual(true);
|
||||
});
|
||||
});
|
||||
describe('initialize failure', () => {
|
||||
beforeAll(() => {
|
||||
reduxHooks.useRequestIsFailed.mockImplementation((key) => key === RequestKeys.initialize);
|
||||
getConfig.mockReturnValue({});
|
||||
el = shallow(<App />);
|
||||
});
|
||||
runBasicTests();
|
||||
it('loads error page', () => {
|
||||
const main = el.instance.findByType('main')[0];
|
||||
expect(main.children.length).toEqual(1);
|
||||
const alert = main.children[0];
|
||||
expect(alert.type).toEqual('Alert');
|
||||
expect(alert.children.length).toEqual(1);
|
||||
const errorPage = alert.children[0];
|
||||
expect(errorPage.type).toEqual('ErrorPage');
|
||||
expect(errorPage.props.message).toEqual(formatMessage(messages.errorMessage, { supportEmail }));
|
||||
});
|
||||
});
|
||||
describe('refresh failure', () => {
|
||||
beforeAll(() => {
|
||||
reduxHooks.useRequestIsFailed.mockImplementation((key) => key === RequestKeys.refreshList);
|
||||
getConfig.mockReturnValue({});
|
||||
el = shallow(<App />);
|
||||
});
|
||||
runBasicTests();
|
||||
it('loads error page', () => {
|
||||
const main = el.instance.findByType('main')[0];
|
||||
expect(main.children.length).toEqual(1);
|
||||
const alert = main.children[0];
|
||||
expect(alert.type).toEqual('Alert');
|
||||
expect(alert.children.length).toEqual(1);
|
||||
const errorPage = alert.children[0];
|
||||
expect(errorPage.type).toEqual('ErrorPage');
|
||||
expect(errorPage.props.message).toEqual(formatMessage(messages.errorMessage, { supportEmail }));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
20
src/Main.jsx
Normal file
20
src/Main.jsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Provider as ReduxProvider } from 'react-redux';
|
||||
import { CurrentAppProvider, PageWrap } from '@openedx/frontend-base';
|
||||
|
||||
import { appId } from './constants';
|
||||
import store from './data/store';
|
||||
import Dashboard from './containers/Dashboard';
|
||||
|
||||
import './app.scss';
|
||||
|
||||
const Main = () => (
|
||||
<CurrentAppProvider appId={appId}>
|
||||
<ReduxProvider store={store}>
|
||||
<PageWrap>
|
||||
<Dashboard />
|
||||
</PageWrap>
|
||||
</ReduxProvider>
|
||||
</CurrentAppProvider>
|
||||
);
|
||||
|
||||
export default Main;
|
||||
1
src/__mocks__/file.js
Normal file
1
src/__mocks__/file.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = 'FileMock';
|
||||
1
src/__mocks__/svg.js
Normal file
1
src/__mocks__/svg.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = 'SvgURL';
|
||||
@@ -2,21 +2,8 @@
|
||||
|
||||
exports[`App router component component initialize failure snapshot 1`] = `
|
||||
<Fragment>
|
||||
<HelmetWrapper
|
||||
defer={true}
|
||||
encodeSpecialCharacters={true}
|
||||
>
|
||||
<title>
|
||||
Learner Home
|
||||
</title>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/x-icon"
|
||||
/>
|
||||
</HelmetWrapper>
|
||||
<div>
|
||||
<AppWrapper>
|
||||
<LearnerDashboardHeader />
|
||||
<main
|
||||
id="main"
|
||||
>
|
||||
@@ -29,112 +16,56 @@ exports[`App router component component initialize failure snapshot 1`] = `
|
||||
</Alert>
|
||||
</main>
|
||||
</AppWrapper>
|
||||
<FooterSlot />
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`App router component component no network failure snapshot 1`] = `
|
||||
<Fragment>
|
||||
<HelmetWrapper
|
||||
defer={true}
|
||||
encodeSpecialCharacters={true}
|
||||
>
|
||||
<title>
|
||||
Learner Home
|
||||
</title>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/x-icon"
|
||||
/>
|
||||
</HelmetWrapper>
|
||||
<div>
|
||||
<AppWrapper>
|
||||
<LearnerDashboardHeader />
|
||||
<main
|
||||
id="main"
|
||||
>
|
||||
<Dashboard />
|
||||
</main>
|
||||
</AppWrapper>
|
||||
<FooterSlot />
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`App router component component no network failure with optimizely project id snapshot 1`] = `
|
||||
<Fragment>
|
||||
<HelmetWrapper
|
||||
defer={true}
|
||||
encodeSpecialCharacters={true}
|
||||
>
|
||||
<title>
|
||||
Learner Home
|
||||
</title>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/x-icon"
|
||||
/>
|
||||
</HelmetWrapper>
|
||||
<div>
|
||||
<AppWrapper>
|
||||
<LearnerDashboardHeader />
|
||||
<main
|
||||
id="main"
|
||||
>
|
||||
<Dashboard />
|
||||
</main>
|
||||
</AppWrapper>
|
||||
<FooterSlot />
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`App router component component no network failure with optimizely url snapshot 1`] = `
|
||||
<Fragment>
|
||||
<HelmetWrapper
|
||||
defer={true}
|
||||
encodeSpecialCharacters={true}
|
||||
>
|
||||
<title>
|
||||
Learner Home
|
||||
</title>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/x-icon"
|
||||
/>
|
||||
</HelmetWrapper>
|
||||
<div>
|
||||
<AppWrapper>
|
||||
<LearnerDashboardHeader />
|
||||
<main
|
||||
id="main"
|
||||
>
|
||||
<Dashboard />
|
||||
</main>
|
||||
</AppWrapper>
|
||||
<FooterSlot />
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`App router component component refresh failure snapshot 1`] = `
|
||||
<Fragment>
|
||||
<HelmetWrapper
|
||||
defer={true}
|
||||
encodeSpecialCharacters={true}
|
||||
>
|
||||
<title>
|
||||
Learner Home
|
||||
</title>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/x-icon"
|
||||
/>
|
||||
</HelmetWrapper>
|
||||
<div>
|
||||
<AppWrapper>
|
||||
<LearnerDashboardHeader />
|
||||
<main
|
||||
id="main"
|
||||
>
|
||||
@@ -147,7 +78,6 @@ exports[`App router component component refresh failure snapshot 1`] = `
|
||||
</Alert>
|
||||
</main>
|
||||
</AppWrapper>
|
||||
<FooterSlot />
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
@@ -10,7 +10,7 @@ exports[`app registry subscribe: APP_INIT_ERROR. snapshot: displays an ErrorPag
|
||||
|
||||
exports[`app registry subscribe: APP_READY. links App to root element 1`] = `
|
||||
<UNDEFINED>
|
||||
<AppProvider
|
||||
<SiteProvider
|
||||
store={
|
||||
{
|
||||
"redux": "store",
|
||||
@@ -38,6 +38,6 @@ exports[`app registry subscribe: APP_READY. links App to root element 1`] = `
|
||||
/>
|
||||
</Routes>
|
||||
</NoticesWrapper>
|
||||
</AppProvider>
|
||||
</SiteProvider>
|
||||
</UNDEFINED>
|
||||
`;
|
||||
|
||||
41
src/app.scss
Executable file
41
src/app.scss
Executable file
@@ -0,0 +1,41 @@
|
||||
@import "~@edx/brand/paragon/fonts";
|
||||
@import "~@edx/brand/paragon/variables";
|
||||
@import "~@openedx/paragon/scss/core/core";
|
||||
@import "~@edx/brand/paragon/overrides";
|
||||
|
||||
$fa-font-path: "~font-awesome/fonts";
|
||||
@import "~font-awesome/scss/font-awesome";
|
||||
|
||||
$input-focus-box-shadow: $input-box-shadow; // hack to get upgrade to paragon 4.0.0 to work
|
||||
|
||||
#learnerDashboardRoot {
|
||||
main {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
// Removing a odd 1.5 scaling on checkboxes.:
|
||||
input[type=checkbox] {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.text-ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.alert.alert-info .alert-icon {
|
||||
color: black;
|
||||
}
|
||||
|
||||
#paragon-portal-root {
|
||||
.pgn__modal-layer {
|
||||
.pgn__modal-close-container {
|
||||
right: 1rem !important;
|
||||
}
|
||||
}
|
||||
.confirm-modal .pgn__modal-body {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/app.ts
Normal file
23
src/app.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { App } from '@openedx/frontend-base';
|
||||
import { appId } from './constants';
|
||||
import routes from './routes';
|
||||
import providers from './providers';
|
||||
import messages from './i18n';
|
||||
import slots from './slots';
|
||||
|
||||
const app: App = {
|
||||
appId,
|
||||
routes,
|
||||
providers,
|
||||
messages,
|
||||
slots,
|
||||
config: {
|
||||
LEARNING_BASE_URL: 'http://apps.local.openedx.io:2000',
|
||||
ENABLE_PROGRAMS: false,
|
||||
ECOMMERCE_BASE_URL: '',
|
||||
ORDER_HISTORY_URL: '',
|
||||
SUPPORT_URL: '',
|
||||
}
|
||||
};
|
||||
|
||||
export default app;
|
||||
@@ -1,15 +0,0 @@
|
||||
<svg width="1350" height="7" viewBox="0 0 1350 7" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_3302_26079)">
|
||||
<rect width="1350" height="6.75" transform="translate(0 -0.375)" fill="#03C7E8"/>
|
||||
<rect y="-0.375" width="585.562" height="6.75" fill="#D23228"/>
|
||||
<path d="M549.281 -0.375H933.188L929.491 6.375H549.281V-0.375Z" fill="#002121"/>
|
||||
<path d="M550.129 13.125L545.062 -10.5L555.188 -10.5L550.129 13.125Z" fill="#D23228"/>
|
||||
<path d="M931.082 13.125L925.594 -6.28125L936.563 -6.28125L931.082 13.125Z" fill="#002121"/>
|
||||
<path d="M0 -0.375H106.312L105.289 6.375H0V-0.375Z" fill="#921108"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3302_26079">
|
||||
<rect width="1350" height="6.75" fill="white" transform="translate(0 -0.375)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 765 B |
@@ -1,26 +0,0 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { logError, logInfo } from '@edx/frontend-platform/logging';
|
||||
|
||||
export const noticesUrl = `${getConfig().LMS_BASE_URL}/notices/api/v1/unacknowledged`;
|
||||
export const error404Message = 'This probably happened because the notices plugin is not installed on platform.';
|
||||
|
||||
export const getNotices = ({ onLoad }) => {
|
||||
const authenticatedUser = getAuthenticatedUser();
|
||||
|
||||
const handleError = async (e) => {
|
||||
// Error probably means that notices is not installed, which is fine.
|
||||
const { customAttributes: { httpErrorStatus } } = e;
|
||||
if (httpErrorStatus === 404) {
|
||||
logInfo(`${e}. ${error404Message}`);
|
||||
} else {
|
||||
logError(e);
|
||||
}
|
||||
};
|
||||
if (authenticatedUser) {
|
||||
return getAuthenticatedHttpClient().get(noticesUrl, {}).then(onLoad).catch(handleError);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default { getNotices };
|
||||
@@ -1,65 +0,0 @@
|
||||
import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { logError, logInfo } from '@edx/frontend-platform/logging';
|
||||
|
||||
import * as api from './api';
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
getConfig: jest.fn(() => ({
|
||||
LMS_BASE_URL: 'test-lms-url',
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||
getAuthenticatedHttpClient: jest.fn(),
|
||||
getAuthenticatedUser: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/logging', () => ({
|
||||
logError: jest.fn(),
|
||||
logInfo: jest.fn(),
|
||||
}));
|
||||
|
||||
const testData = 'test-data';
|
||||
const successfulGet = () => Promise.resolve(testData);
|
||||
const error404 = { customAttributes: { httpErrorStatus: 404 }, test: 'error' };
|
||||
const error404Get = () => Promise.reject(error404);
|
||||
const error500 = { customAttributes: { httpErrorStatus: 500 }, test: 'error' };
|
||||
const error500Get = () => Promise.reject(error500);
|
||||
|
||||
const get = jest.fn().mockImplementation(successfulGet);
|
||||
getAuthenticatedHttpClient.mockReturnValue({ get });
|
||||
const authenticatedUser = { fake: 'user' };
|
||||
getAuthenticatedUser.mockReturnValue(authenticatedUser);
|
||||
|
||||
const onLoad = jest.fn();
|
||||
describe('getNotices api method', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('behavior', () => {
|
||||
describe('not authenticated', () => {
|
||||
it('does not fetch anything', () => {
|
||||
getAuthenticatedUser.mockReturnValueOnce(null);
|
||||
api.getNotices({ onLoad });
|
||||
expect(get).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('authenticated', () => {
|
||||
it('fetches noticesUrl with onLoad behavior', async () => {
|
||||
await api.getNotices({ onLoad });
|
||||
expect(get).toHaveBeenCalledWith(api.noticesUrl, {});
|
||||
expect(onLoad).toHaveBeenCalledWith(testData);
|
||||
});
|
||||
it('calls logInfo if fetch fails with 404', async () => {
|
||||
get.mockImplementation(error404Get);
|
||||
await api.getNotices({ onLoad });
|
||||
expect(logInfo).toHaveBeenCalledWith(`${error404}. ${api.error404Message}`);
|
||||
});
|
||||
it('calls logError if fetch fails with non-404 error', async () => {
|
||||
get.mockImplementation(error500Get);
|
||||
await api.getNotices({ onLoad });
|
||||
expect(logError).toHaveBeenCalledWith(error500);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,35 +0,0 @@
|
||||
import React from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
import { getNotices } from './api';
|
||||
import * as module from './hooks';
|
||||
|
||||
/**
|
||||
* This component uses the platform-plugin-notices plugin to function.
|
||||
* If the user has an unacknowledged notice, they will be rerouted off
|
||||
* course home and onto a full-screen notice page. If the plugin is not
|
||||
* installed, or there are no notices, we just passthrough this component.
|
||||
*/
|
||||
export const state = StrictDict({
|
||||
isRedirected: (val) => React.useState(val), // eslint-disable-line
|
||||
});
|
||||
|
||||
export const useNoticesWrapperData = () => {
|
||||
const [isRedirected, setIsRedirected] = module.state.isRedirected();
|
||||
React.useEffect(() => {
|
||||
if (getConfig().ENABLE_NOTICES) {
|
||||
getNotices({
|
||||
onLoad: (data) => {
|
||||
if (data?.data?.results?.length > 0) {
|
||||
setIsRedirected(true);
|
||||
window.location.replace(`${data.data.results[0]}?next=${window.location.href}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [setIsRedirected]);
|
||||
return { isRedirected };
|
||||
};
|
||||
|
||||
export default useNoticesWrapperData;
|
||||
@@ -1,83 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { MockUseState } from 'testUtils';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getNotices } from './api';
|
||||
import * as hooks from './hooks';
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({ getConfig: jest.fn() }));
|
||||
jest.mock('./api', () => ({ getNotices: jest.fn() }));
|
||||
|
||||
getConfig.mockReturnValue({ ENABLE_NOTICES: true });
|
||||
const state = new MockUseState(hooks);
|
||||
|
||||
let hook;
|
||||
describe('NoticesWrapper hooks', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('state hooks', () => {
|
||||
state.testGetter(state.keys.isRedirected);
|
||||
});
|
||||
describe('useNoticesWrapperData', () => {
|
||||
beforeEach(() => {
|
||||
state.mock();
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes state hooks', () => {
|
||||
hooks.useNoticesWrapperData();
|
||||
expect(hooks.state.isRedirected).toHaveBeenCalledWith();
|
||||
});
|
||||
describe('effects', () => {
|
||||
it('does not call notices if not enabled', () => {
|
||||
getConfig.mockReturnValueOnce({ ENABLE_NOTICES: false });
|
||||
hooks.useNoticesWrapperData();
|
||||
const [cb, prereqs] = React.useEffect.mock.calls[0];
|
||||
expect(prereqs).toEqual([state.setState.isRedirected]);
|
||||
cb();
|
||||
expect(getNotices).not.toHaveBeenCalled();
|
||||
});
|
||||
describe('getNotices call (if enabled) onLoad behavior', () => {
|
||||
it('does not redirect if there are no results', () => {
|
||||
hooks.useNoticesWrapperData();
|
||||
expect(React.useEffect).toHaveBeenCalled();
|
||||
const [cb, prereqs] = React.useEffect.mock.calls[0];
|
||||
expect(prereqs).toEqual([state.setState.isRedirected]);
|
||||
cb();
|
||||
expect(getNotices).toHaveBeenCalled();
|
||||
const { onLoad } = getNotices.mock.calls[0][0];
|
||||
onLoad({});
|
||||
expect(state.setState.isRedirected).not.toHaveBeenCalled();
|
||||
onLoad({ data: {} });
|
||||
expect(state.setState.isRedirected).not.toHaveBeenCalled();
|
||||
onLoad({ data: { results: [] } });
|
||||
expect(state.setState.isRedirected).not.toHaveBeenCalled();
|
||||
});
|
||||
it('redirects and set isRedirected if results are returned', () => {
|
||||
delete window.location;
|
||||
window.location = { replace: jest.fn(), href: 'test-old-href' };
|
||||
hooks.useNoticesWrapperData();
|
||||
const [cb, prereqs] = React.useEffect.mock.calls[0];
|
||||
expect(prereqs).toEqual([state.setState.isRedirected]);
|
||||
cb();
|
||||
expect(getNotices).toHaveBeenCalled();
|
||||
const { onLoad } = getNotices.mock.calls[0][0];
|
||||
const target = 'url-target';
|
||||
onLoad({ data: { results: [target] } });
|
||||
expect(state.setState.isRedirected).toHaveBeenCalledWith(true);
|
||||
expect(window.location.replace).toHaveBeenCalledWith(
|
||||
`${target}?next=${window.location.href}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
it('forwards isRedirected from state call', () => {
|
||||
hook = hooks.useNoticesWrapperData();
|
||||
expect(hook.isRedirected).toEqual(state.stateVals.isRedirected);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import useNoticesWrapperData from './hooks';
|
||||
|
||||
/**
|
||||
* This component uses the platform-plugin-notices plugin to function.
|
||||
* If the user has an unacknowledged notice, they will be rerouted off
|
||||
* course home and onto a full-screen notice page. If the plugin is not
|
||||
* installed, or there are no notices, we just passthrough this component.
|
||||
*/
|
||||
const NoticesWrapper = ({ children }) => {
|
||||
const { isRedirected } = useNoticesWrapperData();
|
||||
return (
|
||||
<div>
|
||||
{isRedirected === true ? null : children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
NoticesWrapper.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default NoticesWrapper;
|
||||
@@ -1,36 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import useNoticesWrapperData from './hooks';
|
||||
import NoticesWrapper from '.';
|
||||
|
||||
jest.mock('./hooks', () => jest.fn());
|
||||
|
||||
const hookProps = { isRedirected: false };
|
||||
useNoticesWrapperData.mockReturnValue(hookProps);
|
||||
|
||||
let el;
|
||||
const children = [<b key={1}>some</b>, <i key={2}>children</i>];
|
||||
describe('NoticesWrapper component', () => {
|
||||
describe('behavior', () => {
|
||||
it('initializes hooks', () => {
|
||||
el = shallow(<NoticesWrapper>{children}</NoticesWrapper>);
|
||||
expect(useNoticesWrapperData).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
it('does not show children if redirected', () => {
|
||||
useNoticesWrapperData.mockReturnValueOnce({ isRedirected: true });
|
||||
el = shallow(<NoticesWrapper>{children}</NoticesWrapper>);
|
||||
expect(el.instance.children.length).toEqual(0);
|
||||
});
|
||||
it('shows children if not redirected', () => {
|
||||
el = shallow(<NoticesWrapper>{children}</NoticesWrapper>);
|
||||
expect(el.instance.children.length).toEqual(2);
|
||||
expect(el.instance.children[0].type).toEqual(shallow(children[0]).type);
|
||||
expect(el.instance.props).toEqual(shallow(children[0]).props);
|
||||
expect(el.instance.children[1].type).toEqual(shallow(children[1]).type);
|
||||
expect(el.instance.props).toEqual(shallow(children[1]).props);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
const configuration = {
|
||||
// BASE_URL: process.env.BASE_URL,
|
||||
LMS_BASE_URL: process.env.LMS_BASE_URL,
|
||||
ECOMMERCE_BASE_URL: process.env.ECOMMERCE_BASE_URL,
|
||||
// LOGIN_URL: process.env.LOGIN_URL,
|
||||
// LOGOUT_URL: process.env.LOGOUT_URL,
|
||||
// CSRF_TOKEN_API_PATH: process.env.CSRF_TOKEN_API_PATH,
|
||||
// REFRESH_ACCESS_TOKEN_ENDPOINT: process.env.REFRESH_ACCESS_TOKEN_ENDPOINT,
|
||||
// DATA_API_BASE_URL: process.env.DATA_API_BASE_URL,
|
||||
// SECURE_COOKIES: process.env.NODE_ENV !== 'development',
|
||||
SEGMENT_KEY: process.env.SEGMENT_KEY,
|
||||
// ACCESS_TOKEN_COOKIE_NAME: process.env.ACCESS_TOKEN_COOKIE_NAME,
|
||||
LEARNING_BASE_URL: process.env.LEARNING_BASE_URL,
|
||||
SESSION_COOKIE_DOMAIN: process.env.SESSION_COOKIE_DOMAIN || '',
|
||||
SUPPORT_URL: process.env.SUPPORT_URL || null,
|
||||
ENABLE_NOTICES: process.env.ENABLE_NOTICES || null,
|
||||
CAREER_LINK_URL: process.env.CAREER_LINK_URL || null,
|
||||
LOGO_URL: process.env.LOGO_URL,
|
||||
ENABLE_EDX_PERSONAL_DASHBOARD: process.env.ENABLE_EDX_PERSONAL_DASHBOARD === 'true',
|
||||
SEARCH_CATALOG_URL: process.env.SEARCH_CATALOG_URL || null,
|
||||
ENABLE_PROGRAMS: process.env.ENABLE_PROGRAMS === 'true',
|
||||
};
|
||||
|
||||
const features = {};
|
||||
|
||||
export { configuration, features };
|
||||
1
src/constants.ts
Normal file
1
src/constants.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const appId = 'org.openedx.frontend.app.learnerDashboard';
|
||||
@@ -1,10 +1,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import track from '../../../../tracking';
|
||||
import { reduxHooks } from '../../../../hooks';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ActionButton from './ActionButton';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import track from '../../../../tracking';
|
||||
import { reduxHooks } from '../../../../hooks';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ActionButton from './ActionButton';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import { reduxHooks } from '../../../../hooks';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ActionButton from './ActionButton';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import track from '../../../../tracking';
|
||||
import { reduxHooks } from '../../../../hooks';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ActionButton from './ActionButton';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -3,9 +3,9 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { ActionRow } from '@openedx/paragon';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { reduxHooks } from '../../../../hooks';
|
||||
import CourseCardActionSlot from '../../../../slots/CourseCardActionSlot';
|
||||
|
||||
import CourseCardActionSlot from 'plugin-slots/CourseCardActionSlot';
|
||||
import SelectSessionButton from './SelectSessionButton';
|
||||
import BeginCourseButton from './BeginCourseButton';
|
||||
import ResumeButton from './ResumeButton';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import CourseCardActionSlot from 'plugin-slots/CourseCardActionSlot';
|
||||
import CourseCardActionSlot from 'slots/CourseCardActionSlot';
|
||||
import SelectSessionButton from './SelectSessionButton';
|
||||
import BeginCourseButton from './BeginCourseButton';
|
||||
import ResumeButton from './ResumeButton';
|
||||
@@ -19,7 +19,7 @@ jest.mock('hooks', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('plugin-slots/CourseCardActionSlot', () => 'CustomActionButton');
|
||||
jest.mock('slots/CourseCardActionSlot', () => 'CustomActionButton');
|
||||
jest.mock('./SelectSessionButton', () => 'SelectSessionButton');
|
||||
jest.mock('./ViewCourseButton', () => 'ViewCourseButton');
|
||||
jest.mock('./BeginCourseButton', () => 'BeginCourseButton');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
import { defineMessages } from '@openedx/frontend-base';
|
||||
|
||||
const messages = defineMessages({
|
||||
beginCourse: {
|
||||
|
||||
@@ -4,10 +4,10 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { MailtoLink, Hyperlink } from '@openedx/paragon';
|
||||
import { CheckCircle } from '@openedx/paragon/icons';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import { utilHooks, reduxHooks } from 'hooks';
|
||||
import Banner from 'components/Banner';
|
||||
import { utilHooks, reduxHooks } from '../../../../hooks';
|
||||
import Banner from '../../../../components/Banner';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
@@ -31,7 +31,7 @@ export const CertificateBanner = ({ cardId }) => {
|
||||
if (certificate.isRestricted) {
|
||||
return (
|
||||
<Banner variant="danger">
|
||||
{ supportEmail ? formatMessage(messages.certRestricted, { supportEmail: emailLink(supportEmail) }) : formatMessage(messages.certRestrictedNoEmail)}
|
||||
{supportEmail ? formatMessage(messages.certRestricted, { supportEmail: emailLink(supportEmail) }) : formatMessage(messages.certRestrictedNoEmail)}
|
||||
{isVerified && ' '}
|
||||
{isVerified && (billingEmail ? formatMessage(messages.certRefundContactBilling, { billingEmail: emailLink(billingEmail) }) : formatMessage(messages.certRefundContactBillingNoEmail))}
|
||||
</Banner>
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Hyperlink } from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import { utilHooks, reduxHooks } from '../../../../hooks';
|
||||
import Banner from '../../../../components/Banner';
|
||||
|
||||
import { utilHooks, reduxHooks } from 'hooks';
|
||||
import Banner from 'components/Banner';
|
||||
import messages from './messages';
|
||||
|
||||
export const CourseBanner = ({ cardId }) => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { StrictDict } from '../../../../../utils';
|
||||
import { reduxHooks } from '../../../../../hooks';
|
||||
|
||||
import ApprovedContent from './views/ApprovedContent';
|
||||
import EligibleContent from './views/EligibleContent';
|
||||
@@ -17,7 +16,9 @@ export const statusComponents = StrictDict({
|
||||
export const useCreditBannerData = (cardId) => {
|
||||
const credit = reduxHooks.useCardCreditData(cardId);
|
||||
const { supportEmail } = reduxHooks.usePlatformSettingsData();
|
||||
if (!credit.isEligible) { return null; }
|
||||
if (!credit.isEligible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { error, purchased, requestStatus } = credit;
|
||||
let ContentComponent = EligibleContent;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import Banner from 'components/Banner';
|
||||
import Banner from '../../../../../components/Banner';
|
||||
|
||||
import { MailtoLink } from '@openedx/paragon';
|
||||
import hooks from './hooks';
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
import { defineMessages } from '@openedx/frontend-base';
|
||||
|
||||
const messages = defineMessages({
|
||||
error: {
|
||||
id: 'learner-dash.courseCard.banners.credit.error',
|
||||
description: '',
|
||||
description: 'Error message for credit transaction with support email link',
|
||||
defaultMessage: 'An error occurred with this transaction. For help, contact {supportEmailLink}.',
|
||||
},
|
||||
errorNoEmail: {
|
||||
id: 'learner-dash.courseCard.banners.credit.errorNoEmail',
|
||||
description: '',
|
||||
description: 'Error message for credit transaction without support email',
|
||||
defaultMessage: 'An error occurred with this transaction.',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import MasqueradeUserContext from '../../../../../../data/contexts/MasqueradeUserContext';
|
||||
import { reduxHooks } from '../../../../../../hooks';
|
||||
import CreditContent from './components/CreditContent';
|
||||
import ProviderLink from './components/ProviderLink';
|
||||
|
||||
@@ -11,7 +12,7 @@ import messages from './messages';
|
||||
|
||||
export const ApprovedContent = ({ cardId }) => {
|
||||
const { providerStatusUrl: href, providerName } = reduxHooks.useCardCreditData(cardId);
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const { isMasquerading } = useContext(MasqueradeUserContext);
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<CreditContent
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from '../../../../../../hooks';
|
||||
import track from '../../../../../../tracking';
|
||||
|
||||
import CreditContent from './components/CreditContent';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import MasqueradeUserContext from '../../../../../../data/contexts/MasqueradeUserContext';
|
||||
import CreditContent from './components/CreditContent';
|
||||
import ProviderLink from './components/ProviderLink';
|
||||
import hooks from './hooks';
|
||||
@@ -13,7 +13,7 @@ import messages from './messages';
|
||||
export const MustRequestContent = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { requestData, createCreditRequest } = hooks.useCreditRequestData(cardId);
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const { isMasquerading } = useContext(MasqueradeUserContext);
|
||||
return (
|
||||
<CreditContent
|
||||
action={{
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import MasqueradeUserContext from '../../../../../../data/contexts/MasqueradeUserContext';
|
||||
import { reduxHooks } from '../../../../../../hooks';
|
||||
import CreditContent from './components/CreditContent';
|
||||
import messages from './messages';
|
||||
|
||||
export const PendingContent = ({ cardId }) => {
|
||||
const { providerStatusUrl: href, providerName } = reduxHooks.useCardCreditData(cardId);
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const { isMasquerading } = useContext(MasqueradeUserContext);
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<CreditContent
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { reduxHooks } from '../../../../../../hooks';
|
||||
import CreditContent from './components/CreditContent';
|
||||
import ProviderLink from './components/ProviderLink';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { reduxHooks } from '../../../../../../../hooks';
|
||||
import { Hyperlink } from '@openedx/paragon';
|
||||
|
||||
export const ProviderLink = ({ cardId }) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
import { apiHooks } from 'hooks';
|
||||
import { StrictDict } from '../../../../../../utils';
|
||||
import { apiHooks } from '../../../../../../hooks';
|
||||
|
||||
import * as module from './hooks';
|
||||
|
||||
|
||||
@@ -1,59 +1,59 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
import { defineMessages } from '@openedx/frontend-base';
|
||||
|
||||
const messages = defineMessages({
|
||||
approved: {
|
||||
id: 'learner-dash.courseCard.banners.credit.approved',
|
||||
description: '',
|
||||
description: 'Message shown when credit request has been approved',
|
||||
defaultMessage: '{congratulations} {providerName} has approved your request for course credit. To see your course credit, visit the {linkToProviderSite} website.',
|
||||
},
|
||||
congratulations: {
|
||||
id: 'learner-dash.courseCard.banners.credit.congratulations',
|
||||
description: '',
|
||||
description: 'Congratulatory message for credit approval',
|
||||
defaultMessage: 'Congratulations!',
|
||||
},
|
||||
eligible: {
|
||||
id: 'learner-dash.courseCard.banners.credit.eligible',
|
||||
description: '',
|
||||
description: 'Message shown when user is eligible to purchase course credit',
|
||||
defaultMessage: 'You have completed this course and are eligible to purchase course credit. Select {getCredit} to get started.',
|
||||
},
|
||||
eligibleFromProvider: {
|
||||
id: 'learner-dash.courseCard.banners.credit.eligibleFromProvider',
|
||||
description: '',
|
||||
description: 'Message shown when user is eligible for credit from a specific provider',
|
||||
defaultMessage: 'You are now eligible for credit from {providerName}. Congratulations!',
|
||||
},
|
||||
getCredit: {
|
||||
id: 'learner-dash.courseCard.banners.credit.getCredit',
|
||||
description: '',
|
||||
description: 'Button text for initiating the credit process',
|
||||
defaultMessage: 'Get Credit',
|
||||
},
|
||||
mustRequest: {
|
||||
id: 'learner-dash.courseCard.banners.credit.mustRequest',
|
||||
description: '',
|
||||
description: 'Message shown after payment to instruct user to request credit',
|
||||
defaultMessage: 'Thank you for your payment. To receive course credit, you must request credit at the {linkToProviderSite} website. Select {requestCredit} to get started',
|
||||
},
|
||||
received: {
|
||||
id: 'learner-dash.courseCard.banners.credit.received',
|
||||
description: '',
|
||||
description: 'Message shown when credit request has been received',
|
||||
defaultMessage: '{providerName} has received your course credit request. We will update you when credit processing is complete.',
|
||||
},
|
||||
rejected: {
|
||||
id: 'learner-dash.courseCard.banners.credit.rejected',
|
||||
description: '',
|
||||
description: 'Message shown when credit request has been rejected',
|
||||
defaultMessage: '{providerName} did not approve your request for course credit. For more information, contact {linkToProviderSite} directly.',
|
||||
},
|
||||
requestCredit: {
|
||||
id: 'learner-dash.courseCard.banners.credit.requestCredit',
|
||||
description: '',
|
||||
description: 'Button text for requesting credit',
|
||||
defaultMessage: 'Request Credit',
|
||||
},
|
||||
viewCredit: {
|
||||
id: 'learner-dash.courseCard.banners.credit.viewCredit',
|
||||
description: '',
|
||||
description: 'Button text for viewing credit details',
|
||||
defaultMessage: 'View Credit',
|
||||
},
|
||||
viewDetails: {
|
||||
id: 'learner-dash.courseCard.banners.credit.viewDetails',
|
||||
description: '',
|
||||
description: 'Button text for viewing credit request details',
|
||||
defaultMessage: 'View Details',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
import { Button, MailtoLink } from '@openedx/paragon';
|
||||
|
||||
import { utilHooks, reduxHooks } from 'hooks';
|
||||
import { utilHooks, reduxHooks } from '../../../../hooks';
|
||||
import Banner from '../../../../components/Banner';
|
||||
|
||||
import Banner from 'components/Banner';
|
||||
import messages from './messages';
|
||||
|
||||
export const EntitlementBanner = ({ cardId }) => {
|
||||
|
||||
@@ -2,10 +2,10 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Program } from '@openedx/paragon/icons';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import Banner from 'components/Banner';
|
||||
import { reduxHooks } from '../../../../../hooks';
|
||||
import Banner from '../../../../../components/Banner';
|
||||
|
||||
import ProgramList from './ProgramsList';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { reduxHooks } from '../../../../../hooks';
|
||||
import RelatedProgramsBanner from '.';
|
||||
|
||||
jest.mock('./ProgramsList', () => 'ProgramsList');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
import { defineMessages } from '@openedx/frontend-base';
|
||||
|
||||
const messages = defineMessages({
|
||||
relatedPrograms: {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { reduxHooks } from '../../../../hooks';
|
||||
import CourseBannerSlot from '../../../../slots/CourseBannerSlot';
|
||||
|
||||
import CourseBannerSlot from 'plugin-slots/CourseBannerSlot';
|
||||
import CertificateBanner from './CertificateBanner';
|
||||
import CreditBanner from './CreditBanner';
|
||||
import EntitlementBanner from './EntitlementBanner';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
import { defineMessages } from '@openedx/frontend-base';
|
||||
|
||||
const messages = defineMessages({
|
||||
auditAccessExpired: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { utilHooks, reduxHooks } from 'hooks';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
import { utilHooks, reduxHooks } from '../../../../hooks';
|
||||
|
||||
import * as hooks from './hooks';
|
||||
import messages from './messages';
|
||||
@@ -10,8 +10,10 @@ export const useAccessMessage = ({ cardId }) => {
|
||||
const courseRun = reduxHooks.useCardCourseRunData(cardId);
|
||||
const formatDate = utilHooks.useFormatDate();
|
||||
if (!courseRun.isStarted) {
|
||||
if (!courseRun.startDate) { return null; }
|
||||
const startDate = formatDate(courseRun.startDate);
|
||||
if (!courseRun.startDate && !courseRun.advertisedStart) {
|
||||
return null;
|
||||
}
|
||||
const startDate = courseRun.advertisedStart ?? formatDate(courseRun.startDate);
|
||||
return formatMessage(messages.courseStarts, { startDate });
|
||||
}
|
||||
if (enrollment.isEnrolled) {
|
||||
@@ -27,7 +29,9 @@ export const useAccessMessage = ({ cardId }) => {
|
||||
{ accessExpirationDate: formatDate(accessExpirationDate) },
|
||||
);
|
||||
}
|
||||
if (!endDate) { return null; }
|
||||
if (!endDate) {
|
||||
return null;
|
||||
}
|
||||
return formatMessage(
|
||||
isArchived ? messages.courseEnded : messages.courseEnds,
|
||||
{ endDate: formatDate(endDate) },
|
||||
@@ -49,7 +53,7 @@ export const useCardDetailsData = ({ cardId }) => {
|
||||
const openSessionModal = reduxHooks.useUpdateSelectSessionModalCallback(cardId);
|
||||
|
||||
return {
|
||||
providerName: providerName || formatMessage(messages.unknownProviderName),
|
||||
providerName: providerName ?? formatMessage(messages.unknownProviderName),
|
||||
accessMessage: hooks.useAccessMessage({ cardId }),
|
||||
isEntitlement,
|
||||
isFulfilled,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import { keyStore } from 'utils';
|
||||
import { utilHooks, reduxHooks } from 'hooks';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
import { defineMessages } from '@openedx/frontend-base';
|
||||
|
||||
const messages = defineMessages({
|
||||
accessExpired: {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import { Badge } from '@openedx/paragon';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import verifiedRibbon from 'assets/verified-ribbon.png';
|
||||
import track from '../../../tracking';
|
||||
import { reduxHooks } from '../../../hooks';
|
||||
import verifiedRibbon from '../../../assets/verified-ribbon.png';
|
||||
import useActionDisabledState from './hooks';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
@@ -3,11 +3,12 @@ import PropTypes from 'prop-types';
|
||||
import * as ReactShare from 'react-share';
|
||||
|
||||
import { StrictDict } from '@edx/react-unit-test-utils';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
import { Dropdown } from '@openedx/paragon';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import MasqueradeUserContext from '../../../../data/contexts/MasqueradeUserContext';
|
||||
import track from '../../../../tracking';
|
||||
import { reduxHooks } from '../../../../hooks';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
@@ -21,7 +22,7 @@ export const SocialShareMenu = ({ cardId, emailSettings }) => {
|
||||
const { courseName } = reduxHooks.useCardCourseData(cardId);
|
||||
const { isEmailEnabled, isExecEd2UCourse } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const { twitter, facebook } = reduxHooks.useCardSocialSettingsData(cardId);
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const { isMasquerading } = useContext(MasqueradeUserContext);
|
||||
|
||||
const handleTwitterShare = reduxHooks.useTrackCourseEvent(track.socialShare, cardId, 'twitter');
|
||||
const handleFacebookShare = reduxHooks.useTrackCourseEvent(track.socialShare, cardId, 'facebook');
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { when } from 'jest-when';
|
||||
import * as ReactShare from 'react-share';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
import { formatMessage, shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import track from 'tracking';
|
||||
@@ -20,8 +20,8 @@ jest.mock('tracking', () => ({
|
||||
socialShare: 'test-social-share-key',
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
...jest.requireActual('@edx/frontend-platform/i18n'),
|
||||
jest.mock('@openedx/frontend-base', () => ({
|
||||
...jest.requireActual('@openedx/frontend-base'),
|
||||
useIntl: jest.fn().mockReturnValue({
|
||||
formatMessage: jest.requireActual('@edx/react-unit-test-utils').formatMessage,
|
||||
}),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useKeyedState, StrictDict } from '@edx/react-unit-test-utils';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import track from '../../../../tracking';
|
||||
import { reduxHooks } from '../../../../hooks';
|
||||
|
||||
export const stateKeys = StrictDict({
|
||||
isUnenrollConfirmVisible: 'isUnenrollConfirmVisible',
|
||||
@@ -32,7 +32,9 @@ export const useHandleToggleDropdown = (cardId) => {
|
||||
cardId,
|
||||
);
|
||||
return (isOpen) => {
|
||||
if (isOpen) { trackCourseEvent(); }
|
||||
if (isOpen) {
|
||||
trackCourseEvent();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import React from 'react';
|
||||
import { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
import { Dropdown, Icon, IconButton } from '@openedx/paragon';
|
||||
import { MoreVert } from '@openedx/paragon/icons';
|
||||
import { StrictDict } from '@edx/react-unit-test-utils';
|
||||
|
||||
import EmailSettingsModal from 'containers/EmailSettingsModal';
|
||||
import UnenrollConfirmModal from 'containers/UnenrollConfirmModal';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import MasqueradeUserContext from '../../../../data/contexts/MasqueradeUserContext';
|
||||
import EmailSettingsModal from '../../../../containers/EmailSettingsModal';
|
||||
import UnenrollConfirmModal from '../../../../containers/UnenrollConfirmModal';
|
||||
import { reduxHooks } from '../../../../hooks';
|
||||
|
||||
import SocialShareMenu from './SocialShareMenu';
|
||||
import {
|
||||
useEmailSettings,
|
||||
@@ -30,7 +32,7 @@ export const CourseCardMenu = ({ cardId }) => {
|
||||
const unenrollModal = useUnenrollData();
|
||||
const handleToggleDropdown = useHandleToggleDropdown(cardId);
|
||||
const { shouldShowUnenrollItem, shouldShowDropdown } = useOptionVisibility(cardId);
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const { isMasquerading } = useContext(MasqueradeUserContext);
|
||||
const { isEmailEnabled } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
|
||||
if (!shouldShowDropdown) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { when } from 'jest-when';
|
||||
|
||||
import { Dropdown } from '@openedx/paragon';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import EmailSettingsModal from 'containers/EmailSettingsModal';
|
||||
import UnenrollConfirmModal from 'containers/UnenrollConfirmModal';
|
||||
@@ -11,8 +11,8 @@ import SocialShareMenu from './SocialShareMenu';
|
||||
import * as hooks from './hooks';
|
||||
import CourseCardMenu, { testIds } from '.';
|
||||
|
||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
...jest.requireActual('@edx/frontend-platform/i18n'),
|
||||
jest.mock('@openedx/frontend-base', () => ({
|
||||
...jest.requireActual('@openedx/frontend-base'),
|
||||
useIntl: jest.fn().mockReturnValue({
|
||||
formatMessage: jest.requireActual('@edx/react-unit-test-utils').formatMessage,
|
||||
}),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
import { defineMessages } from '@openedx/frontend-base';
|
||||
|
||||
const messages = defineMessages({
|
||||
unenroll: {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import track from '../../../tracking';
|
||||
import { reduxHooks } from '../../../hooks';
|
||||
import useActionDisabledState from './hooks';
|
||||
|
||||
const { courseTitleClicked } = track.course;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import { MockUseState } from 'testUtils';
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useContext } from 'react';
|
||||
|
||||
import MasqueradeUserContext from '../../../data/contexts/MasqueradeUserContext';
|
||||
import { reduxHooks } from '../../../hooks';
|
||||
|
||||
export const useActionDisabledState = (cardId) => {
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const { isMasquerading } = useContext(MasqueradeUserContext);
|
||||
const {
|
||||
hasAccess, isAudit, isAuditAccessExpired,
|
||||
} = reduxHooks.useCardEnrollmentData(cardId);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
import { useWindowSize, breakpoints } from '@openedx/paragon';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { reduxHooks } from '../../hooks';
|
||||
|
||||
export const useIsCollapsed = () => {
|
||||
const { width } = useWindowSize();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
import { defineMessages } from '@openedx/frontend-base';
|
||||
|
||||
const messages = defineMessages({
|
||||
bannerAlt: {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import { Button, Chip } from '@openedx/paragon';
|
||||
import { CloseSmall } from '@openedx/paragon/icons';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { reduxHooks } from '../../hooks';
|
||||
|
||||
import messages from './messages';
|
||||
import './index.scss';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import {
|
||||
Button,
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from '@openedx/paragon';
|
||||
import { Close, Tune } from '@openedx/paragon/icons';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { reduxHooks } from '../../hooks';
|
||||
|
||||
import FilterForm from './components/FilterForm';
|
||||
import SortForm from './components/SortForm';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import { Form } from '@openedx/paragon';
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import { FilterKeys } from 'data/constants/app';
|
||||
import { FilterKeys } from '../../../data/constants/app';
|
||||
|
||||
import { Form } from '@openedx/paragon';
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import { SortKeys } from 'data/constants/app';
|
||||
import { SortKeys } from '../../../data/constants/app';
|
||||
|
||||
import { Form } from '@openedx/paragon';
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import { useToggle } from '@openedx/paragon';
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { StrictDict } from '../../utils';
|
||||
import track from '../../tracking';
|
||||
import { reduxHooks } from '../../hooks';
|
||||
|
||||
import * as module from './hooks';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
import { defineMessages } from '@openedx/frontend-base';
|
||||
|
||||
const messages = defineMessages({
|
||||
courseStatus: {
|
||||
|
||||
@@ -4,8 +4,8 @@ import PropTypes from 'prop-types';
|
||||
import { Pagination } from '@openedx/paragon';
|
||||
import {
|
||||
ActiveCourseFilters,
|
||||
} from 'containers/CourseFilterControls';
|
||||
import CourseCard from 'containers/CourseCard';
|
||||
} from '../../../containers/CourseFilterControls';
|
||||
import CourseCard from '../../../containers/CourseCard';
|
||||
|
||||
import { useIsCollapsed } from './hooks';
|
||||
|
||||
|
||||
@@ -9,9 +9,11 @@ exports[`NoCoursesView snapshot 1`] = `
|
||||
alt="No Courses view banner"
|
||||
src="icon/mock/path"
|
||||
/>
|
||||
<h1>
|
||||
<h3
|
||||
className="h1"
|
||||
>
|
||||
Looking for a new challenge?
|
||||
</h1>
|
||||
</h3>
|
||||
<p>
|
||||
Explore our courses to add them to your dashboard.
|
||||
</p>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
import { Button, Image } from '@openedx/paragon';
|
||||
import { Search } from '@openedx/paragon/icons';
|
||||
import { baseAppUrl } from 'data/services/lms/urls';
|
||||
|
||||
import emptyCourseSVG from 'assets/empty-course.svg';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { baseAppUrl } from '../../../data/services/lms/urls';
|
||||
import emptyCourseSVG from '../../../assets/empty-course.svg';
|
||||
import { reduxHooks } from '../../../hooks';
|
||||
|
||||
import messages from './messages';
|
||||
import './index.scss';
|
||||
@@ -19,9 +19,9 @@ export const NoCoursesView = () => {
|
||||
className="d-flex align-items-center justify-content-center mb-4.5"
|
||||
>
|
||||
<Image src={emptyCourseSVG} alt={formatMessage(messages.bannerAlt)} />
|
||||
<h1>
|
||||
<h3 className="h1">
|
||||
{formatMessage(messages.lookingForChallengePrompt)}
|
||||
</h1>
|
||||
</h3>
|
||||
<p>
|
||||
{formatMessage(messages.exploreCoursesPrompt)}
|
||||
</p>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
import { defineMessages } from '@openedx/frontend-base';
|
||||
|
||||
const messages = defineMessages({
|
||||
lookingForChallengePrompt: {
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import React from 'react';
|
||||
|
||||
import queryString from 'query-string';
|
||||
|
||||
import { ListPageSize, SortKeys } from 'data/constants/app';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { StrictDict } from 'utils';
|
||||
import { ListPageSize, SortKeys } from '../../data/constants/app';
|
||||
import { reduxHooks } from '../../hooks';
|
||||
import { StrictDict } from '../../utils';
|
||||
|
||||
import * as module from './hooks';
|
||||
|
||||
@@ -27,12 +25,13 @@ export const useCourseListData = () => {
|
||||
|
||||
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({
|
||||
sortBy,
|
||||
filters,
|
||||
pageSize: querySearch?.disable_pagination === 1 ? 0 : ListPageSize,
|
||||
pageSize: Number(disablePagination) === 1 ? 0 : ListPageSize,
|
||||
});
|
||||
|
||||
const handleRemoveFilter = (filter) => () => removeFilter(filter);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import queryString from 'query-string';
|
||||
|
||||
import { MockUseState } from 'testUtils';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { ListPageSize, SortKeys } from 'data/constants/app';
|
||||
@@ -15,8 +13,10 @@ jest.mock('hooks', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('query-string', () => ({
|
||||
parse: jest.fn(() => ({})),
|
||||
const mockGet = jest.fn(() => ({}));
|
||||
|
||||
global.URLSearchParams = jest.fn().mockImplementation(() => ({
|
||||
get: mockGet,
|
||||
}));
|
||||
|
||||
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', () => {
|
||||
state.mock();
|
||||
state.mockVal(state.keys.sortBy, testSortBy);
|
||||
queryString.parse.mockReturnValueOnce({ disable_pagination: 1 });
|
||||
mockGet.mockReturnValueOnce('1');
|
||||
out = hooks.useCourseListData();
|
||||
expect(reduxHooks.useCurrentCourseList).toHaveBeenCalledWith({
|
||||
sortBy: testSortBy,
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { reduxHooks } from '../../hooks';
|
||||
import {
|
||||
CourseFilterControls,
|
||||
} from 'containers/CourseFilterControls';
|
||||
import CourseListSlot from 'plugin-slots/CourseListSlot';
|
||||
import NoCoursesViewSlot from 'plugin-slots/NoCoursesViewSlot';
|
||||
} from '../../containers/CourseFilterControls';
|
||||
import CourseListSlot from '../../slots/CourseListSlot';
|
||||
import NoCoursesViewSlot from '../../slots/NoCoursesViewSlot';
|
||||
|
||||
import { useCourseListData } from './hooks';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
import { defineMessages } from '@openedx/frontend-base';
|
||||
|
||||
const messages = defineMessages({
|
||||
myCourses: {
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { Container, Col, Row } from '@openedx/paragon';
|
||||
|
||||
import WidgetSidebarSlot from 'plugin-slots/WidgetSidebarSlot';
|
||||
import WidgetSidebarSlot from '../../slots/WidgetSidebarSlot';
|
||||
|
||||
import hooks from './hooks';
|
||||
|
||||
@@ -40,8 +40,7 @@ export const DashboardLayout = ({ children }) => {
|
||||
<Col {...courseListColumnProps} className="course-list-column">
|
||||
{children}
|
||||
</Col>
|
||||
<Col {...columnConfig.sidebar} className="sidebar-column">
|
||||
{!isCollapsed && (<h2 className="course-list-title"> </h2>)}
|
||||
<Col {...columnConfig.sidebar} className={['sidebar-column', !isCollapsed && 'not-collapsed']}>
|
||||
<WidgetSidebarSlot />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user