Compare commits

...

65 Commits

Author SHA1 Message Date
julianajlk
05e299d976 Deactivate notification red dot 2021-06-29 14:35:49 -04:00
julianajlk
ae842b8c24 Rename UpgradeCard to UpgradeNotification 2021-06-28 18:22:03 -04:00
julianajlk
1a5c6a8319 Rename Sidebar to NotificationTray 2021-06-28 18:16:36 -04:00
julianajlk
2f87b762f2 Rename SidebarNotificationButton to NotificationTrigger 2021-06-28 18:16:35 -04:00
julianajlk
01f5c3c95c Update tests 2021-06-28 18:16:34 -04:00
julianajlk
dade755780 Updates after UX review 2021-06-28 18:16:34 -04:00
julianajlk
b618cccb02 Add UpgradeCard to courseware and fix CSS to be usable for course-home and courseware 2021-06-28 18:16:33 -04:00
julianajlk
81f8ff5a13 Add timeOffsetMillis to courseware API 2021-06-28 18:16:33 -04:00
julianajlk
556da6a22f Clean up UpgradeCard SCSS 2021-06-28 18:16:32 -04:00
julianajlk
8291ea5929 Move UpgradeCard to generic directory to be accessible in courseware 2021-06-28 18:16:32 -04:00
Thomas Tracy
37ce01c00a Ttracy/microba 1192 early with info display behavior (#497)
* [feat] Add ID verification Alert to course home

if a user has a verified seat, but is in the unverified certificate
status state, the certificateStatusAlert will now show a message letting
the learner know they need to verify in order to earn a certificate.

This does not remove the message about the verification deadline in the
right sidebar of the course home.
2021-06-28 12:20:06 -04:00
David Ormsbee
9edac2519a fix: remove sequences we shouldn't see by using learning_sequences
Removes sequences we shouldn't see by using the Learning Sequences API
(TNL-8377). Depends on https://github.com/edx/edx-platform/pull/27955

It works by adding a call to the Learning Sequences API and (if that
endpoint is enabled, i.e. returns 200 for this user+course), uses the
results of that endpoint to remove sequences from the Course Blocks API
call. Learning Sequences knows how to do things like bubble up the
content group settings of units to sequences for the case where all
units have the same restrictions and the user would see an empty
sequence.
2021-06-28 11:41:56 -04:00
Bianca Severino
f9fbc1eb49 fix: add missing check for graded units (#508)
This fixes a bug where if the learner needs an integrity signature, but
the unit is not graded, neither the honor code panel nor the unit
content would display.
2021-06-28 10:12:58 -04:00
edX Transifex Bot
5c68c1d554 fix(i18n): update translations 2021-06-28 02:09:03 +05:00
Zachary Hancock
4180a2e7a0 chore: update special exams library (#503) 2021-06-24 13:10:51 -04:00
Brian Mesick
554e63d653 feat: Remove upsell banner on course home (#499)
REV-2233: The upsell banner now duplicates the sidebar banner. Removing it in favor of the new implementation on course home, but keeping the masquerade message that shows instructors when a learner lost access to the course.
2021-06-24 12:46:57 -04:00
Kristin Aoki
c9f299eada fix: Autoscroll on page when using jump_to_id
This PR adds a URL hash check to useEffect. Previously the anchor tags
that use jump_to_id would remain at the top of the page. As a result,
users would have to manually scroll to the target location or just read
the full page. Now when the page has a URL hash, it will send the hash
to the listener in the iframe. Using the message listener, it receives
an object with offset in the event.data and the page will scroll to the
location provided by offset. This change will impact the Learner in the
New Experience view.
2021-06-23 12:57:47 -04:00
Carla Duarte
d1bb46eef3 AA-815: ui and a11y progress tab fixes (#494) 2021-06-23 09:42:12 -04:00
Renovate Bot
492c573f56 fix(deps): update dependency @edx/frontend-component-footer to v10.1.5 2021-06-23 08:03:06 +00:00
Brian Mesick
0c55863a3d Revert "feat: Remove upsell banner on course home (#493)" (#498)
This reverts commit fbd9d858e4.
2021-06-22 10:37:11 -04:00
Brian Mesick
fbd9d858e4 feat: Remove upsell banner on course home (#493)
REV-2233: The upsell banner now duplicates the sidebar banner. Removing it in favor of the new implementation on course home, but keeping the masquerade message that shows instructors when a learner lost access to the course.
2021-06-22 10:01:57 -04:00
Rebecca Graber
7b0429f472 feat: make course recommendations a part of the course celebration (#486) 2021-06-21 13:18:35 -04:00
Brian Mesick
56decd8ed0 feat: Remove course sock from OutlineTab. (#495)
REV-2122: As part of the Value Prop implementation, we are removing the course sock from Course Home
2021-06-21 12:55:11 -04:00
Matthew Piatetsky
3155055276 feat: change color of start/resume course button (#496) 2021-06-21 10:58:03 -04:00
Albert (AJ) St. Aubin
1b84930a84 feature: Added notification and link to get to a users PDF cert.
This feature will allow users with downloadable PDF certificates to see
the certificate status alert and then access their certificate on the
Course Outline page. This should only show once a learner has earned a
certificate and that certificate is available.
2021-06-17 07:33:33 -04:00
Michael Terry
99185a9b8a fix: guard against courseGoals being undefined (#489)
No idea why this would happen honestly - it looks always defined
from the backend, and api.js doesn't transorm it. But we are seeing
JS errors related to it. So trying this as a first pass.

AA-848
2021-06-15 13:13:38 -04:00
Carla Duarte
e9c3a6bc5e fix: update assignment policy logic (#483) 2021-06-14 11:22:57 -04:00
Sagirov Evgeniy
5a30cddd32 feat: Added 'allow_proctoring_opt_out' attribute for SequenceMetadata API. (#485) 2021-06-14 10:44:37 -04:00
Ihor Romaniuk
432cb669f5 feat: add temporary flag for enabling/disabling proctored exams (#464) 2021-06-14 10:42:53 -04:00
Matthew Piatetsky
26e1eb64c5 fix: ensure clicking the bookmarks button doesn't break the unitHasLoaded property (#481) 2021-06-14 09:36:15 -04:00
edX Transifex Bot
30e0e3b8f4 fix(i18n): update translations 2021-06-14 02:08:38 +05:00
Carla Duarte
46e459aaf3 fix: update progress tab assignment policy logic (#482) 2021-06-11 12:05:40 -04:00
Carla Duarte
6949fa8201 fix: updating progress tab to better respect showGrades field (#480) 2021-06-11 09:42:06 -04:00
Albert (AJ) St. Aubin
fab2da4586 feature: Improve the Certificate Alerts in the outline to support new
statuses

[MICROBA-678]

These changes refactor the CertificateAvailableAlert and add new
features to it to support more status alerts for certificates. It
attempts to do so in an iterative manner so that new/updated alerts can
be included over time.
2021-06-10 13:03:12 -04:00
Kristin Aoki
6a402c50ea fix: Scroll page when html anchor tag clicked
This PR adds a listener check to messages. Previously the anchor tags that were set to scroll on the page to another element would open the link outside of the iframe and redirect the parent page. As a result, users would have to have to click the back arrow to navigate back to the course and continue the unit. Now when the listener receives an object with offset in the event.data, the page will scroll to the location provided by offset. The offset is only received when a user clicks on an anchor tag in the unit iframe that focuses on another element on the page. This change will impact the Learner.

Jira issue: TNL-8312
2021-06-09 13:18:46 -04:00
Zachary Hancock
bfeb8c70c0 fix: pin special exams library (#479) 2021-06-09 11:09:05 -04:00
David Ormsbee
cf61c7a747 fix: display Unit titles as <h1> (TNL-8387)
Unit titles were being written to the page as <h2> because the old
courseware experience reserved <h1> for wrapping the header logo link.
We've since determined that this is not a best practice, and the new
courseware MFE in this repo no longer uses a <h1> for that purpose, but
the Unit title was never promoted from <h2> to <h1> until this commit.

Course teams have traditionally been permitted to use <h3>-<h6> in their
content. Making this change does mean that there will now be a gap with
some content, where we skip from <h1> to <h3>. For the short term, we
are NOT recommending course teams use <h2>, until we have a better
chance to evaluate whether that heading should remain reserved for
platform-level use.
2021-06-07 13:49:58 -04:00
Adeel Ehsan
a003059c8f Account activation pop up added: (#474)
VAN-435
2021-06-07 10:33:59 +05:00
Adeel Ehsan
854010ba52 Revert "Account activation pop up added: (#425)" (#473)
This reverts commit 07b82b1d87.
2021-06-05 01:14:52 +05:00
Adeel Ehsan
07b82b1d87 Account activation pop up added: (#425)
VAN-435
2021-06-04 20:13:12 +05:00
Bianca Severino
5c204ad0f9 feat: add honor code component (#465)
This component blocks access to graded units when
the user is required to sign the integrity agreement for
the course. Once signed, it will not appear for the course
again.
2021-06-04 09:06:32 -04:00
Matthew Piatetsky
5bfca28450 feat: update streak discount coupon expiration date (#471) 2021-06-02 17:12:56 -04:00
Carla Duarte
a36da4cd84 AA-807: progress tab eventing (#470) 2021-06-02 14:31:13 -04:00
Diane Kaplan
f08a23ecf9 feat: remove first purchase discount banner from course home (REV-2253) 2021-06-01 09:12:28 -04:00
edX Transifex Bot
3432b0c73b fix(i18n): update translations 2021-05-29 02:00:46 +05:00
edX Transifex Bot
c1c3d5c68f fix(i18n): update translations 2021-05-29 00:03:12 +05:00
julianajlk
2a52534442 fix: gated content banner discount display (#466)
REV-2232
2021-05-28 13:38:49 -04:00
Carla Duarte
519cf27c4e AA-813: fix progress tab related links (#461) 2021-05-26 12:01:18 -04:00
Carla Duarte
9d07f26f13 AA-807: upgrade deadline passed on progress tab (#463) 2021-05-26 12:01:12 -04:00
Albert (AJ) St. Aubin
fdfb60bee8 feat: Updating the messages for certificate availability.
[MICROBA-678]
2021-05-26 07:23:48 -04:00
Dillon Dumesnil
75c9e93241 refactor: Update the defaults for our .env files (#459)
Nulls can provide undesired behavior so we want to switch to empty strings
2021-05-24 09:08:48 -07:00
Vladas Tamoshaitis
a5ba5655b6 feet: [BD-26] Add support for special exams (#435)
* feat: add packages dir to .gitignore

* Investigate exam redirect (#2)

* feat: remove exam redirect

* feat: take control over exam instructions

* refactor: use fedx code structure

* fix: remove debug logging, remove redirect check

Co-authored-by: Vladas Tamoshaitis <vladas.tamoshaitis@raccoongang.com>

* Add state and reducer for check microfrontend_special_exams waffle flag (#4)

* feat: add state and reducer for check microfrontend_special_exams waffle flag

* fix: rename special exams enabled flag

* fix: rename reducer for setting special exams enabled flag

* refactor: timer feature

* feat(tests): extend tests + fix failing ones, fix quality

* fix: revert removing package lock file

Co-authored-by: Vladas Tamoshaitis <vladas.tamoshaitis@raccoongang.com>

* fix: naming of waffle flag helpers to reflect relation with mfe

* fix: change naming of the waffle flag

* fix: revert remove package lock file

* feat: switch to @edx npm package

* fix: Remove redundant references from .gitignore

* fix: add is_mfe_special_exams_enabled to courseMetadata.factory.js

* fix: fix tests for 'Sequence' content wrapped in 'SequenceExamWrapper'

Co-authored-by: Sagirov Eugeniy <sagirov19@gmail.com>
Co-authored-by: Vladas Tamoshaitis <vladas.tamoshaitis@raccoongang.com>
Co-authored-by: Sagirov Evgeniy <34642612+UvgenGen@users.noreply.github.com>
Co-authored-by: Igor Degtiarov <igor.degtiarov@raccoongang.com>
2021-05-24 08:44:01 -04:00
edX Transifex Bot
46056a0c53 fix(i18n): update translations 2021-05-24 02:08:01 +05:00
Carla Duarte
05b2439ff6 AA-816: update grade summary calculations (#456) 2021-05-19 15:44:49 -04:00
Carla Duarte
663bf7562b AA-814: course grade footer fix (#455) 2021-05-19 13:44:28 -04:00
Renovate Bot
58e29d81be fix(deps): update dependency react-redux to v7.2.4 2021-05-17 20:40:00 +00:00
Renovate Bot
027eeb8a49 chore(deps): update dependency glob to v7.1.7 2021-05-17 20:05:07 +00:00
Renovate Bot
e6a4bcd833 chore(deps): update dependency codecov to v3.8.2 2021-05-17 19:09:59 +00:00
Julia Eskew
1da461e2de feat: Support classifying certain errors as ignored errors in an MFE. (#447)
Ignored errors are sent to New Relic as page actions instead of JS errors,
allowing those errors to still be tracked as occurring but without causing
unnecessary alerts.

Ignored errors are configured per-MFE, *not* globally.

Bump the frontend-platform version to 1.10.2.

Add IGNORED_ERROR_REGEX variable for use in development. The actual
production value will be read from the YAML in edx-internal.

TNL-7924
2021-05-17 13:43:01 -04:00
Matthew Piatetsky
3c07cab8c2 [AA-746] fix: add hour/minute to assignment due dates and update dates timeline with branding styles (#450)
* fix: add hour/minute to assignment due dates and update dates timeline with branding styles

* update badge styles
2021-05-17 12:39:34 -04:00
Carla Duarte
110088688a fix: grade range bug (#449) 2021-05-17 09:23:49 -04:00
Matthew Piatetsky
d8243d6ea8 fix: remove currency symbol to avoid double currency symbol with some upgrade buttons (#448) 2021-05-14 09:48:50 -04:00
Carla Duarte
b1fdbcccf3 AA-790: progress tab handle unenrolled/unauthenticated users (#445) 2021-05-13 15:07:19 -04:00
edX Transifex Bot
00205d4b1f fix(i18n): update translations 2021-05-12 22:29:14 +05:00
julianajlk
6100f3ac2e fix: i18n translations for UpgradeCard (#442)
REV-2126 Value Prop
2021-05-12 09:50:08 -04:00
121 changed files with 4655 additions and 2501 deletions

65
.env
View File

@@ -1,32 +1,35 @@
NODE_ENV='production'
ACCESS_TOKEN_COOKIE_NAME=null
BASE_URL=null
CREDENTIALS_BASE_URL=null
CSRF_TOKEN_API_PATH=null
DISCOVERY_API_BASE_URL=null
ENTERPRISE_LEARNER_PORTAL_HOSTNAME=null
ECOMMERCE_BASE_URL=null
INSIGHTS_BASE_URL=null
LANGUAGE_PREFERENCE_COOKIE_NAME=null
LMS_BASE_URL=null
LOGIN_URL=null
LOGOUT_URL=null
LOGO_URL=null
LOGO_TRADEMARK_URL=null
LOGO_WHITE_URL=null
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
STUDIO_BASE_URL=null
SUPPORT_URL=null
SUPPORT_URL_CALCULATOR_MATH=null
SUPPORT_URL_ID_VERIFICATION=null
SUPPORT_URL_VERIFIED_CERTIFICATE=null
TWITTER_HASHTAG=null
TWITTER_URL=null
USER_INFO_COOKIE_NAME=null
ACCESS_TOKEN_COOKIE_NAME=''
BASE_URL=''
CREDENTIALS_BASE_URL=''
CSRF_TOKEN_API_PATH=''
DISCOVERY_API_BASE_URL=''
ECOMMERCE_BASE_URL=''
ENTERPRISE_LEARNER_PORTAL_HOSTNAME=''
IGNORED_ERROR_REGEX=''
INSIGHTS_BASE_URL=''
LANGUAGE_PREFERENCE_COOKIE_NAME=''
LMS_BASE_URL=''
LOGIN_URL=''
LOGOUT_URL=''
LOGO_URL=''
LOGO_TRADEMARK_URL=''
LOGO_WHITE_URL=''
FAVICON_URL=''
MARKETING_SITE_BASE_URL=''
ORDER_HISTORY_URL=''
REFRESH_ACCESS_TOKEN_ENDPOINT=''
SEARCH_CATALOG_URL=''
SEGMENT_KEY=''
SITE_NAME=''
SOCIAL_UTM_MILESTONE_CAMPAIGN=''
STUDIO_BASE_URL=''
SUPPORT_URL=''
SUPPORT_URL_CALCULATOR_MATH=''
SUPPORT_URL_ID_VERIFICATION=''
SUPPORT_URL_VERIFIED_CERTIFICATE=''
TERMS_OF_SERVICE_URL=''
TWITTER_HASHTAG=''
TWITTER_URL=''
USER_INFO_COOKIE_NAME=''
SESSION_COOKIE_DOMAIN=''

View File

@@ -6,6 +6,7 @@ CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
DISCOVERY_API_BASE_URL='http://localhost:18381'
ECOMMERCE_BASE_URL='http://localhost:18130'
ENTERPRISE_LEARNER_PORTAL_HOSTNAME='localhost:8734'
IGNORED_ERROR_REGEX=''
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
LMS_BASE_URL='http://localhost:18000'
LOGIN_URL='http://localhost:18000/login'
@@ -19,7 +20,7 @@ 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
SEGMENT_KEY=''
SITE_NAME='edX'
SOCIAL_UTM_MILESTONE_CAMPAIGN='edxmilestone'
STUDIO_BASE_URL='http://localhost:18010'
@@ -27,6 +28,8 @@ SUPPORT_URL='https://support.edx.org'
SUPPORT_URL_CALCULATOR_MATH='https://support.edx.org/hc/en-us/articles/360000038428-Entering-math-expressions-in-assignments-or-the-calculator'
SUPPORT_URL_ID_VERIFICATION='https://support.edx.org/hc/en-us/articles/206503858-How-do-I-verify-my-identity'
SUPPORT_URL_VERIFIED_CERTIFICATE='https://support.edx.org/hc/en-us/articles/206502008-What-is-a-verified-certificate'
TERMS_OF_SERVICE_URL='https://www.edx.org/edx-terms-service'
TWITTER_HASHTAG='myedxjourney'
TWITTER_URL='https://twitter.com/edXOnline'
USER_INFO_COOKIE_NAME='edx-user-info'
SESSION_COOKIE_DOMAIN='localhost'

View File

@@ -6,6 +6,7 @@ CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
DISCOVERY_API_BASE_URL='http://localhost:18381'
ECOMMERCE_BASE_URL='http://localhost:18130'
ENTERPRISE_LEARNER_PORTAL_HOSTNAME='localhost:8734'
IGNORED_ERROR_REGEX=''
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
LMS_BASE_URL='http://localhost:18000'
LOGIN_URL='http://localhost:18000/login'
@@ -19,7 +20,7 @@ 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
SEGMENT_KEY=''
SITE_NAME='edX'
SOCIAL_UTM_MILESTONE_CAMPAIGN='edxmilestone'
STUDIO_BASE_URL='http://localhost:18010'
@@ -27,6 +28,7 @@ SUPPORT_URL='https://support.edx.org'
SUPPORT_URL_CALCULATOR_MATH='https://support.edx.org/hc/en-us/articles/360000038428-Entering-math-expressions-in-assignments-or-the-calculator'
SUPPORT_URL_ID_VERIFICATION='https://support.edx.org/hc/en-us/articles/206503858-How-do-I-verify-my-identity'
SUPPORT_URL_VERIFIED_CERTIFICATE='https://support.edx.org/hc/en-us/articles/206502008-What-is-a-verified-certificate'
TERMS_OF_SERVICE_URL='https://www.edx.org/edx-terms-service'
TWITTER_HASHTAG='myedxjourney'
TWITTER_URL='https://twitter.com/edXOnline'
USER_INFO_COOKIE_NAME='edx-user-info'

View File

@@ -1,4 +1,5 @@
coverage/*
dist/
packages/
node_modules/
jest.config.js
jest.config.js

2159
package-lock.json generated
View File

@@ -39,9 +39,9 @@
}
},
"@babel/compat-data": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.12.13.tgz",
"integrity": "sha512-U/hshG5R+SIoW7HVWIdmy1cB7s3ki+r3FpyEZiCgpi4tFgPnX/vynY80ZGSASOIrUM6O7VxOgCZgdt7h97bUGg==",
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.0.tgz",
"integrity": "sha512-vu9V3uMM/1o5Hl5OekMUowo3FqXLJSw+s+66nt0fSWVWTtmosdzn45JHOB3cPtZoe6CTBDzvSw0RdOY85Q37+Q==",
"dev": true
},
"@babel/core": {
@@ -92,12 +92,12 @@
}
},
"@babel/generator": {
"version": "7.12.15",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.15.tgz",
"integrity": "sha512-6F2xHxBiFXWNSGb7vyCUTBF8RCLY66rS0zEPcP8t/nQyXjha5EuK4z7H5o7fWG8B4M7y6mqVWq1J+1PuwRhecQ==",
"version": "7.14.1",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.1.tgz",
"integrity": "sha512-TMGhsXMXCP/O1WtQmZjpEYDhCYC9vFhayWZPJSZCGkPJgUqX0rF0wwtrYvnzVxIjcF80tkUertXVk5cwqi5cAQ==",
"dev": true,
"requires": {
"@babel/types": "^7.12.13",
"@babel/types": "^7.14.1",
"jsesc": "^2.5.1",
"source-map": "^0.5.0"
},
@@ -130,34 +130,43 @@
}
},
"@babel/helper-compilation-targets": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.12.13.tgz",
"integrity": "sha512-dXof20y/6wB5HnLOGyLh/gobsMvDNoekcC+8MCV2iaTd5JemhFkPD73QB+tK3iFC9P0xJC73B6MvKkyUfS9cCw==",
"version": "7.13.16",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.16.tgz",
"integrity": "sha512-3gmkYIrpqsLlieFwjkGgLaSHmhnvlAYzZLlYVjlW+QwI+1zE17kGxuJGmIqDQdYp56XdmGeD+Bswx0UTyG18xA==",
"dev": true,
"requires": {
"@babel/compat-data": "^7.12.13",
"@babel/helper-validator-option": "^7.12.11",
"@babel/compat-data": "^7.13.15",
"@babel/helper-validator-option": "^7.12.17",
"browserslist": "^4.14.5",
"semver": "^5.5.0"
"semver": "^6.3.0"
},
"dependencies": {
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
}
}
},
"@babel/helper-create-class-features-plugin": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.12.13.tgz",
"integrity": "sha512-Vs/e9wv7rakKYeywsmEBSRC9KtmE7Px+YBlESekLeJOF0zbGUicGfXSNi3o+tfXSNS48U/7K9mIOOCR79Cl3+Q==",
"version": "7.14.1",
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.14.1.tgz",
"integrity": "sha512-r8rsUahG4ywm0QpGcCrLaUSOuNAISR3IZCg4Fx05Ozq31aCUrQsTLH6KPxy0N5ULoQ4Sn9qjNdGNtbPWAC6hYg==",
"dev": true,
"requires": {
"@babel/helper-annotate-as-pure": "^7.12.13",
"@babel/helper-function-name": "^7.12.13",
"@babel/helper-member-expression-to-functions": "^7.12.13",
"@babel/helper-member-expression-to-functions": "^7.13.12",
"@babel/helper-optimise-call-expression": "^7.12.13",
"@babel/helper-replace-supers": "^7.12.13",
"@babel/helper-replace-supers": "^7.13.12",
"@babel/helper-split-export-declaration": "^7.12.13"
}
},
"@babel/helper-create-regexp-features-plugin": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.13.tgz",
"integrity": "sha512-XC+kiA0J3at6E85dL5UnCYfVOcIZ834QcAY0TIpgUVnz0zDzg+0TtvZTnJ4g9L1dPRGe30Qi03XCIS4tYCLtqw==",
"version": "7.12.17",
"resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.17.tgz",
"integrity": "sha512-p2VGmBu9oefLZ2nQpgnEnG0ZlRPvL8gAGvPUMQwUdaE8k49rOMuZpOwdQoy5qJf6K8jL3bcAMhVUlHAjIgJHUg==",
"dev": true,
"requires": {
"@babel/helper-annotate-as-pure": "^7.12.13",
@@ -165,12 +174,12 @@
}
},
"@babel/helper-explode-assignable-expression": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.12.13.tgz",
"integrity": "sha512-5loeRNvMo9mx1dA/d6yNi+YiKziJZFylZnCo1nmFF4qPU4yJ14abhWESuSMQSlQxWdxdOFzxXjk/PpfudTtYyw==",
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.13.0.tgz",
"integrity": "sha512-qS0peLTDP8kOisG1blKbaoBg/o9OSa1qoumMjTK5pM+KDTtpxpsiubnCGP34vK8BXGcb2M9eigwgvoJryrzwWA==",
"dev": true,
"requires": {
"@babel/types": "^7.12.13"
"@babel/types": "^7.13.0"
}
},
"@babel/helper-function-name": {
@@ -194,47 +203,47 @@
}
},
"@babel/helper-hoist-variables": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.12.13.tgz",
"integrity": "sha512-KSC5XSj5HreRhYQtZ3cnSnQwDzgnbdUDEFsxkN0m6Q3WrCRt72xrnZ8+h+pX7YxM7hr87zIO3a/v5p/H3TrnVw==",
"version": "7.13.16",
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.13.16.tgz",
"integrity": "sha512-1eMtTrXtrwscjcAeO4BVK+vvkxaLJSPFz1w1KLawz6HLNi9bPFGBNwwDyVfiu1Tv/vRRFYfoGaKhmAQPGPn5Wg==",
"dev": true,
"requires": {
"@babel/types": "^7.12.13"
"@babel/traverse": "^7.13.15",
"@babel/types": "^7.13.16"
}
},
"@babel/helper-member-expression-to-functions": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.13.tgz",
"integrity": "sha512-B+7nN0gIL8FZ8SvMcF+EPyB21KnCcZHQZFczCxbiNGV/O0rsrSBlWGLzmtBJ3GMjSVMIm4lpFhR+VdVBuIsUcQ==",
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.12.tgz",
"integrity": "sha512-48ql1CLL59aKbU94Y88Xgb2VFy7a95ykGRbJJaaVv+LX5U8wFpLfiGXJJGUozsmA1oEh/o5Bp60Voq7ACyA/Sw==",
"dev": true,
"requires": {
"@babel/types": "^7.12.13"
"@babel/types": "^7.13.12"
}
},
"@babel/helper-module-imports": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.13.tgz",
"integrity": "sha512-NGmfvRp9Rqxy0uHSSVP+SRIW1q31a7Ji10cLBcqSDUngGentY4FRiHOFZFE1CLU5eiL0oE8reH7Tg1y99TDM/g==",
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz",
"integrity": "sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA==",
"dev": true,
"requires": {
"@babel/types": "^7.12.13"
"@babel/types": "^7.13.12"
}
},
"@babel/helper-module-transforms": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.12.13.tgz",
"integrity": "sha512-acKF7EjqOR67ASIlDTupwkKM1eUisNAjaSduo5Cz+793ikfnpe7p4Q7B7EWU2PCoSTPWsQkR7hRUWEIZPiVLGA==",
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.0.tgz",
"integrity": "sha512-L40t9bxIuGOfpIGA3HNkJhU9qYrf4y5A5LUSw7rGMSn+pcG8dfJ0g6Zval6YJGd2nEjI7oP00fRdnhLKndx6bw==",
"dev": true,
"requires": {
"@babel/helper-module-imports": "^7.12.13",
"@babel/helper-replace-supers": "^7.12.13",
"@babel/helper-simple-access": "^7.12.13",
"@babel/helper-module-imports": "^7.13.12",
"@babel/helper-replace-supers": "^7.13.12",
"@babel/helper-simple-access": "^7.13.12",
"@babel/helper-split-export-declaration": "^7.12.13",
"@babel/helper-validator-identifier": "^7.12.11",
"@babel/helper-validator-identifier": "^7.14.0",
"@babel/template": "^7.12.13",
"@babel/traverse": "^7.12.13",
"@babel/types": "^7.12.13",
"lodash": "^4.17.19"
"@babel/traverse": "^7.14.0",
"@babel/types": "^7.14.0"
}
},
"@babel/helper-optimise-call-expression": {
@@ -247,41 +256,41 @@
}
},
"@babel/helper-plugin-utils": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.12.13.tgz",
"integrity": "sha512-C+10MXCXJLiR6IeG9+Wiejt9jmtFpxUc3MQqCmPY8hfCjyUGl9kT+B2okzEZrtykiwrc4dbCPdDoz0A/HQbDaA==",
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz",
"integrity": "sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ==",
"dev": true
},
"@babel/helper-remap-async-to-generator": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.12.13.tgz",
"integrity": "sha512-Qa6PU9vNcj1NZacZZI1Mvwt+gXDH6CTfgAkSjeRMLE8HxtDK76+YDId6NQR+z7Rgd5arhD2cIbS74r0SxD6PDA==",
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.13.0.tgz",
"integrity": "sha512-pUQpFBE9JvC9lrQbpX0TmeNIy5s7GnZjna2lhhcHC7DzgBs6fWn722Y5cfwgrtrqc7NAJwMvOa0mKhq6XaE4jg==",
"dev": true,
"requires": {
"@babel/helper-annotate-as-pure": "^7.12.13",
"@babel/helper-wrap-function": "^7.12.13",
"@babel/types": "^7.12.13"
"@babel/helper-wrap-function": "^7.13.0",
"@babel/types": "^7.13.0"
}
},
"@babel/helper-replace-supers": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.13.tgz",
"integrity": "sha512-pctAOIAMVStI2TMLhozPKbf5yTEXc0OJa0eENheb4w09SrgOWEs+P4nTOZYJQCqs8JlErGLDPDJTiGIp3ygbLg==",
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.13.12.tgz",
"integrity": "sha512-Gz1eiX+4yDO8mT+heB94aLVNCL+rbuT2xy4YfyNqu8F+OI6vMvJK891qGBTqL9Uc8wxEvRW92Id6G7sDen3fFw==",
"dev": true,
"requires": {
"@babel/helper-member-expression-to-functions": "^7.12.13",
"@babel/helper-member-expression-to-functions": "^7.13.12",
"@babel/helper-optimise-call-expression": "^7.12.13",
"@babel/traverse": "^7.12.13",
"@babel/types": "^7.12.13"
"@babel/traverse": "^7.13.0",
"@babel/types": "^7.13.12"
}
},
"@babel/helper-simple-access": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.12.13.tgz",
"integrity": "sha512-0ski5dyYIHEfwpWGx5GPWhH35j342JaflmCeQmsPWcrOQDtCN6C1zKAVRFVbK53lPW2c9TsuLLSUDf0tIGJ5hA==",
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.13.12.tgz",
"integrity": "sha512-7FEjbrx5SL9cWvXioDbnlYTppcZGuCY6ow3/D5vMggb2Ywgu4dMrpTJX0JdQAIcRRUElOIxF3yEooa9gUb9ZbA==",
"dev": true,
"requires": {
"@babel/types": "^7.12.13"
"@babel/types": "^7.13.12"
}
},
"@babel/helper-skip-transparent-expression-wrappers": {
@@ -303,66 +312,66 @@
}
},
"@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==",
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz",
"integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==",
"dev": true
},
"@babel/helper-validator-option": {
"version": "7.12.11",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.11.tgz",
"integrity": "sha512-TBFCyj939mFSdeX7U7DDj32WtzYY7fDcalgq8v3fBZMNOJQNn7nOYzMaUCiPxPYfCup69mtIpqlKgMZLvQ8Xhw==",
"version": "7.12.17",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.17.tgz",
"integrity": "sha512-TopkMDmLzq8ngChwRlyjR6raKD6gMSae4JdYDB8bByKreQgG0RBTuKe9LRxW3wFtUnjxOPRKBDwEH6Mg5KeDfw==",
"dev": true
},
"@babel/helper-wrap-function": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.12.13.tgz",
"integrity": "sha512-t0aZFEmBJ1LojdtJnhOaQEVejnzYhyjWHSsNSNo8vOYRbAJNh6r6GQF7pd36SqG7OKGbn+AewVQ/0IfYfIuGdw==",
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.13.0.tgz",
"integrity": "sha512-1UX9F7K3BS42fI6qd2A4BjKzgGjToscyZTdp1DjknHLCIvpgne6918io+aL5LXFcER/8QWiwpoY902pVEqgTXA==",
"dev": true,
"requires": {
"@babel/helper-function-name": "^7.12.13",
"@babel/template": "^7.12.13",
"@babel/traverse": "^7.12.13",
"@babel/types": "^7.12.13"
"@babel/traverse": "^7.13.0",
"@babel/types": "^7.13.0"
}
},
"@babel/helpers": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.12.13.tgz",
"integrity": "sha512-oohVzLRZ3GQEk4Cjhfs9YkJA4TdIDTObdBEZGrd6F/T0GPSnuV6l22eMcxlvcvzVIPH3VTtxbseudM1zIE+rPQ==",
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.14.0.tgz",
"integrity": "sha512-+ufuXprtQ1D1iZTO/K9+EBRn+qPWMJjZSw/S0KlFrxCw4tkrzv9grgpDHkY9MeQTjTY8i2sp7Jep8DfU6tN9Mg==",
"dev": true,
"requires": {
"@babel/template": "^7.12.13",
"@babel/traverse": "^7.12.13",
"@babel/types": "^7.12.13"
"@babel/traverse": "^7.14.0",
"@babel/types": "^7.14.0"
}
},
"@babel/highlight": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.12.13.tgz",
"integrity": "sha512-kocDQvIbgMKlWxXe9fof3TQ+gkIPOUSEYhJjqUjvKMez3krV7vbzYCDq39Oj11UAVK7JqPVGQPlgE85dPNlQww==",
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz",
"integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.12.11",
"@babel/helper-validator-identifier": "^7.14.0",
"chalk": "^2.0.0",
"js-tokens": "^4.0.0"
}
},
"@babel/parser": {
"version": "7.12.15",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.15.tgz",
"integrity": "sha512-AQBOU2Z9kWwSZMd6lNjCX0GUgFonL1wAM1db8L8PMk9UDaGsRCArBkU4Sc+UCM3AE4hjbXx+h58Lb3QT4oRmrA==",
"version": "7.14.1",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.1.tgz",
"integrity": "sha512-muUGEKu8E/ftMTPlNp+mc6zL3E9zKWmF5sDHZ5MSsoTP9Wyz64AhEf9kD08xYJ7w6Hdcu8H550ircnPyWSIF0Q==",
"dev": true
},
"@babel/plugin-proposal-async-generator-functions": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.12.13.tgz",
"integrity": "sha512-1KH46Hx4WqP77f978+5Ye/VUbuwQld2hph70yaw2hXS2v7ER2f3nlpNMu909HO2rbvP0NKLlMVDPh9KXklVMhA==",
"version": "7.13.15",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.13.15.tgz",
"integrity": "sha512-VapibkWzFeoa6ubXy/NgV5U2U4MVnUlvnx6wo1XhlsaTrLYWE0UFpDQsVrmn22q5CzeloqJ8gEMHSKxuee6ZdA==",
"dev": true,
"requires": {
"@babel/helper-plugin-utils": "^7.12.13",
"@babel/helper-remap-async-to-generator": "^7.12.13",
"@babel/plugin-syntax-async-generators": "^7.8.0"
"@babel/helper-plugin-utils": "^7.13.0",
"@babel/helper-remap-async-to-generator": "^7.13.0",
"@babel/plugin-syntax-async-generators": "^7.8.4"
}
},
"@babel/plugin-proposal-class-properties": {
@@ -376,33 +385,33 @@
}
},
"@babel/plugin-proposal-dynamic-import": {
"version": "7.12.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.12.1.tgz",
"integrity": "sha512-a4rhUSZFuq5W8/OO8H7BL5zspjnc1FLd9hlOxIK/f7qG4a0qsqk8uvF/ywgBA8/OmjsapjpvaEOYItfGG1qIvQ==",
"version": "7.13.8",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.13.8.tgz",
"integrity": "sha512-ONWKj0H6+wIRCkZi9zSbZtE/r73uOhMVHh256ys0UzfM7I3d4n+spZNWjOnJv2gzopumP2Wxi186vI8N0Y2JyQ==",
"dev": true,
"requires": {
"@babel/helper-plugin-utils": "^7.10.4",
"@babel/plugin-syntax-dynamic-import": "^7.8.0"
"@babel/helper-plugin-utils": "^7.13.0",
"@babel/plugin-syntax-dynamic-import": "^7.8.3"
}
},
"@babel/plugin-proposal-json-strings": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.12.13.tgz",
"integrity": "sha512-v9eEi4GiORDg8x+Dmi5r8ibOe0VXoKDeNPYcTTxdGN4eOWikrJfDJCJrr1l5gKGvsNyGJbrfMftC2dTL6oz7pg==",
"version": "7.13.8",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.13.8.tgz",
"integrity": "sha512-w4zOPKUFPX1mgvTmL/fcEqy34hrQ1CRcGxdphBc6snDnnqJ47EZDIyop6IwXzAC8G916hsIuXB2ZMBCExC5k7Q==",
"dev": true,
"requires": {
"@babel/helper-plugin-utils": "^7.12.13",
"@babel/plugin-syntax-json-strings": "^7.8.0"
"@babel/helper-plugin-utils": "^7.13.0",
"@babel/plugin-syntax-json-strings": "^7.8.3"
}
},
"@babel/plugin-proposal-nullish-coalescing-operator": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.12.13.tgz",
"integrity": "sha512-Qoxpy+OxhDBI5kRqliJFAl4uWXk3Bn24WeFstPH0iLymFehSAUR8MHpqU7njyXv/qbo7oN6yTy5bfCmXdKpo1Q==",
"version": "7.13.8",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.13.8.tgz",
"integrity": "sha512-iePlDPBn//UhxExyS9KyeYU7RM9WScAG+D3Hhno0PLJebAEpDZMocbDe64eqynhNAnwz/vZoL/q/QB2T1OH39A==",
"dev": true,
"requires": {
"@babel/helper-plugin-utils": "^7.12.13",
"@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0"
"@babel/helper-plugin-utils": "^7.13.0",
"@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3"
}
},
"@babel/plugin-proposal-numeric-separator": {
@@ -427,34 +436,34 @@
}
},
"@babel/plugin-proposal-optional-catch-binding": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.12.13.tgz",
"integrity": "sha512-9+MIm6msl9sHWg58NvqpNpLtuFbmpFYk37x8kgnGzAHvX35E1FyAwSUt5hIkSoWJFSAH+iwU8bJ4fcD1zKXOzg==",
"version": "7.13.8",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.13.8.tgz",
"integrity": "sha512-0wS/4DUF1CuTmGo+NiaHfHcVSeSLj5S3e6RivPTg/2k3wOv3jO35tZ6/ZWsQhQMvdgI7CwphjQa/ccarLymHVA==",
"dev": true,
"requires": {
"@babel/helper-plugin-utils": "^7.12.13",
"@babel/plugin-syntax-optional-catch-binding": "^7.8.0"
"@babel/helper-plugin-utils": "^7.13.0",
"@babel/plugin-syntax-optional-catch-binding": "^7.8.3"
}
},
"@babel/plugin-proposal-optional-chaining": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.12.13.tgz",
"integrity": "sha512-0ZwjGfTcnZqyV3y9DSD1Yk3ebp+sIUpT2YDqP8hovzaNZnQq2Kd7PEqa6iOIUDBXBt7Jl3P7YAcEIL5Pz8u09Q==",
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.13.12.tgz",
"integrity": "sha512-fcEdKOkIB7Tf4IxrgEVeFC4zeJSTr78no9wTdBuZZbqF64kzllU0ybo2zrzm7gUQfxGhBgq4E39oRs8Zx/RMYQ==",
"dev": true,
"requires": {
"@babel/helper-plugin-utils": "^7.12.13",
"@babel/helper-plugin-utils": "^7.13.0",
"@babel/helper-skip-transparent-expression-wrappers": "^7.12.1",
"@babel/plugin-syntax-optional-chaining": "^7.8.0"
"@babel/plugin-syntax-optional-chaining": "^7.8.3"
}
},
"@babel/plugin-proposal-private-methods": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.12.13.tgz",
"integrity": "sha512-sV0V57uUwpauixvR7s2o75LmwJI6JECwm5oPUY5beZB1nBl2i37hc7CJGqB5G+58fur5Y6ugvl3LRONk5x34rg==",
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.13.0.tgz",
"integrity": "sha512-MXyyKQd9inhx1kDYPkFRVOBXQ20ES8Pto3T7UZ92xj2mY0EVD8oAVzeyYuVfy/mxAdTSIayOvg+aVzcHV2bn6Q==",
"dev": true,
"requires": {
"@babel/helper-create-class-features-plugin": "^7.12.13",
"@babel/helper-plugin-utils": "^7.12.13"
"@babel/helper-create-class-features-plugin": "^7.13.0",
"@babel/helper-plugin-utils": "^7.13.0"
}
},
"@babel/plugin-proposal-unicode-property-regex": {
@@ -594,23 +603,23 @@
}
},
"@babel/plugin-transform-arrow-functions": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.12.13.tgz",
"integrity": "sha512-tBtuN6qtCTd+iHzVZVOMNp+L04iIJBpqkdY42tWbmjIT5wvR2kx7gxMBsyhQtFzHwBbyGi9h8J8r9HgnOpQHxg==",
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.13.0.tgz",
"integrity": "sha512-96lgJagobeVmazXFaDrbmCLQxBysKu7U6Do3mLsx27gf5Dk85ezysrs2BZUpXD703U/Su1xTBDxxar2oa4jAGg==",
"dev": true,
"requires": {
"@babel/helper-plugin-utils": "^7.12.13"
"@babel/helper-plugin-utils": "^7.13.0"
}
},
"@babel/plugin-transform-async-to-generator": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.12.13.tgz",
"integrity": "sha512-psM9QHcHaDr+HZpRuJcE1PXESuGWSCcbiGFFhhwfzdbTxaGDVzuVtdNYliAwcRo3GFg0Bc8MmI+AvIGYIJG04A==",
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.13.0.tgz",
"integrity": "sha512-3j6E004Dx0K3eGmhxVJxwwI89CTJrce7lg3UrtFuDAVQ/2+SJ/h/aSFOeE6/n0WB1GsOffsJp6MnPQNQ8nmwhg==",
"dev": true,
"requires": {
"@babel/helper-module-imports": "^7.12.13",
"@babel/helper-plugin-utils": "^7.12.13",
"@babel/helper-remap-async-to-generator": "^7.12.13"
"@babel/helper-plugin-utils": "^7.13.0",
"@babel/helper-remap-async-to-generator": "^7.13.0"
}
},
"@babel/plugin-transform-block-scoped-functions": {
@@ -623,45 +632,45 @@
}
},
"@babel/plugin-transform-block-scoping": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.12.13.tgz",
"integrity": "sha512-Pxwe0iqWJX4fOOM2kEZeUuAxHMWb9nK+9oh5d11bsLoB0xMg+mkDpt0eYuDZB7ETrY9bbcVlKUGTOGWy7BHsMQ==",
"version": "7.14.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.14.1.tgz",
"integrity": "sha512-2mQXd0zBrwfp0O1moWIhPpEeTKDvxyHcnma3JATVP1l+CctWBuot6OJG8LQ4DnBj4ZZPSmlb/fm4mu47EOAnVA==",
"dev": true,
"requires": {
"@babel/helper-plugin-utils": "^7.12.13"
"@babel/helper-plugin-utils": "^7.13.0"
}
},
"@babel/plugin-transform-classes": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.12.13.tgz",
"integrity": "sha512-cqZlMlhCC1rVnxE5ZGMtIb896ijL90xppMiuWXcwcOAuFczynpd3KYemb91XFFPi3wJSe/OcrX9lXoowatkkxA==",
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.13.0.tgz",
"integrity": "sha512-9BtHCPUARyVH1oXGcSJD3YpsqRLROJx5ZNP6tN5vnk17N0SVf9WCtf8Nuh1CFmgByKKAIMstitKduoCmsaDK5g==",
"dev": true,
"requires": {
"@babel/helper-annotate-as-pure": "^7.12.13",
"@babel/helper-function-name": "^7.12.13",
"@babel/helper-optimise-call-expression": "^7.12.13",
"@babel/helper-plugin-utils": "^7.12.13",
"@babel/helper-replace-supers": "^7.12.13",
"@babel/helper-plugin-utils": "^7.13.0",
"@babel/helper-replace-supers": "^7.13.0",
"@babel/helper-split-export-declaration": "^7.12.13",
"globals": "^11.1.0"
}
},
"@babel/plugin-transform-computed-properties": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.12.13.tgz",
"integrity": "sha512-dDfuROUPGK1mTtLKyDPUavmj2b6kFu82SmgpztBFEO974KMjJT+Ytj3/oWsTUMBmgPcp9J5Pc1SlcAYRpJ2hRA==",
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.13.0.tgz",
"integrity": "sha512-RRqTYTeZkZAz8WbieLTvKUEUxZlUTdmL5KGMyZj7FnMfLNKV4+r5549aORG/mgojRmFlQMJDUupwAMiF2Q7OUg==",
"dev": true,
"requires": {
"@babel/helper-plugin-utils": "^7.12.13"
"@babel/helper-plugin-utils": "^7.13.0"
}
},
"@babel/plugin-transform-destructuring": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.12.13.tgz",
"integrity": "sha512-Dn83KykIFzjhA3FDPA1z4N+yfF3btDGhjnJwxIj0T43tP0flCujnU8fKgEkf0C1biIpSv9NZegPBQ1J6jYkwvQ==",
"version": "7.13.17",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.13.17.tgz",
"integrity": "sha512-UAUqiLv+uRLO+xuBKKMEpC+t7YRNVRqBsWWq1yKXbBZBje/t3IXCiSinZhjn/DC3qzBfICeYd2EFGEbHsh5RLA==",
"dev": true,
"requires": {
"@babel/helper-plugin-utils": "^7.12.13"
"@babel/helper-plugin-utils": "^7.13.0"
}
},
"@babel/plugin-transform-dotall-regex": {
@@ -694,12 +703,12 @@
}
},
"@babel/plugin-transform-for-of": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.12.13.tgz",
"integrity": "sha512-xCbdgSzXYmHGyVX3+BsQjcd4hv4vA/FDy7Kc8eOpzKmBBPEOTurt0w5fCRQaGl+GSBORKgJdstQ1rHl4jbNseQ==",
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.13.0.tgz",
"integrity": "sha512-IHKT00mwUVYE0zzbkDgNRP6SRzvfGCYsOxIRz8KsiaaHCcT9BWIkO+H9QRJseHBLOGBZkHUdHiqj6r0POsdytg==",
"dev": true,
"requires": {
"@babel/helper-plugin-utils": "^7.12.13"
"@babel/helper-plugin-utils": "^7.13.0"
}
},
"@babel/plugin-transform-function-name": {
@@ -731,49 +740,49 @@
}
},
"@babel/plugin-transform-modules-amd": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.12.13.tgz",
"integrity": "sha512-JHLOU0o81m5UqG0Ulz/fPC68/v+UTuGTWaZBUwpEk1fYQ1D9LfKV6MPn4ttJKqRo5Lm460fkzjLTL4EHvCprvA==",
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.14.0.tgz",
"integrity": "sha512-CF4c5LX4LQ03LebQxJ5JZes2OYjzBuk1TdiF7cG7d5dK4lAdw9NZmaxq5K/mouUdNeqwz3TNjnW6v01UqUNgpQ==",
"dev": true,
"requires": {
"@babel/helper-module-transforms": "^7.12.13",
"@babel/helper-plugin-utils": "^7.12.13",
"@babel/helper-module-transforms": "^7.14.0",
"@babel/helper-plugin-utils": "^7.13.0",
"babel-plugin-dynamic-import-node": "^2.3.3"
}
},
"@babel/plugin-transform-modules-commonjs": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.12.13.tgz",
"integrity": "sha512-OGQoeVXVi1259HjuoDnsQMlMkT9UkZT9TpXAsqWplS/M0N1g3TJAn/ByOCeQu7mfjc5WpSsRU+jV1Hd89ts0kQ==",
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.14.0.tgz",
"integrity": "sha512-EX4QePlsTaRZQmw9BsoPeyh5OCtRGIhwfLquhxGp5e32w+dyL8htOcDwamlitmNFK6xBZYlygjdye9dbd9rUlQ==",
"dev": true,
"requires": {
"@babel/helper-module-transforms": "^7.12.13",
"@babel/helper-plugin-utils": "^7.12.13",
"@babel/helper-simple-access": "^7.12.13",
"@babel/helper-module-transforms": "^7.14.0",
"@babel/helper-plugin-utils": "^7.13.0",
"@babel/helper-simple-access": "^7.13.12",
"babel-plugin-dynamic-import-node": "^2.3.3"
}
},
"@babel/plugin-transform-modules-systemjs": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.12.13.tgz",
"integrity": "sha512-aHfVjhZ8QekaNF/5aNdStCGzwTbU7SI5hUybBKlMzqIMC7w7Ho8hx5a4R/DkTHfRfLwHGGxSpFt9BfxKCoXKoA==",
"version": "7.13.8",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.13.8.tgz",
"integrity": "sha512-hwqctPYjhM6cWvVIlOIe27jCIBgHCsdH2xCJVAYQm7V5yTMoilbVMi9f6wKg0rpQAOn6ZG4AOyvCqFF/hUh6+A==",
"dev": true,
"requires": {
"@babel/helper-hoist-variables": "^7.12.13",
"@babel/helper-module-transforms": "^7.12.13",
"@babel/helper-plugin-utils": "^7.12.13",
"@babel/helper-hoist-variables": "^7.13.0",
"@babel/helper-module-transforms": "^7.13.0",
"@babel/helper-plugin-utils": "^7.13.0",
"@babel/helper-validator-identifier": "^7.12.11",
"babel-plugin-dynamic-import-node": "^2.3.3"
}
},
"@babel/plugin-transform-modules-umd": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.12.13.tgz",
"integrity": "sha512-BgZndyABRML4z6ibpi7Z98m4EVLFI9tVsZDADC14AElFaNHHBcJIovflJ6wtCqFxwy2YJ1tJhGRsr0yLPKoN+w==",
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.14.0.tgz",
"integrity": "sha512-nPZdnWtXXeY7I87UZr9VlsWme3Y0cfFFE41Wbxz4bbaexAjNMInXPFUpRRUJ8NoMm0Cw+zxbqjdPmLhcjfazMw==",
"dev": true,
"requires": {
"@babel/helper-module-transforms": "^7.12.13",
"@babel/helper-plugin-utils": "^7.12.13"
"@babel/helper-module-transforms": "^7.14.0",
"@babel/helper-plugin-utils": "^7.13.0"
}
},
"@babel/plugin-transform-named-capturing-groups-regex": {
@@ -805,12 +814,12 @@
}
},
"@babel/plugin-transform-parameters": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.12.13.tgz",
"integrity": "sha512-e7QqwZalNiBRHCpJg/P8s/VJeSRYgmtWySs1JwvfwPqhBbiWfOcHDKdeAi6oAyIimoKWBlwc8oTgbZHdhCoVZA==",
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.13.0.tgz",
"integrity": "sha512-Jt8k/h/mIwE2JFEOb3lURoY5C85ETcYPnbuAJ96zRBzh1XHtQZfs62ChZ6EP22QlC8c7Xqr9q+e1SU5qttwwjw==",
"dev": true,
"requires": {
"@babel/helper-plugin-utils": "^7.12.13"
"@babel/helper-plugin-utils": "^7.13.0"
}
},
"@babel/plugin-transform-property-literals": {
@@ -823,12 +832,12 @@
}
},
"@babel/plugin-transform-react-constant-elements": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.12.13.tgz",
"integrity": "sha512-qmzKVTn46Upvtxv8LQoQ8mTCdUC83AOVQIQm57e9oekLT5cmK9GOMOfcWhe8jMNx4UJXn/UDhVZ/7lGofVNeDQ==",
"version": "7.13.13",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.13.13.tgz",
"integrity": "sha512-SNJU53VM/SjQL0bZhyU+f4kJQz7bQQajnrZRSaU21hruG/NWY41AEM9AWXeXX90pYr/C2yAmTgI6yW3LlLrAUQ==",
"dev": true,
"requires": {
"@babel/helper-plugin-utils": "^7.12.13"
"@babel/helper-plugin-utils": "^7.13.0"
}
},
"@babel/plugin-transform-react-display-name": {
@@ -841,25 +850,25 @@
}
},
"@babel/plugin-transform-react-jsx": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.12.13.tgz",
"integrity": "sha512-hhXZMYR8t9RvduN2uW4sjl6MRtUhzNE726JvoJhpjhxKgRUVkZqTsA0xc49ALZxQM7H26pZ/lLvB2Yrea9dllA==",
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.13.12.tgz",
"integrity": "sha512-jcEI2UqIcpCqB5U5DRxIl0tQEProI2gcu+g8VTIqxLO5Iidojb4d77q+fwGseCvd8af/lJ9masp4QWzBXFE2xA==",
"dev": true,
"requires": {
"@babel/helper-annotate-as-pure": "^7.12.13",
"@babel/helper-module-imports": "^7.12.13",
"@babel/helper-plugin-utils": "^7.12.13",
"@babel/helper-module-imports": "^7.13.12",
"@babel/helper-plugin-utils": "^7.13.0",
"@babel/plugin-syntax-jsx": "^7.12.13",
"@babel/types": "^7.12.13"
"@babel/types": "^7.13.12"
}
},
"@babel/plugin-transform-react-jsx-development": {
"version": "7.12.12",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.12.12.tgz",
"integrity": "sha512-i1AxnKxHeMxUaWVXQOSIco4tvVvvCxMSfeBMnMM06mpaJt3g+MpxYQQrDfojUQldP1xxraPSJYSMEljoWM/dCg==",
"version": "7.12.17",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.12.17.tgz",
"integrity": "sha512-BPjYV86SVuOaudFhsJR1zjgxxOhJDt6JHNoD48DxWEIxUCAMjV1ys6DYw4SDYZh0b1QsS2vfIA9t/ZsQGsDOUQ==",
"dev": true,
"requires": {
"@babel/plugin-transform-react-jsx": "^7.12.12"
"@babel/plugin-transform-react-jsx": "^7.12.17"
}
},
"@babel/plugin-transform-react-jsx-self": {
@@ -891,9 +900,9 @@
}
},
"@babel/plugin-transform-regenerator": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.12.13.tgz",
"integrity": "sha512-lxb2ZAvSLyJ2PEe47hoGWPmW22v7CtSl9jW8mingV4H2sEX/JOcrAj2nPuGWi56ERUm2bUpjKzONAuT6HCn2EA==",
"version": "7.13.15",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.13.15.tgz",
"integrity": "sha512-Bk9cOLSz8DiurcMETZ8E2YtIVJbFCPGW28DJWUakmyVWtQSm6Wsf0p4B4BfEr/eL2Nkhe/CICiUiMOCi1TPhuQ==",
"dev": true,
"requires": {
"regenerator-transform": "^0.14.2"
@@ -918,12 +927,12 @@
}
},
"@babel/plugin-transform-spread": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.12.13.tgz",
"integrity": "sha512-dUCrqPIowjqk5pXsx1zPftSq4sT0aCeZVAxhdgs3AMgyaDmoUT0G+5h3Dzja27t76aUEIJWlFgPJqJ/d4dbTtg==",
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.13.0.tgz",
"integrity": "sha512-V6vkiXijjzYeFmQTr3dBxPtZYLPcUfY34DebOU27jIl2M/Y8Egm52Hw82CSjjPqd54GTlJs5x+CR7HeNr24ckg==",
"dev": true,
"requires": {
"@babel/helper-plugin-utils": "^7.12.13",
"@babel/helper-plugin-utils": "^7.13.0",
"@babel/helper-skip-transparent-expression-wrappers": "^7.12.1"
}
},
@@ -937,12 +946,12 @@
}
},
"@babel/plugin-transform-template-literals": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.12.13.tgz",
"integrity": "sha512-arIKlWYUgmNsF28EyfmiQHJLJFlAJNYkuQO10jL46ggjBpeb2re1P9K9YGxNJB45BqTbaslVysXDYm/g3sN/Qg==",
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.13.0.tgz",
"integrity": "sha512-d67umW6nlfmr1iehCcBv69eSUSySk1EsIS8aTDX4Xo9qajAh6mYtcl4kJrBkGXuxZPEgVr7RVfAvNW6YQkd4Mw==",
"dev": true,
"requires": {
"@babel/helper-plugin-utils": "^7.12.13"
"@babel/helper-plugin-utils": "^7.13.0"
}
},
"@babel/plugin-transform-typeof-symbol": {
@@ -1074,17 +1083,17 @@
}
},
"@babel/runtime": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.13.tgz",
"integrity": "sha512-8+3UMPBrjFa/6TtKi/7sehPKqfAm4g6K+YQjyyFOLUTxzOngcRZTlAVY8sc2CORJYqdHQY8gRPHmn+qo15rCBw==",
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.0.tgz",
"integrity": "sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
},
"@babel/runtime-corejs3": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.12.13.tgz",
"integrity": "sha512-8fSpqYRETHATtNitsCXq8QQbKJP31/KnDl2Wz2Vtui9nKzjss2ysuZtyVsWjBtvkeEFo346gkwjYPab1hvrXkQ==",
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.14.0.tgz",
"integrity": "sha512-0R0HTZWHLk6G8jIk0FtoX+AatCtKnswS98VhXwGImFc759PJRp4Tru0PQYZofyijTFUr+gT8Mu7sgXVJLQ0ceg==",
"dev": true,
"requires": {
"core-js-pure": "^3.0.0",
@@ -1103,20 +1112,19 @@
}
},
"@babel/traverse": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.13.tgz",
"integrity": "sha512-3Zb4w7eE/OslI0fTp8c7b286/cQps3+vdLW3UcwC8VSJC6GbKn55aeVVu2QJNuCDoeKyptLOFrPq8WqZZBodyA==",
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.0.tgz",
"integrity": "sha512-dZ/a371EE5XNhTHomvtuLTUyx6UEoJmYX+DT5zBCQN3McHemsuIaKKYqsc/fs26BEkHs/lBZy0J571LP5z9kQA==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.12.13",
"@babel/generator": "^7.12.13",
"@babel/generator": "^7.14.0",
"@babel/helper-function-name": "^7.12.13",
"@babel/helper-split-export-declaration": "^7.12.13",
"@babel/parser": "^7.12.13",
"@babel/types": "^7.12.13",
"@babel/parser": "^7.14.0",
"@babel/types": "^7.14.0",
"debug": "^4.1.0",
"globals": "^11.1.0",
"lodash": "^4.17.19"
"globals": "^11.1.0"
},
"dependencies": {
"debug": {
@@ -1137,13 +1145,12 @@
}
},
"@babel/types": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.13.tgz",
"integrity": "sha512-oKrdZTld2im1z8bDwTOQvUbxKwE+854zc16qWZQlcTqMN00pWxHQ4ZeOq0yDMnisOpRykH2/5Qqcrk/OlbAjiQ==",
"version": "7.14.1",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.1.tgz",
"integrity": "sha512-S13Qe85fzLs3gYRUnrpyeIrBJIMYv33qSTg1qoBwiG6nPKwUWAD9odSzWhEedpwOIzSEI6gbdQIWEMiCI42iBA==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.12.11",
"lodash": "^4.17.19",
"@babel/helper-validator-identifier": "^7.14.0",
"to-fast-properties": "^2.0.0"
}
},
@@ -1164,9 +1171,9 @@
}
},
"@cospired/i18n-iso-languages": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@cospired/i18n-iso-languages/-/i18n-iso-languages-2.1.2.tgz",
"integrity": "sha512-XylKOsWRyQm9sNanZnppRORXTLaL34uThyBQpTFwOGAYvNg9PeYsyTTfLA1FTCh02RV+kiwt/O/y14DR/OqpWg=="
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@cospired/i18n-iso-languages/-/i18n-iso-languages-2.2.0.tgz",
"integrity": "sha512-hywY9u9apWGeLxQuRcXw7IW0XkMdXum/hr3TpmHY2fAbXMTFlhhkPCdsQeHzjxMQwTnMgXaZ4j4WOCwKtlDRCQ=="
},
"@edx/brand": {
"version": "npm:@edx/brand-openedx@1.1.0",
@@ -1246,9 +1253,9 @@
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
"integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
@@ -1322,60 +1329,47 @@
}
},
"@edx/frontend-component-footer": {
"version": "10.1.4",
"resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-10.1.4.tgz",
"integrity": "sha512-oRgK9IBg9J4LGYGLkoeRO5X5G9MeSt37uSvZ998BBEIp9fVx8gBMwd6COEJVN8sgZqO/ckFbBOlZXA/7iDWOEw==",
"version": "10.1.5",
"resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-10.1.5.tgz",
"integrity": "sha512-QoQCiR7AYKBD2jx4zQQa+XVPbv7aNC0b1FdFz9GMEcswp8kpsZsupfb76Pp3Z2hiDURj63LpwhUyv1B0zqwVAA==",
"requires": {
"@fortawesome/fontawesome-svg-core": "1.2.34",
"@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/fontawesome-svg-core": "1.2.35",
"@fortawesome/free-brands-svg-icons": "5.15.3",
"@fortawesome/free-regular-svg-icons": "5.15.3",
"@fortawesome/free-solid-svg-icons": "5.15.3",
"@fortawesome/react-fontawesome": "0.1.14"
},
"dependencies": {
"@fortawesome/fontawesome-common-types": {
"version": "0.2.34",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.34.tgz",
"integrity": "sha512-XcIn3iYbTEzGIxD0/dY5+4f019jIcEIWBiHc3KrmK/ROahwxmZ/s+tdj97p/5K0klz4zZUiMfUlYP0ajhSJjmA=="
},
"@fortawesome/fontawesome-svg-core": {
"version": "1.2.34",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.34.tgz",
"integrity": "sha512-0KNN0nc5eIzaJxlv43QcDmTkDY1CqeN6J7OCGSs+fwGPdtv0yOQqRjieopBCmw+yd7uD3N2HeNL3Zm5isDleLg==",
"version": "1.2.35",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.35.tgz",
"integrity": "sha512-uLEXifXIL7hnh2sNZQrIJWNol7cTVIzwI+4qcBIq9QWaZqUblm0IDrtSqbNg+3SQf8SMGHkiSigD++rHmCHjBg==",
"requires": {
"@fortawesome/fontawesome-common-types": "^0.2.34"
"@fortawesome/fontawesome-common-types": "^0.2.35"
}
},
"@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",
"integrity": "sha512-nhEWctDOP6f+Ka10LXAFoF+6mtWidC2iQgTBGRGgydmhBtcIEwyxWVx5wQHa86A1zAMi5TnipDAYQs2qn7DD6A==",
"version": "5.15.3",
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-5.15.3.tgz",
"integrity": "sha512-1hirPcbjj72ZJtFvdnXGPbAbpn3Ox6mH3g5STbANFp3vGSiE5u5ingAKV06mK6ZVqNYxUPlh4DlTnaIvLtF2kw==",
"requires": {
"@fortawesome/fontawesome-common-types": "^0.2.18"
"@fortawesome/fontawesome-common-types": "^0.2.35"
}
},
"@fortawesome/free-regular-svg-icons": {
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.8.2.tgz",
"integrity": "sha512-499WODlsDcXA9hM+Y3ZqoWj1kKhVLCHS8PnJs3zEaoPr5W6yrE9Jnp3C9Hb9KpwnRMh3IRXZ3XqxyUaQFMMRFw==",
"version": "5.15.3",
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.15.3.tgz",
"integrity": "sha512-q4/p8Xehy9qiVTdDWHL4Z+o5PCLRChePGZRTXkl+/Z7erDVL8VcZUuqzJjs6gUz6czss4VIPBRdCz6wP37/zMQ==",
"requires": {
"@fortawesome/fontawesome-common-types": "^0.2.18"
"@fortawesome/fontawesome-common-types": "^0.2.35"
}
},
"@fortawesome/free-solid-svg-icons": {
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.8.2.tgz",
"integrity": "sha512-5tF6WOFlqqO95zY5ukSB6jliDa3jnk1p5L4K/a58ccDFsbjSkhfGuvZkRkeWxH8uMms81pZd6yQTwQrkedeJmg==",
"version": "5.15.3",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.3.tgz",
"integrity": "sha512-XPeeu1IlGYqz4VWGRAT5ukNMd4VHUEEJ7ysZ7pSSgaEtNvSo+FLurybGJVmiqkQdK50OkSja2bfZXOeyMGRD8Q==",
"requires": {
"@fortawesome/fontawesome-common-types": "^0.2.18"
}
},
"@fortawesome/react-fontawesome": {
"version": "0.1.14",
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.14.tgz",
"integrity": "sha512-4wqNb0gRLVaBm/h+lGe8UfPPivcbuJ6ecI4hIgW0LjI7kzpYB9FkN0L9apbVzg+lsBdcTf0AlBtODjcSX5mmKA==",
"requires": {
"prop-types": "^15.7.2"
"@fortawesome/fontawesome-common-types": "^0.2.35"
}
}
}
@@ -1388,16 +1382,56 @@
"query-string": "^6.13.2"
}
},
"@edx/frontend-platform": {
"version": "1.8.4",
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-1.8.4.tgz",
"integrity": "sha512-Xjl5VYFK+OrOY8TaMMGfDK45+dkxr0ai/6SsNDt3KCLTqIqsNLY19mPGsHHpZD8B3H5unQuWTn6VoRZ7q+Ovxg==",
"@edx/frontend-lib-special-exams": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/@edx/frontend-lib-special-exams/-/frontend-lib-special-exams-1.7.1.tgz",
"integrity": "sha512-noR1BBBlk9p33nMJ4zhOPK7aOmeAgWnb7GsSWB/JeipnOea/5gsu8UWeuZW6ClUA2/NR7jUwGi5j1HzU05sjKQ==",
"requires": {
"@cospired/i18n-iso-languages": "2.1.2",
"@fortawesome/fontawesome-svg-core": "1.2.34",
"@fortawesome/free-brands-svg-icons": "5.11.2",
"@fortawesome/free-regular-svg-icons": "5.11.2",
"@fortawesome/free-solid-svg-icons": "5.11.2",
"@fortawesome/react-fontawesome": "0.1.14",
"babel-polyfill": "6.26.0",
"eventemitter3": "^4.0.7"
},
"dependencies": {
"@fortawesome/free-brands-svg-icons": {
"version": "5.11.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-5.11.2.tgz",
"integrity": "sha512-wKK5znpHiZ2S0VgOvbeAnYuzkk3H86rxWajD9PVpfBj3s/kySEWTFKh/uLPyxiTOx8Tsd0OGN4En/s9XudVHLQ==",
"requires": {
"@fortawesome/fontawesome-common-types": "^0.2.25"
}
},
"@fortawesome/free-regular-svg-icons": {
"version": "5.11.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.11.2.tgz",
"integrity": "sha512-k0vbThRv9AvnXYBWi1gn1rFW4X7co/aFkbm0ZNmAR5PoWb9vY9EDDDobg8Ay4ISaXtCPypvJ0W1FWkSpLQwZ6w==",
"requires": {
"@fortawesome/fontawesome-common-types": "^0.2.25"
}
},
"@fortawesome/free-solid-svg-icons": {
"version": "5.11.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.11.2.tgz",
"integrity": "sha512-zBue4i0PAZJUXOmLBBvM7L0O7wmsDC8dFv9IhpW5QL4kT9xhhVUsYg/LX1+5KaukWq4/cbDcKT+RT1aRe543sg==",
"requires": {
"@fortawesome/fontawesome-common-types": "^0.2.25"
}
}
}
},
"@edx/frontend-platform": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-1.11.0.tgz",
"integrity": "sha512-XtqKPWUvXzPJLlIEsoLMuac3TvTtAe1GY9MKu1QQsZDget1plDZqaf3ByRbuxOW8b2g2JVPvFx+Jm3FxTcrmIQ==",
"requires": {
"@cospired/i18n-iso-languages": "2.2.0",
"axios": "0.21.1",
"axios-cache-adapter": "^2.5.0",
"form-urlencoded": "4.1.4",
"glob": "7.1.6",
"glob": "7.1.7",
"history": "4.10.1",
"i18n-iso-countries": "4.3.1",
"jwt-decode": "2.2.0",
@@ -1407,7 +1441,7 @@
"lodash.memoize": "4.1.2",
"lodash.merge": "4.6.2",
"lodash.snakecase": "4.1.1",
"pubsub-js": "1.7.0",
"pubsub-js": "1.9.3",
"react-intl": "2.9.0",
"universal-cookie": "4.0.4"
}
@@ -1440,11 +1474,6 @@
"uncontrollable": "7.2.1"
},
"dependencies": {
"@fortawesome/fontawesome-common-types": {
"version": "0.2.35",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.35.tgz",
"integrity": "sha512-IHUfxSEDS9dDGqYwIW7wTN6tn/O8E0n5PcAHz9cAaBoZw6UpG20IG/YM3NNLaGPwPqgjBAFjIURzqoQs3rrtuw=="
},
"@fortawesome/free-solid-svg-icons": {
"version": "5.15.3",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.3.tgz",
@@ -1465,9 +1494,9 @@
},
"dependencies": {
"tslib": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz",
"integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==",
"dev": true
}
}
@@ -1492,9 +1521,9 @@
}
},
"tslib": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz",
"integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==",
"dev": true
}
}
@@ -1521,17 +1550,17 @@
}
},
"tslib": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz",
"integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==",
"dev": true
}
}
},
"@fortawesome/fontawesome-common-types": {
"version": "0.2.34",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.34.tgz",
"integrity": "sha512-XcIn3iYbTEzGIxD0/dY5+4f019jIcEIWBiHc3KrmK/ROahwxmZ/s+tdj97p/5K0klz4zZUiMfUlYP0ajhSJjmA=="
"version": "0.2.35",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.35.tgz",
"integrity": "sha512-IHUfxSEDS9dDGqYwIW7wTN6tn/O8E0n5PcAHz9cAaBoZw6UpG20IG/YM3NNLaGPwPqgjBAFjIURzqoQs3rrtuw=="
},
"@fortawesome/fontawesome-svg-core": {
"version": "1.2.34",
@@ -1601,9 +1630,9 @@
}
},
"@istanbuljs/schema": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz",
"integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==",
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
"integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
"dev": true
},
"@jest/console": {
@@ -1630,9 +1659,9 @@
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
"integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
@@ -1732,9 +1761,9 @@
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
"integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
@@ -1778,13 +1807,13 @@
"dev": true
},
"micromatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz",
"integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
"integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
"dev": true,
"requires": {
"braces": "^3.0.1",
"picomatch": "^2.0.5"
"picomatch": "^2.2.3"
}
},
"rimraf": {
@@ -1911,9 +1940,9 @@
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
"integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
@@ -2036,9 +2065,9 @@
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
"integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
@@ -2082,13 +2111,13 @@
"dev": true
},
"micromatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz",
"integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
"integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
"dev": true,
"requires": {
"braces": "^3.0.1",
"picomatch": "^2.0.5"
"picomatch": "^2.2.3"
}
},
"slash": {
@@ -2140,9 +2169,9 @@
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
"integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
@@ -2257,9 +2286,9 @@
}
},
"y18n": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz",
"integrity": "sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg==",
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true
},
"yargs": {
@@ -2278,9 +2307,9 @@
}
},
"yargs-parser": {
"version": "20.2.4",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz",
"integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==",
"version": "20.2.7",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz",
"integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==",
"dev": true
}
}
@@ -2349,9 +2378,9 @@
"optional": true
},
"@sinonjs/commons": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.2.tgz",
"integrity": "sha512-sruwd86RJHdsVf/AtBoijDmUqJp3B6hF/DGC23C+JaegnDHaZyewCjoVGTdg3J0uz3Zs7NnIT05OBOmML72lQw==",
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz",
"integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==",
"dev": true,
"requires": {
"type-detect": "4.0.8"
@@ -2463,25 +2492,25 @@
},
"dependencies": {
"@babel/core": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.13.tgz",
"integrity": "sha512-BQKE9kXkPlXHPeqissfxo0lySWJcYdEP0hdtJOH/iJfDdhOCcgtNCjftCJg3qqauB4h+lz2N6ixM++b9DN1Tcw==",
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.14.0.tgz",
"integrity": "sha512-8YqpRig5NmIHlMLw09zMlPTvUVMILjqCOtVgu+TVNWEBvy9b5I3RRyhqnrV4hjgEK7n8P9OqvkWJAFmEL6Wwfw==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.12.13",
"@babel/generator": "^7.12.13",
"@babel/helper-module-transforms": "^7.12.13",
"@babel/helpers": "^7.12.13",
"@babel/parser": "^7.12.13",
"@babel/generator": "^7.14.0",
"@babel/helper-compilation-targets": "^7.13.16",
"@babel/helper-module-transforms": "^7.14.0",
"@babel/helpers": "^7.14.0",
"@babel/parser": "^7.14.0",
"@babel/template": "^7.12.13",
"@babel/traverse": "^7.12.13",
"@babel/types": "^7.12.13",
"@babel/traverse": "^7.14.0",
"@babel/types": "^7.14.0",
"convert-source-map": "^1.7.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.1",
"gensync": "^1.0.0-beta.2",
"json5": "^2.1.2",
"lodash": "^4.17.19",
"semver": "^5.4.1",
"semver": "^6.3.0",
"source-map": "^0.5.0"
}
},
@@ -2500,6 +2529,12 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
},
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@@ -2837,9 +2872,9 @@
"dev": true
},
"@types/babel__core": {
"version": "7.1.12",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.12.tgz",
"integrity": "sha512-wMTHiiTiBAAPebqaPiPDLFA4LYPKr6Ph0Xq/6rq1Ur3v66HXyG+clfR9CNETkD7MQS8ZHvpQOtA53DLws5WAEQ==",
"version": "7.1.14",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.14.tgz",
"integrity": "sha512-zGZJzzBUVDo/eV6KgbE0f0ZI7dInEYvo12Rb70uNQDshC3SkRMb67ja0GgRHZgAX3Za6rhaWlvbDO8rrGyAb1g==",
"dev": true,
"requires": {
"@babel/parser": "^7.1.0",
@@ -2869,18 +2904,18 @@
}
},
"@types/babel__traverse": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.11.0.tgz",
"integrity": "sha512-kSjgDMZONiIfSH1Nxcr5JIRMwUetDki63FSQfpTCz8ogF3Ulqm8+mr5f78dUYs6vMiB6gBusQqfQmBvHZj/lwg==",
"version": "7.11.1",
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.11.1.tgz",
"integrity": "sha512-Vs0hm0vPahPMYi9tDjtP66llufgO3ST16WXaSTtDGEl9cewAl3AibmxWw6TINOqHPT9z0uABKAYjT9jNSg4npw==",
"dev": true,
"requires": {
"@babel/types": "^7.3.0"
}
},
"@types/cheerio": {
"version": "0.22.23",
"resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.23.tgz",
"integrity": "sha512-QfHLujVMlGqcS/ePSf3Oe5hK3H8wi/yN2JYuxSB1U10VvW1fO3K8C+mURQesFYS1Hn7lspOsTT75SKq/XtydQg==",
"version": "0.22.28",
"resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.28.tgz",
"integrity": "sha512-ehUMGSW5IeDxJjbru4awKYMlKGmo1wSSGUVqXtYwlgmUM8X1a0PZttEIm6yEY7vHsY/hh6iPnklF213G0UColw==",
"requires": {
"@types/node": "*"
}
@@ -2899,9 +2934,9 @@
"integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow=="
},
"@types/fs-extra": {
"version": "9.0.6",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.6.tgz",
"integrity": "sha512-ecNRHw4clCkowNOBJH1e77nvbPxHYnWIXMv1IAoG/9+MYGkgoyr3Ppxr7XYFNL41V422EDhyV4/4SSK8L2mlig==",
"version": "9.0.11",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.11.tgz",
"integrity": "sha512-mZsifGG4QeQ7hlkhO56u7zt/ycBgGxSVsFI/6lGTU34VtwkiqrrSDgw0+ygs8kFGWcXnFQWMrzF2h7TtDFNixA==",
"dev": true,
"requires": {
"@types/node": "*"
@@ -2918,14 +2953,23 @@
}
},
"@types/graceful-fs": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.4.tgz",
"integrity": "sha512-mWA/4zFQhfvOA8zWkXobwJvBD7vzcxgrOQ0J5CH1votGqdq9m7+FwtGaqyCZqC3NyyBkc9z4m+iry4LlqcMWJg==",
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
"integrity": "sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/hoist-non-react-statics": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
"requires": {
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0"
}
},
"@types/html-minifier-terser": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz",
@@ -2962,9 +3006,9 @@
}
},
"@types/jest": {
"version": "26.0.20",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.20.tgz",
"integrity": "sha512-9zi2Y+5USJRxd0FsahERhBwlcvFh6D2GLQnY2FH2BzK8J9s9omvNHIbvABwIluXa0fD8XVKMLTO0aOEuUfACAA==",
"version": "26.0.23",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.23.tgz",
"integrity": "sha512-ZHLmWMJ9jJ9PTiT58juykZpL7KjwJywFN3Rr2pTSkyQfydf/rk22yS7W8p5DaVUMQ2BQC7oYiU3FjbTM/mYrOA==",
"dev": true,
"requires": {
"jest-diff": "^26.0.0",
@@ -2984,15 +3028,15 @@
"dev": true
},
"@types/minimatch": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
"integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA==",
"dev": true
},
"@types/node": {
"version": "14.14.25",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.25.tgz",
"integrity": "sha512-EPpXLOVqDvisVxtlbvzfyqSsFeQxltFbluZNRndIb8tr9KiBnYNLzrc1N3pyKUCww2RNrfHDViqDWWE1LCJQtQ=="
"version": "15.0.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-15.0.2.tgz",
"integrity": "sha512-p68+a+KoxpoB47015IeYZYRrdqMUcpbK8re/zpFB8Ld46LHC1lPEbp3EXgkEhAYEcPvjJF6ZO+869SQ0aH1dcA=="
},
"@types/normalize-package-data": {
"version": "2.4.0",
@@ -3007,9 +3051,9 @@
"dev": true
},
"@types/prettier": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.2.0.tgz",
"integrity": "sha512-O3SQC6+6AySHwrspYn2UvC6tjo6jCTMMmylxZUFhE1CulVu5l3AxU6ca9lrJDTQDVllF62LIxVSx5fuYL6LiZg==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.2.3.tgz",
"integrity": "sha512-PijRCG/K3s3w1We6ynUKdxEc5AcuuH3NBmMDP8uvKVp6X43UY7NQlTzczakXP3DJR0F4dfNQIGjU2cUeRYs2AA==",
"dev": true
},
"@types/prop-types": {
@@ -3024,15 +3068,26 @@
"dev": true
},
"@types/react": {
"version": "17.0.4",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.4.tgz",
"integrity": "sha512-onz2BqScSFMoTRdJUZUDD/7xrusM8hBA2Fktk2qgaTYPCgPvWnDEgkrOs8hhPUf2jfcIXkJ5yK6VfYormJS3Jw==",
"version": "17.0.5",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.5.tgz",
"integrity": "sha512-bj4biDB9ZJmGAYTWSKJly6bMr4BLUiBrx9ujiJEoP9XIDY9CTaPGxE5QWN/1WjpPLzYF7/jRNnV2nNxNe970sw==",
"requires": {
"@types/prop-types": "*",
"@types/scheduler": "*",
"csstype": "^3.0.2"
}
},
"@types/react-redux": {
"version": "7.1.16",
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.16.tgz",
"integrity": "sha512-f/FKzIrZwZk7YEO9E1yoxIuDNRiDducxkFlkw/GNMGEnK9n4K8wJzlJBghpSuOVDgEUHoDkDF7Gi9lHNQR4siw==",
"requires": {
"@types/hoist-non-react-statics": "^3.3.0",
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0",
"redux": "^4.0.0"
}
},
"@types/react-transition-group": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.1.tgz",
@@ -3068,9 +3123,9 @@
"dev": true
},
"@types/tapable": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.6.tgz",
"integrity": "sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA==",
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.7.tgz",
"integrity": "sha512-0VBprVqfgFD7Ehb2vd8Lh9TG3jP98gvr8rgehQqzztZNI7o8zS8Ad4jyZneKELphpuE212D8J70LnSNQSyO6bQ==",
"dev": true
},
"@types/testing-library__jest-dom": {
@@ -3083,9 +3138,9 @@
}
},
"@types/uglify-js": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.12.0.tgz",
"integrity": "sha512-sYAF+CF9XZ5cvEBkI7RtrG9g2GtMBkviTnBxYYyq+8BWvO4QtXfwwR6a2LFwCi4evMKZfpv6U43ViYvv17Wz3Q==",
"version": "3.13.0",
"resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.13.0.tgz",
"integrity": "sha512-EGkrJD5Uy+Pg0NUR8uA4bJ5WMfljyad0G+784vLCNUkD+QwOJXUbBYExXfVGf7YtyzdQp3L/XMYcliB987kL5Q==",
"dev": true,
"requires": {
"source-map": "^0.6.1"
@@ -3097,14 +3152,14 @@
"integrity": "sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI="
},
"@types/webpack": {
"version": "4.41.26",
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.26.tgz",
"integrity": "sha512-7ZyTfxjCRwexh+EJFwRUM+CDB2XvgHl4vfuqf1ZKrgGvcS5BrNvPQqJh3tsZ0P6h6Aa1qClVHaJZszLPzpqHeA==",
"version": "4.41.28",
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.28.tgz",
"integrity": "sha512-Nn84RAiJjKRfPFFCVR8LC4ueTtTdfWAMZ03THIzZWRJB+rX24BD3LqPSFnbMscWauEsT4segAsylPDIaZyZyLQ==",
"dev": true,
"requires": {
"@types/anymatch": "*",
"@types/node": "*",
"@types/tapable": "*",
"@types/tapable": "^1",
"@types/uglify-js": "*",
"@types/webpack-sources": "*",
"source-map": "^0.6.0"
@@ -3513,18 +3568,18 @@
"dev": true
},
"ansi-escapes": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz",
"integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==",
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
"integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
"dev": true,
"requires": {
"type-fest": "^0.11.0"
"type-fest": "^0.21.3"
},
"dependencies": {
"type-fest": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz",
"integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==",
"version": "0.21.3",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
"integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
"dev": true
}
}
@@ -3666,9 +3721,9 @@
"dev": true
},
"aria-hidden": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.1.2.tgz",
"integrity": "sha512-WAMH9q3vRimVqP+B0q2eDvx7IPDoY17A2fWwj5atTA/zTYJCNcS6HJ5YErZ5FO3PUHhrV0y0yR1NA0dRNm913A==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.1.3.tgz",
"integrity": "sha512-RhVWFtKH5BiGMycI72q2RAFMLQi8JP9bLuQXgR5a8Znp7P5KOIADSJeyfI8PCVxLEp067B2HbP5JIiI/PXIZeA==",
"requires": {
"tslib": "^1.0.0"
}
@@ -3735,40 +3790,16 @@
"dev": true
},
"array-includes": {
"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==",
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.3.tgz",
"integrity": "sha512-gcem1KlBU7c9rB+Rq8/3PPKsK2kjqeEBa3bD5kkQo4nYlOHQCJqIJFqBXDEfwaRuYTT4E+FxA9xez7Gf/e3Q7A==",
"dev": true,
"requires": {
"call-bind": "^1.0.0",
"call-bind": "^1.0.2",
"define-properties": "^1.1.3",
"es-abstract": "^1.18.0-next.1",
"get-intrinsic": "^1.0.1",
"es-abstract": "^1.18.0-next.2",
"get-intrinsic": "^1.1.1",
"is-string": "^1.0.5"
},
"dependencies": {
"es-abstract": {
"version": "1.18.0-next.2",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.2.tgz",
"integrity": "sha512-Ih4ZMFHEtZupnUh6497zEL4y2+w8+1ljnCyaTa+adcoafI1GOvMwFlDjBLfWR7y9VLfrjRJe9ocuHY1PSR9jjw==",
"dev": true,
"requires": {
"call-bind": "^1.0.2",
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1",
"get-intrinsic": "^1.0.2",
"has": "^1.0.3",
"has-symbols": "^1.0.1",
"is-callable": "^1.2.2",
"is-negative-zero": "^2.0.1",
"is-regex": "^1.1.1",
"object-inspect": "^1.9.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.2",
"string.prototype.trimend": "^1.0.3",
"string.prototype.trimstart": "^1.0.3"
}
}
}
},
"array-union": {
@@ -3810,30 +3841,6 @@
"call-bind": "^1.0.0",
"define-properties": "^1.1.3",
"es-abstract": "^1.18.0-next.1"
},
"dependencies": {
"es-abstract": {
"version": "1.18.0-next.2",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.2.tgz",
"integrity": "sha512-Ih4ZMFHEtZupnUh6497zEL4y2+w8+1ljnCyaTa+adcoafI1GOvMwFlDjBLfWR7y9VLfrjRJe9ocuHY1PSR9jjw==",
"dev": true,
"requires": {
"call-bind": "^1.0.2",
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1",
"get-intrinsic": "^1.0.2",
"has": "^1.0.3",
"has-symbols": "^1.0.1",
"is-callable": "^1.2.2",
"is-negative-zero": "^2.0.1",
"is-regex": "^1.1.1",
"object-inspect": "^1.9.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.2",
"string.prototype.trimend": "^1.0.3",
"string.prototype.trimstart": "^1.0.3"
}
}
}
},
"arrify": {
@@ -3864,9 +3871,9 @@
},
"dependencies": {
"bn.js": {
"version": "4.11.9",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
"dev": true
}
}
@@ -3984,9 +3991,9 @@
}
},
"axios-cache-adapter": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/axios-cache-adapter/-/axios-cache-adapter-2.7.0.tgz",
"integrity": "sha512-itggIo9i8tnsBFnniNh8+7RxXfdCKZT+cEvyjzBdU8IIyudpj4WyrY7288KE8MICs6+u7YFRVlLZi3vvXufz8w==",
"version": "2.7.3",
"resolved": "https://registry.npmjs.org/axios-cache-adapter/-/axios-cache-adapter-2.7.3.tgz",
"integrity": "sha512-A+ZKJ9lhpjthOEp4Z3QR/a9xC4du1ALaAsejgRGrH9ef6kSDxdFrhRpulqsh9khsEnwXxGfgpUuDp1YXMNMEiQ==",
"requires": {
"cache-control-esm": "1.0.0",
"md5": "^2.2.1"
@@ -4115,9 +4122,9 @@
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
"integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
@@ -4250,12 +4257,12 @@
},
"dependencies": {
"intl-messageformat-parser": {
"version": "5.4.2",
"resolved": "https://registry.npmjs.org/intl-messageformat-parser/-/intl-messageformat-parser-5.4.2.tgz",
"integrity": "sha512-VHu6UWgWLJykaSeI1M2DpZMVRLuGCOV91i5I81xnJuAI0MKHP7ZJ3my5naOQkzG10ris3hBr+o5RElF1wQ5IXA==",
"version": "5.5.1",
"resolved": "https://registry.npmjs.org/intl-messageformat-parser/-/intl-messageformat-parser-5.5.1.tgz",
"integrity": "sha512-TvB3LqF2VtP6yI6HXlRT5TxX98HKha6hCcrg9dwlPwNaedVNuQA9KgBdtWKgiyakyCTYHQ+KJeFEstNKfZr64w==",
"dev": true,
"requires": {
"@formatjs/intl-numberformat": "^5.5.0"
"@formatjs/intl-numberformat": "^5.5.2"
}
}
}
@@ -4274,7 +4281,6 @@
"version": "6.26.0",
"resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz",
"integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=",
"dev": true,
"requires": {
"babel-runtime": "^6.26.0",
"core-js": "^2.5.0",
@@ -4284,14 +4290,12 @@
"core-js": {
"version": "2.6.12",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz",
"integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==",
"dev": true
"integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ=="
},
"regenerator-runtime": {
"version": "0.10.5",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz",
"integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=",
"dev": true
"integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg="
}
}
},
@@ -4347,9 +4351,9 @@
}
},
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"base": {
"version": "0.11.2",
@@ -4813,9 +4817,9 @@
"dev": true
},
"bn.js": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.3.tgz",
"integrity": "sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ==",
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz",
"integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==",
"dev": true
},
"body-parser": {
@@ -5022,16 +5026,16 @@
}
},
"browserslist": {
"version": "4.16.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.3.tgz",
"integrity": "sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw==",
"version": "4.16.6",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz",
"integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==",
"dev": true,
"requires": {
"caniuse-lite": "^1.0.30001181",
"colorette": "^1.2.1",
"electron-to-chromium": "^1.3.649",
"caniuse-lite": "^1.0.30001219",
"colorette": "^1.2.2",
"electron-to-chromium": "^1.3.723",
"escalade": "^3.1.1",
"node-releases": "^1.1.70"
"node-releases": "^1.1.71"
}
},
"bser": {
@@ -5316,9 +5320,9 @@
},
"dependencies": {
"tslib": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz",
"integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==",
"dev": true
}
}
@@ -5362,9 +5366,9 @@
}
},
"caniuse-lite": {
"version": "1.0.30001185",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001185.tgz",
"integrity": "sha512-Fpi4kVNtNvJ15H0F6vwmXtb3tukv3Zg3qhKkOGUq7KJ1J6b9kf4dnNgtEAFXhRsJo0gNj9W60+wBvn0JcTvdTg==",
"version": "1.0.30001228",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001228.tgz",
"integrity": "sha512-QQmLOGJ3DEgokHbMSA8cj2a+geXqmnpyOFT0lhQV6P3/YOJvGDEwoedcwxEQ30gJIwIIunHIicunJ2rzK5gB2A==",
"dev": true
},
"caporal": {
@@ -5564,13 +5568,10 @@
"dev": true
},
"chrome-trace-event": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz",
"integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==",
"dev": true,
"requires": {
"tslib": "^1.9.0"
}
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz",
"integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==",
"dev": true
},
"ci-info": {
"version": "2.0.0",
@@ -5774,28 +5775,16 @@
"dev": true
},
"codecov": {
"version": "3.8.1",
"resolved": "https://registry.npmjs.org/codecov/-/codecov-3.8.1.tgz",
"integrity": "sha512-Qm7ltx1pzLPsliZY81jyaQ80dcNR4/JpcX0IHCIWrHBXgseySqbdbYfkdiXd7o/xmzQpGRVCKGYeTrHUpn6Dcw==",
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/codecov/-/codecov-3.8.2.tgz",
"integrity": "sha512-6w/kt/xvmPsWMfDFPE/T054txA9RTgcJEw36PNa6MYX+YV29jCHCRFXwbQ3QZBTOgnex1J2WP8bo2AT8TWWz9g==",
"dev": true,
"requires": {
"argv": "0.0.2",
"ignore-walk": "3.0.3",
"js-yaml": "3.14.0",
"teeny-request": "6.0.1",
"js-yaml": "3.14.1",
"teeny-request": "7.0.1",
"urlgrey": "0.4.4"
},
"dependencies": {
"js-yaml": {
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz",
"integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==",
"dev": true,
"requires": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
}
}
}
},
"collect-v8-coverage": {
@@ -5838,9 +5827,9 @@
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
},
"color-string": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.4.tgz",
"integrity": "sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw==",
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.5.tgz",
"integrity": "sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==",
"dev": true,
"requires": {
"color-name": "^1.0.0",
@@ -5848,9 +5837,9 @@
}
},
"colorette": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz",
"integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
"integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==",
"dev": true
},
"colors": {
@@ -6119,12 +6108,12 @@
"integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA=="
},
"core-js-compat": {
"version": "3.8.3",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.8.3.tgz",
"integrity": "sha512-1sCb0wBXnBIL16pfFG1Gkvei6UzvKyTNYpiC41yrdjEv0UoJoq9E/abTMzyYJ6JpTkAj15dLjbqifIzEBDVvog==",
"version": "3.12.1",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.12.1.tgz",
"integrity": "sha512-i6h5qODpw6EsHAoIdQhKoZdWn+dGBF3dSS8m5tif36RlWvW3A6+yu2S16QHUo3CrkzrnEskMAt9f8FxmY9fhWQ==",
"dev": true,
"requires": {
"browserslist": "^4.16.1",
"browserslist": "^4.16.6",
"semver": "7.0.0"
},
"dependencies": {
@@ -6137,9 +6126,9 @@
}
},
"core-js-pure": {
"version": "3.8.3",
"resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.8.3.tgz",
"integrity": "sha512-V5qQZVAr9K0xu7jXg1M7qTEwuxUgqr7dUOezGaNa7i+Xn9oXAU/d1fzqD9ObuwpVQOaorO5s70ckyi1woP9lVA==",
"version": "3.12.1",
"resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.12.1.tgz",
"integrity": "sha512-1cch+qads4JnDSWsvc7d6nzlKAippwjUlf6vykkTLW53VSV+NkE6muGBToAjEA8pG90cSfcud3JgVmW2ds5TaQ==",
"dev": true
},
"core-util-is": {
@@ -6172,9 +6161,9 @@
},
"dependencies": {
"bn.js": {
"version": "4.11.9",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
"dev": true
}
}
@@ -6352,9 +6341,9 @@
},
"dependencies": {
"domelementtype": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.1.0.tgz",
"integrity": "sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w=="
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz",
"integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A=="
}
}
},
@@ -6460,9 +6449,9 @@
}
},
"cssnano-preset-default": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz",
"integrity": "sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA==",
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.8.tgz",
"integrity": "sha512-LdAyHuq+VRyeVREFmuxUZR1TXjQm8QQU/ktoo/x7bz+SdOge1YKc5eMN6pRW7YWBmyq59CqYba1dJ5cUukEjLQ==",
"dev": true,
"requires": {
"css-declaration-sorter": "^4.0.1",
@@ -6493,7 +6482,7 @@
"postcss-ordered-values": "^4.1.2",
"postcss-reduce-initial": "^4.0.3",
"postcss-reduce-transforms": "^4.0.2",
"postcss-svgo": "^4.0.2",
"postcss-svgo": "^4.0.3",
"postcss-unique-selectors": "^4.0.1"
}
},
@@ -6534,9 +6523,9 @@
},
"dependencies": {
"css-tree": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.2.tgz",
"integrity": "sha512-wCoWush5Aeo48GLhfHPbmvZs59Z+M7k5+B1xDnXbdWNcEF423DoFdqSWE0PM5aNk5nI5cp1q7ms36zGApY/sKQ==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
"integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
"dev": true,
"requires": {
"mdn-data": "2.0.14",
@@ -6624,9 +6613,9 @@
}
},
"damerau-levenshtein": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz",
"integrity": "sha512-JVrozIeElnj3QzfUIt8tB8YMluBJom4Vw9qTPpjGYQ9fYlB3D/rb6OordUxf3xeFB35LKWs0xqcO5U6ySvBtug==",
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.7.tgz",
"integrity": "sha512-VvdQIPGdWP0SqFXghj79Wf/5LArmreyMsGLa6FG6iC4t3j7j5s71TrwWmT/4akbDQIqjfACkLZmjXhA7g2oUZw==",
"dev": true
},
"dashdash": {
@@ -6981,9 +6970,9 @@
"dev": true
},
"detect-node": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz",
"integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==",
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.5.tgz",
"integrity": "sha512-qi86tE6hRcFHy8jI1m2VG+LaPUR1LhqDa5G8tVjuUXmOrpuAgqsA1pN0+ldgr3aKUH+QLI9hCY/OcRYisERejw==",
"dev": true
},
"detect-node-es": {
@@ -7024,9 +7013,9 @@
},
"dependencies": {
"bn.js": {
"version": "4.11.9",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
"dev": true
}
}
@@ -7185,9 +7174,9 @@
},
"dependencies": {
"tslib": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz",
"integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==",
"dev": true
}
}
@@ -7364,9 +7353,9 @@
"dev": true
},
"electron-to-chromium": {
"version": "1.3.657",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.657.tgz",
"integrity": "sha512-/9ROOyvEflEbaZFUeGofD+Tqs/WynbSTbNgNF+/TJJxH1ePD/e6VjZlDJpW3FFFd3nj5l3Hd8ki2vRwy+gyRFw==",
"version": "1.3.727",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.727.tgz",
"integrity": "sha512-Mfz4FIB4FSvEwBpDfdipRIrwd6uo8gUDoRDF4QEYb4h4tSuI3ov594OrjU6on042UlFHouIJpClDODGkPcBSbg==",
"dev": true
},
"elliptic": {
@@ -7385,9 +7374,9 @@
},
"dependencies": {
"bn.js": {
"version": "4.11.9",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
"dev": true
}
}
@@ -7521,21 +7510,26 @@
}
},
"es-abstract": {
"version": "1.17.7",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
"integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0.tgz",
"integrity": "sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw==",
"requires": {
"call-bind": "^1.0.2",
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1",
"get-intrinsic": "^1.1.1",
"has": "^1.0.3",
"has-symbols": "^1.0.1",
"is-callable": "^1.2.2",
"is-regex": "^1.1.1",
"object-inspect": "^1.8.0",
"has-symbols": "^1.0.2",
"is-callable": "^1.2.3",
"is-negative-zero": "^2.0.1",
"is-regex": "^1.1.2",
"is-string": "^1.0.5",
"object-inspect": "^1.9.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1"
"object.assign": "^4.1.2",
"string.prototype.trimend": "^1.0.4",
"string.prototype.trimstart": "^1.0.4",
"unbox-primitive": "^1.0.0"
}
},
"es-check": {
@@ -7617,16 +7611,24 @@
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
},
"escodegen": {
"version": "1.14.3",
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz",
"integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz",
"integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==",
"dev": true,
"requires": {
"esprima": "^4.0.1",
"estraverse": "^4.2.0",
"estraverse": "^5.2.0",
"esutils": "^2.0.2",
"optionator": "^0.8.1",
"source-map": "~0.6.1"
},
"dependencies": {
"estraverse": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz",
"integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==",
"dev": true
}
}
},
"eslint": {
@@ -7684,9 +7686,9 @@
}
},
"glob-parent": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
"integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"requires": {
"is-glob": "^4.0.1"
@@ -8021,19 +8023,18 @@
"eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
"dev": true
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
},
"events": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz",
"integrity": "sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==",
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"dev": true
},
"eventsource": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.0.7.tgz",
"integrity": "sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.0.tgz",
"integrity": "sha512-VSJjT5oCNrFvCS6igjzPAt5hBzQ2qPBFIbJ03zLI9SE0mxwZpMw6BfJrbFHm1a141AavMEB8JHmBhWAd66PfCg==",
"dev": true,
"requires": {
"original": "^1.0.0"
@@ -8108,9 +8109,9 @@
}
},
"exec-sh": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz",
"integrity": "sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A==",
"version": "0.3.6",
"resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.6.tgz",
"integrity": "sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w==",
"dev": true
},
"execa": {
@@ -8317,9 +8318,9 @@
},
"dependencies": {
"type": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/type/-/type-2.1.0.tgz",
"integrity": "sha512-G9absDWvhAWCV2gmF1zKud3OyC61nZDwWvBL2DApaVFogI07CprggiQAOOjvp2NRjYWFzPyu7vwtDrQFq8jeSA==",
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/type/-/type-2.5.0.tgz",
"integrity": "sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw==",
"dev": true
}
}
@@ -8499,9 +8500,9 @@
}
},
"glob-parent": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
"integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"requires": {
"is-glob": "^4.0.1"
@@ -8514,13 +8515,13 @@
"dev": true
},
"micromatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz",
"integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
"integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
"dev": true,
"requires": {
"braces": "^3.0.1",
"picomatch": "^2.0.5"
"picomatch": "^2.2.3"
}
},
"to-regex-range": {
@@ -8546,10 +8547,17 @@
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
"dev": true
},
"fast-xml-parser": {
"version": "3.19.0",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-3.19.0.tgz",
"integrity": "sha512-4pXwmBplsCPv8FOY1WRakF970TjNGnGnfbOnLqjlYvMiF1SR3yOHyxMR/YCXpPTOspNF5gwudqktIP4VsWkvBg==",
"dev": true,
"optional": true
},
"fastq": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.10.1.tgz",
"integrity": "sha512-AWuv6Ery3pM+dY7LYS8YIaCiQvUaos9OB1RyNgaOWnaX+Tik7Onvcsf8x8c+YtDeT0maYLniBip2hox5KtEXXA==",
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.0.tgz",
"integrity": "sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==",
"dev": true,
"requires": {
"reusify": "^1.0.4"
@@ -8691,6 +8699,11 @@
}
}
},
"filter-obj": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz",
"integrity": "sha1-mzERErxsYSehbgFsbF1/GeCAXFs="
},
"finalhandler": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
@@ -8852,9 +8865,9 @@
}
},
"follow-redirects": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.2.tgz",
"integrity": "sha512-6mPTgLxYm3r6Bkkg0vNM0HTjfGrOEtsfbhagQvbxDEsEkpNhw582upBaoRZylzen6krEmxXJgt9Ju6HiI4O7BA=="
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz",
"integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg=="
},
"font-awesome": {
"version": "4.7.0",
@@ -9085,54 +9098,6 @@
"define-properties": "^1.1.3",
"es-abstract": "^1.18.0-next.2",
"functions-have-names": "^1.2.2"
},
"dependencies": {
"es-abstract": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0.tgz",
"integrity": "sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw==",
"requires": {
"call-bind": "^1.0.2",
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1",
"get-intrinsic": "^1.1.1",
"has": "^1.0.3",
"has-symbols": "^1.0.2",
"is-callable": "^1.2.3",
"is-negative-zero": "^2.0.1",
"is-regex": "^1.1.2",
"is-string": "^1.0.5",
"object-inspect": "^1.9.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.2",
"string.prototype.trimend": "^1.0.4",
"string.prototype.trimstart": "^1.0.4",
"unbox-primitive": "^1.0.0"
}
},
"has-symbols": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
"integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw=="
},
"string.prototype.trimend": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz",
"integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==",
"requires": {
"call-bind": "^1.0.2",
"define-properties": "^1.1.3"
}
},
"string.prototype.trimstart": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz",
"integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==",
"requires": {
"call-bind": "^1.0.2",
"define-properties": "^1.1.3"
}
}
}
},
"functional-red-black-tree": {
@@ -9247,9 +9212,9 @@
}
},
"glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"version": "7.1.7",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
"integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -9366,9 +9331,9 @@
}
},
"graceful-fs": {
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.5.tgz",
"integrity": "sha512-kBBSQbz2K0Nyn+31j/w36fUfxkBW9/gfwRWdUY1ULReH3iokVJgddZAFcD1D0xlgTmFxJCbUkUclAlc6/IDJkw==",
"version": "4.2.6",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz",
"integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==",
"dev": true
},
"growly": {
@@ -9410,9 +9375,9 @@
}
},
"harmony-reflect": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.1.tgz",
"integrity": "sha512-WJTeyp0JzGtHcuMsi7rw2VwtkvLa+JyfEKJCFyfcS0+CDkjQ5lHPu7zEhFZP+PDSRrEgXa5Ah0l1MbgbE41XjA==",
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz",
"integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==",
"dev": true
},
"has": {
@@ -9458,9 +9423,9 @@
"optional": true
},
"has-symbols": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz",
"integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg=="
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
"integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw=="
},
"has-to-string-tag-x": {
"version": "1.4.1",
@@ -9591,9 +9556,9 @@
"dev": true
},
"hosted-git-info": {
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
"integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
"dev": true
},
"hpack.js": {
@@ -9658,12 +9623,6 @@
"integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=",
"dev": true
},
"html-comment-regex": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz",
"integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==",
"dev": true
},
"html-encoding-sniffer": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
@@ -9875,21 +9834,15 @@
"dev": true
},
"https-proxy-agent": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz",
"integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
"integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==",
"dev": true,
"requires": {
"agent-base": "5",
"agent-base": "6",
"debug": "4"
},
"dependencies": {
"agent-base": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz",
"integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==",
"dev": true
},
"debug": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
@@ -10265,18 +10218,6 @@
"requires": {
"is-svg": "^4.2.1",
"svgo": "^1.3.2"
},
"dependencies": {
"is-svg": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/is-svg/-/is-svg-4.2.1.tgz",
"integrity": "sha512-PHx3ANecKsKNl5y5+Jvt53Y4J7MfMpbNZkv384QNiswMKAWIbvcqbPz+sYbFKJI8Xv3be01GSFniPmoaP+Ai5A==",
"dev": true,
"optional": true,
"requires": {
"html-comment-regex": "^1.1.2"
}
}
}
},
"imagemin-webp": {
@@ -10444,9 +10385,9 @@
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
"integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
@@ -10626,9 +10567,9 @@
"dev": true
},
"is-bigint": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.1.tgz",
"integrity": "sha512-J0ELF4yHFxHy0cmSxZuheDOz2luOdVvqjwmEcj8H/L1JHeuEDSDbeRP+Dk9kFVk5RTFzbucJ2Kb9F7ixY2QaCg=="
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.2.tgz",
"integrity": "sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA=="
},
"is-binary-path": {
"version": "1.0.1",
@@ -10640,11 +10581,11 @@
}
},
"is-boolean-object": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.0.tgz",
"integrity": "sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA==",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.1.tgz",
"integrity": "sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng==",
"requires": {
"call-bind": "^1.0.0"
"call-bind": "^1.0.2"
}
},
"is-buffer": {
@@ -10681,9 +10622,9 @@
}
},
"is-core-module": {
"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==",
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz",
"integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==",
"dev": true,
"requires": {
"has": "^1.0.3"
@@ -10729,9 +10670,9 @@
}
},
"is-date-object": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz",
"integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g=="
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.4.tgz",
"integrity": "sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A=="
},
"is-descriptor": {
"version": "0.1.6",
@@ -10759,9 +10700,9 @@
"dev": true
},
"is-docker": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.1.1.tgz",
"integrity": "sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw==",
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
"dev": true,
"optional": true
},
@@ -10890,9 +10831,9 @@
}
},
"is-number-object": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.4.tgz",
"integrity": "sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw=="
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.5.tgz",
"integrity": "sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw=="
},
"is-obj": {
"version": "2.0.0",
@@ -10954,18 +10895,18 @@
"optional": true
},
"is-potential-custom-element-name": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz",
"integrity": "sha1-DFLlS8yjkbssSUsh6GJtczbG45c=",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
"dev": true
},
"is-regex": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.2.tgz",
"integrity": "sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz",
"integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==",
"requires": {
"call-bind": "^1.0.2",
"has-symbols": "^1.0.1"
"has-symbols": "^1.0.2"
}
},
"is-resolvable": {
@@ -10994,25 +10935,26 @@
"dev": true
},
"is-string": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz",
"integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ=="
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.6.tgz",
"integrity": "sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w=="
},
"is-svg": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-svg/-/is-svg-3.0.0.tgz",
"integrity": "sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/is-svg/-/is-svg-4.3.1.tgz",
"integrity": "sha512-h2CGs+yPUyvkgTJQS9cJzo9lYK06WgRiXUqBBHtglSzVKAuH4/oWsqk7LGfbSa1hGk9QcZ0SyQtVggvBA8LZXA==",
"dev": true,
"optional": true,
"requires": {
"html-comment-regex": "^1.1.0"
"fast-xml-parser": "^3.19.0"
}
},
"is-symbol": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
"integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
"integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
"requires": {
"has-symbols": "^1.0.1"
"has-symbols": "^1.0.2"
}
},
"is-typedarray": {
@@ -11583,6 +11525,19 @@
"integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
"dev": true
},
"escodegen": {
"version": "1.14.3",
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz",
"integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==",
"dev": true,
"requires": {
"esprima": "^4.0.1",
"estraverse": "^4.2.0",
"esutils": "^2.0.2",
"optionator": "^0.8.1",
"source-map": "~0.6.1"
}
},
"expect": {
"version": "24.9.0",
"resolved": "https://registry.npmjs.org/expect/-/expect-24.9.0.tgz",
@@ -12143,9 +12098,9 @@
"dev": true
},
"node-notifier": {
"version": "5.4.3",
"resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.4.3.tgz",
"integrity": "sha512-M4UBGcs4jeOK9CjTsYwkvH6/MzuUmGCyTW+kCY7uO+1ZVr0+FHGdPdIf5CCLqAaxnRrWidyoQlNkMIIVwbKB8Q==",
"version": "5.4.5",
"resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.4.5.tgz",
"integrity": "sha512-tVbHs7DyTLtzOiN78izLA85zRqB9NvEXkAf014Vx3jtSvn/xBl6bR8ZYifj+dFcFrKI21huSQgJZ6ZtL3B4HfQ==",
"dev": true,
"requires": {
"growly": "^1.3.0",
@@ -12273,9 +12228,9 @@
"dev": true
},
"stack-utils": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.4.tgz",
"integrity": "sha512-IPDJfugEGbfizBwBZRZ3xpccMdRyP5lqsBWXGQWimVjua/ccLCeMOAVjlc1R7LxFjo5sEDhyNIXd8mo/AiDS9w==",
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.5.tgz",
"integrity": "sha512-KZiTzuV3CnSnSvgMRrARVCj+Ht7rMbauGDK0LdVFRGyenwdylpajAp4Q0i6SX8rEmbTpMMf6ryq2gb8pPq2WgQ==",
"dev": true,
"requires": {
"escape-string-regexp": "^2.0.0"
@@ -12596,9 +12551,9 @@
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
"integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
@@ -12642,13 +12597,13 @@
"dev": true
},
"micromatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz",
"integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
"integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
"dev": true,
"requires": {
"braces": "^3.0.1",
"picomatch": "^2.0.5"
"picomatch": "^2.2.3"
}
},
"slash": {
@@ -12699,9 +12654,9 @@
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
"integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
@@ -12772,9 +12727,9 @@
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
"integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
@@ -12871,9 +12826,9 @@
},
"dependencies": {
"anymatch": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
"integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
"integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
"dev": true,
"requires": {
"normalize-path": "^3.0.0",
@@ -12912,13 +12867,13 @@
"dev": true
},
"micromatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz",
"integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
"integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
"dev": true,
"requires": {
"braces": "^3.0.1",
"picomatch": "^2.0.5"
"picomatch": "^2.2.3"
}
},
"to-regex-range": {
@@ -12968,9 +12923,9 @@
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
"integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
@@ -13041,9 +12996,9 @@
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
"integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
@@ -13118,9 +13073,9 @@
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
"integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
@@ -13164,13 +13119,13 @@
"dev": true
},
"micromatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz",
"integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
"integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
"dev": true,
"requires": {
"braces": "^3.0.1",
"picomatch": "^2.0.5"
"picomatch": "^2.2.3"
}
},
"slash": {
@@ -13247,9 +13202,9 @@
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
"integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
@@ -13374,9 +13329,9 @@
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
"integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
@@ -13460,9 +13415,9 @@
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
"integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
@@ -13557,9 +13512,9 @@
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
"integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
@@ -13597,9 +13552,9 @@
}
},
"semver": {
"version": "7.3.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
"integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"dev": true,
"requires": {
"lru-cache": "^6.0.0"
@@ -13655,9 +13610,9 @@
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
"integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
@@ -13701,13 +13656,13 @@
"dev": true
},
"micromatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz",
"integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
"integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
"dev": true,
"requires": {
"braces": "^3.0.1",
"picomatch": "^2.0.5"
"picomatch": "^2.2.3"
}
},
"supports-color": {
@@ -13754,9 +13709,9 @@
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
"integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
@@ -13820,9 +13775,9 @@
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
"integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
@@ -13916,37 +13871,45 @@
"dev": true
},
"jsdom": {
"version": "16.4.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.4.0.tgz",
"integrity": "sha512-lYMm3wYdgPhrl7pDcRmvzPhhrGVBeVhPIqeHjzeiHN3DFmD1RBpbExbi8vU7BJdH8VAZYovR8DMt0PNNDM7k8w==",
"version": "16.5.3",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.5.3.tgz",
"integrity": "sha512-Qj1H+PEvUsOtdPJ056ewXM4UJPCi4hhLA8wpiz9F2YvsRBhuFsXxtrIFAgGBDynQA9isAMGE91PfUYbdMPXuTA==",
"dev": true,
"requires": {
"abab": "^2.0.3",
"acorn": "^7.1.1",
"abab": "^2.0.5",
"acorn": "^8.1.0",
"acorn-globals": "^6.0.0",
"cssom": "^0.4.4",
"cssstyle": "^2.2.0",
"cssstyle": "^2.3.0",
"data-urls": "^2.0.0",
"decimal.js": "^10.2.0",
"decimal.js": "^10.2.1",
"domexception": "^2.0.1",
"escodegen": "^1.14.1",
"escodegen": "^2.0.0",
"html-encoding-sniffer": "^2.0.1",
"is-potential-custom-element-name": "^1.0.0",
"nwsapi": "^2.2.0",
"parse5": "5.1.1",
"parse5": "6.0.1",
"request": "^2.88.2",
"request-promise-native": "^1.0.8",
"saxes": "^5.0.0",
"request-promise-native": "^1.0.9",
"saxes": "^5.0.1",
"symbol-tree": "^3.2.4",
"tough-cookie": "^3.0.1",
"tough-cookie": "^4.0.0",
"w3c-hr-time": "^1.0.2",
"w3c-xmlserializer": "^2.0.0",
"webidl-conversions": "^6.1.0",
"whatwg-encoding": "^1.0.5",
"whatwg-mimetype": "^2.3.0",
"whatwg-url": "^8.0.0",
"ws": "^7.2.3",
"whatwg-url": "^8.5.0",
"ws": "^7.4.4",
"xml-name-validator": "^3.0.0"
},
"dependencies": {
"acorn": {
"version": "8.2.4",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.2.4.tgz",
"integrity": "sha512-Ibt84YwBDDA890eDiDCEqcbwvHlBvzzDkU2cGBBDDI1QWT12jTiXIOn2CIw5KK4i6N5Z2HUxwYjzriDyqaqqZg==",
"dev": true
}
}
},
"jsesc": {
@@ -14215,9 +14178,9 @@
}
},
"lodash": {
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"lodash-es": {
"version": "4.17.21",
@@ -14401,9 +14364,9 @@
},
"dependencies": {
"tslib": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz",
"integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==",
"dev": true
}
}
@@ -14784,9 +14747,9 @@
},
"dependencies": {
"bn.js": {
"version": "4.11.9",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
"dev": true
}
}
@@ -14798,18 +14761,18 @@
"dev": true
},
"mime-db": {
"version": "1.45.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz",
"integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==",
"version": "1.47.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.47.0.tgz",
"integrity": "sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw==",
"dev": true
},
"mime-types": {
"version": "2.1.28",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz",
"integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==",
"version": "2.1.30",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.30.tgz",
"integrity": "sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg==",
"dev": true,
"requires": {
"mime-db": "1.45.0"
"mime-db": "1.47.0"
}
},
"mimic-fn": {
@@ -15116,9 +15079,9 @@
},
"dependencies": {
"tslib": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz",
"integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==",
"dev": true
}
}
@@ -15253,9 +15216,9 @@
"dev": true
},
"node-notifier": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz",
"integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==",
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.2.tgz",
"integrity": "sha512-oJP/9NAdd9+x2Q+rfphB2RJCHjod70RcRLjosiPMMu5gjIfwVnOUGq2nbTjTUbmy0DJ/tFIVT30+Qe3nzl4TJg==",
"dev": true,
"optional": true,
"requires": {
@@ -15278,9 +15241,9 @@
}
},
"semver": {
"version": "7.3.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
"integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"dev": true,
"optional": true,
"requires": {
@@ -15314,9 +15277,9 @@
}
},
"node-releases": {
"version": "1.1.70",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.70.tgz",
"integrity": "sha512-Slf2s69+2/uAD79pVVQo8uSiC34+g8GWY8UH2Qtqv34ZfhYrxpYpfzs9Js9d6O0mbDmALuxaTlplnBTnSELcrw==",
"version": "1.1.71",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.71.tgz",
"integrity": "sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg==",
"dev": true
},
"normalize-package-data": {
@@ -15463,16 +15426,16 @@
"integrity": "sha1-rwt5f/6+r4pSxmN87b6IFs/sG8g="
},
"object-inspect": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz",
"integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw=="
"version": "1.10.3",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz",
"integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw=="
},
"object-is": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.4.tgz",
"integrity": "sha512-1ZvAZ4wlF7IyPVOcE1Omikt7UpaFlOQq0HlSti+ZvDH3UiD2brwGMwDbyV43jao2bKJ+4+WdPJHSd7kgzKYVqg==",
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz",
"integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==",
"requires": {
"call-bind": "^1.0.0",
"call-bind": "^1.0.2",
"define-properties": "^1.1.3"
}
},
@@ -15516,100 +15479,29 @@
"define-properties": "^1.1.3",
"es-abstract": "^1.18.0-next.1",
"has": "^1.0.3"
},
"dependencies": {
"es-abstract": {
"version": "1.18.0-next.2",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.2.tgz",
"integrity": "sha512-Ih4ZMFHEtZupnUh6497zEL4y2+w8+1ljnCyaTa+adcoafI1GOvMwFlDjBLfWR7y9VLfrjRJe9ocuHY1PSR9jjw==",
"requires": {
"call-bind": "^1.0.2",
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1",
"get-intrinsic": "^1.0.2",
"has": "^1.0.3",
"has-symbols": "^1.0.1",
"is-callable": "^1.2.2",
"is-negative-zero": "^2.0.1",
"is-regex": "^1.1.1",
"object-inspect": "^1.9.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.2",
"string.prototype.trimend": "^1.0.3",
"string.prototype.trimstart": "^1.0.3"
}
}
}
},
"object.fromentries": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.3.tgz",
"integrity": "sha512-IDUSMXs6LOSJBWE++L0lzIbSqHl9KDCfff2x/JSEIDtEUavUnyMYC2ZGay/04Zq4UT8lvd4xNhU4/YHKibAOlw==",
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.4.tgz",
"integrity": "sha512-EsFBshs5RUUpQEY1D4q/m59kMfz4YJvxuNCJcv/jWwOJr34EaVnG11ZrZa0UHB3wnzV1wx8m58T4hQL8IuNXlQ==",
"dev": true,
"requires": {
"call-bind": "^1.0.0",
"call-bind": "^1.0.2",
"define-properties": "^1.1.3",
"es-abstract": "^1.18.0-next.1",
"es-abstract": "^1.18.0-next.2",
"has": "^1.0.3"
},
"dependencies": {
"es-abstract": {
"version": "1.18.0-next.2",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.2.tgz",
"integrity": "sha512-Ih4ZMFHEtZupnUh6497zEL4y2+w8+1ljnCyaTa+adcoafI1GOvMwFlDjBLfWR7y9VLfrjRJe9ocuHY1PSR9jjw==",
"dev": true,
"requires": {
"call-bind": "^1.0.2",
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1",
"get-intrinsic": "^1.0.2",
"has": "^1.0.3",
"has-symbols": "^1.0.1",
"is-callable": "^1.2.2",
"is-negative-zero": "^2.0.1",
"is-regex": "^1.1.1",
"object-inspect": "^1.9.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.2",
"string.prototype.trimend": "^1.0.3",
"string.prototype.trimstart": "^1.0.3"
}
}
}
},
"object.getownpropertydescriptors": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.1.tgz",
"integrity": "sha512-6DtXgZ/lIZ9hqx4GtZETobXLR/ZLaa0aqV0kzbn80Rf8Z2e/XFnhA0I7p07N2wH8bBBltr2xQPi6sbKWAY2Eng==",
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.2.tgz",
"integrity": "sha512-WtxeKSzfBjlzL+F9b7M7hewDzMwy+C8NRssHd1YrNlzHzIDrXcXiNOMrezdAEM4UXixgV+vvnyBeN7Rygl2ttQ==",
"dev": true,
"requires": {
"call-bind": "^1.0.0",
"call-bind": "^1.0.2",
"define-properties": "^1.1.3",
"es-abstract": "^1.18.0-next.1"
},
"dependencies": {
"es-abstract": {
"version": "1.18.0-next.2",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.2.tgz",
"integrity": "sha512-Ih4ZMFHEtZupnUh6497zEL4y2+w8+1ljnCyaTa+adcoafI1GOvMwFlDjBLfWR7y9VLfrjRJe9ocuHY1PSR9jjw==",
"dev": true,
"requires": {
"call-bind": "^1.0.2",
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1",
"get-intrinsic": "^1.0.2",
"has": "^1.0.3",
"has-symbols": "^1.0.1",
"is-callable": "^1.2.2",
"is-negative-zero": "^2.0.1",
"is-regex": "^1.1.1",
"object-inspect": "^1.9.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.2",
"string.prototype.trimend": "^1.0.3",
"string.prototype.trimstart": "^1.0.3"
}
}
"es-abstract": "^1.18.0-next.2"
}
},
"object.pick": {
@@ -15622,39 +15514,15 @@
}
},
"object.values": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.2.tgz",
"integrity": "sha512-MYC0jvJopr8EK6dPBiO8Nb9mvjdypOachO5REGk6MXzujbBrAisKo3HmdEI6kZDL6fC31Mwee/5YbtMebixeag==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.3.tgz",
"integrity": "sha512-nkF6PfDB9alkOUxpf1HNm/QlkeW3SReqL5WXeBLpEJJnlPSvRaDQpW3gQTksTN3fgJX4hL42RzKyOin6ff3tyw==",
"dev": true,
"requires": {
"call-bind": "^1.0.0",
"call-bind": "^1.0.2",
"define-properties": "^1.1.3",
"es-abstract": "^1.18.0-next.1",
"es-abstract": "^1.18.0-next.2",
"has": "^1.0.3"
},
"dependencies": {
"es-abstract": {
"version": "1.18.0-next.2",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.2.tgz",
"integrity": "sha512-Ih4ZMFHEtZupnUh6497zEL4y2+w8+1ljnCyaTa+adcoafI1GOvMwFlDjBLfWR7y9VLfrjRJe9ocuHY1PSR9jjw==",
"dev": true,
"requires": {
"call-bind": "^1.0.2",
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1",
"get-intrinsic": "^1.0.2",
"has": "^1.0.3",
"has-symbols": "^1.0.1",
"is-callable": "^1.2.2",
"is-negative-zero": "^2.0.1",
"is-regex": "^1.1.1",
"object-inspect": "^1.9.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.2",
"string.prototype.trimend": "^1.0.3",
"string.prototype.trimstart": "^1.0.3"
}
}
}
},
"obuf": {
@@ -15996,9 +15864,9 @@
},
"dependencies": {
"tslib": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz",
"integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==",
"dev": true
}
}
@@ -16049,9 +15917,9 @@
"integrity": "sha1-8r0iH2zJcKk42IVWq8WJyqqiveE="
},
"parse5": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
"integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
"integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==",
"dev": true
},
"parseurl": {
@@ -16071,9 +15939,9 @@
},
"dependencies": {
"tslib": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz",
"integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==",
"dev": true
}
}
@@ -16140,9 +16008,9 @@
"dev": true
},
"pbkdf2": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz",
"integrity": "sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz",
"integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==",
"dev": true,
"requires": {
"create-hash": "^1.1.2",
@@ -16166,9 +16034,9 @@
"dev": true
},
"picomatch": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
"integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.3.tgz",
"integrity": "sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==",
"dev": true
},
"pify": {
@@ -16981,24 +16849,21 @@
}
},
"postcss-selector-parser": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz",
"integrity": "sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw==",
"version": "6.0.6",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz",
"integrity": "sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg==",
"dev": true,
"requires": {
"cssesc": "^3.0.0",
"indexes-of": "^1.0.1",
"uniq": "^1.0.1",
"util-deprecate": "^1.0.2"
}
},
"postcss-svgo": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-4.0.2.tgz",
"integrity": "sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw==",
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-4.0.3.tgz",
"integrity": "sha512-NoRbrcMWTtUghzuKSoIm6XV+sJdvZ7GZSc3wdBN0W19FTtp2ko8NqLsgoh/m9CzNhU3KLPvQmjIwtaNFkaFTvw==",
"dev": true,
"requires": {
"is-svg": "^3.0.0",
"postcss": "^7.0.0",
"postcss-value-parser": "^3.0.0",
"svgo": "^1.0.0"
@@ -17088,9 +16953,9 @@
"dev": true
},
"react-is": {
"version": "17.0.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz",
"integrity": "sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==",
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true
}
}
@@ -17138,9 +17003,9 @@
"dev": true
},
"prompts": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.0.tgz",
"integrity": "sha512-awZAKrk3vN6CroQukBL+R9051a4R3zCZBlJm/HBfrSZ8iTpYix3VX1vU4mveiLpiwmOJT4wokTF9m6HUk4KqWQ==",
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.1.tgz",
"integrity": "sha512-EQyfIuO2hPDsX1L/blblV+H7I0knhgAd82cVneCwcdND9B8AuCDuRcBH6yIcG4dFzlOUqbazQqwGjx5xmsNLuQ==",
"dev": true,
"requires": {
"kleur": "^3.0.3",
@@ -17227,17 +17092,17 @@
},
"dependencies": {
"bn.js": {
"version": "4.11.9",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
"dev": true
}
}
},
"pubsub-js": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/pubsub-js/-/pubsub-js-1.7.0.tgz",
"integrity": "sha512-Pb68P9qFZxnvDipHMuj9oT1FoIgBcXJ9C9eWdHCLZAnulaUoJ3+Y87RhGMYilWpun6DMWVmvK70T4RP4drZMSA=="
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/pubsub-js/-/pubsub-js-1.9.3.tgz",
"integrity": "sha512-FhYYlPNOywTh7zN38u5AlG67emA47w6JZd7YgdQU1w8gQbZhhIGxVM0AQosdaINHb2ALb+fhfnVyBJAt4D4IzA=="
},
"pump": {
"version": "3.0.0",
@@ -17291,11 +17156,12 @@
"dev": true
},
"query-string": {
"version": "6.13.8",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-6.13.8.tgz",
"integrity": "sha512-jxJzQI2edQPE/NPUOusNjO/ZOGqr1o2OBa/3M00fU76FsLXDVbJDv/p7ng5OdQyorKrkRz1oqfwmbe5MAMePQg==",
"version": "6.14.1",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-6.14.1.tgz",
"integrity": "sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw==",
"requires": {
"decode-uri-component": "^0.2.0",
"filter-obj": "^1.1.0",
"split-on-first": "^1.0.0",
"strict-uri-encode": "^2.0.0"
}
@@ -17318,6 +17184,12 @@
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
"dev": true
},
"queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"dev": true
},
"randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -17366,9 +17238,9 @@
}
},
"react-bootstrap": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.5.2.tgz",
"integrity": "sha512-mGKPY5+lLd7Vtkx2VFivoRkPT4xAHazuFfIhJLTEgHlDfIUSePn7qrmpZe5gXH9zvHV0RsBaQ9cLfXjxnZrOpA==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.6.0.tgz",
"integrity": "sha512-PaeOGeRC2+JH9Uf1PukJgXcIpfGlrKKHEBZIArymjenYzSJ/RhO2UdNX+e7nalsCFFZLRRgQ0/FKkscW2LmmRg==",
"requires": {
"@babel/runtime": "^7.13.8",
"@restart/context": "^2.1.4",
@@ -17388,16 +17260,6 @@
"react-transition-group": "^4.4.1",
"uncontrollable": "^7.2.1",
"warning": "^4.0.3"
},
"dependencies": {
"@babel/runtime": {
"version": "7.13.17",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.17.tgz",
"integrity": "sha512-NCdgJEelPTSh+FEFylhnP1ylq848l1z9t9N0j1Lfbcw0+KXGjsTvUmkxy+voLLXB5SOKMbLLx4jxYliGrYQseA==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
}
}
},
"react-break": {
@@ -17744,9 +17606,9 @@
"dev": true
},
"react-fast-compare": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
"integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw=="
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
"integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
},
"react-focus-lock": {
"version": "2.5.0",
@@ -17783,6 +17645,13 @@
"prop-types": "^15.7.2",
"react-fast-compare": "^2.0.4",
"react-side-effect": "^2.1.0"
},
"dependencies": {
"react-fast-compare": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
"integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw=="
}
}
},
"react-intl": {
@@ -17820,16 +17689,6 @@
"prop-types": "^15.7.2",
"uncontrollable": "^7.2.1",
"warning": "^4.0.3"
},
"dependencies": {
"@babel/runtime": {
"version": "7.13.17",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.17.tgz",
"integrity": "sha512-NCdgJEelPTSh+FEFylhnP1ylq848l1z9t9N0j1Lfbcw0+KXGjsTvUmkxy+voLLXB5SOKMbLLx4jxYliGrYQseA==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
}
}
},
"react-popper": {
@@ -17839,13 +17698,6 @@
"requires": {
"react-fast-compare": "^3.0.1",
"warning": "^4.0.2"
},
"dependencies": {
"react-fast-compare": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
"integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
}
}
},
"react-proptype-conditional-require": {
@@ -17854,11 +17706,12 @@
"integrity": "sha1-acLVdB5t9eCPIw82u8KUTuEiJVU="
},
"react-redux": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.2.tgz",
"integrity": "sha512-8+CQ1EvIVFkYL/vu6Olo7JFLWop1qRUeb46sGtIMDCSpgwPQq8fPLpirIB0iTqFe9XYEFPHssdX8/UwN6pAkEA==",
"version": "7.2.4",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.4.tgz",
"integrity": "sha512-hOQ5eOSkEJEXdpIKbnRyl04LhaWabkDPV+Ix97wqQX3T3d2NQ8DUblNXXtNMavc7DpswyQM6xfaN4HQDKNY2JA==",
"requires": {
"@babel/runtime": "^7.12.1",
"@types/react-redux": "^7.1.16",
"hoist-non-react-statics": "^3.3.2",
"loose-envify": "^1.4.0",
"prop-types": "^15.7.2",
@@ -17866,9 +17719,9 @@
}
},
"react-remove-scroll": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.4.1.tgz",
"integrity": "sha512-K7XZySEzOHMTq7dDwcHsZA6Y7/1uX5RsWhRXVYv8rdh+y9Qz2nMwl9RX/Mwnj/j7JstCGmxyfyC0zbVGXYh3mA==",
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.4.2.tgz",
"integrity": "sha512-mMSIZYQF3jS2uRJXeFDRaVGA+BGs/hIryV64YUKsHFtpgwZloOUcdu0oW8K6OU8uLHt/kM5d0lUZbdpIVwgXtQ==",
"requires": {
"react-remove-scroll-bar": "^2.1.0",
"react-style-singleton": "^2.1.0",
@@ -18251,9 +18104,9 @@
"dev": true
},
"regjsparser": {
"version": "0.6.7",
"resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.7.tgz",
"integrity": "sha512-ib77G0uxsA2ovgiYbCVGx4Pv3PSttAx2vIwidqQzbL2U5S4Q+j00HdSAneSBuyVcMvEnTXMjiGgB+DlXozVhpQ==",
"version": "0.6.9",
"resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.9.tgz",
"integrity": "sha512-ZqbNRz1SNjLAiYuwY0zoXW8Ne675IX5q+YHioAGbCw4X96Mjl2+dcX9B2ciaeyYjViDAfvIjFpQjJgLttTEERQ==",
"dev": true,
"requires": {
"jsesc": "~0.5.0"
@@ -18327,9 +18180,9 @@
},
"dependencies": {
"domelementtype": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.1.0.tgz",
"integrity": "sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz",
"integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==",
"dev": true
}
}
@@ -18393,9 +18246,9 @@
}
},
"repeat-element": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz",
"integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==",
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz",
"integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==",
"dev": true
},
"repeat-string": {
@@ -18516,12 +18369,12 @@
"integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA=="
},
"resolve": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz",
"integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==",
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
"integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
"dev": true,
"requires": {
"is-core-module": "^2.1.0",
"is-core-module": "^2.2.0",
"path-parse": "^1.0.6"
}
},
@@ -18817,10 +18670,13 @@
"dev": true
},
"run-parallel": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.10.tgz",
"integrity": "sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw==",
"dev": true
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
"dev": true,
"requires": {
"queue-microtask": "^1.2.2"
}
},
"run-queue": {
"version": "1.0.3",
@@ -18838,9 +18694,9 @@
"dev": true
},
"rxjs": {
"version": "6.6.3",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz",
"integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==",
"version": "6.6.7",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
"integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
"dev": true,
"requires": {
"tslib": "^1.9.0"
@@ -18926,9 +18782,9 @@
}
},
"semver": {
"version": "7.3.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
"integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"dev": true,
"requires": {
"lru-cache": "^6.0.0"
@@ -19003,9 +18859,9 @@
"dev": true
},
"selfsigned": {
"version": "1.10.8",
"resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.8.tgz",
"integrity": "sha512-2P4PtieJeEwVgTU9QEcwIRDQ/mXJLX8/+I3ur+Pg16nS8oNbrGxEso9NyYWy8NAmXiNl4dlAp5MwoNeCWzON4w==",
"version": "1.10.11",
"resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.11.tgz",
"integrity": "sha512-aVmbPOfViZqOZPgRBT0+3u4yZFHpmnIghLMlAcb5/xhp5ZtB/RVnKhz5vl2M32CLXAqR4kha9zfhNg0Lf/sxKA==",
"dev": true,
"requires": {
"node-forge": "^0.10.0"
@@ -19749,9 +19605,9 @@
}
},
"ssri": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz",
"integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==",
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz",
"integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==",
"dev": true,
"requires": {
"figgy-pudding": "^3.5.1"
@@ -19949,9 +19805,9 @@
"integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY="
},
"string-length": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.1.tgz",
"integrity": "sha512-PKyXUd0LK0ePjSOnWn34V2uD6acUWev9uy0Ft05k0E8xRW+SKcA0F7eMr7h5xlzfn+4O3N+55rduYyet3Jk+jw==",
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
"integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==",
"dev": true,
"requires": {
"char-regex": "^1.0.2",
@@ -19970,9 +19826,9 @@
}
},
"string-width": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
"integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
"integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==",
"dev": true,
"requires": {
"emoji-regex": "^8.0.0",
@@ -19992,59 +19848,35 @@
}
},
"string.prototype.matchall": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.3.tgz",
"integrity": "sha512-OBxYDA2ifZQ2e13cP82dWFMaCV9CGF8GzmN4fljBVw5O5wep0lu4gacm1OL6MjROoUnB8VbkWRThqkV2YFLNxw==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.4.tgz",
"integrity": "sha512-pknFIWVachNcyqRfaQSeu/FUfpvJTe4uskUSZ9Wc1RijsPuzbZ8TyYT8WCNnntCjUEqQ3vUHMAfVj2+wLAisPQ==",
"dev": true,
"requires": {
"call-bind": "^1.0.0",
"call-bind": "^1.0.2",
"define-properties": "^1.1.3",
"es-abstract": "^1.18.0-next.1",
"es-abstract": "^1.18.0-next.2",
"has-symbols": "^1.0.1",
"internal-slot": "^1.0.2",
"regexp.prototype.flags": "^1.3.0",
"side-channel": "^1.0.3"
},
"dependencies": {
"es-abstract": {
"version": "1.18.0-next.2",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.2.tgz",
"integrity": "sha512-Ih4ZMFHEtZupnUh6497zEL4y2+w8+1ljnCyaTa+adcoafI1GOvMwFlDjBLfWR7y9VLfrjRJe9ocuHY1PSR9jjw==",
"dev": true,
"requires": {
"call-bind": "^1.0.2",
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1",
"get-intrinsic": "^1.0.2",
"has": "^1.0.3",
"has-symbols": "^1.0.1",
"is-callable": "^1.2.2",
"is-negative-zero": "^2.0.1",
"is-regex": "^1.1.1",
"object-inspect": "^1.9.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.2",
"string.prototype.trimend": "^1.0.3",
"string.prototype.trimstart": "^1.0.3"
}
}
"internal-slot": "^1.0.3",
"regexp.prototype.flags": "^1.3.1",
"side-channel": "^1.0.4"
}
},
"string.prototype.trimend": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz",
"integrity": "sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz",
"integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==",
"requires": {
"call-bind": "^1.0.0",
"call-bind": "^1.0.2",
"define-properties": "^1.1.3"
}
},
"string.prototype.trimstart": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz",
"integrity": "sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz",
"integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==",
"requires": {
"call-bind": "^1.0.0",
"call-bind": "^1.0.2",
"define-properties": "^1.1.3"
}
},
@@ -20269,9 +20101,9 @@
}
},
"supports-hyperlinks": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz",
"integrity": "sha512-zoE5/e+dnEijk6ASB6/qrK+oYdm2do1hjoLWrqUC/8WEIW1gbxFcKuBof7sW8ArN6e+AYvsE8HBGiVRWL/F5CA==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz",
"integrity": "sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==",
"dev": true,
"requires": {
"has-flag": "^4.0.0",
@@ -20351,9 +20183,9 @@
},
"dependencies": {
"domelementtype": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.1.0.tgz",
"integrity": "sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz",
"integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==",
"dev": true
}
}
@@ -20667,16 +20499,24 @@
}
},
"teeny-request": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-6.0.1.tgz",
"integrity": "sha512-TAK0c9a00ELOqLrZ49cFxvPVogMUFaWY8dUsQc/0CuQPGF+BOxOQzXfE413BAk2kLomwNplvdtMpeaeGWmoc2g==",
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-7.0.1.tgz",
"integrity": "sha512-sasJmQ37klOlplL4Ia/786M5YlOcoLGQyq2TE4WHSRupbAuDaQW0PfVxV4MtdBtRJ4ngzS+1qim8zP6Zp35qCw==",
"dev": true,
"requires": {
"http-proxy-agent": "^4.0.0",
"https-proxy-agent": "^4.0.0",
"node-fetch": "^2.2.0",
"https-proxy-agent": "^5.0.0",
"node-fetch": "^2.6.1",
"stream-events": "^1.0.5",
"uuid": "^3.3.2"
"uuid": "^8.0.0"
},
"dependencies": {
"uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"dev": true
}
}
},
"temp-dir": {
@@ -20960,14 +20800,22 @@
"dev": true
},
"tough-cookie": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz",
"integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz",
"integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==",
"dev": true,
"requires": {
"ip-regex": "^2.1.0",
"psl": "^1.1.28",
"punycode": "^2.1.1"
"psl": "^1.1.33",
"punycode": "^2.1.1",
"universalify": "^0.1.2"
},
"dependencies": {
"universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"dev": true
}
}
},
"tr46": {
@@ -21113,9 +20961,9 @@
}
},
"typescript": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz",
"integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==",
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz",
"integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==",
"dev": true
},
"unbox-primitive": {
@@ -21127,13 +20975,6 @@
"has-bigints": "^1.0.1",
"has-symbols": "^1.0.2",
"which-boxed-primitive": "^1.0.2"
},
"dependencies": {
"has-symbols": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
"integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw=="
}
}
},
"unbzip2-stream": {
@@ -21365,9 +21206,9 @@
}
},
"url-parse": {
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz",
"integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==",
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.1.tgz",
"integrity": "sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q==",
"dev": true,
"requires": {
"querystringify": "^2.1.1",
@@ -21470,15 +21311,15 @@
"dev": true
},
"v8-compile-cache": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz",
"integrity": "sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
"integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
"dev": true
},
"v8-to-istanbul": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-7.1.0.tgz",
"integrity": "sha512-uXUVqNUCLa0AH1vuVxzi+MI4RfxEOKt9pBgKwHbgH7st8Kv2P1m+jvWNnektzBh5QShF3ODgKmUFCf38LnVz1g==",
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-7.1.2.tgz",
"integrity": "sha512-TxNb7YEUwkLXCQYeudi6lgQ/SZrzNO4kMdlqVxaZPUIUjCv6iSSypUQX70kNBSERpQ8fk48+d61FXk+tgqcWow==",
"dev": true,
"requires": {
"@types/istanbul-lib-coverage": "^2.0.1",
@@ -21586,9 +21427,9 @@
},
"dependencies": {
"anymatch": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
"integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
"integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
"dev": true,
"optional": true,
"requires": {
@@ -21648,9 +21489,9 @@
"optional": true
},
"glob-parent": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
"integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"optional": true,
"requires": {
@@ -22032,9 +21873,9 @@
},
"dependencies": {
"mime": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.5.0.tgz",
"integrity": "sha512-ft3WayFSFUVBuJj7BMLKAQcSlItKtfjsKDDsii3rqFDAZ7t11zRe8ASw/GlmivGwVUYtwkQrxiGGpL6gFvB0ag==",
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz",
"integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==",
"dev": true
}
}
@@ -22396,12 +22237,12 @@
"dev": true
},
"whatwg-url": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.4.0.tgz",
"integrity": "sha512-vwTUFf6V4zhcPkWp/4CQPr1TW9Ml6SF4lVyaIMBdJw5i6qUUJ1QWM4Z6YYVkfka0OUIzVo/0aNtGVGk256IKWw==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.5.0.tgz",
"integrity": "sha512-fy+R77xWv0AiqfLl4nuGUlQ3/6b5uNfQ4WAbGQVMYshCTCCPK9psC1nWh3XHuxGVCtlcDDQPQW1csmmIQo+fwg==",
"dev": true,
"requires": {
"lodash.sortby": "^4.7.0",
"lodash": "^4.7.0",
"tr46": "^2.0.2",
"webidl-conversions": "^6.1.0"
}
@@ -22564,9 +22405,9 @@
}
},
"ws": {
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.3.tgz",
"integrity": "sha512-hr6vCR76GsossIRsr8OLR9acVVm1jyfEWvhbNjtgPOrfvAlKzvyeg/P6r8RuDjRyrcQoPQT7K0DGEPc7Ae6jzA==",
"version": "7.4.5",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz",
"integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==",
"dev": true
},
"xml-name-validator": {
@@ -22597,9 +22438,9 @@
"dev": true
},
"y18n": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz",
"integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==",
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
"dev": true
},
"yallist": {
@@ -22610,9 +22451,9 @@
"optional": true
},
"yaml": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz",
"integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==",
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"dev": true
},
"yargs": {

View File

@@ -36,9 +36,10 @@
},
"dependencies": {
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
"@edx/frontend-component-footer": "10.1.4",
"@edx/frontend-component-footer": "10.1.5",
"@edx/frontend-enterprise": "4.2.3",
"@edx/frontend-platform": "1.8.4",
"@edx/frontend-lib-special-exams": "1.7.1",
"@edx/frontend-platform": "1.11.0",
"@edx/paragon": "14.8.0",
"@fortawesome/fontawesome-svg-core": "1.2.34",
"@fortawesome/free-brands-svg-icons": "5.13.1",
@@ -55,7 +56,7 @@
"react-break": "1.3.2",
"react-dom": "16.13.1",
"react-helmet": "6.0.0",
"react-redux": "7.2.2",
"react-redux": "7.2.4",
"react-router": "5.2.0",
"react-router-dom": "5.2.0",
"react-share": "4.2.1",
@@ -71,9 +72,9 @@
"@testing-library/react": "10.3.0",
"@testing-library/user-event": "12.0.17",
"axios-mock-adapter": "1.18.2",
"codecov": "3.8.1",
"codecov": "3.8.2",
"es-check": "5.1.4",
"glob": "7.1.6",
"glob": "7.1.7",
"husky": "3.1.0",
"jest": "24.9.0",
"jest-chain": "1.1.5",

View File

@@ -9,6 +9,7 @@ import { Hyperlink } from '@edx/paragon';
import { Alert, ALERT_TYPES } from '../../generic/user-messages';
import messages from './messages';
import AccessExpirationAlertMMP2P from './AccessExpirationAlertMMP2P';
import AccessExpirationAlertMasquerade from './AccessExpirationAlertMasquerade';
function AccessExpirationAlert({ intl, payload }) {
/** [MM-P2P] Experiment */
@@ -42,24 +43,7 @@ function AccessExpirationAlert({ intl, payload }) {
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>
<AccessExpirationAlertMasquerade payload={payload} />
);
}

View File

@@ -0,0 +1,60 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage, FormattedDate } from '@edx/frontend-platform/i18n';
import { Alert, ALERT_TYPES } from '../../generic/user-messages';
function AccessExpirationAlertMasquerade({ payload }) {
const {
accessExpiration,
userTimezone,
} = payload;
if (!accessExpiration) {
return null;
}
const {
expirationDate,
masqueradingExpiredCourse,
} = accessExpiration;
if (!masqueradingExpiredCourse) {
return null;
}
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
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>
);
}
AccessExpirationAlertMasquerade.propTypes = {
payload: PropTypes.shape({
accessExpiration: PropTypes.shape({
expirationDate: PropTypes.string.isRequired,
masqueradingExpiredCourse: PropTypes.bool.isRequired,
}).isRequired,
userTimezone: PropTypes.string.isRequired,
}).isRequired,
};
export default AccessExpirationAlertMasquerade;

View File

@@ -2,6 +2,7 @@ import React, { useMemo } from 'react';
import { useAlert } from '../../generic/user-messages';
const AccessExpirationAlert = React.lazy(() => import('./AccessExpirationAlert'));
const AccessExpirationAlertMasquerade = React.lazy(() => import('./AccessExpirationAlertMasquerade'));
function useAccessExpirationAlert(accessExpiration, courseId, org, userTimezone, topic, analyticsPageName) {
const isVisible = !!accessExpiration; // If it exists, show it.
@@ -22,4 +23,20 @@ function useAccessExpirationAlert(accessExpiration, courseId, org, userTimezone,
return { clientAccessExpirationAlert: AccessExpirationAlert };
}
export function useAccessExpirationAlertMasquerade(accessExpiration, userTimezone, topic) {
const isVisible = !!accessExpiration; // If it exists, show it.
const payload = {
accessExpiration,
userTimezone,
};
useAlert(isVisible, {
code: 'clientAccessExpirationAlertMasquerade',
payload: useMemo(() => payload, Object.values(payload).sort()),
topic,
});
return { clientAccessExpirationAlertMasquerade: AccessExpirationAlertMasquerade };
}
export default useAccessExpirationAlert;

View File

@@ -1 +1 @@
export { default } from './hooks';
export { default, useAccessExpirationAlertMasquerade } from './hooks';

View File

@@ -0,0 +1,135 @@
import React, { useState } from 'react';
import Cookies from 'js-cookie';
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import {
AlertModal,
Button,
Spinner,
Icon,
} from '@edx/paragon';
import { Check, ArrowForward } from '@edx/paragon/icons';
import { FormattedMessage, injectIntl } from '@edx/frontend-platform/i18n';
import { sendActivationEmail } from '../../courseware/data';
function AccountActivationAlert() {
const [showModal, setShowModal] = useState(false);
const [showSpinner, setShowSpinner] = useState(false);
const [showCheck, setShowCheck] = useState(false);
const handleOnClick = () => {
setShowSpinner(true);
setShowCheck(false);
sendActivationEmail().then(() => {
setShowSpinner(false);
setShowCheck(true);
});
};
const showAccountActivationAlert = Cookies.get('show-account-activation-popup');
if (showAccountActivationAlert !== undefined) {
Cookies.remove('show-account-activation-popup', { path: '/', domain: process.env.SESSION_COOKIE_DOMAIN });
// extra check to make sure cookie was removed before updating the state. Updating the state without removal
// of cookie would make it infinit rendering
if (Cookies.get('show-account-activation-popup') === undefined) {
setShowModal(true);
}
}
const title = (
<h3>
<FormattedMessage
id="account-activation.alert.title"
defaultMessage="Activate your account so you can log back in"
description="Title for account activation alert which is shown after the registration"
/>
</h3>
);
const button = (
<Button
variant="primary"
className=""
onClick={() => setShowModal(false)}
>
<FormattedMessage
id="account-activation.alert.button"
defaultMessage="Continue to {siteName}"
description="account activation alert continue button"
values={{
siteName: getConfig().SITE_NAME,
}}
/>
<Icon src={ArrowForward} className="ml-1 d-inline-block align-bottom" />
</Button>
);
const children = () => {
let bodyContent = null;
const message = (
<FormattedMessage
id="account-activation.alert.message"
defaultMessage="We sent an email to {boldEmail} with a link to activate your account. Cant find it? Check your spam folder or
{sendEmailTag}."
description="Message for account activation alert which is shown after the registration"
values={{
boldEmail: <b>{getAuthenticatedUser() && getAuthenticatedUser().email}</b>,
sendEmailTag: (
// eslint-disable-next-line jsx-a11y/anchor-is-valid
<a href="#" role="button" onClick={handleOnClick}>
<FormattedMessage
id="account-activation.resend.link"
defaultMessage="resend the email"
description="Message for resend link in account activation alert which is shown after the registration"
/>
</a>
),
}}
/>
);
bodyContent = (
<div>
{message}
</div>
);
if (!showCheck && showSpinner) {
bodyContent = (
<div>
{message}
<Spinner
animation="border"
variant="secondary"
style={{ height: '1.5rem', width: '1.5rem' }}
/>
</div>
);
}
if (showCheck && !showSpinner) {
bodyContent = (
<div>
{message}
<Icon
src={Check}
style={{ height: '1.7rem', width: '1.25rem' }}
className="text-success-500 d-inline-block position-fixed"
/>
</div>
);
}
return bodyContent;
};
return (
<AlertModal
isOpen={showModal}
title={title}
footerNode={button}
onClose={() => ({})}
>
{children()}
</AlertModal>
);
}
export default injectIntl(AccountActivationAlert);

View File

@@ -2,4 +2,4 @@ import './courseHomeMetadata.factory';
import './datesTabData.factory';
import './outlineTabData.factory';
import './progressTabData.factory';
import './upgradeCardData.factory';
import './upgradeNotificationData.factory';

View File

@@ -31,6 +31,12 @@ Factory.define('outlineTabData')
.attrs({
access_expiration: null,
can_show_upgrade_sock: false,
cert_data: {
cert_status: null,
cert_web_view_url: null,
certificate_available_date: null,
download_url: null,
},
course_goals: {
goal_options: [],
selected_goal: null,

View File

@@ -12,9 +12,9 @@ Factory.define('progressTabData')
locked_count: 0,
},
course_grade: {
letter_grade: null,
percent: 0,
is_passing: false,
letter_grade: 'pass',
percent: 1,
is_passing: true,
},
section_scores: [
{
@@ -22,6 +22,7 @@ Factory.define('progressTabData')
subsections: [
{
assignment_type: 'Homework',
block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345',
display_name: 'First subsection',
has_graded_assignment: true,
num_points_earned: 0,
@@ -55,6 +56,7 @@ Factory.define('progressTabData')
assignment_policies: [
{
num_droppable: 1,
num_total: 2,
short_label: 'HW',
type: 'Homework',
weight: 1,

View File

@@ -1,6 +1,6 @@
import { Factory } from 'rosie'; // eslint-disable-line import/no-extraneous-dependencies
Factory.define('upgradeCardData')
Factory.define('upgradeNotificationData')
.option('host', 'http://localhost:18000')
.option('dateBlocks', [])
.option('offer', null)

View File

@@ -5,6 +5,7 @@ Object {
"courseHome": Object {
"courseId": "course-v1:edX+DemoX+Demo_Course_1",
"courseStatus": "loaded",
"gradesFeatureIsLocked": false,
"toastBodyLink": null,
"toastBodyText": null,
"toastHeader": "",
@@ -12,8 +13,10 @@ Object {
"courseware": Object {
"courseId": null,
"courseStatus": "loading",
"proctoredExamsEnabledWaffleFlag": false,
"sequenceId": null,
"sequenceStatus": "loading",
"specialExamsEnabledWaffleFlag": false,
},
"models": Object {
"courseHomeMeta": Object {
@@ -298,6 +301,7 @@ Object {
"courseHome": Object {
"courseId": "course-v1:edX+DemoX+Demo_Course_1",
"courseStatus": "loaded",
"gradesFeatureIsLocked": false,
"toastBodyLink": null,
"toastBodyText": null,
"toastHeader": "",
@@ -305,8 +309,10 @@ Object {
"courseware": Object {
"courseId": null,
"courseStatus": "loading",
"proctoredExamsEnabledWaffleFlag": false,
"sequenceId": null,
"sequenceStatus": "loading",
"specialExamsEnabledWaffleFlag": false,
},
"models": Object {
"courseHomeMeta": Object {
@@ -363,6 +369,12 @@ Object {
"course-v1:edX+DemoX+Demo_Course_1": Object {
"accessExpiration": null,
"canShowUpgradeSock": false,
"certData": Object {
"certStatus": null,
"certWebViewUrl": null,
"certificateAvailableDate": null,
"downloadUrl": null,
},
"courseBlocks": Object {
"courses": Object {
"block-v1:edX+DemoX+Demo_Course+type@course+block@bcdabcdabcdabcdabcdabcdabcdabcd3": Object {
@@ -460,3 +472,158 @@ Object {
},
}
`;
exports[`Data layer integration tests Test fetchProgressTab Should fetch, normalize, and save metadata 1`] = `
Object {
"courseHome": Object {
"courseId": "course-v1:edX+DemoX+Demo_Course_1",
"courseStatus": "loaded",
"gradesFeatureIsLocked": false,
"toastBodyLink": null,
"toastBodyText": null,
"toastHeader": "",
},
"courseware": Object {
"courseId": null,
"courseStatus": "loading",
"proctoredExamsEnabledWaffleFlag": false,
"sequenceId": null,
"sequenceStatus": "loading",
"specialExamsEnabledWaffleFlag": false,
},
"models": Object {
"courseHomeMeta": Object {
"course-v1:edX+DemoX+Demo_Course_1": Object {
"canLoadCourseware": false,
"id": "course-v1:edX+DemoX+Demo_Course_1",
"isEnrolled": false,
"isSelfPaced": false,
"isStaff": false,
"number": "DemoX",
"org": "edX",
"originalUserIsStaff": false,
"tabs": Array [
Object {
"slug": "outline",
"title": "Course",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/course/",
},
Object {
"slug": "discussion",
"title": "Discussion",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/discussion/forum/",
},
Object {
"slug": "wiki",
"title": "Wiki",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/course_wiki",
},
Object {
"slug": "progress",
"title": "Progress",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/progress",
},
Object {
"slug": "instructor",
"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",
"verifiedMode": Object {
"currencySymbol": "$",
"price": 10,
"upgradeUrl": "test",
},
},
},
"progress": Object {
"course-v1:edX+DemoX+Demo_Course_1": Object {
"certificateData": Object {},
"completionSummary": Object {
"completeCount": 1,
"incompleteCount": 1,
"lockedCount": 0,
},
"courseGrade": Object {
"isPassing": true,
"letterGrade": "pass",
"percent": 1,
"visiblePercent": 1,
},
"courseId": "course-v1:edX+DemoX+Demo_Course_1",
"end": "3027-03-31T00:00:00Z",
"enrollmentMode": "audit",
"gradingPolicy": Object {
"assignmentPolicies": Array [
Object {
"averageGrade": 1,
"numDroppable": 1,
"shortLabel": "HW",
"type": "Homework",
"weight": 1,
"weightedGrade": 1,
},
],
"gradeRange": Object {
"pass": 0.75,
},
},
"hasScheduledContent": false,
"id": "course-v1:edX+DemoX+Demo_Course_1",
"sectionScores": Array [
Object {
"displayName": "First section",
"subsections": Array [
Object {
"assignmentType": "Homework",
"blockKey": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345",
"displayName": "First subsection",
"hasGradedAssignment": true,
"numPointsEarned": 0,
"numPointsPossible": 1,
"percentGraded": 0,
"showCorrectness": "always",
"showGrades": true,
"url": "http://learning.edx.org/course/course-v1:edX+Test+run/first_subsection",
},
],
},
Object {
"displayName": "Second section",
"subsections": Array [
Object {
"assignmentType": "Homework",
"displayName": "Second subsection",
"hasGradedAssignment": true,
"numPointsEarned": 1,
"numPointsPossible": 1,
"percentGraded": 1,
"showCorrectness": "always",
"showGrades": true,
"url": "http://learning.edx.org/course/course-v1:edX+Test+run/second_subsection",
},
],
},
],
"studioUrl": "http://studio.edx.org/settings/grading/course-v1:edX+Test+run",
"userHasPassingGrade": false,
"verificationData": Object {
"link": null,
"status": "none",
"statusDate": null,
},
"verifiedMode": null,
},
},
},
"recommendations": Object {
"recommendationsStatus": "loading",
},
}
`;

View File

@@ -3,6 +3,90 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { logInfo } from '@edx/frontend-platform/logging';
import { appendBrowserTimezoneToUrl } from '../../utils';
const calculateAssignmentTypeGrades = (points, assignmentWeight, numDroppable) => {
let dropCount = numDroppable;
// Drop the lowest grades
while (dropCount && points.length >= dropCount) {
const lowestScore = Math.min(...points);
const lowestScoreIndex = points.indexOf(lowestScore);
points.splice(lowestScoreIndex, 1);
dropCount--;
}
let averageGrade = 0;
let weightedGrade = 0;
if (points.length) {
averageGrade = points.reduce((a, b) => a + b, 0) / points.length;
weightedGrade = averageGrade * assignmentWeight;
}
return { averageGrade, weightedGrade };
};
function normalizeAssignmentPolicies(assignmentPolicies, sectionScores) {
const gradeByAssignmentType = {};
assignmentPolicies.forEach(assignment => {
// Create an array with the number of total assignments and set the scores to 0
// as placeholders for assignments that have not yet been released
gradeByAssignmentType[assignment.type] = {
grades: Array(assignment.numTotal).fill(0),
numAssignmentsCreated: 0,
numTotalExpectedAssignments: assignment.numTotal,
};
});
sectionScores.forEach((chapter) => {
chapter.subsections.forEach((subsection) => {
if (!(subsection.hasGradedAssignment && subsection.showGrades && subsection.numPointsPossible)) {
return;
}
const {
assignmentType,
numPointsEarned,
numPointsPossible,
} = subsection;
// If a subsection's assignment type does not match an assignment policy in Studio,
// we won't be able to include it in this accumulation of grades by assignment type.
// This may happen if a course author has removed/renamed an assignment policy in Studio and
// neglected to update the subsection's of that assignment type
if (!gradeByAssignmentType[assignmentType]) {
return;
}
let {
numAssignmentsCreated,
} = gradeByAssignmentType[assignmentType];
numAssignmentsCreated++;
if (numAssignmentsCreated <= gradeByAssignmentType[assignmentType].numTotalExpectedAssignments) {
// Remove a placeholder grade so long as the number of recorded created assignments is less than the number
// of expected assignments
gradeByAssignmentType[assignmentType].grades.shift();
}
// Add the graded assignment to the list
gradeByAssignmentType[assignmentType].grades.push(numPointsEarned ? numPointsEarned / numPointsPossible : 0);
// Record the created assignment
gradeByAssignmentType[assignmentType].numAssignmentsCreated = numAssignmentsCreated;
});
});
return assignmentPolicies.map((assignment) => {
const { averageGrade, weightedGrade } = calculateAssignmentTypeGrades(
gradeByAssignmentType[assignment.type].grades,
assignment.weight,
assignment.numDroppable,
);
return {
averageGrade,
numDroppable: assignment.numDroppable,
shortLabel: assignment.shortLabel,
type: assignment.type,
weight: assignment.weight,
weightedGrade,
};
});
}
function normalizeCourseHomeCourseMetadata(metadata) {
const data = camelCaseObject(metadata);
return {
@@ -118,8 +202,8 @@ export async function getDatesTabData(courseId) {
const { httpErrorStatus } = error && error.customAttributes;
if (httpErrorStatus === 404) {
global.location.replace(`${getConfig().LMS_BASE_URL}/courses/${courseId}/dates`);
return {};
}
// 401 can be returned for unauthenticated users or users who are not enrolled
if (httpErrorStatus === 401) {
global.location.replace(`${getConfig().BASE_URL}/course/${courseId}/home`);
}
@@ -132,13 +216,37 @@ export async function getProgressTabData(courseId) {
try {
const { data } = await getAuthenticatedHttpClient().get(url);
const camelCasedData = camelCaseObject(data);
camelCasedData.gradingPolicy.assignmentPolicies = normalizeAssignmentPolicies(
camelCasedData.gradingPolicy.assignmentPolicies,
camelCasedData.sectionScores,
);
// Accumulate the weighted grades by assignment type to calculate the learner facing grade. The grades within
// assignmentPolicies have been filtered by what's visible to the learner.
camelCasedData.courseGrade.visiblePercent = camelCasedData.gradingPolicy.assignmentPolicies
? camelCasedData.gradingPolicy.assignmentPolicies.reduce(
(accumulator, assignment) => accumulator + assignment.weightedGrade, 0,
) : camelCasedData.courseGrade.percent;
camelCasedData.courseGrade.isPassing = camelCasedData.courseGrade.visiblePercent
>= Math.min(...Object.values(data.grading_policy.grade_range));
// We replace gradingPolicy.gradeRange with the original data to preserve the intended casing for the grade.
// For example, if a grade range key is "A", we do not want it to be camel cased (i.e. "A" would become "a")
// in order to preserve a course team's desired grade formatting.
camelCasedData.gradingPolicy.gradeRange = data.grading_policy.grade_range;
return camelCasedData;
} catch (error) {
const { httpErrorStatus } = error && error.customAttributes;
if (httpErrorStatus === 404) {
global.location.replace(`${getConfig().LMS_BASE_URL}/courses/${courseId}/progress`);
}
// 401 can be returned for unauthenticated users or users who are not enrolled
if (httpErrorStatus === 401) {
global.location.replace(`${getConfig().BASE_URL}/course/${courseId}/home`);
}
throw error;
}
}
@@ -200,6 +308,7 @@ export async function getOutlineTabData(courseId) {
const accessExpiration = camelCaseObject(data.access_expiration);
const canShowUpgradeSock = data.can_show_upgrade_sock;
const certData = camelCaseObject(data.cert_data);
const courseBlocks = data.course_blocks ? normalizeOutlineBlocks(courseId, data.course_blocks.blocks) : {};
const courseGoals = camelCaseObject(data.course_goals);
const courseTools = camelCaseObject(data.course_tools);
@@ -217,6 +326,7 @@ export async function getOutlineTabData(courseId) {
return {
accessExpiration,
canShowUpgradeSock,
certData,
courseBlocks,
courseGoals,
courseTools,

View File

@@ -88,6 +88,35 @@ describe('Data layer integration tests', () => {
});
});
describe('Test fetchProgressTab', () => {
const progressBaseUrl = `${getConfig().LMS_BASE_URL}/api/course_home/v1/progress`;
it('Should result in fetch failure if error occurs', async () => {
axiosMock.onGet(courseMetadataUrl).networkError();
axiosMock.onGet(`${progressBaseUrl}/${courseId}`).networkError();
await executeThunk(thunks.fetchProgressTab(courseId), store.dispatch);
expect(loggingService.logError).toHaveBeenCalled();
expect(store.getState().courseHome.courseStatus).toEqual('failed');
});
it('Should fetch, normalize, and save metadata', async () => {
const progressTabData = Factory.build('progressTabData', { courseId });
const progressUrl = `${progressBaseUrl}/${courseId}`;
axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeMetadata);
axiosMock.onGet(progressUrl).reply(200, progressTabData);
await executeThunk(thunks.fetchProgressTab(courseId), store.dispatch);
const state = store.getState();
expect(state.courseHome.courseStatus).toEqual('loaded');
expect(state).toMatchSnapshot();
});
});
describe('Test saveCourseGoal', () => {
it('Should save course goal', async () => {
const goalUrl = `${getConfig().LMS_BASE_URL}/api/course_home/v1/save_course_goal`;

View File

@@ -10,6 +10,7 @@ const slice = createSlice({
initialState: {
courseStatus: 'loading',
courseId: null,
gradesFeatureIsLocked: false,
toastBodyText: null,
toastBodyLink: null,
toastHeader: '',
@@ -37,6 +38,9 @@ const slice = createSlice({
state.toastBodyText = linkText;
state.toastHeader = header;
},
setGradesFeatureStatus: (state, { payload }) => {
state.gradesFeatureIsLocked = payload.gradesFeatureIsLocked;
},
},
});
@@ -45,6 +49,7 @@ export const {
fetchTabSuccess,
fetchTabFailure,
setCallToActionToast,
setGradesFeatureStatus,
} = slice.actions;
export const {

View File

@@ -2,11 +2,12 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
export default function Badge({ children, className }) {
export default function Badge({ children, className, ...rest }) {
return (
<span
className={classNames('dates-badge small ml-2', className)}
className={classNames('dates-badge x-small ml-2 position-absolute', className)}
data-testid="dates-badge"
{...rest}
>
{children}
</span>

View File

@@ -1,4 +1,4 @@
.dates-badge {
border-radius: 4px;
padding: 2px 8px 3px 8px;
padding: 1px 8px;
}

View File

@@ -2,7 +2,12 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useSelector } from 'react-redux';
import { FormattedDate, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
FormattedDate,
FormattedTime,
injectIntl,
intlShape,
} from '@edx/frontend-platform/i18n';
import { Tooltip, OverlayTrigger } from '@edx/paragon';
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -51,7 +56,7 @@ function Day({
{/* Content */}
<div className="d-inline-block ml-3 pl-2">
<div className="mb-1" data-testid="dates-header">
<p className="d-inline text-dark-500 font-weight-bold">
<p className="d-inline text-dark-400">
<FormattedDate
/** [MM-P2P] Experiment */
value={mmp2pOverride ? mmp2p.state.upgradeDeadline : date}
@@ -70,16 +75,27 @@ function Day({
? getBadgeListAndColor(new Date(mmp2p.state.upgradeDeadline), intl, item, items)
: getBadgeListAndColor(date, intl, item, items);
const showDueDateTime = item.dateType === 'assignment-due-date';
const showLink = item.link && isLearnerAssignment(item);
const title = showLink ? (<u><a href={item.link} className="text-reset">{item.title}</a></u>) : item.title;
const available = item.learnerHasAccess && (item.link || !isLearnerAssignment(item));
const textColor = available ? 'text-dark-500' : 'text-dark-200';
const textColor = available ? 'text-primary-700' : 'text-gray-500';
return (
<div key={item.title + item.date} className={textColor} data-testid="dates-item">
<div key={item.title + item.date} className={classNames(textColor, 'small')} data-testid="dates-item">
<div>
<span className="font-weight-bold small mt-1">
{item.assignmentType && `${item.assignmentType}: `}{title}
<span className="small">
<span className="font-weight-bold">{item.assignmentType && `${item.assignmentType}: `}{title}</span>
{showDueDateTime && (
<span>
<span className="mx-1">due</span>
<FormattedTime
value={date}
timeZoneName="short"
{...timezoneFormatArgs}
/>
</span>
)}
</span>
{itemBadges}
{item.extraInfo && (

View File

@@ -77,6 +77,7 @@ function getBadgeListAndColor(date, intl, item, items) {
},
];
let color = null; // first color of any badge
const marginTopStyle = item ? { marginTop: 0 } : { marginTop: '2px' };
const badges = (
<>
{badgesInfo.map(b => {
@@ -96,7 +97,7 @@ function getBadgeListAndColor(date, intl, item, items) {
color = b.bg;
}
return (
<Badge key={b.message.id} className={classNames(b.bg, b.className)}>
<Badge key={b.message.id} style={marginTopStyle} className={classNames(b.bg, b.className)}>
{b.icon && <FontAwesomeIcon icon={b.icon} className="mr-1" />}
{intl.formatMessage(b.message)}
</Badge>

View File

@@ -1,4 +1,4 @@
import React, { useRef, useState } from 'react';
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import { sendTrackEvent, sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
@@ -9,7 +9,6 @@ import { AlertList } from '../../generic/user-messages';
import CourseDates from './widgets/CourseDates';
import CourseGoalCard from './widgets/CourseGoalCard';
import CourseHandouts from './widgets/CourseHandouts';
import CourseSock from '../../generic/course-sock';
import CourseTools from './widgets/CourseTools';
import DatesBannerContainer from '../dates-banner/DatesBannerContainer';
import { fetchOutlineTab } from '../data';
@@ -17,16 +16,16 @@ import genericMessages from '../../generic/messages';
import messages from './messages';
import Section from './Section';
import UpdateGoalSelector from './widgets/UpdateGoalSelector';
import UpgradeCard from './widgets/UpgradeCard';
import useAccessExpirationAlert from '../../alerts/access-expiration-alert';
import useCertificateAvailableAlert from './alerts/certificate-available-alert';
import UpgradeNotification from '../../generic/upgrade-notification/UpgradeNotification';
import { useAccessExpirationAlertMasquerade } from '../../alerts/access-expiration-alert';
import useCertificateAvailableAlert from './alerts/certificate-status-alert';
import useCourseEndAlert from './alerts/course-end-alert';
import useCourseStartAlert from './alerts/course-start-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';
import AccountActivationAlert from '../../alerts/logistration-alert/AccountActivationAlert';
/** [MM-P2P] Experiment */
import { initHomeMMP2P, MMP2PFlyover } from '../../experiments/mm-p2p';
@@ -44,7 +43,6 @@ function OutlineTab({ intl }) {
const {
accessExpiration,
canShowUpgradeSock,
courseBlocks: {
courses,
sections,
@@ -52,7 +50,7 @@ function OutlineTab({ intl }) {
courseGoals: {
goalOptions,
selectedGoal,
},
} = {},
datesBannerInfo,
datesWidget: {
courseDateBlocks,
@@ -86,8 +84,7 @@ function OutlineTab({ intl }) {
};
// Below the course title alerts (appearing in the order listed here)
const offerAlert = useOfferAlert(courseId, offer, org, userTimezone, 'outline-course-alerts', 'course_home');
const accessExpirationAlert = useAccessExpirationAlert(accessExpiration, courseId, org, userTimezone, 'outline-course-alerts', 'course_home');
const accessExpirationAlertMasquerade = useAccessExpirationAlertMasquerade(accessExpiration, userTimezone, 'outline-course-alerts');
const courseStartAlert = useCourseStartAlert(courseId);
const courseEndAlert = useCourseEndAlert(courseId);
const certificateAvailableAlert = useCertificateAvailableAlert(courseId);
@@ -95,8 +92,6 @@ function OutlineTab({ intl }) {
const rootCourseId = courses && Object.keys(courses)[0];
const courseSock = useRef(null);
const logUpgradeLinkClick = () => {
sendTrackEvent('edx.bi.ecommerce.upsell_links_clicked', {
...eventProperties,
@@ -125,7 +120,7 @@ function OutlineTab({ intl }) {
</div>
{resumeCourseUrl && (
<div className="col-12 col-sm-auto p-0">
<Button block href={resumeCourseUrl} onClick={() => logResumeCourseClick()}>
<Button variant="brand" block href={resumeCourseUrl} onClick={() => logResumeCourseClick()}>
{hasVisitedCourse ? intl.formatMessage(messages.resume) : intl.formatMessage(messages.start)}
</Button>
</div>
@@ -133,6 +128,7 @@ function OutlineTab({ intl }) {
</div>
{/** [MM-P2P] Experiment (className for optimizely trigger) */}
<div className="row course-outline-tab">
<AccountActivationAlert />
<div className="col-12">
<AlertList
topic="outline-private-alerts"
@@ -149,11 +145,10 @@ function OutlineTab({ intl }) {
topic="outline-course-alerts"
className="mb-3"
customAlerts={{
...accessExpirationAlert,
...accessExpirationAlertMasquerade,
...certificateAvailableAlert,
...courseEndAlert,
...courseStartAlert,
...offerAlert,
}}
/>
)}
@@ -224,7 +219,7 @@ function OutlineTab({ intl }) {
{ MMP2P.state.isEnabled
? <MMP2PFlyover isStatic options={MMP2P} />
: (
<UpgradeCard
<UpgradeNotification
offer={offer}
verifiedMode={verifiedMode}
accessExpiration={accessExpiration}
@@ -233,6 +228,7 @@ function OutlineTab({ intl }) {
timeOffsetMillis={timeOffsetMillis}
courseId={courseId}
org={org}
shouldDisplayBorder
/>
)}
<CourseDates
@@ -246,16 +242,6 @@ function OutlineTab({ intl }) {
</div>
)}
</div>
{canShowUpgradeSock && (
<CourseSock
courseId={courseId}
offer={offer}
orgKey={org}
pageLocation="Home Page"
ref={courseSock}
verifiedMode={verifiedMode}
/>
)}
</>
);
}

View File

@@ -4,6 +4,7 @@ import { getConfig } from '@edx/frontend-platform';
import { sendTrackEvent, sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import MockAdapter from 'axios-mock-adapter';
import Cookies from 'js-cookie';
import userEvent from '@testing-library/user-event';
import { buildMinimalCourseBlocks } from '../../shared/data/__factories__/courseBlocks.factory';
@@ -13,6 +14,7 @@ import {
import { appendBrowserTimezoneToUrl, executeThunk } from '../../utils';
import * as thunks from '../data/thunks';
import initializeStore from '../../store';
import { CERT_STATUS_TYPE } from './alerts/certificate-status-alert/CertificateStatusAlert';
import OutlineTab from './OutlineTab';
initializeMockApp();
@@ -281,13 +283,13 @@ describe('Outline Tab', () => {
],
});
await fetchAndRender();
expect(screen.getByRole('heading', { name: 'Upcoming Dates' })).toBeInTheDocument();
expect(screen.getByRole('heading', { name: 'Important dates' })).toBeInTheDocument();
});
it('does not render when course date blocks are not populated', async () => {
setMetadata({ is_enrolled: true });
await fetchAndRender();
expect(screen.queryByRole('heading', { name: 'Upcoming Dates' })).not.toBeInTheDocument();
expect(screen.queryByRole('heading', { name: 'Important dates' })).not.toBeInTheDocument();
});
it('sends analytics event onClick of upgrade link', async () => {
@@ -529,60 +531,22 @@ describe('Outline Tab', () => {
},
});
await fetchAndRender();
await screen.findByText('This learner does not have access to this course.', { exact: false });
const check = await screen.queryByText('This learner does not have access to this course.', { exact: false });
expect(check).toBeInTheDocument();
});
it('shows expiration', async () => {
it('does not have special masquerade text', async () => {
setTabData({
access_expiration: {
expiration_date: '2080-01-01T12:00:00Z',
expiration_date: '2020-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 });
});
it('sends analytics event onClick of upgrade link', 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();
// Clearing after render to remove any events sent on view (ex. 'Promotion Viewed')
sendTrackEvent.mockClear();
const upgradeLink = screen.getByRole('link', { name: 'Upgrade now' });
fireEvent.click(upgradeLink);
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.ecommerce.upsell_links_clicked', {
org_key: 'edX',
courserun_key: courseId,
linkCategory: 'FBE_banner',
linkName: 'course_home_audit_access_expires',
linkType: 'link',
pageName: 'course_home',
});
const check = await screen.queryByText('This learner does not have access to this course.', { exact: false });
expect(check).not.toBeInTheDocument();
});
});
@@ -671,7 +635,14 @@ describe('Outline Tab', () => {
const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
setMetadata({ is_enrolled: true });
setTabData({}, {
setTabData({
cert_data: {
cert_status: CERT_STATUS_TYPE.EARNED_NOT_AVAILABLE,
cert_web_view_url: null,
certificate_available_date: tomorrow.toISOString(),
download_url: null,
},
}, {
date_blocks: [
{
date_type: 'course-end-date',
@@ -686,55 +657,95 @@ describe('Outline Tab', () => {
],
});
await fetchAndRender();
await screen.findByText('We are working on generating course certificates.');
expect(screen.queryByText('Your grade and certificate will be ready soon!')).toBeInTheDocument();
});
it('renders verification alert', async () => {
const now = new Date();
const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
setMetadata({ is_enrolled: true });
setTabData({
cert_data: {
cert_status: CERT_STATUS_TYPE.UNVERIFIED,
cert_web_view_url: null,
download_url: null,
},
}, {
date_blocks: [
{
date_type: 'course-end-date',
date: yesterday.toISOString(),
title: 'End',
},
{
date_type: 'certificate-available-date',
date: tomorrow.toISOString(),
title: 'Cert Available',
},
{
date_type: 'verification-deadline-date',
date: tomorrow.toISOString(),
link_text: 'Verify',
title: 'Verification Upgrade Deadline',
},
],
});
await fetchAndRender();
expect(screen.queryByText('Verify your identity to earn a certificate!')).toBeInTheDocument();
});
});
});
describe('Offer Alert', () => {
it('sends analytics event onClick of upgrade link', async () => {
setTabData({
offer: {
code: 'EDXWELCOME',
expiration_date: '2070-01-01T12:00:00Z',
original_price: '$100',
discounted_price: '$85',
percentage: 15,
upgrade_url: 'https://example.com/upgrade',
describe('Certificate (web) Complete Alert', () => {
it('appears', async () => {
const now = new Date();
const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
setMetadata({ is_enrolled: true });
setTabData({
cert_data: {
cert_status: CERT_STATUS_TYPE.DOWNLOADABLE,
cert_web_view_url: 'certificate/testuuid',
certificate_available_date: null,
download_url: null,
},
}, {
date_blocks: [
{
date_type: 'course-end-date',
date: yesterday.toISOString(),
title: 'End',
},
});
await fetchAndRender();
expect(screen.getByRole('link', { name: 'Upgrade now' })).toBeInTheDocument();
],
});
await fetchAndRender();
expect(screen.queryByText('Congratulations! Your certificate is ready.')).toBeInTheDocument();
});
});
it('sends analytics event onClick of upgrade link', async () => {
setTabData({
offer: {
code: 'EDXWELCOME',
expiration_date: '2070-01-01T12:00:00Z',
original_price: '$100',
discounted_price: '$85',
percentage: 15,
upgrade_url: 'https://example.com/upgrade',
describe('Certificate (pdf) Complete Alert', () => {
it('appears', async () => {
const now = new Date();
const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
setMetadata({ is_enrolled: true });
setTabData({
cert_data: {
cert_status: CERT_STATUS_TYPE.DOWNLOADABLE,
cert_web_view_url: null,
certificate_available_date: null,
download_url: 'download/url',
},
}, {
date_blocks: [
{
date_type: 'course-end-date',
date: yesterday.toISOString(),
title: 'End',
},
});
await fetchAndRender();
// Clearing after render to remove any events sent on view (ex. 'Promotion Viewed')
sendTrackEvent.mockClear();
const upgradeLink = screen.getByRole('link', { name: 'Upgrade now' });
fireEvent.click(upgradeLink);
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.ecommerce.upsell_links_clicked', {
org_key: 'edX',
courserun_key: courseId,
linkCategory: 'welcome',
linkName: 'course_home_welcome',
linkType: 'link',
pageName: 'course_home',
});
],
});
await fetchAndRender();
expect(screen.queryByText('Congratulations! Your certificate is ready.')).toBeInTheDocument();
expect(screen.queryByRole('link', { name: 'Download my certificate' })).toBeInTheDocument();
});
});
@@ -964,4 +975,51 @@ describe('Outline Tab', () => {
});
});
});
describe('Accont Activation Alert', () => {
beforeEach(() => {
const intersectionObserverMock = () => ({
observe: () => null,
disconnect: () => null,
});
window.IntersectionObserver = jest.fn().mockImplementation(intersectionObserverMock);
});
it('displays account activation alert if cookie is set true', async () => {
Cookies.set = jest.fn();
Cookies.get = jest.fn().mockImplementation(() => 'true');
Cookies.remove = jest.fn().mockImplementation(() => { Cookies.get = jest.fn(); });
await fetchAndRender();
expect(screen.queryByText('Activate your account so you can log back in')).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'resend the email' })).toBeInTheDocument();
});
it('do not displays account activation alert if cookie is not set true', async () => {
Cookies.set = jest.fn();
Cookies.get = jest.fn();
Cookies.remove = jest.fn().mockImplementation(() => { Cookies.get = jest.fn(); });
await fetchAndRender();
expect(screen.queryByText('Activate your account so you can log back in')).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'resend the email' })).not.toBeInTheDocument();
});
it('sends account activation email on clicking the resened email in account activation alert', async () => {
Cookies.set = jest.fn();
Cookies.get = jest.fn().mockImplementation(() => 'true');
Cookies.remove = jest.fn().mockImplementation(() => { Cookies.get = jest.fn(); });
await fetchAndRender();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
const resendEmailUrl = `${getConfig().LMS_BASE_URL}/api/send_account_activation_email`;
axiosMock.onPost(resendEmailUrl).reply(200, {});
const resendLink = screen.getByRole('button', { name: 'resend the email' });
fireEvent.click(resendLink);
await waitFor(() => expect(axiosMock.history.post).toHaveLength(1));
expect(axiosMock.history.post[0].url).toEqual(resendEmailUrl);
});
});
});

View File

@@ -1,62 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { FormattedMessage, FormattedRelative } from '@edx/frontend-platform/i18n';
import { Hyperlink } from '@edx/paragon';
import { Alert, ALERT_TYPES } from '../../../../generic/user-messages';
function CertificateAvailableAlert({ payload }) {
const {
certDate,
username,
userTimezone,
} = payload;
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
return (
<Alert type={ALERT_TYPES.INFO}>
<strong>
<FormattedMessage
id="learning.outline.alert.cert.title"
defaultMessage="We are working on generating course certificates."
/>
</strong>
<br />
<FormattedMessage
id="learning.outline.alert.cert.when"
defaultMessage="If you have earned a certificate, you will be able to access it {timeRemaining}. You will also be able to view your certificates on your {profileLink}."
values={{
profileLink: (
<Hyperlink
destination={`${getConfig().LMS_BASE_URL}/u/${username}`}
>
<FormattedMessage
id="learning.outline.alert.cert.profile"
defaultMessage="Learner Profile"
/>
</Hyperlink>
),
timeRemaining: (
<FormattedRelative
key="timeRemaining"
value={certDate}
{...timezoneFormatArgs}
/>
),
}}
/>
</Alert>
);
}
CertificateAvailableAlert.propTypes = {
payload: PropTypes.shape({
certDate: PropTypes.string,
username: PropTypes.string,
userTimezone: PropTypes.string,
}).isRequired,
};
export default CertificateAvailableAlert;

View File

@@ -1,44 +0,0 @@
import React, { useMemo } from 'react';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { useAlert } from '../../../../generic/user-messages';
import { useModel } from '../../../../generic/model-store';
const CertificateAvailableAlert = React.lazy(() => import('./CertificateAvailableAlert'));
function useCertificateAvailableAlert(courseId) {
const {
isEnrolled,
} = useModel('courseHomeMeta', courseId);
const {
datesWidget: {
courseDateBlocks,
userTimezone,
},
} = useModel('outline', courseId);
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');
const endDate = endBlock ? new Date(endBlock.date) : null;
const hasEnded = endBlock ? endDate < new Date() : false;
const isVisible = isEnrolled && certBlock && hasEnded; // only show if we're between end and cert dates
const payload = {
certDate: certBlock && certBlock.date,
username,
userTimezone,
};
useAlert(isVisible, {
code: 'clientCertificateAvailableAlert',
payload: useMemo(() => payload, Object.values(payload).sort()),
topic: 'outline-course-alerts',
});
return {
clientCertificateAvailableAlert: CertificateAvailableAlert,
};
}
export default useCertificateAvailableAlert;

View File

@@ -0,0 +1,150 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
FormattedDate,
FormattedMessage,
injectIntl,
intlShape,
} from '@edx/frontend-platform/i18n';
import { Alert, Button } from '@edx/paragon';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCheckCircle, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
import { getConfig } from '@edx/frontend-platform';
import certMessages from './messages';
import certStatusMessages from '../../../progress-tab/certificate-status/messages';
export const CERT_STATUS_TYPE = {
EARNED_NOT_AVAILABLE: 'earned_but_not_available',
DOWNLOADABLE: 'downloadable',
UNVERIFIED: 'unverified',
};
function CertificateStatusAlert({ intl, payload }) {
const {
certificateAvailableDate,
certStatusType,
courseEndDate,
certURL,
isWebCert,
userTimezone,
} = payload;
// eslint-disable-next-line react/prop-types
const AlertWrapper = (props) => props.children(props);
const renderCertAwardedStatus = () => {
const alertProps = {
variant: 'success',
icon: faCheckCircle,
iconClassName: 'alert-icon text-success-500',
};
if (certStatusType === CERT_STATUS_TYPE.EARNED_NOT_AVAILABLE) {
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
const certificateAvailableDateFormatted = <FormattedDate value={certificateAvailableDate} day="numeric" month="long" year="numeric" />;
const courseEndDateFormatted = <FormattedDate value={courseEndDate} day="numeric" month="long" year="numeric" />;
alertProps.header = intl.formatMessage(certMessages.certStatusEarnedNotAvailableHeader);
alertProps.body = (
<p>
<FormattedMessage
id="learning.outline.alert.cert.when"
defaultMessage="This course ended on {courseEndDateFormatted}. Final grades and certificates are
scheduled to be available after {certificateAvailableDate}."
values={{
courseEndDateFormatted,
certificateAvailableDate: certificateAvailableDateFormatted,
}}
{...timezoneFormatArgs}
/>
</p>
);
} else if (certStatusType === CERT_STATUS_TYPE.DOWNLOADABLE) {
alertProps.header = intl.formatMessage(certMessages.certStatusDownloadableHeader);
if (isWebCert) {
alertProps.buttonMessage = intl.formatMessage(certStatusMessages.viewableButton);
} else {
alertProps.buttonMessage = intl.formatMessage(certStatusMessages.downloadableButton);
}
alertProps.buttonVisible = true;
alertProps.buttonLink = certURL;
}
return alertProps;
};
const renderNotIDVerifiedStatus = () => {
const alertProps = {
variant: 'warning',
icon: faExclamationTriangle,
iconClassName: 'alert-icon text-warning-500',
header: intl.formatMessage(certStatusMessages.unverifiedHomeHeader),
buttonMessage: intl.formatMessage(certStatusMessages.unverifiedHomeButton),
body: intl.formatMessage(certStatusMessages.unverifiedHomeBody),
buttonVisible: true,
buttonLink: getConfig().SUPPORT_URL_ID_VERIFICATION,
};
return alertProps;
};
let alertProps = {};
switch (certStatusType) {
case CERT_STATUS_TYPE.EARNED_NOT_AVAILABLE:
case CERT_STATUS_TYPE.DOWNLOADABLE:
alertProps = renderCertAwardedStatus();
break;
case CERT_STATUS_TYPE.UNVERIFIED:
alertProps = renderNotIDVerifiedStatus();
break;
default:
break;
}
return (
<AlertWrapper {...alertProps}>
{({
variant,
buttonVisible,
iconClassName,
icon,
header,
buttonLink,
body,
buttonMessage,
}) => (
<Alert variant={variant}>
<div className="row justify-content-between align-items-center">
<div className={buttonVisible ? 'col-lg-8' : 'col-auto'}>
<FontAwesomeIcon icon={icon} className={iconClassName} />
<Alert.Heading>{header}</Alert.Heading>
{body}
</div>
{buttonVisible && (
<div className="m-auto m-lg-0 pr-lg-3">
<Button
variant="primary"
href={buttonLink}
>
{buttonMessage}
</Button>
</div>
)}
</div>
</Alert>
)}
</AlertWrapper>
);
}
CertificateStatusAlert.propTypes = {
intl: intlShape.isRequired,
payload: PropTypes.shape({
certificateAvailableDate: PropTypes.string,
certStatusType: PropTypes.string,
courseEndDate: PropTypes.string,
certURL: PropTypes.string,
isWebCert: PropTypes.bool,
userTimezone: PropTypes.string,
}).isRequired,
};
export default injectIntl(CertificateStatusAlert);

View File

@@ -0,0 +1,78 @@
import React, { useMemo } from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useAlert } from '../../../../generic/user-messages';
import { useModel } from '../../../../generic/model-store';
import { CERT_STATUS_TYPE } from './CertificateStatusAlert';
const CertificateStatusAlert = React.lazy(() => import('./CertificateStatusAlert'));
function verifyCertStatusType(status) {
// This method will only return cert statuses when we want to alert on them.
// It should be modified when we want to alert on a new status type.
if (status === CERT_STATUS_TYPE.DOWNLOADABLE) {
return CERT_STATUS_TYPE.DOWNLOADABLE;
}
if (status === CERT_STATUS_TYPE.EARNED_NOT_AVAILABLE) {
return CERT_STATUS_TYPE.EARNED_NOT_AVAILABLE;
}
if (status === CERT_STATUS_TYPE.UNVERIFIED) {
return CERT_STATUS_TYPE.UNVERIFIED;
}
return '';
}
function useCertificateStatusAlert(courseId) {
const {
isEnrolled,
} = useModel('courseHomeMeta', courseId);
const {
datesWidget: {
courseDateBlocks,
userTimezone,
},
certData,
} = useModel('outline', courseId);
const {
certStatus,
certWebViewUrl,
certificateAvailableDate,
downloadUrl,
} = certData || {};
const endBlock = courseDateBlocks.find(b => b.dateType === 'course-end-date');
const certStatusType = verifyCertStatusType(certStatus);
const isWebCert = downloadUrl === null;
let certURL = '';
if (certWebViewUrl) {
certURL = `${getConfig().LMS_BASE_URL}${certWebViewUrl}`;
} else if (downloadUrl) {
// PDF Certificate
certURL = downloadUrl;
}
const hasCertStatus = certStatusType !== '';
// Only show if there is a known cert status that we want provide status on.
const isVisible = isEnrolled && hasCertStatus;
const payload = {
certificateAvailableDate,
certURL,
certStatusType,
courseEndDate: endBlock && endBlock.date,
userTimezone,
isWebCert,
};
useAlert(isVisible, {
code: 'clientCertificateStatusAlert',
payload: useMemo(() => payload, Object.values(payload).sort()),
topic: 'outline-course-alerts',
});
return {
clientCertificateStatusAlert: CertificateStatusAlert,
};
}
export default useCertificateStatusAlert;

View File

@@ -0,0 +1,16 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
certStatusEarnedNotAvailableHeader: {
id: 'cert.alert.earned.unavailable.header',
defaultMessage: 'Your grade and certificate will be ready soon!',
description: 'Header alerting the user that their certificate will be available soon.',
},
certStatusDownloadableHeader: {
id: 'cert.alert.earned.ready.header',
defaultMessage: 'Congratulations! Your certificate is ready.',
description: 'Header alerting the user that their certificate is ready.',
},
});
export default messages;

View File

@@ -22,7 +22,7 @@ const messages = defineMessages({
},
dates: {
id: 'learning.outline.dates',
defaultMessage: 'Upcoming Dates',
defaultMessage: 'Important dates',
},
editGoal: {
id: 'learning.outline.editGoal',

View File

@@ -1,60 +0,0 @@
.upgrade-card {
border-radius: 0 !important;
}
.upgrade-card-header{
margin: 1.25rem;
}
.upsell-warning{
background-color: $danger-100;
}
.upsell-warning-light{
background-color: $warning-100;
}
.upsell-warning, .upsell-warning-light{
padding-left: 1.25rem;
padding-right: 1.25rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.upgrade-card-ul{
margin-left: 3rem;
padding-top: 0.875rem;
padding-right: 1.25rem;
}
.upgrade-card-li{
left: -2.125rem;
top: 0 !important;
}
.upgrade-card-text{
padding-top: 0.875rem;
padding-right: 1.25rem;
padding-left: 1.25rem;
}
.upgrade-card-button{
margin-left: 1.25rem;
margin-right: 1.25rem;
margin-bottom: 1.25rem;
}
.discount-info {
border-top: 1px solid rgba(0, 0, 0, 0.125);
padding-top: .75rem;
padding-bottom: .75rem;
}
.inline-link-underline {
text-decoration: underline;
}
.upgrade-card .upgrade-card-message a{
color: $primary-500;
}

View File

@@ -1,6 +1,6 @@
import React from 'react';
import React, { useEffect } from 'react';
import { layoutGenerator } from 'react-break';
import { useSelector } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import CertificateStatus from './certificate-status/CertificateStatus';
import CourseCompletion from './course-completion/CourseCompletion';
@@ -10,6 +10,7 @@ import GradeSummary from './grades/grade-summary/GradeSummary';
import ProgressHeader from './ProgressHeader';
import RelatedLinks from './related-links/RelatedLinks';
import { setGradesFeatureStatus } from '../data/slice';
import { useModel } from '../../generic/model-store';
function ProgressTab() {
@@ -22,8 +23,14 @@ function ProgressTab() {
lockedCount,
},
} = useModel('progress', courseId);
const isLocked = lockedCount > 0;
const applyLockedOverlay = isLocked ? 'locked-overlay' : '';
const gradesFeatureIsLocked = lockedCount > 0;
const applyLockedOverlay = gradesFeatureIsLocked ? 'locked-overlay' : '';
const dispatch = useDispatch();
useEffect(() => {
dispatch(setGradesFeatureStatus({ gradesFeatureIsLocked }));
}, []);
const layout = layoutGenerator({
mobile: 0,
@@ -43,7 +50,7 @@ function ProgressTab() {
<CertificateStatus />
</OnMobile>
<CourseGrade />
<div className={`grades my-4 p-4 rounded shadow-sm ${applyLockedOverlay}`} aria-hidden={isLocked}>
<div className={`grades my-4 p-4 rounded shadow-sm ${applyLockedOverlay}`} aria-hidden={gradesFeatureIsLocked}>
<GradeSummary />
<DetailedGrades />
</div>

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { Factory } from 'rosie';
import { getConfig } from '@edx/frontend-platform';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import MockAdapter from 'axios-mock-adapter';
@@ -52,6 +53,42 @@ describe('Progress Tab', () => {
logUnhandledRequests(axiosMock);
});
describe('Related links', () => {
beforeEach(() => {
sendTrackEvent.mockClear();
});
it('sends event on click of dates tab link', async () => {
await fetchAndRender();
const datesTabLink = screen.getByRole('link', { name: 'Dates' });
fireEvent.click(datesTabLink);
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.course_progress.related_links.clicked', {
org_key: 'edX',
courserun_key: courseId,
is_staff: false,
link_clicked: 'dates',
});
});
it('sends event on click of outline tab link', async () => {
await fetchAndRender();
const outlineTabLink = screen.getAllByRole('link', { name: 'Course Outline' });
fireEvent.click(outlineTabLink[1]); // outlineTabLink[0] corresponds to the link in the DetailedGrades component
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.course_progress.related_links.clicked', {
org_key: 'edX',
courserun_key: courseId,
is_staff: false,
link_clicked: 'course_outline',
});
});
});
describe('Course Grade', () => {
it('renders Course Grade', async () => {
await fetchAndRender();
@@ -59,26 +96,45 @@ describe('Progress Tab', () => {
expect(screen.getByText('This represents your weighted grade against the grade needed to pass this course.')).toBeInTheDocument();
});
it('renders correct copy for non-passing', async () => {
it('renders correct copy in CourseGradeFooter for non-passing', async () => {
setTabData({
course_grade: {
is_passing: false,
letter_grade: null,
percent: 0.5,
},
section_scores: [
{
display_name: 'First section',
subsections: [
{
assignment_type: 'Homework',
block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345',
display_name: 'First subsection',
has_graded_assignment: true,
num_points_earned: 1,
num_points_possible: 2,
percent_graded: 0.0,
show_correctness: 'always',
show_grades: true,
url: 'http://learning.edx.org/course/course-v1:edX+Test+run/first_subsection',
},
],
},
],
});
await fetchAndRender();
expect(screen.queryByRole('button', { name: 'Grade range tooltip' })).not.toBeInTheDocument();
expect(screen.getByText('A weighted grade of 75% is required to pass in this course')).toBeInTheDocument();
});
it('renders correct copy for passing with pass/fail grade range', async () => {
setTabData({
course_grade: {
is_passing: true,
letter_grade: 'Pass',
percent: 0.9,
},
});
it('renders correct copy in CourseGradeFooter for passing with pass/fail grade range', async () => {
await fetchAndRender();
expect(screen.queryByRole('button', { name: 'Grade range tooltip' })).not.toBeInTheDocument();
expect(screen.getByText('Youre currently passing this course')).toBeInTheDocument();
});
it('renders correct copy and tooltip for non-passing with letter grade range', async () => {
it('renders correct copy and tooltip in CourseGradeFooter for non-passing with letter grade range', async () => {
setTabData({
course_grade: {
is_passing: false,
@@ -105,13 +161,32 @@ describe('Progress Tab', () => {
expect(screen.getByText('A weighted grade of 80% is required to pass in this course')).toBeInTheDocument();
});
it('renders correct copy and tooltip for passing with letter grade range', async () => {
it('renders correct copy and tooltip in CourseGradeFooter for passing with letter grade range', async () => {
setTabData({
course_grade: {
is_passing: true,
letter_grade: 'B',
percent: 0.85,
percent: 0.8,
},
section_scores: [
{
display_name: 'First section',
subsections: [
{
assignment_type: 'Homework',
block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345',
display_name: 'First subsection',
has_graded_assignment: true,
num_points_earned: 8,
num_points_possible: 10,
percent_graded: 1.0,
show_correctness: 'always',
show_grades: true,
url: 'http://learning.edx.org/course/course-v1:edX+Test+run/first_subsection',
},
],
},
],
grading_policy: {
assignment_policies: [
{
@@ -132,7 +207,7 @@ describe('Progress Tab', () => {
expect(await screen.findByText('Youre currently passing this course with a grade of B (80-90%)')).toBeInTheDocument();
});
it('renders tooltip for grade range', async () => {
it('renders tooltip in CourseGradeFooter for grade range', async () => {
setTabData({
course_grade: {
percent: 0,
@@ -162,7 +237,61 @@ describe('Progress Tab', () => {
expect(screen.getByText('F: <80%'));
});
it('renders locked feature preview when user has locked content', async () => {
it('renders locked feature preview (CourseGradeHeader) with upgrade button when user has locked content', async () => {
setTabData({
completion_summary: {
complete_count: 1,
incomplete_count: 1,
locked_count: 1,
},
verified_mode: {
access_expiration_date: '2050-01-01T12:00:00',
currency: 'USD',
currency_symbol: '$',
price: 149,
sku: 'ABCD1234',
upgrade_url: 'edx.org/upgrade',
},
});
await fetchAndRender();
expect(screen.getByText('locked feature')).toBeInTheDocument();
expect(screen.getByText('Unlock to view grades and work towards a certificate.')).toBeInTheDocument();
expect(screen.getAllByRole('link', 'Unlock now')).toHaveLength(3);
});
it('sends event on click of upgrade button in locked content header (CourseGradeHeader)', async () => {
sendTrackEvent.mockClear();
setTabData({
completion_summary: {
complete_count: 1,
incomplete_count: 1,
locked_count: 1,
},
verified_mode: {
access_expiration_date: '2050-01-01T12:00:00',
currency: 'USD',
currency_symbol: '$',
price: 149,
sku: 'ABCD1234',
upgrade_url: 'edx.org/upgrade',
},
});
await fetchAndRender();
expect(screen.getByText('locked feature')).toBeInTheDocument();
expect(screen.getByText('Unlock to view grades and work towards a certificate.')).toBeInTheDocument();
const upgradeButton = screen.getAllByRole('link', 'Unlock now')[0];
fireEvent.click(upgradeButton);
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.course_progress.grades_upgrade.clicked', {
org_key: 'edX',
courserun_key: courseId,
is_staff: false,
});
});
it('renders locked feature preview with no upgrade button when user has locked content but cannot upgrade', async () => {
setTabData({
completion_summary: {
complete_count: 1,
@@ -172,11 +301,61 @@ describe('Progress Tab', () => {
});
await fetchAndRender();
expect(screen.getByText('locked feature')).toBeInTheDocument();
expect(screen.getByText('The deadline to upgrade in this course has passed.')).toBeInTheDocument();
});
it('does not render locked feature preview when user does not have locked content', async () => {
await fetchAndRender();
expect(screen.queryByText('locked feature')).not.toBeInTheDocument();
});
it('renders correct current grade tooltip when showGrades is false', async () => {
// The learner has a 50% on the first assignment and a 100% on the second, making their grade a 75%
// The second assignment has showGrades set to false, so the grade reflected to the learner should be 50%.
setTabData({
section_scores: [
{
display_name: 'First section',
subsections: [
{
assignment_type: 'Homework',
block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345',
display_name: 'First subsection',
has_graded_assignment: true,
num_points_earned: 1,
num_points_possible: 2,
percent_graded: 1.0,
show_correctness: 'always',
show_grades: true,
url: 'http://learning.edx.org/course/course-v1:edX+Test+run/first_subsection',
},
],
},
{
display_name: 'Second section',
subsections: [
{
assignment_type: 'Homework',
display_name: 'Second subsection',
has_graded_assignment: true,
num_points_earned: 1,
num_points_possible: 1,
percent_graded: 1.0,
show_correctness: 'always',
show_grades: false,
url: 'http://learning.edx.org/course/course-v1:edX+Test+run/second_subsection',
},
],
},
],
});
await fetchAndRender();
expect(screen.getByTestId('currentGradeTooltipContent').innerHTML).toEqual('50%');
// Although the learner's true grade is passing, we should expect this to reflect the grade that's
// visible to them, which is non-passing
expect(screen.getByText('A weighted grade of 75% is required to pass in this course')).toBeInTheDocument();
});
});
describe('Grade Summary', () => {
@@ -189,11 +368,185 @@ describe('Progress Tab', () => {
setTabData({
grading_policy: {
assignment_policies: [],
grade_range: {
pass: 0.75,
},
},
section_scores: [],
});
await fetchAndRender();
expect(screen.queryByText('Grade summary')).not.toBeInTheDocument();
});
it('calculates grades correctly when number of droppable assignments equals total number of assignments', async () => {
setTabData({
grading_policy: {
assignment_policies: [
{
num_droppable: 2,
num_total: 2,
short_label: 'HW',
type: 'Homework',
weight: 1,
},
],
grade_range: {
pass: 0.75,
},
},
});
await fetchAndRender();
expect(screen.getByText('Grade summary')).toBeInTheDocument();
// The row is comprised of "{Assignment type} {footnote - optional} {weight} {grade} {weighted grade}"
expect(screen.getByRole('row', { name: 'Homework 1 100% 0% 0%' })).toBeInTheDocument();
});
it('calculates grades correctly when number of droppable assignments is less than total number of assignments', async () => {
await fetchAndRender();
expect(screen.getByText('Grade summary')).toBeInTheDocument();
// The row is comprised of "{Assignment type} {footnote - optional} {weight} {grade} {weighted grade}"
expect(screen.getByRole('row', { name: 'Homework 1 100% 100% 100%' })).toBeInTheDocument();
});
it('calculates grades correctly when number of droppable assignments is zero', async () => {
setTabData({
grading_policy: {
assignment_policies: [
{
num_droppable: 0,
num_total: 2,
short_label: 'HW',
type: 'Homework',
weight: 1,
},
],
grade_range: {
pass: 0.75,
},
},
});
await fetchAndRender();
expect(screen.getByText('Grade summary')).toBeInTheDocument();
// The row is comprised of "{Assignment type} {weight} {grade} {weighted grade}"
expect(screen.getByRole('row', { name: 'Homework 100% 50% 50%' })).toBeInTheDocument();
});
it('calculates grades correctly when number of total assignments is less than the number of assignments created', async () => {
setTabData({
grading_policy: {
assignment_policies: [
{
num_droppable: 1,
num_total: 1, // two assignments created in the factory, but 1 is expected per Studio settings
short_label: 'HW',
type: 'Homework',
weight: 1,
},
],
grade_range: {
pass: 0.75,
},
},
});
await fetchAndRender();
expect(screen.getByText('Grade summary')).toBeInTheDocument();
// The row is comprised of "{Assignment type} {footnote - optional} {weight} {grade} {weighted grade}"
expect(screen.getByRole('row', { name: 'Homework 1 100% 100% 100%' })).toBeInTheDocument();
});
it('calculates grades correctly when number of total assignments is greater than the number of assignments created', async () => {
setTabData({
grading_policy: {
assignment_policies: [
{
num_droppable: 0,
num_total: 5, // two assignments created in the factory, but 5 are expected per Studio settings
short_label: 'HW',
type: 'Homework',
weight: 1,
},
],
grade_range: {
pass: 0.75,
},
},
});
await fetchAndRender();
expect(screen.getByText('Grade summary')).toBeInTheDocument();
// The row is comprised of "{Assignment type} {weight} {grade} {weighted grade}"
expect(screen.getByRole('row', { name: 'Homework 100% 20% 20%' })).toBeInTheDocument();
});
it('calculates weighted grades correctly', async () => {
setTabData({
grading_policy: {
assignment_policies: [
{
num_droppable: 1,
num_total: 2,
short_label: 'HW',
type: 'Homework',
weight: 0.5,
},
{
num_droppable: 0,
num_total: 1,
short_label: 'Ex',
type: 'Exam',
weight: 0.5,
},
],
grade_range: {
pass: 0.75,
},
},
});
await fetchAndRender();
expect(screen.getByText('Grade summary')).toBeInTheDocument();
// The row is comprised of "{Assignment type} {footnote - optional} {weight} {grade} {weighted grade}"
expect(screen.getByRole('row', { name: 'Homework 1 50% 100% 50%' })).toBeInTheDocument();
expect(screen.getByRole('row', { name: 'Exam 50% 0% 0%' })).toBeInTheDocument();
});
it('renders correct total weighted grade when showGrades is false', async () => {
// The learner has a 50% on the first assignment and a 100% on the second, making their grade a 75%
// The second assignment has showGrades set to false, so the grade reflected to the learner should be 50%.
setTabData({
section_scores: [
{
display_name: 'First section',
subsections: [
{
assignment_type: 'Homework',
block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345',
display_name: 'First subsection',
has_graded_assignment: true,
num_points_earned: 1,
num_points_possible: 2,
percent_graded: 1.0,
show_correctness: 'always',
show_grades: true,
url: 'http://learning.edx.org/course/course-v1:edX+Test+run/first_subsection',
},
],
},
{
display_name: 'Second section',
subsections: [
{
assignment_type: 'Homework',
display_name: 'Second subsection',
has_graded_assignment: true,
num_points_earned: 1,
num_points_possible: 1,
percent_graded: 1.0,
show_correctness: 'always',
show_grades: false,
url: 'http://learning.edx.org/course/course-v1:edX+Test+run/second_subsection',
},
],
},
],
});
await fetchAndRender();
expect(screen.getByTestId('gradeSummaryFooterTotalWeightedGrade').innerHTML).toEqual('50%');
});
});
describe('Detailed Grades', () => {
@@ -205,6 +558,39 @@ describe('Progress Tab', () => {
expect(screen.getByRole('link', { name: 'Second subsection' }));
});
it('sends event on click of subsection link', async () => {
sendTrackEvent.mockClear();
await fetchAndRender();
expect(screen.getByText('Detailed grades')).toBeInTheDocument();
const subsectionLink = screen.getByRole('link', { name: 'First subsection' });
fireEvent.click(subsectionLink);
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.course_progress.detailed_grades_assignment.clicked', {
org_key: 'edX',
courserun_key: courseId,
is_staff: false,
assignment_block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345',
});
});
it('sends event on click of course outline link', async () => {
sendTrackEvent.mockClear();
await fetchAndRender();
expect(screen.getByText('Detailed grades')).toBeInTheDocument();
const outlineLink = screen.getAllByRole('link', { name: 'Course Outline' })[0];
fireEvent.click(outlineLink);
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.course_progress.detailed_grades.course_outline_link.clicked', {
org_key: 'edX',
courserun_key: courseId,
is_staff: false,
});
});
it('render message when section scores are not populated', async () => {
setTabData({
section_scores: [],
@@ -238,6 +624,7 @@ describe('Progress Tab', () => {
describe('enrolled user', () => {
beforeEach(async () => {
setMetadata({ is_enrolled: true });
sendTrackEvent.mockClear();
});
it('Displays text for nonPassing case when learner does not have a passing grade', async () => {
@@ -245,6 +632,20 @@ describe('Progress Tab', () => {
expect(screen.getByText('In order to qualify for a certificate, you must have a passing grade.')).toBeInTheDocument();
});
it('sends event when visiting progress tab when learner is not passing', async () => {
await fetchAndRender();
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.course_progress.visited', {
org_key: 'edX',
courserun_key: courseId,
is_staff: false,
track_variant: 'audit',
grade_variant: 'not_passing',
certificate_status_variant: 'not_passing',
});
});
it('Displays text for inProgress case when more content is scheduled and the learner does not have a passing grade', async () => {
setTabData({
has_scheduled_content: true,
@@ -253,6 +654,23 @@ describe('Progress Tab', () => {
expect(screen.getByText('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.')).toBeInTheDocument();
});
it('sends event when visiting progress tab when user has scheduled content', async () => {
setTabData({
has_scheduled_content: true,
});
await fetchAndRender();
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.course_progress.visited', {
org_key: 'edX',
courserun_key: courseId,
is_staff: false,
track_variant: 'audit',
grade_variant: 'not_passing',
certificate_status_variant: 'has_scheduled_content',
});
});
it('Displays request certificate link', async () => {
setTabData({
certificate_data: { cert_status: 'requesting' },
@@ -262,6 +680,34 @@ describe('Progress Tab', () => {
expect(screen.getByRole('button', { name: 'Request certificate' })).toBeInTheDocument();
});
it('sends events on view of progress tab and on click of request certificate link', async () => {
setTabData({
certificate_data: { cert_status: 'requesting' },
user_has_passing_grade: true,
});
await fetchAndRender();
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.course_progress.visited', {
org_key: 'edX',
courserun_key: courseId,
is_staff: false,
track_variant: 'audit',
grade_variant: 'passing',
certificate_status_variant: 'requesting',
});
const requestCertificateLink = screen.getByRole('button', { name: 'Request certificate' });
fireEvent.click(requestCertificateLink);
expect(sendTrackEvent).toHaveBeenCalledTimes(2);
expect(sendTrackEvent).toHaveBeenNthCalledWith(2, 'edx.ui.lms.course_progress.certificate_status.clicked', {
org_key: 'edX',
courserun_key: courseId,
is_staff: false,
certificate_status_variant: 'requesting',
});
});
it('Displays verify identity link', async () => {
setTabData({
certificate_data: { cert_status: 'unverified' },
@@ -272,6 +718,35 @@ describe('Progress Tab', () => {
expect(screen.getByRole('link', { name: 'Verify ID' })).toBeInTheDocument();
});
it('sends events on view of progress tab and on click of ID verification link', async () => {
setTabData({
certificate_data: { cert_status: 'unverified' },
user_has_passing_grade: true,
verification_data: { link: 'test' },
});
await fetchAndRender();
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.course_progress.visited', {
org_key: 'edX',
courserun_key: courseId,
is_staff: false,
track_variant: 'audit',
grade_variant: 'passing',
certificate_status_variant: 'unverified',
});
const idVerificationLink = screen.getByRole('link', { name: 'Verify ID' });
fireEvent.click(idVerificationLink);
expect(sendTrackEvent).toHaveBeenCalledTimes(2);
expect(sendTrackEvent).toHaveBeenNthCalledWith(2, 'edx.ui.lms.course_progress.certificate_status.clicked', {
org_key: 'edX',
courserun_key: courseId,
is_staff: false,
certificate_status_variant: 'unverified',
});
});
it('Displays verification pending message', async () => {
setTabData({
certificate_data: { cert_status: 'unverified' },
@@ -283,6 +758,25 @@ describe('Progress Tab', () => {
expect(screen.queryByRole('link', { name: 'Verify ID' })).not.toBeInTheDocument();
});
it('sends event when visiting progress tab with ID verification pending message', async () => {
setTabData({
certificate_data: { cert_status: 'unverified' },
verification_data: { status: 'pending' },
user_has_passing_grade: true,
});
await fetchAndRender();
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.course_progress.visited', {
org_key: 'edX',
courserun_key: courseId,
is_staff: false,
track_variant: 'audit',
grade_variant: 'passing',
certificate_status_variant: 'unverified',
});
});
it('Displays download link', async () => {
setTabData({
certificate_data: {
@@ -295,6 +789,37 @@ describe('Progress Tab', () => {
expect(screen.getByRole('link', { name: 'Download my certificate' })).toBeInTheDocument();
});
it('sends events on view of progress tab and on click of downloadable certificate link', async () => {
setTabData({
certificate_data: {
cert_status: 'downloadable',
download_url: 'fake.download.url',
},
user_has_passing_grade: true,
});
await fetchAndRender();
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.course_progress.visited', {
org_key: 'edX',
courserun_key: courseId,
is_staff: false,
track_variant: 'audit',
grade_variant: 'passing',
certificate_status_variant: 'earned_downloadable',
});
const downloadCertificateLink = screen.getByRole('link', { name: 'Download my certificate' });
fireEvent.click(downloadCertificateLink);
expect(sendTrackEvent).toHaveBeenCalledTimes(2);
expect(sendTrackEvent).toHaveBeenNthCalledWith(2, 'edx.ui.lms.course_progress.certificate_status.clicked', {
org_key: 'edX',
courserun_key: courseId,
is_staff: false,
certificate_status_variant: 'earned_downloadable',
});
});
it('Displays webview link', async () => {
setTabData({
certificate_data: {
@@ -307,6 +832,37 @@ describe('Progress Tab', () => {
expect(screen.getByRole('link', { name: 'View my certificate' })).toBeInTheDocument();
});
it('sends events on view of progress tab and on click of view certificate link', async () => {
setTabData({
certificate_data: {
cert_status: 'downloadable',
cert_web_view_url: '/certificates/cooluuidgoeshere',
},
user_has_passing_grade: true,
});
await fetchAndRender();
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.course_progress.visited', {
org_key: 'edX',
courserun_key: courseId,
is_staff: false,
track_variant: 'audit',
grade_variant: 'passing',
certificate_status_variant: 'earned_viewable',
});
const viewCertificateLink = screen.getByRole('link', { name: 'View my certificate' });
fireEvent.click(viewCertificateLink);
expect(sendTrackEvent).toHaveBeenCalledTimes(2);
expect(sendTrackEvent).toHaveBeenNthCalledWith(2, 'edx.ui.lms.course_progress.certificate_status.clicked', {
org_key: 'edX',
courserun_key: courseId,
is_staff: false,
certificate_status_variant: 'earned_viewable',
});
});
it('Displays certificate is earned but unavailable message', async () => {
setTabData({
certificate_data: { cert_status: 'earned_but_not_available' },
@@ -316,6 +872,57 @@ describe('Progress Tab', () => {
expect(screen.queryByText('Certificate status')).toBeInTheDocument();
});
it('sends event when visiting the progress tab when cert is earned but unavailable', async () => {
setTabData({
certificate_data: { cert_status: 'earned_but_not_available' },
user_has_passing_grade: true,
});
await fetchAndRender();
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.course_progress.visited', {
org_key: 'edX',
courserun_key: courseId,
is_staff: false,
track_variant: 'audit',
grade_variant: 'passing',
certificate_status_variant: 'earned_but_not_available',
});
});
it('sends event with correct grade variant for passing with letter grades', async () => {
setTabData({
certificate_data: { cert_status: 'earned_but_not_available' },
grading_policy: {
assignment_policies: [
{
num_droppable: 1,
num_total: 2,
short_label: 'HW',
type: 'Homework',
weight: 1,
},
],
grade_range: {
A: 0.9,
B: 0.8,
},
},
user_has_passing_grade: true,
});
await fetchAndRender();
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.course_progress.visited', {
org_key: 'edX',
courserun_key: courseId,
is_staff: false,
track_variant: 'audit',
grade_variant: 'passing_grades',
certificate_status_variant: 'earned_but_not_available',
});
});
it('Displays upgrade link when available', async () => {
setTabData({
certificate_data: { cert_status: 'audit_passing' },
@@ -330,6 +937,36 @@ describe('Progress Tab', () => {
expect(screen.getByRole('link', { name: 'Upgrade now' })).toBeInTheDocument();
});
it('sends events on view of progress tab and when audit learner clicks upgrade link', async () => {
setTabData({
certificate_data: { cert_status: 'audit_passing' },
verified_mode: {
upgrade_url: 'http://localhost:18130/basket/add/?sku=8CF08E5',
},
});
await fetchAndRender();
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.course_progress.visited', {
org_key: 'edX',
courserun_key: courseId,
is_staff: false,
track_variant: 'audit',
grade_variant: 'not_passing',
certificate_status_variant: 'audit_passing',
});
const upgradeLink = screen.getByRole('link', { name: 'Upgrade now' });
fireEvent.click(upgradeLink);
expect(sendTrackEvent).toHaveBeenCalledTimes(2);
expect(sendTrackEvent).toHaveBeenNthCalledWith(2, 'edx.ui.lms.course_progress.certificate_status.clicked', {
org_key: 'edX',
courserun_key: courseId,
is_staff: false,
certificate_status_variant: 'audit_passing',
});
});
it('Displays nothing if audit only', async () => {
setTabData({
certificate_data: { cert_status: 'audit_passing' },
@@ -341,6 +978,23 @@ describe('Progress Tab', () => {
expect(screen.queryByRole('link', { name: 'Upgrade now' })).not.toBeInTheDocument();
});
it('sends event when visiting the progress tab even when audit user cannot upgrade (i.e. certificate component does not render)', async () => {
setTabData({
certificate_data: { cert_status: 'audit_passing' },
});
await fetchAndRender();
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.course_progress.visited', {
org_key: 'edX',
courserun_key: courseId,
is_staff: false,
track_variant: 'audit',
grade_variant: 'not_passing',
certificate_status_variant: 'audit_passing_missed_upgrade_deadline',
});
});
it('Does not display the certificate component if it does not match any statuses', async () => {
setTabData({
certificate_data: {
@@ -352,6 +1006,27 @@ describe('Progress Tab', () => {
await fetchAndRender();
expect(screen.queryByTestId('certificate-status-component')).not.toBeInTheDocument();
});
it('sends event when visiting progress tab, although no certificate statuses match', async () => {
setTabData({
certificate_data: {
cert_status: 'bogus_status',
},
user_has_passing_grade: true,
});
setMetadata({ is_enrolled: true });
await fetchAndRender();
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.course_progress.visited', {
org_key: 'edX',
courserun_key: courseId,
is_staff: false,
track_variant: 'audit',
grade_variant: 'passing',
certificate_status_variant: 'certificate_status_disabled',
});
});
});
it('Does not display the certificate component if the user is not enrolled', async () => {

View File

@@ -1,5 +1,7 @@
import React from 'react';
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import {
FormattedDate, FormattedMessage, injectIntl, intlShape,
} from '@edx/frontend-platform/i18n';
@@ -19,12 +21,20 @@ function CertificateStatus({ intl }) {
const {
isEnrolled,
org,
} = useModel('courseHomeMeta', courseId);
const {
certificateData,
end,
enrollmentMode,
gradingPolicy: {
gradeRange,
},
hasScheduledContent,
userHasPassingGrade,
verificationData,
verifiedMode,
} = useModel('progress', courseId);
const mode = getCourseExitMode(
@@ -34,39 +44,48 @@ function CertificateStatus({ intl }) {
userHasPassingGrade,
);
const dispatch = useDispatch();
const { administrator } = getAuthenticatedUser();
const {
end,
verificationData,
certificateData: {
certStatus,
certWebViewUrl,
downloadUrl,
},
verifiedMode,
} = useModel('progress', courseId);
let certStatus;
let certWebViewUrl;
let downloadUrl;
if (certificateData) {
certStatus = certificateData.certStatus;
certWebViewUrl = certificateData.certWebViewUrl;
downloadUrl = certificateData.downloadUrl;
}
let certCase;
let certEventName = certStatus;
let body;
let buttonAction;
let buttonLocation;
let buttonText;
let endDate;
let gradeEventName = 'not_passing';
if (userHasPassingGrade) {
gradeEventName = Object.entries(gradeRange).length > 1 ? 'passing_grades' : 'passing';
}
const dashboardLink = <DashboardLink />;
const idVerificationSupportLink = <IdVerificationSupportLink />;
const profileLink = <ProfileLink />;
if (mode === COURSE_EXIT_MODES.nonPassing) {
if (mode === COURSE_EXIT_MODES.disabled) {
certEventName = 'certificate_status_disabled';
} else if (mode === COURSE_EXIT_MODES.nonPassing) {
certCase = 'notPassing';
certEventName = 'not_passing';
body = intl.formatMessage(messages[`${certCase}Body`]);
} else if (mode === COURSE_EXIT_MODES.inProgress) {
certCase = 'inProgress';
certEventName = 'has_scheduled_content';
body = intl.formatMessage(messages[`${certCase}Body`]);
} else if (mode === COURSE_EXIT_MODES.celebration) {
switch (certStatus) {
case 'requesting':
// Requestable
certCase = 'requestable';
buttonAction = () => { dispatch(requestCert(courseId)); };
body = intl.formatMessage(messages[`${certCase}Body`]);
@@ -105,9 +124,11 @@ function CertificateStatus({ intl }) {
);
if (certWebViewUrl) {
certEventName = 'earned_viewable';
buttonLocation = `${getConfig().LMS_BASE_URL}${certWebViewUrl}`;
buttonText = intl.formatMessage(messages.viewableButton);
} else if (downloadUrl) {
certEventName = 'earned_downloadable';
buttonLocation = downloadUrl;
buttonText = intl.formatMessage(messages.downloadableButton);
}
@@ -133,22 +154,47 @@ function CertificateStatus({ intl }) {
body = intl.formatMessage(messages[`${certCase}Body`]);
buttonLocation = verifiedMode.upgradeUrl;
buttonText = intl.formatMessage(messages[`${certCase}Button`]);
} else {
certCase = null; // Do not render the certificate component if the upgrade deadline has passed
certEventName = 'audit_passing_missed_upgrade_deadline';
}
break;
// This code shouldn't be hit but coding defensively since switch expects a default statement
default:
certCase = null;
certEventName = 'no_certificate_status';
break;
}
}
// Log visit to progress tab
useEffect(() => {
sendTrackEvent('edx.ui.lms.course_progress.visited', {
org_key: org,
courserun_key: courseId,
is_staff: administrator,
track_variant: enrollmentMode,
grade_variant: gradeEventName,
certificate_status_variant: certEventName,
});
}, []);
if (!certCase) {
return null;
}
const header = intl.formatMessage(messages[`${certCase}Header`]);
const logCertificateStatusButtonClicked = () => {
sendTrackEvent('edx.ui.lms.course_progress.certificate_status.clicked', {
org_key: org,
courserun_key: courseId,
is_staff: administrator,
certificate_status_variant: certEventName,
});
};
return (
<section data-testid="certificate-status-component" className="text-dark-700 mb-4">
<Card className="bg-light-200 shadow-sm border-0">
@@ -160,7 +206,17 @@ function CertificateStatus({ intl }) {
{body}
</Card.Text>
{buttonText && (buttonLocation || buttonAction) && (
<Button variant="outline-brand" onClick={buttonAction} href={buttonLocation} block>{buttonText}</Button>
<Button
variant="outline-brand"
onClick={() => {
logCertificateStatusButtonClicked(certStatus);
if (buttonAction) { buttonAction(); }
}}
href={buttonLocation}
block
>
{buttonText}
</Button>
)}
</Card.Body>
</Card>

View File

@@ -77,6 +77,18 @@ const messages = defineMessages({
id: 'progress.certificateStatus.upgradeButton',
defaultMessage: 'Upgrade now',
},
unverifiedHomeHeader: {
id: 'progress.certificateStatus.unverifiedHomeHeader',
defaultMessage: 'Verify your identity to earn a certificate!',
},
unverifiedHomeButton: {
id: 'progress.certificateStatus.unverifiedHomeButton',
defaultMessage: 'Verify my ID',
},
unverifiedHomeBody: {
id: 'progress.certificateStatus.unverifiedHomeBody',
defaultMessage: 'In order to generate a certificate for this course, you must complete the ID verification process.',
},
});
export default messages;

View File

@@ -7,6 +7,10 @@ import { OverlayTrigger, Popover } from '@edx/paragon';
import messages from './messages';
function CompleteDonutSegment({ completePercentage, intl, lockedPercentage }) {
if (!completePercentage) {
return null;
}
const [showCompletePopover, setShowCompletePopover] = useState(false);
const completeSegmentOffset = (3.6 * completePercentage) / 8;
@@ -24,15 +28,6 @@ function CompleteDonutSegment({ completePercentage, intl, lockedPercentage }) {
onFocus={() => setShowCompletePopover(true)}
tabIndex="-1"
>
<circle
className="donut-segment complete-stroke"
cx="21"
cy="21"
r="15.91549430918954"
strokeDasharray={`${completePercentage} ${100 - completePercentage}`}
strokeDashoffset={lockedSegmentOffset + completePercentage}
/>
{/* Tooltip */}
<OverlayTrigger
show={showCompletePopover}
@@ -49,16 +44,33 @@ function CompleteDonutSegment({ completePercentage, intl, lockedPercentage }) {
<rect x="19" y="3" style={{ transform: `rotate(${completeTooltipDegree}deg)` }} />
</OverlayTrigger>
{/* Complete segment */}
<circle
className="donut-segment complete-stroke"
cx="21"
cy="21"
r="15.91549430918954"
strokeDasharray={`${completePercentage} ${100 - completePercentage}`}
strokeDashoffset={lockedSegmentOffset + completePercentage}
/>
{/* Segment dividers */}
{lockedPercentage > 0 && lockedPercentage < 100 && (
<circle
cx="21"
cy="21"
r="15.91549430918954"
className="donut-segment divider-stroke"
strokeDasharray="0.3 99.7"
strokeDashoffset={0.15 + lockedSegmentOffset}
/>
)}
{completePercentage < 100 && lockedPercentage < 100 && lockedPercentage + completePercentage === 100 && (
{completePercentage < 100 && lockedPercentage > 0 && lockedPercentage < 100
&& lockedPercentage + completePercentage === 100 && (
<circle
cx="21"
cy="21"
r="15.91549430918954"
className="donut-segment divider-stroke"
strokeDasharray="0.3 99.7"
strokeDashoffset="25.15"

View File

@@ -22,8 +22,8 @@ function CompletionDonutChart({ intl }) {
} = useModel('progress', courseId);
const numTotalUnits = completeCount + incompleteCount + lockedCount;
const completePercentage = Number(((completeCount / numTotalUnits) * 100).toFixed(0));
const lockedPercentage = Number(((lockedCount / numTotalUnits) * 100).toFixed(0));
const completePercentage = completeCount ? Number(((completeCount / numTotalUnits) * 100).toFixed(0)) : 0;
const lockedPercentage = lockedCount ? Number(((lockedCount / numTotalUnits) * 100).toFixed(0)) : 0;
const incompletePercentage = 100 - completePercentage - lockedPercentage;
return (

View File

@@ -7,6 +7,10 @@ import { OverlayTrigger, Popover } from '@edx/paragon';
import messages from './messages';
function IncompleteDonutSegment({ incompletePercentage, intl }) {
if (!incompletePercentage) {
return null;
}
const [showIncompletePopover, setShowIncompletePopover] = useState(false);
const incompleteSegmentOffset = (3.6 * incompletePercentage) / 16;

View File

@@ -9,7 +9,7 @@ import messages from './messages';
function LockedDonutSegment({ intl, lockedPercentage }) {
const [showLockedPopover, setShowLockedPopover] = useState(false);
if (!lockedPercentage > 0) {
if (!lockedPercentage) {
return null;
}

View File

@@ -13,33 +13,23 @@ import messages from '../messages';
function CourseGrade({ intl }) {
const {
courseId,
gradesFeatureIsLocked,
} = useSelector(state => state.courseHome);
const {
completionSummary: {
lockedCount,
},
gradingPolicy: {
gradeRange,
},
} = useModel('progress', courseId);
let passingGrade;
if (gradeRange.pass) {
passingGrade = gradeRange.pass * 100;
} else {
passingGrade = Object.entries(gradeRange).pop()[1] * 100;
}
const passingGrade = Number((Math.min(...Object.values(gradeRange)) * 100).toFixed(0));
passingGrade = Number(passingGrade.toFixed(0));
const isLocked = lockedCount > 0;
const applyLockedOverlay = isLocked ? 'locked-overlay' : '';
const applyLockedOverlay = gradesFeatureIsLocked ? 'locked-overlay' : '';
return (
<section className="text-dark-700 my-4 rounded shadow-sm">
{isLocked && <CourseGradeHeader />}
<div className={applyLockedOverlay}>
{gradesFeatureIsLocked && <CourseGradeHeader />}
<div className={applyLockedOverlay} aria-hidden={gradesFeatureIsLocked}>
<div className="row w-100 m-0 p-4">
<div className="col-12 col-sm-6 p-0 pr-sm-2">
<h2>{intl.formatMessage(messages.grades)}</h2>

View File

@@ -40,15 +40,17 @@ function CourseGradeFooter({ intl, passingGrade }) {
if (isPassing) {
if (hasLetterGrades) {
const letterGrades = Object.keys(gradeRange);
const gradeIndex = letterGrades.indexOf(letterGrade);
const minGrade = gradeRange[letterGrade] * 100;
const maxGrade = gradeIndex > 0 ? gradeRange[letterGrades[gradeIndex - 1]] * 100 : 100;
const minGradeRangeCutoff = gradeRange[letterGrade] * 100;
const possibleMaxGradeRangeValues = [...Object.values(gradeRange).filter(
(grade) => (grade * 100 > minGradeRangeCutoff),
)];
const maxGradeRangeCutoff = possibleMaxGradeRangeValues.length ? Math.min(...possibleMaxGradeRangeValues) * 100
: 100;
footerText = intl.formatMessage(messages.courseGradeFooterPassingWithGrade, {
letterGrade,
minGrade: minGrade.toFixed(0),
maxGrade: maxGrade.toFixed(0),
minGrade: minGradeRangeCutoff.toFixed(0),
maxGrade: maxGradeRangeCutoff.toFixed(0),
});
} else {
footerText = intl.formatMessage(messages.courseGradeFooterGenericPassing);

View File

@@ -1,6 +1,8 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Locked } from '@edx/paragon/icons';
import { Button, Icon } from '@edx/paragon';
@@ -12,12 +14,24 @@ function CourseGradeHeader({ intl }) {
const {
courseId,
} = useSelector(state => state.courseHome);
const {
org,
} = useModel('courseHomeMeta', courseId);
const {
verifiedMode,
} = useModel('progress', courseId);
const { administrator } = getAuthenticatedUser();
const logUpgradeButtonClick = () => {
sendTrackEvent('edx.ui.lms.course_progress.grades_upgrade.clicked', {
org_key: org,
courserun_key: courseId,
is_staff: administrator,
});
};
return (
<div className="row w-100 m-0 p-4 rounded-top bg-primary-500 text-white">
<div className="col-12 col-md-9 p-0">
<div className={`col-12 ${verifiedMode ? 'col-md-9' : ''} p-0`}>
<div className="row w-100 m-0 p-0">
<div className="col-1 p-0">
<Icon src={Locked} />
@@ -31,17 +45,18 @@ function CourseGradeHeader({ intl }) {
</div>
<div className="row w-100 m-0 p-0 justify-content-end">
<div className="col-11 px-2 p-sm-0 small">
{intl.formatMessage(messages.courseGradePreviewBody)}
{verifiedMode ? intl.formatMessage(messages.courseGradePreviewUnlockCertificateBody)
: intl.formatMessage(messages.courseGradePreviewUpgradeDeadlinePassedBody)}
</div>
</div>
</div>
<div className="col-12 col-md-3 mt-3 mt-md-0 p-0 align-self-center text-right">
{verifiedMode && (
<Button variant="brand" size="sm" href={verifiedMode.upgradeUrl}>
{verifiedMode && (
<div className="col-12 col-md-3 mt-3 mt-md-0 p-0 align-self-center text-right">
<Button variant="brand" size="sm" href={verifiedMode.upgradeUrl} onClick={logUpgradeButtonClick}>
{intl.formatMessage(messages.courseGradePreviewUpgradeButton)}
</Button>
)}
</div>
</div>
)}
</div>
);
}

View File

@@ -17,11 +17,11 @@ function CurrentGradeTooltip({ intl, tooltipClassName }) {
const {
courseGrade: {
isPassing,
percent,
visiblePercent,
},
} = useModel('progress', courseId);
const currentGrade = percent * 100;
const currentGrade = Number((visiblePercent * 100).toFixed(0));
return (
<>
@@ -30,21 +30,21 @@ function CurrentGradeTooltip({ intl, tooltipClassName }) {
placement="top"
overlay={(
<Popover id={`${isPassing ? 'passing' : 'non-passing'}-grade-tooltip`} aria-hidden="true" className={tooltipClassName}>
<Popover.Content className={isPassing ? 'text-white' : 'text-dark-700'}>
<Popover.Content data-testid="currentGradeTooltipContent" className={isPassing ? 'text-white' : 'text-dark-700'}>
{currentGrade.toFixed(0)}%
</Popover.Content>
</Popover>
)}
>
<g>
<circle cx={`${currentGrade}%`} cy="50%" r="8.5" fill="transparent" />
<rect className="grade-bar__divider" x={`${currentGrade}%`} style={{ transform: 'translateY(2.61em)' }} />
<circle cx={`${Math.min(...[currentGrade, 100])}%`} cy="50%" r="8.5" fill="transparent" />
<rect className="grade-bar__divider" x={`${Math.min(...[currentGrade, 100])}%`} style={{ transform: 'translateY(2.61em)' }} />
</g>
</OverlayTrigger>
<text
className="x-small"
textAnchor={currentGrade < 50 ? 'start' : 'end'}
x={`${currentGrade}%`}
x={`${Math.min(...[currentGrade, 100])}%`}
y="20px"
style={{ transform: `translateX(${currentGrade < 50 ? '' : '-'}3.4em)` }}
>

View File

@@ -12,22 +12,19 @@ import messages from '../messages';
function GradeBar({ intl, passingGrade }) {
const {
courseId,
gradesFeatureIsLocked,
} = useSelector(state => state.courseHome);
const {
completionSummary: {
lockedCount,
},
courseGrade: {
isPassing,
percent,
visiblePercent,
},
} = useModel('progress', courseId);
const currentGrade = percent * 100;
const currentGrade = Number((visiblePercent * 100).toFixed(0));
const isLocked = lockedCount > 0;
const lockedTooltipClassName = isLocked ? 'locked-overlay' : '';
const lockedTooltipClassName = gradesFeatureIsLocked ? 'locked-overlay' : '';
return (
<div className="col-12 col-sm-6 align-self-center">

View File

@@ -14,6 +14,7 @@ import messages from '../messages';
function GradeRangeTooltip({ intl, iconButtonClassName, passingGrade }) {
const {
courseId,
gradesFeatureIsLocked,
} = useSelector(state => state.courseHome);
const {
@@ -24,7 +25,9 @@ function GradeRangeTooltip({ intl, iconButtonClassName, passingGrade }) {
const [showTooltip, setShowTooltip] = useState(false);
const gradeRangeEntries = Object.entries(gradeRange);
const orderedGradeRange = Object.entries(gradeRange).sort((a, b) => (
gradeRange[b[0]] - gradeRange[a[0]]
));
return (
<OverlayTrigger
@@ -36,18 +39,18 @@ function GradeRangeTooltip({ intl, iconButtonClassName, passingGrade }) {
<Popover.Content className="px-3">
{intl.formatMessage(messages.courseGradeRangeTooltip)}
<ul className="list-unstyled m-0">
{gradeRangeEntries.map((entry, index) => {
{orderedGradeRange.map((range, index) => {
if (index === 0) {
return (
<li key={entry[0]}>
{entry[0]}: {(entry[1] * 100).toFixed(0)}%-100%
<li key={range[0]}>
{range[0]}: {(range[1] * 100).toFixed(0)}%-100%
</li>
);
}
const previousGrade = gradeRangeEntries[index - 1];
const previousGrade = orderedGradeRange[index - 1];
return (
<li key={entry[0]}>
{entry[0]}: {(entry[1] * 100).toFixed(0)}%-{(previousGrade[1] * 100).toFixed(0)}%
<li key={range[0]}>
{range[0]}: {(range[1] * 100).toFixed(0)}%-{(previousGrade[1] * 100).toFixed(0)}%
</li>
);
})}
@@ -65,6 +68,7 @@ function GradeRangeTooltip({ intl, iconButtonClassName, passingGrade }) {
src={InfoOutline}
iconAs={Icon}
size="inline"
disabled={gradesFeatureIsLocked}
/>
</OverlayTrigger>
);

View File

@@ -1,8 +1,11 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { Link } from 'react-router-dom';
import { getConfig } from '@edx/frontend-platform';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Hyperlink } from '@edx/paragon';
import { useModel } from '../../../../generic/model-store';
import DetailedGradesTable from './DetailedGradesTable';
@@ -10,24 +13,37 @@ import DetailedGradesTable from './DetailedGradesTable';
import messages from '../messages';
function DetailedGrades({ intl }) {
const { administrator } = getAuthenticatedUser();
const {
courseId,
gradesFeatureIsLocked,
} = useSelector(state => state.courseHome);
const {
org,
} = useModel('courseHomeMeta', courseId);
const {
sectionScores,
} = useModel('progress', courseId);
const hasSectionScores = sectionScores.length > 0;
const logOutlineLinkClick = () => {
sendTrackEvent('edx.ui.lms.course_progress.detailed_grades.course_outline_link.clicked', {
org_key: org,
courserun_key: courseId,
is_staff: administrator,
});
};
const outlineLink = (
<Link
className="text-dark-700"
style={{ textDecoration: 'underline' }}
to={`/course/${courseId}/home`}
<Hyperlink
className="muted-link inline-link"
destination={`${getConfig().LMS_BASE_URL}/courses/${courseId}/course`}
onClick={logOutlineLinkClick}
tabIndex={gradesFeatureIsLocked ? '-1' : '0'}
>
{intl.formatMessage(messages.courseOutline)}
</Link>
</Hyperlink>
);
return (

View File

@@ -1,12 +1,32 @@
import React from 'react';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { DataTable } from '@edx/paragon';
import messages from '../messages';
import { useModel } from '../../../../generic/model-store';
function DetailedGradesTable({ intl, sectionScores }) {
const {
courseId,
gradesFeatureIsLocked,
} = useSelector(state => state.courseHome);
const {
org,
} = useModel('courseHomeMeta', courseId);
const { administrator } = getAuthenticatedUser();
const logSubsectionClicked = (blockKey) => {
sendTrackEvent('edx.ui.lms.course_progress.detailed_grades_assignment.clicked', {
org_key: org,
courserun_key: courseId,
is_staff: administrator,
assignment_block_key: blockKey,
});
};
return (
sectionScores.map((chapter) => {
const subsectionScores = chapter.subsections.filter(
@@ -21,7 +41,18 @@ function DetailedGradesTable({ intl, sectionScores }) {
}
const detailedGradesData = subsectionScores.map((subsection) => {
const title = <a href={subsection.url} className="text-dark-700 small">{subsection.displayName}</a>;
const title = (
<a
href={subsection.url}
className="muted-link small"
onClick={() => {
logSubsectionClicked(subsection.blockKey);
}}
tabIndex={gradesFeatureIsLocked ? '-1' : '0'}
>
{subsection.displayName}
</a>
);
return {
subsectionTitle: title,
score: `${subsection.numPointsEarned}/${subsection.numPointsPossible}`,

View File

@@ -1,13 +1,23 @@
import React from 'react';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
function AssignmentTypeCell({ assignmentType, footnoteMarker, footnoteId }) {
const {
gradesFeatureIsLocked,
} = useSelector(state => state.courseHome);
return (
<div className="small">
{assignmentType}
{footnoteId && footnoteMarker && (
<sup>
<a id={`${footnoteId}-ref`} className="text-dark-700" href={`#${footnoteId}-footnote`} aria-describedby="grade-summary-footnote-label">
<a
id={`${footnoteId}-ref`}
className="muted-link"
href={`#${footnoteId}-footnote`}
aria-describedby="grade-summary-footnote-label"
tabIndex={gradesFeatureIsLocked ? '-1' : '0'}
>
{footnoteMarker}
</a>
</sup>
@@ -24,7 +34,7 @@ AssignmentTypeCell.propTypes = {
AssignmentTypeCell.defaultProps = {
footnoteId: '',
footnoteMarker: '',
footnoteMarker: null,
};
export default AssignmentTypeCell;

View File

@@ -1,4 +1,5 @@
import React from 'react';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
@@ -6,6 +7,9 @@ import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/
import messages from '../messages';
function DroppableAssignmentFootnote({ footnotes, intl }) {
const {
gradesFeatureIsLocked,
} = useSelector(state => state.courseHome);
return (
<>
<span id="grade-summary-footnote-label" className="sr-only">{intl.formatMessage(messages.footnotesTitle)}</span>
@@ -15,13 +19,15 @@ function DroppableAssignmentFootnote({ footnotes, intl }) {
<sup>{index + 1}</sup>
<FormattedMessage
id="progress.footnotes.droppableAssignments"
defaultMessage="The lowest {numDroppable, plural, one{# {assignmentType} score} other{# {assignmentType} scores}} will be dropped."
defaultMessage="The lowest {numDroppable, plural, one{# {assignmentType} score is} other{# {assignmentType} scores are}} dropped."
values={{
numDroppable: footnote.numDroppable,
assignmentType: footnote.assignmentType,
}}
/>
<a className="sr-only" href={`#${footnote.id}-ref`}>{intl.formatMessage(messages.backToContent)}</a>
<a className="sr-only" href={`#${footnote.id}-ref`} tabIndex={gradesFeatureIsLocked ? '-1' : '0'}>
{intl.formatMessage(messages.backToContent)}
</a>
</li>
))}
</ul>

View File

@@ -11,7 +11,6 @@ function GradeSummary() {
} = useSelector(state => state.courseHome);
const {
sectionScores,
gradingPolicy: {
assignmentPolicies,
},
@@ -21,27 +20,10 @@ function GradeSummary() {
return null;
}
// accumulate grades for individual assignment types
const gradeByAssignmentType = {};
assignmentPolicies.forEach(assignment => {
gradeByAssignmentType[assignment.type] = { numPointsEarned: 0, numPointsPossible: 0 };
});
sectionScores.forEach((chapter) => {
chapter.subsections.forEach((subsection) => {
if (subsection.hasGradedAssignment) {
gradeByAssignmentType[subsection.assignmentType].numPointsEarned += subsection.numPointsEarned;
gradeByAssignmentType[subsection.assignmentType].numPointsPossible += subsection.numPointsPossible;
}
});
});
return (
<section className="text-dark-700 mb-4">
<GradeSummaryHeader />
<GradeSummaryTable
gradeByAssignmentType={gradeByAssignmentType}
/>
<GradeSummaryTable />
</section>
);
}

View File

@@ -1,4 +1,5 @@
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
@@ -9,6 +10,9 @@ import { InfoOutline } from '@edx/paragon/icons';
import messages from '../messages';
function GradeSummaryHeader({ intl }) {
const {
gradesFeatureIsLocked,
} = useSelector(state => state.courseHome);
const [showTooltip, setShowTooltip] = useState(false);
return (
<div className="row w-100 m-0 align-items-center">
@@ -33,6 +37,7 @@ function GradeSummaryHeader({ intl }) {
iconAs={Icon}
className="mb-3"
size="sm"
disabled={gradesFeatureIsLocked}
/>
</OverlayTrigger>
</div>

View File

@@ -1,6 +1,5 @@
import React from 'react';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { DataTable } from '@edx/paragon';
@@ -12,9 +11,7 @@ import GradeSummaryTableFooter from './GradeSummaryTableFooter';
import messages from '../messages';
function GradeSummaryTable({
gradeByAssignmentType, intl,
}) {
function GradeSummaryTable({ intl }) {
const {
courseId,
} = useSelector(state => state.courseHome);
@@ -27,10 +24,6 @@ function GradeSummaryTable({
const footnotes = [];
const calculateWeightedGrade = (numPointsEarned, numPointsPossible, assignmentWeight) => (
numPointsPossible > 0 ? ((numPointsEarned * assignmentWeight * 100) / numPointsPossible).toFixed(0) : 0
);
const getFootnoteId = (assignment) => {
const footnoteId = assignment.shortLabel ? assignment.shortLabel : assignment.type;
return footnoteId.replace(/[^A-Za-z0-9.-_]+/g, '-');
@@ -38,7 +31,7 @@ function GradeSummaryTable({
const gradeSummaryData = assignmentPolicies.map((assignment) => {
let footnoteId = '';
let footnoteMarker = '';
let footnoteMarker;
if (assignment.numDroppable > 0) {
footnoteId = getFootnoteId(assignment);
@@ -51,17 +44,11 @@ function GradeSummaryTable({
footnoteMarker = footnotes.length;
}
const weightedGrade = calculateWeightedGrade(
gradeByAssignmentType[assignment.type].numPointsEarned,
gradeByAssignmentType[assignment.type].numPointsPossible,
assignment.weight,
);
return {
type: { footnoteId, footnoteMarker, type: assignment.type },
weight: `${assignment.weight * 100}%`,
score: `${gradeByAssignmentType[assignment.type].numPointsEarned}/${gradeByAssignmentType[assignment.type].numPointsPossible}`,
weightedGrade: `${weightedGrade}%`,
weight: `${(assignment.weight * 100).toFixed(0)}%`,
grade: `${(assignment.averageGrade * 100).toFixed(0)}%`,
weightedGrade: `${(assignment.weightedGrade * 100).toFixed(0)}%`,
};
});
@@ -91,8 +78,8 @@ function GradeSummaryTable({
cellClassName: 'float-right small',
},
{
Header: `${intl.formatMessage(messages.score)}`,
accessor: 'score',
Header: `${intl.formatMessage(messages.grade)}`,
accessor: 'grade',
headerClassName: 'justify-content-end h5 mb-0',
cellClassName: 'float-right small',
},
@@ -116,7 +103,6 @@ function GradeSummaryTable({
}
GradeSummaryTable.propTypes = {
gradeByAssignmentType: PropTypes.shape({}).isRequired,
intl: intlShape.isRequired,
};

View File

@@ -15,18 +15,18 @@ function GradeSummaryTableFooter({ intl }) {
const {
courseGrade: {
isPassing,
percent,
visiblePercent,
},
} = useModel('progress', courseId);
const bgColor = isPassing ? 'bg-success-100' : 'bg-warning-100';
const totalGrade = percent * 100;
const totalGrade = (visiblePercent * 100).toFixed(0);
return (
<DataTable.TableFooter className={`border-top border-primary ${bgColor}`}>
<div className="row w-100 m-0">
<div id="weighted-grade-summary" className="col-8 p-0 small">{intl.formatMessage(messages.weightedGradeSummary)}</div>
<div aria-labelledby="weighted-grade-summary" className="col-4 p-0 text-right font-weight-bold small">{totalGrade}%</div>
<div data-testid="gradeSummaryFooterTotalWeightedGrade" aria-labelledby="weighted-grade-summary" className="col-4 p-0 text-right font-weight-bold small">{totalGrade}%</div>
</div>
</DataTable.TableFooter>
);

View File

@@ -37,9 +37,13 @@ const messages = defineMessages({
id: 'progress.courseGrade.preview.header.ariaHidden',
defaultMessage: 'Preview of a ',
},
courseGradePreviewBody: {
id: 'progress.courseGrade.preview.body',
defaultMessage: 'Unlock to view grades and work towards a certificate',
courseGradePreviewUnlockCertificateBody: {
id: 'progress.courseGrade.preview.body.unlockCertificate',
defaultMessage: 'Unlock to view grades and work towards a certificate.',
},
courseGradePreviewUpgradeDeadlinePassedBody: {
id: 'progress.courseGrade.preview.body.upgradeDeadlinePassed',
defaultMessage: 'The deadline to upgrade in this course has passed.',
},
courseGradePreviewUpgradeButton: {
id: 'progress.courseGrade.preview.button.upgrade',
@@ -69,6 +73,10 @@ const messages = defineMessages({
id: 'progress.footnotes.title',
defaultMessage: 'Grade summary footnotes',
},
grade: {
id: 'progress.gradeSummary.grade',
defaultMessage: 'Grade',
},
grades: {
id: 'progress.courseGrade.grades',
defaultMessage: 'Grades',
@@ -88,7 +96,7 @@ const messages = defineMessages({
gradeSummaryTooltipBody: {
id: 'progress.gradeSummary.tooltip.body',
defaultMessage: "Your course assignment's weight is determined by your instructor. "
+ 'By multiplying your score by the weight for that assignment type, your weighted grade is calculated. '
+ 'By multiplying your grade by the weight for that assignment type, your weighted grade is calculated. '
+ "Your weighted grade is what's used to determine if you pass the course.",
},
passingGradeLabel: {

View File

@@ -1,25 +1,47 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { Link } from 'react-router-dom';
import { getConfig } from '@edx/frontend-platform';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Hyperlink } from '@edx/paragon';
import messages from './messages';
import { useModel } from '../../../generic/model-store';
function RelatedLinks({ intl }) {
const {
courseId,
} = useSelector(state => state.courseHome);
const {
org,
} = useModel('courseHomeMeta', courseId);
const { administrator } = getAuthenticatedUser();
const logLinkClicked = (linkName) => {
sendTrackEvent('edx.ui.lms.course_progress.related_links.clicked', {
org_key: org,
courserun_key: courseId,
is_staff: administrator,
link_clicked: linkName,
});
};
return (
<section className="mb-4 x-small">
<h3 className="h4">{intl.formatMessage(messages.relatedLinks)}</h3>
<ul className="pl-4">
<li>
<Link to={`course/${courseId}/dates`}>{intl.formatMessage(messages.datesCardLink)}</Link>
<Hyperlink destination={`${getConfig().LMS_BASE_URL}/courses/${courseId}/dates`} onClick={() => logLinkClicked('dates')}>
{intl.formatMessage(messages.datesCardLink)}
</Hyperlink>
<p>{intl.formatMessage(messages.datesCardDescription)}</p>
</li>
<li>
<Link to={`course/${courseId}/home`}>{intl.formatMessage(messages.outlineCardLink)}</Link>
<Hyperlink destination={`${getConfig().LMS_BASE_URL}/courses/${courseId}/course`} onClick={() => logLinkClicked('course_outline')}>
{intl.formatMessage(messages.outlineCardLink)}
</Hyperlink>
<p>{intl.formatMessage(messages.outlineCardDescription)}</p>
</li>
</ul>

View File

@@ -121,6 +121,8 @@ class CoursewareContainer extends Component {
sequenceId,
courseStatus,
sequenceStatus,
specialExamsEnabledWaffleFlag,
proctoredExamsEnabledWaffleFlag,
sequence,
firstSequenceId,
unitViaSequenceId,
@@ -176,7 +178,11 @@ class CoursewareContainer extends Component {
// Check special exam redirect:
// /course/:courseId/:sequenceId(/:unitId) -> :legacyWebUrl
// because special exams are currently still served in the legacy LMS frontend.
checkSpecialExamRedirect(sequenceStatus, sequence);
const shouldRedirectProctoredExams = specialExamsEnabledWaffleFlag && sequence.isProctored
&& !proctoredExamsEnabledWaffleFlag;
if (!specialExamsEnabledWaffleFlag || shouldRedirectProctoredExams) {
checkSpecialExamRedirect(sequenceStatus, sequence);
}
// Check to sequence to sequence-unit redirect:
// /course/:courseId/:sequenceId -> /course/:courseId/:sequenceId/:unitId
@@ -324,6 +330,7 @@ const sequenceShape = PropTypes.shape({
unitIds: PropTypes.arrayOf(PropTypes.string).isRequired,
sectionId: PropTypes.string.isRequired,
isTimeLimited: PropTypes.bool,
isProctored: PropTypes.bool,
legacyWebUrl: PropTypes.string,
});
@@ -362,6 +369,8 @@ CoursewareContainer.propTypes = {
checkBlockCompletion: PropTypes.func.isRequired,
fetchCourse: PropTypes.func.isRequired,
fetchSequence: PropTypes.func.isRequired,
specialExamsEnabledWaffleFlag: PropTypes.bool.isRequired,
proctoredExamsEnabledWaffleFlag: PropTypes.bool.isRequired,
};
CoursewareContainer.defaultProps = {
@@ -461,7 +470,12 @@ const unitViaSequenceIdSelector = createSelector(
const mapStateToProps = (state) => {
const {
courseId, sequenceId, courseStatus, sequenceStatus,
courseId,
sequenceId,
courseStatus,
sequenceStatus,
specialExamsEnabledWaffleFlag,
proctoredExamsEnabledWaffleFlag,
} = state.courseware;
return {
@@ -469,6 +483,8 @@ const mapStateToProps = (state) => {
sequenceId,
courseStatus,
sequenceStatus,
specialExamsEnabledWaffleFlag,
proctoredExamsEnabledWaffleFlag,
course: currentCourseSelector(state),
sequence: currentSequenceSelector(state),
previousSequence: previousSequenceSelector(state),

View File

@@ -121,12 +121,17 @@ describe('CoursewareContainer', () => {
const courseBlocksUrlRegExp = new RegExp(`${getConfig().LMS_BASE_URL}/api/courses/v2/blocks/*`);
axiosMock.onGet(courseBlocksUrlRegExp).reply(200, courseBlocks);
const learningSequencesUrlRegExp = new RegExp(`${getConfig().LMS_BASE_URL}/api/learning_sequences/v1/course_outline/*`);
axiosMock.onGet(learningSequencesUrlRegExp).reply(403, {});
const courseMetadataUrl = appendBrowserTimezoneToUrl(`${getConfig().LMS_BASE_URL}/api/courseware/course/${courseId}`);
axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata);
sequenceMetadatas.forEach(sequenceMetadata => {
const sequenceMetadataUrl = `${getConfig().LMS_BASE_URL}/api/courseware/sequence/${sequenceMetadata.item_id}`;
axiosMock.onGet(sequenceMetadataUrl).reply(200, sequenceMetadata);
const proctoredExamApiUrl = `${getConfig().LMS_BASE_URL}/api/edx_proctoring/v1/proctored_exam/attempt/course_id/${courseId}/content_id/${sequenceMetadata.item_id}?is_learning_mfe=true`;
axiosMock.onGet(proctoredExamApiUrl).reply(200, { exam: {}, active_attempt: {} });
});
}

View File

@@ -14,7 +14,7 @@ import Sequence from './sequence';
import { CelebrationModal, shouldCelebrateOnSectionLoad } from './celebration';
import ContentTools from './content-tools';
import CourseBreadcrumbs from './CourseBreadcrumbs';
import SidebarNotificationButton from './SidebarNotificationButton';
import NotificationTrigger from './NotificationTrigger';
import CourseSock from '../../generic/course-sock';
import { useModel } from '../../generic/model-store';
@@ -61,17 +61,17 @@ function Course({
courseId, sequenceId, unitId, celebrateFirstSection, dispatch, celebrations,
);
// REV-2130 TODO: temporary cookie code that should be removed.
// In order to see the Value Prop sidebar in prod, a cookie should be set in
// REV-2297 TODO: temporary cookie code that should be removed.
// In order to see the Value Prop Notification Tray in prod, a cookie should be set in
// the browser console and refresh: document.cookie = 'value_prop_cookie=true';
const isValuePropCookieSet = Cookies.get('value_prop_cookie') === 'true';
const shouldDisplaySidebarButton = useWindowSize().width >= responsiveBreakpoints.small.minWidth;
const shouldDisplayNotificationTrigger = useWindowSize().width >= responsiveBreakpoints.small.minWidth;
const [sidebarVisible, setSidebar] = useState(false);
const isSidebarVisible = () => sidebarVisible && setSidebar;
const toggleSidebar = () => {
if (sidebarVisible) { setSidebar(false); } else { setSidebar(true); }
const [notificationTrayVisible, setNotificationTray] = useState(true);
const isNotificationTrayVisible = () => notificationTrayVisible && setNotificationTray;
const toggleNotificationTray = () => {
if (notificationTrayVisible) { setNotificationTray(false); } else { setNotificationTray(true); }
};
/** [MM-P2P] Experiment */
@@ -102,10 +102,10 @@ function Course({
mmp2p={MMP2P}
/>
{ isValuePropCookieSet && shouldDisplaySidebarButton ? (
<SidebarNotificationButton
toggleSidebar={toggleSidebar}
isSidebarVisible={isSidebarVisible}
{ isValuePropCookieSet && shouldDisplayNotificationTrigger ? (
<NotificationTrigger
toggleNotificationTray={toggleNotificationTray}
isNotificationTrayVisible={isNotificationTrayVisible}
/>
) : null}
</div>
@@ -118,9 +118,9 @@ function Course({
unitNavigationHandler={unitNavigationHandler}
nextSequenceHandler={nextSequenceHandler}
previousSequenceHandler={previousSequenceHandler}
toggleSidebar={toggleSidebar}
isSidebarVisible={isSidebarVisible}
sidebarVisible={sidebarVisible}
toggleNotificationTray={toggleNotificationTray}
isNotificationTrayVisible={isNotificationTrayVisible}
notificationTrayVisible={notificationTrayVisible}
isValuePropCookieSet={isValuePropCookieSet}
//* * [MM-P2P] Experiment */
mmp2p={MMP2P}

View File

@@ -20,7 +20,7 @@ describe('Course', () => {
nextSequenceHandler: () => {},
previousSequenceHandler: () => {},
unitNavigationHandler: () => {},
toggleSidebar: () => {},
toggleNotificationTray: () => {},
};
beforeAll(async () => {
@@ -37,7 +37,7 @@ describe('Course', () => {
it('loads learning sequence', async () => {
render(<Course {...mockData} />);
expect(screen.getByRole('navigation', { name: 'breadcrumb' })).toBeInTheDocument();
expect(screen.getByText('Loading learning sequence...')).toBeInTheDocument();
expect(await screen.findByText('Loading learning sequence...')).toBeInTheDocument();
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
@@ -87,11 +87,11 @@ describe('Course', () => {
expect(screen.getByRole('button', { name: 'Learn About Verified Certificates' })).toBeInTheDocument();
});
it('displays sidebar notification button', async () => {
const toggleSidebar = jest.fn();
const isSidebarVisible = jest.fn();
it('displays notification trigger', async () => {
const toggleNotificationTray = jest.fn();
const isNotificationTrayVisible = jest.fn();
// REV-2130 TODO: remove cookie related code once temporary value prop cookie code is removed.
// REV-2297 TODO: remove cookie related code once temporary value prop cookie code is removed.
const cookieName = 'value_prop_cookie';
Cookies.set = jest.fn();
Cookies.get = jest.fn().mockImplementation(() => cookieName);
@@ -101,15 +101,15 @@ describe('Course', () => {
const testStore = await initializeTestStore({ courseMetadata, excludeFetchSequence: true }, false);
const testData = {
...mockData,
toggleSidebar,
isSidebarVisible,
toggleNotificationTray,
isNotificationTrayVisible,
};
render(<Course {...testData} courseId={courseMetadata.id} />, { store: testStore });
const sidebarOpenButton = screen.getByRole('button', { name: /Show sidebar notification/i });
const notificationOpenButton = screen.getByRole('button', { name: /Show notification tray/i });
expect(getSpy).toBeCalledWith(cookieName);
expect(sidebarOpenButton).toBeInTheDocument();
expect(notificationOpenButton).toBeInTheDocument();
});
it('displays offer and expiration alert', async () => {

View File

@@ -11,7 +11,7 @@ import messages from './messages';
function NotificationIcon({ intl, status, notificationColor }) {
return (
<div className="icon-container">
<Icon src={WatchOutline} className="m-0 m-auto" alt={intl.formatMessage(messages.openSidebarButton)} />
<Icon src={WatchOutline} className="m-0 m-auto" alt={intl.formatMessage(messages.openNotificationTrigger)} />
{status === 'active'
? <span className={classNames(notificationColor, 'notification-dot')} data-testid="notification-dot" />
: null}

View File

@@ -0,0 +1,78 @@
import React from 'react';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Icon } from '@edx/paragon';
import { ArrowBackIos, Close } from '@edx/paragon/icons';
import messages from './messages';
import { useModel } from '../../generic/model-store';
import useWindowSize, { responsiveBreakpoints } from '../../generic/tabs/useWindowSize';
import UpgradeNotification from '../../generic/upgrade-notification/UpgradeNotification';
function NotificationTray({
intl, toggleNotificationTray,
}) {
const {
courseId,
} = useSelector(state => state.courseware);
const course = useModel('coursewareMeta', courseId);
const {
accessExpiration,
contentTypeGatingEnabled,
offer,
org,
timeOffsetMillis,
userTimezone,
verifiedMode,
} = course;
const shouldDisplayFullScreen = useWindowSize().width < responsiveBreakpoints.large.minWidth;
return (
<section className={classNames('notification-tray-container ml-0 ml-lg-4', { 'no-notification': !verifiedMode && !shouldDisplayFullScreen })} aria-label={intl.formatMessage(messages.notificationTray)}>
{shouldDisplayFullScreen ? (
<div className="mobile-close-container" onClick={() => { toggleNotificationTray(); }} onKeyDown={() => { toggleNotificationTray(); }} role="button" tabIndex="0" alt={intl.formatMessage(messages.responsiveCloseNotificationTray)}>
<Icon src={ArrowBackIos} />
<span className="mobile-close">{intl.formatMessage(messages.responsiveCloseNotificationTray)}</span>
</div>
) : null}
<div className="notification-tray-header px-3">
<span>{intl.formatMessage(messages.notificationTitle)}</span>
{shouldDisplayFullScreen
? null
: <Icon src={Close} className="close-btn" onClick={() => { toggleNotificationTray(); }} onKeyDown={() => { toggleNotificationTray(); }} role="button" tabIndex="0" alt={intl.formatMessage(messages.closeNotificationTrigger)} />}
</div>
<div className="notification-tray-divider" />
<div>{verifiedMode
? (
<UpgradeNotification
offer={offer}
verifiedMode={verifiedMode}
accessExpiration={accessExpiration}
contentTypeGatingEnabled={contentTypeGatingEnabled}
userTimezone={userTimezone}
timeOffsetMillis={timeOffsetMillis}
courseId={courseId}
org={org}
shouldDisplayBorder={false}
/>
) : <p className="notification-tray-content">{intl.formatMessage(messages.noNotificationsMessage)}</p>}
</div>
</section>
);
}
NotificationTray.propTypes = {
intl: intlShape.isRequired,
toggleNotificationTray: PropTypes.func,
};
NotificationTray.defaultProps = {
toggleNotificationTray: null,
};
export default injectIntl(NotificationTray);

View File

@@ -1,8 +1,9 @@
.sidebar-container {
.notification-tray-container {
border: 1px solid $light-400;
border-radius: 4px;
width: 20rem;
width: 31rem;
vertical-align: top;
height: 100%;
@media (max-width: -1 + map-get($grid-breakpoints, 'lg')) {
position: fixed;
@@ -11,7 +12,6 @@
left: 0;
right: 0;
width: 100%;
height: 100% !important;
background-color: white;
margin: 0;
border: none;
@@ -23,7 +23,7 @@
height: 15rem;
}
.sidebar-header {
.notification-tray-header {
padding: 0.625rem 0;
span {
@@ -35,7 +35,7 @@
float: right;
}
.sidebar-divider {
.notification-tray-divider {
width: 100.5%;
height: 0.5rem;
background: $gray-100;
@@ -43,7 +43,7 @@
border-left: 0;
}
.sidebar-content {
.notification-tray-content {
padding: 1rem;
font-size: 0.875rem;
}

View File

@@ -0,0 +1,87 @@
import React from 'react';
import { Factory } from 'rosie';
import MockAdapter from 'axios-mock-adapter';
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { fetchCourse } from '../data';
import {
render, initializeMockApp, screen, fireEvent, waitFor,
} from '../../setupTest';
import initializeStore from '../../store';
import { appendBrowserTimezoneToUrl, executeThunk } from '../../utils';
import NotificationTray from './NotificationTray';
import useWindowSize from '../../generic/tabs/useWindowSize';
initializeMockApp();
jest.mock('../../generic/tabs/useWindowSize');
jest.mock('@edx/frontend-platform/analytics');
describe('NotificationTray', () => {
let mockData;
let axiosMock;
let store;
const courseId = 'course-v1:edX+DemoX+Demo_Course';
const defaultMetadata = Factory.build('courseMetadata', { id: courseId });
let courseMetadataUrl = `${getConfig().LMS_BASE_URL}/api/courseware/course/${defaultMetadata.id}`;
courseMetadataUrl = appendBrowserTimezoneToUrl(courseMetadataUrl);
function setMetadata(attributes, options) {
const courseMetadata = Factory.build('courseMetadata', { id: courseId, ...attributes }, options);
axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata);
}
async function fetchAndRender(component) {
await executeThunk(fetchCourse(defaultMetadata.id), store.dispatch);
render(component, { store });
}
beforeEach(async () => {
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock.onGet(courseMetadataUrl).reply(200, defaultMetadata);
mockData = {
toggleNotificationTray: () => {},
};
});
it('renders notification tray', async () => {
useWindowSize.mockReturnValue({ width: 1200, height: 422 });
await fetchAndRender(<NotificationTray />);
expect(screen.getByText('Notifications')).toBeInTheDocument();
expect(screen.queryByText('Back to course')).not.toBeInTheDocument();
});
it('renders upgrade card', async () => {
await fetchAndRender(<NotificationTray />);
const UpgradeNotification = document.querySelector('.upgrade-notification');
expect(UpgradeNotification).toBeInTheDocument();
expect(screen.getByRole('link', { name: 'Upgrade for $149' })).toBeInTheDocument();
expect(screen.queryByText('You have no new notifications at this time.')).not.toBeInTheDocument();
});
it('renders no notifications message if no verified mode', async () => {
setMetadata({ verified_mode: null });
await fetchAndRender(<NotificationTray />);
expect(screen.queryByText('You have no new notifications at this time.')).toBeInTheDocument();
});
it('renders notification tray with full screen "Back to course" at response width', async () => {
useWindowSize.mockReturnValue({ width: 991, height: 422 });
const toggleNotificationTray = jest.fn();
const testData = {
...mockData,
toggleNotificationTray,
};
await fetchAndRender(<NotificationTray {...testData} />);
const responsiveCloseButton = screen.getByRole('button', { name: 'Back to course' });
await waitFor(() => expect(responsiveCloseButton).toBeInTheDocument());
fireEvent.click(responsiveCloseButton);
expect(toggleNotificationTray).toHaveBeenCalledTimes(1);
});
});

View File

@@ -0,0 +1,29 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import NotificationIcon from './NotificationIcon';
import messages from './messages';
function NotificationTrigger({ intl, toggleNotificationTray, isNotificationTrayVisible }) {
return (
<button
className={classNames('notification-trigger-btn', { active: isNotificationTrayVisible() })}
type="button"
onClick={() => { toggleNotificationTray(); }}
aria-label={intl.formatMessage(messages.openNotificationTrigger)}
>
{/* REV-2297 TODO: add logic for status "active" if red dot should display */}
<NotificationIcon status="inactive" notificationColor="bg-danger-500" />
</button>
);
}
NotificationTrigger.propTypes = {
intl: intlShape.isRequired,
toggleNotificationTray: PropTypes.func.isRequired,
isNotificationTrayVisible: PropTypes.func.isRequired,
};
export default injectIntl(NotificationTrigger);

View File

@@ -1,4 +1,4 @@
.sidebar-notification-btn {
.notification-trigger-btn {
border: 1px solid $light-400;
background: none;
margin-top: 0.625rem;

View File

@@ -0,0 +1,43 @@
import React from 'react';
import { Factory } from 'rosie';
import {
render, initializeTestStore, screen, fireEvent,
} from '../../setupTest';
import NotificationTrigger from './NotificationTrigger';
describe('Notification Trigger', () => {
let mockData;
const courseMetadata = Factory.build('courseMetadata');
beforeAll(async () => {
await initializeTestStore({ courseMetadata, excludeFetchCourse: true, excludeFetchSequence: true });
mockData = {
toggleNotificationTray: () => {},
isNotificationTrayVisible: () => {},
};
});
it('renders notification trigger with icon', async () => {
const { container } = render(<NotificationTrigger {...mockData} />);
expect(container).toBeInTheDocument();
const buttonIcon = container.querySelectorAll('svg');
expect(buttonIcon).toHaveLength(1);
// REV-2297 TODO: update below test once the status=active or inactive is implemented
// expect(screen.getByTestId('notification-dot')).toBeInTheDocument();
});
it('handles onClick event toggling the notification tray', async () => {
const toggleNotificationTray = jest.fn();
const testData = {
...mockData,
toggleNotificationTray,
};
render(<NotificationTrigger {...testData} />);
const notificationOpenButton = screen.getByRole('button', { name: /Show notification tray/i });
expect(notificationOpenButton).toBeInTheDocument();
fireEvent.click(notificationOpenButton);
expect(toggleNotificationTray).toHaveBeenCalledTimes(1);
});
});

View File

@@ -1,51 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Icon } from '@edx/paragon';
import { ArrowBackIos, Close } from '@edx/paragon/icons';
import messages from './messages';
import useWindowSize, { responsiveBreakpoints } from '../../generic/tabs/useWindowSize';
function Sidebar({
intl, toggleSidebar,
}) {
const shouldDisplayFullScreen = useWindowSize().width < responsiveBreakpoints.large.minWidth;
// REV-2130 TODO: temporary variable set to true, should be replaced with
// whether the course can be upgraded (ie. shouldDisplayUpgradeNotification)
const shouldDisplayNoNotification = true;
return (
<section className={classNames('sidebar-container ml-0 ml-lg-4', { 'no-notification': shouldDisplayNoNotification && !shouldDisplayFullScreen })} aria-label={intl.formatMessage(messages.sidebarNotification)}>
{shouldDisplayFullScreen ? (
<div className="mobile-close-container" onClick={() => { toggleSidebar(); }} onKeyDown={() => { toggleSidebar(); }} role="button" tabIndex="0" alt={intl.formatMessage(messages.responsiveCloseSidebar)}>
<Icon src={ArrowBackIos} />
<span className="mobile-close">{intl.formatMessage(messages.responsiveCloseSidebar)}</span>
</div>
) : null}
<div className="sidebar-header px-3">
<span>{intl.formatMessage(messages.notificationTitle)}</span>
{shouldDisplayFullScreen
? null
: <Icon src={Close} className="close-btn" onClick={() => { toggleSidebar(); }} onKeyDown={() => { toggleSidebar(); }} role="button" tabIndex="0" alt={intl.formatMessage(messages.closeSidebarButton)} />}
</div>
<div className="sidebar-divider" />
<div className="sidebar-content">
{/* REV-2130 TODO: replace logic to display upgrade expiration box if condition is true */}
{shouldDisplayNoNotification ? <p>{intl.formatMessage(messages.noNotificationsMessage)}</p> : null}
</div>
</section>
);
}
Sidebar.propTypes = {
intl: intlShape.isRequired,
toggleSidebar: PropTypes.func,
};
Sidebar.defaultProps = {
toggleSidebar: null,
};
export default injectIntl(Sidebar);

View File

@@ -1,58 +0,0 @@
import React from 'react';
import { Factory } from 'rosie';
import {
render, initializeTestStore, screen, fireEvent, waitFor,
} from '../../setupTest';
import Sidebar from './Sidebar';
import useWindowSize from '../../generic/tabs/useWindowSize';
jest.mock('../../generic/tabs/useWindowSize');
describe('Sidebar', () => {
let mockData;
const courseMetadata = Factory.build('courseMetadata');
beforeEach(async () => {
mockData = {
toggleSidebar: () => {},
};
});
beforeAll(async () => {
await initializeTestStore({ courseMetadata, excludeFetchCourse: true, excludeFetchSequence: true });
});
it('renders sidebar', async () => {
useWindowSize.mockReturnValue({ width: 1200, height: 422 });
const { container } = render(<Sidebar {...mockData} />);
expect(container).toBeInTheDocument();
expect(container).toHaveTextContent('Notifications');
expect(container).not.toHaveTextContent('Back to course');
});
it('renders no notifications message', async () => {
// REV-2130 TODO: add conditional if no expiration box/upgradeable
const testData = { ...mockData };
const { container } = render(<Sidebar {...testData} />);
expect(container).toBeInTheDocument();
expect(container).toHaveTextContent('You have no new notifications at this time.');
});
it('renders sidebar with full screen "Back to course" at response width', async () => {
useWindowSize.mockReturnValue({ width: 991, height: 422 });
const toggleSidebar = jest.fn();
const testData = {
...mockData,
toggleSidebar,
};
render(<Sidebar {...testData} />);
const responsiveCloseButton = screen.getByRole('button', { name: 'Back to course' });
await waitFor(() => expect(responsiveCloseButton).toBeInTheDocument());
fireEvent.click(responsiveCloseButton);
expect(toggleSidebar).toHaveBeenCalledTimes(1);
});
});

View File

@@ -1,29 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import NotificationIcon from './NotificationIcon';
import messages from './messages';
function SidebarNotificationButton({ intl, toggleSidebar, isSidebarVisible }) {
return (
<button
className={classNames('sidebar-notification-btn', { active: isSidebarVisible() })}
type="button"
onClick={() => { toggleSidebar(); }}
aria-label={intl.formatMessage(messages.openSidebarButton)}
>
{/* REV-2130 TODO: add logic for status "active" if red dot should display */}
<NotificationIcon status="active" notificationColor="bg-danger-500" />
</button>
);
}
SidebarNotificationButton.propTypes = {
intl: intlShape.isRequired,
toggleSidebar: PropTypes.func.isRequired,
isSidebarVisible: PropTypes.func.isRequired,
};
export default injectIntl(SidebarNotificationButton);

View File

@@ -1,43 +0,0 @@
import React from 'react';
import { Factory } from 'rosie';
import {
render, initializeTestStore, screen, fireEvent,
} from '../../setupTest';
import SidebarNotificationButton from './SidebarNotificationButton';
describe('Sidebar Notification Button', () => {
let mockData;
const courseMetadata = Factory.build('courseMetadata');
beforeAll(async () => {
await initializeTestStore({ courseMetadata, excludeFetchCourse: true, excludeFetchSequence: true });
mockData = {
toggleSidebar: () => {},
isSidebarVisible: () => {},
};
});
it('renders sidebar notification button with icon', async () => {
const { container } = render(<SidebarNotificationButton {...mockData} />);
expect(container).toBeInTheDocument();
const buttonIcon = container.querySelectorAll('svg');
expect(buttonIcon).toHaveLength(1);
// REV-2130 TODO: update below test once the status=active or inactive is implemented
expect(screen.getByTestId('notification-dot')).toBeInTheDocument();
});
it('handles onClick event toggling the sidebar', async () => {
const toggleSidebar = jest.fn();
const testData = {
...mockData,
toggleSidebar,
};
render(<SidebarNotificationButton {...testData} />);
const sidebarOpenButton = screen.getByRole('button', { name: /Show sidebar notification/i });
expect(sidebarOpenButton).toBeInTheDocument();
fireEvent.click(sidebarOpenButton);
expect(toggleSidebar).toHaveBeenCalledTimes(1);
});
});

View File

@@ -33,7 +33,7 @@ function CatalogSuggestion({ intl, variant }) {
);
return (
<div className="row w-100 mx-0 my-2 justify-content-center">
<div className="row w-100 mx-0 my-2 justify-content-center" data-testid="catalog-suggestion">
<div className="col col-md-8 p-4 bg-info-100 text-center">
<FontAwesomeIcon icon={faSearch} style={{ width: '20px' }} />&nbsp;
<FormattedMessage

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faLinkedinIn } from '@fortawesome/free-brands-svg-icons';
@@ -12,7 +12,6 @@ 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_certificate.png';
@@ -27,7 +26,7 @@ import UpgradeFootnote from './UpgradeFootnote';
import SocialIcons from '../../social-share/SocialIcons';
import { logClick, logVisit } from './utils';
import { DashboardLink, IdVerificationSupportLink, ProfileLink } from '../../../shared/links';
import CourseRecommendations from './CourseRecommendationsExp/CourseRecommendations.exp';
import CourseRecommendations from './CourseRecommendations';
const LINKEDIN_BLUE = '#2867B2';
@@ -60,12 +59,9 @@ function CourseCelebration({ intl }) {
certStatus,
certWebViewUrl,
downloadUrl,
certificateAvailableDate,
} = certificateData || {};
/** [WS-1681 experiment] */
const [showWS1681, setShowWS1681] = useState(window.experiment__courseware_celebration_bShowWS1681);
useEffect(() => { setShowWS1681(window.experiment__courseware_celebration_bShowWS1681); });
const { administrator } = getAuthenticatedUser();
const dashboardLink = <DashboardLink />;
@@ -125,25 +121,20 @@ function CourseCelebration({ intl }) {
break;
case 'earned_but_not_available': {
const endDate = <FormattedDate value={end} day="numeric" month="long" year="numeric" />;
const certAvailableDate = <FormattedDate value={certificateAvailableDate} day="numeric" month="long" year="numeric" />;
certHeader = intl.formatMessage(messages.certificateHeaderNotAvailable);
message = (
<>
<p>
<FormattedMessage
id="courseCelebration.certificateBody.notAvailable.endDate"
defaultMessage="After this course officially ends on {endDate}, you will receive an
email notification with your certificate. Once you have your certificate, be sure
to showcase your accomplishment on LinkedIn or your resumé."
values={{ endDate }}
defaultMessage="This course ended on {endDate} and final grades and certificates are scheduled to be
available after {certAvailableDate}."
values={{ endDate, certAvailableDate }}
/>
</p>
<p>
<FormattedMessage
id="courseCelebration.certificateBody.notAvailable.accessCertificate"
defaultMessage="You will be able to access your certificate any time from your
{dashboardLink} and {profileLink}."
values={{ dashboardLink, profileLink }}
/>
{intl.formatMessage(messages.certificateNotAvailableBodyAccessCert)}
</p>
</>
);
@@ -294,7 +285,7 @@ function CourseCelebration({ intl }) {
</div>
<div className="col-12 px-0 px-md-5">
{certHeader && (
<Alert variant="primary" className="row w-100 m-0">
<Alert variant="success" className="row w-100 m-0">
<div className="col order-1 order-md-0 pl-0 pr-0 pr-md-5">
<div className="h4">{certHeader}</div>
{message}
@@ -334,9 +325,7 @@ function CourseCelebration({ intl }) {
/>
))}
{footnote}
{ showWS1681 && <CourseRecommendations variant={visitEvent} />}
{ !showWS1681 && <CatalogSuggestion variant={visitEvent} /> }
<CourseRecommendations variant={visitEvent} />
</div>
</div>
</>

View File

@@ -21,7 +21,7 @@ jest.mock('@edx/frontend-platform/analytics');
describe('Course Exit Pages', () => {
let axiosMock;
const store = initializeStore();
let store;
const defaultMetadata = Factory.build('courseMetadata', {
user_has_passing_grade: true,
end: '2014-02-05T05:00:00Z',
@@ -31,6 +31,9 @@ describe('Course Exit Pages', () => {
let courseMetadataUrl = `${getConfig().LMS_BASE_URL}/api/courseware/course/${defaultMetadata.id}`;
courseMetadataUrl = appendBrowserTimezoneToUrl(courseMetadataUrl);
const courseBlocksUrlRegExp = new RegExp(`${getConfig().LMS_BASE_URL}/api/courses/v2/blocks/*`);
const discoveryRecommendationsUrl = new RegExp(`${getConfig().DISCOVERY_API_BASE_URL}/api/v1/course_recommendations/*`);
const enrollmentsUrl = new RegExp(`${getConfig().LMS_BASE_URL}/api/enrollment/v1/enrollment*`);
const learningSequencesUrlRegExp = new RegExp(`${getConfig().LMS_BASE_URL}/api/learning_sequences/v1/course_outline/*`);
function setMetadata(attributes) {
const courseMetadata = { ...defaultMetadata, ...attributes };
@@ -43,9 +46,14 @@ describe('Course Exit Pages', () => {
}
beforeEach(() => {
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock.onGet(courseMetadataUrl).reply(200, defaultMetadata);
axiosMock.onGet(courseBlocksUrlRegExp).reply(200, defaultCourseBlocks);
axiosMock.onGet(discoveryRecommendationsUrl).reply(200,
Factory.build('courseRecommendations', {}, { numRecs: 2 }));
axiosMock.onGet(enrollmentsUrl).reply(200, []);
axiosMock.onGet(learningSequencesUrlRegExp).reply(403, {});
logUnhandledRequests(axiosMock);
});
@@ -115,9 +123,14 @@ describe('Course Exit Pages', () => {
});
it('Displays certificate is earned but unavailable message', async () => {
setMetadata({ certificate_data: { cert_status: 'earned_but_not_available' } });
setMetadata({
certificate_data: {
cert_status: 'earned_but_not_available',
certificate_available_date: '2021-05-21T12:00:00Z',
},
});
await fetchAndRender(<CourseCelebration />);
expect(screen.getByText('Your certificate will be available soon!')).toBeInTheDocument();
expect(screen.getByText('Your grade and certificate will be ready soon!')).toBeInTheDocument();
});
it('Displays request certificate link', async () => {
@@ -285,6 +298,61 @@ describe('Course Exit Pages', () => {
expect(screen.queryByTestId('excluded-program-type')).not.toBeInTheDocument();
});
});
describe('Course recommendations', () => {
it('Displays recommendations if at least two are available', async () => {
await fetchAndRender(<CourseCelebration />);
const recommendationsTable = await screen.findByTestId('course-recommendations');
expect(recommendationsTable).toBeInTheDocument();
expect(screen.queryByTestId('catalog-suggestion')).not.toBeInTheDocument();
});
it('Displays the generic catalog suggestion if fewer than two recommendations are available', async () => {
axiosMock.onGet(discoveryRecommendationsUrl).reply(200,
Factory.build('courseRecommendations', {}, { numRecs: 1 }));
await fetchAndRender(<CourseCelebration />);
const catalogSuggestion = await screen.findByTestId('catalog-suggestion');
expect(catalogSuggestion).toBeInTheDocument();
expect(screen.queryByTestId('course-recommendations')).not.toBeInTheDocument();
});
it('Will not recommend a course in which the user is already enrolled', async () => {
const initialRecommendations = Factory.build('courseRecommendations', {}, { numRecs: 2 });
initialRecommendations.recommendations.push(
Factory.build('courseRecommendation', { key: 'edX+EnrolledX', title: 'Already Enrolled' }),
);
initialRecommendations.recommendations.push(
Factory.build('courseRecommendation', { key: 'edX+NotEnrolledX', title: 'Not Already Enrolled' }),
);
axiosMock.onGet(discoveryRecommendationsUrl).reply(200, initialRecommendations);
axiosMock.onGet(enrollmentsUrl).reply(200, [
Factory.build('userEnrollment', '',
{
runKey: 'edX+EnrolledX+1T2021',
}),
]);
await fetchAndRender(<CourseCelebration />);
const recommendationsTable = await screen.findByTestId('course-recommendations');
expect(recommendationsTable).toBeInTheDocument();
expect(screen.queryByText('Already Enrolled')).not.toBeInTheDocument();
expect(screen.queryByText('Not Already Enrolled')).toBeInTheDocument();
});
it('Will not recommend the same course that the user just finished', async () => {
// the uuid returned from the call to discovery is the uuid of the current course
const initialRecommendations = Factory.build('courseRecommendations',
{ uuid: 'my_uuid' },
{ numRecs: 2 });
initialRecommendations.recommendations.push(
Factory.build('courseRecommendation', { uuid: 'my_uuid', title: 'Same Course' }),
);
axiosMock.onGet(discoveryRecommendationsUrl).reply(200, initialRecommendations);
await fetchAndRender(<CourseCelebration />);
const recommendationsTable = await screen.findByTestId('course-recommendations');
expect(recommendationsTable).toBeInTheDocument();
expect(screen.queryByText('Same Course')).not.toBeInTheDocument();
});
});
});
describe('Course Non-passing Experience', () => {
@@ -302,6 +370,7 @@ describe('Course Exit Pages', () => {
const courseBlocks = buildSimpleCourseBlocks(defaultMetadata.id, defaultMetadata.name,
{ hasScheduledContent: true });
axiosMock.onGet(courseBlocksUrlRegExp).reply(200, courseBlocks);
axiosMock.onGet(learningSequencesUrlRegExp).reply(403, {});
await fetchAndRender(<CourseInProgress />);
expect(screen.getByText('More content is coming soon!')).toBeInTheDocument();

View File

@@ -6,15 +6,17 @@ import {
FormattedMessage, injectIntl, intlShape, defineMessages,
} from '@edx/frontend-platform/i18n';
import { useSelector, useDispatch } from 'react-redux';
import { Hyperlink, DataTable, CardView } from '@edx/paragon';
import {
Hyperlink, DataTable, CardView, Card,
} from '@edx/paragon';
import PropTypes from 'prop-types';
import truncate from 'truncate-html';
import { useModel } from '../../../../generic/model-store';
import fetchCourseRecommendations from './data/thunks.exp';
import { FAILED, LOADED, LOADING } from './data/slice.exp';
import CatalogSuggestion from '../CatalogSuggestion';
import PageLoading from '../../../../generic/PageLoading';
import { logClick } from '../utils';
import { useModel } from '../../../generic/model-store/hooks';
import fetchCourseRecommendations from './data/thunks';
import { FAILED, LOADED, LOADING } from './data/slice';
import CatalogSuggestion from './CatalogSuggestion';
import PageLoading from '../../../generic/PageLoading';
import { logClick } from './utils';
const messages = defineMessages({
recommendationsHeading: {
@@ -48,8 +50,7 @@ const ListStyles = {
conjunction: 'conjunction',
};
// TODO: replace custom card (copied from Prospectus) with Paragon Card component
function Card({
function CourseCard({
original: {
title,
image,
@@ -74,24 +75,23 @@ function Card({
return (
<div
className="discovery-card"
role="group"
aria-label={title}
>
<Hyperlink
destination={marketingUrl}
className="discovery-card-link"
className="text-decoration-none"
onClick={onClick}
>
<div className="d-flex flex-column d-card-wrapper">
<div className="d-card-hero">
<img src={image.src} alt="" />
</div>
<div className="d-card-body">
<h3 className="name-heading">
{truncate(title, 70, { reserveLastWord: -1 })}
</h3>
<div className="provider">
<Card style={{ width: '270px', height: '270px' }} className="discovery-card">
<Card.Img variant="top" src={image.src} bsPrefix="d-card-hero" />
<Card.Body>
<Card.Title>
<h3 className="h4 text-gray-700 font-weight-normal">
{truncate(title, 70, { reserveLastWord: -1 })}
</h3>
</Card.Title>
<div className="text-gray-500 small">
<FormattedMessage
id="courseCelebration.recommendations.card.schools.label"
description="Screenreader label for the Schools and Partners running the course."
@@ -104,23 +104,22 @@ function Card({
)}
</FormattedMessage>
</div>
</div>
<div className="d-card-footer">
<div className="card-type">
<FormattedMessage
id="courseCelebration.recommendations.label"
description="Label on a discovery-card that lets a user know that it is a course card"
defaultMessage="Course"
/>
</div>
</div>
</div>
</Card.Body>
<footer className="pl-4 pb-2 x-small text-gray-500">
<FormattedMessage
id="courseCelebration.recommendations.label"
description="Label on a discovery-card that lets a user know that it is a course card"
defaultMessage="Course"
/>
</footer>
</Card>
</Hyperlink>
</div>
);
}
Card.propTypes = {
CourseCard.propTypes = {
original: PropTypes.shape({
marketingUrl: PropTypes.string,
title: PropTypes.string,
@@ -135,7 +134,7 @@ Card.propTypes = {
intl: intlShape.isRequired,
};
const IntlCard = injectIntl(Card);
const IntlCard = injectIntl(CourseCard);
function CourseRecommendations({ intl, variant }) {
const { courseId, recommendationsStatus } = useSelector(state => ({ ...state.recommendations, ...state.courseware }));
@@ -178,7 +177,7 @@ function CourseRecommendations({ intl, variant }) {
));
return (
<div className="course-recommendations d-flex flex-column align-items-center">
<div className="course-recommendations d-flex flex-column align-items-center" data-testid="course-recommendations">
<h2 className="text-center mb-3">{intl.formatMessage(messages.recommendationsHeading)}</h2>
<div className="mb-2 mt-3">
<DataTable

View File

@@ -0,0 +1,30 @@
.course-recommendations {
.pgn__data-table-wrapper {
border: 0;
box-shadow: none;
.pgn__card-grid {
.row > div[class*="col-"] {
justify-content: center;
}
}
}
.discovery-card {
&:hover,
&:focus {
box-shadow: 0 2px 4px 2px $gray-500
}
}
.d-card-hero-top {
height: 102px;
min-height: 102px;
background-color: $gray-200;
overflow: hidden;
border: {
radius: 3px 3px 0 0;
bottom: 1px solid $success-100;
}
}
}

View File

@@ -1,111 +0,0 @@
$default-border-color: $info-300;
.course-recommendations {
.pgn__data-table-wrapper {
border: 0;
.pgn__card-grid {
.row > div[class*="col-"] {
justify-content: center;
}
}
}
.discovery-card {
min-width: 270px;
max-width: 270px;
width: 270px;
height: 270px;
position: relative;
border-bottom: 3px solid $default-border-color;
background-color: $white;
box-shadow: none;
padding: 0;
border: none;
&.custom-link {
background: none;
}
.d-card-wrapper {
height: 270px;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.3);
border: {
color: $primary-200;
width: 1px;
radius: 3px;
}
}
.discovery-card-link {
text-decoration: none;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
&:focus,
&:hover {
border: 0;
outline: none;
.d-card-wrapper {
box-shadow: 0 2px 4px 2px $gray-500;
}
}
}
.d-card-hero {
height: 102px;
background-color: $gray-200;
overflow: hidden;
border: {
radius: 3px 3px 0 0;
bottom: 1px solid $success-100;
}
}
.d-card-body {
padding: 28px 20px 33px;
}
.d-card-footer {
padding: 0 20px;
}
.name-heading {
height: auto;
line-height: 1.15;
color: $gray-700;
font: {
family: $font-family-sans-serif;
size: 1.25rem;
weight: 500;
}
}
.provider {
line-height: 0.86;
color: $gray-500;
margin-bottom: 20px;
font: {
family: $font-family-sans-serif;
size: 0.875rem;
weight: $font-weight-normal;
}
}
.card-type {
height: 20px;
line-height: 1.67;
letter-spacing: 0.2px;
color: $gray-500;
position: absolute;
bottom: 10px;
font: {
family: $font-family-sans-serif;
size: 0.75rem;
weight: $font-weight-normal;
}
}
}
}

View File

@@ -4,9 +4,9 @@ import {
fetchCourseRecommendationsFailure,
fetchCourseRecommendationsRequest,
fetchCourseRecommendationsSuccess,
} from './slice.exp';
import getCourseRecommendations from './api.exp';
import { updateModel } from '../../../../../generic/model-store';
} from './slice';
import getCourseRecommendations from './api';
import { updateModel } from '../../../../generic/model-store';
export default function fetchCourseRecommendations(courseKey, courseId) {
return async (dispatch) => {

View File

@@ -13,7 +13,12 @@ const messages = defineMessages({
},
certificateHeaderNotAvailable: {
id: 'courseCelebration.certificateHeader.notAvailable',
defaultMessage: 'Your certificate will be available soon!',
defaultMessage: 'Your grade and certificate will be ready soon!',
description: 'Header displayed when course certificate is not yet available to be viewed',
},
certificateNotAvailableBodyAccessCert: {
id: 'courseCelebration.certificateBody.notAvailable.accessCertificate',
defaultMessage: 'If you have earned a passing grade, your certificate will be automatically issued.',
description: 'Text displayed when course certificate is not yet available to be viewed',
},
certificateHeaderUnverified: {
@@ -180,6 +185,7 @@ const messages = defineMessages({
id: 'courseExit.viewGradesButton',
defaultMessage: 'View grades',
},
});
export default messages;

View File

@@ -1,33 +1,33 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
sidebarNotification: {
id: 'sidebar.notification.container',
defaultMessage: 'Sidebar notification',
description: 'Sidebar notification section container',
notificationTray: {
id: 'notification.tray.container',
defaultMessage: 'Notification tray',
description: 'Notification tray container',
},
openSidebarButton: {
id: 'sidebar.open.button',
defaultMessage: 'Show sidebar notification',
description: 'Button to open the sidebar and show notifications',
openNotificationTrigger: {
id: 'notification.open.button',
defaultMessage: 'Show notification tray',
description: 'Button to open the notification tray and show notifications',
},
closeSidebarButton: {
id: 'sidebar.close.button',
defaultMessage: 'Close sidebar notification',
description: 'Button for the learner to close the sidebar',
closeNotificationTrigger: {
id: 'notification.close.button',
defaultMessage: 'Close notification tray',
description: 'Button to close the notification tray',
},
responsiveCloseSidebar: {
id: 'sidebar.responsive.close.button',
responsiveCloseNotificationTray: {
id: 'responsive.close.notification',
defaultMessage: 'Back to course',
description: 'Responsive button for the learner to go back to course and close the sidebar',
description: 'Responsive button to go back to course and close the notification tray',
},
notificationTitle: {
id: 'sidebar.notification.title',
id: 'notification.tray.title',
defaultMessage: 'Notifications',
description: 'Title text displayed for sidebar notifications',
description: 'Title text displayed for the notification tray',
},
noNotificationsMessage: {
id: 'sidebar.notification.no.message',
id: 'notification.tray.no.message',
defaultMessage: 'You have no new notifications at this time.',
description: 'Text displayed when the learner has no notifications',
},

View File

@@ -11,6 +11,7 @@ import {
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useSelector } from 'react-redux';
import { history } from '@edx/frontend-platform';
import SequenceExamWrapper from '@edx/frontend-lib-special-exams';
import PageLoading from '../../../generic/PageLoading';
import { UserMessagesContext, ALERT_TYPES } from '../../../generic/user-messages';
@@ -21,8 +22,8 @@ import CourseLicense from '../course-license';
import messages from './messages';
import { SequenceNavigation, UnitNavigation } from './sequence-navigation';
import SequenceContent from './SequenceContent';
import Sidebar from '../Sidebar';
import SidebarNotificationButton from '../SidebarNotificationButton';
import NotificationTray from '../NotificationTray';
import NotificationTrigger from '../NotificationTrigger';
/** [MM-P2P] Experiment */
import { isMobile } from '../../../experiments/mm-p2p/utils';
@@ -36,9 +37,9 @@ function Sequence({
nextSequenceHandler,
previousSequenceHandler,
intl,
toggleSidebar,
sidebarVisible,
isSidebarVisible,
toggleNotificationTray,
notificationTrayVisible,
isNotificationTrayVisible,
isValuePropCookieSet,
mmp2p,
}) {
@@ -46,7 +47,9 @@ function Sequence({
const sequence = useModel('sequences', sequenceId);
const unit = useModel('units', unitId);
const sequenceStatus = useSelector(state => state.courseware.sequenceStatus);
const shouldDisplaySidebarButton = useWindowSize().width < responsiveBreakpoints.small.minWidth;
const specialExamsEnabledWaffleFlag = useSelector(state => state.courseware.specialExamsEnabledWaffleFlag);
const proctoredExamsEnabledWaffleFlag = useSelector(state => state.courseware.proctoredExamsEnabledWaffleFlag);
const shouldDisplayNotificationTrigger = useWindowSize().width < responsiveBreakpoints.small.minWidth;
const handleNext = () => {
const nextIndex = sequence.unitIds.indexOf(unitId) + 1;
@@ -115,11 +118,16 @@ function Sequence({
const handleUnitLoaded = () => {
setUnitHasLoaded(true);
};
// We want hide the unit navigation if we're in the middle of navigating to another unit
// but not if other things about the unit change, like the bookmark status.
// The array property of this useEffect ensures that we only hide the unit navigation
// while navigating to another unit.
useEffect(() => {
if (unit) {
setUnitHasLoaded(false);
}
}, [unit]);
}, [(unit || {}).id]);
if (sequenceStatus === 'loading') {
if (!sequenceId) {
@@ -138,12 +146,18 @@ function Sequence({
because we expect CoursewareContainer to be performing a redirect to the legacy experience while
we're waiting. That redirect may take a few seconds, so we show the spinner in the meantime.
*/
if (sequenceStatus === 'loaded' && sequence.isTimeLimited) {
return (
<PageLoading
srMessage={intl.formatMessage(messages['learn.loading.learning.sequence'])}
/>
);
if (sequenceStatus === 'loaded') {
const shouldRedirectSpecialExams = sequence.isTimeLimited && !specialExamsEnabledWaffleFlag;
const shouldRedirectProctoredExams = sequence.isProctored && specialExamsEnabledWaffleFlag
&& !proctoredExamsEnabledWaffleFlag;
if (shouldRedirectSpecialExams || shouldRedirectProctoredExams) {
return (
<PageLoading
srMessage={intl.formatMessage(messages['learn.loading.learning.sequence'])}
/>
);
}
}
const gated = sequence && sequence.gatedContent !== undefined && sequence.gatedContent.gated;
@@ -151,83 +165,89 @@ function Sequence({
history.push(`/course/${courseId}/course-end`);
};
const defaultContent = (
<div className="sequence-container" style={{ display: 'inline-flex', flexDirection: 'row' }}>
<div className={classNames('sequence', { 'position-relative': shouldDisplayNotificationTrigger })} style={{ width: '100%' }}>
<SequenceNavigation
sequenceId={sequenceId}
unitId={unitId}
className="mb-4"
/** [MM-P2P] Experiment */
mmp2p={mmp2p}
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()}
isValuePropCookieSet={isValuePropCookieSet}
/>
{isValuePropCookieSet && shouldDisplayNotificationTrigger ? (
<NotificationTrigger
toggleNotificationTray={toggleNotificationTray}
isNotificationTrayVisible={isNotificationTrayVisible}
/>
) : null}
<div className="unit-container flex-grow-1">
<SequenceContent
courseId={courseId}
gated={gated}
sequenceId={sequenceId}
unitId={unitId}
unitLoadedHandler={handleUnitLoaded}
/** [MM-P2P] Experiment */
mmp2p={mmp2p}
/>
{unitHasLoaded && (
<UnitNavigation
sequenceId={sequenceId}
unitId={unitId}
onClickPrevious={() => {
logEvent('edx.ui.lms.sequence.previous_selected', 'bottom');
handlePrevious();
}}
onClickNext={() => {
logEvent('edx.ui.lms.sequence.next_selected', 'bottom');
handleNext();
}}
goToCourseExitPage={() => goToCourseExitPage()}
/>
)}
</div>
</div>
{isValuePropCookieSet && notificationTrayVisible ? (
<NotificationTray
toggleNotificationTray={toggleNotificationTray}
notificationTrayVisible={notificationTrayVisible}
/>
) : null }
{/** [MM-P2P] Experiment */}
{(mmp2p.state.isEnabled && mmp2p.flyover.isVisible) && (
isMobile()
? <MMP2PFlyoverMobile options={mmp2p} />
: <MMP2PFlyover options={mmp2p} />
)}
</div>
);
if (sequenceStatus === 'loaded') {
return (
<div>
<div className="sequence-container" style={{ display: 'inline-flex', flexDirection: 'row' }}>
<div className={classNames('sequence', { 'position-relative': shouldDisplaySidebarButton })} style={{ width: '100%' }}>
<SequenceNavigation
sequenceId={sequenceId}
unitId={unitId}
className="mb-4"
/** [MM-P2P] Experiment */
mmp2p={mmp2p}
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()}
isValuePropCookieSet={isValuePropCookieSet}
/>
{isValuePropCookieSet && shouldDisplaySidebarButton ? (
<SidebarNotificationButton
toggleSidebar={toggleSidebar}
isSidebarVisible={isSidebarVisible}
/>
) : null}
<div className="unit-container flex-grow-1">
<SequenceContent
courseId={courseId}
gated={gated}
sequenceId={sequenceId}
unitId={unitId}
unitLoadedHandler={handleUnitLoaded}
/** [MM-P2P] Experiment */
mmp2p={mmp2p}
/>
{unitHasLoaded && (
<UnitNavigation
sequenceId={sequenceId}
unitId={unitId}
onClickPrevious={() => {
logEvent('edx.ui.lms.sequence.previous_selected', 'bottom');
handlePrevious();
}}
onClickNext={() => {
logEvent('edx.ui.lms.sequence.next_selected', 'bottom');
handleNext();
}}
goToCourseExitPage={() => goToCourseExitPage()}
/>
)}
</div>
</div>
{sidebarVisible ? (
<Sidebar
toggleSidebar={toggleSidebar}
sidebarVisible={sidebarVisible}
/>
) : null }
{/** [MM-P2P] Experiment */}
{(mmp2p.state.isEnabled && mmp2p.flyover.isVisible) && (
isMobile()
? <MMP2PFlyoverMobile options={mmp2p} />
: <MMP2PFlyover options={mmp2p} />
)}
</div>
<SequenceExamWrapper sequence={sequence} courseId={courseId}>
{defaultContent}
</SequenceExamWrapper>
<CourseLicense license={course.license || undefined} />
</div>
);
@@ -249,9 +269,9 @@ Sequence.propTypes = {
nextSequenceHandler: PropTypes.func.isRequired,
previousSequenceHandler: PropTypes.func.isRequired,
intl: intlShape.isRequired,
toggleSidebar: PropTypes.func,
sidebarVisible: PropTypes.bool,
isSidebarVisible: PropTypes.func,
toggleNotificationTray: PropTypes.func,
notificationTrayVisible: PropTypes.bool,
isNotificationTrayVisible: PropTypes.func,
isValuePropCookieSet: PropTypes.bool,
/** [MM-P2P] Experiment */
@@ -271,9 +291,9 @@ Sequence.propTypes = {
Sequence.defaultProps = {
sequenceId: null,
unitId: null,
toggleSidebar: null,
sidebarVisible: null,
isSidebarVisible: null,
toggleNotificationTray: null,
notificationTrayVisible: null,
isNotificationTrayVisible: null,
isValuePropCookieSet: null,
/** [MM-P2P] Experiment */

View File

@@ -28,7 +28,7 @@ describe('Sequence', () => {
unitNavigationHandler: () => {},
nextSequenceHandler: () => {},
previousSequenceHandler: () => {},
sidebarVisible: false,
notificationTrayVisible: false,
};
});
@@ -67,11 +67,11 @@ describe('Sequence', () => {
{ store: testStore },
);
expect(screen.getByText('Loading locked content messaging...')).toBeInTheDocument();
// Only `Previous`, `Next` and `Bookmark` buttons.
expect(screen.getAllByRole('button').length).toEqual(3);
await waitFor(() => expect(screen.queryByText('Loading locked content messaging...')).toBeInTheDocument());
// `Previous`, `Active`, `Next` and `Prerequisite` buttons.
expect(screen.getAllByRole('button').length).toEqual(4);
expect(await screen.findByText('Content Locked')).toBeInTheDocument();
expect(screen.getByText('Content Locked')).toBeInTheDocument();
const unitContainer = container.querySelector('.unit-container');
expect(unitContainer.querySelector('svg')).toHaveClass('fa-lock');
expect(screen.getByText(/You must complete the prerequisite/)).toBeInTheDocument();
@@ -120,7 +120,7 @@ describe('Sequence', () => {
it('handles loading unit', async () => {
render(<Sequence {...mockData} />);
expect(screen.getByText('Loading learning sequence...')).toBeInTheDocument();
expect(await screen.findByText('Loading learning sequence...')).toBeInTheDocument();
// Renders navigation buttons plus one button for each unit.
expect(screen.getAllByRole('button')).toHaveLength(3 + unitBlocks.length);
@@ -130,10 +130,11 @@ describe('Sequence', () => {
expect(screen.getAllByRole('button', { name: /previous|next/i }).length).toEqual(4);
});
it('renders sidebar in sequence', async () => {
it('renders notification tray in sequence', async () => {
const testData = {
...mockData,
sidebarVisible: true,
notificationTrayVisible: true,
isValuePropCookieSet: true,
};
render(<Sequence {...testData} />);
@@ -167,6 +168,7 @@ describe('Sequence', () => {
previousSequenceHandler: jest.fn(),
};
render(<Sequence {...testData} />, { store: testStore });
expect(await screen.findByText('Loading learning sequence...')).toBeInTheDocument();
const sequencePreviousButton = screen.getByRole('button', { name: /previous/i });
fireEvent.click(sequencePreviousButton);
@@ -202,6 +204,7 @@ describe('Sequence', () => {
nextSequenceHandler: jest.fn(),
};
render(<Sequence {...testData} />, { store: testStore });
expect(await screen.findByText('Loading learning sequence...')).toBeInTheDocument();
const sequenceNextButton = screen.getByRole('button', { name: /next/i });
fireEvent.click(sequenceNextButton);
@@ -228,7 +231,7 @@ describe('Sequence', () => {
});
});
it('navigates to the previous/next unit if the unit is not in the corner of the sequence', () => {
it('navigates to the previous/next unit if the unit is not in the corner of the sequence', async () => {
const unitNumber = 1;
const testData = {
...mockData,
@@ -239,6 +242,7 @@ describe('Sequence', () => {
nextSequenceHandler: jest.fn(),
};
render(<Sequence {...testData} />, { store: testStore });
await waitFor(() => expect(screen.queryByText('Loading learning sequence...')).toBeInTheDocument());
fireEvent.click(screen.getByRole('button', { name: /previous/i }));
expect(testData.previousSequenceHandler).not.toHaveBeenCalled();
@@ -360,7 +364,7 @@ describe('Sequence', () => {
});
});
it('handles unit navigation button', () => {
it('handles unit navigation button', async () => {
const currentTabNumber = 1;
const targetUnitNumber = 2;
const targetUnit = unitBlocks[targetUnitNumber - 1];
@@ -371,6 +375,7 @@ describe('Sequence', () => {
unitNavigationHandler: jest.fn(),
};
render(<Sequence {...testData} />, { store: testStore });
await waitFor(() => expect(screen.queryByText('Loading learning sequence...')).toBeInTheDocument());
fireEvent.click(screen.getByRole('button', { name: targetUnit.display_name }));
expect(testData.unitNavigationHandler).toHaveBeenCalledWith(targetUnit.id);

View File

@@ -21,6 +21,7 @@ import { fetchCourse } from '../../data/thunks';
/** [MM-P2P] Experiment */
import { MMP2PLockPaywall } from '../../../experiments/mm-p2p';
const HonorCode = React.lazy(() => import('./honor-code'));
const LockPaywall = React.lazy(() => import('./lock-paywall'));
/**
@@ -69,6 +70,15 @@ function useLoadBearingHook(id) {
}, [id]);
}
export function sendUrlHashToFrame(frame) {
const { hash } = window.location;
if (hash) {
// The url hash will be sent to LMS-served iframe in order to find the location of the
// hash within the iframe.
frame.contentWindow.postMessage({ hashName: hash }, `${getConfig().LMS_BASE_URL}`);
}
}
function Unit({
courseId,
format,
@@ -88,21 +98,31 @@ function Unit({
const [iframeHeight, setIframeHeight] = useState(0);
const [hasLoaded, setHasLoaded] = useState(false);
const [modalOptions, setModalOptions] = useState({ open: false });
const [shouldDisplayHonorCode, setShouldDisplayHonorCode] = useState(false);
const unit = useModel('units', id);
const course = useModel('coursewareMeta', courseId);
const {
contentTypeGatingEnabled,
userNeedsIntegritySignature,
} = course;
const dispatch = useDispatch();
// Do not remove this hook. See function description.
useLoadBearingHook(id);
useEffect(() => {
if (userNeedsIntegritySignature && unit.graded) {
setShouldDisplayHonorCode(true);
} else {
setShouldDisplayHonorCode(false);
}
}, [userNeedsIntegritySignature]);
// We use this ref so that we can hold a reference to the currently active event listener.
const messageEventListenerRef = useRef(null);
useEffect(() => {
sendUrlHashToFrame(document.getElementById('unit-iframe'));
function receiveMessage(event) {
const { type, payload } = event.data;
if (type === 'plugin.resize') {
@@ -116,6 +136,10 @@ function Unit({
} else if (type === 'plugin.modal') {
payload.open = true;
setModalOptions(payload);
} else if (event.data.offset) {
// We listen for this message from LMS to know when the page needs to
// be scrolled to another location on the page.
window.scrollTo(0, event.data.offset);
}
}
// If we currently have an event listener, remove it.
@@ -133,7 +157,7 @@ function Unit({
return (
<div className="unit">
<h2 className="mb-0 h3">{unit.title}</h2>
<h1 className="mb-0 h3">{unit.title}</h1>
<BookmarkButton
unitId={unit.id}
isBookmarked={unit.bookmarked}
@@ -154,8 +178,19 @@ function Unit({
{ mmp2p.meta.showLock && (
<MMP2PLockPaywall options={mmp2p} />
)}
{!mmp2p.meta.blockContent && shouldDisplayHonorCode && (
<Suspense
fallback={(
<PageLoading
srMessage={intl.formatMessage(messages['learn.loading.honor.code'])}
/>
)}
>
<HonorCode courseId={courseId} />
</Suspense>
)}
{ /** [MM-P2P] Experiment (conditional) */ }
{!mmp2p.meta.blockContent && !hasLoaded && (
{!mmp2p.meta.blockContent && !shouldDisplayHonorCode && !hasLoaded && (
<PageLoading
srMessage={intl.formatMessage(messages['learn.loading.learning.sequence'])}
/>
@@ -186,7 +221,7 @@ function Unit({
/>
)}
{ /** [MM-P2P] Experiment (conditional) */ }
{ !mmp2p.meta.blockContent && (
{ !mmp2p.meta.blockContent && !shouldDisplayHonorCode && (
<div className="unit-iframe-wrapper">
<iframe
id="unit-iframe"

View File

@@ -3,7 +3,7 @@ import { Factory } from 'rosie';
import {
initializeTestStore, loadUnit, messageEvent, render, screen, waitFor,
} from '../../../setupTest';
import Unit from './Unit';
import Unit, { sendUrlHashToFrame } from './Unit';
describe('Unit', () => {
let mockData;
@@ -11,16 +11,32 @@ describe('Unit', () => {
'courseMetadata',
{ content_type_gating_enabled: true },
);
const unitBlocks = [Factory.build(
'block',
{ type: 'problem' },
{ courseId: courseMetadata.id },
), Factory.build(
'block',
{ type: 'vertical', contains_content_type_gated_content: true, bookmarked: true },
{ courseId: courseMetadata.id },
)];
const [unit, unitThatContainsGatedContent] = unitBlocks;
const courseMetadataNeedsSignature = Factory.build(
'courseMetadata',
{ user_needs_integrity_signature: true },
);
const unitBlocks = [
Factory.build(
'block',
{ type: 'problem', graded: 'true' },
{ courseId: courseMetadata.id },
), Factory.build(
'block',
{
type: 'vertical',
contains_content_type_gated_content: true,
bookmarked: true,
graded: true,
},
{ courseId: courseMetadata.id },
),
Factory.build(
'block',
{ type: 'problem', graded: false },
{ courseId: courseMetadata.id },
),
];
const [unit, unitThatContainsGatedContent, ungradedUnit] = unitBlocks;
beforeAll(async () => {
await initializeTestStore({ courseMetadata, unitBlocks });
@@ -33,7 +49,6 @@ describe('Unit', () => {
it('renders correctly', () => {
render(<Unit {...mockData} />);
expect(screen.getByText('Loading learning sequence...')).toBeInTheDocument();
const renderedUnit = screen.getByTitle(unit.display_name);
expect(renderedUnit).toHaveAttribute('height', String(0));
@@ -44,15 +59,41 @@ describe('Unit', () => {
it('renders proper message for gated content', () => {
render(<Unit {...mockData} id={unitThatContainsGatedContent.id} />);
expect(screen.getByText('Loading learning sequence...')).toBeInTheDocument();
expect(screen.getByText('Loading locked content messaging...')).toBeInTheDocument();
});
it('does not display HonorCode for ungraded units', async () => {
const signatureStore = await initializeTestStore(
{ courseMetadata: courseMetadataNeedsSignature, unitBlocks },
false,
);
const signatureData = {
id: ungradedUnit.id,
courseId: courseMetadataNeedsSignature.id,
format: 'Homework',
};
render(<Unit {...signatureData} />, { store: signatureStore });
expect(screen.getByText('Loading learning sequence...')).toBeInTheDocument();
});
it('displays HonorCode for graded units if user needs integrity signature', async () => {
const signatureStore = await initializeTestStore(
{ courseMetadata: courseMetadataNeedsSignature, unitBlocks },
false,
);
const signatureData = {
id: unit.id,
courseId: courseMetadataNeedsSignature.id,
format: 'Homework',
};
render(<Unit {...signatureData} />, { store: signatureStore });
expect(screen.getByText('Loading honor code messaging...')).toBeInTheDocument();
});
it('handles receiving MessageEvent', async () => {
render(<Unit {...mockData} />);
loadUnit();
// Loading message is gone now.
await waitFor(() => expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument());
// Iframe's height is set via message.
@@ -80,6 +121,16 @@ describe('Unit', () => {
expect(onLoaded).toHaveBeenCalledTimes(1);
});
it('scrolls page on MessagaeEvent when receiving offset', async () => {
// Set message to constain offset data.
const testMessageWithOffset = { offset: 1500 };
render(<Unit {...mockData} />);
window.postMessage(testMessageWithOffset, '*');
await expect(waitFor(() => expect(window.scrollTo()).toHaveBeenCalled()));
expect(window.scrollY === testMessageWithOffset.offset);
});
it('ignores MessageEvent with unhandled type', async () => {
// Clone message and set different type.
const testMessageWithUnhandledType = { ...messageEvent, type: 'wrong type' };
@@ -92,4 +143,37 @@ describe('Unit', () => {
{ timeout: 100 },
)).rejects.toThrowError(/Expected the element to have attribute/);
});
it('scrolls to correct place onLoad', () => {
document.body.innerHTML = "<iframe id='unit-iframe' />";
const mockHashCheck = jest.fn(frameVar => sendUrlHashToFrame(frameVar));
const frame = document.getElementById('unit-iframe');
const originalWindow = { ...window };
const windowSpy = jest.spyOn(global, 'window', 'get');
windowSpy.mockImplementation(() => ({
...originalWindow,
location: {
...originalWindow.location,
hash: '#test',
},
}));
const messageSpy = jest.spyOn(frame.contentWindow, 'postMessage');
messageSpy.mockImplementation(() => ({ hashName: originalWindow.location.hash }));
mockHashCheck(frame);
expect(mockHashCheck).toHaveBeenCalled();
expect(messageSpy).toHaveBeenCalled();
windowSpy.mockRestore();
});
it('calls useEffect and checkForHash', () => {
const mockHashCheck = jest.fn(() => sendUrlHashToFrame());
const effectSpy = jest.spyOn(React, 'useEffect');
effectSpy.mockImplementation(() => mockHashCheck());
render(<Unit {...mockData} />);
expect(React.useEffect).toHaveBeenCalled();
expect(mockHashCheck).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,58 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useDispatch } from 'react-redux';
import { getConfig, history } from '@edx/frontend-platform';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { ActionRow, Alert, Button } from '@edx/paragon';
import { saveIntegritySignature } from '../../../data';
import messages from './messages';
function HonorCode({ intl, courseId }) {
const dispatch = useDispatch();
const siteName = getConfig().SITE_NAME;
const honorCodeUrl = `${process.env.TERMS_OF_SERVICE_URL}#honor-code`;
const handleCancel = () => history.push(`/course/${courseId}/home`);
const handleAgree = () => {
dispatch(saveIntegritySignature(courseId));
};
return (
<Alert variant="light" aria-live="off">
<h4 aria-level="3">
{siteName}{' '}
{intl.formatMessage(messages['learn.honorCode.name'])}
</h4>
<p>
<FormattedMessage
id="learn.honorCode.content"
defaultMessage="Honesty and academic integrity are important to {siteName} and the institutions providing courses and programs on the {siteName} site. By clicking “I agree” below, I confirm that I have read, understand, and will abide by the {link} for the {siteName} Site."
values={{
siteName,
link: <a href={honorCodeUrl}>{intl.formatMessage(messages['learn.honorCode.name'])}</a>,
}}
/>
</p>
<ActionRow>
<ActionRow.Spacer />
<Button variant="tertiary" onClick={handleCancel}>
{intl.formatMessage(messages['learn.honorCode.cancel'])}
</Button>
<Button variant="primary" onClick={handleAgree}>
{intl.formatMessage(messages['learn.honorCode.agree'])}
</Button>
</ActionRow>
</Alert>
);
}
HonorCode.propTypes = {
intl: intlShape.isRequired,
courseId: PropTypes.string.isRequired,
};
export default injectIntl(HonorCode);

View File

@@ -0,0 +1,33 @@
import React from 'react';
import { history } from '@edx/frontend-platform';
import {
fireEvent, initializeTestStore, render, screen,
} from '../../../../setupTest';
import HonorCode from './HonorCode';
jest.mock('@edx/frontend-platform', () => ({
...jest.requireActual('@edx/frontend-platform'),
history: {
push: jest.fn(),
},
}));
describe('Honor Code', () => {
let store;
const mockData = {};
beforeAll(async () => {
store = await initializeTestStore();
const { courseware } = store.getState();
mockData.courseId = courseware.courseId;
});
it('cancel button links to course home ', () => {
render(<HonorCode {...mockData} />);
const cancelButton = screen.getByText('Cancel');
fireEvent.click(cancelButton);
expect(history.push).toHaveBeenCalledWith(`/course/${mockData.courseId}/home`);
});
});

View File

@@ -0,0 +1 @@
export { default } from './HonorCode';

View File

@@ -0,0 +1,21 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'learn.honorCode.name': {
id: 'learn.honorCode.name',
defaultMessage: 'Honor Code',
description: 'Honor code name.',
},
'learn.honorCode.cancel': {
id: 'learn.honorCode.cancel',
defaultMessage: 'Cancel',
description: '"Cancel" button.',
},
'learn.honorCode.agree': {
id: 'learn.honorCode.agree',
defaultMessage: 'I agree',
description: '"I agree" button.',
},
});
export default messages;

View File

@@ -5,12 +5,13 @@ import { faCheck } from '@fortawesome/free-solid-svg-icons';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
Alert, Button, Icon,
Alert, Icon,
} from '@edx/paragon';
import { Locked } from '@edx/paragon/icons';
import messages from './messages';
import certificateLocked from '../../../../generic/assets/edX_locked_certificate.png';
import { useModel } from '../../../../generic/model-store';
import { UpgradeButton } from '../../../../generic/upgrade-button';
import './LockPaywall.scss';
function LockPaywall({
@@ -19,6 +20,7 @@ function LockPaywall({
}) {
const course = useModel('coursewareMeta', courseId);
const {
offer,
org,
verifiedMode,
} = course;
@@ -26,11 +28,6 @@ function LockPaywall({
if (!verifiedMode) {
return null;
}
const {
currencySymbol,
price,
upgradeUrl,
} = verifiedMode;
const eventProperties = {
org_key: org,
@@ -75,7 +72,7 @@ function LockPaywall({
{intl.formatMessage(messages['learn.lockPaywall.list.bullet3.boldtext'])}
</span>
);
const nonProfit = (
const nonProfitMission = (
<span className="font-weight-bold">
{intl.formatMessage(messages['learn.lockPaywall.list.bullet4.boldtext'])}
</span>
@@ -141,8 +138,8 @@ function LockPaywall({
<span className="fa-li"><FontAwesomeIcon icon={faCheck} /></span>
<FormattedMessage
id="gatedContent.paragraph.bulletFour"
defaultMessage="Support our {nonProfit} mission at edX"
values={{ nonProfit }}
defaultMessage="Support our {nonProfitMission} at edX"
values={{ nonProfitMission }}
/>
</li>
</ul>
@@ -154,17 +151,11 @@ function LockPaywall({
className="col-md-auto p-md-0 d-md-flex align-items-md-center mr-md-3"
style={{ textAlign: 'right' }}
>
<Button
className="lock_paywall_upgrade_link"
href={upgradeUrl}
<UpgradeButton
offer={offer}
onClick={logClick}
role="link"
>
{intl.formatMessage(messages['learn.lockPaywall.upgrade.link'], {
currencySymbol,
price,
})}
</Button>
verifiedMode={verifiedMode}
/>
</div>
</div>
</Alert>

View File

@@ -11,12 +11,14 @@ jest.mock('@edx/frontend-platform/analytics');
describe('Lock Paywall', () => {
let store;
const mockData = {};
const mockData = { metadataModel: 'coursewareMeta' };
beforeAll(async () => {
store = await initializeTestStore();
const { courseware } = store.getState();
mockData.courseId = courseware.courseId;
Object.assign(mockData, {
courseId: courseware.courseId,
});
});
it('displays unlock link with price', () => {
@@ -31,6 +33,23 @@ describe('Lock Paywall', () => {
expect(upgradeLink).toHaveAttribute('href', `${upgradeUrl}`);
});
it('displays discounted price if there is an offer/first time purchase', async () => {
const courseMetadata = Factory.build('courseMetadata', {
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 }, false);
render(<LockPaywall {...mockData} courseId={courseMetadata.id} />, { store: testStore });
expect(screen.getByText(/Upgrade for/).textContent).toMatch('$85 ($100)');
});
it('sends analytics event onClick of unlock link', () => {
sendTrackEvent.mockClear();

View File

@@ -11,11 +11,6 @@ const messages = defineMessages({
defaultMessage: 'Upgrade to gain access to locked features like this one and get the most out of your course.',
description: 'Message shown to indicate that a piece of content is unavailable to audit track users.',
},
'learn.lockPaywall.upgrade.link': {
id: 'learn.lockPaywall.upgrade.link',
defaultMessage: 'Upgrade for {currencySymbol}{price}',
description: 'A link users can click that navigates their browser to the upgrade payment page.',
},
'learn.lockPaywall.example.alt': {
id: 'learn.lockPaywall.example.alt',
defaultMessage: 'Example Certificate',
@@ -43,7 +38,7 @@ const messages = defineMessages({
},
'learn.lockPaywall.list.bullet4.boldtext': {
id: 'learn.lockPaywall.list.bullet4.boldtext',
defaultMessage: 'non-profit',
defaultMessage: 'non-profit mission',
description: 'Bolded text to highlight our non-profit status.',
},
});

View File

@@ -6,6 +6,11 @@ const messages = defineMessages({
defaultMessage: 'Loading locked content messaging...',
description: 'Message shown when an interface about locked content is being loaded',
},
'learn.loading.honor.code': {
id: 'learn.loading.honor.codk',
defaultMessage: 'Loading honor code messaging...',
description: 'Message shown when an interface about the honor code is being loaded',
},
'learn.loading.learning.sequence': {
id: 'learn.loading.learning.sequence',
defaultMessage: 'Loading learning sequence...',

View File

@@ -40,7 +40,7 @@ function SequenceNavigation({
sequence.gatedContent !== undefined && sequence.gatedContent.gated
) : undefined;
const shouldDisplaySidebarButton = useWindowSize().width < responsiveBreakpoints.small.minWidth;
const shouldDisplayNotificationTrigger = useWindowSize().width < responsiveBreakpoints.small.minWidth;
const renderUnitButtons = () => {
if (isLocked) {
@@ -70,15 +70,15 @@ function SequenceNavigation({
const disabled = isLastUnit && !exitActive;
return (
<Button variant="link" className="next-btn" onClick={buttonOnClick} disabled={disabled} iconAfter={ChevronRight}>
{isValuePropCookieSet && shouldDisplaySidebarButton ? null : buttonText}
{isValuePropCookieSet && shouldDisplayNotificationTrigger ? null : buttonText}
</Button>
);
};
return sequenceStatus === LOADED && (
<nav className={classNames('sequence-navigation', className)} style={{ width: isValuePropCookieSet && shouldDisplaySidebarButton ? '90%' : null }}>
<nav className={classNames('sequence-navigation', className)} style={{ width: isValuePropCookieSet && shouldDisplayNotificationTrigger ? '90%' : null }}>
<Button variant="link" className="previous-btn" onClick={previousSequenceHandler} disabled={isFirstUnit} iconBefore={ChevronLeft}>
{isValuePropCookieSet && shouldDisplaySidebarButton ? null : intl.formatMessage(messages.previousButton)}
{isValuePropCookieSet && shouldDisplayNotificationTrigger ? null : intl.formatMessage(messages.previousButton)}
</Button>
{renderUnitButtons()}
{renderNextButton()}

View File

@@ -56,4 +56,8 @@ Factory.define('courseMetadata')
verification_status: 'none',
linkedin_add_to_profile_url: null,
related_programs: null,
user_needs_integrity_signature: false,
is_mfe_special_exams_enabled: false,
is_mfe_proctored_exams_enabled: false,
recommendations: null,
});

View File

@@ -0,0 +1,38 @@
import { Factory } from 'rosie'; // eslint-disable-line import/no-extraneous-dependencies
Factory.define('courseRecommendations')
.option('host', '')
.option('numRecs', '', 4)
.sequence('uuid', (i) => `a-uuid-${i}`)
.attr('recommendations', ['numRecs'], (numRecs) => {
const recs = [];
for (let i = 0; i < numRecs; i++) {
recs.push(Factory.build('courseRecommendation'));
}
return recs;
});
Factory.define('courseRecommendation')
.sequence('key', (i) => `edX+DemoX${i}`)
.sequence('uuid', (i) => `abcd-${i}`)
.attrs({
title: 'DemoX',
owners: [
{
uuid: '',
key: 'edX',
},
],
image: {
src: '',
},
})
.attr('course_run_keys', ['key'], (key) => (
[`${key}+1T2021`]
))
.attr('url_slug', ['key'], (key) => key)
.attr('marketing_url', ['url_slug'], (urlSlug) => `https://www.edx.org/course/${urlSlug}`);
Factory.define('userEnrollment')
.option('runKey')
.attr('course_details', ['runKey'], (runKey) => (runKey ? { course_id: runKey } : { course_id: 'edX+EnrolledX' }));

View File

@@ -1,2 +1,3 @@
import './courseMetadata.factory';
import './sequenceMetadata.factory';
import './courseRecommendations.factory';

View File

@@ -0,0 +1,37 @@
import { Factory } from 'rosie'; // eslint-disable-line import/no-extraneous-dependencies
Factory.define('learningSequencesOutline')
.option('courseId', (courseId) => {
if (courseId) {
return courseId;
}
throw new Error('courseId must be specified for learningSequencesOutline factory.');
})
.attrs({
outline: {
sequences: {
},
},
});
export function buildEmptyOutline(courseId) {
return Factory.build(
'learningSequencesOutline',
{},
{ courseId },
);
}
export function buildSimpleOutline(courseId, sequenceBlocks) {
return Factory.build(
'learningSequencesOutline',
{
outline: {
sequences: Object.fromEntries(
sequenceBlocks.map(({ id }) => [id, {}]),
),
},
},
{ courseId },
);
}

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