Compare commits

...

15 Commits

Author SHA1 Message Date
Adolfo R. Brandes
6f4bf0a13e 1.0.0-alpha.1 2025-06-28 19:39:46 +02:00
Adolfo R. Brandes
6bed0308bd chore: bump frontend-base 2025-06-28 19:39:32 +02:00
Adolfo R. Brandes
057b925589 feat: Prepare for publication to NPM (#673) 2025-06-28 14:35:06 -03:00
Adolfo R. Brandes
6202f7bb54 feat: handle dashboard role 2025-06-28 19:19:15 +02:00
Adolfo R. Brandes
d0c27f4377 fix: dev site title 2025-06-26 22:13:27 -03:00
Adolfo R. Brandes
b2c6ec2dc9 chore: clean up gitignore 2025-06-26 22:10:19 -03:00
Adolfo R. Brandes
8175d7e2f6 chore: clean up npmignore 2025-06-26 21:33:37 -03:00
Adolfo R. Brandes
d0051d0a7d chore: prepare for publication
Update package.json for publication as a "buildless" library.
2025-06-26 21:16:12 -03:00
Adolfo R. Brandes
c621d581bd test: remove unused setupTest line 2025-06-26 21:14:33 -03:00
Adolfo R. Brandes
268ccc864d fix: a couple of appId imports 2025-06-26 20:00:06 -03:00
Adolfo R. Brandes
2045854099 fix: make pull_translations 2025-06-26 19:57:24 -03:00
Adolfo R. Brandes
9b439d7d74 fix: i18n message export 2025-06-26 19:55:17 -03:00
Adolfo R. Brandes
b8f4d49a55 fix: test environment selection 2025-06-26 19:54:54 -03:00
Adolfo R. Brandes
1d29810f6c refactor: remove/update dotfiles 2025-06-25 17:05:16 -03:00
Adolfo R. Brandes
89559a4987 refactor: migrate to frontend-base
BREAKING CHANGE: refactors the MFE for frontend-base.
2025-06-24 15:31:23 -03:00
256 changed files with 8698 additions and 6200 deletions

View File

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

44
.env
View File

@@ -1,44 +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
NON_BROWSABLE_COURSES=false

View File

@@ -1,50 +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
NON_BROWSABLE_COURSES=false

View File

@@ -1,49 +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
NON_BROWSABLE_COURSES=false

View File

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

View File

@@ -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
View File

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

32
.gitignore vendored
View File

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

View File

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

View File

@@ -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:

View File

@@ -18,7 +18,7 @@ frontend-app-learner-dashboard
The Learner Home app is a microfrontend (MFE) course listing experience for the Open edX Learning Management System
(LMS). This experience was designed to provide a clean and functional interface to allow learners to view all of their
open enrollments, as well as take relevant actions on those enrollments. It also serves as host to a number of exposed
"widget" containers to provide upsell and discovery widgets as sidebar/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
View 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
View File

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

22
eslint.config.js Normal file
View 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__/*',
],
},
);

View File

@@ -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,
};

View File

@@ -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',
});

10259
package-lock.json generated

File diff suppressed because it is too large Load Diff

80
package.json Executable file → Normal file
View File

@@ -1,79 +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/brand": "npm:@openedx/brand-openedx@^1.2.3",
"@edx/openedx-atlas": "^0.7.0",
"@edx/react-unit-test-utils": "^4.0.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.42.0",
"filesize": "^10.0.0",
"font-awesome": "4.7.0",
"history": "5.3.0",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"prop-types": "15.8.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-helmet": "^6.1.0",
"react-intl": "6.8.9",
"react-redux": "^7.2.4",
"react-router-dom": "6.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",
@@ -82,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"
}
}

View File

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

View File

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

41
site.config.dev.tsx Normal file
View 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
View 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;

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -1,149 +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].el;
expect(dashboard.type).toEqual('Dashboard');
expect(dashboard).toEqual(shallow(<Dashboard />));
});
});
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].el;
expect(dashboard.type).toEqual('Dashboard');
expect(dashboard).toEqual(shallow(<Dashboard />));
});
});
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].el;
expect(dashboard.type).toEqual('Dashboard');
expect(dashboard).toEqual(shallow(<Dashboard />));
});
});
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
View 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
View File

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

1
src/__mocks__/svg.js Normal file
View File

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

View File

@@ -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>
`;

View File

@@ -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
View 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
View 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;

View File

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

View File

@@ -1,25 +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 getNotices = ({ onLoad, notFoundMessage }) => {
const authenticatedUser = getAuthenticatedUser();
const handleError = async (e) => {
// Error probably means that notices is not installed, which is fine.
const { customAttributes: { httpErrorStatus } } = e;
if (httpErrorStatus === 404) {
logInfo(`${e}. ${notFoundMessage}`);
} else {
logError(e);
}
};
if (authenticatedUser) {
return getAuthenticatedHttpClient().get(noticesUrl, {}).then(onLoad).catch(handleError);
}
return null;
};
export default { getNotices };

View File

@@ -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);
});
});
});
});

View File

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

View File

@@ -1,89 +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() }));
const mockFormatMessage = jest.fn(message => message.defaultMessage || 'translated-string');
jest.mock('react-intl', () => ({
useIntl: () => ({
formatMessage: mockFormatMessage,
}),
}));
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, mockFormatMessage]);
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, mockFormatMessage]);
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, mockFormatMessage]);
cb();
expect(getNotices).toHaveBeenCalled();
const { onLoad } = getNotices.mock.calls[0][0];
const target = 'url-target';
onLoad({ data: { results: [target] } });
expect(state.setState.isRedirected).toHaveBeenCalledWith(true);
expect(window.location.replace).toHaveBeenCalledWith(
`${target}?next=${window.location.href}`,
);
});
});
});
});
describe('output', () => {
it('forwards isRedirected from state call', () => {
hook = hooks.useNoticesWrapperData();
expect(hook.isRedirected).toEqual(state.stateVals.isRedirected);
});
});
});
});

View File

@@ -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;

View File

@@ -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);
});
});
});

View File

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

View File

@@ -1,27 +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',
NON_BROWSABLE_COURSES: process.env.NON_BROWSABLE_COURSES === 'true',
};
const features = {};
export { configuration, features };

1
src/constants.ts Normal file
View File

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

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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');

View File

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

View File

@@ -4,10 +4,10 @@ import PropTypes from 'prop-types';
import { MailtoLink, Hyperlink } from '@openedx/paragon';
import { CheckCircle } from '@openedx/paragon/icons';
import { useIntl } from '@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>

View File

@@ -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 }) => {

View File

@@ -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;

View File

@@ -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';

View File

@@ -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.',
},
});

View File

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

View File

@@ -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';

View File

@@ -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={{

View File

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

View File

@@ -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';

View File

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

View File

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

View File

@@ -1,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',
},
});

View File

@@ -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 }) => {

View File

@@ -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';

View File

@@ -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');

View File

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

View File

@@ -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';

View File

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

View File

@@ -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 && !courseRun.advertisedStart) { return null; }
const startDate = courseRun.advertisedStart ? courseRun.advertisedStart : 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,

View File

@@ -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';

View File

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

View File

@@ -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';

View File

@@ -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');

View File

@@ -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,
}),

View File

@@ -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();
}
};
};

View File

@@ -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) {

View File

@@ -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,
}),

View File

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

View File

@@ -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;

View File

@@ -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';

View File

@@ -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';

View File

@@ -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);

View File

@@ -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();

View File

@@ -1,4 +1,4 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import { useIntl } from '@openedx/frontend-base';
import { reduxHooks } from 'hooks';

View File

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

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

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

View File

@@ -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';

View File

@@ -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';

View File

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

View File

@@ -1,8 +1,8 @@
import React from 'react';
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';

View File

@@ -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';
/**

View File

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

View File

@@ -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';

View File

@@ -1,26 +1,19 @@
import React from 'react';
import { useWindowSize, breakpoints } from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import { apiHooks } from 'hooks';
import { StrictDict } from 'utils';
import appMessages from 'messages';
import { useIntl } from '@openedx/frontend-base';
import { StrictDict } from '../../utils';
import appMessages from '../../messages';
import * as module from './hooks';
export const state = StrictDict({
sidebarShowing: (val) => React.useState(val), // eslint-disable-line
});
export const useInitializeDashboard = () => {
const initialize = apiHooks.useInitializeApp();
React.useEffect(() => { initialize(); }, []); // eslint-disable-line
};
export const useDashboardMessages = () => {
const { formatMessage } = useIntl();
return {
spinnerScreenReaderText: formatMessage(appMessages.loadingSR),
pageTitle: formatMessage(appMessages.pageTitle),
spinnerScreenReaderText: formatMessage(appMessages['learner-dash.loadingSR']),
pageTitle: formatMessage(appMessages['learner-dash.title']),
};
};
@@ -37,6 +30,5 @@ export const useDashboardLayoutData = () => {
export default {
useDashboardLayoutData,
useInitializeDashboard,
useDashboardMessages,
};

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