Compare commits
50 Commits
REV1512fly
...
AA-613
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e2f495f52 | ||
|
|
50e649daa3 | ||
|
|
629382f719 | ||
|
|
8835a9cd6a | ||
|
|
3e2eebdd9b | ||
|
|
acd2cc3222 | ||
|
|
9ef3787d4b | ||
|
|
47fd6bfe18 | ||
|
|
984010a8ec | ||
|
|
58543a34b3 | ||
|
|
293dc9f4c3 | ||
|
|
68c8d31dd1 | ||
|
|
96ef87886f | ||
|
|
48e3f43062 | ||
|
|
d74557d681 | ||
|
|
0e18e0908a | ||
|
|
958c13ca93 | ||
|
|
26de2cebeb | ||
|
|
bd8496a5e2 | ||
|
|
aa56239f54 | ||
|
|
a0b85111eb | ||
|
|
07b252ecc6 | ||
|
|
92b364e0f8 | ||
|
|
9247eb3098 | ||
|
|
9e0f5d7e22 | ||
|
|
db4b4d18cc | ||
|
|
92a464b2da | ||
|
|
7fed8db02a | ||
|
|
8161f4d9a0 | ||
|
|
c6661e71b1 | ||
|
|
6dc7ff761a | ||
|
|
de4a7d9f34 | ||
|
|
5f239583fd | ||
|
|
aff49aa8a9 | ||
|
|
6e10bffd40 | ||
|
|
37a4dcce18 | ||
|
|
c16da21602 | ||
|
|
55ea84f9a6 | ||
|
|
4341a828db | ||
|
|
255e36baa8 | ||
|
|
2fc4c8c153 | ||
|
|
79f5c7fcf4 | ||
|
|
743621ff51 | ||
|
|
9272498c9e | ||
|
|
e89aef78b5 | ||
|
|
3d41c56a0a | ||
|
|
e4060b7481 | ||
|
|
4e92053151 | ||
|
|
38700499d4 | ||
|
|
0e3fc032ab |
1
.env
1
.env
@@ -17,6 +17,7 @@ FAVICON_URL=null
|
||||
MARKETING_SITE_BASE_URL=null
|
||||
ORDER_HISTORY_URL=null
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT=null
|
||||
SEARCH_CATALOG_URL=null
|
||||
SEGMENT_KEY=null
|
||||
SITE_NAME=null
|
||||
SOCIAL_UTM_MILESTONE_CAMPAIGN=null
|
||||
|
||||
@@ -17,6 +17,7 @@ MARKETING_SITE_BASE_URL='http://localhost:18000'
|
||||
ORDER_HISTORY_URL='http://localhost:1996/orders'
|
||||
PORT=2000
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
SEARCH_CATALOG_URL='http://localhost:18000/courses'
|
||||
SEGMENT_KEY=null
|
||||
SITE_NAME='edX'
|
||||
SOCIAL_UTM_MILESTONE_CAMPAIGN='edxmilestone'
|
||||
|
||||
@@ -17,6 +17,7 @@ MARKETING_SITE_BASE_URL='http://localhost:18000'
|
||||
ORDER_HISTORY_URL='http://localhost:1996/orders'
|
||||
PORT=2000
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
SEARCH_CATALOG_URL='http://localhost:18000/courses'
|
||||
SEGMENT_KEY=null
|
||||
SITE_NAME='edX'
|
||||
SOCIAL_UTM_MILESTONE_CAMPAIGN='edxmilestone'
|
||||
|
||||
@@ -21,6 +21,31 @@ For example, they discourage using class or element name selectors to find compo
|
||||
during a test. Instead, you should find them by user-oriented attributes like labels,
|
||||
text, or roles. As a last resort, by a `data-testid` tag.
|
||||
|
||||
## Mocking data
|
||||
We'll use [Rosie](https://github.com/rosiejs/rosie) as a tool for building JavaScript objects.
|
||||
Our main use case for Rosie is to use factories in order to mock the data we'd like to fetch when rendering components.
|
||||
[axios-mock-adapter](https://www.npmjs.com/package/axios-mock-adapter) allows us to mock the response of an HTTP request.
|
||||
|
||||
For example, we may use a factory to build a course metadata object:
|
||||
|
||||
`const courseMetadata = Factory.build('courseMetadata');`
|
||||
|
||||
Then we'd pass that `courseMetadata` object into an axios mock call:
|
||||
|
||||
`axiosMock.onGet('example.com').reply(200, courseMetadata);`
|
||||
|
||||
This way, when a component sends a GET request to `example.com` within the test's lifecycle, the request will be intercepted
|
||||
by the axios-mock-adapter, and the courseMetadata object will be returned.
|
||||
|
||||
These factories should live within the data directories they intend to mock
|
||||
```
|
||||
courseware
|
||||
| data
|
||||
| __factories__
|
||||
| courseMetadata.factory.js /* used to define the Rosie factory */
|
||||
| api.js /* getCourseMetadata() lives here */
|
||||
```
|
||||
|
||||
## What to Test
|
||||
We have not found exhaustive unit testing of frontend code to be worth the trouble.
|
||||
Rather, let's focus on testing non-obvious behavior.
|
||||
|
||||
1372
package-lock.json
generated
1372
package-lock.json
generated
@@ -111,12 +111,31 @@
|
||||
}
|
||||
},
|
||||
"@babel/helper-annotate-as-pure": {
|
||||
"version": "7.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz",
|
||||
"integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==",
|
||||
"version": "7.12.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.10.tgz",
|
||||
"integrity": "sha512-XplmVbC1n+KY6jL8/fgLVXXUauDIB+lD5+GsQEh6F6GBF1dq1qy4DP4yXWzDKcoqXB3X58t61e85Fitoww4JVQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.10.4"
|
||||
"@babel/types": "^7.12.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz",
|
||||
"integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.11.tgz",
|
||||
"integrity": "sha512-ukA9SQtKThINm++CX1CwmliMrE54J6nIYB5XTwL5f/CLFW9owfls+YSU8tVW15RQ2w+a3fSbPjC6HdQNtWZkiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.12.11",
|
||||
"lodash": "^4.17.19",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@babel/helper-builder-binary-assignment-operator-visitor": {
|
||||
@@ -140,14 +159,14 @@
|
||||
}
|
||||
},
|
||||
"@babel/helper-builder-react-jsx-experimental": {
|
||||
"version": "7.12.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.12.4.tgz",
|
||||
"integrity": "sha512-AjEa0jrQqNk7eDQOo0pTfUOwQBMF+xVqrausQwT9/rTKy0g04ggFNaJpaE09IQMn9yExluigWMJcj0WC7bq+Og==",
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.12.11.tgz",
|
||||
"integrity": "sha512-4oGVOekPI8dh9JphkPXC68iIuP6qp/RPbaPmorRmEFbRAHZjSqxPjqHudn18GVDPgCuFM/KdFXc63C17Ygfa9w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-annotate-as-pure": "^7.10.4",
|
||||
"@babel/helper-module-imports": "^7.12.1",
|
||||
"@babel/types": "^7.12.1"
|
||||
"@babel/helper-annotate-as-pure": "^7.12.10",
|
||||
"@babel/helper-module-imports": "^7.12.5",
|
||||
"@babel/types": "^7.12.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": {
|
||||
@@ -159,13 +178,19 @@
|
||||
"@babel/types": "^7.12.5"
|
||||
}
|
||||
},
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz",
|
||||
"integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz",
|
||||
"integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==",
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.11.tgz",
|
||||
"integrity": "sha512-ukA9SQtKThINm++CX1CwmliMrE54J6nIYB5XTwL5f/CLFW9owfls+YSU8tVW15RQ2w+a3fSbPjC6HdQNtWZkiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.10.4",
|
||||
"@babel/helper-validator-identifier": "^7.12.11",
|
||||
"lodash": "^4.17.19",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
@@ -198,12 +223,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/generator": {
|
||||
"version": "7.12.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.5.tgz",
|
||||
"integrity": "sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A==",
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.11.tgz",
|
||||
"integrity": "sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.12.5",
|
||||
"@babel/types": "^7.12.11",
|
||||
"jsesc": "^2.5.1",
|
||||
"source-map": "^0.5.0"
|
||||
}
|
||||
@@ -218,47 +243,64 @@
|
||||
}
|
||||
},
|
||||
"@babel/helper-replace-supers": {
|
||||
"version": "7.12.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.5.tgz",
|
||||
"integrity": "sha512-5YILoed0ZyIpF4gKcpZitEnXEJ9UoDRki1Ey6xz46rxOzfNMAhVIJMoune1hmPVxh40LRv1+oafz7UsWX+vyWA==",
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.11.tgz",
|
||||
"integrity": "sha512-q+w1cqmhL7R0FNzth/PLLp2N+scXEK/L2AHbXUyydxp828F4FEa5WcVoqui9vFRiHDQErj9Zof8azP32uGVTRA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-member-expression-to-functions": "^7.12.1",
|
||||
"@babel/helper-optimise-call-expression": "^7.10.4",
|
||||
"@babel/traverse": "^7.12.5",
|
||||
"@babel/types": "^7.12.5"
|
||||
"@babel/helper-member-expression-to-functions": "^7.12.7",
|
||||
"@babel/helper-optimise-call-expression": "^7.12.10",
|
||||
"@babel/traverse": "^7.12.10",
|
||||
"@babel/types": "^7.12.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/helper-optimise-call-expression": {
|
||||
"version": "7.12.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.10.tgz",
|
||||
"integrity": "sha512-4tpbU0SrSTjjt65UMWSrUOPZTsgvPgGG4S8QSTNHacKzpS51IVWGDj0yCwyeZND/i+LSN2g/O63jEXEWm49sYQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.12.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz",
|
||||
"integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.7.tgz",
|
||||
"integrity": "sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg==",
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.11.tgz",
|
||||
"integrity": "sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/traverse": {
|
||||
"version": "7.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.7.tgz",
|
||||
"integrity": "sha512-nMWaqsQEeSvMNypswUDzjqQ+0rR6pqCtoQpsqGJC4/Khm9cISwPTSpai57F6/jDaOoEGz8yE/WxcO3PV6tKSmQ==",
|
||||
"version": "7.12.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.10.tgz",
|
||||
"integrity": "sha512-6aEtf0IeRgbYWzta29lePeYSk+YAFIC3kyqESeft8o5CkFlYIMX+EQDDWEiAQ9LHOA3d0oHdgrSsID/CKqXJlg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/generator": "^7.12.5",
|
||||
"@babel/generator": "^7.12.10",
|
||||
"@babel/helper-function-name": "^7.10.4",
|
||||
"@babel/helper-split-export-declaration": "^7.11.0",
|
||||
"@babel/parser": "^7.12.7",
|
||||
"@babel/types": "^7.12.7",
|
||||
"@babel/parser": "^7.12.10",
|
||||
"@babel/types": "^7.12.10",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0",
|
||||
"lodash": "^4.17.19"
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz",
|
||||
"integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==",
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.11.tgz",
|
||||
"integrity": "sha512-ukA9SQtKThINm++CX1CwmliMrE54J6nIYB5XTwL5f/CLFW9owfls+YSU8tVW15RQ2w+a3fSbPjC6HdQNtWZkiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.10.4",
|
||||
"@babel/helper-validator-identifier": "^7.12.11",
|
||||
"lodash": "^4.17.19",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
@@ -316,13 +358,19 @@
|
||||
"@babel/types": "^7.12.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz",
|
||||
"integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz",
|
||||
"integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==",
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.11.tgz",
|
||||
"integrity": "sha512-ukA9SQtKThINm++CX1CwmliMrE54J6nIYB5XTwL5f/CLFW9owfls+YSU8tVW15RQ2w+a3fSbPjC6HdQNtWZkiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.10.4",
|
||||
"@babel/helper-validator-identifier": "^7.12.11",
|
||||
"lodash": "^4.17.19",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
@@ -417,13 +465,19 @@
|
||||
"@babel/types": "^7.12.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz",
|
||||
"integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz",
|
||||
"integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==",
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.11.tgz",
|
||||
"integrity": "sha512-ukA9SQtKThINm++CX1CwmliMrE54J6nIYB5XTwL5f/CLFW9owfls+YSU8tVW15RQ2w+a3fSbPjC6HdQNtWZkiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.10.4",
|
||||
"@babel/helper-validator-identifier": "^7.12.11",
|
||||
"lodash": "^4.17.19",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
@@ -461,13 +515,19 @@
|
||||
"@babel/types": "^7.12.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz",
|
||||
"integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz",
|
||||
"integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==",
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.11.tgz",
|
||||
"integrity": "sha512-ukA9SQtKThINm++CX1CwmliMrE54J6nIYB5XTwL5f/CLFW9owfls+YSU8tVW15RQ2w+a3fSbPjC6HdQNtWZkiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.10.4",
|
||||
"@babel/helper-validator-identifier": "^7.12.11",
|
||||
"lodash": "^4.17.19",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
@@ -490,9 +550,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@babel/helper-validator-option": {
|
||||
"version": "7.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.1.tgz",
|
||||
"integrity": "sha512-YpJabsXlJVWP0USHjnC/AQDTLlZERbON577YUVO/wLpqyj6HAtVYnWaQaN0iUN+1/tWn3c+uKKXjRut5115Y2A==",
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.11.tgz",
|
||||
"integrity": "sha512-TBFCyj939mFSdeX7U7DDj32WtzYY7fDcalgq8v3fBZMNOJQNn7nOYzMaUCiPxPYfCup69mtIpqlKgMZLvQ8Xhw==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/helper-wrap-function": {
|
||||
@@ -803,13 +863,19 @@
|
||||
"@babel/types": "^7.12.5"
|
||||
}
|
||||
},
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz",
|
||||
"integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz",
|
||||
"integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==",
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.11.tgz",
|
||||
"integrity": "sha512-ukA9SQtKThINm++CX1CwmliMrE54J6nIYB5XTwL5f/CLFW9owfls+YSU8tVW15RQ2w+a3fSbPjC6HdQNtWZkiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.10.4",
|
||||
"@babel/helper-validator-identifier": "^7.12.11",
|
||||
"lodash": "^4.17.19",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
@@ -826,9 +892,9 @@
|
||||
}
|
||||
},
|
||||
"@babel/plugin-transform-block-scoping": {
|
||||
"version": "7.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.12.1.tgz",
|
||||
"integrity": "sha512-zJyAC9sZdE60r1nVQHblcfCj29Dh2Y0DOvlMkcqSo0ckqjiCwNiUezUKw+RjOCwGfpLRwnAeQ2XlLpsnGkvv9w==",
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.12.11.tgz",
|
||||
"integrity": "sha512-atR1Rxc3hM+VPg/NvNvfYw0npQEAcHuJ+MGZnFn6h3bo+1U3BWXMdFMlvVRApBTWKQMX7SOwRJZA5FBF/JQbvA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-plugin-utils": "^7.10.4"
|
||||
@@ -851,12 +917,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/generator": {
|
||||
"version": "7.12.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.5.tgz",
|
||||
"integrity": "sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A==",
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.11.tgz",
|
||||
"integrity": "sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.12.5",
|
||||
"@babel/types": "^7.12.11",
|
||||
"jsesc": "^2.5.1",
|
||||
"source-map": "^0.5.0"
|
||||
}
|
||||
@@ -871,47 +937,64 @@
|
||||
}
|
||||
},
|
||||
"@babel/helper-replace-supers": {
|
||||
"version": "7.12.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.5.tgz",
|
||||
"integrity": "sha512-5YILoed0ZyIpF4gKcpZitEnXEJ9UoDRki1Ey6xz46rxOzfNMAhVIJMoune1hmPVxh40LRv1+oafz7UsWX+vyWA==",
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.11.tgz",
|
||||
"integrity": "sha512-q+w1cqmhL7R0FNzth/PLLp2N+scXEK/L2AHbXUyydxp828F4FEa5WcVoqui9vFRiHDQErj9Zof8azP32uGVTRA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-member-expression-to-functions": "^7.12.1",
|
||||
"@babel/helper-optimise-call-expression": "^7.10.4",
|
||||
"@babel/traverse": "^7.12.5",
|
||||
"@babel/types": "^7.12.5"
|
||||
"@babel/helper-member-expression-to-functions": "^7.12.7",
|
||||
"@babel/helper-optimise-call-expression": "^7.12.10",
|
||||
"@babel/traverse": "^7.12.10",
|
||||
"@babel/types": "^7.12.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/helper-optimise-call-expression": {
|
||||
"version": "7.12.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.10.tgz",
|
||||
"integrity": "sha512-4tpbU0SrSTjjt65UMWSrUOPZTsgvPgGG4S8QSTNHacKzpS51IVWGDj0yCwyeZND/i+LSN2g/O63jEXEWm49sYQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.12.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz",
|
||||
"integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.7.tgz",
|
||||
"integrity": "sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg==",
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.11.tgz",
|
||||
"integrity": "sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/traverse": {
|
||||
"version": "7.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.7.tgz",
|
||||
"integrity": "sha512-nMWaqsQEeSvMNypswUDzjqQ+0rR6pqCtoQpsqGJC4/Khm9cISwPTSpai57F6/jDaOoEGz8yE/WxcO3PV6tKSmQ==",
|
||||
"version": "7.12.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.10.tgz",
|
||||
"integrity": "sha512-6aEtf0IeRgbYWzta29lePeYSk+YAFIC3kyqESeft8o5CkFlYIMX+EQDDWEiAQ9LHOA3d0oHdgrSsID/CKqXJlg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/generator": "^7.12.5",
|
||||
"@babel/generator": "^7.12.10",
|
||||
"@babel/helper-function-name": "^7.10.4",
|
||||
"@babel/helper-split-export-declaration": "^7.11.0",
|
||||
"@babel/parser": "^7.12.7",
|
||||
"@babel/types": "^7.12.7",
|
||||
"@babel/parser": "^7.12.10",
|
||||
"@babel/types": "^7.12.10",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0",
|
||||
"lodash": "^4.17.19"
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz",
|
||||
"integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==",
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.11.tgz",
|
||||
"integrity": "sha512-ukA9SQtKThINm++CX1CwmliMrE54J6nIYB5XTwL5f/CLFW9owfls+YSU8tVW15RQ2w+a3fSbPjC6HdQNtWZkiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.10.4",
|
||||
"@babel/helper-validator-identifier": "^7.12.11",
|
||||
"lodash": "^4.17.19",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
@@ -1035,12 +1118,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/generator": {
|
||||
"version": "7.12.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.5.tgz",
|
||||
"integrity": "sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A==",
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.11.tgz",
|
||||
"integrity": "sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.12.5",
|
||||
"@babel/types": "^7.12.11",
|
||||
"jsesc": "^2.5.1",
|
||||
"source-map": "^0.5.0"
|
||||
}
|
||||
@@ -1080,16 +1163,25 @@
|
||||
"lodash": "^4.17.19"
|
||||
}
|
||||
},
|
||||
"@babel/helper-replace-supers": {
|
||||
"version": "7.12.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.5.tgz",
|
||||
"integrity": "sha512-5YILoed0ZyIpF4gKcpZitEnXEJ9UoDRki1Ey6xz46rxOzfNMAhVIJMoune1hmPVxh40LRv1+oafz7UsWX+vyWA==",
|
||||
"@babel/helper-optimise-call-expression": {
|
||||
"version": "7.12.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.10.tgz",
|
||||
"integrity": "sha512-4tpbU0SrSTjjt65UMWSrUOPZTsgvPgGG4S8QSTNHacKzpS51IVWGDj0yCwyeZND/i+LSN2g/O63jEXEWm49sYQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-member-expression-to-functions": "^7.12.1",
|
||||
"@babel/helper-optimise-call-expression": "^7.10.4",
|
||||
"@babel/traverse": "^7.12.5",
|
||||
"@babel/types": "^7.12.5"
|
||||
"@babel/types": "^7.12.10"
|
||||
}
|
||||
},
|
||||
"@babel/helper-replace-supers": {
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.11.tgz",
|
||||
"integrity": "sha512-q+w1cqmhL7R0FNzth/PLLp2N+scXEK/L2AHbXUyydxp828F4FEa5WcVoqui9vFRiHDQErj9Zof8azP32uGVTRA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-member-expression-to-functions": "^7.12.7",
|
||||
"@babel/helper-optimise-call-expression": "^7.12.10",
|
||||
"@babel/traverse": "^7.12.10",
|
||||
"@babel/types": "^7.12.11"
|
||||
}
|
||||
},
|
||||
"@babel/helper-simple-access": {
|
||||
@@ -1102,37 +1194,45 @@
|
||||
}
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.7.tgz",
|
||||
"integrity": "sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg==",
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.11.tgz",
|
||||
"integrity": "sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/traverse": {
|
||||
"version": "7.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.7.tgz",
|
||||
"integrity": "sha512-nMWaqsQEeSvMNypswUDzjqQ+0rR6pqCtoQpsqGJC4/Khm9cISwPTSpai57F6/jDaOoEGz8yE/WxcO3PV6tKSmQ==",
|
||||
"version": "7.12.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.10.tgz",
|
||||
"integrity": "sha512-6aEtf0IeRgbYWzta29lePeYSk+YAFIC3kyqESeft8o5CkFlYIMX+EQDDWEiAQ9LHOA3d0oHdgrSsID/CKqXJlg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/generator": "^7.12.5",
|
||||
"@babel/generator": "^7.12.10",
|
||||
"@babel/helper-function-name": "^7.10.4",
|
||||
"@babel/helper-split-export-declaration": "^7.11.0",
|
||||
"@babel/parser": "^7.12.7",
|
||||
"@babel/types": "^7.12.7",
|
||||
"@babel/parser": "^7.12.10",
|
||||
"@babel/types": "^7.12.10",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0",
|
||||
"lodash": "^4.17.19"
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz",
|
||||
"integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==",
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.11.tgz",
|
||||
"integrity": "sha512-ukA9SQtKThINm++CX1CwmliMrE54J6nIYB5XTwL5f/CLFW9owfls+YSU8tVW15RQ2w+a3fSbPjC6HdQNtWZkiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.10.4",
|
||||
"@babel/helper-validator-identifier": "^7.12.11",
|
||||
"lodash": "^4.17.19",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz",
|
||||
"integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
@@ -1171,12 +1271,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/generator": {
|
||||
"version": "7.12.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.5.tgz",
|
||||
"integrity": "sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A==",
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.11.tgz",
|
||||
"integrity": "sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.12.5",
|
||||
"@babel/types": "^7.12.11",
|
||||
"jsesc": "^2.5.1",
|
||||
"source-map": "^0.5.0"
|
||||
}
|
||||
@@ -1216,16 +1316,25 @@
|
||||
"lodash": "^4.17.19"
|
||||
}
|
||||
},
|
||||
"@babel/helper-replace-supers": {
|
||||
"version": "7.12.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.5.tgz",
|
||||
"integrity": "sha512-5YILoed0ZyIpF4gKcpZitEnXEJ9UoDRki1Ey6xz46rxOzfNMAhVIJMoune1hmPVxh40LRv1+oafz7UsWX+vyWA==",
|
||||
"@babel/helper-optimise-call-expression": {
|
||||
"version": "7.12.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.10.tgz",
|
||||
"integrity": "sha512-4tpbU0SrSTjjt65UMWSrUOPZTsgvPgGG4S8QSTNHacKzpS51IVWGDj0yCwyeZND/i+LSN2g/O63jEXEWm49sYQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-member-expression-to-functions": "^7.12.1",
|
||||
"@babel/helper-optimise-call-expression": "^7.10.4",
|
||||
"@babel/traverse": "^7.12.5",
|
||||
"@babel/types": "^7.12.5"
|
||||
"@babel/types": "^7.12.10"
|
||||
}
|
||||
},
|
||||
"@babel/helper-replace-supers": {
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.11.tgz",
|
||||
"integrity": "sha512-q+w1cqmhL7R0FNzth/PLLp2N+scXEK/L2AHbXUyydxp828F4FEa5WcVoqui9vFRiHDQErj9Zof8azP32uGVTRA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-member-expression-to-functions": "^7.12.7",
|
||||
"@babel/helper-optimise-call-expression": "^7.12.10",
|
||||
"@babel/traverse": "^7.12.10",
|
||||
"@babel/types": "^7.12.11"
|
||||
}
|
||||
},
|
||||
"@babel/helper-simple-access": {
|
||||
@@ -1238,37 +1347,45 @@
|
||||
}
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.7.tgz",
|
||||
"integrity": "sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg==",
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.11.tgz",
|
||||
"integrity": "sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/traverse": {
|
||||
"version": "7.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.7.tgz",
|
||||
"integrity": "sha512-nMWaqsQEeSvMNypswUDzjqQ+0rR6pqCtoQpsqGJC4/Khm9cISwPTSpai57F6/jDaOoEGz8yE/WxcO3PV6tKSmQ==",
|
||||
"version": "7.12.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.10.tgz",
|
||||
"integrity": "sha512-6aEtf0IeRgbYWzta29lePeYSk+YAFIC3kyqESeft8o5CkFlYIMX+EQDDWEiAQ9LHOA3d0oHdgrSsID/CKqXJlg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/generator": "^7.12.5",
|
||||
"@babel/generator": "^7.12.10",
|
||||
"@babel/helper-function-name": "^7.10.4",
|
||||
"@babel/helper-split-export-declaration": "^7.11.0",
|
||||
"@babel/parser": "^7.12.7",
|
||||
"@babel/types": "^7.12.7",
|
||||
"@babel/parser": "^7.12.10",
|
||||
"@babel/types": "^7.12.10",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0",
|
||||
"lodash": "^4.17.19"
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz",
|
||||
"integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==",
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.11.tgz",
|
||||
"integrity": "sha512-ukA9SQtKThINm++CX1CwmliMrE54J6nIYB5XTwL5f/CLFW9owfls+YSU8tVW15RQ2w+a3fSbPjC6HdQNtWZkiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.10.4",
|
||||
"@babel/helper-validator-identifier": "^7.12.11",
|
||||
"lodash": "^4.17.19",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz",
|
||||
"integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
@@ -1308,12 +1425,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/generator": {
|
||||
"version": "7.12.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.5.tgz",
|
||||
"integrity": "sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A==",
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.11.tgz",
|
||||
"integrity": "sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.12.5",
|
||||
"@babel/types": "^7.12.11",
|
||||
"jsesc": "^2.5.1",
|
||||
"source-map": "^0.5.0"
|
||||
}
|
||||
@@ -1353,16 +1470,25 @@
|
||||
"lodash": "^4.17.19"
|
||||
}
|
||||
},
|
||||
"@babel/helper-replace-supers": {
|
||||
"version": "7.12.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.5.tgz",
|
||||
"integrity": "sha512-5YILoed0ZyIpF4gKcpZitEnXEJ9UoDRki1Ey6xz46rxOzfNMAhVIJMoune1hmPVxh40LRv1+oafz7UsWX+vyWA==",
|
||||
"@babel/helper-optimise-call-expression": {
|
||||
"version": "7.12.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.10.tgz",
|
||||
"integrity": "sha512-4tpbU0SrSTjjt65UMWSrUOPZTsgvPgGG4S8QSTNHacKzpS51IVWGDj0yCwyeZND/i+LSN2g/O63jEXEWm49sYQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-member-expression-to-functions": "^7.12.1",
|
||||
"@babel/helper-optimise-call-expression": "^7.10.4",
|
||||
"@babel/traverse": "^7.12.5",
|
||||
"@babel/types": "^7.12.5"
|
||||
"@babel/types": "^7.12.10"
|
||||
}
|
||||
},
|
||||
"@babel/helper-replace-supers": {
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.11.tgz",
|
||||
"integrity": "sha512-q+w1cqmhL7R0FNzth/PLLp2N+scXEK/L2AHbXUyydxp828F4FEa5WcVoqui9vFRiHDQErj9Zof8azP32uGVTRA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-member-expression-to-functions": "^7.12.7",
|
||||
"@babel/helper-optimise-call-expression": "^7.12.10",
|
||||
"@babel/traverse": "^7.12.10",
|
||||
"@babel/types": "^7.12.11"
|
||||
}
|
||||
},
|
||||
"@babel/helper-simple-access": {
|
||||
@@ -1375,37 +1501,45 @@
|
||||
}
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.7.tgz",
|
||||
"integrity": "sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg==",
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.11.tgz",
|
||||
"integrity": "sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/traverse": {
|
||||
"version": "7.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.7.tgz",
|
||||
"integrity": "sha512-nMWaqsQEeSvMNypswUDzjqQ+0rR6pqCtoQpsqGJC4/Khm9cISwPTSpai57F6/jDaOoEGz8yE/WxcO3PV6tKSmQ==",
|
||||
"version": "7.12.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.10.tgz",
|
||||
"integrity": "sha512-6aEtf0IeRgbYWzta29lePeYSk+YAFIC3kyqESeft8o5CkFlYIMX+EQDDWEiAQ9LHOA3d0oHdgrSsID/CKqXJlg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/generator": "^7.12.5",
|
||||
"@babel/generator": "^7.12.10",
|
||||
"@babel/helper-function-name": "^7.10.4",
|
||||
"@babel/helper-split-export-declaration": "^7.11.0",
|
||||
"@babel/parser": "^7.12.7",
|
||||
"@babel/types": "^7.12.7",
|
||||
"@babel/parser": "^7.12.10",
|
||||
"@babel/types": "^7.12.10",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0",
|
||||
"lodash": "^4.17.19"
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz",
|
||||
"integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==",
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.11.tgz",
|
||||
"integrity": "sha512-ukA9SQtKThINm++CX1CwmliMrE54J6nIYB5XTwL5f/CLFW9owfls+YSU8tVW15RQ2w+a3fSbPjC6HdQNtWZkiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.10.4",
|
||||
"@babel/helper-validator-identifier": "^7.12.11",
|
||||
"lodash": "^4.17.19",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz",
|
||||
"integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
@@ -1442,12 +1576,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/generator": {
|
||||
"version": "7.12.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.5.tgz",
|
||||
"integrity": "sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A==",
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.11.tgz",
|
||||
"integrity": "sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.12.5",
|
||||
"@babel/types": "^7.12.11",
|
||||
"jsesc": "^2.5.1",
|
||||
"source-map": "^0.5.0"
|
||||
}
|
||||
@@ -1487,16 +1621,25 @@
|
||||
"lodash": "^4.17.19"
|
||||
}
|
||||
},
|
||||
"@babel/helper-replace-supers": {
|
||||
"version": "7.12.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.5.tgz",
|
||||
"integrity": "sha512-5YILoed0ZyIpF4gKcpZitEnXEJ9UoDRki1Ey6xz46rxOzfNMAhVIJMoune1hmPVxh40LRv1+oafz7UsWX+vyWA==",
|
||||
"@babel/helper-optimise-call-expression": {
|
||||
"version": "7.12.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.10.tgz",
|
||||
"integrity": "sha512-4tpbU0SrSTjjt65UMWSrUOPZTsgvPgGG4S8QSTNHacKzpS51IVWGDj0yCwyeZND/i+LSN2g/O63jEXEWm49sYQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-member-expression-to-functions": "^7.12.1",
|
||||
"@babel/helper-optimise-call-expression": "^7.10.4",
|
||||
"@babel/traverse": "^7.12.5",
|
||||
"@babel/types": "^7.12.5"
|
||||
"@babel/types": "^7.12.10"
|
||||
}
|
||||
},
|
||||
"@babel/helper-replace-supers": {
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.11.tgz",
|
||||
"integrity": "sha512-q+w1cqmhL7R0FNzth/PLLp2N+scXEK/L2AHbXUyydxp828F4FEa5WcVoqui9vFRiHDQErj9Zof8azP32uGVTRA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-member-expression-to-functions": "^7.12.7",
|
||||
"@babel/helper-optimise-call-expression": "^7.12.10",
|
||||
"@babel/traverse": "^7.12.10",
|
||||
"@babel/types": "^7.12.11"
|
||||
}
|
||||
},
|
||||
"@babel/helper-simple-access": {
|
||||
@@ -1509,37 +1652,45 @@
|
||||
}
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.7.tgz",
|
||||
"integrity": "sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg==",
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.11.tgz",
|
||||
"integrity": "sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/traverse": {
|
||||
"version": "7.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.7.tgz",
|
||||
"integrity": "sha512-nMWaqsQEeSvMNypswUDzjqQ+0rR6pqCtoQpsqGJC4/Khm9cISwPTSpai57F6/jDaOoEGz8yE/WxcO3PV6tKSmQ==",
|
||||
"version": "7.12.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.10.tgz",
|
||||
"integrity": "sha512-6aEtf0IeRgbYWzta29lePeYSk+YAFIC3kyqESeft8o5CkFlYIMX+EQDDWEiAQ9LHOA3d0oHdgrSsID/CKqXJlg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/generator": "^7.12.5",
|
||||
"@babel/generator": "^7.12.10",
|
||||
"@babel/helper-function-name": "^7.10.4",
|
||||
"@babel/helper-split-export-declaration": "^7.11.0",
|
||||
"@babel/parser": "^7.12.7",
|
||||
"@babel/types": "^7.12.7",
|
||||
"@babel/parser": "^7.12.10",
|
||||
"@babel/types": "^7.12.10",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0",
|
||||
"lodash": "^4.17.19"
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz",
|
||||
"integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==",
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.11.tgz",
|
||||
"integrity": "sha512-ukA9SQtKThINm++CX1CwmliMrE54J6nIYB5XTwL5f/CLFW9owfls+YSU8tVW15RQ2w+a3fSbPjC6HdQNtWZkiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.10.4",
|
||||
"@babel/helper-validator-identifier": "^7.12.11",
|
||||
"lodash": "^4.17.19",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz",
|
||||
"integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
@@ -1594,12 +1745,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/generator": {
|
||||
"version": "7.12.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.5.tgz",
|
||||
"integrity": "sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A==",
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.11.tgz",
|
||||
"integrity": "sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.12.5",
|
||||
"@babel/types": "^7.12.11",
|
||||
"jsesc": "^2.5.1",
|
||||
"source-map": "^0.5.0"
|
||||
}
|
||||
@@ -1613,48 +1764,63 @@
|
||||
"@babel/types": "^7.12.7"
|
||||
}
|
||||
},
|
||||
"@babel/helper-replace-supers": {
|
||||
"version": "7.12.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.5.tgz",
|
||||
"integrity": "sha512-5YILoed0ZyIpF4gKcpZitEnXEJ9UoDRki1Ey6xz46rxOzfNMAhVIJMoune1hmPVxh40LRv1+oafz7UsWX+vyWA==",
|
||||
"@babel/helper-optimise-call-expression": {
|
||||
"version": "7.12.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.10.tgz",
|
||||
"integrity": "sha512-4tpbU0SrSTjjt65UMWSrUOPZTsgvPgGG4S8QSTNHacKzpS51IVWGDj0yCwyeZND/i+LSN2g/O63jEXEWm49sYQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-member-expression-to-functions": "^7.12.1",
|
||||
"@babel/helper-optimise-call-expression": "^7.10.4",
|
||||
"@babel/traverse": "^7.12.5",
|
||||
"@babel/types": "^7.12.5"
|
||||
"@babel/types": "^7.12.10"
|
||||
}
|
||||
},
|
||||
"@babel/helper-replace-supers": {
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.11.tgz",
|
||||
"integrity": "sha512-q+w1cqmhL7R0FNzth/PLLp2N+scXEK/L2AHbXUyydxp828F4FEa5WcVoqui9vFRiHDQErj9Zof8azP32uGVTRA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-member-expression-to-functions": "^7.12.7",
|
||||
"@babel/helper-optimise-call-expression": "^7.12.10",
|
||||
"@babel/traverse": "^7.12.10",
|
||||
"@babel/types": "^7.12.11"
|
||||
}
|
||||
},
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz",
|
||||
"integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.7.tgz",
|
||||
"integrity": "sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg==",
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.11.tgz",
|
||||
"integrity": "sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/traverse": {
|
||||
"version": "7.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.7.tgz",
|
||||
"integrity": "sha512-nMWaqsQEeSvMNypswUDzjqQ+0rR6pqCtoQpsqGJC4/Khm9cISwPTSpai57F6/jDaOoEGz8yE/WxcO3PV6tKSmQ==",
|
||||
"version": "7.12.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.10.tgz",
|
||||
"integrity": "sha512-6aEtf0IeRgbYWzta29lePeYSk+YAFIC3kyqESeft8o5CkFlYIMX+EQDDWEiAQ9LHOA3d0oHdgrSsID/CKqXJlg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/generator": "^7.12.5",
|
||||
"@babel/generator": "^7.12.10",
|
||||
"@babel/helper-function-name": "^7.10.4",
|
||||
"@babel/helper-split-export-declaration": "^7.11.0",
|
||||
"@babel/parser": "^7.12.7",
|
||||
"@babel/types": "^7.12.7",
|
||||
"@babel/parser": "^7.12.10",
|
||||
"@babel/types": "^7.12.10",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0",
|
||||
"lodash": "^4.17.19"
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz",
|
||||
"integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==",
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.11.tgz",
|
||||
"integrity": "sha512-ukA9SQtKThINm++CX1CwmliMrE54J6nIYB5XTwL5f/CLFW9owfls+YSU8tVW15RQ2w+a3fSbPjC6HdQNtWZkiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.10.4",
|
||||
"@babel/helper-validator-identifier": "^7.12.11",
|
||||
"lodash": "^4.17.19",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
@@ -1719,24 +1885,24 @@
|
||||
}
|
||||
},
|
||||
"@babel/plugin-transform-react-jsx": {
|
||||
"version": "7.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.12.7.tgz",
|
||||
"integrity": "sha512-YFlTi6MEsclFAPIDNZYiCRbneg1MFGao9pPG9uD5htwE0vDbPaMUMeYd6itWjw7K4kro4UbdQf3ljmFl9y48dQ==",
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.12.11.tgz",
|
||||
"integrity": "sha512-5nWOw6mTylaFU72BdZfa0dP1HsGdY3IMExpxn8LBE8dNmkQjB+W+sR+JwIdtbzkPvVuFviT3zyNbSUkuVTVxbw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-builder-react-jsx": "^7.10.4",
|
||||
"@babel/helper-builder-react-jsx-experimental": "^7.12.4",
|
||||
"@babel/helper-builder-react-jsx-experimental": "^7.12.11",
|
||||
"@babel/helper-plugin-utils": "^7.10.4",
|
||||
"@babel/plugin-syntax-jsx": "^7.12.1"
|
||||
}
|
||||
},
|
||||
"@babel/plugin-transform-react-jsx-development": {
|
||||
"version": "7.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.12.7.tgz",
|
||||
"integrity": "sha512-Rs3ETtMtR3VLXFeYRChle5SsP/P9Jp/6dsewBQfokDSzKJThlsuFcnzLTDRALiUmTC48ej19YD9uN1mupEeEDg==",
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.12.11.tgz",
|
||||
"integrity": "sha512-5MvsGschXeXJsbzQGR/BH89ATMzCsM7rx95n+R7/852cGoK2JgMbacDw/A9Pmrfex4tArdMab0L5SBV4SB/Nxg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-builder-react-jsx-experimental": "^7.12.4",
|
||||
"@babel/helper-builder-react-jsx-experimental": "^7.12.11",
|
||||
"@babel/helper-plugin-utils": "^7.10.4",
|
||||
"@babel/plugin-syntax-jsx": "^7.12.1"
|
||||
}
|
||||
@@ -1825,9 +1991,9 @@
|
||||
}
|
||||
},
|
||||
"@babel/plugin-transform-typeof-symbol": {
|
||||
"version": "7.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.12.1.tgz",
|
||||
"integrity": "sha512-EPGgpGy+O5Kg5pJFNDKuxt9RdmTgj5sgrus2XVeMp/ZIbOESadgILUbm50SNpghOh3/6yrbsH+NB5+WJTmsA7Q==",
|
||||
"version": "7.12.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.12.10.tgz",
|
||||
"integrity": "sha512-JQ6H8Rnsogh//ijxspCjc21YPd3VLVoYtAwv3zQmqAt8YGYUtdo5usNhdl4b9/Vir2kPFZl6n1h0PfUz4hJhaA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-plugin-utils": "^7.10.4"
|
||||
@@ -2059,9 +2225,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@edx/frontend-build": {
|
||||
"version": "5.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-build/-/frontend-build-5.5.1.tgz",
|
||||
"integrity": "sha512-qltHAJMuHcEJJep2kq+ND7cIjK8PZKkHbLQlc+T5DvgAppOsvnidU0NdXK+eTY+cKVz5s2ysmgs9mb4g0ugYHA==",
|
||||
"version": "5.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-build/-/frontend-build-5.5.5.tgz",
|
||||
"integrity": "sha512-4+6niDfuMLp4fNiolofwqQCNtK+F0ad5lkMfgMDhovUzyylQ77XKAnd0A3Xjh+yiC1essZaDbV2SYNSAkLxVWQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/cli": "7.10.5",
|
||||
@@ -2091,7 +2257,7 @@
|
||||
"eslint-plugin-jsx-a11y": "6.2.3",
|
||||
"eslint-plugin-react": "7.19.0",
|
||||
"eslint-plugin-react-hooks": "1.7.0",
|
||||
"file-loader": "6.0.0",
|
||||
"file-loader": "6.2.0",
|
||||
"html-webpack-new-relic-plugin": "2.0.0",
|
||||
"html-webpack-plugin": "4.5.0",
|
||||
"identity-obj-proxy": "3.0.0",
|
||||
@@ -2107,7 +2273,7 @@
|
||||
"sass-loader": "10.0.2",
|
||||
"source-map-loader": "0.2.4",
|
||||
"style-loader": "1.1.3",
|
||||
"url-loader": "4.1.0",
|
||||
"url-loader": "4.1.1",
|
||||
"webpack": "4.44.2",
|
||||
"webpack-bundle-analyzer": "3.9.0",
|
||||
"webpack-cli": "3.3.12",
|
||||
@@ -2201,25 +2367,17 @@
|
||||
}
|
||||
},
|
||||
"@edx/frontend-component-footer": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-10.1.0.tgz",
|
||||
"integrity": "sha512-gPgE78/nWSARRwmQW9cUyYLQk8Lh+h4iwZRk8gUryr/do9jA1K2xdpjUA0Fm2S8HOG7W9Yc+GCYxxOgS6QgvIg==",
|
||||
"version": "10.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-10.1.2.tgz",
|
||||
"integrity": "sha512-R41uS1kqZ9ll0VCcT9a/TCGCjQ/msLyn9kiUH5LFCisfB7c1LU91XmVgpVk9P9+wTMuIhwQmgTNn0UJ64oQO9g==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.30",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.32",
|
||||
"@fortawesome/free-brands-svg-icons": "5.8.2",
|
||||
"@fortawesome/free-regular-svg-icons": "5.8.2",
|
||||
"@fortawesome/free-solid-svg-icons": "5.8.2",
|
||||
"@fortawesome/react-fontawesome": "0.1.11"
|
||||
"@fortawesome/react-fontawesome": "0.1.13"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": {
|
||||
"version": "1.2.30",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.30.tgz",
|
||||
"integrity": "sha512-E3sAXATKCSVnT17HYmZjjbcmwihrNOCkoU7dVMlasrcwiJAHxSKeZ+4WN5O+ElgO/FaYgJmASl8p9N7/B/RttA==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.30"
|
||||
}
|
||||
},
|
||||
"@fortawesome/free-brands-svg-icons": {
|
||||
"version": "5.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-5.8.2.tgz",
|
||||
@@ -2243,14 +2401,6 @@
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.18"
|
||||
}
|
||||
},
|
||||
"@fortawesome/react-fontawesome": {
|
||||
"version": "0.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.11.tgz",
|
||||
"integrity": "sha512-sClfojasRifQKI0OPqTy8Ln8iIhnxR/Pv/hukBhWnBz9kQRmqi6JSH3nghlhAY7SUeIIM7B5/D2G8WjX0iepVg==",
|
||||
"requires": {
|
||||
"prop-types": "^15.7.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2280,12 +2430,12 @@
|
||||
}
|
||||
},
|
||||
"@edx/frontend-platform": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-1.8.0.tgz",
|
||||
"integrity": "sha512-R4LGBKaSBWC9xxG3eTN78zw5CkBes4xQgazt78ApgenxzIoun+y0tdWyc/8PpjbKmyj0nACpsfARyeQy4rnGKA==",
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-1.8.1.tgz",
|
||||
"integrity": "sha512-oHLSamyIDuCtdS7eDzLcYs4zsAOGf4uuywnnIKg1CnlEHzUrXnfmcajbNM3Wre5jJocGH+1qACuaGLELO+3sAQ==",
|
||||
"requires": {
|
||||
"@cospired/i18n-iso-languages": "2.1.2",
|
||||
"axios": "0.18.1",
|
||||
"axios": "0.21.1",
|
||||
"axios-cache-adapter": "^2.5.0",
|
||||
"form-urlencoded": "4.1.4",
|
||||
"glob": "7.1.6",
|
||||
@@ -2387,20 +2537,20 @@
|
||||
}
|
||||
},
|
||||
"@formatjs/ts-transformer": {
|
||||
"version": "2.12.8",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/ts-transformer/-/ts-transformer-2.12.8.tgz",
|
||||
"integrity": "sha512-gu1Nj1tNXM1yO5PKQtaT+/bedzbQGQrj+u2hrTeZN3Mto1+ofAEdF3/cB131ba9rKTQiG3eCoxByWNuQkvTxFA==",
|
||||
"version": "2.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/ts-transformer/-/ts-transformer-2.12.11.tgz",
|
||||
"integrity": "sha512-XjknAXQEy7s8Q9LsyECFo1369kctH/C841o/JeDqHRDhkgn1vV/IlF3v2qli7mxEc+L2JcO8LUwqOALpTBW/5A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"intl-messageformat-parser": "6.0.17",
|
||||
"intl-messageformat-parser": "6.1.0",
|
||||
"tslib": "^2.0.1",
|
||||
"typescript": "^4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"intl-messageformat-parser": {
|
||||
"version": "6.0.17",
|
||||
"resolved": "https://registry.npmjs.org/intl-messageformat-parser/-/intl-messageformat-parser-6.0.17.tgz",
|
||||
"integrity": "sha512-YLzTGKk+tXm0tuEuaATJjVl4C9HNjwC4VslrWQI9vQez1z7czbdmbNQD4ftXiAMLy/Pla+UxyH4jZt9UzI8Y4w==",
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/intl-messageformat-parser/-/intl-messageformat-parser-6.1.0.tgz",
|
||||
"integrity": "sha512-nPPh2kOrKqlh4D9bCAetxkrUiq5/6S1exPQyg52Ihusy0ECNGhZ0Qmq8pFRK9gWIuiQPVmLA7eSNp8diC2tX3w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@formatjs/ecma402-abstract": "1.5.0",
|
||||
@@ -3157,9 +3307,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"yargs": {
|
||||
"version": "16.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.1.1.tgz",
|
||||
"integrity": "sha512-hAD1RcFP/wfgfxgMVswPE+z3tlPFtxG8/yWUrG2i17sTWGCGqWnxKcLTF4cUKDUK8fzokwsmO9H0TDkRbMHy8w==",
|
||||
"version": "16.2.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
|
||||
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"cliui": "^7.0.2",
|
||||
@@ -3344,13 +3494,19 @@
|
||||
"@babel/types": "^7.12.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz",
|
||||
"integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz",
|
||||
"integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==",
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.11.tgz",
|
||||
"integrity": "sha512-ukA9SQtKThINm++CX1CwmliMrE54J6nIYB5XTwL5f/CLFW9owfls+YSU8tVW15RQ2w+a3fSbPjC6HdQNtWZkiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.10.4",
|
||||
"@babel/helper-validator-identifier": "^7.12.11",
|
||||
"lodash": "^4.17.19",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
@@ -3370,36 +3526,35 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": {
|
||||
"version": "7.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.7.tgz",
|
||||
"integrity": "sha512-tRKx9B53kJe8NCGGIxEQb2Bkr0riUIEuN7Sc1fxhs5H8lKlCWUvQCSNMVIB0Meva7hcbCRJ76de15KoLltdoqw==",
|
||||
"version": "7.12.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.10.tgz",
|
||||
"integrity": "sha512-eTAlQKq65zHfkHZV0sIVODCPGVgoo1HdBlbSLi9CqOzuZanMv2ihzY+4paiKr1mH+XmYESMAmJ/dpZ68eN6d8w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/generator": "^7.12.5",
|
||||
"@babel/generator": "^7.12.10",
|
||||
"@babel/helper-module-transforms": "^7.12.1",
|
||||
"@babel/helpers": "^7.12.5",
|
||||
"@babel/parser": "^7.12.7",
|
||||
"@babel/parser": "^7.12.10",
|
||||
"@babel/template": "^7.12.7",
|
||||
"@babel/traverse": "^7.12.7",
|
||||
"@babel/types": "^7.12.7",
|
||||
"@babel/traverse": "^7.12.10",
|
||||
"@babel/types": "^7.12.10",
|
||||
"convert-source-map": "^1.7.0",
|
||||
"debug": "^4.1.0",
|
||||
"gensync": "^1.0.0-beta.1",
|
||||
"json5": "^2.1.2",
|
||||
"lodash": "^4.17.19",
|
||||
"resolve": "^1.3.2",
|
||||
"semver": "^5.4.1",
|
||||
"source-map": "^0.5.0"
|
||||
}
|
||||
},
|
||||
"@babel/generator": {
|
||||
"version": "7.12.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.5.tgz",
|
||||
"integrity": "sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A==",
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.11.tgz",
|
||||
"integrity": "sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.12.5",
|
||||
"@babel/types": "^7.12.11",
|
||||
"jsesc": "^2.5.1",
|
||||
"source-map": "^0.5.0"
|
||||
}
|
||||
@@ -3439,16 +3594,25 @@
|
||||
"lodash": "^4.17.19"
|
||||
}
|
||||
},
|
||||
"@babel/helper-replace-supers": {
|
||||
"version": "7.12.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.5.tgz",
|
||||
"integrity": "sha512-5YILoed0ZyIpF4gKcpZitEnXEJ9UoDRki1Ey6xz46rxOzfNMAhVIJMoune1hmPVxh40LRv1+oafz7UsWX+vyWA==",
|
||||
"@babel/helper-optimise-call-expression": {
|
||||
"version": "7.12.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.10.tgz",
|
||||
"integrity": "sha512-4tpbU0SrSTjjt65UMWSrUOPZTsgvPgGG4S8QSTNHacKzpS51IVWGDj0yCwyeZND/i+LSN2g/O63jEXEWm49sYQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-member-expression-to-functions": "^7.12.1",
|
||||
"@babel/helper-optimise-call-expression": "^7.10.4",
|
||||
"@babel/traverse": "^7.12.5",
|
||||
"@babel/types": "^7.12.5"
|
||||
"@babel/types": "^7.12.10"
|
||||
}
|
||||
},
|
||||
"@babel/helper-replace-supers": {
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.11.tgz",
|
||||
"integrity": "sha512-q+w1cqmhL7R0FNzth/PLLp2N+scXEK/L2AHbXUyydxp828F4FEa5WcVoqui9vFRiHDQErj9Zof8azP32uGVTRA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-member-expression-to-functions": "^7.12.7",
|
||||
"@babel/helper-optimise-call-expression": "^7.12.10",
|
||||
"@babel/traverse": "^7.12.10",
|
||||
"@babel/types": "^7.12.11"
|
||||
}
|
||||
},
|
||||
"@babel/helper-simple-access": {
|
||||
@@ -3460,6 +3624,12 @@
|
||||
"@babel/types": "^7.12.1"
|
||||
}
|
||||
},
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz",
|
||||
"integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/helpers": {
|
||||
"version": "7.12.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.12.5.tgz",
|
||||
@@ -3472,9 +3642,9 @@
|
||||
}
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.7.tgz",
|
||||
"integrity": "sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg==",
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.11.tgz",
|
||||
"integrity": "sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/template": {
|
||||
@@ -3489,29 +3659,29 @@
|
||||
}
|
||||
},
|
||||
"@babel/traverse": {
|
||||
"version": "7.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.7.tgz",
|
||||
"integrity": "sha512-nMWaqsQEeSvMNypswUDzjqQ+0rR6pqCtoQpsqGJC4/Khm9cISwPTSpai57F6/jDaOoEGz8yE/WxcO3PV6tKSmQ==",
|
||||
"version": "7.12.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.10.tgz",
|
||||
"integrity": "sha512-6aEtf0IeRgbYWzta29lePeYSk+YAFIC3kyqESeft8o5CkFlYIMX+EQDDWEiAQ9LHOA3d0oHdgrSsID/CKqXJlg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/generator": "^7.12.5",
|
||||
"@babel/generator": "^7.12.10",
|
||||
"@babel/helper-function-name": "^7.10.4",
|
||||
"@babel/helper-split-export-declaration": "^7.11.0",
|
||||
"@babel/parser": "^7.12.7",
|
||||
"@babel/types": "^7.12.7",
|
||||
"@babel/parser": "^7.12.10",
|
||||
"@babel/types": "^7.12.10",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0",
|
||||
"lodash": "^4.17.19"
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz",
|
||||
"integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==",
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.11.tgz",
|
||||
"integrity": "sha512-ukA9SQtKThINm++CX1CwmliMrE54J6nIYB5XTwL5f/CLFW9owfls+YSU8tVW15RQ2w+a3fSbPjC6HdQNtWZkiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.10.4",
|
||||
"@babel/helper-validator-identifier": "^7.12.11",
|
||||
"lodash": "^4.17.19",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
@@ -3935,9 +4105,9 @@
|
||||
"integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow=="
|
||||
},
|
||||
"@types/fs-extra": {
|
||||
"version": "9.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.4.tgz",
|
||||
"integrity": "sha512-50GO5ez44lxK5MDH90DYHFFfqxH7+fTqEEnvguQRzJ/tY9qFrMSHLiYHite+F3SNmf7+LHC1eMXojuD+E3Qcyg==",
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.5.tgz",
|
||||
"integrity": "sha512-wr3t7wIW1c0A2BIJtdVp4EflriVaVVAsCAIHVzzh8B+GiFv9X1xeJjCs4upRXtzp7kQ6lP5xvskjoD4awJ1ZeA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
@@ -4251,9 +4421,9 @@
|
||||
}
|
||||
},
|
||||
"@types/webpack-sources": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-2.0.0.tgz",
|
||||
"integrity": "sha512-a5kPx98CNFRKQ+wqawroFunvFqv7GHm/3KOI52NY9xWADgc8smu4R6prt4EU/M4QfVjvgBkMqU4fBhw3QfMVkg==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-2.1.0.tgz",
|
||||
"integrity": "sha512-LXn/oYIpBeucgP1EIJbKQ2/4ZmpvRl+dlrFdX7+94SKRUV3Evy3FsfMZY318vGhkWUS5MPhtOM3w1/hCOAOXcg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
@@ -4837,14 +5007,56 @@
|
||||
"dev": true
|
||||
},
|
||||
"array-includes": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.1.tgz",
|
||||
"integrity": "sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ==",
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.2.tgz",
|
||||
"integrity": "sha512-w2GspexNQpx+PutG3QpT437/BenZBj0M/MZGn5mzv/MofYqo0xmRHzn4lFsoDlWJ+THYsGJmFlW68WlDFx7VRw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind": "^1.0.0",
|
||||
"define-properties": "^1.1.3",
|
||||
"es-abstract": "^1.17.0",
|
||||
"es-abstract": "^1.18.0-next.1",
|
||||
"get-intrinsic": "^1.0.1",
|
||||
"is-string": "^1.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"es-abstract": {
|
||||
"version": "1.18.0-next.1",
|
||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz",
|
||||
"integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"es-to-primitive": "^1.2.1",
|
||||
"function-bind": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
"has-symbols": "^1.0.1",
|
||||
"is-callable": "^1.2.2",
|
||||
"is-negative-zero": "^2.0.0",
|
||||
"is-regex": "^1.1.1",
|
||||
"object-inspect": "^1.8.0",
|
||||
"object-keys": "^1.1.1",
|
||||
"object.assign": "^4.1.1",
|
||||
"string.prototype.trimend": "^1.0.1",
|
||||
"string.prototype.trimstart": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"is-callable": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz",
|
||||
"integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==",
|
||||
"dev": true
|
||||
},
|
||||
"object.assign": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
|
||||
"integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind": "^1.0.0",
|
||||
"define-properties": "^1.1.3",
|
||||
"has-symbols": "^1.0.1",
|
||||
"object-keys": "^1.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"array-union": {
|
||||
@@ -5068,12 +5280,18 @@
|
||||
"dev": true
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.18.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.18.1.tgz",
|
||||
"integrity": "sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
|
||||
"integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
|
||||
"requires": {
|
||||
"follow-redirects": "1.5.10",
|
||||
"is-buffer": "^2.0.2"
|
||||
"follow-redirects": "^1.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"follow-redirects": {
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz",
|
||||
"integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"axios-cache-adapter": {
|
||||
@@ -5367,9 +5585,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": {
|
||||
"version": "2.6.11",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz",
|
||||
"integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==",
|
||||
"version": "2.6.12",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz",
|
||||
"integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==",
|
||||
"dev": true
|
||||
},
|
||||
"regenerator-runtime": {
|
||||
@@ -5381,9 +5599,9 @@
|
||||
}
|
||||
},
|
||||
"babel-preset-current-node-syntax": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.0.tgz",
|
||||
"integrity": "sha512-mGkvkpocWJes1CmMKtgGUwCeeq0pOhALyymozzDWYomHTbDLwueDYG6p4TK1YOeYHCzBzYPsWkgTto10JubI1Q==",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz",
|
||||
"integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/plugin-syntax-async-generators": "^7.8.4",
|
||||
@@ -6082,16 +6300,16 @@
|
||||
}
|
||||
},
|
||||
"browserslist": {
|
||||
"version": "4.14.7",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.7.tgz",
|
||||
"integrity": "sha512-BSVRLCeG3Xt/j/1cCGj1019Wbty0H+Yvu2AOuZSuoaUWn3RatbL33Cxk+Q4jRMRAbOm0p7SLravLjpnT6s0vzQ==",
|
||||
"version": "4.16.0",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.0.tgz",
|
||||
"integrity": "sha512-/j6k8R0p3nxOC6kx5JGAxsnhc9ixaWJfYc+TNTzxg6+ARaESAvQGV7h0uNOB4t+pLQJZWzcrMxXOxjgsCj3dqQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"caniuse-lite": "^1.0.30001157",
|
||||
"caniuse-lite": "^1.0.30001165",
|
||||
"colorette": "^1.2.1",
|
||||
"electron-to-chromium": "^1.3.591",
|
||||
"electron-to-chromium": "^1.3.621",
|
||||
"escalade": "^3.1.1",
|
||||
"node-releases": "^1.1.66"
|
||||
"node-releases": "^1.1.67"
|
||||
}
|
||||
},
|
||||
"bser": {
|
||||
@@ -6359,13 +6577,21 @@
|
||||
"dev": true
|
||||
},
|
||||
"camel-case": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.1.tgz",
|
||||
"integrity": "sha512-7fa2WcG4fYFkclIvEmxBbTvmibwF2/agfEBc6q3lOpVu0A13ltLsA+Hr/8Hp6kp5f+G7hKi6t8lys6XxP+1K6Q==",
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz",
|
||||
"integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"pascal-case": "^3.1.1",
|
||||
"tslib": "^1.10.0"
|
||||
"pascal-case": "^3.1.2",
|
||||
"tslib": "^2.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz",
|
||||
"integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"camelcase": {
|
||||
@@ -6407,9 +6633,9 @@
|
||||
}
|
||||
},
|
||||
"caniuse-lite": {
|
||||
"version": "1.0.30001159",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001159.tgz",
|
||||
"integrity": "sha512-w9Ph56jOsS8RL20K9cLND3u/+5WASWdhC/PPrf+V3/HsM3uHOavWOR1Xzakbv4Puo/srmPHudkmCRWM7Aq+/UA==",
|
||||
"version": "1.0.30001168",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001168.tgz",
|
||||
"integrity": "sha512-P2zmX7swIXKu+GMMR01TWa4csIKELTNnZKc+f1CjebmZJQtTAEXmpQSoKVJVVcvPGAA0TEYTOUp3VehavZSFPQ==",
|
||||
"dev": true
|
||||
},
|
||||
"caporal": {
|
||||
@@ -7134,12 +7360,12 @@
|
||||
"integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA=="
|
||||
},
|
||||
"core-js-compat": {
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.7.0.tgz",
|
||||
"integrity": "sha512-V8yBI3+ZLDVomoWICO6kq/CD28Y4r1M7CWeO4AGpMdMfseu8bkSubBmUPySMGKRTS+su4XQ07zUkAsiu9FCWTg==",
|
||||
"version": "3.8.1",
|
||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.8.1.tgz",
|
||||
"integrity": "sha512-a16TLmy9NVD1rkjUGbwuyWkiDoN0FDpAwrfLONvHFQx0D9k7J9y0srwMT8QP/Z6HE3MIFaVynEeYwZwPX1o5RQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"browserslist": "^4.14.6",
|
||||
"browserslist": "^4.15.0",
|
||||
"semver": "7.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -7364,9 +7590,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"domelementtype": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.2.tgz",
|
||||
"integrity": "sha512-wFwTwCVebUrMgGeAwRL/NhZtHAUyT9n9yg4IMDwf10+6iCMxSkVq9MGCVEH+QZWo1nNidy8kNvwmv4zWHDTqvA==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.1.0.tgz",
|
||||
"integrity": "sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
@@ -7541,18 +7767,18 @@
|
||||
"dev": true
|
||||
},
|
||||
"csso": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/csso/-/csso-4.1.1.tgz",
|
||||
"integrity": "sha512-Rvq+e1e0TFB8E8X+8MQjHSY6vtol45s5gxtLI/018UsAn2IBMmwNEZRM/h+HVnAJRHjasLIKKUO3uvoMM28LvA==",
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz",
|
||||
"integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"css-tree": "^1.0.0"
|
||||
"css-tree": "^1.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"css-tree": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.1.tgz",
|
||||
"integrity": "sha512-NVN42M2fjszcUNpDbdkvutgQSlFYsr1z7kqeuCagHnNLBfYor6uP1WL1KrkmdYZ5Y1vTBCIOI/C/+8T98fJ71w==",
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.2.tgz",
|
||||
"integrity": "sha512-wCoWush5Aeo48GLhfHPbmvZs59Z+M7k5+B1xDnXbdWNcEF423DoFdqSWE0PM5aNk5nI5cp1q7ms36zGApY/sKQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"mdn-data": "2.0.14",
|
||||
@@ -7669,6 +7895,7 @@
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
|
||||
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
@@ -8192,13 +8419,21 @@
|
||||
}
|
||||
},
|
||||
"dot-case": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.3.tgz",
|
||||
"integrity": "sha512-7hwEmg6RiSQfm/GwPL4AAWXKy3YNNZA3oFv2Pdiey0mwkRCPZ9x6SZbkLcn8Ma5PYeVokzoD4Twv2n7LKp5WeA==",
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
|
||||
"integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"no-case": "^3.0.3",
|
||||
"tslib": "^1.10.0"
|
||||
"no-case": "^3.0.4",
|
||||
"tslib": "^2.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz",
|
||||
"integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"dot-prop": {
|
||||
@@ -8335,9 +8570,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"electron-to-chromium": {
|
||||
"version": "1.3.603",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.603.tgz",
|
||||
"integrity": "sha512-J8OHxOeJkoSLgBXfV9BHgKccgfLMHh+CoeRo6wJsi6m0k3otaxS/5vrHpMNSEYY4MISwewqanPOuhAtuE8riQQ==",
|
||||
"version": "1.3.628",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.628.tgz",
|
||||
"integrity": "sha512-fmhO4YGo/kapy+xL9Eq/cZwDASaTHZu3psIFYo4yc+RY1LzbZr84xjKlDImDrlrmWhOxsrDi98nX097U/xK/cQ==",
|
||||
"dev": true
|
||||
},
|
||||
"elliptic": {
|
||||
@@ -8438,9 +8673,9 @@
|
||||
"integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ=="
|
||||
},
|
||||
"errno": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz",
|
||||
"integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==",
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz",
|
||||
"integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"prr": "~1.0.1"
|
||||
@@ -9598,13 +9833,38 @@
|
||||
}
|
||||
},
|
||||
"file-loader": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.0.0.tgz",
|
||||
"integrity": "sha512-/aMOAYEFXDdjG0wytpTL5YQLfZnnTmLNjn+AIrJ/6HVnTfDqLsVKUUwkDf4I4kgex36BvjuXEn/TX9B/1ESyqQ==",
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz",
|
||||
"integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"loader-utils": "^2.0.0",
|
||||
"schema-utils": "^2.6.5"
|
||||
"schema-utils": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
}
|
||||
},
|
||||
"schema-utils": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz",
|
||||
"integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/json-schema": "^7.0.6",
|
||||
"ajv": "^6.12.5",
|
||||
"ajv-keywords": "^3.5.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"file-type": {
|
||||
@@ -9805,6 +10065,7 @@
|
||||
"version": "1.5.10",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
|
||||
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"debug": "=3.1.0"
|
||||
}
|
||||
@@ -10522,9 +10783,9 @@
|
||||
}
|
||||
},
|
||||
"html-entities": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.3.1.tgz",
|
||||
"integrity": "sha512-rhE/4Z3hIhzHAUKbW8jVcCyuT5oJCXXqhN/6mXXVCpzTmvJnoH2HL/bt3EZ6p55jbFJBeAe1ZNpL5BugLujxNA==",
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.3.3.tgz",
|
||||
"integrity": "sha512-/VulV3SYni1taM7a4RMdceqzJWR39gpZHjBwUnsCFKWV/GJkD14CJ5F7eWcZozmHJK0/f/H5U3b3SiPkuvxMgg==",
|
||||
"dev": true
|
||||
},
|
||||
"html-escaper": {
|
||||
@@ -11256,9 +11517,9 @@
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
|
||||
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||
"dev": true
|
||||
},
|
||||
"inquirer": {
|
||||
@@ -11465,10 +11726,13 @@
|
||||
}
|
||||
},
|
||||
"is-arguments": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz",
|
||||
"integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==",
|
||||
"dev": true
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.0.tgz",
|
||||
"integrity": "sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"is-arrayish": {
|
||||
"version": "0.2.1",
|
||||
@@ -11488,7 +11752,8 @@
|
||||
"is-buffer": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz",
|
||||
"integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A=="
|
||||
"integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==",
|
||||
"dev": true
|
||||
},
|
||||
"is-callable": {
|
||||
"version": "1.2.0",
|
||||
@@ -11519,9 +11784,9 @@
|
||||
}
|
||||
},
|
||||
"is-core-module": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.1.0.tgz",
|
||||
"integrity": "sha512-YcV7BgVMRFRua2FqQzKtTDMz8iCuLEyGKjr70q8Zm1yy2qKcurbFEd79PAdHV77oL3NrAaOVQIbMmiHQCHB7ZA==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz",
|
||||
"integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has": "^1.0.3"
|
||||
@@ -11746,9 +12011,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"is-object": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz",
|
||||
"integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz",
|
||||
"integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
@@ -14433,11 +14698,23 @@
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true
|
||||
},
|
||||
"lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"yallist": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.3.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
|
||||
"integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==",
|
||||
"dev": true
|
||||
"version": "7.3.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
|
||||
"integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
@@ -14447,6 +14724,12 @@
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -15201,9 +15484,9 @@
|
||||
}
|
||||
},
|
||||
"loglevel": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.0.tgz",
|
||||
"integrity": "sha512-i2sY04nal5jDcagM3FMfG++T69GEEM8CYuOfeOIvmXzOIcwE9a/CJPR0MFM97pYMj/u10lzz7/zd7+qwhrBTqQ==",
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.1.tgz",
|
||||
"integrity": "sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==",
|
||||
"dev": true
|
||||
},
|
||||
"longest": {
|
||||
@@ -15233,12 +15516,20 @@
|
||||
}
|
||||
},
|
||||
"lower-case": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.1.tgz",
|
||||
"integrity": "sha512-LiWgfDLLb1dwbFQZsSglpRj+1ctGnayXz3Uv0/WO8n558JycT5fg6zkNcnW0G68Nn0aEldTFeEfmjCfmqry/rQ==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
|
||||
"integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"tslib": "^1.10.0"
|
||||
"tslib": "^2.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz",
|
||||
"integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"lowercase-keys": {
|
||||
@@ -15870,13 +16161,21 @@
|
||||
"dev": true
|
||||
},
|
||||
"no-case": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.3.tgz",
|
||||
"integrity": "sha512-ehY/mVQCf9BL0gKfsJBvFJen+1V//U+0HQMPrWct40ixE4jnv0bfvxDbWtAHL9EcaPEOJHVVYKoQn1TlZUB8Tw==",
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
|
||||
"integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lower-case": "^2.0.1",
|
||||
"tslib": "^1.10.0"
|
||||
"lower-case": "^2.0.2",
|
||||
"tslib": "^2.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz",
|
||||
"integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node-fetch": {
|
||||
@@ -15975,9 +16274,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node-notifier": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.0.tgz",
|
||||
"integrity": "sha512-46z7DUmcjoYdaWyXouuFNNfUo6eFa94t23c53c+lG/9Cvauk4a98rAUp9672X5dxGdQmLpPzTxzu8f/OeEPaFA==",
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz",
|
||||
"integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
@@ -15989,17 +16288,30 @@
|
||||
"which": "^2.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"semver": {
|
||||
"version": "7.3.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
|
||||
"integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==",
|
||||
"lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"yallist": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.3.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
|
||||
"integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"uuid": {
|
||||
"version": "8.3.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz",
|
||||
"integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==",
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
@@ -16012,6 +16324,13 @@
|
||||
"requires": {
|
||||
"isexe": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -16226,15 +16545,55 @@
|
||||
}
|
||||
},
|
||||
"object.fromentries": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.2.tgz",
|
||||
"integrity": "sha512-r3ZiBH7MQppDJVLx6fhD618GKNG40CZYH9wgwdhKxBDDbQgjeWGGd4AtkZad84d291YxvWe7bJGuE65Anh0dxQ==",
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.3.tgz",
|
||||
"integrity": "sha512-IDUSMXs6LOSJBWE++L0lzIbSqHl9KDCfff2x/JSEIDtEUavUnyMYC2ZGay/04Zq4UT8lvd4xNhU4/YHKibAOlw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind": "^1.0.0",
|
||||
"define-properties": "^1.1.3",
|
||||
"es-abstract": "^1.17.0-next.1",
|
||||
"function-bind": "^1.1.1",
|
||||
"es-abstract": "^1.18.0-next.1",
|
||||
"has": "^1.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"es-abstract": {
|
||||
"version": "1.18.0-next.1",
|
||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz",
|
||||
"integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"es-to-primitive": "^1.2.1",
|
||||
"function-bind": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
"has-symbols": "^1.0.1",
|
||||
"is-callable": "^1.2.2",
|
||||
"is-negative-zero": "^2.0.0",
|
||||
"is-regex": "^1.1.1",
|
||||
"object-inspect": "^1.8.0",
|
||||
"object-keys": "^1.1.1",
|
||||
"object.assign": "^4.1.1",
|
||||
"string.prototype.trimend": "^1.0.1",
|
||||
"string.prototype.trimstart": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"is-callable": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz",
|
||||
"integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==",
|
||||
"dev": true
|
||||
},
|
||||
"object.assign": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
|
||||
"integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind": "^1.0.0",
|
||||
"define-properties": "^1.1.3",
|
||||
"has-symbols": "^1.0.1",
|
||||
"object-keys": "^1.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"object.getownpropertydescriptors": {
|
||||
@@ -16257,15 +16616,55 @@
|
||||
}
|
||||
},
|
||||
"object.values": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz",
|
||||
"integrity": "sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==",
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.2.tgz",
|
||||
"integrity": "sha512-MYC0jvJopr8EK6dPBiO8Nb9mvjdypOachO5REGk6MXzujbBrAisKo3HmdEI6kZDL6fC31Mwee/5YbtMebixeag==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind": "^1.0.0",
|
||||
"define-properties": "^1.1.3",
|
||||
"es-abstract": "^1.17.0-next.1",
|
||||
"function-bind": "^1.1.1",
|
||||
"es-abstract": "^1.18.0-next.1",
|
||||
"has": "^1.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"es-abstract": {
|
||||
"version": "1.18.0-next.1",
|
||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz",
|
||||
"integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"es-to-primitive": "^1.2.1",
|
||||
"function-bind": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
"has-symbols": "^1.0.1",
|
||||
"is-callable": "^1.2.2",
|
||||
"is-negative-zero": "^2.0.0",
|
||||
"is-regex": "^1.1.1",
|
||||
"object-inspect": "^1.8.0",
|
||||
"object-keys": "^1.1.1",
|
||||
"object.assign": "^4.1.1",
|
||||
"string.prototype.trimend": "^1.0.1",
|
||||
"string.prototype.trimstart": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"is-callable": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz",
|
||||
"integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==",
|
||||
"dev": true
|
||||
},
|
||||
"object.assign": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
|
||||
"integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind": "^1.0.0",
|
||||
"define-properties": "^1.1.3",
|
||||
"has-symbols": "^1.0.1",
|
||||
"object-keys": "^1.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"obuf": {
|
||||
@@ -16559,13 +16958,21 @@
|
||||
}
|
||||
},
|
||||
"param-case": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.3.tgz",
|
||||
"integrity": "sha512-VWBVyimc1+QrzappRs7waeN2YmoZFCGXWASRYX1/rGHtXqEcrGEIDm+jqIwFa2fRXNgQEwrxaYuIrX0WcAguTA==",
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
|
||||
"integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"dot-case": "^3.0.3",
|
||||
"tslib": "^1.10.0"
|
||||
"dot-case": "^3.0.4",
|
||||
"tslib": "^2.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz",
|
||||
"integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"parent-module": {
|
||||
@@ -16626,13 +17033,21 @@
|
||||
"dev": true
|
||||
},
|
||||
"pascal-case": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.1.tgz",
|
||||
"integrity": "sha512-XIeHKqIrsquVTQL2crjq3NfJUxmdLasn3TYOU0VBM+UX2a6ztAWBlJQBePLGY7VHW8+2dRadeIPK5+KImwTxQA==",
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz",
|
||||
"integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"no-case": "^3.0.3",
|
||||
"tslib": "^1.10.0"
|
||||
"no-case": "^3.0.4",
|
||||
"tslib": "^2.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz",
|
||||
"integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"pascalcase": {
|
||||
@@ -16937,9 +17352,9 @@
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
@@ -18836,9 +19251,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"domelementtype": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.2.tgz",
|
||||
"integrity": "sha512-wFwTwCVebUrMgGeAwRL/NhZtHAUyT9n9yg4IMDwf10+6iCMxSkVq9MGCVEH+QZWo1nNidy8kNvwmv4zWHDTqvA==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.1.0.tgz",
|
||||
"integrity": "sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
@@ -19435,10 +19850,28 @@
|
||||
"semver": "^7.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"yallist": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.3.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
|
||||
"integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==",
|
||||
"version": "7.3.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
|
||||
"integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
@@ -20032,9 +20465,9 @@
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
@@ -21473,9 +21906,9 @@
|
||||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.2.tgz",
|
||||
"integrity": "sha512-thGloWsGH3SOxv1SoY7QojKi0tc+8FnOmiarEGMbd/lar7QOEd3hvlx3Fp5y6FlDUGl9L+pd4n2e+oToGMmhRQ==",
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz",
|
||||
"integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==",
|
||||
"dev": true
|
||||
},
|
||||
"unbzip2-stream": {
|
||||
@@ -21683,14 +22116,39 @@
|
||||
}
|
||||
},
|
||||
"url-loader": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/url-loader/-/url-loader-4.1.0.tgz",
|
||||
"integrity": "sha512-IzgAAIC8wRrg6NYkFIJY09vtktQcsvU8V6HhtQj9PTefbYImzLB1hufqo4m+RyM5N3mLx5BqJKccgxJS+W3kqw==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz",
|
||||
"integrity": "sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"loader-utils": "^2.0.0",
|
||||
"mime-types": "^2.1.26",
|
||||
"schema-utils": "^2.6.5"
|
||||
"mime-types": "^2.1.27",
|
||||
"schema-utils": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
}
|
||||
},
|
||||
"schema-utils": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz",
|
||||
"integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/json-schema": "^7.0.6",
|
||||
"ajv": "^6.12.5",
|
||||
"ajv-keywords": "^3.5.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"url-parse": {
|
||||
@@ -22348,9 +22806,9 @@
|
||||
}
|
||||
},
|
||||
"webpack-dev-middleware": {
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz",
|
||||
"integrity": "sha512-1xC42LxbYoqLNAhV6YzTYacicgMZQTqRd27Sim9wn5hJrX3I5nxYy1SxSd4+gjUFsz1dQFj+yEe6zEVmSkeJjw==",
|
||||
"version": "3.7.3",
|
||||
"resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.3.tgz",
|
||||
"integrity": "sha512-djelc/zGiz9nZj/U7PTBi2ViorGJXEWo/3ltkPbDyxCXhhEXkW0ce99falaok4TPj+AsxLiXJR0EBOb0zh9fKQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"memory-fs": "^0.4.1",
|
||||
@@ -22361,9 +22819,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"mime": {
|
||||
"version": "2.4.6",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz",
|
||||
"integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==",
|
||||
"version": "2.4.7",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-2.4.7.tgz",
|
||||
"integrity": "sha512-dhNd1uA2u397uQk3Nv5LM4lm93WYDUXFn3Fu291FJerns4jyTudqhIWe4W04YLy7Uk1tm1Ore04NpjRvQp/NPA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
@@ -22881,9 +23339,9 @@
|
||||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.0.tgz",
|
||||
"integrity": "sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ==",
|
||||
"version": "7.4.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.1.tgz",
|
||||
"integrity": "sha512-pTsP8UAfhy3sk1lSk/O/s4tjD0CRwvMnzvwr4OKGX7ZvqZtUyx4KIJB5JWbkykPoc55tixMGgTNoh3k4FkNGFQ==",
|
||||
"dev": true
|
||||
},
|
||||
"xml-name-validator": {
|
||||
@@ -22899,9 +23357,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"xregexp": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.4.0.tgz",
|
||||
"integrity": "sha512-83y4aa8o8o4NZe+L+46wpa+F1cWR/wCGOWI3tzqUso0w3/KAvXy0+Di7Oe/cbNMixDR4Jmi7NEybWU6ps25Wkg==",
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.4.1.tgz",
|
||||
"integrity": "sha512-2u9HwfadaJaY9zHtRRnH6BY6CQVNQKkYm3oLtC9gJXXzfsbACg5X5e4EZZGVAH+YIfa+QA9lsFQTTe3HURF3ag==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime-corejs3": "^7.12.1"
|
||||
|
||||
@@ -35,10 +35,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
||||
"@edx/frontend-component-footer": "^10.1.0",
|
||||
"@edx/frontend-component-footer": "10.1.2",
|
||||
"@edx/frontend-enterprise": "4.2.3",
|
||||
"@edx/frontend-platform": "1.8.0",
|
||||
"@edx/paragon": "^12.3.1",
|
||||
"@edx/frontend-platform": "1.8.1",
|
||||
"@edx/paragon": "12.3.1",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.32",
|
||||
"@fortawesome/free-brands-svg-icons": "5.13.1",
|
||||
"@fortawesome/free-regular-svg-icons": "5.13.1",
|
||||
@@ -62,7 +62,7 @@
|
||||
"truncate-html": "1.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/frontend-build": "5.5.1",
|
||||
"@edx/frontend-build": "5.5.5",
|
||||
"@testing-library/dom": "7.16.3",
|
||||
"@testing-library/jest-dom": "5.10.1",
|
||||
"@testing-library/react": "10.3.0",
|
||||
|
||||
@@ -1,24 +1,140 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
FormattedMessage, FormattedDate, injectIntl, intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
|
||||
import { Alert, ALERT_TYPES } from '../../generic/user-messages';
|
||||
import messages from './messages';
|
||||
|
||||
function AccessExpirationAlert({ payload }) {
|
||||
function AccessExpirationAlert({ intl, payload }) {
|
||||
const {
|
||||
rawHtml,
|
||||
accessExpiration,
|
||||
userTimezone,
|
||||
} = payload;
|
||||
return rawHtml && (
|
||||
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
|
||||
|
||||
if (!accessExpiration) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
expirationDate,
|
||||
masqueradingExpiredCourse,
|
||||
upgradeDeadline,
|
||||
upgradeUrl,
|
||||
} = accessExpiration;
|
||||
|
||||
if (masqueradingExpiredCourse) {
|
||||
return (
|
||||
<Alert type={ALERT_TYPES.INFO}>
|
||||
<FormattedMessage
|
||||
id="learning.accessExpiration.expired"
|
||||
defaultMessage="This learner does not have access to this course. Their access expired on {date}."
|
||||
values={{
|
||||
date: (
|
||||
<FormattedDate
|
||||
key="accessExpirationExpiredDate"
|
||||
day="numeric"
|
||||
month="short"
|
||||
year="numeric"
|
||||
value={expirationDate}
|
||||
{...timezoneFormatArgs}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
let deadlineMessage = null;
|
||||
if (upgradeDeadline && upgradeUrl) {
|
||||
deadlineMessage = (
|
||||
<>
|
||||
<br />
|
||||
<FormattedMessage
|
||||
id="learning.accessExpiration.deadline"
|
||||
defaultMessage="Upgrade by {date} to get unlimited access to the course as long as it exists on the site."
|
||||
values={{
|
||||
date: (
|
||||
<FormattedDate
|
||||
key="accessExpirationUpgradeDeadline"
|
||||
day="numeric"
|
||||
month="short"
|
||||
year="numeric"
|
||||
value={upgradeDeadline}
|
||||
{...timezoneFormatArgs}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<Hyperlink
|
||||
className="font-weight-bold"
|
||||
style={{ textDecoration: 'underline' }}
|
||||
destination={upgradeUrl}
|
||||
>
|
||||
{intl.formatMessage(messages.upgradeNow)}
|
||||
</Hyperlink>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert type={ALERT_TYPES.INFO}>
|
||||
{/* eslint-disable-next-line react/no-danger */}
|
||||
<div dangerouslySetInnerHTML={{ __html: rawHtml }} />
|
||||
<span className="font-weight-bold">
|
||||
<FormattedMessage
|
||||
id="learning.accessExpiration.header"
|
||||
defaultMessage="Audit Access Expires {date}"
|
||||
values={{
|
||||
date: (
|
||||
<FormattedDate
|
||||
key="accessExpirationHeaderDate"
|
||||
day="numeric"
|
||||
month="short"
|
||||
year="numeric"
|
||||
value={expirationDate}
|
||||
{...timezoneFormatArgs}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<br />
|
||||
<FormattedMessage
|
||||
id="learning.accessExpiration.body"
|
||||
defaultMessage="You lose all access to this course, including your progress, on {date}."
|
||||
values={{
|
||||
date: (
|
||||
<FormattedDate
|
||||
key="accessExpirationBodyDate"
|
||||
day="numeric"
|
||||
month="short"
|
||||
year="numeric"
|
||||
value={expirationDate}
|
||||
{...timezoneFormatArgs}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{deadlineMessage}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
AccessExpirationAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
payload: PropTypes.shape({
|
||||
rawHtml: PropTypes.string.isRequired,
|
||||
accessExpiration: PropTypes.shape({
|
||||
expirationDate: PropTypes.string.isRequired,
|
||||
masqueradingExpiredCourse: PropTypes.bool.isRequired,
|
||||
upgradeDeadline: PropTypes.string,
|
||||
upgradeUrl: PropTypes.string,
|
||||
}).isRequired,
|
||||
userTimezone: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default AccessExpirationAlert;
|
||||
export default injectIntl(AccessExpirationAlert);
|
||||
|
||||
@@ -3,13 +3,16 @@ import { useAlert } from '../../generic/user-messages';
|
||||
|
||||
const AccessExpirationAlert = React.lazy(() => import('./AccessExpirationAlert'));
|
||||
|
||||
function useAccessExpirationAlert(courseExpiredMessage, topic) {
|
||||
const rawHtml = courseExpiredMessage || null;
|
||||
const isVisible = !!rawHtml; // If it exists, show it.
|
||||
function useAccessExpirationAlert(accessExpiration, userTimezone, topic) {
|
||||
const isVisible = !!accessExpiration; // If it exists, show it.
|
||||
const payload = {
|
||||
accessExpiration,
|
||||
userTimezone,
|
||||
};
|
||||
|
||||
useAlert(isVisible, {
|
||||
code: 'clientAccessExpirationAlert',
|
||||
payload: useMemo(() => ({ rawHtml }), [rawHtml]),
|
||||
payload: useMemo(() => payload, Object.values(payload).sort()),
|
||||
topic,
|
||||
});
|
||||
|
||||
|
||||
10
src/alerts/access-expiration-alert/messages.js
Normal file
10
src/alerts/access-expiration-alert/messages.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
upgradeNow: {
|
||||
id: 'learning.accessExpiration.upgradeNow',
|
||||
defaultMessage: 'Upgrade now',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -5,6 +5,7 @@ import { Button } from '@edx/paragon';
|
||||
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||
import { useModel } from '../../generic/model-store';
|
||||
import { Alert, ALERT_TYPES } from '../../generic/user-messages';
|
||||
|
||||
import messages from './messages';
|
||||
@@ -18,8 +19,13 @@ function EnrollmentAlert({ intl, payload }) {
|
||||
isStaff,
|
||||
} = payload;
|
||||
|
||||
const {
|
||||
org,
|
||||
} = useModel('courseHomeMeta', courseId);
|
||||
|
||||
const { enrollClickHandler, loading } = useEnrollClickHandler(
|
||||
courseId,
|
||||
org,
|
||||
intl.formatMessage(messages.success),
|
||||
);
|
||||
|
||||
@@ -33,8 +39,8 @@ function EnrollmentAlert({ intl, payload }) {
|
||||
}
|
||||
|
||||
const button = canEnroll && (
|
||||
<Button disabled={loading} variant="link" className="p-0 border-0 align-top" onClick={enrollClickHandler}>
|
||||
{intl.formatMessage(messages.enroll)}
|
||||
<Button disabled={loading} variant="link" className="p-0 border-0 align-top" style={{ textDecoration: 'underline' }} onClick={enrollClickHandler}>
|
||||
{intl.formatMessage(messages.enrollNowSentence)}
|
||||
</Button>
|
||||
);
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import React, {
|
||||
useContext, useState, useCallback, useMemo,
|
||||
} from 'react';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
|
||||
import { UserMessagesContext, ALERT_TYPES, useAlert } from '../../generic/user-messages';
|
||||
import { useModel } from '../../generic/model-store';
|
||||
@@ -11,14 +13,23 @@ import { postCourseEnrollment } from './data/api';
|
||||
const EnrollmentAlert = React.lazy(() => import('./EnrollmentAlert'));
|
||||
|
||||
export function useEnrollmentAlert(courseId) {
|
||||
const course = useModel('courses', courseId);
|
||||
const { authenticatedUser } = useContext(AppContext);
|
||||
const course = useModel('courseHomeMeta', courseId);
|
||||
const outline = useModel('outline', courseId);
|
||||
const isVisible = course && course.isEnrolled !== undefined && !course.isEnrolled;
|
||||
const enrolledUser = course && course.isEnrolled !== undefined && course.isEnrolled;
|
||||
const privateOutline = outline && outline.courseBlocks && !outline.courseBlocks.courses;
|
||||
/**
|
||||
* This alert should render if
|
||||
* 1. the user is not enrolled,
|
||||
* 2. the user is authenticated, AND
|
||||
* 3. the course is private.
|
||||
*/
|
||||
const isVisible = !enrolledUser && authenticatedUser !== null && privateOutline;
|
||||
const payload = {
|
||||
canEnroll: outline.enrollAlert.canEnroll,
|
||||
canEnroll: outline && outline.enrollAlert ? outline.enrollAlert.canEnroll : false,
|
||||
courseId,
|
||||
extraText: outline.enrollAlert.extraText,
|
||||
isStaff: course.isStaff,
|
||||
extraText: outline && outline.enrollAlert ? outline.enrollAlert.extraText : '',
|
||||
isStaff: course && course.isStaff,
|
||||
};
|
||||
|
||||
useAlert(isVisible, {
|
||||
@@ -30,7 +41,7 @@ export function useEnrollmentAlert(courseId) {
|
||||
return { clientEnrollmentAlert: EnrollmentAlert };
|
||||
}
|
||||
|
||||
export function useEnrollClickHandler(courseId, successText) {
|
||||
export function useEnrollClickHandler(courseId, orgId, successText) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { addFlash } = useContext(UserMessagesContext);
|
||||
const enrollClickHandler = useCallback(() => {
|
||||
@@ -44,6 +55,10 @@ export function useEnrollClickHandler(courseId, successText) {
|
||||
topic: 'course',
|
||||
});
|
||||
setLoading(false);
|
||||
sendTrackEvent('edx.bi.user.course-home.enrollment', {
|
||||
org_key: orgId,
|
||||
courserun_key: courseId,
|
||||
});
|
||||
global.location.reload();
|
||||
});
|
||||
}, [courseId]);
|
||||
|
||||
@@ -11,9 +11,15 @@ const messages = defineMessages({
|
||||
defaultMessage: 'You are viewing this course as staff, and are not enrolled.',
|
||||
description: 'Message shown to indicate that a user is not enrolled, but is able to view a course anyway because they are staff. Shown as part of an alert, along with a link to enroll.',
|
||||
},
|
||||
enroll: {
|
||||
id: 'learning.enrollment.enroll.now',
|
||||
defaultMessage: 'Enroll Now',
|
||||
enrollNowInline: {
|
||||
id: 'learning.enrollment.enrollNow.Inline',
|
||||
defaultMessage: 'Enroll now',
|
||||
description: 'A link prompting the user to click on it to enroll in the currently viewed course.'
|
||||
+ 'This text is meant to be used at the beginning of a sentence (example: Enroll now to view course content.)',
|
||||
},
|
||||
enrollNowSentence: {
|
||||
id: 'learning.enrollment.enrollNow.Sentence',
|
||||
defaultMessage: 'Enroll now.',
|
||||
description: 'A link prompting the user to click on it to enroll in the currently viewed course.',
|
||||
},
|
||||
success: {
|
||||
|
||||
@@ -2,23 +2,30 @@ import React from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
|
||||
import { Alert } from '../../generic/user-messages';
|
||||
import messages from './messages';
|
||||
import genericMessages from '../../generic/messages';
|
||||
|
||||
function LogistrationAlert({ intl }) {
|
||||
const signIn = (
|
||||
<a href={`${getLoginRedirectUrl(global.location.href)}`}>
|
||||
{intl.formatMessage(messages.login)}
|
||||
</a>
|
||||
<Hyperlink
|
||||
style={{ textDecoration: 'underline' }}
|
||||
destination={`${getLoginRedirectUrl(global.location.href)}`}
|
||||
>
|
||||
{intl.formatMessage(genericMessages.signInLowercase)}
|
||||
</Hyperlink>
|
||||
);
|
||||
|
||||
// TODO: Pull this registration URL building out into a function, like the login one above.
|
||||
// This is complicated by the fact that we don't have a REGISTER_URL env variable available.
|
||||
const register = (
|
||||
<a href={`${getConfig().LMS_BASE_URL}/register?next=${encodeURIComponent(global.location.href)}`}>
|
||||
{intl.formatMessage(messages.register)}
|
||||
</a>
|
||||
<Hyperlink
|
||||
style={{ textDecoration: 'underline' }}
|
||||
destination={`${getConfig().LMS_BASE_URL}/register?next=${encodeURIComponent(global.location.href)}`}
|
||||
>
|
||||
{intl.formatMessage(genericMessages.registerLowercase)}
|
||||
</Hyperlink>
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -26,7 +33,7 @@ function LogistrationAlert({ intl }) {
|
||||
<FormattedMessage
|
||||
id="learning.logistration.alert"
|
||||
description="Prompts the user to sign in or register to see course content."
|
||||
defaultMessage="Please {signIn} or {register} to see course content."
|
||||
defaultMessage="To see course content, {signIn} or {register}."
|
||||
values={{
|
||||
signIn,
|
||||
register,
|
||||
|
||||
@@ -2,12 +2,20 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { ALERT_TYPES, useAlert } from '../../generic/user-messages';
|
||||
import { useModel } from '../../generic/model-store';
|
||||
|
||||
const LogistrationAlert = React.lazy(() => import('./LogistrationAlert'));
|
||||
|
||||
export function useLogistrationAlert() {
|
||||
export function useLogistrationAlert(courseId) {
|
||||
const { authenticatedUser } = useContext(AppContext);
|
||||
const isVisible = authenticatedUser === null;
|
||||
const outline = useModel('outline', courseId);
|
||||
const privateOutline = outline && outline.courseBlocks && !outline.courseBlocks.courses;
|
||||
/**
|
||||
* This alert should render if
|
||||
* 1. the user is not authenticated, AND
|
||||
* 2. the course is private.
|
||||
*/
|
||||
const isVisible = authenticatedUser === null && privateOutline;
|
||||
|
||||
useAlert(isVisible, {
|
||||
code: 'clientLogistrationAlert',
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
login: {
|
||||
id: 'learning.logistration.login',
|
||||
defaultMessage: 'sign in',
|
||||
description: 'Text in a link, prompting the user to log in. Used in "learning.logistration.alert"',
|
||||
},
|
||||
register: {
|
||||
id: 'learning.logistration.register',
|
||||
defaultMessage: 'register',
|
||||
description: 'Text in a link, prompting the user to create an account. Used in "learning.logistration.alert"',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,24 +1,87 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
FormattedMessage, FormattedDate, injectIntl, intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
|
||||
import { Alert, ALERT_TYPES } from '../../generic/user-messages';
|
||||
import { FormattedPricing } from '../../generic/upgrade-button';
|
||||
import messages from './messages';
|
||||
|
||||
function OfferAlert({ payload }) {
|
||||
function OfferAlert({ intl, payload }) {
|
||||
const {
|
||||
rawHtml,
|
||||
offer,
|
||||
userTimezone,
|
||||
} = payload;
|
||||
return rawHtml && (
|
||||
|
||||
if (!offer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
code,
|
||||
expirationDate,
|
||||
percentage,
|
||||
upgradeUrl,
|
||||
} = offer;
|
||||
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
|
||||
|
||||
return (
|
||||
<Alert type={ALERT_TYPES.INFO}>
|
||||
{/* eslint-disable-next-line react/no-danger */}
|
||||
<div dangerouslySetInnerHTML={{ __html: rawHtml }} />
|
||||
{/* the first-purchase-offer-banner class can be removed post REV-1512 experiment */}
|
||||
<span className="font-weight-bold first-purchase-offer-banner">
|
||||
<FormattedMessage
|
||||
id="learning.offer.header"
|
||||
defaultMessage="Upgrade by {date} and save {percentage}% [{fullPricing}]"
|
||||
values={{
|
||||
date: (
|
||||
<FormattedDate
|
||||
key="offerDate"
|
||||
day="numeric"
|
||||
month="long"
|
||||
value={expirationDate}
|
||||
{...timezoneFormatArgs}
|
||||
/>
|
||||
),
|
||||
fullPricing: <FormattedPricing offer={offer} />,
|
||||
percentage,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<br />
|
||||
<FormattedMessage
|
||||
id="learning.offer.code"
|
||||
defaultMessage="Use code {code} at checkout!"
|
||||
values={{
|
||||
code: (<b>{code}</b>),
|
||||
}}
|
||||
/>
|
||||
|
||||
<Hyperlink
|
||||
className="font-weight-bold"
|
||||
style={{ textDecoration: 'underline' }}
|
||||
destination={upgradeUrl}
|
||||
>
|
||||
{intl.formatMessage(messages.upgradeNow)}
|
||||
</Hyperlink>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
OfferAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
payload: PropTypes.shape({
|
||||
rawHtml: PropTypes.string.isRequired,
|
||||
offer: PropTypes.shape({
|
||||
code: PropTypes.string.isRequired,
|
||||
discountedPrice: PropTypes.string.isRequired,
|
||||
expirationDate: PropTypes.string.isRequired,
|
||||
originalPrice: PropTypes.string.isRequired,
|
||||
percentage: PropTypes.number.isRequired,
|
||||
upgradeUrl: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
userTimezone: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default OfferAlert;
|
||||
export default injectIntl(OfferAlert);
|
||||
|
||||
@@ -3,14 +3,17 @@ import { useAlert } from '../../generic/user-messages';
|
||||
|
||||
const OfferAlert = React.lazy(() => import('./OfferAlert'));
|
||||
|
||||
export function useOfferAlert(offerHtml, topic) {
|
||||
const rawHtml = offerHtml || null;
|
||||
const isVisible = !!rawHtml; // if it exists, show it.
|
||||
export function useOfferAlert(offer, userTimezone, topic) {
|
||||
const isVisible = !!offer; // if it exists, show it.
|
||||
const payload = {
|
||||
offer,
|
||||
userTimezone,
|
||||
};
|
||||
|
||||
useAlert(isVisible, {
|
||||
code: 'clientOfferAlert',
|
||||
topic,
|
||||
payload: useMemo(() => ({ rawHtml }), [rawHtml]),
|
||||
payload: useMemo(() => payload, Object.values(payload).sort()),
|
||||
});
|
||||
|
||||
return { clientOfferAlert: OfferAlert };
|
||||
|
||||
10
src/alerts/offer-alert/messages.js
Normal file
10
src/alerts/offer-alert/messages.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
upgradeNow: {
|
||||
id: 'learning.offer.upgradeNow',
|
||||
defaultMessage: 'Upgrade now',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
34
src/course-header/AnonymousUserMenu.jsx
Normal file
34
src/course-header/AnonymousUserMenu.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@edx/paragon';
|
||||
|
||||
import genericMessages from '../generic/messages';
|
||||
|
||||
function AnonymousUserMenu({ intl }) {
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
className="mr-3"
|
||||
variant="outline-primary"
|
||||
href={`${getConfig().LMS_BASE_URL}/register?next=${encodeURIComponent(global.location.href)}`}
|
||||
>
|
||||
{intl.formatMessage(genericMessages.registerSentenceCase)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
href={`${getLoginRedirectUrl(global.location.href)}`}
|
||||
>
|
||||
{intl.formatMessage(genericMessages.signInSentenceCase)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
AnonymousUserMenu.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(AnonymousUserMenu);
|
||||
74
src/course-header/AuthenticatedUserDropdown.jsx
Normal file
74
src/course-header/AuthenticatedUserDropdown.jsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faUserCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Dropdown } from '@edx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
function AuthenticatedUserDropdown({ enterpriseLearnerPortalLink, intl, username }) {
|
||||
let dashboardMenuItem = (
|
||||
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/dashboard`}>
|
||||
{intl.formatMessage(messages.dashboard)}
|
||||
</Dropdown.Item>
|
||||
);
|
||||
if (enterpriseLearnerPortalLink && Object.keys(enterpriseLearnerPortalLink).length > 0) {
|
||||
dashboardMenuItem = (
|
||||
<Dropdown.Item
|
||||
href={enterpriseLearnerPortalLink.href}
|
||||
>
|
||||
{enterpriseLearnerPortalLink.content}
|
||||
</Dropdown.Item>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<a className="text-gray-700 mr-3" href={`${getConfig().SUPPORT_URL}`}>{intl.formatMessage(messages.help)}</a>
|
||||
<Dropdown className="user-dropdown">
|
||||
<Dropdown.Toggle variant="outline-primary">
|
||||
<FontAwesomeIcon icon={faUserCircle} className="d-md-none" size="lg" />
|
||||
<span data-hj-suppress className="d-none d-md-inline">
|
||||
{username}
|
||||
</span>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu className="dropdown-menu-right">
|
||||
{dashboardMenuItem}
|
||||
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/u/${username}`}>
|
||||
{intl.formatMessage(messages.profile)}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/account/settings`}>
|
||||
{intl.formatMessage(messages.account)}
|
||||
</Dropdown.Item>
|
||||
{!enterpriseLearnerPortalLink && (
|
||||
// Users should only see Order History if they do not have an available
|
||||
// learner portal, because an available learner portal currently means
|
||||
// that they access content via Subscriptions, in which context an "order"
|
||||
// is not relevant.
|
||||
<Dropdown.Item href={getConfig().ORDER_HISTORY_URL}>
|
||||
{intl.formatMessage(messages.orderHistory)}
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
<Dropdown.Item href={getConfig().LOGOUT_URL}>
|
||||
{intl.formatMessage(messages.signOut)}
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
AuthenticatedUserDropdown.propTypes = {
|
||||
enterpriseLearnerPortalLink: PropTypes.string,
|
||||
intl: intlShape.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
AuthenticatedUserDropdown.defaultProps = {
|
||||
enterpriseLearnerPortalLink: '',
|
||||
};
|
||||
|
||||
export default injectIntl(AuthenticatedUserDropdown);
|
||||
@@ -1,14 +1,12 @@
|
||||
import React, { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Dropdown } from '@edx/paragon';
|
||||
import { useEnterpriseConfig } from '@edx/frontend-enterprise';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faUserCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import AnonymousUserMenu from './AnonymousUserMenu';
|
||||
import AuthenticatedUserDropdown from './AuthenticatedUserDropdown';
|
||||
import messages from './messages';
|
||||
|
||||
function LinkedLogo({
|
||||
@@ -41,21 +39,6 @@ function Header({
|
||||
getConfig().LMS_BASE_URL,
|
||||
);
|
||||
|
||||
let dashboardMenuItem = (
|
||||
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/dashboard`}>
|
||||
{intl.formatMessage(messages.dashboard)}
|
||||
</Dropdown.Item>
|
||||
);
|
||||
if (enterpriseLearnerPortalLink && Object.keys(enterpriseLearnerPortalLink).length > 0) {
|
||||
dashboardMenuItem = (
|
||||
<Dropdown.Item
|
||||
href={enterpriseLearnerPortalLink.href}
|
||||
>
|
||||
{enterpriseLearnerPortalLink.content}
|
||||
</Dropdown.Item>
|
||||
);
|
||||
}
|
||||
|
||||
let headerLogo = (
|
||||
<LinkedLogo
|
||||
className="logo"
|
||||
@@ -77,42 +60,22 @@ function Header({
|
||||
|
||||
return (
|
||||
<header className="course-header">
|
||||
<a className="sr-only sr-only-focusable" href="#main-content">{intl.formatMessage(messages.skipNavLink)}</a>
|
||||
<div className="container-fluid py-2 d-flex align-items-center">
|
||||
{headerLogo}
|
||||
<div className="flex-grow-1 course-title-lockup" style={{ lineHeight: 1 }}>
|
||||
<span className="d-block small m-0">{courseOrg} {courseNumber}</span>
|
||||
<span className="d-block m-0 font-weight-bold course-title">{courseTitle}</span>
|
||||
</div>
|
||||
<a className="text-gray-700 mr-3" href={`${getConfig().SUPPORT_URL}`}>{intl.formatMessage(messages.help)}</a>
|
||||
<Dropdown className="user-dropdown">
|
||||
<Dropdown.Toggle variant="outline-primary">
|
||||
<FontAwesomeIcon icon={faUserCircle} className="d-md-none" size="lg" />
|
||||
<span data-hj-suppress className="d-none d-md-inline">
|
||||
{authenticatedUser.username}
|
||||
</span>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu className="dropdown-menu-right">
|
||||
{dashboardMenuItem}
|
||||
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/u/${authenticatedUser.username}`}>
|
||||
{intl.formatMessage(messages.profile)}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/account/settings`}>
|
||||
{intl.formatMessage(messages.account)}
|
||||
</Dropdown.Item>
|
||||
{!enterpriseLearnerPortalLink && (
|
||||
// Users should only see Order History if they do not have an available
|
||||
// learner portal, because an available learner portal currently means
|
||||
// that they access content via Subscriptions, in which context an "order"
|
||||
// is not relevant.
|
||||
<Dropdown.Item href={getConfig().ORDER_HISTORY_URL}>
|
||||
{intl.formatMessage(messages.orderHistory)}
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
<Dropdown.Item href={getConfig().LOGOUT_URL}>
|
||||
{intl.formatMessage(messages.signOut)}
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
{authenticatedUser && (
|
||||
<AuthenticatedUserDropdown
|
||||
enterpriseLearnerPortalLink={enterpriseLearnerPortalLink}
|
||||
username={authenticatedUser.username}
|
||||
/>
|
||||
)}
|
||||
{!authenticatedUser && (
|
||||
<AnonymousUserMenu />
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
||||
@@ -31,6 +31,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Order History',
|
||||
description: 'The text for the user menu Order History navigation link.',
|
||||
},
|
||||
skipNavLink: {
|
||||
id: 'header.navigation.skipNavLink',
|
||||
defaultMessage: 'Skip to main content.',
|
||||
description: 'A link used by screen readers to allow users to skip to the main content of the page.',
|
||||
},
|
||||
signOut: {
|
||||
id: 'header.menu.signOut.label',
|
||||
defaultMessage: 'Sign Out',
|
||||
|
||||
@@ -67,6 +67,16 @@ Factory.define('courseHomeMetadata')
|
||||
},
|
||||
{ courseId, path: 'instructor' },
|
||||
),
|
||||
Factory.build(
|
||||
'tab',
|
||||
{
|
||||
title: 'Dates',
|
||||
priority: 5,
|
||||
slug: 'dates',
|
||||
type: 'dates',
|
||||
},
|
||||
{ courseId, path: 'dates' },
|
||||
),
|
||||
];
|
||||
|
||||
return tabs.map(
|
||||
|
||||
@@ -9,6 +9,7 @@ Factory.define('datesTabData')
|
||||
content_type_gating_enabled: false,
|
||||
missed_gated_content: false,
|
||||
missed_deadlines: false,
|
||||
verified_upgrade_link: 'http://localhost:18130/basket/add/?sku=8CF08E5',
|
||||
},
|
||||
course_date_blocks: [
|
||||
{
|
||||
@@ -216,9 +217,7 @@ Factory.define('datesTabData')
|
||||
extra_info: null,
|
||||
},
|
||||
],
|
||||
missed_deadlines: false,
|
||||
missed_gated_content: false,
|
||||
has_ended: false,
|
||||
learner_is_full_access: true,
|
||||
user_timezone: 'America/New_York',
|
||||
verified_upgrade_link: 'http://localhost:18130/basket/add/?sku=8CF08E5',
|
||||
});
|
||||
|
||||
@@ -34,8 +34,8 @@ Factory.define('outlineTabData')
|
||||
upgrade_url: `${host}/dashboard`,
|
||||
}))
|
||||
.attrs({
|
||||
access_expiration: null,
|
||||
can_show_upgrade_sock: true,
|
||||
course_expired_html: null,
|
||||
course_goals: {
|
||||
goal_options: [],
|
||||
selected_goal: null,
|
||||
@@ -50,6 +50,6 @@ Factory.define('outlineTabData')
|
||||
extra_text: 'Contact the administrator.',
|
||||
},
|
||||
handouts_html: '<ul><li>Handout 1</li></ul>',
|
||||
offer_html: null,
|
||||
offer: null,
|
||||
welcome_message_html: '<p>Welcome to this course!</p>',
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ Object {
|
||||
"sequenceStatus": "loading",
|
||||
},
|
||||
"models": Object {
|
||||
"courses": Object {
|
||||
"courseHomeMeta": Object {
|
||||
"course-v1:edX+DemoX+Demo_Course_1": Object {
|
||||
"courseId": "course-v1:edX+DemoX+Demo_Course_1",
|
||||
"id": "course-v1:edX+DemoX+Demo_Course_1",
|
||||
@@ -28,7 +28,7 @@ Object {
|
||||
"originalUserIsStaff": false,
|
||||
"tabs": Array [
|
||||
Object {
|
||||
"slug": "courseware",
|
||||
"slug": "outline",
|
||||
"title": "Course",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/course/",
|
||||
},
|
||||
@@ -52,6 +52,11 @@ Object {
|
||||
"title": "Instructor",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/instructor",
|
||||
},
|
||||
Object {
|
||||
"slug": "dates",
|
||||
"title": "Dates",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/dates",
|
||||
},
|
||||
],
|
||||
"title": "Demonstration Course",
|
||||
},
|
||||
@@ -268,13 +273,12 @@ Object {
|
||||
"contentTypeGatingEnabled": false,
|
||||
"missedDeadlines": false,
|
||||
"missedGatedContent": false,
|
||||
"verifiedUpgradeLink": "http://localhost:18130/basket/add/?sku=8CF08E5",
|
||||
},
|
||||
"hasEnded": false,
|
||||
"id": "course-v1:edX+DemoX+Demo_Course_1",
|
||||
"learnerIsFullAccess": true,
|
||||
"missedDeadlines": false,
|
||||
"missedGatedContent": false,
|
||||
"userTimezone": "America/New_York",
|
||||
"verifiedUpgradeLink": "http://localhost:18130/basket/add/?sku=8CF08E5",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -297,7 +301,7 @@ Object {
|
||||
"sequenceStatus": "loading",
|
||||
},
|
||||
"models": Object {
|
||||
"courses": Object {
|
||||
"courseHomeMeta": Object {
|
||||
"course-v1:edX+DemoX+Demo_Course_1": Object {
|
||||
"courseId": "course-v1:edX+DemoX+Demo_Course_1",
|
||||
"id": "course-v1:edX+DemoX+Demo_Course_1",
|
||||
@@ -309,7 +313,7 @@ Object {
|
||||
"originalUserIsStaff": false,
|
||||
"tabs": Array [
|
||||
Object {
|
||||
"slug": "courseware",
|
||||
"slug": "outline",
|
||||
"title": "Course",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/course/",
|
||||
},
|
||||
@@ -333,12 +337,18 @@ Object {
|
||||
"title": "Instructor",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/instructor",
|
||||
},
|
||||
Object {
|
||||
"slug": "dates",
|
||||
"title": "Dates",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/dates",
|
||||
},
|
||||
],
|
||||
"title": "Demonstration Course",
|
||||
},
|
||||
},
|
||||
"outline": Object {
|
||||
"course-v1:edX+DemoX+Demo_Course_1": Object {
|
||||
"accessExpiration": null,
|
||||
"canShowUpgradeSock": true,
|
||||
"courseBlocks": Object {
|
||||
"courses": Object {
|
||||
@@ -375,7 +385,6 @@ Object {
|
||||
},
|
||||
},
|
||||
},
|
||||
"courseExpiredHtml": null,
|
||||
"courseGoals": Object {
|
||||
"goalOptions": Array [],
|
||||
"selectedGoal": null,
|
||||
@@ -403,7 +412,7 @@ Object {
|
||||
"handoutsHtml": "<ul><li>Handout 1</li></ul>",
|
||||
"hasEnded": undefined,
|
||||
"id": "course-v1:edX+DemoX+Demo_Course_1",
|
||||
"offerHtml": null,
|
||||
"offer": null,
|
||||
"resumeCourse": Object {
|
||||
"hasVisitedCourse": false,
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+Test+Block@12345abcde",
|
||||
|
||||
@@ -7,7 +7,9 @@ function normalizeCourseHomeCourseMetadata(metadata) {
|
||||
return {
|
||||
...data,
|
||||
tabs: data.tabs.map(tab => ({
|
||||
slug: tab.tabId,
|
||||
// The API uses "courseware" as a slug for both courseware and the outline tab. We switch it to "outline" here for
|
||||
// use within the MFE to differentiate between course home and courseware.
|
||||
slug: tab.tabId === 'courseware' ? 'outline' : tab.tabId,
|
||||
title: tab.title,
|
||||
url: tab.url,
|
||||
})),
|
||||
@@ -105,6 +107,10 @@ export async function getDatesTabData(courseId) {
|
||||
global.location.replace(`${getConfig().LMS_BASE_URL}/courses/${courseId}/dates`);
|
||||
return {};
|
||||
}
|
||||
if (httpErrorStatus === 401) {
|
||||
global.location.replace(`${getConfig().LMS_BASE_URL}/courses/${courseId}/course`);
|
||||
return {};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -124,6 +130,20 @@ export async function getProgressTabData(courseId) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getProctoringInfoData(courseId) {
|
||||
const url = `${getConfig().LMS_BASE_URL}/api/edx_proctoring/v1/user_onboarding/status?course_id=${encodeURIComponent(courseId)}`;
|
||||
try {
|
||||
const { data } = await getAuthenticatedHttpClient().get(url);
|
||||
return data;
|
||||
} catch (error) {
|
||||
const { httpErrorStatus } = error && error.customAttributes;
|
||||
if (httpErrorStatus === 404) {
|
||||
return {};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getOutlineTabData(courseId) {
|
||||
const url = `${getConfig().LMS_BASE_URL}/api/course_home/v1/outline/${courseId}`;
|
||||
let { tabData } = {};
|
||||
@@ -141,42 +161,45 @@ export async function getOutlineTabData(courseId) {
|
||||
const {
|
||||
data,
|
||||
} = tabData;
|
||||
const accessExpiration = camelCaseObject(data.access_expiration);
|
||||
const canShowUpgradeSock = data.can_show_upgrade_sock;
|
||||
const courseBlocks = data.course_blocks ? normalizeOutlineBlocks(courseId, data.course_blocks.blocks) : {};
|
||||
const courseGoals = camelCaseObject(data.course_goals);
|
||||
const courseExpiredHtml = data.course_expired_html;
|
||||
const courseTools = camelCaseObject(data.course_tools);
|
||||
const datesBannerInfo = camelCaseObject(data.dates_banner_info);
|
||||
const datesWidget = camelCaseObject(data.dates_widget);
|
||||
const enrollAlert = camelCaseObject(data.enroll_alert);
|
||||
const handoutsHtml = data.handouts_html;
|
||||
const hasEnded = data.has_ended;
|
||||
const offerHtml = data.offer_html;
|
||||
const offer = camelCaseObject(data.offer);
|
||||
const resumeCourse = camelCaseObject(data.resume_course);
|
||||
const verifiedMode = camelCaseObject(data.verified_mode);
|
||||
const welcomeMessageHtml = data.welcome_message_html;
|
||||
|
||||
return {
|
||||
accessExpiration,
|
||||
canShowUpgradeSock,
|
||||
courseBlocks,
|
||||
courseGoals,
|
||||
courseExpiredHtml,
|
||||
courseTools,
|
||||
datesBannerInfo,
|
||||
datesWidget,
|
||||
enrollAlert,
|
||||
handoutsHtml,
|
||||
hasEnded,
|
||||
offerHtml,
|
||||
offer,
|
||||
resumeCourse,
|
||||
verifiedMode,
|
||||
welcomeMessageHtml,
|
||||
};
|
||||
}
|
||||
|
||||
export async function postCourseDeadlines(courseId) {
|
||||
export async function postCourseDeadlines(courseId, model) {
|
||||
const url = new URL(`${getConfig().LMS_BASE_URL}/api/course_experience/v1/reset_course_deadlines`);
|
||||
return getAuthenticatedHttpClient().post(url.href, { course_key: courseId });
|
||||
return getAuthenticatedHttpClient().post(url.href, {
|
||||
course_key: courseId,
|
||||
research_event_data: { location: `${model}-tab` },
|
||||
});
|
||||
}
|
||||
|
||||
export async function postCourseGoals(courseId, goalKey) {
|
||||
@@ -194,7 +217,10 @@ export async function postRequestCert(courseId) {
|
||||
await getAuthenticatedHttpClient().post(url.href);
|
||||
}
|
||||
|
||||
export async function executePostFromPostEvent(postData) {
|
||||
export async function executePostFromPostEvent(postData, researchEventData) {
|
||||
const url = new URL(postData.url);
|
||||
return getAuthenticatedHttpClient().post(url.href, { course_key: postData.bodyParams.courseId });
|
||||
return getAuthenticatedHttpClient().post(url.href, {
|
||||
course_key: postData.bodyParams.courseId,
|
||||
research_event_data: researchEventData,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -102,16 +102,17 @@ describe('Data layer integration tests', () => {
|
||||
describe('Test resetDeadlines', () => {
|
||||
it('Should reset course deadlines', async () => {
|
||||
const resetUrl = `${getConfig().LMS_BASE_URL}/api/course_experience/v1/reset_course_deadlines`;
|
||||
const model = 'dates';
|
||||
axiosMock.onPost(resetUrl).reply(201, {});
|
||||
|
||||
const getTabDataMock = jest.fn(() => ({
|
||||
type: 'MOCK_ACTION',
|
||||
}));
|
||||
|
||||
await executeThunk(thunks.resetDeadlines(courseId, getTabDataMock), store.dispatch);
|
||||
await executeThunk(thunks.resetDeadlines(courseId, model, getTabDataMock), store.dispatch);
|
||||
|
||||
expect(axiosMock.history.post[0].url).toEqual(resetUrl);
|
||||
expect(axiosMock.history.post[0].data).toEqual(`{"course_key":"${courseId}"}`);
|
||||
expect(axiosMock.history.post[0].data).toEqual(`{"course_key":"${courseId}","research_event_data":{"location":"dates-tab"}}`);
|
||||
|
||||
expect(getTabDataMock).toHaveBeenCalledWith(courseId);
|
||||
});
|
||||
|
||||
@@ -39,7 +39,7 @@ export function fetchTab(courseId, tab, getTabData) {
|
||||
|
||||
if (fetchedCourseHomeCourseMetadata) {
|
||||
dispatch(addModel({
|
||||
modelType: 'courses',
|
||||
modelType: 'courseHomeMeta',
|
||||
model: {
|
||||
id: courseId,
|
||||
...courseHomeCourseMetadataResult.value,
|
||||
@@ -90,9 +90,9 @@ export function requestCert(courseId) {
|
||||
return async () => postRequestCert(courseId);
|
||||
}
|
||||
|
||||
export function resetDeadlines(courseId, getTabData) {
|
||||
export function resetDeadlines(courseId, model, getTabData) {
|
||||
return async (dispatch) => {
|
||||
postCourseDeadlines(courseId).then(response => {
|
||||
postCourseDeadlines(courseId, model).then(response => {
|
||||
const { data } = response;
|
||||
const {
|
||||
header,
|
||||
@@ -111,9 +111,12 @@ export async function saveCourseGoal(courseId, goalKey) {
|
||||
|
||||
export function processEvent(eventData, getTabData) {
|
||||
return async (dispatch) => {
|
||||
// Pulling this out early so the data doesn't get camelCased and is easier
|
||||
// to use when it's passed to the backend
|
||||
const { research_event_data: researchEventData } = eventData;
|
||||
const event = camelCaseObject(eventData);
|
||||
if (event.eventName === eventTypes.POST_EVENT) {
|
||||
executePostFromPostEvent(event.postData).then(response => {
|
||||
executePostFromPostEvent(event.postData, researchEventData).then(response => {
|
||||
const { data } = response;
|
||||
const {
|
||||
header,
|
||||
|
||||
@@ -27,7 +27,7 @@ function DatesBannerContainer({
|
||||
|
||||
const {
|
||||
isSelfPaced,
|
||||
} = useModel('courses', courseId);
|
||||
} = useModel('courseHomeMeta', courseId);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const hasDeadlines = courseDateBlocks.some(x => x.dateType === 'assignment-due-date');
|
||||
@@ -54,7 +54,7 @@ function DatesBannerContainer({
|
||||
{
|
||||
name: 'resetDatesBanner',
|
||||
shouldDisplay: resetDates,
|
||||
clickHandler: () => dispatch(resetDeadlines(courseId, tabFetch)),
|
||||
clickHandler: () => dispatch(resetDeadlines(courseId, model, tabFetch)),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import classNames from 'classnames';
|
||||
export default function Badge({ children, className }) {
|
||||
return (
|
||||
<span
|
||||
className={classNames('dates-badge badge align-text-bottom font-italic ml-2 px-2 py-1', className)}
|
||||
className={classNames('dates-badge small ml-2', className)}
|
||||
data-testid="dates-badge"
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
.dates-badge {
|
||||
font-size: 0.75rem;
|
||||
border-radius: 4px;
|
||||
padding: 2px 8px 3px 8px;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ function DatesTab({ intl }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div role="heading" aria-level="1" className="h4 my-3">
|
||||
<div role="heading" aria-level="1" className="h2 my-3">
|
||||
{intl.formatMessage(messages.title)}
|
||||
</div>
|
||||
<DatesBannerContainer
|
||||
|
||||
@@ -11,7 +11,7 @@ import userEvent from '@testing-library/user-event';
|
||||
|
||||
import DatesTab from './DatesTab';
|
||||
import { fetchDatesTab } from '../data';
|
||||
import { initializeMockApp } from '../../setupTest';
|
||||
import { fireEvent, initializeMockApp, waitFor } from '../../setupTest';
|
||||
import initializeStore from '../../store';
|
||||
import { TabContainer } from '../../tab-page';
|
||||
import { UserMessagesProvider } from '../../generic/user-messages';
|
||||
@@ -33,14 +33,6 @@ describe('DatesTab', () => {
|
||||
</UserMessagesProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
const courseMetadata = Factory.build('courseHomeMetadata');
|
||||
const { courseId } = courseMetadata;
|
||||
|
||||
beforeEach(() => {
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/course_home/v1/course_metadata/${courseId}`).reply(200, courseMetadata);
|
||||
history.push(`/course/${courseId}/dates`); // so tab can pull course id from url
|
||||
});
|
||||
|
||||
// The dates tab is largely repetitive non-interactive static data. Thus it's a little tough to follow
|
||||
// testing-library's advice around testing the way your user uses the site (i.e. can't find form elements by label or
|
||||
@@ -67,7 +59,14 @@ describe('DatesTab', () => {
|
||||
describe('when receiving a full set of dates data', () => {
|
||||
beforeEach(() => {
|
||||
const datesTabData = Factory.build('datesTabData');
|
||||
const courseMetadata = Factory.build('courseHomeMetadata');
|
||||
const { courseId } = courseMetadata;
|
||||
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/course_home/v1/course_metadata/${courseId}`).reply(200, courseMetadata);
|
||||
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/course_home/v1/dates/${courseId}`).reply(200, datesTabData);
|
||||
history.push(`/course/${courseId}/dates`); // so tab can pull course id from url
|
||||
|
||||
render(component);
|
||||
});
|
||||
|
||||
@@ -76,21 +75,21 @@ describe('DatesTab', () => {
|
||||
const badges = within(header).getAllByTestId('dates-badge');
|
||||
expect(badges).toHaveLength(2);
|
||||
expect(badges[0]).toHaveTextContent('Completed');
|
||||
expect(badges[1]).toHaveTextContent('Not Yet Released');
|
||||
expect(badges[1]).toHaveTextContent('Not yet released');
|
||||
});
|
||||
|
||||
it('handles unreleased & past due', async () => {
|
||||
const { header } = await getDay('Mon, May 4, 2020');
|
||||
const badges = within(header).getAllByTestId('dates-badge');
|
||||
expect(badges).toHaveLength(2);
|
||||
expect(badges[0]).toHaveTextContent('Past Due');
|
||||
expect(badges[1]).toHaveTextContent('Not Yet Released');
|
||||
expect(badges[0]).toHaveTextContent('Past due');
|
||||
expect(badges[1]).toHaveTextContent('Not yet released');
|
||||
});
|
||||
|
||||
it('handles verified only', async () => {
|
||||
const { day } = await getDay('Sun, Aug 18, 2030');
|
||||
const badge = within(day).getByTestId('dates-badge');
|
||||
expect(badge).toHaveTextContent('Verified Only');
|
||||
expect(badge).toHaveTextContent('Verified only');
|
||||
});
|
||||
|
||||
it('verified only has no link', async () => {
|
||||
@@ -101,7 +100,7 @@ describe('DatesTab', () => {
|
||||
it('same status items have header badge', async () => {
|
||||
const { day, header } = await getDay('Tue, May 26, 2020');
|
||||
const badge = within(header).getByTestId('dates-badge');
|
||||
expect(badge).toHaveTextContent('Past Due'); // one header badge
|
||||
expect(badge).toHaveTextContent('Past due'); // one header badge
|
||||
expect(within(day).getAllByTestId('dates-badge')).toHaveLength(1); // no other badges
|
||||
});
|
||||
|
||||
@@ -111,7 +110,7 @@ describe('DatesTab', () => {
|
||||
expect(headerBadges).toHaveLength(0); // no header badges
|
||||
expect(items).toHaveLength(2);
|
||||
expect(within(items[0]).getByTestId('dates-badge')).toHaveTextContent('Completed');
|
||||
expect(within(items[1]).getByTestId('dates-badge')).toHaveTextContent('Past Due');
|
||||
expect(within(items[1]).getByTestId('dates-badge')).toHaveTextContent('Past due');
|
||||
});
|
||||
|
||||
it('shows extra info', async () => {
|
||||
@@ -128,4 +127,113 @@ describe('DatesTab', () => {
|
||||
waitForElementToBeRemoved(tooltip); // and it's gone again
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dates banner container ', () => {
|
||||
const courseMetadata = Factory.build('courseHomeMetadata', { is_self_paced: true, is_enrolled: true });
|
||||
const { courseId } = courseMetadata;
|
||||
const datesTabData = Factory.build('datesTabData');
|
||||
|
||||
beforeEach(() => {
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/course_home/v1/course_metadata/${courseId}`).reply(200, courseMetadata);
|
||||
history.push(`/course/${courseId}/dates`);
|
||||
});
|
||||
|
||||
it('renders datesTabInfoBanner', async () => {
|
||||
datesTabData.datesBannerInfo = {
|
||||
contentTypeGatingEnabled: false,
|
||||
missedDeadlines: false,
|
||||
missedGatedContent: false,
|
||||
};
|
||||
|
||||
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/course_home/v1/dates/${courseId}`).reply(200, datesTabData);
|
||||
render(component);
|
||||
|
||||
await waitFor(() => expect(screen.getByText("We've built a suggested schedule to help you stay on track.")).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('renders upgradeToCompleteGradedBanner', async () => {
|
||||
datesTabData.datesBannerInfo = {
|
||||
contentTypeGatingEnabled: true,
|
||||
missedDeadlines: false,
|
||||
missedGatedContent: false,
|
||||
verifiedUpgradeLink: 'http://localhost:18130/basket/add/?sku=8CF08E5',
|
||||
};
|
||||
|
||||
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/course_home/v1/dates/${courseId}`).reply(200, datesTabData);
|
||||
render(component);
|
||||
|
||||
await waitFor(() => expect(screen.getByText('You are auditing this course,')).toBeInTheDocument());
|
||||
expect(screen.getByText('which means that you are unable to participate in graded assignments. To complete graded assignments as part of this course, you can upgrade today.')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Upgrade now' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders upgradeToResetBanner', async () => {
|
||||
datesTabData.datesBannerInfo = {
|
||||
contentTypeGatingEnabled: true,
|
||||
missedDeadlines: true,
|
||||
missedGatedContent: true,
|
||||
verifiedUpgradeLink: 'http://localhost:18130/basket/add/?sku=8CF08E5',
|
||||
};
|
||||
|
||||
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/course_home/v1/dates/${courseId}`).reply(200, datesTabData);
|
||||
render(component);
|
||||
|
||||
await waitFor(() => expect(screen.getByText('You are auditing this course,')).toBeInTheDocument());
|
||||
expect(screen.getByText('which means that you are unable to participate in graded assignments. It looks like you missed some important deadlines based on our suggested schedule. To complete graded assignments as part of this course and shift the past due assignments into the future, you can upgrade today.')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Upgrade to shift due dates' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders resetDatesBanner', async () => {
|
||||
datesTabData.datesBannerInfo = {
|
||||
contentTypeGatingEnabled: true,
|
||||
missedDeadlines: true,
|
||||
missedGatedContent: false,
|
||||
verifiedUpgradeLink: 'http://localhost:18130/basket/add/?sku=8CF08E5',
|
||||
};
|
||||
|
||||
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/course_home/v1/dates/${courseId}`).reply(200, datesTabData);
|
||||
render(component);
|
||||
|
||||
await waitFor(() => expect(screen.getByText('It looks like you missed some important deadlines based on our suggested schedule.')).toBeInTheDocument());
|
||||
expect(screen.getByText('To keep yourself on track, you can update this schedule and shift the past due assignments into the future. Don’t worry—you won’t lose any of the progress you’ve made when you shift your due dates.')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Shift due dates' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles shift due dates click', async () => {
|
||||
datesTabData.datesBannerInfo = {
|
||||
contentTypeGatingEnabled: true,
|
||||
missedDeadlines: true,
|
||||
missedGatedContent: false,
|
||||
verifiedUpgradeLink: 'http://localhost:18130/basket/add/?sku=8CF08E5',
|
||||
};
|
||||
|
||||
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/course_home/v1/dates/${courseId}`).reply(200, datesTabData);
|
||||
render(component);
|
||||
|
||||
// confirm "Shift due dates" button has rendered
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: 'Shift due dates' })).toBeInTheDocument());
|
||||
|
||||
// update response to reflect shifted dates
|
||||
datesTabData.datesBannerInfo = {
|
||||
contentTypeGatingEnabled: true,
|
||||
missedDeadlines: false,
|
||||
missedGatedContent: false,
|
||||
verifiedUpgradeLink: 'http://localhost:18130/basket/add/?sku=8CF08E5',
|
||||
};
|
||||
|
||||
const resetDeadlinesData = {
|
||||
header: "You've successfully shifted your dates!",
|
||||
};
|
||||
axiosMock.onPost(`${getConfig().LMS_BASE_URL}/api/course_experience/v1/reset_course_deadlines`).reply(200, resetDeadlinesData);
|
||||
|
||||
// click "Shift due dates"
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Shift due dates' }));
|
||||
|
||||
// wait for page to reload & Toast to render
|
||||
await waitFor(() => expect(screen.getByText("You've successfully shifted your dates!")).toBeInTheDocument());
|
||||
// confirm "Shift due dates" button has not rendered
|
||||
expect(screen.queryByRole('button', { name: 'Shift due dates' })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,13 +37,15 @@ function getBadgeListAndColor(date, intl, item, items) {
|
||||
{
|
||||
message: messages.today,
|
||||
shownForDay: isToday,
|
||||
bg: 'dates-bg-today',
|
||||
bg: 'bg-warning-300',
|
||||
className: 'text-gray-900',
|
||||
},
|
||||
{
|
||||
message: messages.completed,
|
||||
shownForDay: assignments.length && assignments.every(isComplete),
|
||||
shownForItem: x => isLearnerAssignment(x) && isComplete(x),
|
||||
bg: 'bg-dark-100',
|
||||
className: 'text-gray-900',
|
||||
},
|
||||
{
|
||||
message: messages.pastDue,
|
||||
@@ -63,7 +65,7 @@ function getBadgeListAndColor(date, intl, item, items) {
|
||||
message: messages.unreleased,
|
||||
shownForDay: assignments.length && assignments.every(isUnreleased),
|
||||
shownForItem: x => isLearnerAssignment(x) && isUnreleased(x),
|
||||
className: 'border border-dark-200 text-gray-500 align-top',
|
||||
className: 'border border-gray-500 text-gray-500',
|
||||
},
|
||||
{
|
||||
message: messages.verifiedOnly,
|
||||
|
||||
@@ -7,15 +7,15 @@ const messages = defineMessages({
|
||||
},
|
||||
dueNext: {
|
||||
id: 'learning.dates.badge.dueNext',
|
||||
defaultMessage: 'Due Next',
|
||||
defaultMessage: 'Due next',
|
||||
},
|
||||
pastDue: {
|
||||
id: 'learning.dates.badge.pastDue',
|
||||
defaultMessage: 'Past Due',
|
||||
defaultMessage: 'Past due',
|
||||
},
|
||||
title: {
|
||||
id: 'learning.dates.title',
|
||||
defaultMessage: 'Important Dates',
|
||||
defaultMessage: 'Important dates',
|
||||
},
|
||||
today: {
|
||||
id: 'learning.dates.badge.today',
|
||||
@@ -23,11 +23,11 @@ const messages = defineMessages({
|
||||
},
|
||||
unreleased: {
|
||||
id: 'learning.dates.badge.unreleased',
|
||||
defaultMessage: 'Not Yet Released',
|
||||
defaultMessage: 'Not yet released',
|
||||
},
|
||||
verifiedOnly: {
|
||||
id: 'learning.dates.badge.verifiedOnly',
|
||||
defaultMessage: 'Verified Only',
|
||||
defaultMessage: 'Verified only',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export default function DateSummary({
|
||||
const linkedTitle = dateBlock.link && isLearnerAssignment(dateBlock);
|
||||
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
|
||||
return (
|
||||
<section className="container p-0 mb-3">
|
||||
<li className="container p-0 mb-3 small text-dark-500">
|
||||
<div className="row">
|
||||
<FontAwesomeIcon icon={faCalendarAlt} className="ml-3 mt-1 mr-1" fixedWidth />
|
||||
<div className="ml-1 font-weight-bold">
|
||||
@@ -27,7 +27,7 @@ export default function DateSummary({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row ml-4 pl-1 pr-2">
|
||||
<div className="row ml-4 pr-2">
|
||||
<div className="date-summary-text">
|
||||
{linkedTitle
|
||||
&& <div className="font-weight-bold mt-2"><a href={dateBlock.link}>{dateBlock.title}</a></div>}
|
||||
@@ -39,7 +39,7 @@ export default function DateSummary({
|
||||
{!linkedTitle && dateBlock.link
|
||||
&& <a href={dateBlock.link} className="description-link">{dateBlock.linkText}</a>}
|
||||
</div>
|
||||
</section>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
11
src/course-home/outline-tab/LmsHtmlFragment.css
Normal file
11
src/course-home/outline-tab/LmsHtmlFragment.css
Normal file
@@ -0,0 +1,11 @@
|
||||
body a {
|
||||
color: #00688D;
|
||||
}
|
||||
|
||||
body.inline-link a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
body.small {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
@@ -3,16 +3,22 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
export default function LmsHtmlFragment({ html, title, ...rest }) {
|
||||
export default function LmsHtmlFragment({
|
||||
className,
|
||||
html,
|
||||
title,
|
||||
...rest
|
||||
}) {
|
||||
const wholePage = `
|
||||
<html>
|
||||
<head>
|
||||
<base href="${getConfig().LMS_BASE_URL}" target="_parent">
|
||||
<link rel="stylesheet" href="/static/css/bootstrap/lms-main.css">
|
||||
</head>
|
||||
<body>${html}</body>
|
||||
</html>
|
||||
`;
|
||||
<html>
|
||||
<head>
|
||||
<base href="${getConfig().LMS_BASE_URL}" target="_parent">
|
||||
<link rel="stylesheet" href="/static/css/bootstrap/lms-main.css">
|
||||
<link rel="stylesheet" type="text/css" href="${getConfig().BASE_URL}/src/course-home/outline-tab/LmsHtmlFragment.css">
|
||||
</head>
|
||||
<body class="${className}">${html}</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
const iframe = useRef(null);
|
||||
function handleLoad() {
|
||||
@@ -33,7 +39,12 @@ export default function LmsHtmlFragment({ html, title, ...rest }) {
|
||||
);
|
||||
}
|
||||
|
||||
LmsHtmlFragment.defaultProps = {
|
||||
className: '',
|
||||
};
|
||||
|
||||
LmsHtmlFragment.propTypes = {
|
||||
className: PropTypes.string,
|
||||
html: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { Button, Toast } from '@edx/paragon';
|
||||
@@ -21,11 +22,11 @@ import useAccessExpirationAlert from '../../alerts/access-expiration-alert';
|
||||
import useCertificateAvailableAlert from './alerts/certificate-available-alert';
|
||||
import useCourseEndAlert from './alerts/course-end-alert';
|
||||
import useCourseStartAlert from './alerts/course-start-alert';
|
||||
import useEnrollmentAlert from '../../alerts/enrollment-alert';
|
||||
import useLogistrationAlert from '../../alerts/logistration-alert';
|
||||
import useOfferAlert from '../../alerts/offer-alert';
|
||||
import usePrivateCourseAlert from './alerts/private-course-alert';
|
||||
import { useModel } from '../../generic/model-store';
|
||||
import WelcomeMessage from './widgets/WelcomeMessage';
|
||||
import ProctoringInfoPanel from './widgets/ProctoringInfoPanel';
|
||||
|
||||
function OutlineTab({ intl }) {
|
||||
const {
|
||||
@@ -33,16 +34,12 @@ function OutlineTab({ intl }) {
|
||||
} = useSelector(state => state.courseHome);
|
||||
|
||||
const {
|
||||
org,
|
||||
title,
|
||||
start,
|
||||
end,
|
||||
enrollmentStart,
|
||||
enrollmentEnd,
|
||||
enrollmentMode,
|
||||
isEnrolled,
|
||||
} = useModel('courses', courseId);
|
||||
} = useModel('courseHomeMeta', courseId);
|
||||
|
||||
const {
|
||||
accessExpiration,
|
||||
canShowUpgradeSock,
|
||||
courseBlocks: {
|
||||
courses,
|
||||
@@ -52,17 +49,17 @@ function OutlineTab({ intl }) {
|
||||
goalOptions,
|
||||
selectedGoal,
|
||||
},
|
||||
courseExpiredHtml,
|
||||
datesBannerInfo,
|
||||
datesWidget: {
|
||||
courseDateBlocks,
|
||||
userTimezone,
|
||||
},
|
||||
hasEnded,
|
||||
resumeCourse: {
|
||||
hasVisitedCourse,
|
||||
url: resumeCourseUrl,
|
||||
},
|
||||
offerHtml,
|
||||
offer,
|
||||
verifiedMode,
|
||||
} = useModel('outline', courseId);
|
||||
|
||||
@@ -70,16 +67,22 @@ function OutlineTab({ intl }) {
|
||||
const [goalToastHeader, setGoalToastHeader] = useState('');
|
||||
const [expandAll, setExpandAll] = useState(false);
|
||||
|
||||
// Above the tab alerts (appearing in the order listed here)
|
||||
const logistrationAlert = useLogistrationAlert();
|
||||
const enrollmentAlert = useEnrollmentAlert(courseId);
|
||||
const logResumeCourseClick = () => {
|
||||
sendTrackingLogEvent('edx.course.home.resume_course.clicked', {
|
||||
courserun_key: courseId,
|
||||
event_type: hasVisitedCourse ? 'resume' : 'start',
|
||||
org_key: org,
|
||||
url: resumeCourseUrl,
|
||||
});
|
||||
};
|
||||
|
||||
// Below the course title alerts (appearing in the order listed here)
|
||||
const offerAlert = useOfferAlert(offerHtml, 'outline-course-alerts');
|
||||
const accessExpirationAlert = useAccessExpirationAlert(courseExpiredHtml, 'outline-course-alerts');
|
||||
const offerAlert = useOfferAlert(offer, userTimezone, 'outline-course-alerts');
|
||||
const accessExpirationAlert = useAccessExpirationAlert(accessExpiration, userTimezone, 'outline-course-alerts');
|
||||
const courseStartAlert = useCourseStartAlert(courseId);
|
||||
const courseEndAlert = useCourseEndAlert(courseId);
|
||||
const certificateAvailableAlert = useCertificateAvailableAlert(courseId);
|
||||
const privateCourseAlert = usePrivateCourseAlert(courseId);
|
||||
|
||||
const rootCourseId = courses && Object.keys(courses)[0];
|
||||
|
||||
@@ -87,14 +90,6 @@ function OutlineTab({ intl }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlertList
|
||||
topic="outline"
|
||||
className="mb-3"
|
||||
customAlerts={{
|
||||
...enrollmentAlert,
|
||||
...logistrationAlert,
|
||||
}}
|
||||
/>
|
||||
<Toast
|
||||
closeLabel={intl.formatMessage(genericMessages.close)}
|
||||
onClose={() => setGoalToastHeader('')}
|
||||
@@ -104,27 +99,26 @@ function OutlineTab({ intl }) {
|
||||
</Toast>
|
||||
<div className="row w-100 m-0 mb-3 justify-content-between">
|
||||
<div className="col-12 col-sm-auto p-0">
|
||||
<div role="heading" aria-level="1" className="h4">{title}</div>
|
||||
<div role="heading" aria-level="1" className="h2">{title}</div>
|
||||
</div>
|
||||
{resumeCourseUrl && (
|
||||
<div className="col-12 col-sm-auto p-0">
|
||||
<a className="btn btn-primary btn-block" href={resumeCourseUrl}>
|
||||
<Button block href={resumeCourseUrl} onClick={() => logResumeCourseClick()}>
|
||||
{hasVisitedCourse ? intl.formatMessage(messages.resume) : intl.formatMessage(messages.start)}
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<AlertList
|
||||
topic="outline-private-alerts"
|
||||
customAlerts={{
|
||||
...privateCourseAlert,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="col col-12 col-md-8">
|
||||
{!courseGoalToDisplay && goalOptions.length > 0 && (
|
||||
<CourseGoalCard
|
||||
courseId={courseId}
|
||||
goalOptions={goalOptions}
|
||||
title={title}
|
||||
setGoalToDisplay={(newGoal) => { setCourseGoalToDisplay(newGoal); }}
|
||||
setGoalToastHeader={(newHeader) => { setGoalToastHeader(newHeader); }}
|
||||
/>
|
||||
)}
|
||||
<AlertList
|
||||
topic="outline-course-alerts"
|
||||
className="mb-3"
|
||||
@@ -136,13 +130,24 @@ function OutlineTab({ intl }) {
|
||||
...offerAlert,
|
||||
}}
|
||||
/>
|
||||
<DatesBannerContainer
|
||||
courseDateBlocks={courseDateBlocks}
|
||||
datesBannerInfo={datesBannerInfo}
|
||||
hasEnded={hasEnded}
|
||||
model="outline"
|
||||
tabFetch={fetchOutlineTab}
|
||||
/>
|
||||
{courseDateBlocks && (
|
||||
<DatesBannerContainer
|
||||
courseDateBlocks={courseDateBlocks}
|
||||
datesBannerInfo={datesBannerInfo}
|
||||
hasEnded={hasEnded}
|
||||
model="outline"
|
||||
tabFetch={fetchOutlineTab}
|
||||
/>
|
||||
)}
|
||||
{!courseGoalToDisplay && goalOptions.length > 0 && (
|
||||
<CourseGoalCard
|
||||
courseId={courseId}
|
||||
goalOptions={goalOptions}
|
||||
title={title}
|
||||
setGoalToDisplay={(newGoal) => { setCourseGoalToDisplay(newGoal); }}
|
||||
setGoalToastHeader={(newHeader) => { setGoalToastHeader(newHeader); }}
|
||||
/>
|
||||
)}
|
||||
<WelcomeMessage courseId={courseId} />
|
||||
{rootCourseId && (
|
||||
<>
|
||||
@@ -153,50 +158,60 @@ function OutlineTab({ intl }) {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{courses[rootCourseId].sectionIds.map((sectionId) => (
|
||||
<Section
|
||||
key={sectionId}
|
||||
courseId={courseId}
|
||||
defaultOpen={sections[sectionId].resumeBlock}
|
||||
expand={expandAll}
|
||||
section={sections[sectionId]}
|
||||
/>
|
||||
))}
|
||||
<ol className="list-unstyled">
|
||||
{courses[rootCourseId].sectionIds.map((sectionId) => (
|
||||
<Section
|
||||
key={sectionId}
|
||||
courseId={courseId}
|
||||
defaultOpen={sections[sectionId].resumeBlock}
|
||||
expand={expandAll}
|
||||
section={sections[sectionId]}
|
||||
/>
|
||||
))}
|
||||
</ol>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="col col-12 col-md-4">
|
||||
{courseGoalToDisplay && goalOptions.length > 0 && (
|
||||
<UpdateGoalSelector
|
||||
{rootCourseId && (
|
||||
<div className="col col-12 col-md-4">
|
||||
<ProctoringInfoPanel
|
||||
courseId={courseId}
|
||||
goalOptions={goalOptions}
|
||||
selectedGoal={courseGoalToDisplay}
|
||||
setGoalToDisplay={(newGoal) => { setCourseGoalToDisplay(newGoal); }}
|
||||
setGoalToastHeader={(newHeader) => { setGoalToastHeader(newHeader); }}
|
||||
/>
|
||||
)}
|
||||
<CourseTools
|
||||
courseId={courseId}
|
||||
/>
|
||||
<UpgradeCard
|
||||
courseId={courseId}
|
||||
onLearnMore={canShowUpgradeSock ? () => { courseSock.current.showToUser(); } : null}
|
||||
/>
|
||||
<CourseDates
|
||||
start={start}
|
||||
end={end}
|
||||
enrollmentStart={enrollmentStart}
|
||||
enrollmentEnd={enrollmentEnd}
|
||||
enrollmentMode={enrollmentMode}
|
||||
isEnrolled={isEnrolled}
|
||||
courseId={courseId}
|
||||
/>
|
||||
<CourseHandouts
|
||||
courseId={courseId}
|
||||
/>
|
||||
</div>
|
||||
{courseGoalToDisplay && goalOptions.length > 0 && (
|
||||
<UpdateGoalSelector
|
||||
courseId={courseId}
|
||||
goalOptions={goalOptions}
|
||||
selectedGoal={courseGoalToDisplay}
|
||||
setGoalToDisplay={(newGoal) => { setCourseGoalToDisplay(newGoal); }}
|
||||
setGoalToastHeader={(newHeader) => { setGoalToastHeader(newHeader); }}
|
||||
/>
|
||||
)}
|
||||
<CourseTools
|
||||
courseId={courseId}
|
||||
/>
|
||||
<UpgradeCard
|
||||
courseId={courseId}
|
||||
onLearnMore={canShowUpgradeSock ? () => { courseSock.current.showToUser(); } : null}
|
||||
/>
|
||||
<CourseDates
|
||||
courseId={courseId}
|
||||
/>
|
||||
<CourseHandouts
|
||||
courseId={courseId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{canShowUpgradeSock && <CourseSock ref={courseSock} verifiedMode={verifiedMode} />}
|
||||
{canShowUpgradeSock && (
|
||||
<CourseSock
|
||||
courseId={courseId}
|
||||
offer={offer}
|
||||
orgKey={org}
|
||||
pageLocation="Home Page"
|
||||
ref={courseSock}
|
||||
verifiedMode={verifiedMode}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,10 +5,9 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { ALERT_TYPES } from '../../generic/user-messages';
|
||||
import buildSimpleCourseBlocks from '../data/__factories__/courseBlocks.factory';
|
||||
import {
|
||||
fireEvent, initializeMockApp, logUnhandledRequests, render, screen, waitFor,
|
||||
fireEvent, initializeMockApp, logUnhandledRequests, render, screen, waitFor, act,
|
||||
} from '../../setupTest';
|
||||
import executeThunk from '../../utils';
|
||||
import * as thunks from '../data/thunks';
|
||||
@@ -26,6 +25,7 @@ describe('Outline Tab', () => {
|
||||
const enrollmentUrl = `${getConfig().LMS_BASE_URL}/api/enrollment/v1/enrollment`;
|
||||
const goalUrl = `${getConfig().LMS_BASE_URL}/api/course_home/v1/save_course_goal`;
|
||||
const outlineUrl = `${getConfig().LMS_BASE_URL}/api/course_home/v1/outline/${courseId}`;
|
||||
const proctoringInfoUrl = `${getConfig().LMS_BASE_URL}/api/edx_proctoring/v1/user_onboarding/status?course_id=${encodeURIComponent(courseId)}`;
|
||||
|
||||
const store = initializeStore();
|
||||
const defaultMetadata = Factory.build('courseHomeMetadata', { courseId });
|
||||
@@ -43,7 +43,7 @@ describe('Outline Tab', () => {
|
||||
|
||||
async function fetchAndRender() {
|
||||
await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
|
||||
render(<OutlineTab />, { store });
|
||||
await act(async () => render(<OutlineTab />, { store }));
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -54,6 +54,7 @@ describe('Outline Tab', () => {
|
||||
axiosMock.onPost(enrollmentUrl).reply(200, {});
|
||||
axiosMock.onPost(goalUrl).reply(200, { header: 'Success' });
|
||||
axiosMock.onGet(outlineUrl).reply(200, defaultTabData);
|
||||
axiosMock.onGet(proctoringInfoUrl).reply(200, { onboarding_status: 'created', onboarding_link: 'test' });
|
||||
|
||||
logUnhandledRequests(axiosMock);
|
||||
});
|
||||
@@ -72,7 +73,7 @@ describe('Outline Tab', () => {
|
||||
},
|
||||
});
|
||||
await fetchAndRender();
|
||||
expect(screen.getByRole('link', { name: 'Resume Course' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Resume course' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('expands section that contains resume block', async () => {
|
||||
@@ -88,7 +89,7 @@ describe('Outline Tab', () => {
|
||||
it('handles expand/collapse all button click', async () => {
|
||||
await fetchAndRender();
|
||||
// Button renders as "Expand All"
|
||||
const expandButton = screen.getByRole('button', { name: 'Expand All' });
|
||||
const expandButton = screen.getByRole('button', { name: 'Expand all' });
|
||||
expect(expandButton).toBeInTheDocument();
|
||||
|
||||
// Section initially renders collapsed
|
||||
@@ -124,6 +125,10 @@ describe('Outline Tab', () => {
|
||||
});
|
||||
|
||||
describe('Welcome Message', () => {
|
||||
beforeEach(() => {
|
||||
setMetadata({ is_enrolled: true });
|
||||
});
|
||||
|
||||
it('does not render show more/less button under 100 words', async () => {
|
||||
await fetchAndRender();
|
||||
expect(screen.getByTestId('alert-container-welcome')).toBeInTheDocument();
|
||||
@@ -274,20 +279,12 @@ describe('Outline Tab', () => {
|
||||
});
|
||||
|
||||
describe('Alert List', () => {
|
||||
describe('Enrollment Alert', () => {
|
||||
let alertMessage;
|
||||
let staffMessage;
|
||||
|
||||
beforeEach(() => {
|
||||
const extraText = defaultTabData.enroll_alert.extra_text;
|
||||
alertMessage = `You must be enrolled in the course to see course content. ${extraText}`;
|
||||
staffMessage = 'You are viewing this course as staff, and are not enrolled.';
|
||||
});
|
||||
|
||||
it('does not display enrollment alert for enrolled user', async () => {
|
||||
describe('Private Course Alert', () => {
|
||||
it('does not display alert for enrolled user', async () => {
|
||||
setMetadata({ is_enrolled: true });
|
||||
await fetchAndRender();
|
||||
expect(screen.queryByText(alertMessage)).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Enroll now' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('to access the full course')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not display enrollment button if enrollment is not available', async () => {
|
||||
@@ -297,29 +294,21 @@ describe('Outline Tab', () => {
|
||||
},
|
||||
});
|
||||
await fetchAndRender();
|
||||
expect(screen.queryByRole('button', { name: 'Enroll Now' })).not.toBeInTheDocument();
|
||||
|
||||
const alert = await screen.findByText('Welcome to Demonstration Course');
|
||||
expect(alert.parentElement).toHaveAttribute('role', 'alert');
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'Enroll now' })).not.toBeInTheDocument();
|
||||
expect(screen.getByText('You must be enrolled in the course to see course content.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays enrollment alert for unenrolled user', async () => {
|
||||
it('displays alert for unenrolled user', async () => {
|
||||
await fetchAndRender();
|
||||
|
||||
const alert = await screen.findByText(alertMessage);
|
||||
expect(alert).toHaveAttribute('role', 'alert');
|
||||
const alertContainer = await screen.findByTestId(`alert-container-${ALERT_TYPES.ERROR}`);
|
||||
expect(screen.queryByText(staffMessage)).not.toBeInTheDocument();
|
||||
const alert = await screen.findByText('Welcome to Demonstration Course');
|
||||
expect(alert.parentElement).toHaveAttribute('role', 'alert');
|
||||
|
||||
expect(alertContainer.querySelector('svg')).toHaveClass('fa-exclamation-triangle');
|
||||
});
|
||||
|
||||
it('displays different message for unenrolled staff user', async () => {
|
||||
setMetadata({ is_staff: true });
|
||||
await fetchAndRender();
|
||||
|
||||
const alert = await screen.findByText(staffMessage);
|
||||
expect(alert).toHaveAttribute('role', 'alert');
|
||||
expect(screen.queryByText(alertMessage)).not.toBeInTheDocument();
|
||||
const alertContainer = await screen.findByTestId(`alert-container-${ALERT_TYPES.INFO}`);
|
||||
expect(alertContainer.querySelector('svg')).toHaveClass('fa-info-circle');
|
||||
expect(screen.getByRole('button', { name: 'Enroll now' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles button click', async () => {
|
||||
@@ -330,7 +319,7 @@ describe('Outline Tab', () => {
|
||||
};
|
||||
await fetchAndRender();
|
||||
|
||||
const button = await screen.findByRole('button', { name: 'Enroll Now' });
|
||||
const button = await screen.findByRole('button', { name: 'Enroll now' });
|
||||
fireEvent.click(button);
|
||||
await waitFor(() => expect(axiosMock.history.post).toHaveLength(1));
|
||||
expect(axiosMock.history.post[0].data)
|
||||
@@ -342,11 +331,43 @@ describe('Outline Tab', () => {
|
||||
});
|
||||
|
||||
describe('Access Expiration Alert', () => {
|
||||
// Appears if course_expired_html is provided
|
||||
it('appears', async () => {
|
||||
setTabData({ course_expired_html: '<p>Course Will Expire, Uh Oh</p>' });
|
||||
it('has special masquerade text', async () => {
|
||||
setTabData({
|
||||
access_expiration: {
|
||||
expiration_date: '2020-01-01T12:00:00Z',
|
||||
masquerading_expired_course: true,
|
||||
upgrade_deadline: null,
|
||||
upgrade_url: null,
|
||||
},
|
||||
});
|
||||
await fetchAndRender();
|
||||
await screen.findByText('Course Will Expire, Uh Oh');
|
||||
await screen.findByText('This learner does not have access to this course.', { exact: false });
|
||||
});
|
||||
|
||||
it('shows expiration', async () => {
|
||||
setTabData({
|
||||
access_expiration: {
|
||||
expiration_date: '2080-01-01T12:00:00Z',
|
||||
masquerading_expired_course: false,
|
||||
upgrade_deadline: null,
|
||||
upgrade_url: null,
|
||||
},
|
||||
});
|
||||
await fetchAndRender();
|
||||
await screen.findByText('Audit Access Expires');
|
||||
});
|
||||
|
||||
it('shows upgrade prompt', async () => {
|
||||
setTabData({
|
||||
access_expiration: {
|
||||
expiration_date: '2080-01-01T12:00:00Z',
|
||||
masquerading_expired_course: false,
|
||||
upgrade_deadline: '2070-01-01T12:00:00Z',
|
||||
upgrade_url: 'https://example.com/upgrade',
|
||||
},
|
||||
});
|
||||
await fetchAndRender();
|
||||
await screen.findByText('to get unlimited access to the course as long as it exists on the site.', { exact: false });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -454,4 +475,63 @@ describe('Outline Tab', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Proctoring Info Panel', () => {
|
||||
it('appears', async () => {
|
||||
await fetchAndRender();
|
||||
await screen.findByText('This course contains proctored exams');
|
||||
expect(screen.queryByRole('link', { name: 'Review instructions and system requirements' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('appears for verified', async () => {
|
||||
axiosMock.onGet(proctoringInfoUrl).reply(200, { onboarding_status: 'verified', onboarding_link: 'test' });
|
||||
await fetchAndRender();
|
||||
await screen.findByText('This course contains proctored exams');
|
||||
expect(screen.queryByRole('link', { name: 'Complete Onboarding' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('link', { name: 'Review instructions and system requirements' })).toBeInTheDocument();
|
||||
expect(screen.queryByText('You must complete the onboarding process prior to taking any proctored exam.')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Onboarding profile review, including identity verification, can take 2+ business days.')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('appears for rejected', async () => {
|
||||
axiosMock.onGet(proctoringInfoUrl).reply(200, { onboarding_status: 'rejected', onboarding_link: 'test' });
|
||||
await fetchAndRender();
|
||||
await screen.findByText('This course contains proctored exams');
|
||||
expect(screen.queryByRole('link', { name: 'Complete Onboarding' })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('link', { name: 'Review instructions and system requirements' })).toBeInTheDocument();
|
||||
expect(screen.queryByText('You must complete the onboarding process prior to taking any proctored exam.')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Onboarding profile review, including identity verification, can take 2+ business days.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('appears for submitted', async () => {
|
||||
axiosMock.onGet(proctoringInfoUrl).reply(200, { onboarding_status: 'submitted', onboarding_link: 'test' });
|
||||
await fetchAndRender();
|
||||
await screen.findByText('This course contains proctored exams');
|
||||
expect(screen.queryByText('Your submitted profile is in review.')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Onboarding profile review, including identity verification, can take 2+ business days.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('appears for second_review_required', async () => {
|
||||
axiosMock.onGet(proctoringInfoUrl).reply(200, { onboarding_status: 'second_review_required', onboarding_link: 'test' });
|
||||
await fetchAndRender();
|
||||
await screen.findByText('This course contains proctored exams');
|
||||
expect(screen.queryByText('Your submitted profile is in review.')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Onboarding profile review, including identity verification, can take 2+ business days.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('appears for no status', async () => {
|
||||
axiosMock.onGet(proctoringInfoUrl).reply(200, { onboarding_status: '', onboarding_link: 'test' });
|
||||
await fetchAndRender();
|
||||
await screen.findByText('This course contains proctored exams');
|
||||
expect(screen.queryByRole('link', { name: 'Complete Onboarding' })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('link', { name: 'Review instructions and system requirements' })).toBeInTheDocument();
|
||||
expect(screen.queryByText('You must complete the onboarding process prior to taking any proctored exam.')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Onboarding profile review, including identity verification, can take 2+ business days.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not appear for 404', async () => {
|
||||
axiosMock.onGet(proctoringInfoUrl).reply(404);
|
||||
expect(screen.queryByRole('link', { name: 'Review instructions and system requirements' })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,23 +39,27 @@ function Section({
|
||||
}, []);
|
||||
|
||||
const sectionTitle = (
|
||||
<div>
|
||||
{complete ? (
|
||||
<FontAwesomeIcon
|
||||
icon={fasCheckCircle}
|
||||
className="float-left mt-1 text-success"
|
||||
aria-hidden="true"
|
||||
title={intl.formatMessage(messages.completedSection)}
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
icon={farCheckCircle}
|
||||
className="float-left mt-1 text-gray-200"
|
||||
aria-hidden="true"
|
||||
title={intl.formatMessage(messages.incompleteSection)}
|
||||
/>
|
||||
)}
|
||||
<div className="ml-3 pl-3 font-weight-bold">
|
||||
<div className="row w-100 m-0">
|
||||
<div className="col-auto p-0">
|
||||
{complete ? (
|
||||
<FontAwesomeIcon
|
||||
icon={fasCheckCircle}
|
||||
fixedWidth
|
||||
className="float-left mt-1 text-success"
|
||||
aria-hidden="true"
|
||||
title={intl.formatMessage(messages.completedSection)}
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
icon={farCheckCircle}
|
||||
fixedWidth
|
||||
className="float-left mt-1 text-gray-400"
|
||||
aria-hidden="true"
|
||||
title={intl.formatMessage(messages.incompleteSection)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-10 ml-3 p-0 font-weight-bold text-dark-500">
|
||||
{title}
|
||||
<span className="sr-only">
|
||||
, {intl.formatMessage(complete ? messages.completedSection : messages.incompleteSection)}
|
||||
@@ -65,37 +69,41 @@ function Section({
|
||||
);
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
className="mb-2"
|
||||
styling="card-lg"
|
||||
title={sectionTitle}
|
||||
open={open}
|
||||
onToggle={() => { setOpen(!open); }}
|
||||
iconWhenClosed={(
|
||||
<IconButton
|
||||
alt={intl.formatMessage(messages.openSection)}
|
||||
icon={faPlus}
|
||||
onClick={() => { setOpen(true); }}
|
||||
/>
|
||||
)}
|
||||
iconWhenOpen={(
|
||||
<IconButton
|
||||
alt={intl.formatMessage(genericMessages.close)}
|
||||
icon={faMinus}
|
||||
onClick={() => { setOpen(false); }}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{sequenceIds.map((sequenceId, index) => (
|
||||
<SequenceLink
|
||||
key={sequenceId}
|
||||
id={sequenceId}
|
||||
courseId={courseId}
|
||||
sequence={sequences[sequenceId]}
|
||||
first={index === 0}
|
||||
/>
|
||||
))}
|
||||
</Collapsible>
|
||||
<li>
|
||||
<Collapsible
|
||||
className="mb-2"
|
||||
styling="card-lg"
|
||||
title={sectionTitle}
|
||||
open={open}
|
||||
onToggle={() => { setOpen(!open); }}
|
||||
iconWhenClosed={(
|
||||
<IconButton
|
||||
alt={intl.formatMessage(messages.openSection)}
|
||||
icon={faPlus}
|
||||
onClick={() => { setOpen(true); }}
|
||||
/>
|
||||
)}
|
||||
iconWhenOpen={(
|
||||
<IconButton
|
||||
alt={intl.formatMessage(genericMessages.close)}
|
||||
icon={faMinus}
|
||||
onClick={() => { setOpen(false); }}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<ol className="list-unstyled">
|
||||
{sequenceIds.map((sequenceId, index) => (
|
||||
<SequenceLink
|
||||
key={sequenceId}
|
||||
id={sequenceId}
|
||||
courseId={courseId}
|
||||
sequence={sequences[sequenceId]}
|
||||
first={index === 0}
|
||||
/>
|
||||
))}
|
||||
</ol>
|
||||
</Collapsible>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -40,59 +40,61 @@ function SequenceLink({
|
||||
const displayTitle = showLink ? <Link to={`/course/${courseId}/${id}`}>{title}</Link> : title;
|
||||
|
||||
return (
|
||||
<div className={classNames('', { 'mt-2 pt-2 border-top border-light': !first })}>
|
||||
<div className="row w-100 m-0">
|
||||
<div className="col-auto p-0">
|
||||
{complete ? (
|
||||
<FontAwesomeIcon
|
||||
icon={fasCheckCircle}
|
||||
fixedWidth
|
||||
className="float-left text-success mt-1"
|
||||
aria-hidden="true"
|
||||
title={intl.formatMessage(messages.completedAssignment)}
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
icon={farCheckCircle}
|
||||
fixedWidth
|
||||
className="float-left text-gray-200 mt-1"
|
||||
aria-hidden="true"
|
||||
title={intl.formatMessage(messages.incompleteAssignment)}
|
||||
/>
|
||||
)}
|
||||
<li>
|
||||
<div className={classNames('', { 'mt-2 pt-2 border-top border-light': !first })}>
|
||||
<div className="row w-100 m-0">
|
||||
<div className="col-auto p-0">
|
||||
{complete ? (
|
||||
<FontAwesomeIcon
|
||||
icon={fasCheckCircle}
|
||||
fixedWidth
|
||||
className="float-left text-success mt-1"
|
||||
aria-hidden="true"
|
||||
title={intl.formatMessage(messages.completedAssignment)}
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
icon={farCheckCircle}
|
||||
fixedWidth
|
||||
className="float-left text-gray-400 mt-1"
|
||||
aria-hidden="true"
|
||||
title={intl.formatMessage(messages.incompleteAssignment)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-10 p-0 ml-3 text-break">{displayTitle}</div>
|
||||
<span className="sr-only">
|
||||
, {intl.formatMessage(complete ? messages.completedAssignment : messages.incompleteAssignment)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-10 p-0 ml-2 pl-1 text-break">{displayTitle}</div>
|
||||
<span className="sr-only">
|
||||
, {intl.formatMessage(complete ? messages.completedAssignment : messages.incompleteAssignment)}
|
||||
</span>
|
||||
{due && (
|
||||
<div className="row w-100 m-0 ml-3 pl-3">
|
||||
<small className="text-body pl-2">
|
||||
<FormattedMessage
|
||||
id="learning.outline.sequence-due"
|
||||
defaultMessage="{description} due {assignmentDue}"
|
||||
description="Used below an assignment title"
|
||||
values={{
|
||||
assignmentDue: (
|
||||
<FormattedTime
|
||||
key={`${id}-due`}
|
||||
day="numeric"
|
||||
month="short"
|
||||
year="numeric"
|
||||
hour12={false}
|
||||
timeZoneName="short"
|
||||
value={due}
|
||||
{...timezoneFormatArgs}
|
||||
/>
|
||||
),
|
||||
description: description || '',
|
||||
}}
|
||||
/>
|
||||
</small>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{due && (
|
||||
<div className="row w-100 m-0 ml-3 pl-3">
|
||||
<small className="text-body">
|
||||
<FormattedMessage
|
||||
id="learning.outline.sequence-due"
|
||||
defaultMessage="{description} due {assignmentDue}"
|
||||
description="Used below an assignment title"
|
||||
values={{
|
||||
assignmentDue: (
|
||||
<FormattedTime
|
||||
key={`${id}-due`}
|
||||
day="numeric"
|
||||
month="short"
|
||||
year="numeric"
|
||||
hour12={false}
|
||||
timeZoneName="short"
|
||||
value={due}
|
||||
{...timezoneFormatArgs}
|
||||
/>
|
||||
),
|
||||
description: description || '',
|
||||
}}
|
||||
/>
|
||||
</small>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,14 +9,15 @@ const CertificateAvailableAlert = React.lazy(() => import('./CertificateAvailabl
|
||||
function useCertificateAvailableAlert(courseId) {
|
||||
const {
|
||||
isEnrolled,
|
||||
} = useModel('courses', courseId);
|
||||
} = useModel('courseHomeMeta', courseId);
|
||||
const {
|
||||
datesWidget: {
|
||||
courseDateBlocks,
|
||||
userTimezone,
|
||||
},
|
||||
} = useModel('outline', courseId);
|
||||
const { username } = getAuthenticatedUser();
|
||||
const authenticatedUser = getAuthenticatedUser();
|
||||
const username = authenticatedUser ? authenticatedUser.username : '';
|
||||
|
||||
const certBlock = courseDateBlocks.find(b => b.dateType === 'certificate-available-date');
|
||||
const endBlock = courseDateBlocks.find(b => b.dateType === 'course-end-date');
|
||||
|
||||
@@ -13,7 +13,6 @@ const DAY_MS = 24 * 60 * 60 * 1000; // in ms
|
||||
|
||||
function CourseEndAlert({ payload }) {
|
||||
const {
|
||||
delta,
|
||||
description,
|
||||
endDate,
|
||||
userTimezone,
|
||||
@@ -30,6 +29,7 @@ function CourseEndAlert({ payload }) {
|
||||
);
|
||||
|
||||
let msg;
|
||||
const delta = new Date(endDate) - new Date();
|
||||
if (delta < DAY_MS) {
|
||||
const courseEndTime = (
|
||||
<FormattedTime
|
||||
@@ -88,7 +88,6 @@ function CourseEndAlert({ payload }) {
|
||||
|
||||
CourseEndAlert.propTypes = {
|
||||
payload: PropTypes.shape({
|
||||
delta: PropTypes.number,
|
||||
description: PropTypes.string,
|
||||
endDate: PropTypes.string,
|
||||
userTimezone: PropTypes.string,
|
||||
|
||||
@@ -11,7 +11,7 @@ const WARNING_PERIOD_MS = 14 * 24 * 60 * 60 * 1000; // 14 days
|
||||
export function useCourseEndAlert(courseId) {
|
||||
const {
|
||||
isEnrolled,
|
||||
} = useModel('courses', courseId);
|
||||
} = useModel('courseHomeMeta', courseId);
|
||||
const {
|
||||
datesWidget: {
|
||||
courseDateBlocks,
|
||||
@@ -24,7 +24,6 @@ export function useCourseEndAlert(courseId) {
|
||||
const delta = endBlock ? endDate - new Date() : 0;
|
||||
const isVisible = isEnrolled && endBlock && delta > 0 && delta < WARNING_PERIOD_MS;
|
||||
const payload = {
|
||||
delta,
|
||||
description: endBlock && endBlock.description,
|
||||
endDate: endBlock && endBlock.date,
|
||||
userTimezone,
|
||||
|
||||
@@ -13,7 +13,6 @@ const DAY_MS = 24 * 60 * 60 * 1000; // in ms
|
||||
|
||||
function CourseStartAlert({ payload }) {
|
||||
const {
|
||||
delta,
|
||||
startDate,
|
||||
userTimezone,
|
||||
} = payload;
|
||||
@@ -28,6 +27,7 @@ function CourseStartAlert({ payload }) {
|
||||
/>
|
||||
);
|
||||
|
||||
const delta = new Date(startDate) - new Date();
|
||||
if (delta < DAY_MS) {
|
||||
return (
|
||||
<Alert type={ALERT_TYPES.INFO}>
|
||||
@@ -88,7 +88,6 @@ function CourseStartAlert({ payload }) {
|
||||
|
||||
CourseStartAlert.propTypes = {
|
||||
payload: PropTypes.shape({
|
||||
delta: PropTypes.number,
|
||||
startDate: PropTypes.string,
|
||||
userTimezone: PropTypes.string,
|
||||
}).isRequired,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useAlert } from '../../../../generic/user-messages';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
|
||||
@@ -7,7 +7,7 @@ const CourseStartAlert = React.lazy(() => import('./CourseStartAlert'));
|
||||
function useCourseStartAlert(courseId) {
|
||||
const {
|
||||
isEnrolled,
|
||||
} = useModel('courses', courseId);
|
||||
} = useModel('courseHomeMeta', courseId);
|
||||
const {
|
||||
datesWidget: {
|
||||
courseDateBlocks,
|
||||
@@ -18,14 +18,14 @@ function useCourseStartAlert(courseId) {
|
||||
const startBlock = courseDateBlocks.find(b => b.dateType === 'course-start-date');
|
||||
const delta = startBlock ? new Date(startBlock.date) - new Date() : 0;
|
||||
const isVisible = isEnrolled && startBlock && delta > 0;
|
||||
const payload = {
|
||||
startDate: startBlock && startBlock.date,
|
||||
userTimezone,
|
||||
};
|
||||
|
||||
useAlert(isVisible, {
|
||||
code: 'clientCourseStartAlert',
|
||||
payload: {
|
||||
delta,
|
||||
startDate: startBlock && startBlock.date,
|
||||
userTimezone,
|
||||
},
|
||||
payload: useMemo(() => payload, Object.values(payload).sort()),
|
||||
topic: 'outline-course-alerts',
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
|
||||
import { Button, Hyperlink } from '@edx/paragon';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { Alert } from '../../../../generic/user-messages';
|
||||
import enrollmentMessages from '../../../../alerts/enrollment-alert/messages';
|
||||
import genericMessages from '../../../../generic/messages';
|
||||
import outlineMessages from '../../messages';
|
||||
import { useEnrollClickHandler } from '../../../../alerts/enrollment-alert/hooks';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
|
||||
function PrivateCourseAlert({ intl, payload }) {
|
||||
const {
|
||||
anonymousUser,
|
||||
canEnroll,
|
||||
courseId,
|
||||
} = payload;
|
||||
|
||||
const {
|
||||
org,
|
||||
title,
|
||||
} = useModel('courseHomeMeta', courseId);
|
||||
|
||||
const { enrollClickHandler, loading } = useEnrollClickHandler(
|
||||
courseId,
|
||||
org,
|
||||
intl.formatMessage(enrollmentMessages.success),
|
||||
);
|
||||
|
||||
const enrollNow = (
|
||||
<Button
|
||||
disabled={loading}
|
||||
variant="link"
|
||||
className="p-0 border-0 align-top"
|
||||
style={{ textDecoration: 'underline' }}
|
||||
onClick={enrollClickHandler}
|
||||
>
|
||||
{intl.formatMessage(enrollmentMessages.enrollNowInline)}
|
||||
</Button>
|
||||
);
|
||||
|
||||
const register = (
|
||||
<Hyperlink
|
||||
style={{ textDecoration: 'underline' }}
|
||||
destination={`${getConfig().LMS_BASE_URL}/register?next=${encodeURIComponent(global.location.href)}`}
|
||||
>
|
||||
{intl.formatMessage(genericMessages.registerLowercase)}
|
||||
</Hyperlink>
|
||||
);
|
||||
|
||||
const signIn = (
|
||||
<Hyperlink
|
||||
style={{ textDecoration: 'underline' }}
|
||||
destination={`${getLoginRedirectUrl(global.location.href)}`}
|
||||
>
|
||||
{intl.formatMessage(genericMessages.signInSentenceCase)}
|
||||
</Hyperlink>
|
||||
);
|
||||
|
||||
return (
|
||||
<Alert type="welcome">
|
||||
{anonymousUser && (
|
||||
<>
|
||||
<p className="font-weight-bold">
|
||||
{intl.formatMessage(enrollmentMessages.alert)}
|
||||
</p>
|
||||
<FormattedMessage
|
||||
id="learning.privateCourse.signInOrRegister"
|
||||
description="Prompts the user to sign in or register to see course content."
|
||||
defaultMessage="{signIn} or {register} and then enroll in this course."
|
||||
values={{
|
||||
signIn,
|
||||
register,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{!anonymousUser && (
|
||||
<>
|
||||
<p className="font-weight-bold">{intl.formatMessage(outlineMessages.welcomeTo)} {title}</p>
|
||||
{canEnroll && (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="learning.privateCourse.canEnroll"
|
||||
description="Prompts the user to enroll in the course to see course content."
|
||||
defaultMessage="{enrollNow} to access the full course."
|
||||
values={{ enrollNow }}
|
||||
/>
|
||||
{loading && <FontAwesomeIcon icon={faSpinner} spin />}
|
||||
</>
|
||||
)}
|
||||
{!canEnroll && (
|
||||
<>
|
||||
{intl.formatMessage(enrollmentMessages.alert)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
PrivateCourseAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
payload: PropTypes.shape({
|
||||
anonymousUser: PropTypes.bool,
|
||||
canEnroll: PropTypes.bool,
|
||||
courseId: PropTypes.string,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(PrivateCourseAlert);
|
||||
@@ -0,0 +1,36 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import React, { useContext, useMemo } from 'react';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { ALERT_TYPES, useAlert } from '../../../../generic/user-messages';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
|
||||
const PrivateCourseAlert = React.lazy(() => import('./PrivateCourseAlert'));
|
||||
|
||||
export function usePrivateCourseAlert(courseId) {
|
||||
const { authenticatedUser } = useContext(AppContext);
|
||||
const course = useModel('courseHomeMeta', courseId);
|
||||
const outline = useModel('outline', courseId);
|
||||
const enrolledUser = course && course.isEnrolled !== undefined && course.isEnrolled;
|
||||
const privateOutline = outline && outline.courseBlocks && !outline.courseBlocks.courses;
|
||||
/**
|
||||
* This alert should render if the user is not enrolled AND
|
||||
* 1. the user is anonymous AND the outline is private, OR
|
||||
* 2. the user is authenticated.
|
||||
* */
|
||||
const isVisible = !enrolledUser && (privateOutline || authenticatedUser !== null);
|
||||
const payload = {
|
||||
anonymousUser: authenticatedUser === null,
|
||||
canEnroll: outline && outline.enrollAlert ? outline.enrollAlert.canEnroll : false,
|
||||
courseId,
|
||||
};
|
||||
|
||||
useAlert(isVisible, {
|
||||
code: 'clientPrivateCourseAlert',
|
||||
dismissible: false,
|
||||
payload: useMemo(() => payload, Object.values(payload).sort()),
|
||||
topic: 'outline-private-alerts',
|
||||
type: ALERT_TYPES.WELCOME,
|
||||
});
|
||||
|
||||
return { clientPrivateCourseAlert: PrivateCourseAlert };
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { usePrivateCourseAlert as default } from './hooks';
|
||||
@@ -0,0 +1,11 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
enroll: {
|
||||
id: 'alert.enroll',
|
||||
defaultMessage: 'You must be enrolled in the course to see course content.',
|
||||
description: 'Text instructing the learner to enroll in the course in order to see course content.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -7,7 +7,7 @@ const messages = defineMessages({
|
||||
},
|
||||
collapseAll: {
|
||||
id: 'learning.outline.collapseAll',
|
||||
defaultMessage: 'Collapse All',
|
||||
defaultMessage: 'Collapse all',
|
||||
description: 'Label for button to close all of the collapsible sections',
|
||||
},
|
||||
completedAssignment: {
|
||||
@@ -31,7 +31,7 @@ const messages = defineMessages({
|
||||
},
|
||||
expandAll: {
|
||||
id: 'learning.outline.expandAll',
|
||||
defaultMessage: 'Expand All',
|
||||
defaultMessage: 'Expand all',
|
||||
description: 'Label for button to open all of the collapsible sections',
|
||||
},
|
||||
goal: {
|
||||
@@ -43,11 +43,6 @@ const messages = defineMessages({
|
||||
id: 'learning.outline.goalUnsure',
|
||||
defaultMessage: 'Not sure yet',
|
||||
},
|
||||
goalWelcome: {
|
||||
id: 'learning.outline.goalWelcome',
|
||||
defaultMessage: 'Welcome to',
|
||||
description: 'This precedes the title of the course',
|
||||
},
|
||||
handouts: {
|
||||
id: 'learning.outline.handouts',
|
||||
defaultMessage: 'Course Handouts',
|
||||
@@ -73,7 +68,7 @@ const messages = defineMessages({
|
||||
},
|
||||
resume: {
|
||||
id: 'learning.outline.resume',
|
||||
defaultMessage: 'Resume Course',
|
||||
defaultMessage: 'Resume course',
|
||||
},
|
||||
setGoal: {
|
||||
id: 'learning.outline.setGoal',
|
||||
@@ -112,6 +107,87 @@ const messages = defineMessages({
|
||||
id: 'learning.outline.welcomeMessageShowLessButton',
|
||||
defaultMessage: 'Show Less',
|
||||
},
|
||||
welcomeTo: {
|
||||
id: 'learning.outline.goalWelcome',
|
||||
defaultMessage: 'Welcome to',
|
||||
description: 'This precedes the title of the course',
|
||||
},
|
||||
proctoringInfoPanel: {
|
||||
id: 'learning.proctoringPanel.header',
|
||||
defaultMessage: 'This course contains proctored exams',
|
||||
},
|
||||
notStartedProctoringStatus: {
|
||||
id: 'learning.proctoringPanel.status.notStarted',
|
||||
defaultMessage: 'Not Started',
|
||||
},
|
||||
startedProctoringStatus: {
|
||||
id: 'learning.proctoringPanel.status.started',
|
||||
defaultMessage: 'Started',
|
||||
},
|
||||
submittedProctoringStatus: {
|
||||
id: 'learning.proctoringPanel.status.submitted',
|
||||
defaultMessage: 'Submitted',
|
||||
},
|
||||
verifiedProctoringStatus: {
|
||||
id: 'learning.proctoringPanel.status.verified',
|
||||
defaultMessage: 'Verified',
|
||||
},
|
||||
rejectedProctoringStatus: {
|
||||
id: 'learning.proctoringPanel.status.rejected',
|
||||
defaultMessage: 'Rejected',
|
||||
},
|
||||
errorProctoringStatus: {
|
||||
id: 'learning.proctoringPanel.status.error',
|
||||
defaultMessage: 'Error',
|
||||
},
|
||||
proctoringCurrentStatus: {
|
||||
id: 'learning.proctoringPanel.status',
|
||||
defaultMessage: 'Current Onboarding Status:',
|
||||
},
|
||||
notStartedProctoringMessage: {
|
||||
id: 'learning.proctoringPanel.message.notStarted',
|
||||
defaultMessage: 'You have not started your onboarding exam.',
|
||||
},
|
||||
startedProctoringMessage: {
|
||||
id: 'learning.proctoringPanel.message.started',
|
||||
defaultMessage: 'You have started your onboarding exam.',
|
||||
},
|
||||
submittedProctoringMessage: {
|
||||
id: 'learning.proctoringPanel.message.submitted',
|
||||
defaultMessage: 'You have submitted your onboarding exam.',
|
||||
},
|
||||
verifiedProctoringMessage: {
|
||||
id: 'learning.proctoringPanel.message.verified',
|
||||
defaultMessage: 'You can now take proctored exams in this course.',
|
||||
},
|
||||
rejectedProctoringMessage: {
|
||||
id: 'learning.proctoringPanel.message.rejected',
|
||||
defaultMessage: 'Your onboarding exam has been rejected. Please retry onboarding.',
|
||||
},
|
||||
errorProctoringMessage: {
|
||||
id: 'learning.proctoringPanel.message.error',
|
||||
defaultMessage: 'An error has occurred during your onboarding exam. Please retry onboarding.',
|
||||
},
|
||||
proctoringPanelGeneralInfo: {
|
||||
id: 'learning.proctoringPanel.generalInfo',
|
||||
defaultMessage: 'You must complete the onboarding process prior to taking any proctored exam. ',
|
||||
},
|
||||
proctoringPanelGeneralInfoSubmitted: {
|
||||
id: 'learning.proctoringPanel.generalInfoSubmitted',
|
||||
defaultMessage: 'Your submitted profile is in review.',
|
||||
},
|
||||
proctoringPanelGeneralTime: {
|
||||
id: 'learning.proctoringPanel.generalTime',
|
||||
defaultMessage: 'Onboarding profile review, including identity verification, can take 2+ business days.',
|
||||
},
|
||||
proctoringOnboardingButton: {
|
||||
id: 'learning.proctoringPanel.onboardingButton',
|
||||
defaultMessage: 'Complete Onboarding',
|
||||
},
|
||||
proctoringReviewRequirementsButton: {
|
||||
id: 'learning.proctoringPanel.reviewRequirementsButton',
|
||||
defaultMessage: 'Review instructions and system requirements',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -16,17 +16,23 @@ function CourseDates({ courseId, intl }) {
|
||||
},
|
||||
} = useModel('outline', courseId);
|
||||
|
||||
if (courseDateBlocks.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="mb-4">
|
||||
<h2 className="h6">{intl.formatMessage(messages.dates)}</h2>
|
||||
{courseDateBlocks.map((courseDateBlock) => (
|
||||
<DateSummary
|
||||
key={courseDateBlock.title + courseDateBlock.date}
|
||||
dateBlock={courseDateBlock}
|
||||
userTimezone={userTimezone}
|
||||
/>
|
||||
))}
|
||||
<a className="font-weight-bold ml-4 pl-1" href={datesTabLink}>
|
||||
<h2 className="h4">{intl.formatMessage(messages.dates)}</h2>
|
||||
<ol className="list-unstyled">
|
||||
{courseDateBlocks.map((courseDateBlock) => (
|
||||
<DateSummary
|
||||
key={courseDateBlock.title + courseDateBlock.date}
|
||||
dateBlock={courseDateBlock}
|
||||
userTimezone={userTimezone}
|
||||
/>
|
||||
))}
|
||||
</ol>
|
||||
<a className="font-weight-bold ml-4 pl-1 small" href={datesTabLink}>
|
||||
{intl.formatMessage(messages.allDates)}
|
||||
</a>
|
||||
</section>
|
||||
|
||||
@@ -38,7 +38,7 @@ function CourseGoalCard({
|
||||
<Card.Body>
|
||||
<div className="row w-100 m-0 justify-content-between align-items-center">
|
||||
<div className="col col-8 p-0">
|
||||
<Card.Title className="h6 m-0">{intl.formatMessage(messages.goalWelcome)} {title}</Card.Title>
|
||||
<h2 className="h4 m-0">{intl.formatMessage(messages.welcomeTo)} {title}</h2>
|
||||
</div>
|
||||
<div className="col col-auto p-0">
|
||||
<Button
|
||||
@@ -54,7 +54,7 @@ function CourseGoalCard({
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Card.Text className="my-2 mx-1">{intl.formatMessage(messages.setGoal)}</Card.Text>
|
||||
<Card.Text className="my-2 mx-1 text-dark-500">{intl.formatMessage(messages.setGoal)}</Card.Text>
|
||||
<div className="row w-100 m-0">
|
||||
{goalOptions.map((goal) => {
|
||||
const [goalKey, goalText] = goal;
|
||||
|
||||
@@ -18,8 +18,9 @@ function CourseHandouts({ courseId, intl }) {
|
||||
|
||||
return (
|
||||
<section className="mb-4">
|
||||
<h2 className="h6">{intl.formatMessage(messages.handouts)}</h2>
|
||||
<h2 className="h4">{intl.formatMessage(messages.handouts)}</h2>
|
||||
<LmsHtmlFragment
|
||||
className="small"
|
||||
html={handoutsHtml}
|
||||
title={intl.formatMessage(messages.handouts)}
|
||||
/>
|
||||
|
||||
@@ -14,7 +14,7 @@ import messages from '../messages';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
|
||||
function CourseTools({ courseId, intl }) {
|
||||
const { org } = useModel('courses', courseId);
|
||||
const { org } = useModel('courseHomeMeta', courseId);
|
||||
const {
|
||||
courseTools,
|
||||
} = useModel('outline', courseId);
|
||||
@@ -55,15 +55,17 @@ function CourseTools({ courseId, intl }) {
|
||||
|
||||
return (
|
||||
<section className="mb-4">
|
||||
<h2 className="h6">{intl.formatMessage(messages.tools)}</h2>
|
||||
{courseTools.map((courseTool) => (
|
||||
<div key={courseTool.analyticsId}>
|
||||
<a href={courseTool.url} onClick={() => logClick(courseTool.analyticsId)}>
|
||||
<FontAwesomeIcon icon={renderIcon(courseTool.analyticsId)} className="mr-2" fixedWidth />
|
||||
{courseTool.title}
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
<h2 className="h4">{intl.formatMessage(messages.tools)}</h2>
|
||||
<ul className="list-unstyled">
|
||||
{courseTools.map((courseTool) => (
|
||||
<li key={courseTool.analyticsId} className="small">
|
||||
<a href={courseTool.url} onClick={() => logClick(courseTool.analyticsId)}>
|
||||
<FontAwesomeIcon icon={renderIcon(courseTool.analyticsId)} className="mr-2" fixedWidth />
|
||||
{courseTool.title}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
111
src/course-home/outline-tab/widgets/ProctoringInfoPanel.jsx
Normal file
111
src/course-home/outline-tab/widgets/ProctoringInfoPanel.jsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@edx/paragon';
|
||||
|
||||
import messages from '../messages';
|
||||
import { getProctoringInfoData } from '../../data/api';
|
||||
|
||||
function ProctoringInfoPanel({ courseId, intl }) {
|
||||
const [status, setStatus] = useState('');
|
||||
const [link, setLink] = useState('');
|
||||
const [readableStatus, setReadableStatus] = useState('');
|
||||
|
||||
function getReadableStatusClass(examStatus) {
|
||||
let readableClass = '';
|
||||
if (['created', 'download_software_clicked', 'ready_to_start'].includes(examStatus) || !examStatus) {
|
||||
readableClass = 'notStarted';
|
||||
} else if (['started', 'ready_to_submit'].includes(examStatus)) {
|
||||
readableClass = 'started';
|
||||
} else if (['second_review_required', 'submitted'].includes(examStatus)) {
|
||||
readableClass = 'submitted';
|
||||
} else if (['verified', 'rejected', 'error'].includes(examStatus)) {
|
||||
readableClass = examStatus;
|
||||
}
|
||||
return readableClass;
|
||||
}
|
||||
|
||||
function isNotYetSubmitted(examStatus) {
|
||||
const NO_SHOW_STATES = ['submitted', 'second_review_required', 'verified'];
|
||||
return !NO_SHOW_STATES.includes(examStatus);
|
||||
}
|
||||
|
||||
function getBorderClass(examStatus) {
|
||||
let borderClass = '';
|
||||
if (['submitted', 'second_review_required'].includes(examStatus)) {
|
||||
borderClass = 'proctoring-onboarding-submitted';
|
||||
} else if (examStatus === 'verified') {
|
||||
borderClass = 'proctoring-onboarding-success';
|
||||
}
|
||||
return borderClass;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getProctoringInfoData(courseId)
|
||||
.then(
|
||||
response => {
|
||||
if (response) {
|
||||
setStatus(response.onboarding_status);
|
||||
setLink(response.onboarding_link);
|
||||
setReadableStatus(getReadableStatusClass(response.onboarding_status));
|
||||
}
|
||||
},
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{ link && (
|
||||
<section className={`mb-4 p-3 outline-sidebar-proctoring-panel ${getBorderClass(status)}`}>
|
||||
<h2 className="h4" id="outline-sidebar-upgrade-header">{intl.formatMessage(messages.proctoringInfoPanel)}</h2>
|
||||
<div>
|
||||
{readableStatus && (
|
||||
<>
|
||||
<p className="h6">
|
||||
{intl.formatMessage(messages.proctoringCurrentStatus)} {intl.formatMessage(messages[`${readableStatus}ProctoringStatus`])}
|
||||
</p>
|
||||
<p>
|
||||
{intl.formatMessage(messages[`${readableStatus}ProctoringMessage`])}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{(readableStatus !== 'verified') && (
|
||||
<>
|
||||
<p>
|
||||
{isNotYetSubmitted(status) && (
|
||||
<>
|
||||
{intl.formatMessage(messages.proctoringPanelGeneralInfo)}
|
||||
</>
|
||||
)}
|
||||
{!isNotYetSubmitted(status) && (
|
||||
<>
|
||||
{intl.formatMessage(messages.proctoringPanelGeneralInfoSubmitted)}
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
<p>{intl.formatMessage(messages.proctoringPanelGeneralTime)}</p>
|
||||
</>
|
||||
)}
|
||||
{isNotYetSubmitted(status) && (
|
||||
<Button variant="primary" block href={`${getConfig().LMS_BASE_URL}${link}`}>
|
||||
{intl.formatMessage(messages.proctoringOnboardingButton)}
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="outline-primary" block href="https://support.edx.org/hc/en-us/sections/115004169247-Taking-Timed-and-Proctored-Exams">
|
||||
{intl.formatMessage(messages.proctoringReviewRequirementsButton)}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ProctoringInfoPanel.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(ProctoringInfoPanel);
|
||||
10
src/course-home/outline-tab/widgets/ProctoringInfoPanel.scss
Normal file
10
src/course-home/outline-tab/widgets/ProctoringInfoPanel.scss
Normal file
@@ -0,0 +1,10 @@
|
||||
.outline-sidebar-proctoring-panel {
|
||||
border: 1px solid $dark-500;
|
||||
border-top: 5px solid $brand-600;
|
||||
}
|
||||
.proctoring-onboarding-success {
|
||||
border-top: 5px solid $primary-500;
|
||||
}
|
||||
.proctoring-onboarding-submitted {
|
||||
border-top: 5px solid $dark-500;
|
||||
}
|
||||
@@ -39,7 +39,7 @@ function UpdateGoalSelector({
|
||||
<section className="mb-4">
|
||||
<div className="row w-100 m-0">
|
||||
<div className="col-12 p-0">
|
||||
<label className="h6 m-0" htmlFor="edit-goal-selector">
|
||||
<label className="h4 m-0" htmlFor="edit-goal-selector">
|
||||
{intl.formatMessage(messages.goal)}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
||||
import { sendTrackEvent, sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@edx/paragon';
|
||||
|
||||
import messages from '../messages';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
import VerifiedCert from '../../../generic/assets/edX_verified_certificate.png';
|
||||
import { UpgradeButton } from '../../../generic/upgrade-button';
|
||||
import VerifiedCert from '../../../generic/assets/edX_certificate.png';
|
||||
|
||||
function UpgradeCard({ courseId, intl, onLearnMore }) {
|
||||
const { org } = useModel('courses', courseId);
|
||||
const { org } = useModel('courseHomeMeta', courseId);
|
||||
const {
|
||||
offer,
|
||||
verifiedMode,
|
||||
} = useModel('outline', courseId);
|
||||
|
||||
@@ -24,46 +26,62 @@ function UpgradeCard({ courseId, intl, onLearnMore }) {
|
||||
courserun_key: courseId,
|
||||
};
|
||||
|
||||
const promotionEventProperties = {
|
||||
creative: 'sidebarupsell',
|
||||
name: 'In-Course Verification Prompt',
|
||||
position: 'sidebar-message',
|
||||
promotion_id: 'courseware_verified_certificate_upsell',
|
||||
...eventProperties,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
sendTrackingLogEvent('edx.bi.course.upgrade.sidebarupsell.displayed', eventProperties);
|
||||
sendTrackEvent('Promotion Viewed', promotionEventProperties);
|
||||
});
|
||||
|
||||
const logClick = () => {
|
||||
sendTrackingLogEvent('edx.bi.course.upgrade.sidebarupsell.clicked', eventProperties);
|
||||
sendTrackingLogEvent('edx.course.enrollment.upgrade.clicked', {
|
||||
...eventProperties,
|
||||
location: 'sidebar-message',
|
||||
});
|
||||
sendTrackEvent('Promotion Clicked', promotionEventProperties);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="mb-4 p-4 outline-sidebar-upgrade-card">
|
||||
<h2 className="h6" id="outline-sidebar-upgrade-header">{intl.formatMessage(messages.upgradeTitle)}</h2>
|
||||
<img
|
||||
alt={intl.formatMessage(messages.certAlt)}
|
||||
src={VerifiedCert}
|
||||
style={{ width: '124px' }}
|
||||
/>
|
||||
<div className="float-right d-flex flex-column align-items-center">
|
||||
<Button
|
||||
variant="success"
|
||||
href={verifiedMode.upgradeUrl}
|
||||
onClick={logClick}
|
||||
>
|
||||
{intl.formatMessage(messages.upgradeButton, {
|
||||
price: verifiedMode.price,
|
||||
symbol: verifiedMode.currencySymbol,
|
||||
})}
|
||||
</Button>
|
||||
{onLearnMore && (
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
onClick={onLearnMore}
|
||||
aria-labelledby="outline-sidebar-upgrade-header"
|
||||
>
|
||||
{intl.formatMessage(messages.learnMore)}
|
||||
</Button>
|
||||
)}
|
||||
<section className="mb-4 p-3 outline-sidebar-upgrade-card">
|
||||
<h2 className="h4" id="outline-sidebar-upgrade-header">{intl.formatMessage(messages.upgradeTitle)}</h2>
|
||||
<div className="row w-100 m-0">
|
||||
<div className="col-6 col-md-12 col-lg-3 col-xl-4 p-0 text-md-center text-lg-left">
|
||||
<img
|
||||
alt={intl.formatMessage(messages.certAlt)}
|
||||
className="w-100"
|
||||
src={VerifiedCert}
|
||||
style={{ maxWidth: '10rem' }}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-6 col-md-12 col-lg-9 col-xl-8 p-0 pl-lg-2 text-center mt-md-2 mt-lg-0">
|
||||
<div className="row w-100 m-0 justify-content-center">
|
||||
<UpgradeButton
|
||||
offer={offer}
|
||||
onClick={logClick}
|
||||
verifiedMode={verifiedMode}
|
||||
/>
|
||||
{onLearnMore && (
|
||||
<div className="col-12">
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="pb-0"
|
||||
onClick={onLearnMore}
|
||||
aria-labelledby="outline-sidebar-upgrade-header"
|
||||
>
|
||||
{intl.formatMessage(messages.learnMore)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.outline-sidebar-upgrade-card {
|
||||
border: 1px solid $gray-500;
|
||||
border-top: 5px solid $success-500;
|
||||
border: 1px solid $dark-500;
|
||||
border-top: 5px solid $dark-500;
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ function WelcomeMessage({ courseId, intl }) {
|
||||
<TransitionReplace className="mb-3" enterDuration={200} exitDuration={200}>
|
||||
{showShortMessage ? (
|
||||
<LmsHtmlFragment
|
||||
className="inline-link"
|
||||
data-testid="short-welcome-message-iframe"
|
||||
key="short-html"
|
||||
html={shortWelcomeMessageHtml}
|
||||
@@ -62,6 +63,7 @@ function WelcomeMessage({ courseId, intl }) {
|
||||
/>
|
||||
) : (
|
||||
<LmsHtmlFragment
|
||||
className="inline-link"
|
||||
data-testid="long-welcome-message-iframe"
|
||||
key="full-html"
|
||||
html={welcomeMessageHtml}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { requestCert } from '../data/thunks';
|
||||
|
||||
import { useModel } from '../../generic/model-store';
|
||||
import messages from './messages';
|
||||
import VerifiedCert from '../../generic/assets/edX_verified_certificate.png';
|
||||
import VerifiedCert from '../../generic/assets/edX_certificate.png';
|
||||
|
||||
function CertificateBanner({ intl }) {
|
||||
const {
|
||||
|
||||
@@ -234,6 +234,7 @@ class CoursewareContainer extends Component {
|
||||
courseId={courseId}
|
||||
unitId={routeUnitId}
|
||||
courseStatus={courseStatus}
|
||||
metadataModel="coursewareMeta"
|
||||
>
|
||||
<Course
|
||||
courseId={courseId}
|
||||
@@ -296,7 +297,7 @@ CoursewareContainer.defaultProps = {
|
||||
};
|
||||
|
||||
const currentCourseSelector = createSelector(
|
||||
(state) => state.models.courses || {},
|
||||
(state) => state.models.coursewareMeta || {},
|
||||
(state) => state.courseware.courseId,
|
||||
(coursesById, courseId) => (coursesById[courseId] ? coursesById[courseId] : null),
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -24,7 +24,7 @@ function Course({
|
||||
previousSequenceHandler,
|
||||
unitNavigationHandler,
|
||||
}) {
|
||||
const course = useModel('courses', courseId);
|
||||
const course = useModel('coursewareMeta', courseId);
|
||||
const sequence = useModel('sequences', sequenceId);
|
||||
const section = useModel('sections', sequence ? sequence.sectionId : null);
|
||||
|
||||
@@ -35,21 +35,52 @@ function Course({
|
||||
].filter(element => element != null).map(element => element.title);
|
||||
|
||||
const {
|
||||
accessExpiration,
|
||||
canShowUpgradeSock,
|
||||
celebrations,
|
||||
courseExpiredMessage,
|
||||
offerHtml,
|
||||
offer,
|
||||
org,
|
||||
userTimezone,
|
||||
verifiedMode,
|
||||
} = course;
|
||||
|
||||
// Below the tabs, above the breadcrumbs alerts (appearing in the order listed here)
|
||||
const offerAlert = useOfferAlert(offerHtml, 'course');
|
||||
const accessExpirationAlert = useAccessExpirationAlert(courseExpiredMessage, 'course');
|
||||
const offerAlert = useOfferAlert(offer, userTimezone, 'course');
|
||||
const accessExpirationAlert = useAccessExpirationAlert(accessExpiration, userTimezone, 'course');
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const celebrateFirstSection = celebrations && celebrations.firstSection;
|
||||
const celebrationOpen = shouldCelebrateOnSectionLoad(courseId, sequenceId, unitId, celebrateFirstSection, dispatch);
|
||||
|
||||
// The below block of code should be reverted after the REV1512 experiment
|
||||
const [REV1512FlyoverEnabled, setREV1512FlyoverEnabled] = useState(false);
|
||||
window.enableREV1512Flyover = () => {
|
||||
setREV1512FlyoverEnabled(true);
|
||||
};
|
||||
const getCookie = (name) => {
|
||||
const match = document.cookie.match(`${name}=([^;]*)`);
|
||||
return match ? match[1] : undefined;
|
||||
};
|
||||
const userAgent = typeof window.navigator === 'undefined' ? '' : navigator.userAgent;
|
||||
const isMobile = Boolean(
|
||||
userAgent.match(/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i),
|
||||
);
|
||||
const [REV1512FlyoverVisible, setREV1512FlyoverVisible] = useState(isMobile ? false : !(getCookie(`REV1512FlyoverVisible${courseId}`) === 'false'));
|
||||
const isREV1512FlyoverVisible = () => REV1512FlyoverEnabled && (REV1512FlyoverVisible || getCookie('REV1512FlyoverVisible') === 'true');
|
||||
const toggleREV1512Flyover = () => {
|
||||
const setCookie = (cookieName, value, domain) => {
|
||||
const cookieDomain = (typeof domain === 'undefined') ? '' : `domain=${domain};`;
|
||||
const exdate = new Date();
|
||||
exdate.setHours(exdate.getHours() + 4);
|
||||
const cookieValue = `${escape(value)}; expires=${exdate.toUTCString()}`;
|
||||
document.cookie = `${cookieName}=${cookieValue};${cookieDomain}path=/`;
|
||||
};
|
||||
const isVisible = isREV1512FlyoverVisible();
|
||||
setCookie(`REV1512FlyoverVisible${courseId}`, !isVisible);
|
||||
setREV1512FlyoverVisible(!isVisible);
|
||||
};
|
||||
// The above block of code should be reverted after the REV1512 experiment
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
@@ -67,6 +98,9 @@ function Course({
|
||||
courseId={courseId}
|
||||
sectionId={section ? section.id : null}
|
||||
sequenceId={sequenceId}
|
||||
toggleREV1512Flyover={toggleREV1512Flyover} /* This line should be reverted after REV1512 experiment */
|
||||
REV1512FlyoverEnabled={REV1512FlyoverEnabled} /* This line should be reverted after REV1512 experiment */
|
||||
isREV1512FlyoverVisible={isREV1512FlyoverVisible} /* This line should be reverted after REV1512 experiment */
|
||||
/>
|
||||
<AlertList topic="sequence" />
|
||||
<Sequence
|
||||
@@ -76,6 +110,9 @@ function Course({
|
||||
unitNavigationHandler={unitNavigationHandler}
|
||||
nextSequenceHandler={nextSequenceHandler}
|
||||
previousSequenceHandler={previousSequenceHandler}
|
||||
toggleREV1512Flyover={toggleREV1512Flyover} /* This line should be reverted after REV1512 experiment */
|
||||
isREV1512FlyoverVisible={isREV1512FlyoverVisible} /* This line should be reverted after REV1512 experiment */
|
||||
REV1512FlyoverEnabled={REV1512FlyoverEnabled} /* This line should be reverted after REV1512 experiment */
|
||||
/>
|
||||
{celebrationOpen && (
|
||||
<CelebrationModal
|
||||
@@ -83,7 +120,15 @@ function Course({
|
||||
open
|
||||
/>
|
||||
)}
|
||||
{canShowUpgradeSock && <CourseSock verifiedMode={verifiedMode} />}
|
||||
{canShowUpgradeSock && (
|
||||
<CourseSock
|
||||
courseId={courseId}
|
||||
offer={offer}
|
||||
orgKey={org}
|
||||
pageLocation="Course Content Page"
|
||||
verifiedMode={verifiedMode}
|
||||
/>
|
||||
)}
|
||||
<ContentTools course={course} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -46,7 +46,7 @@ describe('Course', () => {
|
||||
const { models } = store.getState();
|
||||
const sequence = models.sequences[mockData.sequenceId];
|
||||
const section = models.sections[sequence.sectionId];
|
||||
const course = models.courses[mockData.courseId];
|
||||
const course = models.coursewareMeta[mockData.courseId];
|
||||
expect(document.title).toMatch(
|
||||
`${sequence.title} | ${section.title} | ${course.title} | edX`,
|
||||
);
|
||||
@@ -85,23 +85,27 @@ describe('Course', () => {
|
||||
});
|
||||
|
||||
it('displays offer and expiration alert', async () => {
|
||||
const offerText = 'test-offer';
|
||||
const offerId = `${offerText}-id`;
|
||||
const offerHtml = `<div data-testid="${offerId}">${offerText}</div>`;
|
||||
|
||||
const expirationText = 'test-expiration';
|
||||
const expirationId = `${expirationText}-id`;
|
||||
const expirationHtml = `<div data-testid="${expirationId}">${expirationText}</div>`;
|
||||
|
||||
const courseMetadata = Factory.build('courseMetadata', {
|
||||
offer_html: offerHtml,
|
||||
course_expired_message: expirationHtml,
|
||||
access_expiration: {
|
||||
expiration_date: '2080-01-01T12:00:00Z',
|
||||
masquerading_expired_course: false,
|
||||
upgrade_deadline: null,
|
||||
upgrade_url: null,
|
||||
},
|
||||
offer: {
|
||||
code: 'EDXWELCOME',
|
||||
expiration_date: '2070-01-01T12:00:00Z',
|
||||
original_price: '$100',
|
||||
discounted_price: '$85',
|
||||
percentage: 15,
|
||||
upgrade_url: 'https://example.com/upgrade',
|
||||
},
|
||||
});
|
||||
const testStore = await initializeTestStore({ courseMetadata, excludeFetchSequence: true }, false);
|
||||
render(<Course {...mockData} courseId={courseMetadata.id} />, { store: testStore });
|
||||
|
||||
expect(await screen.findByTestId(offerId)).toHaveTextContent(offerText);
|
||||
expect(screen.getByTestId(expirationId)).toHaveTextContent(expirationText);
|
||||
await screen.findByText('EDXWELCOME');
|
||||
await screen.findByText('Audit Access Expires');
|
||||
});
|
||||
|
||||
it('passes handlers to the sequence', async () => {
|
||||
|
||||
@@ -36,8 +36,11 @@ export default function CourseBreadcrumbs({
|
||||
courseId,
|
||||
sectionId,
|
||||
sequenceId,
|
||||
toggleREV1512Flyover, /* This line should be reverted after the REV1512 experiment */
|
||||
REV1512FlyoverEnabled, /* This line should be reverted after the REV1512 experiment */
|
||||
isREV1512FlyoverVisible, /* This line should be reverted after the REV1512 experiment */
|
||||
}) {
|
||||
const course = useModel('courses', courseId);
|
||||
const course = useModel('coursewareMeta', courseId);
|
||||
const sequence = useModel('sequences', sequenceId);
|
||||
const section = useModel('sections', sectionId);
|
||||
const courseStatus = useSelector(state => state.courseware.courseStatus);
|
||||
@@ -54,6 +57,12 @@ export default function CourseBreadcrumbs({
|
||||
return [];
|
||||
}, [courseStatus, sequenceStatus]);
|
||||
|
||||
// These should be reverted after the REV1512 experiment
|
||||
const userAgent = typeof window.navigator === 'undefined' ? '' : navigator.userAgent;
|
||||
const isMobile = Boolean(
|
||||
userAgent.match(/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i),
|
||||
);
|
||||
|
||||
return (
|
||||
<nav aria-label="breadcrumb" className="my-4">
|
||||
<ol className="list-unstyled d-flex m-0">
|
||||
@@ -82,6 +91,22 @@ export default function CourseBreadcrumbs({
|
||||
{label}
|
||||
</CourseBreadcrumb>
|
||||
))}
|
||||
{/* The below block of code should be reverted after the REV1512 experiment */}
|
||||
{REV1512FlyoverEnabled
|
||||
&& !isMobile && (
|
||||
<div
|
||||
className="toggleFlyoverButton"
|
||||
aria-hidden="true"
|
||||
style={{ marginLeft: 'auto', marginTop: '-16px', borderBottom: isREV1512FlyoverVisible() ? '2px solid #00262b' : 'none' }}
|
||||
onClick={() => {
|
||||
toggleREV1512Flyover();
|
||||
}}
|
||||
>
|
||||
<svg width="54" height="40" viewBox="0 0 54 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0.5" y="0.5" width="53" height="39" rx="1.5" fill="white" stroke="#E7E8E9" /><path d="M36 20C36 15.6 32.4 12 28 12C27.7 12 27.5 12.2 27.5 12.5C27.5 12.8 27.7 13 28 13C31.85 13 35 16.15 35 20C35 23.85 31.85 27 28 27C24.15 27 21 23.85 21 20C21 19.7 20.8 19.5 20.5 19.5C20.3 19.5 20.1 19.65 20.05 19.8C20 19.85 20 19.95 20 20C20 24.4 23.6 28 28 28C32.4 28 36 24.4 36 20Z" fill="black" stroke="black" strokeWidth="0.6" /><path d="M23.1065 14.52C22.9403 14.36 22.691 14.36 22.5247 14.52C22.3585 14.68 22.3585 14.92 22.5247 15.08C22.6078 15.16 22.7325 15.2 22.8156 15.2C22.9403 15.2 23.0234 15.16 23.1065 15.08C23.2312 14.96 23.2312 14.68 23.1065 14.52Z" fill="black" stroke="black" strokeWidth="0.6" /><path d="M27.6848 15.2C27.3939 15.2 27.2 15.3973 27.2 15.6932V19.6384C27.2 19.6877 27.2 19.7863 27.2484 19.8356C27.2969 19.8849 27.2969 19.9343 27.3454 19.9836L29.5757 22.2521C29.6727 22.3507 29.8181 22.4 29.9151 22.4C30.0121 22.4 30.1575 22.3507 30.2545 22.2521C30.4484 22.0548 30.4484 21.7589 30.2545 21.5617L28.1696 19.4411V15.6932C28.1696 15.3973 27.9757 15.2 27.6848 15.2Z" fill="black" stroke="black" strokeWidth="0.6" /><circle cx="35.5" cy="14.5" r="4.5" fill="#C32D3A" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</ol>
|
||||
</nav>
|
||||
);
|
||||
@@ -91,6 +116,9 @@ CourseBreadcrumbs.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
sectionId: PropTypes.string,
|
||||
sequenceId: PropTypes.string,
|
||||
toggleREV1512Flyover: PropTypes.func.isRequired, /* This line should be reverted after the REV1512 experiment */
|
||||
REV1512FlyoverEnabled: PropTypes.bool.isRequired, /* This line should be reverted after the REV1512 experiment */
|
||||
isREV1512FlyoverVisible: PropTypes.func.isRequired, /* This line should be reverted after the REV1512 experiment */
|
||||
};
|
||||
|
||||
CourseBreadcrumbs.defaultProps = {
|
||||
|
||||
@@ -14,7 +14,7 @@ import { useModel } from '../../../generic/model-store';
|
||||
function CelebrationModal({
|
||||
courseId, intl, open, ...rest
|
||||
}) {
|
||||
const { org } = useModel('courses', courseId);
|
||||
const { org } = useModel('coursewareMeta', courseId);
|
||||
|
||||
const layout = layoutGenerator({
|
||||
mobile: 0,
|
||||
|
||||
@@ -52,7 +52,7 @@ function shouldCelebrateOnSectionLoad(courseId, sequenceId, unitId, celebrateFir
|
||||
|
||||
// Update our local copy of course data from LMS
|
||||
dispatch(updateModel({
|
||||
modelType: 'courses',
|
||||
modelType: 'coursewareMeta',
|
||||
model: {
|
||||
id: courseId,
|
||||
celebrations: {
|
||||
|
||||
54
src/courseware/course/course-exit/CatalogSuggestion.jsx
Normal file
54
src/courseware/course/course-exit/CatalogSuggestion.jsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import {
|
||||
FormattedMessage, injectIntl, intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
import { faSearch } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
|
||||
import messages from './messages';
|
||||
import { logClick } from './utils';
|
||||
|
||||
function CatalogSuggestion({ intl, variant }) {
|
||||
const { courseId } = useSelector(state => state.courseware);
|
||||
const { org } = useModel('coursewareMeta', courseId);
|
||||
const { administrator } = getAuthenticatedUser();
|
||||
|
||||
const searchOurCatalogLink = (
|
||||
<Hyperlink
|
||||
style={{ textDecoration: 'underline' }}
|
||||
destination={getConfig().SEARCH_CATALOG_URL}
|
||||
className="text-reset"
|
||||
onClick={() => logClick(org, courseId, administrator, 'catalog_search', { variant })}
|
||||
>
|
||||
{intl.formatMessage(messages.searchOurCatalogLink)}
|
||||
</Hyperlink>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="row w-100 mx-0 my-2 justify-content-center">
|
||||
<div className="col col-md-8 p-4 bg-info-100 text-center">
|
||||
<FontAwesomeIcon icon={faSearch} style={{ width: '20px' }} />
|
||||
<FormattedMessage
|
||||
id="courseExit.catalogSearchSuggestion"
|
||||
defaultMessage="Looking to learn more? {searchOurCatalogLink} to find more courses and programs to explore."
|
||||
values={{ searchOurCatalogLink }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
CatalogSuggestion.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
variant: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CatalogSuggestion);
|
||||
@@ -12,10 +12,12 @@ import { Alert, Button, Hyperlink } from '@edx/paragon';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
|
||||
import CatalogSuggestion from './CatalogSuggestion';
|
||||
import CelebrationMobile from './assets/celebration_456x328.gif';
|
||||
import CelebrationDesktop from './assets/celebration_750x540.gif';
|
||||
import certificate from '../../../generic/assets/edX_verified_certificate.png';
|
||||
import certificateLocked from '../../../generic/assets/edX_locked_verified_certificate.png';
|
||||
import certificate from '../../../generic/assets/edX_certificate.png';
|
||||
import certificateLocked from '../../../generic/assets/edX_locked_certificate.png';
|
||||
import { FormattedPricing } from '../../../generic/upgrade-button';
|
||||
import messages from './messages';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
import { requestCert } from '../../../course-home/data/thunks';
|
||||
@@ -42,12 +44,14 @@ function CourseCelebration({ intl }) {
|
||||
certificateData,
|
||||
end,
|
||||
linkedinAddToProfileUrl,
|
||||
offer,
|
||||
org,
|
||||
relatedPrograms,
|
||||
title,
|
||||
verifiedMode,
|
||||
verifyIdentityUrl,
|
||||
verificationStatus,
|
||||
} = useModel('courses', courseId);
|
||||
} = useModel('coursewareMeta', courseId);
|
||||
|
||||
const {
|
||||
certStatus,
|
||||
@@ -85,10 +89,12 @@ function CourseCelebration({ intl }) {
|
||||
</Hyperlink>
|
||||
);
|
||||
|
||||
let buttonPrefix = null;
|
||||
let buttonLocation;
|
||||
let buttonText;
|
||||
let buttonVariant = 'outline-primary';
|
||||
let buttonEvent = null;
|
||||
let buttonSuffix = null;
|
||||
let certificateImage = certificate;
|
||||
let footnote;
|
||||
let message;
|
||||
@@ -117,9 +123,22 @@ function CourseCelebration({ intl }) {
|
||||
buttonLocation = downloadUrl;
|
||||
buttonText = intl.formatMessage(messages.downloadButton);
|
||||
}
|
||||
if (linkedinAddToProfileUrl) {
|
||||
buttonPrefix = (
|
||||
<Button
|
||||
className="mr-3"
|
||||
href={linkedinAddToProfileUrl}
|
||||
onClick={() => logClick(org, courseId, administrator, 'linkedin_add_to_profile')}
|
||||
style={{ backgroundColor: LINKEDIN_BLUE, border: 'none' }}
|
||||
>
|
||||
<FontAwesomeIcon icon={faLinkedinIn} className="mr-3" />
|
||||
{`${intl.formatMessage(messages.linkedinAddToProfileButton)}`}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
buttonEvent = 'view_cert';
|
||||
visitEvent = 'celebration_with_cert';
|
||||
footnote = <DashboardFootnote />;
|
||||
footnote = <DashboardFootnote variant={visitEvent} />;
|
||||
break;
|
||||
case 'earned_but_not_available': {
|
||||
const endDate = <FormattedDate value={end} day="numeric" month="long" year="numeric" />;
|
||||
@@ -146,21 +165,33 @@ function CourseCelebration({ intl }) {
|
||||
</>
|
||||
);
|
||||
visitEvent = 'celebration_with_unavailable_cert';
|
||||
footnote = <DashboardFootnote />;
|
||||
footnote = <DashboardFootnote variant={visitEvent} />;
|
||||
break;
|
||||
}
|
||||
case 'requesting':
|
||||
buttonText = intl.formatMessage(messages.requestCertificateButton);
|
||||
// The requesting status needs a different button because it does a POST instead of a GET.
|
||||
// So we don't set buttonLocation and instead define a custom button as a buttonPrefix.
|
||||
buttonEvent = 'request_cert';
|
||||
buttonPrefix = (
|
||||
<Button
|
||||
variant={buttonVariant}
|
||||
onClick={() => {
|
||||
logClick(org, courseId, administrator, buttonEvent);
|
||||
dispatch(requestCert(courseId));
|
||||
}}
|
||||
>
|
||||
{intl.formatMessage(messages.requestCertificateButton)}
|
||||
</Button>
|
||||
);
|
||||
certHeader = intl.formatMessage(messages.certificateHeaderRequestable);
|
||||
message = (<p>{intl.formatMessage(messages.requestCertificateBodyText)}</p>);
|
||||
visitEvent = 'celebration_with_requestable_cert';
|
||||
footnote = <DashboardFootnote />;
|
||||
footnote = <DashboardFootnote variant={visitEvent} />;
|
||||
break;
|
||||
case 'unverified':
|
||||
certHeader = intl.formatMessage(messages.certificateHeaderUnverified);
|
||||
visitEvent = 'celebration_unverified';
|
||||
footnote = <DashboardFootnote />;
|
||||
footnote = <DashboardFootnote variant={visitEvent} />;
|
||||
if (verificationStatus === 'pending') {
|
||||
message = (<p>{intl.formatMessage(messages.verificationPending)}</p>);
|
||||
} else {
|
||||
@@ -192,7 +223,7 @@ function CourseCelebration({ intl }) {
|
||||
assignments in this course. Upon completion, you will receive a verified certificate which is a
|
||||
valuable credential to improve your job prospects and advance your career, or highlight your
|
||||
certificate in school applications."
|
||||
values={{ price: verifiedMode.currencySymbol + verifiedMode.price }}
|
||||
values={{ price: <FormattedPricing inline offer={offer} verifiedMode={verifiedMode} /> }}
|
||||
/>
|
||||
<br />
|
||||
{getConfig().SUPPORT_URL_VERIFIED_CERTIFICATE && (
|
||||
@@ -210,13 +241,29 @@ function CourseCelebration({ intl }) {
|
||||
buttonEvent = 'upgrade';
|
||||
buttonLocation = verifiedMode.upgradeUrl;
|
||||
buttonVariant = 'primary';
|
||||
if (offer) {
|
||||
buttonSuffix = (
|
||||
<span className="ml-2 align-middle">
|
||||
<FormattedMessage
|
||||
id="courseCelebration.upgradeDiscountCodePrompt"
|
||||
defaultMessage="Use code {code} at checkout for {percent}% off!"
|
||||
values={{
|
||||
code: (<b>{offer.code}</b>),
|
||||
percent: offer.percentage,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
certificateImage = certificateLocked;
|
||||
visitEvent = 'celebration_upgrade';
|
||||
if (verifiedMode.accessExpirationDate) {
|
||||
footnote = <UpgradeFootnote deadline={verifiedMode.accessExpirationDate} href={verifiedMode.upgradeUrl} />;
|
||||
} else {
|
||||
footnote = <DashboardFootnote />;
|
||||
footnote = <DashboardFootnote variant={visitEvent} />;
|
||||
}
|
||||
} else {
|
||||
visitEvent = 'celebration_audit_no_upgrade';
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@@ -228,7 +275,7 @@ function CourseCelebration({ intl }) {
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{`${intl.formatMessage(messages.congratulationsHeader)} | ${getConfig().SITE_NAME}`}</title>
|
||||
<title>{`${intl.formatMessage(messages.congratulationsHeader)} | ${title} | ${getConfig().SITE_NAME}`}</title>
|
||||
</Helmet>
|
||||
<div className="row w-100 mx-0 mb-4 px-5 py-4 border border-light">
|
||||
<div className="col-12 p-0 h2 text-center">
|
||||
@@ -267,39 +314,19 @@ function CourseCelebration({ intl }) {
|
||||
<div className="col order-1 order-md-0 pl-0 pr-0 pr-md-5">
|
||||
<div className="h4">{certHeader}</div>
|
||||
{message}
|
||||
{/* The requesting status needs a different button because it does a POST instead of a GET */}
|
||||
{certStatus === 'requesting' && (
|
||||
<Button
|
||||
variant={buttonVariant}
|
||||
onClick={() => {
|
||||
logClick(org, courseId, administrator, buttonEvent);
|
||||
dispatch(requestCert(courseId));
|
||||
}}
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
)}
|
||||
{certStatus === 'downloadable' && linkedinAddToProfileUrl && (
|
||||
<Button
|
||||
className="mr-3 mt-2"
|
||||
href={linkedinAddToProfileUrl}
|
||||
onClick={() => logClick(org, courseId, administrator, 'linkedin_add_to_profile')}
|
||||
style={{ backgroundColor: LINKEDIN_BLUE, border: 'none' }}
|
||||
>
|
||||
<FontAwesomeIcon icon={faLinkedinIn} className="mr-3" />
|
||||
{`${intl.formatMessage(messages.linkedinAddToProfileButton)}`}
|
||||
</Button>
|
||||
)}
|
||||
{buttonLocation && (
|
||||
<Button
|
||||
className="mt-2"
|
||||
variant={buttonVariant}
|
||||
href={buttonLocation}
|
||||
onClick={() => logClick(org, courseId, administrator, buttonEvent)}
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
)}
|
||||
<div className="mt-2">
|
||||
{buttonPrefix}
|
||||
{buttonLocation && (
|
||||
<Button
|
||||
variant={buttonVariant}
|
||||
href={buttonLocation}
|
||||
onClick={() => logClick(org, courseId, administrator, buttonEvent)}
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
)}
|
||||
{buttonSuffix}
|
||||
</div>
|
||||
</div>
|
||||
{certStatus !== 'unverified' && (
|
||||
<div className="col-12 order-0 col-md-3 order-md-1 w-100 mb-3 p-0 text-center">
|
||||
@@ -323,6 +350,7 @@ function CourseCelebration({ intl }) {
|
||||
/>
|
||||
))}
|
||||
{footnote}
|
||||
<CatalogSuggestion variant={visitEvent} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useSelector } from 'react-redux';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
import CourseCelebration from './CourseCelebration';
|
||||
import CourseInProgress from './CourseInProgress';
|
||||
import CourseNonPassing from './CourseNonPassing';
|
||||
import { COURSE_EXIT_MODES, getCourseExitMode } from './utils';
|
||||
import messages from './messages';
|
||||
@@ -18,6 +19,8 @@ function CourseExit({ intl }) {
|
||||
let body = null;
|
||||
if (mode === COURSE_EXIT_MODES.nonPassing) {
|
||||
body = (<CourseNonPassing />);
|
||||
} else if (mode === COURSE_EXIT_MODES.inProgress) {
|
||||
body = (<CourseInProgress />);
|
||||
} else if (mode === COURSE_EXIT_MODES.celebration) {
|
||||
body = (<CourseCelebration />);
|
||||
} else {
|
||||
|
||||
@@ -13,6 +13,7 @@ import initializeStore from '../../../store';
|
||||
import executeThunk from '../../../utils';
|
||||
import CourseCelebration from './CourseCelebration';
|
||||
import CourseExit from './CourseExit';
|
||||
import CourseInProgress from './CourseInProgress';
|
||||
import CourseNonPassing from './CourseNonPassing';
|
||||
|
||||
initializeMockApp();
|
||||
@@ -55,6 +56,9 @@ describe('Course Exit Pages', () => {
|
||||
cert_status: 'downloadable',
|
||||
cert_web_view_url: '/certificates/cooluuidgoeshere',
|
||||
},
|
||||
enrollment: {
|
||||
is_active: true,
|
||||
},
|
||||
});
|
||||
await fetchAndRender(<CourseExit />);
|
||||
expect(screen.getByText('Congratulations!')).toBeInTheDocument();
|
||||
@@ -65,6 +69,9 @@ describe('Course Exit Pages', () => {
|
||||
certificate_data: {
|
||||
cert_status: 'unverified',
|
||||
},
|
||||
enrollment: {
|
||||
is_active: true,
|
||||
},
|
||||
user_has_passing_grade: false,
|
||||
});
|
||||
await fetchAndRender(<CourseExit />);
|
||||
@@ -287,4 +294,17 @@ describe('Course Exit Pages', () => {
|
||||
expect(screen.getByRole('link', { name: 'View grades' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Course in progress experience', () => {
|
||||
it('Displays link to dates tab', async () => {
|
||||
setMetadata({ user_has_passing_grade: false });
|
||||
const courseBlocks = buildSimpleCourseBlocks(defaultMetadata.id, defaultMetadata.name,
|
||||
{ hasScheduledContent: true });
|
||||
axiosMock.onGet(courseBlocksUrlRegExp).reply(200, courseBlocks);
|
||||
|
||||
await fetchAndRender(<CourseInProgress />);
|
||||
expect(screen.getByText('More content is coming soon!')).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'View course schedule' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
63
src/courseware/course/course-exit/CourseInProgress.jsx
Normal file
63
src/courseware/course/course-exit/CourseInProgress.jsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Alert, Button } from '@edx/paragon';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
|
||||
import CatalogSuggestion from './CatalogSuggestion';
|
||||
import DashboardFootnote from './DashboardFootnote';
|
||||
import messages from './messages';
|
||||
import { logClick, logVisit } from './utils';
|
||||
|
||||
function CourseInProgress({ intl }) {
|
||||
const { courseId } = useSelector(state => state.courseware);
|
||||
const { org, tabs, title } = useModel('coursewareMeta', courseId);
|
||||
const { administrator } = getAuthenticatedUser();
|
||||
|
||||
// Get dates tab link for 'view course schedule' button
|
||||
const datesTab = tabs.find(tab => tab.slug === 'dates');
|
||||
const datesTabLink = datesTab && datesTab.url;
|
||||
|
||||
useEffect(() => logVisit(org, courseId, administrator, 'in_progress'), [org, courseId, administrator]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{`${intl.formatMessage(messages.endOfCourseTitle)} | ${title} | ${getConfig().SITE_NAME}`}</title>
|
||||
</Helmet>
|
||||
<div className="row w-100 mx-0 mb-4 px-5 py-4 border border-light justify-content-center">
|
||||
<div className="col-12 p-0 h2 text-center">
|
||||
{ intl.formatMessage(messages.courseInProgressHeader) }
|
||||
</div>
|
||||
<Alert variant="primary" className="col col-lg-10 mt-4 d-flex">
|
||||
<div className="row w-100 m-0 align-items-start">
|
||||
<div className="flex-grow-1 col-md p-0">{ intl.formatMessage(messages.courseInProgressDescription) }</div>
|
||||
{datesTabLink && (
|
||||
<Button
|
||||
variant="primary"
|
||||
className="flex-shrink-0 mt-3 mt-md-0 mb-1 mb-md-0 ml-md-5"
|
||||
href={datesTabLink}
|
||||
onClick={() => logClick(org, courseId, administrator, 'view_dates_tab')}
|
||||
>
|
||||
{intl.formatMessage(messages.viewCourseScheduleButton)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Alert>
|
||||
<DashboardFootnote variant="in_progress" />
|
||||
<CatalogSuggestion variant="in_progress" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
CourseInProgress.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CourseInProgress);
|
||||
@@ -9,13 +9,14 @@ import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
|
||||
import CatalogSuggestion from './CatalogSuggestion';
|
||||
import DashboardFootnote from './DashboardFootnote';
|
||||
import messages from './messages';
|
||||
import { logClick, logVisit } from './utils';
|
||||
|
||||
function CourseNonPassing({ intl }) {
|
||||
const { courseId } = useSelector(state => state.courseware);
|
||||
const { org, tabs } = useModel('courses', courseId);
|
||||
const { org, tabs, title } = useModel('coursewareMeta', courseId);
|
||||
const { administrator } = getAuthenticatedUser();
|
||||
|
||||
// Get progress tab link for 'view grades' button
|
||||
@@ -27,7 +28,7 @@ function CourseNonPassing({ intl }) {
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{`${intl.formatMessage(messages.endOfCourseTitle)} | ${getConfig().SITE_NAME}`}</title>
|
||||
<title>{`${intl.formatMessage(messages.endOfCourseTitle)} | ${title} | ${getConfig().SITE_NAME}`}</title>
|
||||
</Helmet>
|
||||
<div className="row w-100 mx-0 mb-4 px-5 py-4 border border-light justify-content-center">
|
||||
<div className="col-12 p-0 h2 text-center">
|
||||
@@ -39,7 +40,7 @@ function CourseNonPassing({ intl }) {
|
||||
{progressLink && (
|
||||
<Button
|
||||
variant="primary"
|
||||
className="flex-shrink-0 mt-3 mt-sm-0 mb-1 mb-sm-0 ml-2 ml-sm-5"
|
||||
className="flex-shrink-0 mt-3 mt-sm-0 mb-1 mb-sm-0 ml-sm-5"
|
||||
href={progressLink}
|
||||
onClick={() => logClick(org, courseId, administrator, 'view_grades')}
|
||||
>
|
||||
@@ -48,7 +49,8 @@ function CourseNonPassing({ intl }) {
|
||||
)}
|
||||
</div>
|
||||
</Alert>
|
||||
<DashboardFootnote />
|
||||
<DashboardFootnote variant="nonpassing" />
|
||||
<CatalogSuggestion variant="nonpassing" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import {
|
||||
FormattedMessage, injectIntl, intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
@@ -7,15 +10,23 @@ import { Hyperlink } from '@edx/paragon';
|
||||
import { faCalendarAlt } from '@fortawesome/free-regular-svg-icons';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
|
||||
import Footnote from './Footnote';
|
||||
import messages from './messages';
|
||||
import { logClick } from './utils';
|
||||
|
||||
function DashboardFootnote({ intl, variant }) {
|
||||
const { courseId } = useSelector(state => state.courseware);
|
||||
const { org } = useModel('coursewareMeta', courseId);
|
||||
const { administrator } = getAuthenticatedUser();
|
||||
|
||||
function DashboardFootnote({ intl }) {
|
||||
const dashboardLink = (
|
||||
<Hyperlink
|
||||
style={{ textDecoration: 'underline' }}
|
||||
destination={`${getConfig().LMS_BASE_URL}/dashboard`}
|
||||
className="text-reset"
|
||||
onClick={() => logClick(org, courseId, administrator, 'dashboard_footnote', { variant })}
|
||||
>
|
||||
{intl.formatMessage(messages.dashboardLink)}
|
||||
</Hyperlink>
|
||||
@@ -37,6 +48,7 @@ function DashboardFootnote({ intl }) {
|
||||
|
||||
DashboardFootnote.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
variant: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(DashboardFootnote);
|
||||
|
||||
@@ -4,10 +4,7 @@ import PropTypes from 'prop-types';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Alert, Button, Hyperlink } from '@edx/paragon';
|
||||
import microBachelorsCertImage from '../../../generic/assets/edX_microBachelors_certificate.png';
|
||||
import microMastersCertImage from '../../../generic/assets/edX_microMasters_certificate.png';
|
||||
import professionalCertImage from '../../../generic/assets/edX_professionalCertificate_certificate.png';
|
||||
import xSeriesCertImage from '../../../generic/assets/edX_xSeries_certificate.png';
|
||||
import certImage from '../../../generic/assets/edX_certificate.png';
|
||||
import messages from './messages';
|
||||
|
||||
/**
|
||||
@@ -20,6 +17,8 @@ import messages from './messages';
|
||||
* four aforementioned types. This will not impact the parent components (i.e. CourseCelebration will render normally).
|
||||
*/
|
||||
|
||||
const programTypes = ['microbachelors', 'micromasters', 'professional-certificate', 'xseries'];
|
||||
|
||||
function ProgramCompletion({
|
||||
intl,
|
||||
progress,
|
||||
@@ -27,28 +26,10 @@ function ProgramCompletion({
|
||||
type,
|
||||
url,
|
||||
}) {
|
||||
if (progress.notStarted !== 0 || progress.inProgress !== 0) {
|
||||
if (!programTypes.includes(type) || progress.notStarted !== 0 || progress.inProgress !== 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let certImage;
|
||||
switch (type) {
|
||||
case 'microbachelors':
|
||||
certImage = microBachelorsCertImage;
|
||||
break;
|
||||
case 'micromasters':
|
||||
certImage = microMastersCertImage;
|
||||
break;
|
||||
case 'professional-certificate':
|
||||
certImage = professionalCertImage;
|
||||
break;
|
||||
case 'xseries':
|
||||
certImage = xSeriesCertImage;
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
const programLink = (
|
||||
<Hyperlink
|
||||
style={{ textDecoration: 'underline' }}
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useModel } from '../../../generic/model-store';
|
||||
|
||||
function UpgradeFootnote({ deadline, href, intl }) {
|
||||
const { courseId } = useSelector(state => state.courseware);
|
||||
const { org } = useModel('courses', courseId);
|
||||
const { org } = useModel('coursewareMeta', courseId);
|
||||
const { administrator } = getAuthenticatedUser();
|
||||
|
||||
const upgradeLink = (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import CourseExit from './CourseExit';
|
||||
import { getCourseExitText } from './utils';
|
||||
import { getCourseExitNavigation } from './utils';
|
||||
|
||||
export { CourseExit, getCourseExitText };
|
||||
export { CourseExit, getCourseExitNavigation };
|
||||
|
||||
@@ -44,6 +44,14 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Four people raising their hands in celebration',
|
||||
description: 'Alt text used to describe celebratory image',
|
||||
},
|
||||
courseInProgressDescription: {
|
||||
id: 'courseExit.courseInProgressDescription',
|
||||
defaultMessage: 'It looks like there is more content in this course that will be released in the future. Look out for email updates or check back on your course for when this content will be available.',
|
||||
},
|
||||
courseInProgressHeader: {
|
||||
id: 'courseExit.courseInProgressHeader',
|
||||
defaultMessage: 'More content is coming soon!',
|
||||
},
|
||||
dashboardLink: {
|
||||
id: 'courseExit.dashboardLink',
|
||||
defaultMessage: 'Dashboard',
|
||||
@@ -114,6 +122,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Request certificate',
|
||||
description: 'Button to request the course certificate',
|
||||
},
|
||||
searchOurCatalogLink: {
|
||||
id: 'courseExit.searchOurCatalogLink',
|
||||
defaultMessage: 'Search our catalog',
|
||||
description: 'First part of a sentence that continues afterward',
|
||||
},
|
||||
shareHeader: {
|
||||
id: 'courseCelebration.shareHeader',
|
||||
defaultMessage: 'You have completed your course. Share your success on social media or email.',
|
||||
@@ -149,6 +162,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'View my certificate',
|
||||
description: 'Button to view the course certificate',
|
||||
},
|
||||
viewCourseScheduleButton: {
|
||||
id: 'courseExit.viewCourseScheduleButton',
|
||||
defaultMessage: 'View course schedule',
|
||||
description: 'Button to view the course schedule',
|
||||
},
|
||||
viewCoursesButton: {
|
||||
id: 'courseExit.viewCoursesButton',
|
||||
defaultMessage: 'View my courses',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
|
||||
@@ -8,6 +9,7 @@ const COURSE_EXIT_MODES = {
|
||||
disabled: 0,
|
||||
celebration: 1,
|
||||
nonPassing: 2,
|
||||
inProgress: 3,
|
||||
};
|
||||
|
||||
// These are taken from the edx-platform `get_cert_data` function found in lms/courseware/views/views.py
|
||||
@@ -28,10 +30,14 @@ function getCourseExitMode(courseId) {
|
||||
const {
|
||||
certificateData,
|
||||
courseExitPageIsActive,
|
||||
hasScheduledContent,
|
||||
isEnrolled,
|
||||
userHasPassingGrade,
|
||||
} = useModel('courses', courseId);
|
||||
} = useModel('coursewareMeta', courseId);
|
||||
|
||||
if (!courseExitPageIsActive) {
|
||||
const authenticatedUser = getAuthenticatedUser();
|
||||
|
||||
if (!courseExitPageIsActive || !authenticatedUser || !isEnrolled) {
|
||||
return COURSE_EXIT_MODES.disabled;
|
||||
}
|
||||
|
||||
@@ -49,6 +55,9 @@ function getCourseExitMode(courseId) {
|
||||
isEligibleForCertificate = NON_CERTIFICATE_STATUSES.indexOf(certStatus) === -1;
|
||||
}
|
||||
|
||||
if (hasScheduledContent && !userHasPassingGrade) {
|
||||
return COURSE_EXIT_MODES.inProgress;
|
||||
}
|
||||
if (isEligibleForCertificate && !userHasPassingGrade) {
|
||||
return COURSE_EXIT_MODES.nonPassing;
|
||||
}
|
||||
@@ -58,21 +67,28 @@ function getCourseExitMode(courseId) {
|
||||
return COURSE_EXIT_MODES.disabled;
|
||||
}
|
||||
|
||||
// Returns null if course exit is either not active or not handling the current case
|
||||
function getCourseExitText(courseId, intl) {
|
||||
switch (getCourseExitMode(courseId)) {
|
||||
// Returns null in order to render the default navigation text
|
||||
function getCourseExitNavigation(courseId, intl) {
|
||||
const exitMode = getCourseExitMode(courseId);
|
||||
const exitActive = exitMode !== COURSE_EXIT_MODES.disabled;
|
||||
|
||||
let exitText;
|
||||
switch (exitMode) {
|
||||
case COURSE_EXIT_MODES.celebration:
|
||||
return intl.formatMessage(messages.nextButtonComplete);
|
||||
exitText = intl.formatMessage(messages.nextButtonComplete);
|
||||
break;
|
||||
case COURSE_EXIT_MODES.nonPassing:
|
||||
return intl.formatMessage(messages.nextButtonEnd);
|
||||
exitText = intl.formatMessage(messages.nextButtonEnd);
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
exitText = null;
|
||||
}
|
||||
return { exitActive, exitText };
|
||||
}
|
||||
|
||||
// Meant to be used as part of a button's onClick handler.
|
||||
// For convenience, you can pass a falsy event and it will be ignored.
|
||||
const logClick = (org, courseId, administrator, event) => {
|
||||
const logClick = (org, courseId, administrator, event, extraProperties) => {
|
||||
if (!event) {
|
||||
return;
|
||||
}
|
||||
@@ -81,6 +97,7 @@ const logClick = (org, courseId, administrator, event) => {
|
||||
org_key: org,
|
||||
courserun_key: courseId,
|
||||
is_staff: administrator,
|
||||
...extraProperties,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -103,7 +120,7 @@ const logVisit = (org, courseId, administrator, variant) => {
|
||||
export {
|
||||
COURSE_EXIT_MODES,
|
||||
getCourseExitMode,
|
||||
getCourseExitText,
|
||||
getCourseExitNavigation,
|
||||
logClick,
|
||||
logVisit,
|
||||
};
|
||||
|
||||
@@ -11,6 +11,10 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { history } from '@edx/frontend-platform';
|
||||
|
||||
// These should be reverted after the REV1512 experiment
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faChevronLeft } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import PageLoading from '../../../generic/PageLoading';
|
||||
import { UserMessagesContext, ALERT_TYPES } from '../../../generic/user-messages';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
@@ -20,6 +24,163 @@ import messages from './messages';
|
||||
import { SequenceNavigation, UnitNavigation } from './sequence-navigation';
|
||||
import SequenceContent from './SequenceContent';
|
||||
|
||||
function REV1512Flyover({ toggleREV1512Flyover }) {
|
||||
// This component should be reverted after the REV1512 experiment
|
||||
return (
|
||||
<div
|
||||
className="rev-1512-box"
|
||||
style={{
|
||||
border: 'solid 1px #e1dddb',
|
||||
height: '393px',
|
||||
width: '330px',
|
||||
verticalAlign: 'top',
|
||||
marginLeft: '20px',
|
||||
padding: '0 20px 20px 20px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="rev-1512-notification-div"
|
||||
style={{
|
||||
margin: '0 -20px 15px',
|
||||
padding: '9px 20px 0',
|
||||
fontSize: '16px',
|
||||
}}
|
||||
>
|
||||
<span className="rev-1512-notification-span">Notifications</span>
|
||||
<span
|
||||
onClick={() => {
|
||||
toggleREV1512Flyover();
|
||||
}}
|
||||
className="hideFlyover"
|
||||
onKeyPress={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
toggleREV1512Flyover();
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={{
|
||||
float: 'right',
|
||||
marginTop: '5.5px',
|
||||
}}
|
||||
>
|
||||
<path d="M9.60625 7L13.5152 3.09102C13.9949 2.61133 13.9949 1.83359 13.5152 1.35352L12.6465 0.484766C12.1668 0.00507814 11.3891 0.00507814 10.909 0.484766L7 4.39375L3.09102 0.484766C2.61133 0.00507814 1.83359 0.00507814 1.35352 0.484766L0.484766 1.35352C0.00507814 1.8332 0.00507814 2.61094 0.484766 3.09102L4.39375 7L0.484766 10.909C0.00507814 11.3887 0.00507814 12.1664 0.484766 12.6465L1.35352 13.5152C1.8332 13.9949 2.61133 13.9949 3.09102 13.5152L7 9.60625L10.909 13.5152C11.3887 13.9949 12.1668 13.9949 12.6465 13.5152L13.5152 12.6465C13.9949 12.1668 13.9949 11.3891 13.5152 10.909L9.60625 7Z" fill="black" />
|
||||
</svg>
|
||||
</span>
|
||||
<div
|
||||
className="rev-1512-notification-block"
|
||||
style={{
|
||||
height: '9px',
|
||||
background: '#F9F9F9',
|
||||
margin: '7px -20px 0',
|
||||
borderTop: '1px solid rgb(225, 221, 219)',
|
||||
borderBottom: '1px solid rgb(225, 221, 219)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="rev-1512-optimizely-flyover-container" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
REV1512Flyover.propTypes = {
|
||||
toggleREV1512Flyover: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
function REV1512FlyoverMobile({ toggleREV1512Flyover }) {
|
||||
// This component should be reverted after the REV1512 experiment
|
||||
return (
|
||||
<div
|
||||
className="rev-1512-box"
|
||||
style={{
|
||||
verticalAlign: 'top',
|
||||
padding: '0 20px 20px 20px',
|
||||
position: 'fixed',
|
||||
backgroundColor: 'white',
|
||||
zIndex: 1,
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
top: 0,
|
||||
left: 0,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="rev-1512-mobile-return-div"
|
||||
style={{
|
||||
margin: '0 -20px',
|
||||
padding: '9px 20px 15px',
|
||||
fontSize: '16px',
|
||||
borderBottom: '1px solid rgb(225, 221, 219)',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="rev-1512-mobile-return-span"
|
||||
onClick={toggleREV1512Flyover}
|
||||
onKeyPress={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
toggleREV1512Flyover();
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
style={{
|
||||
color: '#00262B',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faChevronLeft}
|
||||
className="mr-2 fa-lg"
|
||||
style={{
|
||||
marginBottom: 2,
|
||||
}}
|
||||
/>
|
||||
<span className="rev-1512-mobile-return-text">
|
||||
Back to course
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="rev-1512-notification-div"
|
||||
style={{
|
||||
margin: '0 -20px 15px',
|
||||
padding: '9px 20px 0',
|
||||
fontSize: '16px',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="rev-1512-notification-span"
|
||||
style={{
|
||||
color: '#00262B',
|
||||
}}
|
||||
>
|
||||
Notifications
|
||||
</span>
|
||||
<div
|
||||
className="rev-1512-notification-block"
|
||||
style={{
|
||||
height: '9px',
|
||||
background: '#F9F9F9',
|
||||
margin: '7px -20px 0',
|
||||
borderTop: '1px solid rgb(225, 221, 219)',
|
||||
borderBottom: '1px solid rgb(225, 221, 219)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="rev-1512-optimizely-flyover-container" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
REV1512FlyoverMobile.propTypes = {
|
||||
toggleREV1512Flyover: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
function Sequence({
|
||||
unitId,
|
||||
sequenceId,
|
||||
@@ -28,8 +189,11 @@ function Sequence({
|
||||
nextSequenceHandler,
|
||||
previousSequenceHandler,
|
||||
intl,
|
||||
isREV1512FlyoverVisible, /* This line should be reverted after the REV1512 experiment */
|
||||
REV1512FlyoverEnabled, /* This line should be reverted after the REV1512 experiment */
|
||||
toggleREV1512Flyover, /* This line should be reverted after the REV1512 experiment */
|
||||
}) {
|
||||
const course = useModel('courses', courseId);
|
||||
const course = useModel('coursewareMeta', courseId);
|
||||
const sequence = useModel('sequences', sequenceId);
|
||||
const unit = useModel('units', unitId);
|
||||
const sequenceStatus = useSelector(state => state.courseware.sequenceStatus);
|
||||
@@ -43,6 +207,12 @@ function Sequence({
|
||||
}
|
||||
};
|
||||
|
||||
// These should be reverted after the REV1512 experiment:
|
||||
const userAgent = typeof window.navigator === 'undefined' ? '' : navigator.userAgent;
|
||||
const isMobile = Boolean(
|
||||
userAgent.match(/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i),
|
||||
);
|
||||
|
||||
const handlePrevious = () => {
|
||||
const previousIndex = sequence.unitIds.indexOf(unitId) - 1;
|
||||
if (previousIndex >= 0) {
|
||||
@@ -138,35 +308,39 @@ function Sequence({
|
||||
|
||||
if (sequenceStatus === 'loaded') {
|
||||
return (
|
||||
<div className="sequence-container">
|
||||
<div className="sequence">
|
||||
<SequenceNavigation
|
||||
sequenceId={sequenceId}
|
||||
unitId={unitId}
|
||||
className="mb-4"
|
||||
nextSequenceHandler={() => {
|
||||
logEvent('edx.ui.lms.sequence.next_selected', 'top');
|
||||
handleNext();
|
||||
}}
|
||||
onNavigate={(destinationUnitId) => {
|
||||
logEvent('edx.ui.lms.sequence.tab_selected', 'top', destinationUnitId);
|
||||
handleNavigate(destinationUnitId);
|
||||
}}
|
||||
previousSequenceHandler={() => {
|
||||
logEvent('edx.ui.lms.sequence.previous_selected', 'top');
|
||||
handlePrevious();
|
||||
}}
|
||||
goToCourseExitPage={() => goToCourseExitPage()}
|
||||
/>
|
||||
<div className="unit-container flex-grow-1">
|
||||
<SequenceContent
|
||||
courseId={courseId}
|
||||
gated={gated}
|
||||
<div>
|
||||
<div className="sequence-container" style={{ display: 'inline-flex', flexDirection: 'row' }}>
|
||||
<div className="sequence" style={{ width: '100%' }}>
|
||||
<SequenceNavigation
|
||||
sequenceId={sequenceId}
|
||||
unitId={unitId}
|
||||
unitLoadedHandler={handleUnitLoaded}
|
||||
className="mb-4"
|
||||
toggleREV1512Flyover={toggleREV1512Flyover} /* This line should be reverted after REV1512 experiment */
|
||||
REV1512FlyoverEnabled={REV1512FlyoverEnabled} /* This line should be reverted after REV1512 experiment */
|
||||
isREV1512FlyoverVisible={isREV1512FlyoverVisible} /* should be reverted after REV1512 experiment */
|
||||
nextSequenceHandler={() => {
|
||||
logEvent('edx.ui.lms.sequence.next_selected', 'top');
|
||||
handleNext();
|
||||
}}
|
||||
onNavigate={(destinationUnitId) => {
|
||||
logEvent('edx.ui.lms.sequence.tab_selected', 'top', destinationUnitId);
|
||||
handleNavigate(destinationUnitId);
|
||||
}}
|
||||
previousSequenceHandler={() => {
|
||||
logEvent('edx.ui.lms.sequence.previous_selected', 'top');
|
||||
handlePrevious();
|
||||
}}
|
||||
goToCourseExitPage={() => goToCourseExitPage()}
|
||||
/>
|
||||
{unitHasLoaded && (
|
||||
<div className="unit-container flex-grow-1">
|
||||
<SequenceContent
|
||||
courseId={courseId}
|
||||
gated={gated}
|
||||
sequenceId={sequenceId}
|
||||
unitId={unitId}
|
||||
unitLoadedHandler={handleUnitLoaded}
|
||||
/>
|
||||
{unitHasLoaded && (
|
||||
<UnitNavigation
|
||||
sequenceId={sequenceId}
|
||||
unitId={unitId}
|
||||
@@ -180,8 +354,15 @@ function Sequence({
|
||||
}}
|
||||
goToCourseExitPage={() => goToCourseExitPage()}
|
||||
/>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* This block of code should be reverted post REV1512 experiment */}
|
||||
{REV1512FlyoverEnabled && isREV1512FlyoverVisible() && (
|
||||
isMobile
|
||||
? <REV1512FlyoverMobile toggleREV1512Flyover={toggleREV1512Flyover} />
|
||||
: <REV1512Flyover toggleREV1512Flyover={toggleREV1512Flyover} />
|
||||
)}
|
||||
</div>
|
||||
<CourseLicense license={course.license || undefined} />
|
||||
</div>
|
||||
@@ -204,6 +385,9 @@ Sequence.propTypes = {
|
||||
nextSequenceHandler: PropTypes.func.isRequired,
|
||||
previousSequenceHandler: PropTypes.func.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
toggleREV1512Flyover: PropTypes.func.isRequired, /* This line should be reverted after the REV1512 experiment */
|
||||
isREV1512FlyoverVisible: PropTypes.func.isRequired, /* This line should be reverted after the REV1512 experiment */
|
||||
REV1512FlyoverEnabled: PropTypes.bool.isRequired, /* This line should be reverted after the REV1512 experiment */
|
||||
};
|
||||
|
||||
Sequence.defaultProps = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, {
|
||||
Suspense,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
@@ -9,6 +10,7 @@ import { useDispatch } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { Modal } from '@edx/paragon';
|
||||
import messages from './messages';
|
||||
import BookmarkButton from '../bookmark/BookmarkButton';
|
||||
@@ -18,6 +20,7 @@ import { processEvent } from '../../../course-home/data/thunks';
|
||||
import { fetchCourse } from '../../data/thunks';
|
||||
|
||||
const LockPaywall = React.lazy(() => import('./lock-paywall'));
|
||||
const LockPaywallValuePropExperiment = React.lazy(() => import('./lock-paywall-value-prop'));
|
||||
|
||||
/**
|
||||
* We discovered an error in Firefox where - upon iframe load - React would cease to call any
|
||||
@@ -58,7 +61,9 @@ function Unit({
|
||||
id,
|
||||
intl,
|
||||
}) {
|
||||
let iframeUrl = `${getConfig().LMS_BASE_URL}/xblock/${id}?show_title=0&show_bookmark_button=0`;
|
||||
const { authenticatedUser } = useContext(AppContext);
|
||||
const view = authenticatedUser ? 'student_view' : 'public_view';
|
||||
let iframeUrl = `${getConfig().LMS_BASE_URL}/xblock/${id}?show_title=0&show_bookmark_button=0&recheck_access=1&view=${view}`;
|
||||
if (format) {
|
||||
iframeUrl += `&format=${format}`;
|
||||
}
|
||||
@@ -66,9 +71,17 @@ function Unit({
|
||||
const [iframeHeight, setIframeHeight] = useState(0);
|
||||
const [hasLoaded, setHasLoaded] = useState(false);
|
||||
const [modalOptions, setModalOptions] = useState({ open: false });
|
||||
const [rev1512ValuePropExperimentLock, setRev1512ValuePropExperimentLock] = useState(
|
||||
window.rev1512ValuePropExperimentLock,
|
||||
);
|
||||
/* TODO: The code block below + code referencing it should be deleted after REV1512 value prop experiment */
|
||||
window.rev1512ToggleValuePropPaywallLock = () => {
|
||||
window.rev1512ValuePropExperimentLock = !rev1512ValuePropExperimentLock;
|
||||
setRev1512ValuePropExperimentLock(!rev1512ValuePropExperimentLock);
|
||||
};
|
||||
|
||||
const unit = useModel('units', id);
|
||||
const course = useModel('courses', courseId);
|
||||
const course = useModel('coursewareMeta', courseId);
|
||||
const {
|
||||
contentTypeGatingEnabled,
|
||||
} = course;
|
||||
@@ -117,7 +130,7 @@ function Unit({
|
||||
isBookmarked={unit.bookmarked}
|
||||
isProcessing={unit.bookmarkedUpdateState === 'loading'}
|
||||
/>
|
||||
{ contentTypeGatingEnabled && unit.graded && (
|
||||
{contentTypeGatingEnabled && unit.containsContentTypeGatedContent && (
|
||||
<Suspense
|
||||
fallback={(
|
||||
<PageLoading
|
||||
@@ -125,9 +138,9 @@ function Unit({
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<LockPaywall
|
||||
courseId={courseId}
|
||||
/>
|
||||
{(rev1512ValuePropExperimentLock)
|
||||
? <LockPaywallValuePropExperiment courseId={courseId} />
|
||||
: <LockPaywall courseId={courseId} />}
|
||||
</Suspense>
|
||||
)}
|
||||
{!hasLoaded && (
|
||||
|
||||
@@ -17,10 +17,10 @@ describe('Unit', () => {
|
||||
{ courseId: courseMetadata.id },
|
||||
), Factory.build(
|
||||
'block',
|
||||
{ type: 'vertical', graded: true, bookmarked: true },
|
||||
{ type: 'vertical', contains_content_type_gated_content: true, bookmarked: true },
|
||||
{ courseId: courseMetadata.id },
|
||||
)];
|
||||
const [unit, gradedUnit] = unitBlocks;
|
||||
const [unit, unitThatContainsGatedContent] = unitBlocks;
|
||||
|
||||
beforeAll(async () => {
|
||||
await initializeTestStore({ courseMetadata, unitBlocks });
|
||||
@@ -38,12 +38,12 @@ describe('Unit', () => {
|
||||
const renderedUnit = screen.getByTitle(unit.display_name);
|
||||
expect(renderedUnit).toHaveAttribute('height', String(0));
|
||||
expect(renderedUnit).toHaveAttribute(
|
||||
'src', `http://localhost:18000/xblock/${mockData.id}?show_title=0&show_bookmark_button=0&format=${mockData.format}`,
|
||||
'src', `http://localhost:18000/xblock/${mockData.id}?show_title=0&show_bookmark_button=0&recheck_access=1&view=student_view&format=${mockData.format}`,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders proper message for gated content', () => {
|
||||
render(<Unit {...mockData} id={gradedUnit.id} />);
|
||||
render(<Unit {...mockData} id={unitThatContainsGatedContent.id} />);
|
||||
|
||||
expect(screen.getByText('Loading learning sequence...')).toBeInTheDocument();
|
||||
expect(screen.getByText('Loading locked content messaging...')).toBeInTheDocument();
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
/* TODO: This file should be deleted after REV1512 value prop experiment */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faLock } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faCheckCircle } from '@fortawesome/free-regular-svg-icons';
|
||||
import {
|
||||
injectIntl, getLocale,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@edx/paragon';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import VerifiedCert from '../../../../generic/assets/edX_certificate.png';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
import './LockPaywall.scss';
|
||||
|
||||
function LockPaywall({
|
||||
courseId,
|
||||
}) {
|
||||
const course = useModel('coursewareMeta', courseId);
|
||||
const {
|
||||
verifiedMode,
|
||||
} = course;
|
||||
if (!verifiedMode) {
|
||||
return null;
|
||||
}
|
||||
const {
|
||||
currencySymbol,
|
||||
price,
|
||||
upgradeUrl,
|
||||
} = verifiedMode;
|
||||
|
||||
const isSpanish = getLocale() === 'es-419';
|
||||
|
||||
let upgradeButtonText;
|
||||
|
||||
if (document.querySelector('.price.discount') !== null) {
|
||||
let discountPrice = document.querySelector('.price.discount').textContent;
|
||||
if (discountPrice !== null) {
|
||||
discountPrice = discountPrice.replace(/[^0-9.]/g, '');
|
||||
}
|
||||
|
||||
if (isSpanish) {
|
||||
upgradeButtonText = (
|
||||
<>
|
||||
<span className="font-weight-bold" style={{ paddingRight: '5px' }}>
|
||||
Cómpralo por {currencySymbol}{discountPrice}
|
||||
</span>
|
||||
<span style={{ textDecoration: 'line-through' }}>
|
||||
({currencySymbol}{price})
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
upgradeButtonText = (
|
||||
<>
|
||||
<span className="font-weight-bold" style={{ paddingRight: '5px' }}>
|
||||
Upgrade for {currencySymbol}{discountPrice}
|
||||
</span>
|
||||
<span style={{ textDecoration: 'line-through' }}>
|
||||
({currencySymbol}{price})
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
} else if (isSpanish) {
|
||||
upgradeButtonText = (
|
||||
<>
|
||||
<span className="font-weight-bold" style={{ paddingRight: '5px' }}>
|
||||
Cómpralo por {currencySymbol}{price}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
upgradeButtonText = (
|
||||
<>
|
||||
<span className="font-weight-bold" style={{ paddingRight: '5px' }}>
|
||||
Upgrade for {currencySymbol}{price}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const circleCheckIcon = (
|
||||
<FontAwesomeIcon
|
||||
icon={faCheckCircle}
|
||||
className="float-left mt-1"
|
||||
fixedWidth
|
||||
aria-hidden="true"
|
||||
title="icon"
|
||||
style={{ marginRight: '15px' }}
|
||||
/>
|
||||
);
|
||||
const verifiedCertificateLink = (
|
||||
<b>
|
||||
<a
|
||||
className="value-prop-verified-certificate-link"
|
||||
style={{ textDecoration: 'underline', color: '#00688D' }}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
href="https://www.edx.org/verified-certificate"
|
||||
>
|
||||
{ (isSpanish) ? 'certificado verificado' : 'verified certificate' }
|
||||
</a>
|
||||
</b>
|
||||
);
|
||||
|
||||
const userAgent = typeof window.navigator === 'undefined' ? '' : navigator.userAgent;
|
||||
const isMobile = Boolean(
|
||||
userAgent.match(/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="border border-gray d-flex justify-content-between mt-2 p-3 value-prop-lock-paywall-banner">
|
||||
<div className={classNames({ 'is-mobile': isMobile })}>
|
||||
<div className="font-weight-bold top-banner-text-header">
|
||||
<FontAwesomeIcon icon={faLock} className="text-black mr-2 ml-1 lock-icon" style={{ fontSize: '1rem' }} />
|
||||
<span className="top-banner-title">
|
||||
{
|
||||
isSpanish
|
||||
? 'Las tareas calificadas están bloqueadas'
|
||||
: 'Graded assignments are locked'
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div className="font-weight top-banner-text">
|
||||
{
|
||||
isSpanish
|
||||
? 'Cámbiate a la opción verificada para obtener acceso a funciones bloqueadas como esta y aprovechar al máximo tu curso.'
|
||||
: 'Upgrade to gain access to locked features like this one and get the most out of your course.'
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className={classNames('mb-0', 'cert-list-wrapper', { 'd-flex': !isMobile })}>
|
||||
|
||||
<div className="certificate-image-banner-container">
|
||||
<img
|
||||
alt="Example Certificate"
|
||||
src={VerifiedCert}
|
||||
className="border-0 certificate-image-banner"
|
||||
/>
|
||||
</div>
|
||||
<div className="list-container" style={{ float: 'left', paddingLeft: '18px', paddingBottom: '24px' }}>
|
||||
<div style={{ paddingBottom: '5px' }}>
|
||||
{
|
||||
isSpanish
|
||||
? 'Cuando te cambias a la opción verificada, tú:'
|
||||
: 'When you upgrade, you:'
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className="list-item-row">
|
||||
<div className="check-circle-icon-wrapper">{circleCheckIcon}</div>
|
||||
<div className="list-item-wrapper">
|
||||
<span>
|
||||
{
|
||||
isSpanish
|
||||
? <>Obtén un {verifiedCertificateLink} de finalización para compartirlo en tu currículum</>
|
||||
: <>Earn a {verifiedCertificateLink} of completion to showcase on your resume</>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="list-item-row">
|
||||
<div className="check-circle-icon-wrapper">{circleCheckIcon}</div>
|
||||
<div className="list-item-wrapper">
|
||||
{
|
||||
isSpanish
|
||||
? 'Desbloquea el acceso a todas las actividades del curso, incluidas las '
|
||||
: 'Unlock access to all course activities, including '
|
||||
}
|
||||
<span className="font-weight-bold">
|
||||
{
|
||||
isSpanish
|
||||
? 'tareas calificadas'
|
||||
: 'graded assignments'
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="list-item-row">
|
||||
<div className="check-circle-icon-wrapper">{circleCheckIcon}</div>
|
||||
<div className="list-item-wrapper">
|
||||
<span className="font-weight-bold">
|
||||
{
|
||||
isSpanish
|
||||
? 'Acceso completo'
|
||||
: 'Gain full access'
|
||||
}
|
||||
</span>
|
||||
{
|
||||
isSpanish
|
||||
? ' al contenido y los materiales del curso, incluso después de que finalice el curso'
|
||||
: ' to course content and materials, even after the course ends'
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="list-item-row">
|
||||
<div className="check-circle-icon-wrapper">{circleCheckIcon}</div>
|
||||
<div className="list-item-wrapper">
|
||||
{
|
||||
isSpanish
|
||||
? 'Apoya nuestra '
|
||||
: 'Support our '
|
||||
}
|
||||
<span className="font-weight-bold">
|
||||
{
|
||||
isSpanish
|
||||
? 'misión sin fines de lucro'
|
||||
: 'non-profit mission'
|
||||
}
|
||||
</span>
|
||||
{
|
||||
isSpanish
|
||||
? ' en edX'
|
||||
: ' at edX'
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="value-prop-upgrade-button-container">
|
||||
<Button variant="primary" href={upgradeUrl} className="value-prop-lock-paywall-upgrade-link">
|
||||
{upgradeButtonText}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
LockPaywall.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
export default injectIntl(LockPaywall);
|
||||
@@ -0,0 +1,93 @@
|
||||
/* TODO: This file should be deleted after REV1512 value prop experiment */
|
||||
.value-prop-lock-paywall-banner {
|
||||
background-color: #f8f9fa;
|
||||
|
||||
.list-item-row {
|
||||
display: flex;
|
||||
margin-left: -7px;
|
||||
line-height: 1.5rem;
|
||||
padding-left: 5px;
|
||||
}
|
||||
.certificate-image-banner {
|
||||
width: 172px;
|
||||
height: 123px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.certificate-image-banner-container {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
padding-left: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.top-banner-text {
|
||||
margin-bottom: 16px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
.top-banner-text-header {
|
||||
padding-left: 20px;
|
||||
}
|
||||
.value-prop-upgrade-button-container {
|
||||
padding-left: 20px;
|
||||
}
|
||||
.is-mobile {
|
||||
padding-top: 10px;
|
||||
|
||||
.certificate-image-banner-container {
|
||||
text-align: center;
|
||||
display: block;
|
||||
float: none;
|
||||
padding-left: 0px;
|
||||
}
|
||||
.certificate-image-banner {
|
||||
width: 180px;
|
||||
height: 128px;
|
||||
margin-top: 3px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.top-banner-text-header {
|
||||
margin-left:36px;
|
||||
position: relative;
|
||||
padding-left: 10px;
|
||||
}
|
||||
.top-banner-text {
|
||||
margin-left:36px;
|
||||
margin-bottom: 4px;
|
||||
position: relative;
|
||||
padding-left: 10px;
|
||||
}
|
||||
.lock-icon {
|
||||
position: absolute;
|
||||
left: -28px;
|
||||
top: 4px;
|
||||
}
|
||||
.value-prop-upgrade-button-container {
|
||||
text-align: center;
|
||||
padding-bottom: 20px;
|
||||
padding-top: 20px;
|
||||
padding-left: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 576px) {
|
||||
.top-banner-text-header {
|
||||
padding-top: 10px;
|
||||
}
|
||||
.top-banner-text {
|
||||
padding-left: 47px !important;
|
||||
}
|
||||
.cert-list-wrapper {
|
||||
display: block !important;
|
||||
}
|
||||
.certificate-image-banner-container {
|
||||
width: 100%;
|
||||
padding-bottom: 10px;
|
||||
padding-left: 0px !important;
|
||||
text-align: center;
|
||||
}
|
||||
.value-prop-upgrade-button-container {
|
||||
padding-bottom: 20px;
|
||||
text-align: center;
|
||||
padding-left: 0px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/* TODO: This file should be deleted after REV1512 value prop experiment */
|
||||
import React from 'react';
|
||||
import { Factory } from 'rosie';
|
||||
import { initializeTestStore, render, screen } from '../../../../setupTest';
|
||||
import LockPaywall from './LockPaywall';
|
||||
|
||||
describe('Lock Paywall', () => {
|
||||
let store;
|
||||
const mockData = {};
|
||||
|
||||
beforeAll(async () => {
|
||||
store = await initializeTestStore();
|
||||
const { courseware } = store.getState();
|
||||
mockData.courseId = courseware.courseId;
|
||||
});
|
||||
|
||||
it('displays message along with lock icon', () => {
|
||||
const { container } = render(<LockPaywall {...mockData} />);
|
||||
|
||||
const lockIcon = container.querySelector('svg');
|
||||
expect(lockIcon).toHaveClass('fa-lock');
|
||||
expect(lockIcon.parentElement).toHaveTextContent('Graded assignments are locked');
|
||||
});
|
||||
|
||||
it('displays unlock link with price', () => {
|
||||
const {
|
||||
currencySymbol,
|
||||
price,
|
||||
upgradeUrl,
|
||||
} = store.getState().models.coursewareMeta[mockData.courseId].verifiedMode;
|
||||
render(<LockPaywall {...mockData} />);
|
||||
|
||||
const upgradeLink = screen.getByRole('link', { name: `Upgrade for ${currencySymbol}${price}` });
|
||||
expect(upgradeLink).toHaveAttribute('href', `${upgradeUrl}`);
|
||||
});
|
||||
|
||||
it('does not display anything if course does not have verified mode', async () => {
|
||||
const courseMetadata = Factory.build('courseMetadata', { verified_mode: null });
|
||||
const testStore = await initializeTestStore({ courseMetadata, excludeFetchSequence: true }, false);
|
||||
const { container } = render(<LockPaywall {...mockData} courseId={courseMetadata.id} />, { store: testStore });
|
||||
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
/* TODO: This file + folder should be deleted after REV1512 value prop experiment */
|
||||
export { default } from './LockPaywall';
|
||||
@@ -5,14 +5,14 @@ import { faLock } from '@fortawesome/free-solid-svg-icons';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './messages';
|
||||
import VerifiedCert from '../../../../generic/assets/edX_verified_certificate.png';
|
||||
import VerifiedCert from '../../../../generic/assets/edX_certificate.png';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
|
||||
function LockPaywall({
|
||||
intl,
|
||||
courseId,
|
||||
}) {
|
||||
const course = useModel('courses', courseId);
|
||||
const course = useModel('coursewareMeta', courseId);
|
||||
const {
|
||||
verifiedMode,
|
||||
} = course;
|
||||
@@ -35,7 +35,7 @@ function LockPaywall({
|
||||
<p className="mb-0">
|
||||
<span>{intl.formatMessage(messages['learn.lockPaywall.content'])}</span>
|
||||
|
||||
<a href={upgradeUrl}>
|
||||
<a className="lock_paywall_upgrade_link" href={upgradeUrl}>
|
||||
{intl.formatMessage(messages['learn.lockPaywall.upgrade.link'], {
|
||||
currencySymbol,
|
||||
price,
|
||||
|
||||
@@ -22,7 +22,11 @@ describe('Lock Paywall', () => {
|
||||
});
|
||||
|
||||
it('displays unlock link with price', () => {
|
||||
const { currencySymbol, price, upgradeUrl } = store.getState().models.courses[mockData.courseId].verifiedMode;
|
||||
const {
|
||||
currencySymbol,
|
||||
price,
|
||||
upgradeUrl,
|
||||
} = store.getState().models.coursewareMeta[mockData.courseId].verifiedMode;
|
||||
render(<LockPaywall {...mockData} />);
|
||||
|
||||
const upgradeLink = screen.getByRole('link', { name: `Upgrade to unlock (${currencySymbol}${price})` });
|
||||
|
||||
@@ -7,7 +7,7 @@ import { faChevronLeft, faChevronRight } from '@fortawesome/free-solid-svg-icons
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
import { getCourseExitText } from '../../course-exit';
|
||||
import { getCourseExitNavigation } from '../../course-exit';
|
||||
import UnitButton from './UnitButton';
|
||||
import SequenceNavigationTabs from './SequenceNavigationTabs';
|
||||
import { useSequenceNavigationMetadata } from './hooks';
|
||||
@@ -25,6 +25,9 @@ function SequenceNavigation({
|
||||
nextSequenceHandler,
|
||||
previousSequenceHandler,
|
||||
goToCourseExitPage,
|
||||
toggleREV1512Flyover, /* This line should be reverted after the REV1512 experiment */
|
||||
REV1512FlyoverEnabled, /* This line should be reverted after the REV1512 experiment */
|
||||
isREV1512FlyoverVisible, /* This line should be reverted after the REV1512 experiment */
|
||||
}) {
|
||||
const sequence = useModel('sequences', sequenceId);
|
||||
const { isFirstUnit, isLastUnit } = useSequenceNavigationMetadata(sequenceId, unitId);
|
||||
@@ -58,10 +61,10 @@ function SequenceNavigation({
|
||||
};
|
||||
|
||||
const renderNextButton = () => {
|
||||
const exitText = getCourseExitText(courseId, intl);
|
||||
const { exitActive, exitText } = getCourseExitNavigation(courseId, intl);
|
||||
const buttonOnClick = isLastUnit ? goToCourseExitPage : nextSequenceHandler;
|
||||
const buttonText = (isLastUnit && exitText) ? exitText : intl.formatMessage(messages.nextButton);
|
||||
const disabled = isLastUnit && !exitText;
|
||||
const disabled = isLastUnit && !exitActive;
|
||||
return (
|
||||
<Button variant="link" className="next-btn" onClick={buttonOnClick} disabled={disabled}>
|
||||
{buttonText}
|
||||
@@ -70,6 +73,12 @@ function SequenceNavigation({
|
||||
);
|
||||
};
|
||||
|
||||
// These should be reverted after the REV1512 experiment
|
||||
const userAgent = typeof window.navigator === 'undefined' ? '' : navigator.userAgent;
|
||||
const isMobile = Boolean(
|
||||
userAgent.match(/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i),
|
||||
);
|
||||
|
||||
return sequenceStatus === LOADED && (
|
||||
<nav className={classNames('sequence-navigation', className)}>
|
||||
<Button variant="link" className="previous-btn" onClick={previousSequenceHandler} disabled={isFirstUnit}>
|
||||
@@ -78,6 +87,22 @@ function SequenceNavigation({
|
||||
</Button>
|
||||
{renderUnitButtons()}
|
||||
{renderNextButton()}
|
||||
{REV1512FlyoverEnabled
|
||||
&& isMobile && (
|
||||
<div
|
||||
className="toggleFlyoverButton toggleFlyoverButtonMobile"
|
||||
aria-hidden="true"
|
||||
style={{ borderBottom: isREV1512FlyoverVisible() ? '2px solid #00262b' : 'none' }}
|
||||
onClick={() => {
|
||||
toggleREV1512Flyover();
|
||||
}}
|
||||
>
|
||||
<svg width="54" height="40" viewBox="0 0 54 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0.5" y="0.5" width="53" height="39" rx="1.5" fill="white" stroke="#E7E8E9" /><path d="M36 20C36 15.6 32.4 12 28 12C27.7 12 27.5 12.2 27.5 12.5C27.5 12.8 27.7 13 28 13C31.85 13 35 16.15 35 20C35 23.85 31.85 27 28 27C24.15 27 21 23.85 21 20C21 19.7 20.8 19.5 20.5 19.5C20.3 19.5 20.1 19.65 20.05 19.8C20 19.85 20 19.95 20 20C20 24.4 23.6 28 28 28C32.4 28 36 24.4 36 20Z" fill="black" stroke="black" strokeWidth="0.6" /><path d="M23.1065 14.52C22.9403 14.36 22.691 14.36 22.5247 14.52C22.3585 14.68 22.3585 14.92 22.5247 15.08C22.6078 15.16 22.7325 15.2 22.8156 15.2C22.9403 15.2 23.0234 15.16 23.1065 15.08C23.2312 14.96 23.2312 14.68 23.1065 14.52Z" fill="black" stroke="black" strokeWidth="0.6" /><path d="M27.6848 15.2C27.3939 15.2 27.2 15.3973 27.2 15.6932V19.6384C27.2 19.6877 27.2 19.7863 27.2484 19.8356C27.2969 19.8849 27.2969 19.9343 27.3454 19.9836L29.5757 22.2521C29.6727 22.3507 29.8181 22.4 29.9151 22.4C30.0121 22.4 30.1575 22.3507 30.2545 22.2521C30.4484 22.0548 30.4484 21.7589 30.2545 21.5617L28.1696 19.4411V15.6932C28.1696 15.3973 27.9757 15.2 27.6848 15.2Z" fill="black" stroke="black" strokeWidth="0.6" /><circle cx="35.5" cy="14.5" r="4.5" fill="#C32D3A" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
<div className="rev1512ToggleFlyoverSequenceLocation" />
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -91,6 +116,9 @@ SequenceNavigation.propTypes = {
|
||||
nextSequenceHandler: PropTypes.func.isRequired,
|
||||
previousSequenceHandler: PropTypes.func.isRequired,
|
||||
goToCourseExitPage: PropTypes.func.isRequired,
|
||||
toggleREV1512Flyover: PropTypes.func.isRequired, /* This line should be reverted after the REV1512 experiment */
|
||||
REV1512FlyoverEnabled: PropTypes.bool.isRequired, /* This line should be reverted after the REV1512 experiment */
|
||||
isREV1512FlyoverVisible: PropTypes.func.isRequired, /* This line should be reverted after the REV1512 experiment */
|
||||
};
|
||||
|
||||
SequenceNavigation.defaultProps = {
|
||||
|
||||
@@ -115,7 +115,7 @@ describe('Sequence Navigation', () => {
|
||||
});
|
||||
|
||||
it('displays end of course message instead of the "Next" button as needed', async () => {
|
||||
const testMetadata = { ...courseMetadata, certificate_data: { cert_status: 'notpassing' } };
|
||||
const testMetadata = { ...courseMetadata, certificate_data: { cert_status: 'notpassing' }, enrollment: { is_active: true } };
|
||||
const testStore = await initializeTestStore({ courseMetadata: testMetadata, unitBlocks }, false);
|
||||
// Have to refetch the sequenceId since the new store generates new sequences
|
||||
const { courseware } = testStore.getState();
|
||||
@@ -134,6 +134,7 @@ describe('Sequence Navigation', () => {
|
||||
const testMetadata = {
|
||||
...courseMetadata,
|
||||
certificate_data: { cert_status: 'downloadable' },
|
||||
enrollment: { is_active: true },
|
||||
user_has_passing_grade: true,
|
||||
};
|
||||
const testStore = await initializeTestStore({ courseMetadata: testMetadata, unitBlocks }, false);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { faChevronLeft, faChevronRight } from '@fortawesome/free-solid-svg-icons
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { getCourseExitText } from '../../course-exit';
|
||||
import { getCourseExitNavigation } from '../../course-exit';
|
||||
|
||||
import { useSequenceNavigationMetadata } from './hooks';
|
||||
import messages from './messages';
|
||||
@@ -23,10 +23,10 @@ function UnitNavigation({
|
||||
const { courseId } = useSelector(state => state.courseware);
|
||||
|
||||
const renderNextButton = () => {
|
||||
const exitText = getCourseExitText(courseId, intl);
|
||||
const { exitActive, exitText } = getCourseExitNavigation(courseId, intl);
|
||||
const buttonOnClick = isLastUnit ? goToCourseExitPage : onClickNext;
|
||||
const buttonText = (isLastUnit && exitText) ? exitText : intl.formatMessage(messages.nextButton);
|
||||
const disabled = isLastUnit && !exitText;
|
||||
const disabled = isLastUnit && !exitActive;
|
||||
return (
|
||||
<Button variant="outline-primary" className="next-button" onClick={buttonOnClick} disabled={disabled}>
|
||||
{buttonText}
|
||||
|
||||
@@ -90,7 +90,7 @@ describe('Unit Navigation', () => {
|
||||
});
|
||||
|
||||
it('displays end of course message instead of the "Next" button as needed', async () => {
|
||||
const testCourseMetadata = { ...courseMetadata, certificate_data: { cert_status: 'notpassing' } };
|
||||
const testCourseMetadata = { ...courseMetadata, certificate_data: { cert_status: 'notpassing' }, enrollment: { is_active: true } };
|
||||
const testStore = await initializeTestStore({ courseMetadata: testCourseMetadata, unitBlocks }, false);
|
||||
// Have to refetch the sequenceId since the new store generates new sequences
|
||||
const { courseware } = testStore.getState();
|
||||
@@ -109,6 +109,7 @@ describe('Unit Navigation', () => {
|
||||
const testCourseMetadata = {
|
||||
...courseMetadata,
|
||||
certificate_data: { cert_status: 'downloadable' },
|
||||
enrollment: { is_active: true },
|
||||
user_has_passing_grade: true,
|
||||
};
|
||||
const testStore = await initializeTestStore({ courseMetadata: testCourseMetadata, unitBlocks }, false);
|
||||
|
||||
@@ -77,7 +77,11 @@ export default function buildSimpleCourseBlocks(courseId, title, options = {}) {
|
||||
return {
|
||||
courseBlocks: options.courseBlocks || Factory.build(
|
||||
'courseBlocks',
|
||||
{ courseId },
|
||||
{
|
||||
courseId,
|
||||
hasScheduledContent: options.hasScheduledContent || false,
|
||||
title: 'Demo Course',
|
||||
},
|
||||
{
|
||||
units: unitBlocks,
|
||||
sequence: sequenceBlock,
|
||||
|
||||
@@ -116,6 +116,16 @@ Factory.define('courseMetadata')
|
||||
},
|
||||
{ courseId: id, path: 'instructor' },
|
||||
),
|
||||
Factory.build(
|
||||
'tab',
|
||||
{
|
||||
title: 'Dates',
|
||||
priority: 5,
|
||||
slug: 'dates',
|
||||
type: 'dates',
|
||||
},
|
||||
{ courseId: id, path: 'dates' },
|
||||
),
|
||||
];
|
||||
|
||||
return tabs;
|
||||
|
||||
@@ -48,6 +48,7 @@ Factory.define('sequenceMetadata')
|
||||
complete: unitBlock.complete || null,
|
||||
content: '',
|
||||
page_title: unitBlock.display_name,
|
||||
contains_content_type_gated_content: unitBlock.contains_content_type_gated_content,
|
||||
}),
|
||||
))
|
||||
.attrs({
|
||||
|
||||
@@ -16,6 +16,7 @@ export function normalizeBlocks(courseId, blocks) {
|
||||
id: courseId,
|
||||
title: block.display_name,
|
||||
sectionIds: block.children || [],
|
||||
hasScheduledContent: block.has_scheduled_content || false,
|
||||
};
|
||||
break;
|
||||
case 'chapter':
|
||||
@@ -86,12 +87,12 @@ export function normalizeBlocks(courseId, blocks) {
|
||||
}
|
||||
|
||||
export async function getCourseBlocks(courseId) {
|
||||
const { username } = getAuthenticatedUser();
|
||||
const authenticatedUser = getAuthenticatedUser();
|
||||
const url = new URL(`${getConfig().LMS_BASE_URL}/api/courses/v2/blocks/`);
|
||||
url.searchParams.append('course_id', courseId);
|
||||
url.searchParams.append('username', username);
|
||||
url.searchParams.append('username', authenticatedUser ? authenticatedUser.username : '');
|
||||
url.searchParams.append('depth', 3);
|
||||
url.searchParams.append('requested_fields', 'children,show_gated_sections,graded,special_exam_info');
|
||||
url.searchParams.append('requested_fields', 'children,show_gated_sections,graded,special_exam_info,has_scheduled_content');
|
||||
|
||||
const { data } = await getAuthenticatedHttpClient().get(url.href, {});
|
||||
return normalizeBlocks(courseId, data.blocks);
|
||||
@@ -110,14 +111,13 @@ function normalizeTabUrls(id, tabs) {
|
||||
|
||||
function normalizeMetadata(metadata) {
|
||||
return {
|
||||
accessExpiration: camelCaseObject(metadata.access_expiration),
|
||||
canShowUpgradeSock: metadata.can_show_upgrade_sock,
|
||||
contentTypeGatingEnabled: metadata.content_type_gating_enabled,
|
||||
// TODO: TNL-7185: return course expired _date_, instead of _message_
|
||||
courseExpiredMessage: metadata.course_expired_message,
|
||||
id: metadata.id,
|
||||
title: metadata.name,
|
||||
number: metadata.number,
|
||||
offerHtml: metadata.offer_html,
|
||||
offer: camelCaseObject(metadata.offer),
|
||||
org: metadata.org,
|
||||
enrollmentStart: metadata.enrollment_start,
|
||||
enrollmentEnd: metadata.enrollment_end,
|
||||
@@ -131,6 +131,7 @@ function normalizeMetadata(metadata) {
|
||||
license: metadata.license,
|
||||
verifiedMode: camelCaseObject(metadata.verified_mode),
|
||||
tabs: normalizeTabUrls(metadata.id, camelCaseObject(metadata.tabs)),
|
||||
userTimezone: metadata.user_timezone,
|
||||
showCalculator: metadata.show_calculator,
|
||||
notes: camelCaseObject(metadata.notes),
|
||||
marketingUrl: metadata.marketing_url,
|
||||
@@ -182,6 +183,7 @@ function normalizeSequenceMetadata(sequence) {
|
||||
complete: unit.complete,
|
||||
title: unit.page_title,
|
||||
contentType: unit.type,
|
||||
containsContentTypeGatedContent: unit.contains_content_type_gated_content,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ describe('Data layer integration tests', () => {
|
||||
expect(state.courseware.courseStatus).toEqual('denied');
|
||||
|
||||
// check that at least one key camel cased, thus course data normalized
|
||||
expect(state.models.courses[forbiddenCourseMetadata.id].canLoadCourseware).not.toBeUndefined();
|
||||
expect(state.models.coursewareMeta[forbiddenCourseMetadata.id].canLoadCourseware).not.toBeUndefined();
|
||||
});
|
||||
|
||||
it('Should fetch, normalize, and save metadata', async () => {
|
||||
@@ -98,7 +98,7 @@ describe('Data layer integration tests', () => {
|
||||
expect(state.courseware.sequenceId).toEqual(null);
|
||||
|
||||
// check that at least one key camel cased, thus course data normalized
|
||||
expect(state.models.courses[courseId].canLoadCourseware).not.toBeUndefined();
|
||||
expect(state.models.coursewareMeta[courseId].canLoadCourseware).not.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user