Compare commits

...

26 Commits

Author SHA1 Message Date
Nathan Sprenkle
650e9321b1 Simplify Bulk Management Config (#200)
* refactor: simplify bulk management enabling

Formerly, a course had to have bulk management enabled and have a master's
track. This painted us into a corner where we had to create a
workaround for enabling bulk management on non-master's track courses.
Instead, this relies only on an enabled flag from edx-platform (based on
a course waffle flag) which simplifies the enabling code here.

* feat: remove unneeded bulk management allow-list
2021-08-04 11:04:55 -04:00
Ben Warzeski
e8a8cca483 fix: add excludedCourseRoles to bulk-grades fetch (#209) 2021-08-04 09:46:51 -04:00
Ben Warzeski
f5d2a34660 Fix cohort url (#208)
* fix cohorts fetch url

* v1.4.45
2021-08-03 15:37:12 -04:00
Ben Warzeski
97d3a29a7f fix: add track back into lms api service calls. (#207)
* fix: add track back into lms api service calls.

* fix tests

* v1.4.44
2021-07-30 11:59:01 -04:00
Leangseu Kim
5b8f67e8d2 fix: update csv import error messages
update csv error according discussed

Ticket[au-66]

version bump
2021-07-26 12:59:42 -04:00
Ben Warzeski
c6e33307ba refactor: add transifex support to user-facing messages (#203)
* clean up and test segment integration

* add transifex config

* move user-facing messages into messages files and translate in usage

* lint cleanup

* fix introduced typos

* remove dead code

* remove should-be-ignored temp translation files

* make HistoryHeader use node-type to support translations

* fix apostrophe

* fix snapshot

* v1.4.42
2021-07-22 10:45:18 -04:00
Ben Warzeski
a4df8f7238 refactor: update test coverage (#202)
* clean up and test segment integration

* add tests for action utils

* add tests for store aggregator and utils

* clean up un-used paths in thunkAction testUtils

* clean up filter reducer coverage

* add filter reducer tests for filterMenu actions

* clean up grades selectors coverage

* separate App and add unit tests

* ignore external files in coverage analysis

* remove old/unused code from StrictDict and clean up tests

* clean up FileUploadForm coverage

* more cleanup for StrictDict tests

* clean up GradesTab test coverage

* clean up GradesTab coverage

* ignore reducer-mapping for unit-test coverage

* clean up AssignmentFilter test coverage

* add index-level test coverage

* temp remove snapshots

* re-add App snapshot

* v1.4.41
2021-07-02 12:32:18 -04:00
Ben Warzeski
15b76edb5d refactor: lms service testing (#199)
* v1.4.40

* ignore accepted import eslint errors

* clean up LmsApiService into smaller, tested modules in lms service

* set default format before initial fetches

* fix bulk grades export and grade filtering

* fix clearing assignment grade filter badge

* re-connect grade format control
2021-06-30 11:50:07 -04:00
Ben Warzeski
1ab2fee004 add reasonable width to grade format button (#198) 2021-06-24 10:59:11 -04:00
Ben Warzeski
9c9ba45fec Fix grade format button (#197)
* re-organize components based on function

* clean up GradebookFilters test

* fix grade format button

* v1.4.39

* uuuugh, actually fix button
2021-06-24 08:59:42 -04:00
Ben Warzeski
85d566d257 re-organize components based on function (#196)
* re-organize components based on function

* clean up GradebookFilters test

* fix grade format button

* v1.4.39
2021-06-23 13:56:10 -04:00
Ben Warzeski
5688adcd57 Refactor: add unit tests and docstrings to remaining components (#195)
* add unit tests and docstrings

* v1.4.38
2021-06-22 15:25:57 -04:00
Ben Warzeski
b4f4a27f73 refactor - Bulk management tab unit tests (#194)
* simplify histry row entries

* clean up BulkManagement tab component and add docstrings/unit tests

* slightly more breakup and docstrings

* fix: make updateQueryParams push to history again

* update paragon to get latest Hyperlink version

* fix display issues from PR

* make redux exposure only available in development

* fix message

* add oxford comma

* update snapshots

* v1.4.37
2021-06-21 16:01:41 -04:00
Ben Warzeski
868048381b refactor: unit test and docstring FilterBadges (#193)
* unit test and docstring FilterBadges

* v1.4.38
2021-06-16 15:34:50 -04:00
Chris Chávez
02c154ef50 fix: Hard-coded link in Header component changed by LMS_BASE_URL env var (#186)
* feat: LOGO_DESTINATION env var added for use it on Header component

On index.js file of the Header component, currenly there is a hard-coded link of "https://www.edx.org". I added a new enviroment variable LOGO_DESTINATION to use it in this Header.
I have also added a mechanism to use the LMS_BASE_URL environment variable if the LOGO_DESTINATION environment variable is not found or is null.

* fix: Hard-coded link in Header component changed by a env var

`index.jsx` file of Header component updated with change the hard-coded link of https://www.edx.org with a component property.
`index.jsx` file of root project updated. Added the environment variable LMS BASE_URL to the parameters of the Header Component

* fix: lint errors in previous commits

* style: change structure of the solution
2021-06-14 13:51:20 -04:00
Ben Warzeski
7acefe0468 refactor: gradebook table tests (#192)
* clean up, test, and docstring Gradebook table

* v1.4.36
2021-06-09 14:34:15 -04:00
Ben Warzeski
a836cc1b5b refactor: add edit modal tests (#191)
* update drawer layout and container logic

* add tests and docstrings for EditModal components

* typo fix and merge conflict

* v1.4.35
2021-06-09 10:48:23 -04:00
David Joy
6a3db4a11b Conditionally enable segment and clean up environment variables (#181)
* fix(deps): bumping frontend-platform to latest

This picks up a change in which a blank SEGMENT_KEY no longer causes the MFE to make a request to segment which results in a 404.

This was a requested fix for lilac.

* fix: cleanup environment variables

- Removes stray commas from the end of each environment variable in .env
- Removes CSRF_COOKIE_NAME completely - it is not used.
- Replaces default values with empty strings ‘’ - this will make the defaults falsy, rather than have them get converted to a string “null” value, which is very misleading.  It also enables the SEGMENT_KEY fix mentioned in the prior commit to work.

* fix: conditionally enable the segment middleware

Only add it if SEGMENT_KEY is truthy.

* build: bumping version number
2021-06-08 11:14:37 -04:00
Ben Warzeski
d727420c37 refactor: update drawer logic and container layout (#190)
* update drawer layout and container logic

* stop renaming the FilterBadges component

* docstring for WithSidebar

* add unit tests

* v1.4.33
2021-06-07 16:24:37 -04:00
Ben Warzeski
2029a7cef3 State to redux (#187)
* add app redux module

* simplify fetchGrades logic now that almost everything it needs is in redux

* simplified thunkAction logic with new app store

* component redux helper cleanup

* simplify PageButtons

* simplify FilterBadges props

* clean up BulkManagement

* re-org and simplify data logic for EditModal

* re-org and simplify data logic for Gradebook Table

* clean up StatusAlerts

* clean up SearchControls

* clean up GradebookHeader

* clean up AssignmentFilter

* clean up AssignmentGradeFilter

* simplify CourseGradeFilter

* simplify StudentGroupsFilter

* simplify GradebookFilters.index

* simplify Gradebook component :-)

* linting

* fix on-clear search behavior

* remove un-needed GradeButton variants

* add FilterBadge docstrings

* v1.4.32
2021-06-07 10:09:07 -04:00
Nathan Sprenkle
8462249d55 Trim unused footer props/vars (#189) 2021-06-04 13:46:07 -04:00
Ben Warzeski
40059ec41e Fix page buttons (#188)
* add prev/next grades selectors

* fix PageButtons props
2021-06-01 12:23:33 -04:00
Ben Warzeski
2ee522352e pull GradebookHeader out of monolith (#185)
* pull GradebookHeader out of monolith

* v1.4.30
2021-05-26 15:05:43 -04:00
Ben Warzeski
189152f51b refactor: redux refactor to use redux toolkit (#182)
* add redux-toolkit

* update actions to use redux toolkit action creators

* add StrictDict

* ready for testing

* update testing for reducers

* update unit testing for reducers

* export reducers from initial state

* update unit test

* reorder the test reducer's handler

* remove unnecessary testing data

update

* update actions to use redux toolkit action creators

* testing

* thunkActions tests

* component thunkAction reference updates

* linting

* a little bit of doc and syntax cleanup

* fix tests

* assignment type actions tests

* actions testing and cleanup

* selectors cleanup

* fix store action reference

* update package-lock.json

* add a bit of test coverage

* strict selector export

* docstrings for test utils

* docstrings

* fix assignment filtering

* some cleanup

* update version to 1.4.29

Co-authored-by: Leangseu Kim <lkim@edx.org>
2021-05-25 15:08:36 -04:00
Nathan Sprenkle
3bc2511cc1 fix: bad prop causing bulk mgmt errors to not show (#184) 2021-05-18 11:04:57 -04:00
leangseu-edx
f60e3c1188 fix:csv override previous fail state on success (#183)
On success and set uploadSuccess to true every time. This is important for unit testing.
[EDUCATOR-5796]
2021-05-17 14:29:40 -04:00
269 changed files with 15441 additions and 6905 deletions

65
.env
View File

@@ -1,35 +1,32 @@
NODE_ENV='production',
NODE_ENV='production'
NODE_PATH=./src
BASE_URL=null,
LMS_BASE_URL=null,
LOGIN_URL=null,
LOGOUT_URL=null,
CSRF_TOKEN_API_PATH=null,
REFRESH_ACCESS_TOKEN_ENDPOINT=null,
DATA_API_BASE_URL=null,
SEGMENT_KEY=null,
FEATURE_FLAGS={},
ACCESS_TOKEN_COOKIE_NAME=null,
CSRF_COOKIE_NAME='csrftoken',
NEW_RELIC_APP_ID=null,
NEW_RELIC_LICENSE_KEY=null,
SITE_NAME='',
MARKETING_SITE_BASE_URL=null,
SUPPORT_URL=null,
CONTACT_URL=null,
OPEN_SOURCE_URL=null,
TERMS_OF_SERVICE_URL=null,
PRIVACY_POLICY_URL=null,
FACEBOOK_URL=null,
TWITTER_URL=null,
YOU_TUBE_URL=null,
LINKED_IN_URL=null,
REDDIT_URL=null,
APPLE_APP_STORE_URL=null,
GOOGLE_PLAY_URL=null,
ENTERPRISE_MARKETING_URL=null,
ENTERPRISE_MARKETING_UTM_SOURCE=null,
ENTERPRISE_MARKETING_UTM_CAMPAIGN=null,
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=null,
BULK_MANAGEMENT_SPECIAL_ACCESS_COURSE_IDS=null,
BASE_URL=''
LMS_BASE_URL=''
LOGIN_URL=''
LOGOUT_URL=''
CSRF_TOKEN_API_PATH=''
REFRESH_ACCESS_TOKEN_ENDPOINT=''
DATA_API_BASE_URL=''
SEGMENT_KEY=''
FEATURE_FLAGS={}
ACCESS_TOKEN_COOKIE_NAME=''
NEW_RELIC_APP_ID=''
NEW_RELIC_LICENSE_KEY=''
SITE_NAME=''
MARKETING_SITE_BASE_URL=''
SUPPORT_URL=''
CONTACT_URL=''
OPEN_SOURCE_URL=''
TERMS_OF_SERVICE_URL=''
PRIVACY_POLICY_URL=''
FACEBOOK_URL=''
TWITTER_URL=''
YOU_TUBE_URL=''
LINKED_IN_URL=''
REDDIT_URL=''
APPLE_APP_STORE_URL=''
GOOGLE_PLAY_URL=''
ENTERPRISE_MARKETING_URL=''
ENTERPRISE_MARKETING_UTM_SOURCE=''
ENTERPRISE_MARKETING_UTM_CAMPAIGN=''
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=''

View File

@@ -14,13 +14,11 @@ REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
USER_INFO_COOKIE_NAME='edx-user-info'
SITE_NAME=localhost
DATA_API_BASE_URL='http://localhost:8000'
// LMS_CLIENT_ID should match the lms DOT client application id your LMS containe
LMS_CLIENT_ID='login-service-client-id'
SEGMENT_KEY=null
SEGMENT_KEY=''
FEATURE_FLAGS={}
CSRF_COOKIE_NAME='csrftoken'
MARKETING_SITE_BASE_URL='http://localhost:18000'
SUPPORT_URL='http://localhost:18000/support'
CONTACT_URL='http://localhost:18000/contact'
@@ -38,5 +36,3 @@ ENTERPRISE_MARKETING_URL='http://example.com'
ENTERPRISE_MARKETING_UTM_SOURCE='example.com'
ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
BULK_MANAGEMENT_SPECIAL_ACCESS_COURSE_IDS=null

View File

@@ -1,6 +1,13 @@
const { createConfig } = require('@edx/frontend-build');
const config = createConfig('eslint');
const config = createConfig('eslint', {
rules: {
'import/no-named-as-default': 'off',
'import/no-named-as-default-member': 'off',
'import/no-self-import': 'off',
'spaced-comment': ['error', 'always', { 'block': { 'exceptions': ['*'] } }],
},
});
config.settings = {
"import/resolver": {

4
.gitignore vendored
View File

@@ -17,3 +17,7 @@ dist/
### Development environments ###
.idea
.vscode
### transifex ###
src/i18n/transifex_input.json
temp

8
.tx/config Normal file
View File

@@ -0,0 +1,8 @@
[main]
host = https://www.transifex.com
[edx-platform.frontend-app-gradebook]
file_filter = src/i18n/messages/<lang>.json
source_file = src/i18n/transifex_input.json
source_lang = en
type = KEYVALUEJSON

View File

@@ -2,8 +2,64 @@ npm-install-%: ## install specified % npm package
npm install $* --save-dev
git add package.json
validate-no-uncommitted-package-lock-changes:
git diff --exit-code package-lock.json
transifex_resource = frontend-app-gradebook
transifex_langs = "ar,fr,es_419,zh_CN"
test:
npm run test
transifex_utils = ./node_modules/.bin/transifex-utils.js
i18n = ./src/i18n
transifex_input = $(i18n)/transifex_input.json
tx_url1 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/translation/en/strings/
tx_url2 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/source/
# This directory must match .babelrc .
transifex_temp = ./temp/babel-plugin-react-intl
NPM_TESTS=build i18n_extract lint test is-es5
.PHONY: test
test: $(addprefix test.npm.,$(NPM_TESTS)) ## validate ci suite
.PHONY: test.npm.*
test.npm.%: validate-no-uncommitted-package-lock-changes
test -d node_modules || $(MAKE) requirements
npm run $(*)
.PHONY: requirements
requirements: ## install ci requirements
npm ci
i18n.extract:
# Pulling display strings from .jsx files into .json files...
rm -rf $(transifex_temp)
npm run-script i18n_extract
i18n.concat:
# Gathering JSON messages into one file...
$(transifex_utils) $(transifex_temp) $(transifex_input)
extract_translations: | requirements i18n.extract i18n.concat
# Despite the name, we actually need this target to detect changes in the incoming translated message files as well.
detect_changed_source_translations:
# Checking for changed translations...
git diff --exit-code $(i18n)
# Pushes translations to Transifex. You must run make extract_translations first.
push_translations:
# Pushing strings to Transifex...
tx push -s
# Fetching hashes from Transifex...
./node_modules/reactifex/bash_scripts/get_hashed_strings.sh $(tx_url1)
# Writing out comments to file...
$(transifex_utils) $(transifex_temp) --comments
# Pushing comments to Transifex...
./node_modules/reactifex/bash_scripts/put_comments.sh $(tx_url2)
# Pulls translations from Transifex.
pull_translations:
tx pull -f --mode reviewed --language=$(transifex_langs)
# This target is used by Travis.
validate-no-uncommitted-package-lock-changes:
# Checking for package-lock.json changes...
git diff --exit-code package-lock.json

View File

@@ -68,7 +68,7 @@ Confirm the following workflows:
- [ ] Clicking "Save Grade" applies the override, shows the successful "grade has been edited" banner and updates score in grades table (may take a few seconds).
- [ ] Opening back up the "Edit Grades" modal shows the change as an entry in the override history table.
- [ ] *Masters only*: "Bulk Management" allows overriding grades in bulk.
- [ ] *Master's (or selectively-enabled) only*: "Bulk Management" allows overriding grades in bulk.
- Open a non-masters-track course.
- [ ] Verify that the "Bulk Management" tab does not appear.
- [ ] Verify that the "Bulk Management" button does not appear.

View File

@@ -14,7 +14,7 @@ Suggested resources:
- [Adding Exercises and Tools](https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/grading/index.html)
- [Set the Assignment Type and Due Date for a Subsection](https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/developing_course/course_subsections.html#set-the-assignment-type-and-due-date-for-a-subsection)
## Enable Gradebook and feature toggles for course
## Enable Gradebook for course
See README.md #Quickstart for more detailed instructions.
@@ -25,7 +25,13 @@ As an admin user, visit Django Admin (`{lms-url}/admin`) to modify features.
- [ ] Set name to `grades.assume_zero_grade_if_absent`, select "Active", and click "Save"
- In Waffle_Utils > Waffle flag course overrides:
- [ ] Add a new flag called `grades.writeable_gradebook`, select "Force On", and enable it for your course
- [ ] Add a new flag called `grades.bulk_management`, select "Force On", and enable it for your course
## Enable Bulk Management
Bulk Management is an added feature to allow modifying grades in bulk via CSV upload. Bulk Management is default enabled for Master's track courses but can be selectively enabled for other courses with a waffle flag following the steps below.
- In Waffle_Utils > Waffle flag course overrides:
- [ ] Add a new flag called `grades.bulk_management`, select "Force On", and enable it for your course.
## Create a Master's track for testing Master's-only features

View File

@@ -8,4 +8,8 @@ module.exports = createConfig('jest', {
snapshotSerializers: [
'enzyme-to-json/serializer',
],
coveragePathIgnorePatterns: [
'src/segment.js',
'src/postcss.config.js',
],
});

1413
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "@edx/frontend-app-gradebook",
"version": "1.4.26",
"version": "1.4.46",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -1275,9 +1275,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-edx.org@1.6.1",
@@ -3292,11 +3292,11 @@
}
},
"@edx/frontend-platform": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-1.8.1.tgz",
"integrity": "sha512-oHLSamyIDuCtdS7eDzLcYs4zsAOGf4uuywnnIKg1CnlEHzUrXnfmcajbNM3Wre5jJocGH+1qACuaGLELO+3sAQ==",
"version": "1.9.5",
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-1.9.5.tgz",
"integrity": "sha512-noCb39j1CeCnLFhCAKMrJUBDoyKLRv2ORfEkEPeIFxGG7b49sSFR4J/ijHXJgwJ7WhUo6+hj42Fg/Zt3xEYj7g==",
"requires": {
"@cospired/i18n-iso-languages": "2.1.2",
"@cospired/i18n-iso-languages": "2.2.0",
"axios": "0.21.1",
"axios-cache-adapter": "^2.5.0",
"form-urlencoded": "4.1.4",
@@ -3310,30 +3310,15 @@
"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"
},
"dependencies": {
"axios": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
"integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
"requires": {
"follow-redirects": "^1.10.0"
}
},
"follow-redirects": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz",
"integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg=="
}
}
},
"@edx/paragon": {
"version": "14.6.1",
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-14.6.1.tgz",
"integrity": "sha512-y80VLJZ6ZYEEw+T4/BFR+rqWPyb8Q8rlQRin/BssMNbEErsZLG+JpGU6Ff/r1n3XhvXC+B1XL4dZCb+kM6Wz2w==",
"version": "14.16.4",
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-14.16.4.tgz",
"integrity": "sha512-3af3jXEyg+ojIhWOov64VZo7LWgdLuPgS5SaaraGWpTFhcp9WnKaFxicqGtzhx9lH4RqS+SC7gua5pn2xR4UzA==",
"requires": {
"@fortawesome/fontawesome-svg-core": "^1.2.30",
"@fortawesome/free-solid-svg-icons": "^5.14.0",
@@ -4233,18 +4218,35 @@
"resolved": "https://registry.npmjs.org/@redux-beacon/segment/-/segment-1.1.0.tgz",
"integrity": "sha512-NLRoP3Jfx5z99YX6TFFznwXIMjqjD6/qdMZIKFRgGO8NtMWrCruA8EeQYPJZUBnuOjw6RtOA1UdjbqyRmdhc/Q=="
},
"@reduxjs/toolkit": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.5.1.tgz",
"integrity": "sha512-PngZKuwVZsd+mimnmhiOQzoD0FiMjqVks6ituO1//Ft5UEX5Ca9of13NEjo//pU22Jk7z/mdXVsmDfgsig1osA==",
"requires": {
"immer": "^8.0.1",
"redux": "^4.0.0",
"redux-thunk": "^2.3.0",
"reselect": "^4.0.0"
},
"dependencies": {
"immer": {
"version": "8.0.4",
"resolved": "https://registry.npmjs.org/immer/-/immer-8.0.4.tgz",
"integrity": "sha512-jMfL18P+/6P6epANRvRk6q8t+3gGhqsJ9EuJ25AXE+9bNTYtssvzeYbEd0mXRYWCmmXSIbnlpz6vd6iJlmGGGQ=="
}
}
},
"@restart/context": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@restart/context/-/context-2.1.4.tgz",
"integrity": "sha512-INJYZQJP7g+IoDUh/475NlGiTeMfwTXUEr3tmRneckHIxNolGOW9CTq83S8cxq0CgJwwcMzMJFchxvlwe7Rk8Q=="
},
"@restart/hooks": {
"version": "0.3.26",
"resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.3.26.tgz",
"integrity": "sha512-7Hwk2ZMYm+JLWcb7R9qIXk1OoUg1Z+saKWqZXlrvFwT3w6UArVNWgxYOzf+PJoK9zZejp8okPAKTctthhXLt5g==",
"version": "0.3.27",
"resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.3.27.tgz",
"integrity": "sha512-s984xV/EapUIfkjlf8wz9weP2O9TNKR96C68FfMEy2bE69+H4cNv3RD4Mf97lW7Htt7PjZrYTjSC8f3SB9VCXw==",
"requires": {
"lodash": "^4.17.20",
"lodash-es": "^4.17.20"
"dequal": "^2.0.2"
}
},
"@semantic-release/commit-analyzer": {
@@ -5119,14 +5121,6 @@
"@types/node": "*"
}
},
"@types/classnames": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.3.1.tgz",
"integrity": "sha512-zeOWb0JGBoVmlQoznvqXbE0tEC/HONsnoUNH19Hc96NFsTAwTXbTqb8FMYkru1F/iqp7a18Ws3nWJvtA1sHD1A==",
"requires": {
"classnames": "*"
}
},
"@types/color-name": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
@@ -5260,9 +5254,9 @@
"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.11",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.11.tgz",
"integrity": "sha512-yFRQbD+whVonItSk7ZzP/L+gPTJVBkL/7shLEF+i9GC/1cV3JmUxEQz6+9ylhUpWSDuqo1N9qEvqS6vTj4USUA==",
"requires": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -5934,9 +5928,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"
},
@@ -6162,6 +6156,23 @@
"dev": true,
"requires": {
"util": "0.10.3"
},
"dependencies": {
"inherits": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
"integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=",
"dev": true
},
"util": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
"integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
"dev": true,
"requires": {
"inherits": "2.0.1"
}
}
}
},
"assert-ok": {
@@ -6250,6 +6261,14 @@
"postcss-value-parser": "^4.1.0"
}
},
"available-typed-arrays": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz",
"integrity": "sha512-XWX3OX8Onv97LMk/ftVyBibpGwY5a8SmuxZPzeOxqmuEqUCOM9ZE+uIaD1VNJ5QnvU2UQusvmKbuM1FR8QWGfQ==",
"requires": {
"array-filter": "^1.0.0"
}
},
"aws-sign2": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
@@ -6264,7 +6283,6 @@
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
"integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
"dev": true,
"requires": {
"follow-redirects": "^1.10.0"
},
@@ -6272,18 +6290,17 @@
"follow-redirects": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz",
"integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==",
"dev": true
"integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg=="
}
}
},
"axios-cache-adapter": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/axios-cache-adapter/-/axios-cache-adapter-2.5.0.tgz",
"integrity": "sha512-YcMPdMoqmSLoZx7A5YD/PdYGuX6/Y9M2tHBhaIXvXrPeGgNnbW7nb3+uArWlT53WGHLfclnu2voMmS7jGXVg6A==",
"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",
"lodash": "^4.17.11"
"md5": "^2.2.1"
}
},
"axios-mock-adapter": {
@@ -7808,6 +7825,11 @@
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
"dev": true
},
"charenc": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
"integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc="
},
"check-types": {
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/check-types/-/check-types-8.0.3.tgz",
@@ -9089,6 +9111,11 @@
"which": "^1.2.9"
}
},
"crypt": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
"integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs="
},
"crypto-browserify": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
@@ -9824,6 +9851,11 @@
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==",
"dev": true
},
"dequal": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.2.tgz",
"integrity": "sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug=="
},
"des.js": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz",
@@ -11789,18 +11821,11 @@
}
},
"focus-lock": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/focus-lock/-/focus-lock-0.8.1.tgz",
"integrity": "sha512-/LFZOIo82WDsyyv7h7oc0MJF9ACOvDRdx9rWPZ2pgMfNWu/z8hQDBtOchuB/0BVLmuFOZjV02YwUVzNsWx/EzA==",
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/focus-lock/-/focus-lock-0.9.1.tgz",
"integrity": "sha512-/2Nj60Cps6yOLSO+CkVbeSKfwfns5XbX6HOedIK9PdzODP04N9c3xqOcPXayN0WsT9YjJvAnXmI0NdqNIDf5Kw==",
"requires": {
"tslib": "^1.9.3"
},
"dependencies": {
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
"tslib": "^2.0.3"
}
},
"follow-redirects": {
@@ -11823,6 +11848,11 @@
"integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=",
"dev": true
},
"foreach": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz",
"integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k="
},
"forever-agent": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
@@ -13619,8 +13649,7 @@
"is-arguments": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz",
"integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==",
"dev": true
"integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA=="
},
"is-arrayish": {
"version": "0.2.1",
@@ -13649,6 +13678,11 @@
"call-bind": "^1.0.0"
}
},
"is-buffer": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
},
"is-callable": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz",
@@ -13797,6 +13831,11 @@
"integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==",
"dev": true
},
"is-generator-function": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.9.tgz",
"integrity": "sha512-ZJ34p1uvIfptHCN7sFTjGibB9/oBg17sHqzDLfuwhvmN/qLVvIQXRQ8licZQ35WJ8KuEQt/etnnzQFI9C9Ue/A=="
},
"is-gif": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-gif/-/is-gif-3.0.0.tgz",
@@ -14067,6 +14106,117 @@
"text-extensions": "^1.0.0"
}
},
"is-typed-array": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.5.tgz",
"integrity": "sha512-S+GRDgJlR3PyEbsX/Fobd9cqpZBuvUS+8asRqYDMLCb2qMzt1oz5m5oxQCxOgUDxiWsOVNi4yaF+/uvdlHlYug==",
"requires": {
"available-typed-arrays": "^1.0.2",
"call-bind": "^1.0.2",
"es-abstract": "^1.18.0-next.2",
"foreach": "^2.0.5",
"has-symbols": "^1.0.1"
},
"dependencies": {
"call-bind": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
"requires": {
"function-bind": "^1.1.1",
"get-intrinsic": "^1.0.2"
}
},
"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"
},
"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=="
}
}
},
"get-intrinsic": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
"integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
"requires": {
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-symbols": "^1.0.1"
}
},
"is-callable": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz",
"integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ=="
},
"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==",
"requires": {
"call-bind": "^1.0.2",
"has-symbols": "^1.0.1"
}
},
"object-inspect": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.2.tgz",
"integrity": "sha512-gz58rdPpadwztRrPjZE9DZLOABUpTGdcANUgOwBFO1C+HZZhePoP83M65WGDmbpwFYJSWqavbl4SgDn4k8RYTA=="
},
"object.assign": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
"integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
"requires": {
"call-bind": "^1.0.0",
"define-properties": "^1.1.3",
"has-symbols": "^1.0.1",
"object-keys": "^1.1.1"
}
},
"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"
}
}
}
},
"is-typedarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
@@ -16656,11 +16806,6 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
},
"lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
},
"lodash.camelcase": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
@@ -16884,7 +17029,7 @@
"dependencies": {
"query-string": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-2.4.2.tgz",
"resolved": "http://registry.npmjs.org/query-string/-/query-string-2.4.2.tgz",
"integrity": "sha1-fbBmZCCAS6qSrp8miWKFWnYUPfs=",
"requires": {
"strict-uri-encode": "^1.0.0"
@@ -17020,6 +17165,16 @@
"css-mediaquery": "^0.1.2"
}
},
"md5": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz",
"integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==",
"requires": {
"charenc": "0.0.2",
"crypt": "0.0.2",
"is-buffer": "~1.1.6"
}
},
"md5.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
@@ -18001,7 +18156,7 @@
"dependencies": {
"JSONStream": {
"version": "1.3.5",
"resolved": false,
"resolved": "",
"integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==",
"dev": true,
"requires": {
@@ -18011,13 +18166,13 @@
},
"abbrev": {
"version": "1.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"dev": true
},
"agent-base": {
"version": "4.3.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==",
"dev": true,
"requires": {
@@ -18026,7 +18181,7 @@
},
"agentkeepalive": {
"version": "3.5.2",
"resolved": false,
"resolved": "",
"integrity": "sha512-e0L/HNe6qkQ7H19kTlRRqUibEAwDK5AFk6y3PtMsuut2VAH6+Q4xZml1tNDJD7kSAyqmbG/K08K5WEJYtUrSlQ==",
"dev": true,
"requires": {
@@ -18035,7 +18190,7 @@
},
"ajv": {
"version": "5.5.2",
"resolved": false,
"resolved": "",
"integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
"dev": true,
"requires": {
@@ -18047,7 +18202,7 @@
},
"ansi-align": {
"version": "2.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=",
"dev": true,
"requires": {
@@ -18056,13 +18211,13 @@
},
"ansi-regex": {
"version": "2.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
"dev": true
},
"ansi-styles": {
"version": "3.2.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"requires": {
@@ -18071,31 +18226,31 @@
},
"ansicolors": {
"version": "0.3.2",
"resolved": false,
"resolved": "",
"integrity": "sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk=",
"dev": true
},
"ansistyles": {
"version": "0.1.3",
"resolved": false,
"resolved": "",
"integrity": "sha1-XeYEFb2gcbs3EnhUyGT0GyMlRTk=",
"dev": true
},
"aproba": {
"version": "2.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==",
"dev": true
},
"archy": {
"version": "1.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=",
"dev": true
},
"are-we-there-yet": {
"version": "1.1.4",
"resolved": false,
"resolved": "",
"integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=",
"dev": true,
"requires": {
@@ -18105,7 +18260,7 @@
"dependencies": {
"readable-stream": {
"version": "2.3.6",
"resolved": false,
"resolved": "",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"dev": true,
"requires": {
@@ -18120,7 +18275,7 @@
},
"string_decoder": {
"version": "1.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
@@ -18131,13 +18286,13 @@
},
"asap": {
"version": "2.0.6",
"resolved": false,
"resolved": "",
"integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=",
"dev": true
},
"asn1": {
"version": "0.2.4",
"resolved": false,
"resolved": "",
"integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
"dev": true,
"requires": {
@@ -18146,37 +18301,37 @@
},
"assert-plus": {
"version": "1.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
"dev": true
},
"asynckit": {
"version": "0.4.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
"dev": true
},
"aws-sign2": {
"version": "0.7.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=",
"dev": true
},
"aws4": {
"version": "1.8.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==",
"dev": true
},
"balanced-match": {
"version": "1.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true
},
"bcrypt-pbkdf": {
"version": "1.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
"dev": true,
"optional": true,
@@ -18186,7 +18341,7 @@
},
"bin-links": {
"version": "1.1.8",
"resolved": false,
"resolved": "",
"integrity": "sha512-KgmVfx+QqggqP9dA3iIc5pA4T1qEEEL+hOhOhNPaUm77OTrJoOXE/C05SJLNJe6m/2wUK7F1tDSou7n5TfCDzQ==",
"dev": true,
"requires": {
@@ -18200,13 +18355,13 @@
},
"bluebird": {
"version": "3.5.5",
"resolved": false,
"resolved": "",
"integrity": "sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==",
"dev": true
},
"boxen": {
"version": "1.3.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==",
"dev": true,
"requires": {
@@ -18221,7 +18376,7 @@
},
"brace-expansion": {
"version": "1.1.11",
"resolved": false,
"resolved": "",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"requires": {
@@ -18231,31 +18386,31 @@
},
"buffer-from": {
"version": "1.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-83apNb8KK0Se60UE1+4Ukbe3HbfELJ6UlI4ldtOGs7So4KD26orJM8hIY9lxdzP+UpItH1Yh/Y8GUvNFWFFRxA==",
"dev": true
},
"builtins": {
"version": "1.0.3",
"resolved": false,
"resolved": "",
"integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=",
"dev": true
},
"byline": {
"version": "5.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=",
"dev": true
},
"byte-size": {
"version": "5.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-/XuKeqWocKsYa/cBY1YbSJSWWqTi4cFgr9S6OyM7PBaPbr9zvNGwWP33vt0uqGhwDdN+y3yhbXVILEUpnwEWGw==",
"dev": true
},
"cacache": {
"version": "12.0.3",
"resolved": false,
"resolved": "",
"integrity": "sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw==",
"dev": true,
"requires": {
@@ -18278,31 +18433,31 @@
},
"call-limit": {
"version": "1.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-5twvci5b9eRBw2wCfPtN0GmlR2/gadZqyFpPhOK6CvMFoFgA+USnZ6Jpu1lhG9h85pQ3Ouil3PfXWRD4EUaRiQ==",
"dev": true
},
"camelcase": {
"version": "4.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=",
"dev": true
},
"capture-stack-trace": {
"version": "1.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=",
"dev": true
},
"caseless": {
"version": "0.12.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
"dev": true
},
"chalk": {
"version": "2.4.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
"dev": true,
"requires": {
@@ -18313,19 +18468,19 @@
},
"chownr": {
"version": "1.1.4",
"resolved": false,
"resolved": "",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"dev": true
},
"ci-info": {
"version": "2.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==",
"dev": true
},
"cidr-regex": {
"version": "2.0.10",
"resolved": false,
"resolved": "",
"integrity": "sha512-sB3ogMQXWvreNPbJUZMRApxuRYd+KoIo4RGQ81VatjmMW6WJPo+IJZ2846FGItr9VzKo5w7DXzijPLGtSd0N3Q==",
"dev": true,
"requires": {
@@ -18334,13 +18489,13 @@
},
"cli-boxes": {
"version": "1.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=",
"dev": true
},
"cli-columns": {
"version": "3.1.2",
"resolved": false,
"resolved": "",
"integrity": "sha1-ZzLZcpee/CrkRKHwjgj6E5yWoY4=",
"dev": true,
"requires": {
@@ -18350,7 +18505,7 @@
},
"cli-table3": {
"version": "0.5.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==",
"dev": true,
"requires": {
@@ -18361,7 +18516,7 @@
},
"cliui": {
"version": "5.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
"dev": true,
"requires": {
@@ -18372,19 +18527,19 @@
"dependencies": {
"ansi-regex": {
"version": "4.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"dev": true
},
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
"dev": true
},
"string-width": {
"version": "3.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
"dev": true,
"requires": {
@@ -18395,7 +18550,7 @@
},
"strip-ansi": {
"version": "5.2.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"dev": true,
"requires": {
@@ -18406,13 +18561,13 @@
},
"clone": {
"version": "1.0.4",
"resolved": false,
"resolved": "",
"integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=",
"dev": true
},
"cmd-shim": {
"version": "3.0.3",
"resolved": false,
"resolved": "",
"integrity": "sha512-DtGg+0xiFhQIntSBRzL2fRQBnmtAVwXIDo4Qq46HPpObYquxMaZS4sb82U9nH91qJrlosC1wa9gwr0QyL/HypA==",
"dev": true,
"requires": {
@@ -18422,19 +18577,19 @@
},
"co": {
"version": "4.6.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=",
"dev": true
},
"code-point-at": {
"version": "1.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
"dev": true
},
"color-convert": {
"version": "1.9.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==",
"dev": true,
"requires": {
@@ -18443,20 +18598,20 @@
},
"color-name": {
"version": "1.1.3",
"resolved": false,
"resolved": "",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"dev": true
},
"colors": {
"version": "1.3.3",
"resolved": false,
"resolved": "",
"integrity": "sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg==",
"dev": true,
"optional": true
},
"columnify": {
"version": "1.5.4",
"resolved": false,
"resolved": "",
"integrity": "sha1-Rzfd8ce2mop8NAVweC6UfuyOeLs=",
"dev": true,
"requires": {
@@ -18466,7 +18621,7 @@
},
"combined-stream": {
"version": "1.0.6",
"resolved": false,
"resolved": "",
"integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=",
"dev": true,
"requires": {
@@ -18475,13 +18630,13 @@
},
"concat-map": {
"version": "0.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true
},
"concat-stream": {
"version": "1.6.2",
"resolved": false,
"resolved": "",
"integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
"dev": true,
"requires": {
@@ -18493,7 +18648,7 @@
"dependencies": {
"readable-stream": {
"version": "2.3.6",
"resolved": false,
"resolved": "",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"dev": true,
"requires": {
@@ -18508,7 +18663,7 @@
},
"string_decoder": {
"version": "1.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
@@ -18519,7 +18674,7 @@
},
"config-chain": {
"version": "1.1.12",
"resolved": false,
"resolved": "",
"integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==",
"dev": true,
"requires": {
@@ -18529,7 +18684,7 @@
},
"configstore": {
"version": "3.1.5",
"resolved": false,
"resolved": "",
"integrity": "sha512-nlOhI4+fdzoK5xmJ+NY+1gZK56bwEaWZr8fYuXohZ9Vkc1o3a4T/R3M+yE/w7x/ZVJ1zF8c+oaOvF0dztdUgmA==",
"dev": true,
"requires": {
@@ -18543,13 +18698,13 @@
},
"console-control-strings": {
"version": "1.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
"dev": true
},
"copy-concurrently": {
"version": "1.0.5",
"resolved": false,
"resolved": "",
"integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==",
"dev": true,
"requires": {
@@ -18563,13 +18718,13 @@
"dependencies": {
"aproba": {
"version": "1.2.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
"dev": true
},
"iferr": {
"version": "0.1.5",
"resolved": false,
"resolved": "",
"integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=",
"dev": true
}
@@ -18577,13 +18732,13 @@
},
"core-util-is": {
"version": "1.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
"dev": true
},
"create-error-class": {
"version": "3.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=",
"dev": true,
"requires": {
@@ -18592,7 +18747,7 @@
},
"cross-spawn": {
"version": "5.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
"dev": true,
"requires": {
@@ -18603,7 +18758,7 @@
"dependencies": {
"lru-cache": {
"version": "4.1.5",
"resolved": false,
"resolved": "",
"integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
"dev": true,
"requires": {
@@ -18613,7 +18768,7 @@
},
"yallist": {
"version": "2.1.2",
"resolved": false,
"resolved": "",
"integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
"dev": true
}
@@ -18621,19 +18776,19 @@
},
"crypto-random-string": {
"version": "1.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=",
"dev": true
},
"cyclist": {
"version": "0.2.2",
"resolved": false,
"resolved": "",
"integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=",
"dev": true
},
"dashdash": {
"version": "1.14.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
"dev": true,
"requires": {
@@ -18642,7 +18797,7 @@
},
"debug": {
"version": "3.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"dev": true,
"requires": {
@@ -18651,7 +18806,7 @@
"dependencies": {
"ms": {
"version": "2.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"dev": true
}
@@ -18659,31 +18814,31 @@
},
"debuglog": {
"version": "1.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=",
"dev": true
},
"decamelize": {
"version": "1.2.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
"dev": true
},
"decode-uri-component": {
"version": "0.2.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
"dev": true
},
"deep-extend": {
"version": "0.6.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"dev": true
},
"defaults": {
"version": "1.0.3",
"resolved": false,
"resolved": "",
"integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=",
"dev": true,
"requires": {
@@ -18692,7 +18847,7 @@
},
"define-properties": {
"version": "1.1.3",
"resolved": false,
"resolved": "",
"integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
"dev": true,
"requires": {
@@ -18701,31 +18856,31 @@
},
"delayed-stream": {
"version": "1.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
"dev": true
},
"delegates": {
"version": "1.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
"dev": true
},
"detect-indent": {
"version": "5.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-OHHMCmoALow+Wzz38zYmRnXwa50=",
"dev": true
},
"detect-newline": {
"version": "2.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=",
"dev": true
},
"dezalgo": {
"version": "1.0.3",
"resolved": false,
"resolved": "",
"integrity": "sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY=",
"dev": true,
"requires": {
@@ -18735,7 +18890,7 @@
},
"dot-prop": {
"version": "4.2.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==",
"dev": true,
"requires": {
@@ -18744,19 +18899,19 @@
},
"dotenv": {
"version": "5.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-4As8uPrjfwb7VXC+WnLCbXK7y+Ueb2B3zgNCePYfhxS1PYeaO1YTeplffTEcbfLhvFNGLAz90VvJs9yomG7bow==",
"dev": true
},
"duplexer3": {
"version": "0.1.4",
"resolved": false,
"resolved": "",
"integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=",
"dev": true
},
"duplexify": {
"version": "3.6.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-fO3Di4tBKJpYTFHAxTU00BcfWMY9w24r/x21a6rZRbsD/ToUgGxsMbiGRmB7uVAXeGKXD9MwiLZa5E97EVgIRQ==",
"dev": true,
"requires": {
@@ -18768,7 +18923,7 @@
"dependencies": {
"readable-stream": {
"version": "2.3.6",
"resolved": false,
"resolved": "",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"dev": true,
"requires": {
@@ -18783,7 +18938,7 @@
},
"string_decoder": {
"version": "1.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
@@ -18794,7 +18949,7 @@
},
"ecc-jsbn": {
"version": "0.1.2",
"resolved": false,
"resolved": "",
"integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
"dev": true,
"optional": true,
@@ -18805,19 +18960,19 @@
},
"editor": {
"version": "1.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-YMf4e9YrzGqJT6jM1q+3gjok90I=",
"dev": true
},
"emoji-regex": {
"version": "7.0.3",
"resolved": false,
"resolved": "",
"integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
"dev": true
},
"encoding": {
"version": "0.1.12",
"resolved": false,
"resolved": "",
"integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
"dev": true,
"requires": {
@@ -18826,7 +18981,7 @@
},
"end-of-stream": {
"version": "1.4.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==",
"dev": true,
"requires": {
@@ -18835,19 +18990,19 @@
},
"env-paths": {
"version": "2.2.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==",
"dev": true
},
"err-code": {
"version": "1.1.2",
"resolved": false,
"resolved": "",
"integrity": "sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA=",
"dev": true
},
"errno": {
"version": "0.1.7",
"resolved": false,
"resolved": "",
"integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==",
"dev": true,
"requires": {
@@ -18856,7 +19011,7 @@
},
"es-abstract": {
"version": "1.12.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==",
"dev": true,
"requires": {
@@ -18869,7 +19024,7 @@
},
"es-to-primitive": {
"version": "1.2.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==",
"dev": true,
"requires": {
@@ -18880,13 +19035,13 @@
},
"es6-promise": {
"version": "4.2.8",
"resolved": false,
"resolved": "",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==",
"dev": true
},
"es6-promisify": {
"version": "5.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
"dev": true,
"requires": {
@@ -18895,13 +19050,13 @@
},
"escape-string-regexp": {
"version": "1.0.5",
"resolved": false,
"resolved": "",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
"dev": true
},
"execa": {
"version": "0.7.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=",
"dev": true,
"requires": {
@@ -18916,7 +19071,7 @@
"dependencies": {
"get-stream": {
"version": "3.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=",
"dev": true
}
@@ -18924,43 +19079,43 @@
},
"extend": {
"version": "3.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"dev": true
},
"extsprintf": {
"version": "1.3.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
"dev": true
},
"fast-deep-equal": {
"version": "1.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=",
"dev": true
},
"fast-json-stable-stringify": {
"version": "2.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=",
"dev": true
},
"figgy-pudding": {
"version": "3.5.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==",
"dev": true
},
"find-npm-prefix": {
"version": "1.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha512-KEftzJ+H90x6pcKtdXZEPsQse8/y/UnvzRKrOSQFprnrGaFuJ62fVkP34Iu2IYuMvyauCyoLTNkJZgrrGA2wkA==",
"dev": true
},
"flush-write-stream": {
"version": "1.0.3",
"resolved": false,
"resolved": "",
"integrity": "sha512-calZMC10u0FMUqoiunI2AiGIIUtUIvifNwkHhNupZH4cbNnW1Itkoh/Nf5HFYmDrwWPjrUxpkZT0KhuCq0jmGw==",
"dev": true,
"requires": {
@@ -18970,7 +19125,7 @@
"dependencies": {
"readable-stream": {
"version": "2.3.6",
"resolved": false,
"resolved": "",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"dev": true,
"requires": {
@@ -18985,7 +19140,7 @@
},
"string_decoder": {
"version": "1.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
@@ -18996,13 +19151,13 @@
},
"forever-agent": {
"version": "0.6.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=",
"dev": true
},
"form-data": {
"version": "2.3.2",
"resolved": false,
"resolved": "",
"integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=",
"dev": true,
"requires": {
@@ -19013,7 +19168,7 @@
},
"from2": {
"version": "2.3.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=",
"dev": true,
"requires": {
@@ -19023,7 +19178,7 @@
"dependencies": {
"readable-stream": {
"version": "2.3.6",
"resolved": false,
"resolved": "",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"dev": true,
"requires": {
@@ -19038,7 +19193,7 @@
},
"string_decoder": {
"version": "1.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
@@ -19049,7 +19204,7 @@
},
"fs-minipass": {
"version": "1.2.7",
"resolved": false,
"resolved": "",
"integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==",
"dev": true,
"requires": {
@@ -19058,7 +19213,7 @@
"dependencies": {
"minipass": {
"version": "2.9.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
"dev": true,
"requires": {
@@ -19070,7 +19225,7 @@
},
"fs-vacuum": {
"version": "1.2.10",
"resolved": false,
"resolved": "",
"integrity": "sha1-t2Kb7AekAxolSP35n17PHMizHjY=",
"dev": true,
"requires": {
@@ -19081,7 +19236,7 @@
},
"fs-write-stream-atomic": {
"version": "1.0.10",
"resolved": false,
"resolved": "",
"integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=",
"dev": true,
"requires": {
@@ -19093,13 +19248,13 @@
"dependencies": {
"iferr": {
"version": "0.1.5",
"resolved": false,
"resolved": "",
"integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=",
"dev": true
},
"readable-stream": {
"version": "2.3.6",
"resolved": false,
"resolved": "",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"dev": true,
"requires": {
@@ -19114,7 +19269,7 @@
},
"string_decoder": {
"version": "1.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
@@ -19125,19 +19280,19 @@
},
"fs.realpath": {
"version": "1.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true
},
"function-bind": {
"version": "1.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
},
"gauge": {
"version": "2.7.4",
"resolved": false,
"resolved": "",
"integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
"dev": true,
"requires": {
@@ -19153,13 +19308,13 @@
"dependencies": {
"aproba": {
"version": "1.2.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
"dev": true
},
"string-width": {
"version": "1.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"dev": true,
"requires": {
@@ -19172,13 +19327,13 @@
},
"genfun": {
"version": "5.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-KGDOARWVga7+rnB3z9Sd2Letx515owfk0hSxHGuqjANb1M+x2bGZGqHLiozPsYMdM2OubeMni/Hpwmjq6qIUhA==",
"dev": true
},
"gentle-fs": {
"version": "2.3.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-OlwBBwqCFPcjm33rF2BjW+Pr6/ll2741l+xooiwTCeaX2CA1ZuclavyMBe0/KlR21/XGsgY6hzEQZ15BdNa13Q==",
"dev": true,
"requires": {
@@ -19197,13 +19352,13 @@
"dependencies": {
"aproba": {
"version": "1.2.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
"dev": true
},
"iferr": {
"version": "0.1.5",
"resolved": false,
"resolved": "",
"integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=",
"dev": true
}
@@ -19211,13 +19366,13 @@
},
"get-caller-file": {
"version": "2.0.5",
"resolved": false,
"resolved": "",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true
},
"get-stream": {
"version": "4.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
"dev": true,
"requires": {
@@ -19226,7 +19381,7 @@
},
"getpass": {
"version": "0.1.7",
"resolved": false,
"resolved": "",
"integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
"dev": true,
"requires": {
@@ -19235,7 +19390,7 @@
},
"glob": {
"version": "7.1.6",
"resolved": false,
"resolved": "",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"dev": true,
"requires": {
@@ -19249,7 +19404,7 @@
},
"global-dirs": {
"version": "0.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=",
"dev": true,
"requires": {
@@ -19258,7 +19413,7 @@
},
"got": {
"version": "6.7.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=",
"dev": true,
"requires": {
@@ -19277,7 +19432,7 @@
"dependencies": {
"get-stream": {
"version": "3.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=",
"dev": true
}
@@ -19285,19 +19440,19 @@
},
"graceful-fs": {
"version": "4.2.4",
"resolved": false,
"resolved": "",
"integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==",
"dev": true
},
"har-schema": {
"version": "2.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=",
"dev": true
},
"har-validator": {
"version": "5.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==",
"dev": true,
"requires": {
@@ -19307,7 +19462,7 @@
},
"has": {
"version": "1.0.3",
"resolved": false,
"resolved": "",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"requires": {
@@ -19316,37 +19471,37 @@
},
"has-flag": {
"version": "3.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
"dev": true
},
"has-symbols": {
"version": "1.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=",
"dev": true
},
"has-unicode": {
"version": "2.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=",
"dev": true
},
"hosted-git-info": {
"version": "2.8.8",
"resolved": false,
"resolved": "",
"integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
"dev": true
},
"http-cache-semantics": {
"version": "3.8.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==",
"dev": true
},
"http-proxy-agent": {
"version": "2.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==",
"dev": true,
"requires": {
@@ -19356,7 +19511,7 @@
},
"http-signature": {
"version": "1.2.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
"dev": true,
"requires": {
@@ -19367,7 +19522,7 @@
},
"https-proxy-agent": {
"version": "2.2.4",
"resolved": false,
"resolved": "",
"integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==",
"dev": true,
"requires": {
@@ -19377,7 +19532,7 @@
},
"humanize-ms": {
"version": "1.2.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=",
"dev": true,
"requires": {
@@ -19386,7 +19541,7 @@
},
"iconv-lite": {
"version": "0.4.23",
"resolved": false,
"resolved": "",
"integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
"dev": true,
"requires": {
@@ -19395,13 +19550,13 @@
},
"iferr": {
"version": "1.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha512-9AfeLfji44r5TKInjhz3W9DyZI1zR1JAf2hVBMGhddAKPqBsupb89jGfbCTHIGZd6fGZl9WlHdn4AObygyMKwg==",
"dev": true
},
"ignore-walk": {
"version": "3.0.3",
"resolved": false,
"resolved": "",
"integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==",
"dev": true,
"requires": {
@@ -19410,25 +19565,25 @@
},
"import-lazy": {
"version": "2.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=",
"dev": true
},
"imurmurhash": {
"version": "0.1.4",
"resolved": false,
"resolved": "",
"integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
"dev": true
},
"infer-owner": {
"version": "1.0.4",
"resolved": false,
"resolved": "",
"integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==",
"dev": true
},
"inflight": {
"version": "1.0.6",
"resolved": false,
"resolved": "",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dev": true,
"requires": {
@@ -19438,19 +19593,19 @@
},
"inherits": {
"version": "2.0.4",
"resolved": false,
"resolved": "",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
},
"ini": {
"version": "1.3.5",
"resolved": false,
"resolved": "",
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
"dev": true
},
"init-package-json": {
"version": "1.10.3",
"resolved": false,
"resolved": "",
"integrity": "sha512-zKSiXKhQveNteyhcj1CoOP8tqp1QuxPIPBl8Bid99DGLFqA1p87M6lNgfjJHSBoWJJlidGOv5rWjyYKEB3g2Jw==",
"dev": true,
"requires": {
@@ -19466,25 +19621,25 @@
},
"ip": {
"version": "1.1.5",
"resolved": false,
"resolved": "",
"integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=",
"dev": true
},
"ip-regex": {
"version": "2.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=",
"dev": true
},
"is-callable": {
"version": "1.1.4",
"resolved": false,
"resolved": "",
"integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==",
"dev": true
},
"is-ci": {
"version": "1.2.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==",
"dev": true,
"requires": {
@@ -19493,7 +19648,7 @@
"dependencies": {
"ci-info": {
"version": "1.6.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==",
"dev": true
}
@@ -19501,7 +19656,7 @@
},
"is-cidr": {
"version": "3.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-8Xnnbjsb0x462VoYiGlhEi+drY8SFwrHiSYuzc/CEwco55vkehTaxAyIjEdpi3EMvLPPJAJi9FlzP+h+03gp0Q==",
"dev": true,
"requires": {
@@ -19510,13 +19665,13 @@
},
"is-date-object": {
"version": "1.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=",
"dev": true
},
"is-fullwidth-code-point": {
"version": "1.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"dev": true,
"requires": {
@@ -19525,7 +19680,7 @@
},
"is-installed-globally": {
"version": "0.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=",
"dev": true,
"requires": {
@@ -19535,19 +19690,19 @@
},
"is-npm": {
"version": "1.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=",
"dev": true
},
"is-obj": {
"version": "1.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=",
"dev": true
},
"is-path-inside": {
"version": "1.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=",
"dev": true,
"requires": {
@@ -19556,13 +19711,13 @@
},
"is-redirect": {
"version": "1.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=",
"dev": true
},
"is-regex": {
"version": "1.0.4",
"resolved": false,
"resolved": "",
"integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=",
"dev": true,
"requires": {
@@ -19571,19 +19726,19 @@
},
"is-retry-allowed": {
"version": "1.2.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==",
"dev": true
},
"is-stream": {
"version": "1.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=",
"dev": true
},
"is-symbol": {
"version": "1.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==",
"dev": true,
"requires": {
@@ -19592,68 +19747,68 @@
},
"is-typedarray": {
"version": "1.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=",
"dev": true
},
"isarray": {
"version": "1.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"dev": true
},
"isexe": {
"version": "2.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
"dev": true
},
"isstream": {
"version": "0.1.2",
"resolved": false,
"resolved": "",
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
"dev": true
},
"jsbn": {
"version": "0.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
"dev": true,
"optional": true
},
"json-parse-better-errors": {
"version": "1.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
"dev": true
},
"json-schema": {
"version": "0.2.3",
"resolved": false,
"resolved": "",
"integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=",
"dev": true
},
"json-schema-traverse": {
"version": "0.3.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=",
"dev": true
},
"json-stringify-safe": {
"version": "5.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
"dev": true
},
"jsonparse": {
"version": "1.3.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=",
"dev": true
},
"jsprim": {
"version": "1.4.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
"dev": true,
"requires": {
@@ -19665,7 +19820,7 @@
},
"latest-version": {
"version": "3.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=",
"dev": true,
"requires": {
@@ -19674,13 +19829,13 @@
},
"lazy-property": {
"version": "1.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-hN3Es3Bnm6i9TNz6TAa0PVcREUc=",
"dev": true
},
"libcipm": {
"version": "4.0.8",
"resolved": false,
"resolved": "",
"integrity": "sha512-IN3hh2yDJQtZZ5paSV4fbvJg4aHxCCg5tcZID/dSVlTuUiWktsgaldVljJv6Z5OUlYspx6xQkbR0efNodnIrOA==",
"dev": true,
"requires": {
@@ -19703,7 +19858,7 @@
},
"libnpm": {
"version": "3.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-d7jU5ZcMiTfBqTUJVZ3xid44fE5ERBm9vBnmhp2ECD2Ls+FNXWxHSkO7gtvrnbLO78gwPdNPz1HpsF3W4rjkBQ==",
"dev": true,
"requires": {
@@ -19731,7 +19886,7 @@
},
"libnpmaccess": {
"version": "3.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha512-01512AK7MqByrI2mfC7h5j8N9V4I7MHJuk9buo8Gv+5QgThpOgpjB7sQBDDkeZqRteFb1QM/6YNdHfG7cDvfAQ==",
"dev": true,
"requires": {
@@ -19743,7 +19898,7 @@
},
"libnpmconfig": {
"version": "1.2.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-9esX8rTQAHqarx6qeZqmGQKBNZR5OIbl/Ayr0qQDy3oXja2iFVQQI81R6GZ2a02bSNZ9p3YOGX1O6HHCb1X7kA==",
"dev": true,
"requires": {
@@ -19754,7 +19909,7 @@
"dependencies": {
"find-up": {
"version": "3.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
"dev": true,
"requires": {
@@ -19763,7 +19918,7 @@
},
"locate-path": {
"version": "3.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
"dev": true,
"requires": {
@@ -19773,7 +19928,7 @@
},
"p-limit": {
"version": "2.2.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==",
"dev": true,
"requires": {
@@ -19782,7 +19937,7 @@
},
"p-locate": {
"version": "3.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
"dev": true,
"requires": {
@@ -19791,7 +19946,7 @@
},
"p-try": {
"version": "2.2.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"dev": true
}
@@ -19799,7 +19954,7 @@
},
"libnpmhook": {
"version": "5.0.3",
"resolved": false,
"resolved": "",
"integrity": "sha512-UdNLMuefVZra/wbnBXECZPefHMGsVDTq5zaM/LgKNE9Keyl5YXQTnGAzEo+nFOpdRqTWI9LYi4ApqF9uVCCtuA==",
"dev": true,
"requires": {
@@ -19811,7 +19966,7 @@
},
"libnpmorg": {
"version": "1.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-0sRUXLh+PLBgZmARvthhYXQAWn0fOsa6T5l3JSe2n9vKG/lCVK4nuG7pDsa7uMq+uTt2epdPK+a2g6btcY11Ww==",
"dev": true,
"requires": {
@@ -19823,7 +19978,7 @@
},
"libnpmpublish": {
"version": "1.1.2",
"resolved": false,
"resolved": "",
"integrity": "sha512-2yIwaXrhTTcF7bkJKIKmaCV9wZOALf/gsTDxVSu/Gu/6wiG3fA8ce8YKstiWKTxSFNC0R7isPUb6tXTVFZHt2g==",
"dev": true,
"requires": {
@@ -19840,7 +19995,7 @@
},
"libnpmsearch": {
"version": "2.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha512-VTBbV55Q6fRzTdzziYCr64+f8AopQ1YZ+BdPOv16UegIEaE8C0Kch01wo4s3kRTFV64P121WZJwgmBwrq68zYg==",
"dev": true,
"requires": {
@@ -19851,7 +20006,7 @@
},
"libnpmteam": {
"version": "1.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha512-p420vM28Us04NAcg1rzgGW63LMM6rwe+6rtZpfDxCcXxM0zUTLl7nPFEnRF3JfFBF5skF/yuZDUthTsHgde8QA==",
"dev": true,
"requires": {
@@ -19863,7 +20018,7 @@
},
"libnpx": {
"version": "10.2.4",
"resolved": false,
"resolved": "",
"integrity": "sha512-BPc0D1cOjBeS8VIBKUu5F80s6njm0wbVt7CsGMrIcJ+SI7pi7V0uVPGpEMH9H5L8csOcclTxAXFE2VAsJXUhfA==",
"dev": true,
"requires": {
@@ -19879,7 +20034,7 @@
},
"lock-verify": {
"version": "2.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-vcLpxnGvrqisKvLQ2C2v0/u7LVly17ak2YSgoK4PrdsYBXQIax19vhKiLfvKNFx7FRrpTnitrpzF/uuCMuorIg==",
"dev": true,
"requires": {
@@ -19889,7 +20044,7 @@
},
"lockfile": {
"version": "1.0.4",
"resolved": false,
"resolved": "",
"integrity": "sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA==",
"dev": true,
"requires": {
@@ -19898,13 +20053,13 @@
},
"lodash._baseindexof": {
"version": "3.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-/lK1OhxnYeQmGNZU5KJXie1hgiw=",
"dev": true
},
"lodash._baseuniq": {
"version": "4.6.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-DrtE5FaBSveQXGIS+iybLVG4Qeg=",
"dev": true,
"requires": {
@@ -19914,19 +20069,19 @@
},
"lodash._bindcallback": {
"version": "3.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-5THCdkTPi1epnhftlbNcdIeJOS4=",
"dev": true
},
"lodash._cacheindexof": {
"version": "3.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha1-PcaayCSY0u5ePOVgkbr9Ktx73pI=",
"dev": true
},
"lodash._createcache": {
"version": "3.1.2",
"resolved": false,
"resolved": "",
"integrity": "sha1-VtagZAF2JeeevKa4AY4XRAvc8JM=",
"dev": true,
"requires": {
@@ -19935,61 +20090,61 @@
},
"lodash._createset": {
"version": "4.0.3",
"resolved": false,
"resolved": "",
"integrity": "sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY=",
"dev": true
},
"lodash._getnative": {
"version": "3.9.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=",
"dev": true
},
"lodash._root": {
"version": "3.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=",
"dev": true
},
"lodash.clonedeep": {
"version": "4.5.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=",
"dev": true
},
"lodash.restparam": {
"version": "3.6.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=",
"dev": true
},
"lodash.union": {
"version": "4.6.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=",
"dev": true
},
"lodash.uniq": {
"version": "4.5.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=",
"dev": true
},
"lodash.without": {
"version": "4.4.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-PNRXSgC2e643OpS3SHcmQFB7eqw=",
"dev": true
},
"lowercase-keys": {
"version": "1.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==",
"dev": true
},
"lru-cache": {
"version": "5.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
"dev": true,
"requires": {
@@ -19998,7 +20153,7 @@
},
"make-dir": {
"version": "1.3.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==",
"dev": true,
"requires": {
@@ -20007,7 +20162,7 @@
},
"make-fetch-happen": {
"version": "5.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha512-07JHC0r1ykIoruKO8ifMXu+xEU8qOXDFETylktdug6vJDACnP+HKevOu3PXyNPzFyTSlz8vrBYlBO1JZRe8Cag==",
"dev": true,
"requires": {
@@ -20026,19 +20181,19 @@
},
"meant": {
"version": "1.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha512-KN+1uowN/NK+sT/Lzx7WSGIj2u+3xe5n2LbwObfjOhPZiA+cCfCm6idVl0RkEfjThkw5XJ96CyRcanq6GmKtUg==",
"dev": true
},
"mime-db": {
"version": "1.35.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-JWT/IcCTsB0Io3AhWUMjRqucrHSPsSf2xKLaRldJVULioggvkJvggZ3VXNNSRkCddE6D+BUI4HEIZIA2OjwIvg==",
"dev": true
},
"mime-types": {
"version": "2.1.19",
"resolved": false,
"resolved": "",
"integrity": "sha512-P1tKYHVSZ6uFo26mtnve4HQFE3koh1UWVkp8YUC+ESBHe945xWSoXuHHiGarDqcEZ+whpCDnlNw5LON0kLo+sw==",
"dev": true,
"requires": {
@@ -20047,7 +20202,7 @@
},
"minimatch": {
"version": "3.0.4",
"resolved": false,
"resolved": "",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"requires": {
@@ -20056,13 +20211,13 @@
},
"minimist": {
"version": "1.2.5",
"resolved": false,
"resolved": "",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true
},
"minizlib": {
"version": "1.3.3",
"resolved": false,
"resolved": "",
"integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==",
"dev": true,
"requires": {
@@ -20071,7 +20226,7 @@
"dependencies": {
"minipass": {
"version": "2.9.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
"dev": true,
"requires": {
@@ -20083,7 +20238,7 @@
},
"mississippi": {
"version": "3.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==",
"dev": true,
"requires": {
@@ -20101,7 +20256,7 @@
},
"mkdirp": {
"version": "0.5.5",
"resolved": false,
"resolved": "",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"dev": true,
"requires": {
@@ -20110,7 +20265,7 @@
"dependencies": {
"minimist": {
"version": "1.2.5",
"resolved": false,
"resolved": "",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true
}
@@ -20118,7 +20273,7 @@
},
"move-concurrently": {
"version": "1.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=",
"dev": true,
"requires": {
@@ -20132,7 +20287,7 @@
"dependencies": {
"aproba": {
"version": "1.2.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
"dev": true
}
@@ -20140,19 +20295,19 @@
},
"ms": {
"version": "2.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
"dev": true
},
"mute-stream": {
"version": "0.0.7",
"resolved": false,
"resolved": "",
"integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=",
"dev": true
},
"node-fetch-npm": {
"version": "2.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha512-nJIxm1QmAj4v3nfCvEeCrYSoVwXyxLnaPBK5W1W5DGEJwjlKuC2VEUycGw5oxk+4zZahRrB84PUJJgEmhFTDFw==",
"dev": true,
"requires": {
@@ -20163,7 +20318,7 @@
},
"node-gyp": {
"version": "5.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-OUTryc5bt/P8zVgNUmC6xdXiDJxLMAW8cF5tLQOT9E5sOQj+UeQxnnPy74K3CLCa/SOjjBlbuzDLR8ANwA+wmw==",
"dev": true,
"requires": {
@@ -20182,7 +20337,7 @@
},
"nopt": {
"version": "4.0.3",
"resolved": false,
"resolved": "",
"integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==",
"dev": true,
"requires": {
@@ -20192,7 +20347,7 @@
},
"normalize-package-data": {
"version": "2.5.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
"dev": true,
"requires": {
@@ -20204,7 +20359,7 @@
"dependencies": {
"resolve": {
"version": "1.10.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==",
"dev": true,
"requires": {
@@ -20215,7 +20370,7 @@
},
"npm-audit-report": {
"version": "1.3.3",
"resolved": false,
"resolved": "",
"integrity": "sha512-8nH/JjsFfAWMvn474HB9mpmMjrnKb1Hx/oTAdjv4PT9iZBvBxiZ+wtDUapHCJwLqYGQVPaAfs+vL5+5k9QndXw==",
"dev": true,
"requires": {
@@ -20225,7 +20380,7 @@
},
"npm-bundled": {
"version": "1.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==",
"dev": true,
"requires": {
@@ -20234,13 +20389,13 @@
},
"npm-cache-filename": {
"version": "1.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha1-3tMGxbC/yHCp6fr4I7xfKD4FrhE=",
"dev": true
},
"npm-install-checks": {
"version": "3.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha512-E4kzkyZDIWoin6uT5howP8VDvkM+E8IQDcHAycaAxMbwkqhIg5eEYALnXOl3Hq9MrkdQB/2/g1xwBINXdKSRkg==",
"dev": true,
"requires": {
@@ -20249,7 +20404,7 @@
},
"npm-lifecycle": {
"version": "3.1.5",
"resolved": false,
"resolved": "",
"integrity": "sha512-lDLVkjfZmvmfvpvBzA4vzee9cn+Me4orq0QF8glbswJVEbIcSNWib7qGOffolysc3teCqbbPZZkzbr3GQZTL1g==",
"dev": true,
"requires": {
@@ -20265,19 +20420,19 @@
},
"npm-logical-tree": {
"version": "1.2.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-AJI/qxDB2PWI4LG1CYN579AY1vCiNyWfkiquCsJWqntRu/WwimVrC8yXeILBFHDwxfOejxewlmnvW9XXjMlYIg==",
"dev": true
},
"npm-normalize-package-bin": {
"version": "1.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==",
"dev": true
},
"npm-package-arg": {
"version": "6.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-qBpssaL3IOZWi5vEKUKW0cO7kzLeT+EQO9W8RsLOZf76KF9E/K9+wH0C7t06HXPpaH8WH5xF1MExLuCwbTqRUg==",
"dev": true,
"requires": {
@@ -20289,7 +20444,7 @@
},
"npm-packlist": {
"version": "1.4.8",
"resolved": false,
"resolved": "",
"integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==",
"dev": true,
"requires": {
@@ -20300,7 +20455,7 @@
},
"npm-pick-manifest": {
"version": "3.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha512-wNprTNg+X5nf+tDi+hbjdHhM4bX+mKqv6XmPh7B5eG+QY9VARfQPfCEH013H5GqfNj6ee8Ij2fg8yk0mzps1Vw==",
"dev": true,
"requires": {
@@ -20311,7 +20466,7 @@
},
"npm-profile": {
"version": "4.0.4",
"resolved": false,
"resolved": "",
"integrity": "sha512-Ta8xq8TLMpqssF0H60BXS1A90iMoM6GeKwsmravJ6wYjWwSzcYBTdyWa3DZCYqPutacBMEm7cxiOkiIeCUAHDQ==",
"dev": true,
"requires": {
@@ -20322,7 +20477,7 @@
},
"npm-registry-fetch": {
"version": "4.0.7",
"resolved": false,
"resolved": "",
"integrity": "sha512-cny9v0+Mq6Tjz+e0erFAB+RYJ/AVGzkjnISiobqP8OWj9c9FLoZZu8/SPSKJWE17F1tk4018wfjV+ZbIbqC7fQ==",
"dev": true,
"requires": {
@@ -20337,7 +20492,7 @@
"dependencies": {
"safe-buffer": {
"version": "5.2.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true
}
@@ -20345,7 +20500,7 @@
},
"npm-run-path": {
"version": "2.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=",
"dev": true,
"requires": {
@@ -20354,13 +20509,13 @@
},
"npm-user-validate": {
"version": "1.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-jOyg9c6gTU6TUZ73LQVXp1Ei6VE=",
"dev": true
},
"npmlog": {
"version": "4.1.2",
"resolved": false,
"resolved": "",
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
"dev": true,
"requires": {
@@ -20372,31 +20527,31 @@
},
"number-is-nan": {
"version": "1.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
"dev": true
},
"oauth-sign": {
"version": "0.9.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
"dev": true
},
"object-assign": {
"version": "4.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
"dev": true
},
"object-keys": {
"version": "1.0.12",
"resolved": false,
"resolved": "",
"integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==",
"dev": true
},
"object.getownpropertydescriptors": {
"version": "2.0.3",
"resolved": false,
"resolved": "",
"integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=",
"dev": true,
"requires": {
@@ -20406,7 +20561,7 @@
},
"once": {
"version": "1.4.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"requires": {
@@ -20415,25 +20570,25 @@
},
"opener": {
"version": "1.5.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA==",
"dev": true
},
"os-homedir": {
"version": "1.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
"dev": true
},
"os-tmpdir": {
"version": "1.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
"dev": true
},
"osenv": {
"version": "0.1.5",
"resolved": false,
"resolved": "",
"integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
"dev": true,
"requires": {
@@ -20443,13 +20598,13 @@
},
"p-finally": {
"version": "1.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
"dev": true
},
"package-json": {
"version": "4.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=",
"dev": true,
"requires": {
@@ -20461,7 +20616,7 @@
},
"pacote": {
"version": "9.5.12",
"resolved": false,
"resolved": "",
"integrity": "sha512-BUIj/4kKbwWg4RtnBncXPJd15piFSVNpTzY0rysSr3VnMowTYgkGKcaHrbReepAkjTr8lH2CVWRi58Spg2CicQ==",
"dev": true,
"requires": {
@@ -20499,7 +20654,7 @@
"dependencies": {
"minipass": {
"version": "2.9.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
"dev": true,
"requires": {
@@ -20511,7 +20666,7 @@
},
"parallel-transform": {
"version": "1.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=",
"dev": true,
"requires": {
@@ -20522,7 +20677,7 @@
"dependencies": {
"readable-stream": {
"version": "2.3.6",
"resolved": false,
"resolved": "",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"dev": true,
"requires": {
@@ -20537,7 +20692,7 @@
},
"string_decoder": {
"version": "1.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
@@ -20548,67 +20703,67 @@
},
"path-exists": {
"version": "3.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
"dev": true
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"dev": true
},
"path-is-inside": {
"version": "1.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=",
"dev": true
},
"path-key": {
"version": "2.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
"dev": true
},
"path-parse": {
"version": "1.0.6",
"resolved": false,
"resolved": "",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
"dev": true
},
"performance-now": {
"version": "2.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=",
"dev": true
},
"pify": {
"version": "3.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
"dev": true
},
"prepend-http": {
"version": "1.0.4",
"resolved": false,
"resolved": "",
"integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=",
"dev": true
},
"process-nextick-args": {
"version": "2.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
"dev": true
},
"promise-inflight": {
"version": "1.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=",
"dev": true
},
"promise-retry": {
"version": "1.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-ZznpaOMFHaIM5kl/srUPaRHfPW0=",
"dev": true,
"requires": {
@@ -20618,7 +20773,7 @@
"dependencies": {
"retry": {
"version": "0.10.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=",
"dev": true
}
@@ -20626,7 +20781,7 @@
},
"promzard": {
"version": "0.3.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-JqXW7ox97kyxIggwWs+5O6OCqe4=",
"dev": true,
"requires": {
@@ -20635,13 +20790,13 @@
},
"proto-list": {
"version": "1.2.4",
"resolved": false,
"resolved": "",
"integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=",
"dev": true
},
"protoduck": {
"version": "5.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-WxoCeDCoCBY55BMvj4cAEjdVUFGRWed9ZxPlqTKYyw1nDDTQ4pqmnIMAGfJlg7Dx35uB/M+PHJPTmGOvaCaPTg==",
"dev": true,
"requires": {
@@ -20650,25 +20805,25 @@
},
"prr": {
"version": "1.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
"dev": true
},
"pseudomap": {
"version": "1.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=",
"dev": true
},
"psl": {
"version": "1.1.29",
"resolved": false,
"resolved": "",
"integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==",
"dev": true
},
"pump": {
"version": "3.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
"dev": true,
"requires": {
@@ -20678,7 +20833,7 @@
},
"pumpify": {
"version": "1.5.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==",
"dev": true,
"requires": {
@@ -20689,7 +20844,7 @@
"dependencies": {
"pump": {
"version": "2.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==",
"dev": true,
"requires": {
@@ -20701,25 +20856,25 @@
},
"punycode": {
"version": "1.4.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
"dev": true
},
"qrcode-terminal": {
"version": "0.12.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==",
"dev": true
},
"qs": {
"version": "6.5.2",
"resolved": false,
"resolved": "",
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
"dev": true
},
"query-string": {
"version": "6.8.2",
"resolved": false,
"resolved": "",
"integrity": "sha512-J3Qi8XZJXh93t2FiKyd/7Ec6GNifsjKXUsVFkSBj/kjLsDylWhnCz4NT1bkPcKotttPW+QbKGqqPH8OoI2pdqw==",
"dev": true,
"requires": {
@@ -20730,13 +20885,13 @@
},
"qw": {
"version": "1.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-77/cdA+a0FQwRCassYNBLMi5ltQ=",
"dev": true
},
"rc": {
"version": "1.2.8",
"resolved": false,
"resolved": "",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"dev": true,
"requires": {
@@ -20748,7 +20903,7 @@
},
"read": {
"version": "1.0.7",
"resolved": false,
"resolved": "",
"integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=",
"dev": true,
"requires": {
@@ -20757,7 +20912,7 @@
},
"read-cmd-shim": {
"version": "1.0.5",
"resolved": false,
"resolved": "",
"integrity": "sha512-v5yCqQ/7okKoZZkBQUAfTsQ3sVJtXdNfbPnI5cceppoxEVLYA3k+VtV2omkeo8MS94JCy4fSiUwlRBAwCVRPUA==",
"dev": true,
"requires": {
@@ -20766,7 +20921,7 @@
},
"read-installed": {
"version": "4.0.3",
"resolved": false,
"resolved": "",
"integrity": "sha1-/5uLZ/GH0eTCm5/rMfayI6zRkGc=",
"dev": true,
"requires": {
@@ -20781,7 +20936,7 @@
},
"read-package-json": {
"version": "2.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-dAiqGtVc/q5doFz6096CcnXhpYk0ZN8dEKVkGLU0CsASt8SrgF6SF7OTKAYubfvFhWaqofl+Y8HK19GR8jwW+A==",
"dev": true,
"requires": {
@@ -20794,7 +20949,7 @@
},
"read-package-tree": {
"version": "5.3.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-mLUDsD5JVtlZxjSlPPx1RETkNjjvQYuweKwNVt1Sn8kP5Jh44pvYuUHCp6xSVDZWbNxVxG5lyZJ921aJH61sTw==",
"dev": true,
"requires": {
@@ -20805,7 +20960,7 @@
},
"readable-stream": {
"version": "3.6.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"requires": {
@@ -20816,7 +20971,7 @@
},
"readdir-scoped-modules": {
"version": "1.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==",
"dev": true,
"requires": {
@@ -20828,7 +20983,7 @@
},
"registry-auth-token": {
"version": "3.4.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==",
"dev": true,
"requires": {
@@ -20838,7 +20993,7 @@
},
"registry-url": {
"version": "3.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=",
"dev": true,
"requires": {
@@ -20847,7 +21002,7 @@
},
"request": {
"version": "2.88.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
"dev": true,
"requires": {
@@ -20875,31 +21030,31 @@
},
"require-directory": {
"version": "2.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
"dev": true
},
"require-main-filename": {
"version": "2.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"dev": true
},
"resolve-from": {
"version": "4.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true
},
"retry": {
"version": "0.12.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=",
"dev": true
},
"rimraf": {
"version": "2.7.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"dev": true,
"requires": {
@@ -20908,7 +21063,7 @@
},
"run-queue": {
"version": "1.0.3",
"resolved": false,
"resolved": "",
"integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=",
"dev": true,
"requires": {
@@ -20917,7 +21072,7 @@
"dependencies": {
"aproba": {
"version": "1.2.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
"dev": true
}
@@ -20925,25 +21080,25 @@
},
"safe-buffer": {
"version": "5.1.2",
"resolved": false,
"resolved": "",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true
},
"safer-buffer": {
"version": "2.1.2",
"resolved": false,
"resolved": "",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true
},
"semver": {
"version": "5.7.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true
},
"semver-diff": {
"version": "2.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=",
"dev": true,
"requires": {
@@ -20952,13 +21107,13 @@
},
"set-blocking": {
"version": "2.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
"dev": true
},
"sha": {
"version": "3.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-DOYnM37cNsLNSGIG/zZWch5CKIRNoLdYUQTQlcgkRkoYIUwDYjqDyye16YcDZg/OPdcbUgTKMjc4SY6TB7ZAPw==",
"dev": true,
"requires": {
@@ -20967,7 +21122,7 @@
},
"shebang-command": {
"version": "1.2.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
"dev": true,
"requires": {
@@ -20976,31 +21131,31 @@
},
"shebang-regex": {
"version": "1.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
"dev": true
},
"signal-exit": {
"version": "3.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
"dev": true
},
"slide": {
"version": "1.1.6",
"resolved": false,
"resolved": "",
"integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=",
"dev": true
},
"smart-buffer": {
"version": "4.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw==",
"dev": true
},
"socks": {
"version": "2.3.3",
"resolved": false,
"resolved": "",
"integrity": "sha512-o5t52PCNtVdiOvzMry7wU4aOqYWL0PeCXRWBEiJow4/i/wr+wpsJQ9awEu1EonLIqsfGd5qSgDdxEOvCdmBEpA==",
"dev": true,
"requires": {
@@ -21010,7 +21165,7 @@
},
"socks-proxy-agent": {
"version": "4.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg==",
"dev": true,
"requires": {
@@ -21020,7 +21175,7 @@
"dependencies": {
"agent-base": {
"version": "4.2.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==",
"dev": true,
"requires": {
@@ -21031,13 +21186,13 @@
},
"sorted-object": {
"version": "2.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-fWMfS9OnmKJK8d/8+/6DM3pd9fw=",
"dev": true
},
"sorted-union-stream": {
"version": "2.1.3",
"resolved": false,
"resolved": "",
"integrity": "sha1-x3lMfgd4gAUv9xqNSi27Sppjisc=",
"dev": true,
"requires": {
@@ -21047,7 +21202,7 @@
"dependencies": {
"from2": {
"version": "1.3.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-iEE7qqX5pZfP3pIh2GmGzTwGHf0=",
"dev": true,
"requires": {
@@ -21057,13 +21212,13 @@
},
"isarray": {
"version": "0.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
"dev": true
},
"readable-stream": {
"version": "1.1.14",
"resolved": false,
"resolved": "",
"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
"dev": true,
"requires": {
@@ -21075,7 +21230,7 @@
},
"string_decoder": {
"version": "0.10.31",
"resolved": false,
"resolved": "",
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
"dev": true
}
@@ -21083,7 +21238,7 @@
},
"spdx-correct": {
"version": "3.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==",
"dev": true,
"requires": {
@@ -21093,13 +21248,13 @@
},
"spdx-exceptions": {
"version": "2.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==",
"dev": true
},
"spdx-expression-parse": {
"version": "3.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==",
"dev": true,
"requires": {
@@ -21109,19 +21264,19 @@
},
"spdx-license-ids": {
"version": "3.0.5",
"resolved": false,
"resolved": "",
"integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==",
"dev": true
},
"split-on-first": {
"version": "1.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==",
"dev": true
},
"sshpk": {
"version": "1.14.2",
"resolved": false,
"resolved": "",
"integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=",
"dev": true,
"requires": {
@@ -21138,7 +21293,7 @@
},
"ssri": {
"version": "6.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==",
"dev": true,
"requires": {
@@ -21147,7 +21302,7 @@
},
"stream-each": {
"version": "1.2.2",
"resolved": false,
"resolved": "",
"integrity": "sha512-mc1dbFhGBxvTM3bIWmAAINbqiuAk9TATcfIQC8P+/+HJefgaiTlMn2dHvkX8qlI12KeYKSQ1Ua9RrIqrn1VPoA==",
"dev": true,
"requires": {
@@ -21157,7 +21312,7 @@
},
"stream-iterate": {
"version": "1.2.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-K9fHcpbBcCpGSIuK1B95hl7s1OE=",
"dev": true,
"requires": {
@@ -21167,7 +21322,7 @@
"dependencies": {
"readable-stream": {
"version": "2.3.6",
"resolved": false,
"resolved": "",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"dev": true,
"requires": {
@@ -21182,7 +21337,7 @@
},
"string_decoder": {
"version": "1.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
@@ -21193,19 +21348,19 @@
},
"stream-shift": {
"version": "1.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=",
"dev": true
},
"strict-uri-encode": {
"version": "2.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY=",
"dev": true
},
"string-width": {
"version": "2.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
"dev": true,
"requires": {
@@ -21215,19 +21370,19 @@
"dependencies": {
"ansi-regex": {
"version": "3.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
"dev": true
},
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
"dev": true
},
"strip-ansi": {
"version": "4.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
"dev": true,
"requires": {
@@ -21238,7 +21393,7 @@
},
"string_decoder": {
"version": "1.3.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"dev": true,
"requires": {
@@ -21247,7 +21402,7 @@
"dependencies": {
"safe-buffer": {
"version": "5.2.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==",
"dev": true
}
@@ -21255,13 +21410,13 @@
},
"stringify-package": {
"version": "1.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-sa4DUQsYciMP1xhKWGuFM04fB0LG/9DlluZoSVywUMRNvzid6XucHK0/90xGxRoHrAaROrcHK1aPKaijCtSrhg==",
"dev": true
},
"strip-ansi": {
"version": "3.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"dev": true,
"requires": {
@@ -21270,19 +21425,19 @@
},
"strip-eof": {
"version": "1.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=",
"dev": true
},
"strip-json-comments": {
"version": "2.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
"dev": true
},
"supports-color": {
"version": "5.4.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==",
"dev": true,
"requires": {
@@ -21291,7 +21446,7 @@
},
"tar": {
"version": "4.4.13",
"resolved": false,
"resolved": "",
"integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==",
"dev": true,
"requires": {
@@ -21306,7 +21461,7 @@
"dependencies": {
"minipass": {
"version": "2.9.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
"dev": true,
"requires": {
@@ -21318,7 +21473,7 @@
},
"term-size": {
"version": "1.2.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=",
"dev": true,
"requires": {
@@ -21327,19 +21482,19 @@
},
"text-table": {
"version": "0.2.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
"dev": true
},
"through": {
"version": "2.3.8",
"resolved": false,
"resolved": "",
"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
"dev": true
},
"through2": {
"version": "2.0.3",
"resolved": false,
"resolved": "",
"integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=",
"dev": true,
"requires": {
@@ -21349,7 +21504,7 @@
"dependencies": {
"readable-stream": {
"version": "2.3.6",
"resolved": false,
"resolved": "",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"dev": true,
"requires": {
@@ -21364,7 +21519,7 @@
},
"string_decoder": {
"version": "1.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
@@ -21375,19 +21530,19 @@
},
"timed-out": {
"version": "4.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=",
"dev": true
},
"tiny-relative-date": {
"version": "1.3.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-MOQHpzllWxDCHHaDno30hhLfbouoYlOI8YlMNtvKe1zXbjEVhbcEovQxvZrPvtiYW630GQDoMMarCnjfyfHA+A==",
"dev": true
},
"tough-cookie": {
"version": "2.4.3",
"resolved": false,
"resolved": "",
"integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
"dev": true,
"requires": {
@@ -21397,7 +21552,7 @@
},
"tunnel-agent": {
"version": "0.6.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
"dev": true,
"requires": {
@@ -21406,32 +21561,32 @@
},
"tweetnacl": {
"version": "0.14.5",
"resolved": false,
"resolved": "",
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
"dev": true,
"optional": true
},
"typedarray": {
"version": "0.0.6",
"resolved": false,
"resolved": "",
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
"dev": true
},
"uid-number": {
"version": "0.0.6",
"resolved": false,
"resolved": "",
"integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=",
"dev": true
},
"umask": {
"version": "1.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-8pzr8B31F5ErtY/5xOUP3o4zMg0=",
"dev": true
},
"unique-filename": {
"version": "1.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==",
"dev": true,
"requires": {
@@ -21440,7 +21595,7 @@
},
"unique-slug": {
"version": "2.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-22Z258fMBimHj/GWCXx4hVrp9Ks=",
"dev": true,
"requires": {
@@ -21449,7 +21604,7 @@
},
"unique-string": {
"version": "1.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=",
"dev": true,
"requires": {
@@ -21458,19 +21613,19 @@
},
"unpipe": {
"version": "1.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=",
"dev": true
},
"unzip-response": {
"version": "2.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=",
"dev": true
},
"update-notifier": {
"version": "2.5.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==",
"dev": true,
"requires": {
@@ -21488,7 +21643,7 @@
},
"url-parse-lax": {
"version": "1.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=",
"dev": true,
"requires": {
@@ -21497,19 +21652,19 @@
},
"util-deprecate": {
"version": "1.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
"dev": true
},
"util-extend": {
"version": "1.0.3",
"resolved": false,
"resolved": "",
"integrity": "sha1-p8IW0mdUUWljeztu3GypEZ4v+T8=",
"dev": true
},
"util-promisify": {
"version": "2.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-PCI2R2xNMsX/PEcAKt18E7moKlM=",
"dev": true,
"requires": {
@@ -21518,13 +21673,13 @@
},
"uuid": {
"version": "3.3.3",
"resolved": false,
"resolved": "",
"integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==",
"dev": true
},
"validate-npm-package-license": {
"version": "3.0.4",
"resolved": false,
"resolved": "",
"integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
"dev": true,
"requires": {
@@ -21534,7 +21689,7 @@
},
"validate-npm-package-name": {
"version": "3.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-X6kS2B630MdK/BQN5zF/DKffQ34=",
"dev": true,
"requires": {
@@ -21543,7 +21698,7 @@
},
"verror": {
"version": "1.10.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
"dev": true,
"requires": {
@@ -21554,7 +21709,7 @@
},
"wcwidth": {
"version": "1.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=",
"dev": true,
"requires": {
@@ -21563,7 +21718,7 @@
},
"which": {
"version": "1.3.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
"dev": true,
"requires": {
@@ -21572,13 +21727,13 @@
},
"which-module": {
"version": "2.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
"dev": true
},
"wide-align": {
"version": "1.1.2",
"resolved": false,
"resolved": "",
"integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==",
"dev": true,
"requires": {
@@ -21587,7 +21742,7 @@
"dependencies": {
"string-width": {
"version": "1.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"dev": true,
"requires": {
@@ -21600,7 +21755,7 @@
},
"widest-line": {
"version": "2.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==",
"dev": true,
"requires": {
@@ -21609,7 +21764,7 @@
},
"worker-farm": {
"version": "1.7.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==",
"dev": true,
"requires": {
@@ -21618,7 +21773,7 @@
},
"wrap-ansi": {
"version": "5.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
"dev": true,
"requires": {
@@ -21629,19 +21784,19 @@
"dependencies": {
"ansi-regex": {
"version": "4.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"dev": true
},
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
"dev": true
},
"string-width": {
"version": "3.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
"dev": true,
"requires": {
@@ -21652,7 +21807,7 @@
},
"strip-ansi": {
"version": "5.2.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"dev": true,
"requires": {
@@ -21663,13 +21818,13 @@
},
"wrappy": {
"version": "1.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
},
"write-file-atomic": {
"version": "2.4.3",
"resolved": false,
"resolved": "",
"integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==",
"dev": true,
"requires": {
@@ -21680,31 +21835,31 @@
},
"xdg-basedir": {
"version": "3.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=",
"dev": true
},
"xtend": {
"version": "4.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=",
"dev": true
},
"y18n": {
"version": "4.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
"dev": true
},
"yallist": {
"version": "3.0.3",
"resolved": false,
"resolved": "",
"integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==",
"dev": true
},
"yargs": {
"version": "14.2.3",
"resolved": false,
"resolved": "",
"integrity": "sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==",
"dev": true,
"requires": {
@@ -21723,13 +21878,13 @@
"dependencies": {
"ansi-regex": {
"version": "4.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"dev": true
},
"find-up": {
"version": "3.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
"dev": true,
"requires": {
@@ -21738,13 +21893,13 @@
},
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
"dev": true
},
"locate-path": {
"version": "3.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
"dev": true,
"requires": {
@@ -21754,7 +21909,7 @@
},
"p-limit": {
"version": "2.3.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"dev": true,
"requires": {
@@ -21763,7 +21918,7 @@
},
"p-locate": {
"version": "3.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
"dev": true,
"requires": {
@@ -21772,13 +21927,13 @@
},
"p-try": {
"version": "2.2.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"dev": true
},
"string-width": {
"version": "3.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
"dev": true,
"requires": {
@@ -21789,7 +21944,7 @@
},
"strip-ansi": {
"version": "5.2.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"dev": true,
"requires": {
@@ -21800,7 +21955,7 @@
},
"yargs-parser": {
"version": "15.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==",
"dev": true,
"requires": {
@@ -21810,7 +21965,7 @@
"dependencies": {
"camelcase": {
"version": "5.3.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"dev": true
}
@@ -23711,9 +23866,9 @@
}
},
"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",
@@ -23895,37 +24050,41 @@
}
},
"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.1",
"resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.6.1.tgz",
"integrity": "sha512-ojEPQ6OtyIMdLg0Smofk+85PKN6MLKQX3bU0Vwmok/4yNa8DQ2vCGhO2IgHJvT+ERQZ4X+gAQcdn6msAHSwLBg==",
"requires": {
"@babel/runtime": "^7.13.8",
"@babel/runtime": "^7.14.0",
"@restart/context": "^2.1.4",
"@restart/hooks": "^0.3.26",
"@types/classnames": "^2.2.10",
"@types/invariant": "^2.2.33",
"@types/prop-types": "^15.7.3",
"@types/react": ">=16.9.35",
"@types/react": ">=16.14.8",
"@types/react-transition-group": "^4.4.1",
"@types/warning": "^3.0.0",
"classnames": "^2.2.6",
"dom-helpers": "^5.1.2",
"classnames": "^2.3.1",
"dom-helpers": "^5.2.1",
"invariant": "^2.2.4",
"prop-types": "^15.7.2",
"prop-types-extra": "^1.1.0",
"react-overlays": "^5.0.0",
"react-overlays": "^5.0.1",
"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==",
"version": "7.14.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.6.tgz",
"integrity": "sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
},
"classnames": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
"integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
}
}
},
@@ -23938,9 +24097,9 @@
},
"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==",
"version": "7.14.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.6.tgz",
"integrity": "sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
@@ -24368,12 +24527,12 @@
"integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
},
"react-focus-lock": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.5.0.tgz",
"integrity": "sha512-XLxj6uTXgz0US8TmqNU2jMfnXwZG0mH2r/afQqvPEaX6nyEll5LHVcEXk2XDUQ34RVeLPkO/xK5x6c/qiuSq/A==",
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.5.1.tgz",
"integrity": "sha512-gOToRZKVEymGEjFaTRUKgJsdYQrNosoiK7yZnXnnd8bYew4vMzk3Rxb0Q4nyrGwsFuUmgQiSAulQirA0J+v4hA==",
"requires": {
"@babel/runtime": "^7.0.0",
"focus-lock": "^0.8.1",
"focus-lock": "^0.9.1",
"prop-types": "^15.6.2",
"react-clientside-effect": "^1.2.2",
"use-callback-ref": "^1.2.1",
@@ -24431,9 +24590,9 @@
},
"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==",
"version": "7.14.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.6.tgz",
"integrity": "sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
@@ -24469,9 +24628,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",
@@ -24610,9 +24769,9 @@
}
},
"react-transition-group": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz",
"integrity": "sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==",
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz",
"integrity": "sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==",
"requires": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
@@ -24620,6 +24779,12 @@
"prop-types": "^15.6.2"
}
},
"reactifex": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/reactifex/-/reactifex-1.1.1.tgz",
"integrity": "sha512-HH2N/b5tRxh7ypIgCRsiBl/CTxRkTEPf9DhIstaM6hne4WiwM5/bBbWuvVlRZc/i3FdqZED3pZ//6n4mtxma4w==",
"dev": true
},
"read-pkg": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",
@@ -25190,6 +25355,11 @@
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
"dev": true
},
"reselect": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz",
"integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA=="
},
"resolve": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz",
@@ -28614,8 +28784,7 @@
"tslib": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz",
"integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==",
"dev": true
"integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ=="
},
"tty-browserify": {
"version": "0.0.0",
@@ -29061,20 +29230,16 @@
}
},
"util": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
"integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
"dev": true,
"version": "0.12.3",
"resolved": "https://registry.npmjs.org/util/-/util-0.12.3.tgz",
"integrity": "sha512-I8XkoQwE+fPQEhy9v012V+TSdH2kp9ts29i20TaaDUXsg7x/onePbhFJUExBfv/2ay1ZOp/Vsm3nDlmnFGSAog==",
"requires": {
"inherits": "2.0.1"
},
"dependencies": {
"inherits": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
"integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=",
"dev": true
}
"inherits": "^2.0.3",
"is-arguments": "^1.0.4",
"is-generator-function": "^1.0.7",
"is-typed-array": "^1.1.3",
"safe-buffer": "^5.1.2",
"which-typed-array": "^1.1.2"
}
},
"util-deprecate": {
@@ -30064,6 +30229,152 @@
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
},
"which-typed-array": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.4.tgz",
"integrity": "sha512-49E0SpUe90cjpoc7BOJwyPHRqSAd12c10Qm2amdEZrJPCY2NDxaW01zHITrem+rnETY3dwrbH3UUrUwagfCYDA==",
"requires": {
"available-typed-arrays": "^1.0.2",
"call-bind": "^1.0.0",
"es-abstract": "^1.18.0-next.1",
"foreach": "^2.0.5",
"function-bind": "^1.1.1",
"has-symbols": "^1.0.1",
"is-typed-array": "^1.1.3"
},
"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"
},
"dependencies": {
"call-bind": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
"requires": {
"function-bind": "^1.1.1",
"get-intrinsic": "^1.0.2"
}
},
"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=="
}
}
},
"get-intrinsic": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
"integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
"requires": {
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-symbols": "^1.0.1"
}
},
"is-callable": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz",
"integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ=="
},
"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==",
"requires": {
"call-bind": "^1.0.2",
"has-symbols": "^1.0.1"
},
"dependencies": {
"call-bind": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
"requires": {
"function-bind": "^1.1.1",
"get-intrinsic": "^1.0.2"
}
}
}
},
"object-inspect": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.2.tgz",
"integrity": "sha512-gz58rdPpadwztRrPjZE9DZLOABUpTGdcANUgOwBFO1C+HZZhePoP83M65WGDmbpwFYJSWqavbl4SgDn4k8RYTA=="
},
"object.assign": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
"integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
"requires": {
"call-bind": "^1.0.0",
"define-properties": "^1.1.3",
"has-symbols": "^1.0.1",
"object-keys": "^1.1.1"
}
},
"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"
},
"dependencies": {
"call-bind": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
"requires": {
"function-bind": "^1.1.1",
"get-intrinsic": "^1.0.2"
}
}
}
},
"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"
},
"dependencies": {
"call-bind": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
"requires": {
"function-bind": "^1.1.1",
"get-intrinsic": "^1.0.2"
}
}
}
}
}
},
"wide-align": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "@edx/frontend-app-gradebook",
"version": "1.4.26",
"version": "1.4.46",
"description": "edx editable gradebook-ui to manipulate grade overrides on subsections",
"repository": {
"type": "git",
@@ -10,6 +10,7 @@
"build": "fedx-scripts webpack",
"coveralls": "cat ./coverage/lcov.info | coveralls",
"is-es5": "es-check es5 ./dist/*.js",
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
"lint": "fedx-scripts eslint --ext .jsx,.js src/",
"lint-fix": "fedx-scripts eslint --fix --ext .jsx,.js src/",
"prepush": "npm run lint",
@@ -28,13 +29,14 @@
"dependencies": {
"@edx/brand": "npm:@edx/brand-edx.org@^1.3.2",
"@edx/frontend-component-footer": "10.1.1",
"@edx/frontend-platform": "1.8.1",
"@edx/paragon": "14.6.1",
"@edx/frontend-platform": "1.9.5",
"@edx/paragon": "14.16.4",
"@fortawesome/fontawesome-svg-core": "^1.2.25",
"@fortawesome/free-brands-svg-icons": "^5.11.2",
"@fortawesome/free-solid-svg-icons": "^5.11.2",
"@fortawesome/react-fontawesome": "^0.1.5",
"@redux-beacon/segment": "^1.0.0",
"@reduxjs/toolkit": "^1.5.1",
"classnames": "^2.2.6",
"core-js": "3.6.5",
"email-prop-type": "^1.1.7",
@@ -58,6 +60,7 @@
"redux-logger": "3.0.6",
"redux-thunk": "2.3.0",
"regenerator-runtime": "^0.13.7",
"util": "^0.12.3",
"whatwg-fetch": "^2.0.4"
},
"devDependencies": {
@@ -73,6 +76,7 @@
"jest": "24.9.0",
"react-dev-utils": "^5.0.3",
"react-test-renderer": "^16.10.1",
"reactifex": "1.1.1",
"redux-mock-store": "^1.5.3",
"semantic-release": "^17.2.3",
"travis-deploy-once": "^5.0.11"

36
src/App.jsx Executable file
View File

@@ -0,0 +1,36 @@
import React from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import { Provider } from 'react-redux';
import { IntlProvider } from 'react-intl';
import Footer from '@edx/frontend-component-footer';
import { routePath } from 'data/constants/app';
import store from 'data/store';
import GradebookPage from 'containers/GradebookPage';
import EdxHeader from 'components/EdxHeader';
import './App.scss';
const App = () => (
<IntlProvider locale="en">
<Provider store={store}>
<Router>
<div>
<EdxHeader />
<main>
<Switch>
<Route
exact
path={routePath}
component={GradebookPage}
/>
</Switch>
</main>
<Footer logo={process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG} />
</div>
</Router>
</Provider>
</IntlProvider>
);
export default App;

View File

@@ -11,5 +11,6 @@ $input-focus-box-shadow: $input-box-shadow; // hack to get upgrade to paragon 4.
@import "~@edx/frontend-component-footer/dist/_footer";
@import "./components/Gradebook/gradebook";
@import "./components/Drawer/Drawer";
@import "./components/GradesTab/GradesTab";
@import "./components/WithSidebar/WithSidebar";
@import "./components/GradebookFilters/GradebookFilters";

86
src/App.test.jsx Normal file
View File

@@ -0,0 +1,86 @@
import React from 'react';
import { shallow } from 'enzyme';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import { Provider } from 'react-redux';
import { IntlProvider } from 'react-intl';
import Footer from '@edx/frontend-component-footer';
import { routePath } from 'data/constants/app';
import store from 'data/store';
import GradebookPage from 'containers/GradebookPage';
import EdxHeader from 'components/EdxHeader';
import App from './App';
jest.mock('react-router-dom', () => ({
BrowserRouter: () => 'BrowserRouter',
Route: () => 'Route',
Switch: () => 'Switch',
}));
jest.mock('react-redux', () => ({
Provider: () => 'Provider',
}));
jest.mock('react-intl', () => ({
IntlProvider: () => 'IntlProvider',
}));
jest.mock('data/constants/app', () => ({
routePath: '/:courseId',
}));
jest.mock('@edx/frontend-component-footer', () => 'Footer');
jest.mock('data/store', () => 'testStore');
jest.mock('containers/GradebookPage', () => 'GradebookPage');
jest.mock('components/EdxHeader', () => 'EdxHeader');
const logo = 'fakeLogo.png';
let el;
let router;
describe('App router component', () => {
test('snapshot', () => {
expect(shallow(<App />)).toMatchSnapshot();
});
describe('component', () => {
beforeEach(() => {
process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG = logo;
el = shallow(<App />);
router = el.childAt(0).childAt(0);
});
describe('IntlProvider', () => {
test('outer-wrapper component', () => {
expect(el.type()).toBe(IntlProvider);
});
test('"en" locale', () => {
expect(el.props().locale).toEqual('en');
});
});
describe('Provider, inside IntlProvider', () => {
test('first child, passed the redux store props', () => {
expect(el.childAt(0).type()).toBe(Provider);
expect(el.childAt(0).props().store).toEqual(store);
});
});
describe('Router', () => {
test('first child of Provider', () => {
expect(router.type()).toBe(Router);
});
test('EdxHeader is above/outside-of the routing', () => {
expect(router.childAt(0).childAt(0).type()).toBe(EdxHeader);
expect(router.childAt(0).childAt(1).type()).toBe('main');
});
test('Routing - GradebookPage is only route', () => {
expect(router.find('main')).toEqual(shallow(
<main>
<Switch>
<Route exact path={routePath} component={GradebookPage} />
</Switch>
</main>,
));
});
});
test('Footer logo drawn from env variable', () => {
expect(router.find(Footer).props().logo).toEqual(logo);
});
});
});

View File

@@ -0,0 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`App router component snapshot 1`] = `
<IntlProvider
locale="en"
>
<Provider
store="testStore"
>
<BrowserRouter>
<div>
<EdxHeader />
<main>
<Switch>
<Route
component="GradebookPage"
exact={true}
path="/:courseId"
/>
</Switch>
</main>
<Footer />
</div>
</BrowserRouter>
</Provider>
</IntlProvider>
`;

View File

@@ -0,0 +1,54 @@
/* eslint-disable react/sort-comp, react/button-has-type */
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { connect } from 'react-redux';
import { Alert } from '@edx/paragon';
import selectors from 'data/selectors';
import messages from './messages';
/**
* <BulkManagementAlerts />
* Alerts to display at the top of the BulkManagement tab
*/
export const BulkManagementAlerts = ({
bulkImportError,
uploadSuccess,
}) => (
<>
<Alert
variant="danger"
show={!!bulkImportError}
dismissible={false}
>
{bulkImportError}
</Alert>
<Alert
variant="success"
show={uploadSuccess}
dismissible={false}
>
<FormattedMessage {...messages.successDialog} />
</Alert>
</>
);
BulkManagementAlerts.defaultProps = {
bulkImportError: '',
uploadSuccess: false,
};
BulkManagementAlerts.propTypes = {
// redux
bulkImportError: PropTypes.string,
uploadSuccess: PropTypes.bool,
};
export const mapStateToProps = (state) => ({
bulkImportError: selectors.grades.bulkImportError(state),
uploadSuccess: selectors.grades.uploadSuccess(state),
});
export default connect(mapStateToProps)(BulkManagementAlerts);

View File

@@ -0,0 +1,89 @@
import React from 'react';
import { shallow } from 'enzyme';
import { Alert } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import selectors from 'data/selectors';
import messages from './messages';
import { BulkManagementAlerts, mapStateToProps } from './BulkManagementAlerts';
jest.mock('@edx/frontend-platform/i18n', () => ({
defineMessages: m => m,
FormattedMessage: () => 'FormattedMessage',
}));
jest.mock('@edx/paragon', () => ({
Alert: () => 'Alert',
}));
jest.mock('data/selectors', () => ({
__esModule: true,
default: {
grades: {
bulkImportError: (state) => ({ bulkImportError: state }),
uploadSuccess: (state) => ({ uploadSuccess: state }),
},
},
}));
const errorMessage = 'Oh noooooo';
describe('BulkManagementAlerts', () => {
describe('component', () => {
let el;
describe('no errer, no upload success', () => {
beforeEach(() => {
el = shallow(<BulkManagementAlerts />);
});
test('snapshot - bulkImportError closed, success closed', () => {
expect(el).toMatchSnapshot();
});
test('closed danger alert', () => {
expect(el.childAt(0).is(Alert)).toEqual(true);
expect(el.childAt(0).props().show).toEqual(false);
expect(el.childAt(0).props().variant).toEqual('danger');
});
test('closed success alert', () => {
expect(el.childAt(1).is(Alert)).toEqual(true);
expect(el.childAt(1).props().show).toEqual(false);
expect(el.childAt(1).props().variant).toEqual('success');
});
});
describe('no errer, no upload success', () => {
beforeEach(() => {
el = shallow(<BulkManagementAlerts uploadSuccess bulkImportError={errorMessage} />);
});
const assertions = [
'danger alert open with bulkImportError',
'success alert open with messages.successDialog',
];
test(`snapshot - ${assertions.join(', ')}`, () => {
expect(el).toMatchSnapshot();
});
test('open danger alert with bulkImportError content', () => {
expect(el.childAt(0).is(Alert)).toEqual(true);
expect(el.childAt(0).children().text()).toEqual(errorMessage);
expect(el.childAt(0).props().show).toEqual(true);
});
test('open success alert with messages.successDialog content', () => {
expect(el.childAt(1).is(Alert)).toEqual(true);
expect(el.childAt(1).children().getElement()).toEqual(
<FormattedMessage {...messages.successDialog} />,
);
expect(el.childAt(1).props().show).toEqual(true);
});
});
});
describe('mapStateToProps', () => {
let mapped;
const testState = { a: 'puppy', named: 'Ember' };
beforeEach(() => {
mapped = mapStateToProps(testState);
});
test('bulkImportError from grades.bulkImportError', () => {
expect(mapped.bulkImportError).toEqual(selectors.grades.bulkImportError(testState));
});
test('uploadSuccess from grades.uploadSuccess', () => {
expect(mapped.uploadSuccess).toEqual(selectors.grades.uploadSuccess(testState));
});
});
});

View File

@@ -0,0 +1,95 @@
/* eslint-disable react/button-has-type, import/no-named-as-default */
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import {
Button,
Form,
FormControl,
FormGroup,
} from '@edx/paragon';
import selectors from 'data/selectors';
import thunkActions from 'data/thunkActions';
import messages from './messages';
/**
* <FileUploadForm />
* File-type input wrapped with hidden control such that when a valid file is
* added, it is automattically uploaded.
*/
export class FileUploadForm extends React.Component {
constructor(props) {
super(props);
this.fileInputRef = React.createRef();
this.handleClickImportGrades = this.handleClickImportGrades.bind(this);
this.handleFileInputChange = this.handleFileInputChange.bind(this);
}
get fileInput() {
return this.fileInputRef.current;
}
get formData() {
const data = new FormData();
data.append('csv', this.fileInput.files[0]);
return data;
}
get hasFile() {
return this.fileInput && this.fileInput.files[0];
}
handleClickImportGrades() {
if (this.fileInput) { this.fileInput.click(); }
}
handleFileInputChange() {
return this.hasFile && (
this.props.submitFileUploadFormData(this.formData).then(
() => { this.fileInput.value = null; },
)
);
}
render() {
const { gradeExportUrl } = this.props;
return (
<>
<Form action={gradeExportUrl} method="post">
<FormGroup controlId="csv">
<FormControl
className="d-none"
type="file"
label={<FormattedMessage {...messages.csvUploadLabel} />}
onChange={this.handleFileInputChange}
ref={this.fileInputRef}
/>
</FormGroup>
</Form>
<Button variant="primary" onClick={this.handleClickImportGrades}>
<FormattedMessage {...messages.importBtnText} />
</Button>
</>
);
}
}
FileUploadForm.propTypes = {
// redux
gradeExportUrl: PropTypes.string.isRequired,
submitFileUploadFormData: PropTypes.func.isRequired,
};
export const mapStateToProps = (state) => ({
gradeExportUrl: selectors.root.gradeExportUrl(state),
});
export const mapDispatchToProps = {
submitFileUploadFormData: thunkActions.grades.submitFileUploadFormData,
};
export default connect(mapStateToProps, mapDispatchToProps)(FileUploadForm);

View File

@@ -0,0 +1,214 @@
/* eslint-disable import/no-named-as-default */
import React from 'react';
import { shallow } from 'enzyme';
import TestRenderer from 'react-test-renderer';
import {
Button,
Form,
FormControl,
FormGroup,
} from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import selectors from 'data/selectors';
import thunkActions from 'data/thunkActions';
import { FileUploadForm, mapStateToProps, mapDispatchToProps } from './FileUploadForm';
import messages from './messages';
jest.mock('@edx/frontend-platform/i18n', () => ({
defineMessages: m => m,
FormattedMessage: () => 'FormattedMessage',
}));
jest.mock('data/selectors', () => ({
__esModule: true,
default: {
grades: {
bulkImportError: jest.fn(state => ({ bulkImportError: state })),
},
root: {
gradeExportUrl: jest.fn(state => ({ gradeExportUrl: state })),
},
},
}));
jest.mock('data/thunkActions', () => ({
__esModule: true,
default: {
grades: { submitFileUploadFormData: jest.fn() },
},
}));
jest.mock('./BulkManagementAlerts', () => 'BulkManagementAlerts');
jest.mock('./ResultsSummary', () => 'ResultsSummary');
const mockRef = { click: jest.fn(), files: [] };
describe('FileUploadForm', () => {
beforeEach(() => {
mockRef.click.mockClear();
});
describe('component', () => {
let props;
let el;
let inst;
beforeEach(() => {
props = {
gradeExportUrl: 'fakeUrl',
submitFileUploadFormData: jest.fn(),
};
});
describe('snapshot', () => {
const snapshotSegments = [
'export form w/ alerts and file input',
'import btn',
];
test(`snapshot - loads ${snapshotSegments.join(', ')}`, () => {
jest.mock('@edx/paragon', () => ({
Button: () => 'Button',
Form: () => 'Form',
FormControl: () => 'FormControl',
FormGroup: () => 'FormGroup',
}));
el = shallow(<FileUploadForm {...props} />);
el.instance().handleFileInputChange = jest.fn().mockName('this.handleFileInputChange');
el.instance().fileInputRef = jest.fn().mockName('this.fileInputRef');
el.instance().handleClickImportGrades = jest.fn().mockName('this.handleClickImportGrades');
el.instance().formatHistoryRow = jest.fn(entry => entry.originalFilename);
expect(el.instance().render()).toMatchSnapshot();
});
});
describe('render', () => {
beforeEach(() => {
el = TestRenderer.create(
<FileUploadForm {...props} />,
{ createNodeMock: () => mockRef },
);
inst = el.root;
});
describe('alert form', () => {
let form;
beforeEach(() => {
form = inst.findByType(Form);
});
test('post action points to gradeExportUrl', () => {
expect(form.props.action).toEqual(props.gradeExportUrl);
expect(form.props.method).toEqual('post');
});
describe('file input', () => {
let formGroup;
beforeEach(() => {
formGroup = inst.findByType(FormGroup);
});
test('group with controlId="csv"', () => {
expect(formGroup.props.controlId).toEqual('csv');
});
test('file control with onChange from handleFileInputChange', () => {
const control = inst.findByType(FormControl);
expect(
control.props.onChange,
).toEqual(el.getInstance().handleFileInputChange);
});
test('fileInputRef points to control', () => {
expect(
// eslint-disable-next-line no-underscore-dangle
inst.findByType(FormControl)._fiber.ref,
).toEqual(el.getInstance().fileInputRef);
});
});
});
describe('import button', () => {
let btn;
beforeEach(() => {
btn = inst.findByType(Button);
});
test('handleClickImportGrade on click', () => {
expect(btn.props.onClick).toEqual(el.getInstance().handleClickImportGrades);
});
test('text from messages.importBtn', () => {
const messageEl = btn.findByType(FormattedMessage);
expect(messageEl.props).toEqual(messages.importBtnText);
});
});
});
describe('fileInput helper', () => {
test('links to fileInputRef.current', () => {
el = TestRenderer.create(
<FileUploadForm {...props} />,
{ createNodeMock: () => mockRef },
);
expect(el.getInstance().fileInput).not.toEqual(undefined);
expect(el.getInstance().fileInput).toEqual(el.getInstance().fileInputRef.current);
});
});
describe('behavior', () => {
let fileInput;
beforeEach(() => {
el = TestRenderer.create(
<FileUploadForm {...props} />,
{ createNodeMock: () => mockRef },
);
fileInput = jest.spyOn(el.getInstance(), 'fileInput', 'get');
});
describe('handleFileInputChange', () => {
it('does nothing (does not fail) if fileInput has not loaded', () => {
fileInput.mockReturnValue(null);
el.getInstance().handleClickImportGrades();
expect(mockRef.click).not.toHaveBeenCalled();
});
it('calls fileInput.click if is loaded', () => {
el.getInstance().handleClickImportGrades();
expect(mockRef.click).toHaveBeenCalled();
});
});
describe('handleClickImportGrades', () => {
it('does nothing if file input has not loaded with files', () => {
fileInput.mockReturnValue(null);
el.getInstance().handleFileInputChange();
expect(props.submitFileUploadFormData).not.toHaveBeenCalled();
fileInput.mockReturnValue({ files: [] });
el.getInstance().handleFileInputChange();
expect(props.submitFileUploadFormData).not.toHaveBeenCalled();
});
it('calls submitFileUploadFormData and then clears fileInput if has files', () => {
fileInput.mockReturnValue({ files: ['some', 'files'], value: 'a value' });
const formData = { fake: 'form data' };
jest.spyOn(el.getInstance(), 'formData', 'get').mockReturnValue(formData);
const submit = jest.fn(() => ({ then: (thenCB) => { thenCB(); } }));
el.update(<FileUploadForm {...props} submitFileUploadFormData={submit} />);
el.getInstance().handleFileInputChange();
expect(submit).toHaveBeenCalledWith(formData);
expect(el.getInstance().fileInput.value).toEqual(null);
});
});
describe('formData', () => {
test('returns FormData object with csv value from fileInput.files[0]', () => {
const file = { a: 'fake file' };
const value = 'aValue';
fileInput.mockReturnValue({ files: [file], value });
const expected = new FormData();
expected.append('csv', file);
expect([...el.getInstance().formData.entries()]).toEqual([...expected.entries()]);
});
});
});
});
describe('mapStateToProps', () => {
const testState = { a: 'simple', test: 'state' };
let mapped;
beforeEach(() => {
mapped = mapStateToProps(testState);
});
test('gradeExportUrl from root.gradeExportUrl', () => {
expect(mapped.gradeExportUrl).toEqual(selectors.root.gradeExportUrl(testState));
});
});
describe('mapDispatchToProps', () => {
test('submitFileUploadFormData from thunkActions.grades', () => {
expect(
mapDispatchToProps.submitFileUploadFormData,
).toEqual(thunkActions.grades.submitFileUploadFormData);
});
});
});

View File

@@ -0,0 +1,70 @@
/* eslint-disable react/button-has-type, import/no-named-as-default */
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Table } from '@edx/paragon';
import { bulkManagementColumns } from 'data/constants/app';
import selectors from 'data/selectors';
import ResultsSummary from './ResultsSummary';
import messages from './messages';
export const mapHistoryRows = ({
resultsSummary,
originalFilename,
user,
...rest
}) => ({
resultsSummary: (<ResultsSummary {...resultsSummary} />),
filename: (<span className="wrap-text-in-cell">{originalFilename}</span>),
user: (<span className="wrap-text-in-cell">{user}</span>),
...rest,
});
/**
* <HistoryTable />
* Table with history of bulk management uploads, including a results summary which
* displays total, skipped, and failed uploads
*/
export const HistoryTable = ({
bulkManagementHistory,
}) => (
<>
<p>
<FormattedMessage {...messages.hint1} />
<br />
<FormattedMessage {...messages.hint2} />
</p>
<Table
data={bulkManagementHistory.map(mapHistoryRows)}
hasFixedColumnWidths
columns={bulkManagementColumns}
className="table-striped"
/>
</>
);
HistoryTable.defaultProps = {
bulkManagementHistory: [],
};
HistoryTable.propTypes = {
// redux
bulkManagementHistory: PropTypes.arrayOf(PropTypes.shape({
originalFilename: PropTypes.string.isRequired,
user: PropTypes.string.isRequired,
timeUploaded: PropTypes.string.isRequired,
resultsSummary: PropTypes.shape({
rowId: PropTypes.number.isRequired,
text: PropTypes.string.isRequired,
}),
})),
};
export const mapStateToProps = (state) => ({
bulkManagementHistory: selectors.grades.bulkManagementHistoryEntries(state),
});
export default connect(mapStateToProps)(HistoryTable);

View File

@@ -0,0 +1,121 @@
/* eslint-disable import/no-named-as-default */
import React from 'react';
import { shallow } from 'enzyme';
import { Table } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import selectors from 'data/selectors';
import { bulkManagementColumns } from 'data/constants/app';
import ResultsSummary from './ResultsSummary';
import { HistoryTable, mapStateToProps } from './HistoryTable';
import messages from './messages';
jest.mock('@edx/frontend-platform/i18n', () => ({
defineMessages: m => m,
FormattedMessage: () => 'FormattedMessage',
}));
jest.mock('@edx/paragon', () => ({
Table: () => 'Table',
}));
jest.mock('data/selectors', () => ({
__esModule: true,
default: {
grades: {
bulkManagementHistoryEntries: jest.fn(state => ({ historyEntries: state })),
},
},
}));
jest.mock('./ResultsSummary', () => 'ResultsSummary');
describe('HistoryTable', () => {
describe('component', () => {
const entry1 = {
originalFilename: 'blue.png',
user: 'Eifel',
timeUploaded: '65',
resultsSummary: {
rowId: 12,
courseId: 'Da Bu Dee',
text: 'Da ba daa',
},
};
const entry2 = {
originalFilename: 'allStar.jpg',
user: 'Smashmouth',
timeUploaded: '2000s?',
resultsSummary: {
courseId: 'rockstar',
rowId: 2,
text: 'all that glitters is gold',
},
};
const props = {
bulkManagementHistory: [entry1, entry2],
};
let el;
describe('snapshot', () => {
beforeEach(() => {
el = shallow(<HistoryTable {...props} />);
});
const snapshotSegments = [
'hints display',
'formatted table',
];
test(`snapshot - loads ${snapshotSegments.join(', ')}`, () => {
expect(el).toMatchSnapshot();
});
test('hints with break in between', () => {
const hints = el.find('p');
expect(hints.childAt(0).getElement()).toEqual(<FormattedMessage {...messages.hint1} />);
expect(hints.childAt(1).is('br')).toEqual(true);
expect(hints.childAt(2).getElement()).toEqual(<FormattedMessage {...messages.hint2} />);
});
describe('history table', () => {
let table;
beforeEach(() => {
table = el.find(Table);
});
describe('data (from bulkManagementHistory.map(this.formatHistoryRow)', () => {
const fieldAssertions = [
'maps resultsSummay to ResultsSummary',
'wraps filename and user',
'forwards the rest',
];
test(`snapshot: ${fieldAssertions.join(', ')}`, () => {
expect(table.props().data).toMatchSnapshot();
});
test(fieldAssertions.join(', '), () => {
const rows = table.props().data;
expect(rows[0].resultsSummary).toEqual(<ResultsSummary {...entry1.resultsSummary} />);
expect(rows[0].user).toEqual(<span className="wrap-text-in-cell">{entry1.user}</span>);
expect(
rows[0].filename,
).toEqual(<span className="wrap-text-in-cell">{entry1.originalFilename}</span>);
expect(rows[1].resultsSummary).toEqual(<ResultsSummary {...entry2.resultsSummary} />);
expect(rows[1].user).toEqual(<span className="wrap-text-in-cell">{entry2.user}</span>);
expect(
rows[1].filename,
).toEqual(<span className="wrap-text-in-cell">{entry2.originalFilename}</span>);
});
});
test('columns from bulkManagementColumns', () => {
expect(table.props().columns).toEqual(bulkManagementColumns);
});
});
});
});
describe('mapStateToProps', () => {
const testState = { a: 'simple', test: 'state' };
let mapped;
beforeEach(() => {
mapped = mapStateToProps(testState);
});
test('bulkManagementHistory from grades.bulkManagementHistoryEntries', () => {
expect(
mapped.bulkManagementHistory,
).toEqual(selectors.grades.bulkManagementHistoryEntries(testState));
});
});
});

View File

@@ -0,0 +1,38 @@
/* eslint-disable react/sort-comp, react/button-has-type */
import React from 'react';
import PropTypes from 'prop-types';
import { Hyperlink, Icon } from '@edx/paragon';
import { Download } from '@edx/paragon/icons';
import lms from 'data/services/lms';
/**
* <ResultsSummary {...{ courseId, rowId, text }} />
* displays a result summary cell for a single bulk management upgrade history entry.
* @param {string} courseId - course identifier
* @param {number} rowId - row/error identifier
* @param {string} text - summary string
*/
const ResultsSummary = ({
rowId,
text,
}) => (
<Hyperlink
href={lms.urls.bulkGradesUrlByRow(rowId)}
destination="www.edx.org"
target="_blank"
rel="noopener noreferrer"
showLaunchIcon={false}
>
<Icon src={Download} className="d-inline-block" />
{text}
</Hyperlink>
);
ResultsSummary.propTypes = {
rowId: PropTypes.number.isRequired,
text: PropTypes.string.isRequired,
};
export default ResultsSummary;

View File

@@ -0,0 +1,53 @@
import React from 'react';
import { shallow } from 'enzyme';
import { Icon } from '@edx/paragon';
import { Download } from '@edx/paragon/icons';
import lms from 'data/services/lms';
import ResultsSummary from './ResultsSummary';
jest.mock('@edx/paragon', () => ({
Hyperlink: () => 'Hyperlink',
Icon: () => 'Icon',
}));
jest.mock('@edx/paragon/icons', () => ({
Download: 'DownloadIcon',
}));
jest.mock('data/services/lms', () => ({
urls: {
bulkGradesUrlByRow: jest.fn((rowId) => ({ url: { rowId } })),
},
}));
describe('ResultsSummary component', () => {
const props = {
rowId: 42,
text: 'texty',
};
let el;
const assertions = [
'safe hyperlink with bulkGradesUrl with course and row id',
'download icon',
'results text',
];
beforeEach(() => {
el = shallow(<ResultsSummary {...props} />);
});
test(`snapshot - ${assertions.join(', ')}`, () => {
expect(el).toMatchSnapshot();
});
test('Hyperlink has target="_blank" and rel="noopener noreferrer"', () => {
expect(el.props().target).toEqual('_blank');
expect(el.props().rel).toEqual('noopener noreferrer');
});
test('Hyperlink has href to bulkGradesUrl', () => {
expect(el.props().href).toEqual(lms.urls.bulkGradesUrlByRow(props.rowId));
});
test('displays Download Icon and text', () => {
const icon = el.childAt(0);
expect(icon.is(Icon)).toEqual(true);
expect(icon.props().src).toEqual(Download);
expect(el.childAt(1).text()).toEqual(props.text);
});
});

View File

@@ -0,0 +1,45 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BulkManagementAlerts component no errer, no upload success snapshot - bulkImportError closed, success closed 1`] = `
<Fragment>
<Alert
dismissible={false}
show={false}
variant="danger"
/>
<Alert
dismissible={false}
show={false}
variant="success"
>
<FormattedMessage
defaultMessage="CSV processing. File uploads may take several minutes to complete."
description="Success Dialog message in BulkManagement Tab File Upload Form"
id="gradebook.BulkManagementTab.successDialog"
/>
</Alert>
</Fragment>
`;
exports[`BulkManagementAlerts component no errer, no upload success snapshot - danger alert open with bulkImportError, success alert open with messages.successDialog 1`] = `
<Fragment>
<Alert
dismissible={false}
show={true}
variant="danger"
>
Oh noooooo
</Alert>
<Alert
dismissible={false}
show={true}
variant="success"
>
<FormattedMessage
defaultMessage="CSV processing. File uploads may take several minutes to complete."
description="Success Dialog message in BulkManagement Tab File Upload Form"
id="gradebook.BulkManagementTab.successDialog"
/>
</Alert>
</Fragment>
`;

View File

@@ -0,0 +1,45 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FileUploadForm component snapshot snapshot - loads export form w/ alerts and file input, import btn 1`] = `
<React.Fragment>
<Form
action="fakeUrl"
inline={false}
method="post"
>
<FormGroup
as="div"
controlId="csv"
isInvalid={false}
isValid={false}
>
<ForwardRef
as="input"
className="d-none"
label={
<FormattedMessage
defaultMessage="Upload Grade CSV"
description="Button in BulkManagementTab Alerts"
id="gradebook.BulkManagementTab.csvUploadLabel"
/>
}
onChange={[MockFunction this.handleFileInputChange]}
plaintext={false}
type="file"
/>
</FormGroup>
</Form>
<ForwardRef
active={false}
disabled={false}
onClick={[MockFunction this.handleClickImportGrades]}
variant="primary"
>
<FormattedMessage
defaultMessage="Import Grades"
description="Button in BulkManagement Tab File Upload Form"
id="gradebook.BulkManagementTab.importBtnText"
/>
</ForwardRef>
</React.Fragment>
`;

View File

@@ -0,0 +1,132 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`HistoryTable component snapshot history table data (from bulkManagementHistory.map(this.formatHistoryRow) snapshot: maps resultsSummay to ResultsSummary, wraps filename and user, forwards the rest 1`] = `
Array [
Object {
"filename": <span
className="wrap-text-in-cell"
>
blue.png
</span>,
"resultsSummary": <ResultsSummary
courseId="Da Bu Dee"
rowId={12}
text="Da ba daa"
/>,
"timeUploaded": "65",
"user": <span
className="wrap-text-in-cell"
>
Eifel
</span>,
},
Object {
"filename": <span
className="wrap-text-in-cell"
>
allStar.jpg
</span>,
"resultsSummary": <ResultsSummary
courseId="rockstar"
rowId={2}
text="all that glitters is gold"
/>,
"timeUploaded": "2000s?",
"user": <span
className="wrap-text-in-cell"
>
Smashmouth
</span>,
},
]
`;
exports[`HistoryTable component snapshot snapshot - loads hints display, formatted table 1`] = `
<Fragment>
<p>
<FormattedMessage
defaultMessage="Results appear in the table below."
description="Hint text on BulkManagement Tab History Table"
id="gradebook.BulkManagementTab.hint1"
/>
<br />
<FormattedMessage
defaultMessage="Grade processing may take a few seconds."
description="Hint text on BulkManagement Tab History Table"
id="gradebook.BulkManagementTab.hint2"
/>
</p>
<Table
className="table-striped"
columns={
Array [
Object {
"columnSortable": false,
"key": "filename",
"label": "Gradebook",
"width": "col-5",
},
Object {
"columnSortable": false,
"key": "resultsSummary",
"label": "Download Summary",
"width": "col",
},
Object {
"columnSortable": false,
"key": "user",
"label": "Who",
"width": "col-1",
},
Object {
"columnSortable": false,
"key": "timeUploaded",
"label": "When",
"width": "col",
},
]
}
data={
Array [
Object {
"filename": <span
className="wrap-text-in-cell"
>
blue.png
</span>,
"resultsSummary": <ResultsSummary
courseId="Da Bu Dee"
rowId={12}
text="Da ba daa"
/>,
"timeUploaded": "65",
"user": <span
className="wrap-text-in-cell"
>
Eifel
</span>,
},
Object {
"filename": <span
className="wrap-text-in-cell"
>
allStar.jpg
</span>,
"resultsSummary": <ResultsSummary
courseId="rockstar"
rowId={2}
text="all that glitters is gold"
/>,
"timeUploaded": "2000s?",
"user": <span
className="wrap-text-in-cell"
>
Smashmouth
</span>,
},
]
}
hasFixedColumnWidths={true}
/>
</Fragment>
`;

View File

@@ -0,0 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ResultsSummary component snapshot - safe hyperlink with bulkGradesUrl with course and row id, download icon, results text 1`] = `
<Hyperlink
destination="www.edx.org"
href={
Object {
"url": Object {
"rowId": 42,
},
}
}
rel="noopener noreferrer"
showLaunchIcon={false}
target="_blank"
>
<Icon
className="d-inline-block"
src="DownloadIcon"
/>
texty
</Hyperlink>
`;

View File

@@ -0,0 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BulkManagementTab component snapshot snapshot - loads heading from messages.BulkManagementTab.heading, <BulkManagementAlerts />, <FileUploadForm />, <HistoryTable /> 1`] = `
<div>
<h4>
<FormattedMessage
defaultMessage="Use this feature by downloading a CSV for bulk management, overriding grades locally, and coming back here to upload."
description="Heading text for BulkManagement Tab"
id="gradebook.BulkManagementTab.heading"
/>
</h4>
<BulkManagementAlerts />
<FileUploadForm />
<HistoryTable />
</div>
`;

View File

@@ -0,0 +1,23 @@
/* eslint-disable react/button-has-type, import/no-named-as-default */
import React from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import messages from './messages';
import BulkManagementAlerts from './BulkManagementAlerts';
import FileUploadForm from './FileUploadForm';
import HistoryTable from './HistoryTable';
/**
* <BulkManagementTab />
* top-level view for managing uploads of bulk management override csvs.
*/
export const BulkManagementTab = () => (
<div>
<h4><FormattedMessage {...(messages.heading)} /></h4>
<BulkManagementAlerts />
<FileUploadForm />
<HistoryTable />
</div>
);
export default BulkManagementTab;

View File

@@ -0,0 +1,48 @@
/* eslint-disable import/no-named-as-default */
import React from 'react';
import { shallow } from 'enzyme';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { BulkManagementTab } from '.';
import BulkManagementAlerts from './BulkManagementAlerts';
import FileUploadForm from './FileUploadForm';
import HistoryTable from './HistoryTable';
import messages from './messages';
jest.mock('./BulkManagementAlerts', () => 'BulkManagementAlerts');
jest.mock('./FileUploadForm', () => 'FileUploadForm');
jest.mock('./HistoryTable', () => 'HistoryTable');
describe('BulkManagementTab', () => {
describe('component', () => {
let el;
beforeEach(() => {
el = shallow(<BulkManagementTab />);
});
describe('snapshot', () => {
const snapshotSegments = [
'heading from messages.BulkManagementTab.heading',
'<BulkManagementAlerts />',
'<FileUploadForm />',
'<HistoryTable />',
];
test(`snapshot - loads ${snapshotSegments.join(', ')}`, () => {
expect(el).toMatchSnapshot();
});
test('heading - h4 loaded from messages', () => {
const heading = el.find('h4');
expect(heading.getElement()).toEqual((
<h4>
<FormattedMessage {...messages.heading} />
</h4>
));
});
test('heading, then alerts, then upload form, then table', () => {
expect(el.childAt(0).is('h4')).toEqual(true);
expect(el.childAt(1).is(BulkManagementAlerts)).toEqual(true);
expect(el.childAt(2).is(FileUploadForm)).toEqual(true);
expect(el.childAt(3).is(HistoryTable)).toEqual(true);
});
});
});
});

View File

@@ -0,0 +1,36 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
csvUploadLabel: {
id: 'gradebook.BulkManagementTab.csvUploadLabel',
defaultMessage: 'Upload Grade CSV',
description: 'Button in BulkManagementTab Alerts',
},
heading: {
id: 'gradebook.BulkManagementTab.heading',
defaultMessage: 'Use this feature by downloading a CSV for bulk management, overriding grades locally, and coming back here to upload.',
description: 'Heading text for BulkManagement Tab',
},
hint1: {
id: 'gradebook.BulkManagementTab.hint1',
defaultMessage: 'Results appear in the table below.',
description: 'Hint text on BulkManagement Tab History Table',
},
hint2: {
id: 'gradebook.BulkManagementTab.hint2',
defaultMessage: 'Grade processing may take a few seconds.',
description: 'Hint text on BulkManagement Tab History Table',
},
importBtnText: {
id: 'gradebook.BulkManagementTab.importBtnText',
defaultMessage: 'Import Grades',
description: 'Button in BulkManagement Tab File Upload Form',
},
successDialog: {
id: 'gradebook.BulkManagementTab.successDialog',
defaultMessage: 'CSV processing. File uploads may take several minutes to complete.',
description: 'Success Dialog message in BulkManagement Tab File Upload Form',
},
});
export default messages;

View File

@@ -1,48 +0,0 @@
$drawer-width: 350px;
.drawer-contents {
overflow-x: auto;
transition: margin 300ms cubic-bezier(0.4,0,0.2,1);
margin-left: 0;
.drawer.open + & {
margin-left: $drawer-width;
}
&.opened {
width: calc(100vw - #{$drawer-width});
}
}
.drawer-contents {
overflow-x: auto;
transition: margin 300ms cubic-bezier(0.4,0,0.2,1);
margin-left: 0;
.drawer.open + & {
margin-left: $drawer-width;
}
&.opened {
width: calc(100vw - #{$drawer-width});
}
}
.drawer-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 15px;
}
.drawer-container .collapsible {
margin-bottom: 1em;
}
.drawer {
height: 100%;
width: $drawer-width;
position: absolute;
transform: translateX(-$drawer-width);
flex-direction: column;
transition: transform 300ms cubic-bezier(0.4,0,0.2,1);
&.open {
transform: translateX(0%);
}
}

View File

@@ -1,85 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Button } from '@edx/paragon';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faTimes } from '@fortawesome/free-solid-svg-icons';
export default class Drawer extends React.Component {
constructor(props) {
super(props);
this.state = {
open: props.initiallyOpen,
transitioning: false,
};
}
close = () => {
if (this.state.open) {
this.toggleOpen();
}
};
toggleOpen = () => {
this.setState({ transitioning: true });
// defer the transition to the next repaint so we can be sure that
// opening drawer is visible before it transitions
// (the start state of the opening animation doesn't work if the element starts hidden)
this.deferToNextRepaint(() => this.setState(prevState => ({ open: !prevState.open })));
};
handleSlideDone = (e) => {
if (e.currentTarget === e.target) {
this.setState({ transitioning: false });
}
};
deferToNextRepaint(callback) {
window.requestAnimationFrame(() => window.setTimeout(callback, 0));
}
render() {
return (
<div className="d-flex drawer-container">
<aside
className={classNames(
'drawer',
{
open: this.state.open,
'd-none': !this.state.transitioning && !this.state.open,
},
)}
onTransitionEnd={this.handleSlideDone}
>
<div className="drawer-header">
<h2>{this.props.title}</h2>
<Button
className="p-1"
onClick={this.close}
aria-label="Close Filters"
>
<FontAwesomeIcon icon={faTimes} />
</Button>
</div>
{this.props.children}
</aside>
<div
className={classNames(
'drawer-contents',
'position-relative',
!this.state.drawerTransitioning && this.state.drawerOpen && 'opened',
)}
>
{this.props.mainContent(this.toggleOpen)}
</div>
</div>
);
}
}
Drawer.propTypes = {
initiallyOpen: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired,
mainContent: PropTypes.func.isRequired,
title: PropTypes.node.isRequired,
};

View File

@@ -0,0 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Header snapshot - has edx link with logo url 1`] = `
<div
className="mb-3"
>
<header
className="d-flex justify-content-center align-items-center p-3 border-bottom-blue"
>
<Hyperlink
destination="undefined/dashboard"
>
<img
alt="edX logo"
height="30"
src="www.ourLogo.url"
width="60"
/>
</Hyperlink>
<div />
</header>
</div>
`;

View File

@@ -0,0 +1,21 @@
import React from 'react';
import { Hyperlink } from '@edx/paragon';
import { getConfig } from '@edx/frontend-platform';
/**
* <EdxHeader />
* Gradebook MFE app header.
* Displays edx logo, linked to lms dashboard
*/
const EdxHeader = () => (
<div className="mb-3">
<header className="d-flex justify-content-center align-items-center p-3 border-bottom-blue">
<Hyperlink destination={`${getConfig().LMS_BASE_URL}/dashboard`}>
<img src={getConfig().LOGO_URL} alt="edX logo" height="30" width="60" />
</Hyperlink>
<div />
</header>
</div>
);
export default EdxHeader;

View File

@@ -0,0 +1,21 @@
import React from 'react';
import { shallow } from 'enzyme';
import { getConfig } from '@edx/frontend-platform';
import Header from '.';
jest.mock('@edx/paragon', () => ({
Hyperlink: () => 'Hyperlink',
}));
jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(),
}));
describe('Header', () => {
test('snapshot - has edx link with logo url', () => {
const url = 'www.ourLogo.url';
getConfig.mockReturnValue({ LOGO_URL: url });
expect(shallow(<Header />)).toMatchSnapshot();
});
});

View File

@@ -1,198 +0,0 @@
import { connect } from 'react-redux';
import React from 'react';
import PropTypes from 'prop-types';
import initialFilters from '../../data/constants/filters';
function FilterBadge({
name, value, onClick, showValue,
}) {
return (
<div>
<span className="badge badge-info">
<span>{name}{showValue && `: ${value}`}</span>
<button type="button" className="btn-info" aria-label="Close" onClick={onClick}>
<span aria-hidden="true">&times;</span>
</button>
</span>
<br />
</div>
);
}
FilterBadge.defaultProps = {
showValue: true,
};
FilterBadge.propTypes = {
name: PropTypes.string.isRequired,
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.bool,
]).isRequired,
onClick: PropTypes.func.isRequired,
showValue: PropTypes.bool,
};
function RangeFilterBadge({
displayName,
filterName1,
filterValue1,
filterName2,
filterValue2,
handleBadgeClose,
}) {
return ((filterValue1 !== initialFilters[filterName1])
|| (filterValue2 !== initialFilters[filterName2]))
&& (
<FilterBadge
name={displayName}
value={`${filterValue1} - ${filterValue2}`}
onClick={handleBadgeClose}
/>
);
}
RangeFilterBadge.propTypes = {
displayName: PropTypes.string.isRequired,
filterName1: PropTypes.string.isRequired,
filterValue1: PropTypes.string.isRequired,
filterName2: PropTypes.string.isRequired,
filterValue2: PropTypes.string.isRequired,
handleBadgeClose: PropTypes.func.isRequired,
};
function SingleValueFilterBadge({
displayName, filterName, filterValue, handleBadgeClose, showValue,
}) {
return (filterValue !== initialFilters[filterName])
&& (
<FilterBadge
name={displayName}
value={filterValue}
onClick={handleBadgeClose}
showValue={showValue}
/>
);
}
SingleValueFilterBadge.defaultProps = {
showValue: true,
};
SingleValueFilterBadge.propTypes = {
displayName: PropTypes.string.isRequired,
filterName: PropTypes.string.isRequired,
filterValue: PropTypes.oneOfType([
PropTypes.string,
PropTypes.bool,
]).isRequired,
handleBadgeClose: PropTypes.func.isRequired,
showValue: PropTypes.bool,
};
function FilterBadges({
assignment,
assignmentType,
track,
cohort,
assignmentGradeMin,
assignmentGradeMax,
courseGradeMin,
courseGradeMax,
includeCourseRoleMembers,
handleFilterBadgeClose,
}) {
return (
<div>
<SingleValueFilterBadge
displayName="Assignment Type"
filterName="assignmentType"
filterValue={assignmentType}
handleBadgeClose={handleFilterBadgeClose(['assignmentType'])}
/>
<SingleValueFilterBadge
displayName="Assignment"
filterName="assignment"
filterValue={assignment}
handleBadgeClose={handleFilterBadgeClose(['assignment', 'assignmentGradeMax', 'assignmentGradeMin'])}
/>
<RangeFilterBadge
displayName="Assignment Grade"
filterName1="assignmentGradeMin"
filterValue1={assignmentGradeMin}
filterName2="assignmentGradeMax"
filterValue2={assignmentGradeMax}
handleBadgeClose={handleFilterBadgeClose(['assignmentGradeMin', 'assignmentGradeMax'])}
/>
<RangeFilterBadge
displayName="Course Grade"
filterName1="courseGradeMin"
filterValue1={courseGradeMin}
filterName2="courseGradeMax"
filterValue2={courseGradeMax}
handleBadgeClose={handleFilterBadgeClose(['courseGradeMin', 'courseGradeMax'])}
/>
<SingleValueFilterBadge
displayName="Track"
filterName="track"
filterValue={track}
handleBadgeClose={handleFilterBadgeClose(['track'])}
/>
<SingleValueFilterBadge
displayName="Cohort"
filterName="cohort"
filterValue={cohort}
handleBadgeClose={handleFilterBadgeClose(['cohort'])}
/>
<SingleValueFilterBadge
displayName="Including Course Team Members"
filterName="includeCourseRoleMembers"
filterValue={includeCourseRoleMembers}
showValue={false}
handleBadgeClose={handleFilterBadgeClose(['includeCourseRoleMembers'])}
/>
</div>
);
}
const mapStateToProps = state => (
{
assignment: (state.filters.assignment || {}).label,
assignmentType: state.filters.assignmentType,
track: state.filters.track,
cohort: state.filters.cohort,
assignmentGradeMin: state.filters.assignmentGradeMin,
assignmentGradeMax: state.filters.assignmentGradeMax,
courseGradeMin: state.filters.courseGradeMin,
courseGradeMax: state.filters.courseGradeMax,
includeCourseRoleMembers: state.filters.includeCourseRoleMembers,
}
);
const ConnectedFilterBadges = connect(mapStateToProps)(FilterBadges);
export default ConnectedFilterBadges;
FilterBadges.defaultProps = {
assignment: initialFilters.assignmentType,
assignmentType: initialFilters.assignmentType,
track: initialFilters.track,
cohort: initialFilters.cohort,
assignmentGradeMin: initialFilters.assignmentGradeMin,
assignmentGradeMax: initialFilters.assignmentGradeMax,
courseGradeMin: initialFilters.courseGradeMin,
courseGradeMax: initialFilters.courseGradeMax,
includeCourseRoleMembers: initialFilters.includeCourseRoleMembers,
};
FilterBadges.propTypes = {
assignment: PropTypes.string,
assignmentType: PropTypes.string,
track: PropTypes.string,
cohort: PropTypes.string,
assignmentGradeMin: PropTypes.string,
assignmentGradeMax: PropTypes.string,
courseGradeMin: PropTypes.string,
courseGradeMax: PropTypes.string,
includeCourseRoleMembers: PropTypes.bool,
handleFilterBadgeClose: PropTypes.func.isRequired,
};

View File

@@ -1,199 +0,0 @@
/* eslint-disable react/sort-comp, react/button-has-type */
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import {
Button,
StatusAlert,
Table,
} from '@edx/paragon';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faDownload } from '@fortawesome/free-solid-svg-icons';
import selectors from 'data/selectors';
import { configuration } from '../../config';
import { submitFileUploadFormData } from '../../data/actions/grades';
export class BulkManagement extends React.Component {
constructor(props) {
super(props);
this.fileFormRef = React.createRef();
this.fileInputRef = React.createRef();
}
formatHistoryRow = (row) => {
const {
summaryOfRowsProcessed: {
total,
successfullyProcessed,
failed,
skipped,
},
unique_id: courseId,
originalFilename,
id,
user: username,
...rest
} = row;
const resultsText = [
`${total} Students: ${successfullyProcessed} processed`,
...(skipped > 0 ? [`${skipped} skipped`] : []),
...(failed > 0 ? [`${failed} failed`] : []),
].join(', ');
const resultsSummary = (
<a
href={`${configuration.LMS_BASE_URL}/api/bulk_grades/course/${courseId}/?error_id=${id}`}
target="_blank"
rel="noopener noreferrer"
>
<FontAwesomeIcon icon={faDownload} />
{resultsText}
</a>
);
const createWrappedCell = (text) => (<span className="wrap-text-in-cell">{text}</span>);
const filename = createWrappedCell(originalFilename);
const user = createWrappedCell(username);
return {
resultsSummary,
filename,
user,
...rest,
};
};
handleClickImportGrades = () => {
const fileInput = this.fileInputRef.current;
if (fileInput) {
fileInput.click();
}
};
handleFileInputChange = (event) => {
const fileInput = event.target;
const file = fileInput.files[0];
const form = this.fileFormRef.current;
if (file && form) {
const formData = new FormData(form);
this.props.submitFileUploadFormData(this.props.courseId, formData).then(() => {
fileInput.value = null;
});
}
};
render() {
return (
<div>
<h4>Use this feature by downloading a CSV for bulk management,
overriding grades locally, and coming back here to upload.
</h4>
<form ref={this.fileFormRef} action={this.props.gradeExportUrl} method="post">
<StatusAlert
alertType="danger"
dialog={this.props.bulkImportError}
isOpen={this.props.bulkImportError}
dismissible={false}
/>
<StatusAlert
alertType="success"
dialog="CSV processing. File uploads may take several minutes to complete"
open={this.props.uploadSuccess}
dismissible={false}
/>
<input
className="d-none"
type="file"
name="csv"
label="Upload Grade CSV"
onChange={this.handleFileInputChange}
ref={this.fileInputRef}
/>
</form>
<Button
variant="primary"
onClick={this.handleClickImportGrades}
>
Import Grades
</Button>
<p>
Results appear in the table below.<br />
Grade processing may take a few seconds.
</p>
<Table
data={this.props.bulkManagementHistory.map(this.formatHistoryRow)}
hasFixedColumnWidths
columns={[
{
key: 'filename',
label: 'Gradebook',
columnSortable: false,
width: 'col-5',
},
{
key: 'resultsSummary',
label: 'Download Summary',
columnSortable: false,
width: 'col',
},
{
key: 'user',
label: 'Who',
columnSortable: false,
width: 'col-1',
},
{
key: 'timeUploaded',
label: 'When',
columnSortable: false,
width: 'col',
},
]}
className="table-striped"
/>
</div>
);
}
}
BulkManagement.defaultProps = {
bulkImportError: '',
bulkManagementHistory: [],
courseId: '',
uploadSuccess: false,
};
BulkManagement.propTypes = {
courseId: PropTypes.string,
gradeExportUrl: PropTypes.string.isRequired,
// redux
bulkImportError: PropTypes.string,
bulkManagementHistory: PropTypes.arrayOf(PropTypes.shape({
originalFilename: PropTypes.string.isRequired,
user: PropTypes.string.isRequired,
timeUploaded: PropTypes.string.isRequired,
summaryOfRowsProcessed: PropTypes.shape({
total: PropTypes.number.isRequired,
successfullyProcessed: PropTypes.number.isRequired,
failed: PropTypes.number.isRequired,
skipped: PropTypes.number.isRequired,
}).isRequired,
})),
submitFileUploadFormData: PropTypes.func.isRequired,
uploadSuccess: PropTypes.bool,
};
export const mapStateToProps = (state) => {
const { grades } = selectors;
return {
bulkImportError: grades.bulkImportError(state),
bulkManagementHistory: grades.bulkManagementHistoryEntries(state),
uploadSuccess: grades.uploadSuccess(state),
};
};
export const mapDispatchToProps = {
submitFileUploadFormData,
};
export default connect(mapStateToProps, mapDispatchToProps)(BulkManagement);

View File

@@ -1,90 +0,0 @@
/* eslint-disable react/sort-comp, react/button-has-type */
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { StatefulButton } from '@edx/paragon';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faDownload, faSpinner } from '@fortawesome/free-solid-svg-icons';
import {
downloadBulkGradesReport,
downloadInterventionReport,
} from '../../data/actions/grades';
export class BulkManagementControls extends React.Component {
handleClickDownloadInterventions = () => {
this.props.downloadInterventionReport(this.props.courseId);
window.location = this.props.interventionExportUrl;
};
// At present, we don't store label and value in google analytics. By setting the label
// property of the below events, I want to verify that we can set the label of google anlatyics
// The following properties of a google analytics event are:
// category (used), name(used), lavel(not used), value(not used)
handleClickExportGrades = () => {
this.props.downloadBulkGradesReport(this.props.courseId);
window.location = this.props.gradeExportUrl;
};
render() {
return (
<div>
<StatefulButton
variant="outline-primary"
onClick={this.handleClickExportGrades}
state={this.props.showSpinner ? 'pending' : 'default'}
labels={{
default: 'Bulk Management',
pending: 'Bulk Management',
}}
icons={{
default: <FontAwesomeIcon className="mr-2" icon={faDownload} />,
pending: <FontAwesomeIcon className="fa-spin mr-2" icon={faSpinner} />,
}}
disabledStates={['pending']}
/>
<StatefulButton
variant="outline-primary"
onClick={this.handleClickDownloadInterventions}
state={this.props.showSpinner ? 'pending' : 'default'}
className="ml-2"
labels={{
default: 'Interventions*',
pending: 'Interventions*',
}}
icons={{
default: <FontAwesomeIcon className="mr-2" icon={faDownload} />,
pending: <FontAwesomeIcon className="fa-spin mr-2" icon={faSpinner} />,
}}
disabledStates={['pending']}
/>
</div>
);
}
}
BulkManagementControls.defaultProps = {
courseId: '',
showSpinner: false,
};
BulkManagementControls.propTypes = {
courseId: PropTypes.string,
gradeExportUrl: PropTypes.string.isRequired,
interventionExportUrl: PropTypes.string.isRequired,
showSpinner: PropTypes.bool,
// redux
downloadBulkGradesReport: PropTypes.func.isRequired,
downloadInterventionReport: PropTypes.func.isRequired,
};
export const mapStateToProps = () => ({ });
export const mapDispatchToProps = {
downloadBulkGradesReport,
downloadInterventionReport,
};
export default connect(mapStateToProps, mapDispatchToProps)(BulkManagementControls);

View File

@@ -1,210 +0,0 @@
/* eslint-disable react/sort-comp, react/button-has-type */
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import {
Button,
Modal,
StatusAlert,
Table,
} from '@edx/paragon';
import selectors from 'data/selectors';
import {
doneViewingAssignment,
updateGrades,
} from '../../data/actions/grades';
const GRADE_OVERRIDE_HISTORY_COLUMNS = [
{ label: 'Date', key: 'date' },
{ label: 'Grader', key: 'grader' },
{ label: 'Reason', key: 'reason' },
{ label: 'Adjusted grade', key: 'adjustedGrade' },
];
export class EditModal extends React.Component {
constructor(props) {
super(props);
this.overrideReasonInput = React.createRef();
}
componentDidMount() {
this.overrideReasonInput.current.focus();
}
handleAdjustedGradeClick = () => {
this.props.updateGrades(
this.props.courseId, [
{
user_id: this.props.updateUserId,
usage_id: this.props.updateModuleId,
grade: {
earned_graded_override: this.props.adjustedGradeValue,
comment: this.props.reasonForChange,
},
},
],
this.props.filterValue,
this.props.selectedCohort,
this.props.selectedTrack,
);
this.closeAssignmentModal();
}
closeAssignmentModal = () => {
this.props.doneViewingAssignment();
this.props.setGradebookState({
adjustedGradePossible: '',
adjustedGradeValue: '',
modalOpen: false,
reasonForChange: '',
updateModuleId: null,
updateUserId: null,
});
};
render() {
return (
<Modal
open={this.props.open}
title="Edit Grades"
closeText="Cancel"
body={(
<div>
<div>
<div className="grade-history-header grade-history-assignment">Assignment: </div>
<div>{this.props.assignmentName}</div>
<div className="grade-history-header grade-history-student">Student: </div>
<div>{this.props.updateUserName}</div>
<div className="grade-history-header grade-history-original-grade">Original Grade: </div>
<div>{this.props.gradeOriginalEarnedGraded}</div>
<div className="grade-history-header grade-history-current-grade">Current Grade: </div>
<div>{this.props.gradeOverrideCurrentEarnedGradedOverride}</div>
</div>
<StatusAlert
alertType="danger"
dialog={this.props.gradeOverrideHistoryError}
open={!!this.props.gradeOverrideHistoryError}
dismissible={false}
/>
{!this.props.gradeOverrideHistoryError && (
<Table
columns={GRADE_OVERRIDE_HISTORY_COLUMNS}
data={[...this.props.gradeOverrides, {
date: this.props.todaysDate,
reason: (<input
type="text"
name="reasonForChange"
value={this.props.reasonForChange}
onChange={this.props.setReasonForChange}
ref={this.overrideReasonInput}
/>),
adjustedGrade: (
<span>
<input
type="text"
name="adjustedGradeValue"
value={this.props.adjustedGradeValue}
onChange={this.props.setAdjustedGradeValue}
/>
{(this.props.adjustedGradePossible || this.props.gradeOriginalPossibleGraded) && ' / '}
{this.props.adjustedGradePossible || this.props.gradeOriginalPossibleGraded}
</span>),
}]}
/>
)}
<div>Showing most recent actions (max 5). To see more, please contact
support.
</div>
<div>Note: Once you save, your changes will be visible to students.</div>
</div>
)}
buttons={[
<Button
variant="primary"
onClick={this.handleAdjustedGradeClick}
>
Save Grade
</Button>,
]}
onClose={this.closeAssignmentModal}
/>
);
}
}
EditModal.defaultProps = {
adjustedGradeValue: null,
courseId: '',
gradeOverrideCurrentEarnedGradedOverride: null,
gradeOverrideHistoryError: '',
gradeOverrides: [],
gradeOriginalEarnedGraded: null,
gradeOriginalPossibleGraded: null,
selectedCohort: null,
selectedTrack: null,
updateModuleId: '',
updateUserId: '',
updateUserName: '',
};
EditModal.propTypes = {
courseId: PropTypes.string,
// Gradebook State
adjustedGradePossible: PropTypes.string.isRequired,
// should pick one?
adjustedGradeValue: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
assignmentName: PropTypes.string.isRequired,
filterValue: PropTypes.string.isRequired,
open: PropTypes.bool.isRequired,
reasonForChange: PropTypes.string.isRequired,
todaysDate: PropTypes.string.isRequired,
updateModuleId: PropTypes.string,
updateUserId: PropTypes.number,
updateUserName: PropTypes.string,
// Gradebook State Setters
setAdjustedGradeValue: PropTypes.func.isRequired,
setGradebookState: PropTypes.func.isRequired,
setReasonForChange: PropTypes.func.isRequired,
// redux
doneViewingAssignment: PropTypes.func.isRequired,
gradeOverrideCurrentEarnedGradedOverride: PropTypes.number,
gradeOverrideHistoryError: PropTypes.string,
gradeOverrides: PropTypes.arrayOf(PropTypes.shape({
date: PropTypes.string,
grader: PropTypes.string,
reason: PropTypes.string,
adjustedGrade: PropTypes.number,
})),
gradeOriginalEarnedGraded: PropTypes.number,
gradeOriginalPossibleGraded: PropTypes.number,
selectedCohort: PropTypes.string,
selectedTrack: PropTypes.string,
updateGrades: PropTypes.func.isRequired,
};
export const mapStateToProps = (state) => {
const { filters, grades } = selectors;
return {
gradeOverrides: grades.gradeOverrides(state),
gradeOverrideCurrentEarnedGradedOverride: grades.gradeOverrideCurrentEarnedGradedOverride(state),
gradeOverrideHistoryError: grades.gradeOverrideHistoryError(state),
gradeOriginalEarnedGraded: grades.gradeOriginalEarnedGraded(state),
gradeOriginalPossibleGraded: grades.gradeOriginalPossibleGraded(state),
selectedCohort: filters.cohort(state),
selectedTrack: filters.track(state),
};
};
export const mapDispatchToProps = {
doneViewingAssignment,
updateGrades,
};
export default connect(mapStateToProps, mapDispatchToProps)(EditModal);

View File

@@ -1,125 +0,0 @@
/* eslint-disable react/sort-comp, react/button-has-type */
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Button } from '@edx/paragon';
import * as gradesActions from 'data/actions/grades';
import * as filterActions from 'data/actions/filters';
import selectors from 'data/selectors';
import PercentGroup from '../PercentGroup';
export class AssignmentGradeFilter extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleSetMax = this.handleSetMax.bind(this);
this.handleSetMin = this.handleSetMin.bind(this);
}
handleSubmit() {
const {
assignmentGradeMin,
assignmentGradeMax,
} = this.props.filterValues;
this.props.updateAssignmentLimits(
assignmentGradeMin,
assignmentGradeMax,
);
this.props.getUserGrades(
this.props.courseId,
this.props.selectedCohort,
this.props.selectedTrack,
this.props.selectedAssignmentType,
);
this.props.updateQueryParams({
assignmentGradeMin,
assignmentGradeMax,
});
}
handleSetMax(event) {
this.props.setFilters({ assignmentGradeMax: event.target.value });
}
handleSetMin(event) {
this.props.setFilters({ assignmentGradeMin: event.target.value });
}
render() {
return (
<div className="grade-filter-inputs">
<PercentGroup
id="assignmentGradeMin"
label="Min Grade"
value={this.props.filterValues.assignmentGradeMin}
disabled={!this.props.selectedAssignment}
onChange={this.handleSetMin}
/>
<PercentGroup
id="assignmentGradeMax"
label="Max Grade"
value={this.props.filterValues.assignmentGradeMax}
disabled={!this.props.selectedAssignment}
onChange={this.handleSetMax}
/>
<div className="grade-filter-action">
<Button
type="submit"
variant="outline-secondary"
name="assignmentGradeMinMax"
disabled={!this.props.selectedAssignment}
onClick={this.handleSubmit}
>
Apply
</Button>
</div>
</div>
);
}
}
AssignmentGradeFilter.defaultProps = {
selectedAssignment: '',
selectedAssignmentType: '',
selectedCohort: null,
selectedTrack: null,
};
AssignmentGradeFilter.propTypes = {
courseId: PropTypes.string.isRequired,
filterValues: PropTypes.shape({
assignmentGradeMin: PropTypes.string.isRequired,
assignmentGradeMax: PropTypes.string.isRequired,
}).isRequired,
setFilters: PropTypes.func.isRequired,
updateQueryParams: PropTypes.func.isRequired,
// redux
getUserGrades: PropTypes.func.isRequired,
selectedAssignmentType: PropTypes.string,
selectedAssignment: PropTypes.string,
selectedCohort: PropTypes.string,
selectedTrack: PropTypes.string,
updateAssignmentLimits: PropTypes.func.isRequired,
};
export const mapStateToProps = (state) => {
const { filters } = selectors;
return {
selectedAssignment: filters.selectedAssignmentLabel(state),
selectedAssignmentType: filters.assignmentType(state),
selectedCohort: filters.cohort(state),
selectedTrack: filters.track(state),
};
};
export const mapDispatchToProps = {
getUserGrades: gradesActions.fetchGrades,
updateAssignmentLimits: filterActions.updateAssignmentLimits,
};
export default connect(mapStateToProps, mapDispatchToProps)(AssignmentGradeFilter);

View File

@@ -1,174 +0,0 @@
import React from 'react';
import { mount, shallow } from 'enzyme';
import { updateAssignmentLimits } from 'data/actions/filters';
import { fetchGrades } from 'data/actions/grades';
import {
AssignmentGradeFilter,
mapStateToProps,
mapDispatchToProps,
} from '.';
describe('AssignmentGradeFilter', () => {
let props = {
filterValues: {
assignmentGradeMin: '1',
assignmentGradeMax: '100',
},
courseId: '12345',
selectedAssignmentType: 'assgnFilterLabel1',
selectedAssignment: 'assgN1',
selectedCohort: 'a cohort',
selectedTrack: 'a track',
};
beforeEach(() => {
props = {
...props,
setFilters: jest.fn(),
updateQueryParams: jest.fn(),
getUserGrades: jest.fn(),
updateAssignmentLimits: jest.fn(),
};
});
describe('Component', () => {
describe('behavior', () => {
describe('handleSubmit', () => {
let el;
beforeEach(() => {
el = mount(<AssignmentGradeFilter {...props} />);
el.instance().handleSubmit();
});
it('calls props.updateAssignmentLimits with min and max', () => {
expect(props.updateAssignmentLimits).toHaveBeenCalledWith(
props.filterValues.assignmentGradeMin,
props.filterValues.assignmentGradeMax,
);
});
it('calls getUserGrades w/ selection', () => {
expect(props.getUserGrades).toHaveBeenCalledWith(
props.courseId,
props.selectedCohort,
props.selectedTrack,
props.selectedAssignmentType,
);
});
it('updates queryParams with assignment grade min and max', () => {
expect(props.updateQueryParams).toHaveBeenCalledWith({
assignmentGradeMin: props.filterValues.assignmentGradeMin,
assignmentGradeMax: props.filterValues.assignmentGradeMax,
});
});
});
describe('handleSetMin', () => {
it('calls setFilters for assignmentGradeMin', () => {
const testVal = 23;
const el = mount(<AssignmentGradeFilter {...props} />);
el.instance().handleSetMin({ target: { value: testVal } });
expect(props.setFilters).toHaveBeenCalledWith({
assignmentGradeMin: testVal,
});
});
});
describe('handleSetMax', () => {
it('calls setFilters for assignmentGradeMax', () => {
const testVal = 92;
const el = mount(<AssignmentGradeFilter {...props} />);
el.instance().handleSetMax({ target: { value: testVal } });
expect(props.setFilters).toHaveBeenCalledWith({
assignmentGradeMax: testVal,
});
});
});
});
describe('snapshots', () => {
let el;
const mockMethods = () => {
el.instance().handleSubmit = jest.fn().mockName('handleSubmit');
el.instance().handleSetMax = jest.fn().mockName('handleSetMax');
el.instance().handleSetMin = jest.fn().mockName('handleSetMin');
};
test('smoke test', () => {
el = shallow(<AssignmentGradeFilter {...props} />);
mockMethods(el);
expect(el.instance().render()).toMatchSnapshot();
});
test('buttons and groups disabled if no selected assignment', () => {
el = shallow(<AssignmentGradeFilter
{...props}
selectedAssignment={undefined}
/>);
mockMethods(el);
expect(el.instance().render()).toMatchSnapshot();
});
});
});
describe('mapStateToProps', () => {
const state = {
filters: {
assignment: { label: 'assigNment' },
assignmentType: 'assignMentType',
cohort: 'COhort',
track: 'traCK',
},
};
describe('selectedAsssignment', () => {
it('is undefined if no assignment is passed', () => {
expect(
mapStateToProps({ filters: {} }).selectedAssignment,
).toEqual(undefined);
});
it('returns the label of selected assignment if there is one', () => {
expect(
mapStateToProps(state).selectedAssignment,
).toEqual(
state.filters.assignment.label,
);
});
});
describe('selectedAssignmentType', () => {
it('is drawn from state.filters.assignmentType', () => {
expect(
mapStateToProps(state).selectedAssignmentType,
).toEqual(
state.filters.assignmentType,
);
});
});
describe('selectedCohort', () => {
it('is drawn from state.filters.cohort', () => {
expect(
mapStateToProps(state).selectedCohort,
).toEqual(
state.filters.cohort,
);
});
});
describe('selectedTrack', () => {
it('is drawn from state.filters.track', () => {
expect(
mapStateToProps(state).selectedTrack,
).toEqual(
state.filters.track,
);
});
});
});
describe('mapDispatchToProps', () => {
test('getUserGrades', () => {
expect(mapDispatchToProps.getUserGrades).toEqual(
fetchGrades,
);
});
test('updateAssignmentLimits', () => {
expect(
mapDispatchToProps.updateAssignmentLimits,
).toEqual(
updateAssignmentLimits,
);
});
});
});

View File

@@ -1,136 +0,0 @@
/* eslint-disable react/sort-comp, react/button-has-type, import/no-named-as-default */
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import {
Button,
} from '@edx/paragon';
import { updateCourseGradeFilter } from 'data/actions/filters';
import { fetchGrades } from 'data/actions/grades';
import selectors from 'data/selectors';
import PercentGroup from '../PercentGroup';
export class CourseGradeFilter extends React.Component {
constructor(props) {
super(props);
this.handleApplyClick = this.handleApplyClick.bind(this);
this.handleUpdateMin = this.handleUpdateMin.bind(this);
this.handleUpdateMax = this.handleUpdateMax.bind(this);
this.updateCourseGradeFilters = this.updateCourseGradeFilters.bind(this);
}
handleApplyClick() {
const { courseGradeMin, courseGradeMax } = this.props.filterValues;
const isMinValid = this.isGradeFilterValueInRange(courseGradeMin);
const isMaxValid = this.isGradeFilterValueInRange(courseGradeMax);
this.props.setFilters({
isMinCourseGradeFilterValid: isMinValid,
isMaxCourseGradeFilterValid: isMaxValid,
});
if (isMinValid && isMaxValid) {
this.updateCourseGradeFilters();
}
}
updateCourseGradeFilters() {
const { courseGradeMin, courseGradeMax } = this.props.filterValues;
this.props.updateFilter(
courseGradeMin,
courseGradeMax,
this.props.courseId,
);
this.props.getUserGrades(
this.props.courseId,
this.props.selectedCohort,
this.props.selectedTrack,
this.props.selectedAssignmentType,
{ courseGradeMin, courseGradeMax },
);
this.props.updateQueryParams({ courseGradeMin, courseGradeMax });
}
handleUpdateMin(event) {
this.props.setFilters({ courseGradeMin: event.target.value });
}
handleUpdateMax(event) {
this.props.setFilters({ courseGradeMax: event.target.value });
}
isGradeFilterValueInRange = (value) => {
const valueAsInt = parseInt(value, 10);
return valueAsInt >= 0 && valueAsInt <= 100;
};
render() {
return (
<>
<div className="grade-filter-inputs">
<PercentGroup
id="minimum-grade"
label="Min Grade"
value={this.props.filterValues.courseGradeMin}
onChange={this.handleUpdateMin}
/>
<PercentGroup
id="maximum-grade"
label="Max Grade"
value={this.props.filterValues.courseGradeMax}
onChange={this.handleUpdateMax}
/>
</div>
<div className="grade-filter-action">
<Button
variant="outline-secondary"
onClick={this.handleApplyClick}
>
Apply
</Button>
</div>
</>
);
}
}
CourseGradeFilter.defaultProps = {
courseId: '',
selectedAssignmentType: '',
selectedCohort: null,
selectedTrack: null,
};
CourseGradeFilter.propTypes = {
courseId: PropTypes.string,
filterValues: PropTypes.shape({
courseGradeMin: PropTypes.string.isRequired,
courseGradeMax: PropTypes.string.isRequired,
}).isRequired,
setFilters: PropTypes.func.isRequired,
updateQueryParams: PropTypes.func.isRequired,
// Redux
getUserGrades: PropTypes.func.isRequired,
selectedAssignmentType: PropTypes.string,
selectedCohort: PropTypes.string,
selectedTrack: PropTypes.string,
updateFilter: PropTypes.func.isRequired,
};
export const mapStateToProps = (state) => {
const { filters } = selectors;
return {
selectedCohort: filters.cohort(state),
selectedTrack: filters.track(state),
selectedAssignmentType: filters.assignmentType(state),
};
};
export const mapDispatchToProps = {
updateFilter: updateCourseGradeFilter,
getUserGrades: fetchGrades,
};
export default connect(mapStateToProps, mapDispatchToProps)(CourseGradeFilter);

View File

@@ -1,196 +0,0 @@
/* eslint-disable import/no-named-as-default */
import React from 'react';
import { shallow } from 'enzyme';
import { updateCourseGradeFilter } from 'data/actions/filters';
import { fetchGrades } from 'data/actions/grades';
import {
CourseGradeFilter,
mapStateToProps,
mapDispatchToProps,
} from '.';
jest.mock('@edx/paragon', () => ({
Button: 'Button',
Collapsible: 'Collapsible',
}));
describe('CourseGradeFilter', () => {
let props = {
filterValues: {
courseGradeMin: '5',
courseGradeMax: '92',
},
courseId: '12345',
selectedAssignmentType: 'assignMent type 1',
selectedCohort: 'COHort',
selectedTrack: 'TracK',
};
beforeEach(() => {
props = {
...props,
getUserGrades: jest.fn(),
setFilters: jest.fn(),
updateQueryParams: jest.fn(),
updateFilter: jest.fn(),
};
});
describe('Component', () => {
describe('snapshots', () => {
test('basic snapshot', () => {
const el = shallow(<CourseGradeFilter {...props} />);
el.instance().handleUpdateMin = jest.fn().mockName(
'handleUpdateMin',
);
el.instance().handleUpdateMax = jest.fn().mockName(
'handleUpdateMax',
);
el.instance().handleApplyClick = jest.fn().mockName(
'handleApplyClick',
);
expect(el.instance().render()).toMatchSnapshot();
});
});
describe('behavior', () => {
let el;
const testVal = 'TESTvalue';
beforeEach(() => {
el = shallow(<CourseGradeFilter {...props} />);
});
describe('handleApplyClick', () => {
beforeEach(() => {
el.instance().updateCourseGradeFilters = jest.fn();
});
it('calls setFilters for isMin(Max)CourseGradeFilterValid', () => {
el.instance().isGradeFilterValueInRange = jest.fn().mockImplementation(v => v >= 50);
el.instance().handleApplyClick();
expect(props.setFilters).toHaveBeenCalledWith({
isMinCourseGradeFilterValid: false,
isMaxCourseGradeFilterValid: true,
});
});
it('calls updateCourseGradeFilters only if both min and max are valid', () => {
const isValid = jest.fn().mockImplementation(v => v >= 50);
el.instance().isGradeFilterValueInRange = isValid;
el.instance().handleApplyClick();
expect(el.instance().updateCourseGradeFilters).not.toHaveBeenCalled();
isValid.mockImplementation(v => v <= 50);
el.instance().handleApplyClick();
expect(el.instance().updateCourseGradeFilters).not.toHaveBeenCalled();
isValid.mockImplementation(v => v >= 0);
el.instance().handleApplyClick();
expect(el.instance().updateCourseGradeFilters).toHaveBeenCalled();
});
});
describe('updateCourseGradeFilters', () => {
beforeEach(() => {
el.instance().updateCourseGradeFilters();
});
it('calls props.updateFilter with selection', () => {
expect(props.updateFilter).toHaveBeenCalledWith(
props.filterValues.courseGradeMin,
props.filterValues.courseGradeMax,
props.courseId,
);
});
it('calls props.getUserGrades with selection', () => {
expect(props.getUserGrades).toHaveBeenCalledWith(
props.courseId,
props.selectedCohort,
props.selectedTrack,
props.selectedAssignmentType,
{
courseGradeMin: props.filterValues.courseGradeMin,
courseGradeMax: props.filterValues.courseGradeMax,
},
);
});
it('updates query params with courseGradeMin and courseGradeMax', () => {
expect(props.updateQueryParams).toHaveBeenCalledWith({
courseGradeMin: props.filterValues.courseGradeMin,
courseGradeMax: props.filterValues.courseGradeMax,
});
});
});
describe('handleUpdateMin', () => {
it('calls props.setCourseGradeMin with event value', () => {
el.instance().handleUpdateMin(
{ target: { value: testVal } },
);
expect(props.setFilters).toHaveBeenCalledWith({
courseGradeMin: testVal,
});
});
});
describe('handleUpdateMax', () => {
it('calls props.setCourseGradeMax with event value', () => {
el.instance().handleUpdateMax(
{ target: { value: testVal } },
);
expect(props.setFilters).toHaveBeenCalledWith({
courseGradeMax: testVal,
});
});
});
describe('isFilterValueInRange', () => {
it('returns true for values between 0 and 100', () => {
expect(el.instance().isGradeFilterValueInRange('0')).toEqual(true);
expect(el.instance().isGradeFilterValueInRange(1.1)).toEqual(true);
expect(el.instance().isGradeFilterValueInRange('43')).toEqual(true);
expect(el.instance().isGradeFilterValueInRange(98.6)).toEqual(true);
expect(el.instance().isGradeFilterValueInRange(100)).toEqual(true);
});
it('returns false for values below 0 and above 100', () => {
expect(el.instance().isGradeFilterValueInRange(-1)).toEqual(false);
expect(el.instance().isGradeFilterValueInRange(101)).toEqual(false);
});
});
});
});
describe('mapStateToProps', () => {
const state = {
filters: {
cohort: 'COHort',
track: 'TRacK',
assignmentType: 'TYPe',
},
};
describe('selectedAssignmentType', () => {
test('drawn from filters.assignmentType', () => {
expect(mapStateToProps(state).selectedAssignmentType).toEqual(
state.filters.assignmentType,
);
});
});
describe('selectedCohort', () => {
test('drawn from filters.cohort', () => {
expect(mapStateToProps(state).selectedCohort).toEqual(
state.filters.cohort,
);
});
});
describe('selectedTrack', () => {
test('drawn from filters.track', () => {
expect(mapStateToProps(state).selectedTrack).toEqual(
state.filters.track,
);
});
});
});
describe('mapDispatchToProps', () => {
describe('updateFilter', () => {
test('from updateCourseGradeFilter', () => {
expect(mapDispatchToProps.updateFilter).toEqual(updateCourseGradeFilter);
});
});
describe('getUserGrades', () => {
test('from fetchGrades', () => {
expect(mapDispatchToProps.getUserGrades).toEqual(fetchGrades);
});
});
});
});

View File

@@ -1,153 +0,0 @@
/* eslint-disable react/sort-comp, react/button-has-type, import/no-named-as-default */
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { fetchGrades } from 'data/actions/grades';
import selectors from 'data/selectors';
import SelectGroup from '../SelectGroup';
export class StudentGroupsFilter extends React.Component {
constructor(props) {
super(props);
this.updateCohorts = this.updateCohorts.bind(this);
this.updateTracks = this.updateTracks.bind(this);
}
mapCohortsEntries = () => {
const mapper = ({ id, name }) => (
<option key={id} value={name}>{name}</option>
);
return [
<option value="Cohort-All" key="0">Cohort-All</option>,
...this.props.cohorts.map(mapper),
];
};
mapTracksEntries = () => {
const mapper = ({ slug, name }) => (
<option key={slug} value={name}>{name}</option>
);
return [
<option value="Track-All" key="0">Track-All</option>,
...this.props.tracks.map(mapper),
];
};
mapSelectedCohortEntry = () => {
const selectedCohortEntry = this.props.cohorts.find(
(x) => x.id === parseInt(this.props.selectedCohort, 10),
);
return selectedCohortEntry ? selectedCohortEntry.name : 'Cohorts';
};
mapSelectedTrackEntry = () => {
const selectedTrackEntry = this.props.tracks.find(
({ slug }) => slug === this.props.selectedTrack,
);
return selectedTrackEntry ? selectedTrackEntry.name : 'Tracks';
};
selectedTrackSlugFromEvent(event) {
const selectedTrackItem = this.props.tracks.find(
({ name }) => name === event.target.value,
);
return selectedTrackItem ? selectedTrackItem.slug : null;
}
selectedCohortIdFromEvent(event) {
const selectedCohortItem = this.props.cohorts.find(
x => x.name === event.target.value,
);
return selectedCohortItem ? selectedCohortItem.id.toString() : null;
}
updateTracks(event) {
const selectedTrackSlug = this.selectedTrackSlugFromEvent(event);
this.props.getUserGrades(
this.props.courseId,
this.props.selectedCohort,
selectedTrackSlug,
this.props.selectedAssignmentType,
);
this.props.updateQueryParams({ track: selectedTrackSlug });
}
updateCohorts(event) {
const selectedCohortId = this.selectedCohortIdFromEvent(event);
this.props.getUserGrades(
this.props.courseId,
selectedCohortId,
this.props.selectedTrack,
this.props.selectedAssignmentType,
);
this.props.updateQueryParams({ cohort: selectedCohortId });
}
render() {
return (
<>
<SelectGroup
id="Tracks"
label="Tracks"
value={this.mapSelectedTrackEntry()}
onChange={this.updateTracks}
options={this.mapTracksEntries()}
/>
<SelectGroup
id="Cohorts"
label="Cohorts"
value={this.mapSelectedCohortEntry()}
disabled={this.props.cohorts.length === 0}
onChange={this.updateCohorts}
options={this.mapCohortsEntries()}
/>
</>
);
}
}
StudentGroupsFilter.defaultProps = {
cohorts: [],
courseId: '',
selectedAssignmentType: '',
selectedCohort: null,
selectedTrack: null,
tracks: [],
};
StudentGroupsFilter.propTypes = {
courseId: PropTypes.string,
updateQueryParams: PropTypes.func.isRequired,
// redux
cohorts: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string,
id: PropTypes.number,
})),
getUserGrades: PropTypes.func.isRequired,
selectedAssignmentType: PropTypes.string,
selectedCohort: PropTypes.string,
selectedTrack: PropTypes.string,
tracks: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string,
slug: PropTypes.string,
})),
};
export const mapStateToProps = (state) => {
const { filters, cohorts, tracks } = selectors;
return {
cohorts: cohorts.allCohorts(state),
selectedAssignmentType: filters.assignmentType(state),
selectedCohort: filters.cohort(state),
selectedTrack: filters.track(state),
tracks: tracks.allTracks(state),
};
};
export const mapDispatchToProps = {
getUserGrades: fetchGrades,
};
export default connect(mapStateToProps, mapDispatchToProps)(StudentGroupsFilter);

View File

@@ -1,75 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GradebookFilters Component snapshots basic snapshot 1`] = `
<React.Fragment>
<Collapsible
className="filter-group mb-3"
defaultOpen={true}
title="Assignments"
>
<div>
<Connect(AssignmentTypeFilter)
updateQueryParams={[MockFunction]}
/>
<Connect(AssignmentFilter)
courseId="12345"
updateQueryParams={[MockFunction]}
/>
<Connect(AssignmentGradeFilter)
courseId="12345"
filterValues={
Object {
"assignmentGradeMax": "90",
"assignmentGradeMin": "10",
"courseGradeMax": "80",
"courseGradeMin": "20",
}
}
setFilters={[MockFunction]}
updateQueryParams={[MockFunction]}
/>
</div>
</Collapsible>
<Collapsible
className="filter-group mb-3"
defaultOpen={true}
title="Overall Grade"
>
<Connect(CourseGradeFilter)
courseId="12345"
filterValues={
Object {
"assignmentGradeMax": "90",
"assignmentGradeMin": "10",
"courseGradeMax": "80",
"courseGradeMin": "20",
}
}
setFilters={[MockFunction]}
updateQueryParams={[MockFunction]}
/>
</Collapsible>
<Collapsible
className="filter-group mb-3"
defaultOpen={true}
title="Student Groups"
>
<Connect(StudentGroupsFilter)
courseId="12345"
updateQueryParams={[MockFunction]}
/>
</Collapsible>
<Collapsible
className="filter-group mb-3"
defaultOpen={true}
title="Include Course Team Members"
>
<Checkbox
checked={true}
onChange={[MockFunction handleIncludeTeamMembersChange]}
>
Include Course Team Members
</Checkbox>
</Collapsible>
</React.Fragment>
`;

View File

@@ -1,234 +0,0 @@
/* eslint-disable react/sort-comp, react/button-has-type */
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import {
Table, OverlayTrigger, Tooltip, Icon,
} from '@edx/paragon';
import { formatDateForDisplay } from '../../data/actions/utils';
import { fetchGradeOverrideHistory } from '../../data/actions/grades';
import { EMAIL_HEADING, TOTAL_COURSE_GRADE_HEADING, USERNAME_HEADING } from '../../data/constants/grades';
import selectors from '../../data/selectors';
const DECIMAL_PRECISION = 2;
export class GradebookTable extends React.Component {
setNewModalState = (userEntry, subsection) => {
this.props.fetchGradeOverrideHistory(
subsection.module_id,
userEntry.user_id,
);
let adjustedGradePossible = '';
if (subsection.attempted) {
adjustedGradePossible = subsection.score_possible;
}
this.props.setGradebookState({
adjustedGradePossible,
adjustedGradeValue: '',
assignmentName: `${subsection.subsection_name}`,
modalOpen: true,
reasonForChange: '',
todaysDate: formatDateForDisplay(new Date()),
updateModuleId: subsection.module_id,
updateUserId: userEntry.user_id,
updateUserName: userEntry.username,
});
}
getLearnerInformation = entry => (
<div>
<div>{entry.username}</div>
{entry.external_user_key && <div className="student-key">{entry.external_user_key}</div>}
</div>
)
roundGrade = percent => parseFloat((percent || 0).toFixed(DECIMAL_PRECISION));
formatter = {
percent: (entries, areGradesFrozen) => entries.map((entry) => {
const learnerInformation = this.getLearnerInformation(entry);
const results = {
[USERNAME_HEADING]: (
<div><span className="wrap-text-in-cell">{learnerInformation}</span></div>
),
[EMAIL_HEADING]: (
<span className="wrap-text-in-cell">{entry.email}</span>
),
};
const assignments = entry.section_breakdown
.reduce((acc, subsection) => {
if (areGradesFrozen) {
acc[subsection.label] = `${this.roundGrade(subsection.percent * 100)} %`;
} else {
acc[subsection.label] = (
<button
className="btn btn-header link-style grade-button"
onClick={() => this.setNewModalState(entry, subsection)}
>
{this.roundGrade(subsection.percent * 100)}%
</button>
);
}
return acc;
}, {});
const totals = { [TOTAL_COURSE_GRADE_HEADING]: `${this.roundGrade(entry.percent * 100)}%` };
return Object.assign(results, assignments, totals);
}),
absolute: (entries, areGradesFrozen) => entries.map((entry) => {
const learnerInformation = this.getLearnerInformation(entry);
const results = {
[USERNAME_HEADING]: (
<div><span className="wrap-text-in-cell">{learnerInformation}</span></div>
),
[EMAIL_HEADING]: (
<span className="wrap-text-in-cell">{entry.email}</span>
),
};
const assignments = entry.section_breakdown
.reduce((acc, subsection) => {
const scoreEarned = this.roundGrade(subsection.score_earned);
const scorePossible = this.roundGrade(subsection.score_possible);
let label = `${scoreEarned}`;
if (subsection.attempted) {
label = `${scoreEarned}/${scorePossible}`;
}
if (areGradesFrozen) {
acc[subsection.label] = label;
} else {
acc[subsection.label] = (
<button
className="btn btn-header link-style"
onClick={() => this.setNewModalState(entry, subsection)}
>
{label}
</button>
);
}
return acc;
}, {});
// Show this as a percent no matter what the other setting is. The data
// we're getting gives the final grade as a percentage so making it appear
// to be "out of" 100 is misleading.
const totals = { [TOTAL_COURSE_GRADE_HEADING]: `${this.roundGrade(entry.percent * 100)}%` };
return Object.assign(results, assignments, totals);
}),
};
formatHeadings = () => {
let headings = [...this.props.headings];
if (headings.length > 0) {
const headerLabelReplacements = {};
headerLabelReplacements[USERNAME_HEADING] = (
<div>
<div>Username</div>
<div className="font-weight-normal student-key">Student Key*</div>
</div>
);
headerLabelReplacements[EMAIL_HEADING] = 'Email*';
const totalGradePercentageMessage = 'Total Grade values are always displayed as a percentage.';
headerLabelReplacements[TOTAL_COURSE_GRADE_HEADING] = (
<div>
<OverlayTrigger
trigger={['hover', 'focus']}
key="left-basic"
placement="left"
overlay={(<Tooltip id="course-grade-tooltip">{totalGradePercentageMessage}</Tooltip>)}
>
<div>
{TOTAL_COURSE_GRADE_HEADING}
<div id="courseGradeTooltipIcon">
<Icon className="fa fa-info-circle" screenReaderText={totalGradePercentageMessage} />
</div>
</div>
</OverlayTrigger>
</div>
);
headings = headings.map(heading => {
const result = {
label: heading,
key: heading,
};
if (headerLabelReplacements[heading] !== undefined) {
result.label = headerLabelReplacements[heading];
}
return result;
});
}
return headings;
}
render() {
return (
<div className="gradebook-container">
<div className="gbook">
<Table
columns={this.formatHeadings()}
data={this.formatter[this.props.format](
this.props.grades,
this.props.areGradesFrozen,
)}
rowHeaderColumnKey="username"
hasFixedColumnWidths
/>
</div>
</div>
);
}
}
GradebookTable.defaultProps = {
areGradesFrozen: false,
grades: [],
};
GradebookTable.propTypes = {
setGradebookState: PropTypes.func.isRequired,
// redux
areGradesFrozen: PropTypes.bool,
format: PropTypes.string.isRequired,
grades: PropTypes.arrayOf(PropTypes.shape({
percent: PropTypes.number,
section_breakdown: PropTypes.arrayOf(PropTypes.shape({
attempted: PropTypes.bool,
category: PropTypes.string,
label: PropTypes.string,
module_id: PropTypes.string,
percent: PropTypes.number,
scoreEarned: PropTypes.number,
scorePossible: PropTypes.number,
subsection_name: PropTypes.string,
})),
user_id: PropTypes.number,
user_name: PropTypes.string,
})),
headings: PropTypes.arrayOf(PropTypes.string).isRequired,
fetchGradeOverrideHistory: PropTypes.func.isRequired,
};
export const mapStateToProps = (state) => {
const { assignmentTypes, grades, root } = selectors;
return {
areGradesFrozen: assignmentTypes.areGradesFrozen(state),
format: grades.gradeFormat(state),
grades: grades.allGrades(state),
headings: root.getHeadings(state),
};
};
export const mapDispatchToProps = {
fetchGradeOverrideHistory,
};
export default connect(mapStateToProps, mapDispatchToProps)(GradebookTable);

View File

@@ -1,114 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Button, Icon, SearchField } from '@edx/paragon';
import selectors from 'data/selectors';
import {
fetchGrades,
fetchMatchingUserGrades,
} from '../../data/actions/grades';
/**
* Controls for filtering the GradebookTable. Contains the "Edit Filters" button for opening the filter drawer
* as well as the search box for searching by username/email.
*/
export class SearchControls extends React.Component {
constructor(props) {
super(props);
this.onSubmit = this.onSubmit.bind(this);
this.onChange = this.onChange.bind(this);
this.onClear = this.onClear.bind(this);
}
/** Submitting searches for user matching the username/email in `value` */
onSubmit(value) {
this.props.searchForUser(
this.props.courseId,
value,
this.props.selectedCohort,
this.props.selectedTrack,
this.props.selectedAssignmentType,
);
}
/** Changing the search value stores the key in Gradebook. Currently unused */
onChange(filterValue) {
this.props.setFilterValue(filterValue);
}
/** Clearing the search box falls back to showing students with already applied filters */
onClear() {
this.props.getUserGrades(
this.props.courseId,
this.props.selectedCohort,
this.props.selectedTrack,
this.props.selectedAssignmentType,
);
}
render() {
return (
<>
<h4>Step 1: Filter the Grade Report</h4>
<div className="d-flex justify-content-between">
<Button
id="edit-filters-btn"
className="btn-primary align-self-start"
onClick={this.props.toggleFilterDrawer}
>
<Icon className="fa fa-filter" /> Edit Filters
</Button>
<div>
<SearchField
onSubmit={this.onSubmit}
inputLabel="Search for a learner"
onChange={this.onChange}
onClear={this.onClear}
value={this.props.filterValue}
/>
<small className="form-text text-muted search-help-text">Search by username, email, or student key</small>
</div>
</div>
</>
);
}
}
SearchControls.defaultProps = {
courseId: '',
filterValue: '',
selectedAssignmentType: '',
selectedCohort: null,
selectedTrack: null,
};
SearchControls.propTypes = {
courseId: PropTypes.string,
filterValue: PropTypes.string,
setFilterValue: PropTypes.func.isRequired,
toggleFilterDrawer: PropTypes.func.isRequired,
// From Redux
getUserGrades: PropTypes.func.isRequired,
searchForUser: PropTypes.func.isRequired,
selectedAssignmentType: PropTypes.string,
selectedCohort: PropTypes.string,
selectedTrack: PropTypes.string,
};
export const mapStateToProps = (state) => {
const { filters } = selectors;
return {
selectedAssignmentType: filters.assignmentType(state),
selectedTrack: filters.track(state),
selectedCohort: filters.cohort(state),
};
};
export const mapDispatchToProps = {
getUserGrades: fetchGrades,
searchForUser: fetchMatchingUserGrades,
};
export default connect(mapStateToProps, mapDispatchToProps)(SearchControls);

View File

@@ -1,118 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import {
fetchGrades,
fetchMatchingUserGrades,
} from '../../data/actions/grades';
import { mapDispatchToProps, mapStateToProps, SearchControls } from './SearchControls';
jest.mock('@edx/paragon', () => ({
Icon: 'Icon',
Button: 'Button',
SearchField: 'SearchField',
}));
describe('SearchControls', () => {
let props;
beforeEach(() => {
jest.resetAllMocks();
props = {
courseId: 'course-v1:edX+DEV101+T1',
filterValue: 'alice',
selectedAssignmentType: 'homework',
selectedCohort: 'spring term',
selectedTrack: 'masters',
getUserGrades: jest.fn(),
searchForUser: jest.fn(),
setFilterValue: jest.fn(),
toggleFilterDrawer: jest.fn().mockName('toggleFilterDrawer'),
};
});
const searchControls = (overriddenProps) => {
props = { ...props, ...overriddenProps };
return shallow(<SearchControls {...props} />);
};
describe('Component', () => {
describe('onSubmit', () => {
it('calls props.searchForUser with correct data', () => {
const wrapper = searchControls();
wrapper.instance().onSubmit('bob');
expect(props.searchForUser).toHaveBeenCalledWith(
props.courseId,
'bob',
props.selectedCohort,
props.selectedTrack,
props.selectedAssignmentType,
);
});
});
describe('onChange', () => {
it('saves the changed search value to Gradebook state', () => {
const wrapper = searchControls();
wrapper.instance().onChange('bob');
expect(props.setFilterValue).toHaveBeenCalledWith('bob');
});
});
describe('onClear', () => {
it('re-runs search with existing filters', () => {
const wrapper = searchControls();
wrapper.instance().onClear();
expect(props.getUserGrades).toHaveBeenCalledWith(
props.courseId,
props.selectedCohort,
props.selectedTrack,
props.selectedAssignmentType,
);
});
});
describe('mapStateToProps', () => {
const state = {
filters: {
assignmentType: 'labs',
track: 'honor',
cohort: 'fall term',
},
};
it('maps assignment type filter correctly', () => {
expect(mapStateToProps(state).selectedAssignmentType).toEqual(state.filters.assignmentType);
});
it('maps track filter correctly', () => {
expect(mapStateToProps(state).selectedTrack).toEqual(state.filters.track);
});
it('maps cohort filter correctly', () => {
expect(mapStateToProps(state).selectedCohort).toEqual(state.filters.cohort);
});
});
describe('mapDispatchToProps', () => {
test('getUserGrades', () => {
expect(mapDispatchToProps.getUserGrades).toEqual(fetchGrades);
});
test('searchForUser', () => {
expect(mapDispatchToProps.searchForUser).toEqual(fetchMatchingUserGrades);
});
});
describe('Snapshots', () => {
test('basic snapshot', () => {
const wrapper = searchControls();
wrapper.instance().onChange = jest.fn().mockName('onChange');
wrapper.instance().onClear = jest.fn().mockName('onClear');
wrapper.instance().onSubmit = jest.fn().mockName('onSubmit');
expect(wrapper.instance().render()).toMatchSnapshot();
});
});
});
});

View File

@@ -1,99 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import {
StatusAlerts,
mapDispatchToProps,
mapStateToProps,
maxCourseGradeInvalidMessage,
minCourseGradeInvalidMessage,
} from './StatusAlerts';
import { closeBanner } from '../../data/actions/grades';
jest.mock('@edx/paragon', () => ({
StatusAlert: 'StatusAlert',
}));
describe('StatusAlerts', () => {
let props = {
showSuccessBanner: true,
isMaxCourseGradeFilterValid: true,
isMinCourseGradeFilterValid: true,
};
beforeEach(() => {
props = {
...props,
handleCloseSuccessBanner: jest.fn().mockName('handleCloseSuccessBanner'),
};
});
describe('snapshots', () => {
let el;
it('basic snapshot', () => {
el = shallow(<StatusAlerts {...props} />);
const courseGradeFilterAlertDialogText = 'the quiCk brown does somEthing or other';
jest.spyOn(
el.instance(),
'courseGradeFilterAlertDialogText',
'get',
).mockReturnValue(courseGradeFilterAlertDialogText);
expect(el.instance().render()).toMatchSnapshot();
});
});
describe('behavior', () => {
it.each([
[false, false],
[false, true],
[true, false],
[true, true],
])('min + max course grade validity', (isMinCourseGradeFilterValid, isMaxCourseGradeFilterValid) => {
props = {
...props,
isMinCourseGradeFilterValid,
isMaxCourseGradeFilterValid,
};
const el = shallow(<StatusAlerts {...props} />);
expect(
el.instance().isCourseGradeFilterAlertOpen,
).toEqual(
!isMinCourseGradeFilterValid || !isMaxCourseGradeFilterValid,
);
if (!isMaxCourseGradeFilterValid) {
expect(
el.instance().courseGradeFilterAlertDialogText,
).toEqual(
expect.stringContaining(maxCourseGradeInvalidMessage),
);
}
if (!isMinCourseGradeFilterValid) {
expect(
el.instance().courseGradeFilterAlertDialogText,
).toEqual(
expect.stringContaining(minCourseGradeInvalidMessage),
);
}
});
});
describe('mapStateToProps', () => {
it('showSuccessBanner', () => {
const arbitraryValue = 'AppleBananaCucumber';
const state = {
grades: {
showSuccess: arbitraryValue,
},
};
expect(mapStateToProps(state).showSuccessBanner).toBe(arbitraryValue);
});
});
describe('handleCloseSuccessBanner', () => {
test('handleCloseSuccessBanner', () => {
expect(
mapDispatchToProps.handleCloseSuccessBanner,
).toEqual(
closeBanner,
);
});
});
});

View File

@@ -1,37 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SearchControls Component Snapshots basic snapshot 1`] = `
<React.Fragment>
<h4>
Step 1: Filter the Grade Report
</h4>
<div
className="d-flex justify-content-between"
>
<Button
className="btn-primary align-self-start"
id="edit-filters-btn"
onClick={[MockFunction toggleFilterDrawer]}
>
<Icon
className="fa fa-filter"
/>
Edit Filters
</Button>
<div>
<SearchField
inputLabel="Search for a learner"
onChange={[MockFunction onChange]}
onClear={[MockFunction onClear]}
onSubmit={[MockFunction onSubmit]}
value="alice"
/>
<small
className="form-text text-muted search-help-text"
>
Search by username, email, or student key
</small>
</div>
</div>
</React.Fragment>
`;

View File

@@ -1,319 +0,0 @@
/* eslint-disable react/sort-comp, react/button-has-type, import/no-named-as-default */
import React from 'react';
import PropTypes from 'prop-types';
import {
Icon,
InputSelect,
Tab,
Tabs,
} from '@edx/paragon';
import queryString from 'query-string';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faFilter } from '@fortawesome/free-solid-svg-icons';
import { configuration } from '../../config';
import PageButtons from '../PageButtons';
import Drawer from '../Drawer';
import initialFilters from '../../data/constants/filters';
import ConnectedFilterBadges from '../FilterBadges';
import BulkManagement from './BulkManagement';
import BulkManagementControls from './BulkManagementControls';
import EditModal from './EditModal';
import GradebookFilters from './GradebookFilters';
import GradebookTable from './GradebookTable';
import SearchControls from './SearchControls';
import StatusAlerts from './StatusAlerts';
export default class Gradebook extends React.Component {
constructor(props) {
super(props);
this.state = {
adjustedGradePossible: '',
adjustedGradeValue: 0,
assignmentGradeMin: '0',
assignmentGradeMax: '100',
assignmentName: '',
courseGradeMin: '0',
courseGradeMax: '100',
filterValue: '',
isMinCourseGradeFilterValid: true,
isMaxCourseGradeFilterValid: true,
modalOpen: false,
reasonForChange: '',
todaysDate: '',
updateModuleId: null,
updateUserId: null,
};
this.myRef = React.createRef();
}
componentDidMount() {
const urlQuery = queryString.parse(this.props.location.search);
this.props.initializeFilters(urlQuery);
this.props.getRoles(this.props.courseId);
const newStateFields = {};
['assignmentGradeMin', 'assignmentGradeMax', 'courseGradeMin', 'courseGradeMax'].forEach((attr) => {
if (urlQuery[attr]) {
newStateFields[attr] = urlQuery[attr];
}
});
this.setState(newStateFields);
}
onChange(e) {
this.setState({ [e.target.name]: e.target.value });
}
getActiveTabs = () => {
if (this.props.showBulkManagement) {
return ['Grades', 'Bulk Management'];
}
return ['Grades'];
};
updateQueryParams = (queryParams) => {
const parsed = queryString.parse(this.props.location.search);
Object.keys(queryParams).forEach((key) => {
if (queryParams[key]) {
parsed[key] = queryParams[key];
} else {
delete parsed[key];
}
});
this.props.history.push(`?${queryString.stringify(parsed)}`);
};
lmsInstructorDashboardUrl = courseId => `${configuration.LMS_BASE_URL}/courses/${courseId}/instructor`;
handleFilterBadgeClose = filterNames => () => {
this.props.resetFilters(filterNames);
const queryParams = {};
filterNames.forEach((filterName) => {
queryParams[filterName] = false;
});
this.updateQueryParams(queryParams);
const stateUpdate = {};
const rangeStateFilters = ['assignmentGradeMin', 'assignmentGradeMax', 'courseGradeMin', 'courseGradeMax'];
rangeStateFilters.forEach((filterName) => {
if (filterNames.includes(filterName)) {
stateUpdate[filterName] = initialFilters[filterName];
}
});
this.setState(stateUpdate);
this.props.getUserGrades(
this.props.courseId,
filterNames.includes('cohort') ? initialFilters.cohort : this.props.selectedCohort,
filterNames.includes('track') ? initialFilters.track : this.props.selectedTrack,
filterNames.includes('assignmentType') ? initialFilters.assignmentType : this.props.selectedAssignmentType,
);
}
createStateFieldSetter = (key) => (value) => this.setState({ [key]: value });
createStateFieldOnChange = (key) => ({ target }) => this.setState({ [key]: target.value });
createLimitedSetter = (...keys) => (values) => this.setState(
keys.reduce(
(obj, key) => (values[key] === undefined ? obj : { ...obj, [key]: values[key] }),
{},
),
)
safeSetState = this.createLimitedSetter(
'adjustedGradePossible',
'adjustedGradeValue',
'assignmentName',
'filterValue',
'modalOpen',
'reasonForChange',
'todaysDate',
'updateModuleId',
'updateUserId',
'updateUserName',
);
setFilters = this.createLimitedSetter(
'assignmentGradeMin',
'assignmentGradeMax',
'courseGradeMin',
'courseGradeMax',
'isMinCourseGradeFilterValid',
'isMaxCourseGradeFilterValid',
);
filterValues = () => ({
assignmentGradeMin: this.state.assignmentGradeMin,
assignmentGradeMax: this.state.assignmentGradeMax,
courseGradeMin: this.state.courseGradeMin,
courseGradeMax: this.state.courseGradeMax,
});
render() {
return (
<Drawer
mainContent={toggleFilterDrawer => (
<div className="px-3 gradebook-content">
<a
href={this.lmsInstructorDashboardUrl(this.props.courseId)}
className="mb-3"
>
<span aria-hidden="true">{'<< '}</span> Back to Dashboard
</a>
<h1>Gradebook</h1>
<h3> {this.props.courseId}</h3>
{this.props.areGradesFrozen
&& (
<div className="alert alert-warning" role="alert">
The grades for this course are now frozen. Editing of grades is no longer allowed.
</div>
)}
{(this.props.canUserViewGradebook === false)
&& (
<div className="alert alert-warning" role="alert">
You are not authorized to view the gradebook for this course.
</div>
)}
<Tabs defaultActiveKey="grades">
<Tab eventKey="grades" title="Grades">
{this.props.showSpinner && (
<div className="spinner-overlay">
<Icon className="fa fa-spinner fa-spin fa-5x color-black" />
</div>
)}
<SearchControls
courseId={this.props.courseId}
filterValue={this.state.filterValue}
setFilterValue={this.createStateFieldSetter('filterValue')}
toggleFilterDrawer={toggleFilterDrawer}
/>
<ConnectedFilterBadges
handleFilterBadgeClose={this.handleFilterBadgeClose}
/>
<StatusAlerts
isMinCourseGradeFilterValid={this.state.isMinCourseGradeFilterValid}
isMaxCourseGradeFilterValid={this.state.isMaxCourseGradeFilterValid}
/>
<h4>Step 2: View or Modify Individual Grades</h4>
{this.props.totalUsersCount
? (
<div>
Showing
<span className="font-weight-bold"> {this.props.filteredUsersCount} </span>
of
<span className="font-weight-bold"> {this.props.totalUsersCount} </span>
total learners
</div>
)
: null}
<div className="d-flex justify-content-between align-items-center mb-2">
<InputSelect
label="Score View:"
name="ScoreView"
value="percent"
options={[{ label: 'Percent', value: 'percent' }, { label: 'Absolute', value: 'absolute' }]}
onChange={this.props.toggleFormat}
/>
{this.props.showBulkManagement && (
<BulkManagementControls
courseId={this.props.courseId}
gradeExportUrl={this.props.gradeExportUrl}
interventionExportUrl={this.props.interventionExportUrl}
showSpinner={this.props.showSpinner}
/>
)}
</div>
<GradebookTable setGradebookState={this.safeSetState} />
{PageButtons(this.props)}
<p>* available for learners in the Master&apos;s track only</p>
<EditModal
assignmentName={this.state.assignmentName}
adjustedGradeValue={this.state.adjustedGradeValue}
adjustedGradePossible={this.state.adjustedGradePossible}
courseId={this.props.courseId}
filterValue={this.state.filterValue}
onChange={this.onChange}
open={this.state.modalOpen}
reasonForChange={this.state.reasonForChange}
setAdjustedGradeValue={this.createStateFieldOnChange('adjustedGradeValue')}
setGradebookState={this.safeSetState}
setReasonForChange={this.createStateFieldOnChange('reasonForChange')}
todaysDate={this.state.todaysDate}
updateModuleId={this.state.updateModuleId}
updateUserId={this.state.updateUserId}
updateUserName={this.state.updateUserName}
/>
</Tab>
{this.props.showBulkManagement
&& (
<Tab eventKey="bulk_management" title="Bulk Management">
<BulkManagement
courseId={this.props.courseId}
gradeExportUrl={this.props.gradeExportUrl}
/>
</Tab>
)}
</Tabs>
</div>
)}
initiallyOpen={false}
title={(
<>
<FontAwesomeIcon icon={faFilter} /> Filter By...
</>
)}
>
<GradebookFilters
setFilters={this.setFilters}
filterValues={this.filterValues()}
updateQueryParams={this.updateQueryParams}
courseId={this.props.courseId}
/>
</Drawer>
);
}
}
Gradebook.defaultProps = {
areGradesFrozen: false,
canUserViewGradebook: false,
courseId: '',
filteredUsersCount: null,
location: {
search: '',
},
selectedAssignmentType: '',
selectedCohort: null,
selectedTrack: null,
showBulkManagement: false,
showSpinner: false,
totalUsersCount: null,
};
Gradebook.propTypes = {
areGradesFrozen: PropTypes.bool,
canUserViewGradebook: PropTypes.bool,
courseId: PropTypes.string,
filteredUsersCount: PropTypes.number,
getRoles: PropTypes.func.isRequired,
getUserGrades: PropTypes.func.isRequired,
gradeExportUrl: PropTypes.string.isRequired,
history: PropTypes.shape({
push: PropTypes.func,
}).isRequired,
initializeFilters: PropTypes.func.isRequired,
interventionExportUrl: PropTypes.string.isRequired,
location: PropTypes.shape({
search: PropTypes.string,
}),
resetFilters: PropTypes.func.isRequired,
selectedAssignmentType: PropTypes.string,
selectedCohort: PropTypes.string,
selectedTrack: PropTypes.string,
showBulkManagement: PropTypes.bool,
showSpinner: PropTypes.bool,
toggleFormat: PropTypes.func.isRequired,
totalUsersCount: PropTypes.number,
};

View File

@@ -7,7 +7,13 @@ exports[`AssignmentFilter Component snapshots basic snapshot 1`] = `
<SelectGroup
disabled={false}
id="assignment"
label="Assignment"
label={
<FormattedMessage
defaultMessage="Assignment"
description="Assignment filter select label in Gradebook Filters"
id="gradebook.GradebookFilters.assignmentFilterLabel"
/>
}
onChange={[MockFunction handleChange]}
options={
Array [

View File

@@ -3,12 +3,17 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import * as gradesActions from 'data/actions/grades';
import * as filterActions from 'data/actions/filters';
import selectors from 'data/selectors';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import selectors from 'data/selectors';
import actions from 'data/actions';
import thunkActions from 'data/thunkActions';
import messages from '../messages';
import SelectGroup from '../SelectGroup';
const { fetchGradesIfAssignmentGradeFiltersSet } = thunkActions.grades;
export class AssignmentFilter extends React.Component {
constructor(props) {
super(props);
@@ -17,17 +22,14 @@ export class AssignmentFilter extends React.Component {
handleChange(event) {
const assignment = event.target.value;
const selectedFilterOption = this.props.assignmentFilterOptions.find(assig => assig.label === assignment);
const selectedFilterOption = this.props.assignmentFilterOptions.find(
({ label }) => label === assignment,
);
const { type, id } = selectedFilterOption || {};
const typedValue = { label: assignment, type, id };
this.props.updateAssignmentFilter(typedValue);
this.props.updateQueryParams({ assignment: id });
this.props.updateGradesIfAssignmentGradeFiltersSet(
this.props.courseId,
this.props.selectedCohort,
this.props.selectedTrack,
this.props.selectedAssignmentType,
);
this.props.fetchGradesIfAssignmentGradeFiltersSet();
}
get options() {
@@ -47,7 +49,7 @@ export class AssignmentFilter extends React.Component {
<div className="student-filters">
<SelectGroup
id="assignment"
label="Assignment"
label={<FormattedMessage {...messages.assignment} />}
value={this.props.selectedAssignment}
onChange={this.handleChange}
disabled={this.props.assignmentFilterOptions.length === 0}
@@ -61,15 +63,10 @@ export class AssignmentFilter extends React.Component {
AssignmentFilter.defaultProps = {
assignmentFilterOptions: [],
selectedAssignment: '',
selectedAssignmentType: '',
selectedCohort: null,
selectedTrack: null,
};
AssignmentFilter.propTypes = {
courseId: PropTypes.string.isRequired,
updateQueryParams: PropTypes.func.isRequired,
// redux
assignmentFilterOptions: PropTypes.arrayOf(PropTypes.shape({
label: PropTypes.string,
@@ -77,11 +74,8 @@ AssignmentFilter.propTypes = {
type: PropTypes.string,
id: PropTypes.string,
})),
selectedAssignmentType: PropTypes.string,
selectedAssignment: PropTypes.string,
selectedCohort: PropTypes.string,
selectedTrack: PropTypes.string,
updateGradesIfAssignmentGradeFiltersSet: PropTypes.func.isRequired,
fetchGradesIfAssignmentGradeFiltersSet: PropTypes.func.isRequired,
updateAssignmentFilter: PropTypes.func.isRequired,
};
@@ -97,8 +91,8 @@ export const mapStateToProps = (state) => {
};
export const mapDispatchToProps = {
updateAssignmentFilter: filterActions.updateAssignmentFilter,
updateGradesIfAssignmentGradeFiltersSet: gradesActions.updateGradesIfAssignmentGradeFiltersSet,
updateAssignmentFilter: actions.filters.update.assignment,
fetchGradesIfAssignmentGradeFiltersSet,
};
export default connect(mapStateToProps, mapDispatchToProps)(AssignmentFilter);

View File

@@ -2,14 +2,18 @@ import React from 'react';
import { mount, shallow } from 'enzyme';
import selectors from 'data/selectors';
import { updateAssignmentFilter } from 'data/actions/filters';
import { updateGradesIfAssignmentGradeFiltersSet } from 'data/actions/grades';
import actions from 'data/actions';
import { fetchGradesIfAssignmentGradeFiltersSet } from 'data/thunkActions/grades';
import {
AssignmentFilter,
mapStateToProps,
mapDispatchToProps,
} from '.';
jest.mock('data/thunkActions/grades', () => ({
updateGradesIfAssignmentGradeFiltersSet: jest.fn(),
}));
jest.mock('data/selectors', () => ({
/** Mocking to use passed state for validation purposes */
filters: {
@@ -28,7 +32,6 @@ jest.mock('data/selectors', () => ({
describe('AssignmentFilter', () => {
let props = {
courseId: '12345',
assignmentFilterOptions: [
{
label: 'assgN1',
@@ -43,17 +46,14 @@ describe('AssignmentFilter', () => {
id: 'assgn_iD2',
},
],
selectedAssignmentType: 'assgnFilterLabel1',
selectedAssignment: 'assgN1',
selectedCohort: 'a cohort',
selectedTrack: 'a track',
};
beforeEach(() => {
props = {
...props,
updateQueryParams: jest.fn(),
updateGradesIfAssignmentGradeFiltersSet: jest.fn(),
fetchGradesIfAssignmentGradeFiltersSet: jest.fn(),
updateAssignmentFilter: jest.fn(),
};
});
@@ -69,7 +69,6 @@ describe('AssignmentFilter', () => {
el = mount(<AssignmentFilter {...props} />);
el.instance().handleChange(event);
});
it('calls props.updateAssignmentFilter with selection', () => {
expect(props.updateAssignmentFilter).toHaveBeenCalledWith({
label: newAssgn,
@@ -83,14 +82,33 @@ describe('AssignmentFilter', () => {
assignment: selected.id,
});
});
it('calls props.updateGradesIfAssignmentGradeFiltersSet', () => {
const method = props.updateGradesIfAssignmentGradeFiltersSet;
expect(method).toHaveBeenCalledWith(
props.courseId,
props.selectedCohort,
props.selectedTrack,
props.selectedAssignmentType,
);
it('calls props.fetchGradesIfAssignmentGradeFiltersSet', () => {
const method = props.fetchGradesIfAssignmentGradeFiltersSet;
expect(method).toHaveBeenCalledWith();
});
describe('no selected option', () => {
const value = 'fake';
beforeEach(() => {
el = mount(<AssignmentFilter {...props} />);
el.instance().handleChange({ target: { value } });
});
it('calls props.updateAssignmentFilter with selection', () => {
expect(props.updateAssignmentFilter).toHaveBeenCalledWith({
label: value,
type: undefined,
id: undefined,
});
});
it('calls props.updateQueryParams with selected assignment id',
() => {
expect(props.updateQueryParams).toHaveBeenCalledWith({
assignment: undefined,
});
});
it('calls props.fetchGradesIfAssignmentGradeFiltersSet', () => {
const method = props.fetchGradesIfAssignmentGradeFiltersSet;
expect(method).toHaveBeenCalledWith();
});
});
});
});
@@ -129,43 +147,16 @@ describe('AssignmentFilter', () => {
);
});
});
describe('selectedAssignmentType', () => {
it('is selected from filters.assignmentType', () => {
expect(
mapStateToProps(state).selectedAssignmentType,
).toEqual(
selectors.filters.assignmentType(state),
);
});
});
describe('selectedCohort', () => {
it('is selected from filters.cohort', () => {
expect(
mapStateToProps(state).selectedCohort,
).toEqual(
selectors.filters.cohort(state),
);
});
});
describe('selectedTrack', () => {
it('is selected from filters.track', () => {
expect(
mapStateToProps(state).selectedTrack,
).toEqual(
selectors.filters.track(state),
);
});
});
});
describe('mapDispatchToProps', () => {
test('updateAssignmentFilter', () => {
expect(mapDispatchToProps.updateAssignmentFilter).toEqual(
updateAssignmentFilter,
actions.filters.update.assignment,
);
});
test('updateGradesIfAsssignmentGradeFiltersSet', () => {
const prop = mapDispatchToProps.updateGradesIfAssignmentGradeFiltersSet;
expect(prop).toEqual(updateGradesIfAssignmentGradeFiltersSet);
test('fetchGradesIfAsssignmentGradeFiltersSet', () => {
const prop = mapDispatchToProps.fetchGradesIfAssignmentGradeFiltersSet;
expect(prop).toEqual(fetchGradesIfAssignmentGradeFiltersSet);
});
});
});

View File

@@ -7,21 +7,33 @@ exports[`AssignmentGradeFilter Component snapshots buttons and groups disabled i
<PercentGroup
disabled={true}
id="assignmentGradeMin"
label="Min Grade"
label={
<FormattedMessage
defaultMessage="Min Grade"
description="Min-grade filter select label in Gradebook Filters"
id="gradebook.GradebookFilters.minGradeFilterLabel"
/>
}
onChange={[MockFunction handleSetMin]}
value="1"
value="2"
/>
<PercentGroup
disabled={true}
id="assignmentGradeMax"
label="Max Grade"
label={
<FormattedMessage
defaultMessage="Max Grade"
description="Max-grade filter select label in Gradebook Filters"
id="gradebook.GradebookFilters.maxGradeFilterLabel"
/>
}
onChange={[MockFunction handleSetMax]}
value="100"
value="98"
/>
<div
className="grade-filter-action"
>
<Button
<ForwardRef
active={false}
disabled={true}
name="assignmentGradeMinMax"
@@ -30,7 +42,7 @@ exports[`AssignmentGradeFilter Component snapshots buttons and groups disabled i
variant="outline-secondary"
>
Apply
</Button>
</ForwardRef>
</div>
</div>
`;
@@ -42,21 +54,33 @@ exports[`AssignmentGradeFilter Component snapshots smoke test 1`] = `
<PercentGroup
disabled={false}
id="assignmentGradeMin"
label="Min Grade"
label={
<FormattedMessage
defaultMessage="Min Grade"
description="Min-grade filter select label in Gradebook Filters"
id="gradebook.GradebookFilters.minGradeFilterLabel"
/>
}
onChange={[MockFunction handleSetMin]}
value="1"
value="2"
/>
<PercentGroup
disabled={false}
id="assignmentGradeMax"
label="Max Grade"
label={
<FormattedMessage
defaultMessage="Max Grade"
description="Max-grade filter select label in Gradebook Filters"
id="gradebook.GradebookFilters.maxGradeFilterLabel"
/>
}
onChange={[MockFunction handleSetMax]}
value="100"
value="98"
/>
<div
className="grade-filter-action"
>
<Button
<ForwardRef
active={false}
disabled={false}
name="assignmentGradeMinMax"
@@ -65,7 +89,7 @@ exports[`AssignmentGradeFilter Component snapshots smoke test 1`] = `
variant="outline-secondary"
>
Apply
</Button>
</ForwardRef>
</div>
</div>
`;

View File

@@ -0,0 +1,103 @@
/* eslint-disable react/sort-comp, react/button-has-type */
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Button } from '@edx/paragon';
import selectors from 'data/selectors';
import actions from 'data/actions';
import thunkActions from 'data/thunkActions';
import messages from '../messages';
import PercentGroup from '../PercentGroup';
export class AssignmentGradeFilter extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleSetMax = this.handleSetMax.bind(this);
this.handleSetMin = this.handleSetMin.bind(this);
}
handleSubmit() {
this.props.updateAssignmentLimits(this.props.localAssignmentLimits);
this.props.fetchGrades();
this.props.updateQueryParams(this.props.localAssignmentLimits);
}
handleSetMax({ target: { value } }) {
this.props.setFilter({ assignmentGradeMax: value });
}
handleSetMin({ target: { value } }) {
this.props.setFilter({ assignmentGradeMin: value });
}
render() {
const {
localAssignmentLimits: { assignmentGradeMax, assignmentGradeMin },
} = this.props;
return (
<div className="grade-filter-inputs">
<PercentGroup
id="assignmentGradeMin"
label={<FormattedMessage {...messages.minGrade} />}
value={assignmentGradeMin}
disabled={!this.props.selectedAssignment}
onChange={this.handleSetMin}
/>
<PercentGroup
id="assignmentGradeMax"
label={<FormattedMessage {...messages.maxGrade} />}
value={assignmentGradeMax}
disabled={!this.props.selectedAssignment}
onChange={this.handleSetMax}
/>
<div className="grade-filter-action">
<Button
type="submit"
variant="outline-secondary"
name="assignmentGradeMinMax"
disabled={!this.props.selectedAssignment}
onClick={this.handleSubmit}
>
Apply
</Button>
</div>
</div>
);
}
}
AssignmentGradeFilter.defaultProps = {
selectedAssignment: '',
};
AssignmentGradeFilter.propTypes = {
updateQueryParams: PropTypes.func.isRequired,
// redux
fetchGrades: PropTypes.func.isRequired,
localAssignmentLimits: PropTypes.shape({
assignmentGradeMax: PropTypes.string,
assignmentGradeMin: PropTypes.string,
}).isRequired,
selectedAssignment: PropTypes.string,
setFilter: PropTypes.func.isRequired,
updateAssignmentLimits: PropTypes.func.isRequired,
};
export const mapStateToProps = (state) => ({
localAssignmentLimits: selectors.app.assignmentGradeLimits(state),
selectedAssignment: selectors.filters.selectedAssignmentLabel(state),
});
export const mapDispatchToProps = {
fetchGrades: thunkActions.grades.fetchGrades,
setFilter: actions.app.setLocalFilter,
updateAssignmentLimits: actions.filters.update.assignmentLimits,
};
export default connect(mapStateToProps, mapDispatchToProps)(AssignmentGradeFilter);

View File

@@ -0,0 +1,143 @@
import React from 'react';
import { mount, shallow } from 'enzyme';
import actions from 'data/actions';
import selectors from 'data/selectors';
import { fetchGrades } from 'data/thunkActions/grades';
import {
AssignmentGradeFilter,
mapStateToProps,
mapDispatchToProps,
} from '.';
jest.mock('data/selectors', () => ({
__esModule: true,
default: {
app: {},
filters: {},
grades: {},
},
}));
jest.mock('data/thunkActions/grades', () => ({
fetchGrades: jest.fn(),
}));
describe('AssignmentGradeFilter', () => {
let props = {};
beforeEach(() => {
props = {
...props,
updateQueryParams: jest.fn(),
fetchGrades: jest.fn(),
localAssignmentLimits: {
assignmentGradeMax: '98',
assignmentGradeMin: '2',
},
selectedAssignment: 'Potions 101.5',
setFilter: jest.fn(),
updateAssignmentLimits: jest.fn(),
};
});
describe('Component', () => {
describe('behavior', () => {
let el;
beforeEach(() => {
el = mount(<AssignmentGradeFilter {...props} />);
});
describe('handleSubmit', () => {
beforeEach(() => {
el.instance().handleSubmit();
});
it('calls props.updateAssignmentLimits with local assignment limits', () => {
expect(props.updateAssignmentLimits).toHaveBeenCalledWith(props.localAssignmentLimits);
});
it('calls fetchUserGrades', () => {
expect(props.fetchGrades).toHaveBeenCalledWith();
});
it('updates queryParams with assignment grade min and max', () => {
expect(props.updateQueryParams).toHaveBeenCalledWith(props.localAssignmentLimits);
});
});
describe('handleSetMin', () => {
it('calls setFilters for assignmentGradeMin', () => {
const testVal = 23;
el.instance().handleSetMin({ target: { value: testVal } });
expect(props.setFilter).toHaveBeenCalledWith({
assignmentGradeMin: testVal,
});
});
});
describe('handleSetMax', () => {
it('calls setFilters for assignmentGradeMax', () => {
const testVal = 92;
el.instance().handleSetMax({ target: { value: testVal } });
expect(props.setFilter).toHaveBeenCalledWith({
assignmentGradeMax: testVal,
});
});
});
});
describe('snapshots', () => {
let el;
const mockMethods = () => {
el.instance().handleSubmit = jest.fn().mockName('handleSubmit');
el.instance().handleSetMax = jest.fn().mockName('handleSetMax');
el.instance().handleSetMin = jest.fn().mockName('handleSetMin');
};
test('smoke test', () => {
el = shallow(<AssignmentGradeFilter {...props} />);
mockMethods(el);
expect(el.instance().render()).toMatchSnapshot();
});
test('buttons and groups disabled if no selected assignment', () => {
el = shallow(<AssignmentGradeFilter
{...props}
selectedAssignment={undefined}
/>);
mockMethods(el);
expect(el.instance().render()).toMatchSnapshot();
});
});
});
describe('mapStateToProps', () => {
const testState = { belle: 'in', the: 'castle' };
let mappedProps;
beforeEach(() => {
selectors.app.assignmentGradeLimits = jest.fn((state) => ({ gradeLimits: state }));
selectors.filters.selectedAssignmentLabel = jest.fn((state) => ({ assignmentLabel: state }));
mappedProps = mapStateToProps(testState);
});
describe('localAssignmentLimits', () => {
it('returns selectors.app.assignmentGradeLimits', () => {
expect(
mappedProps.localAssignmentLimits,
).toEqual(selectors.app.assignmentGradeLimits(testState));
});
});
describe('selectedAsssignment', () => {
it('returns selectors.filters.selectedAssignmentLabel', () => {
expect(
mappedProps.selectedAssignment,
).toEqual(selectors.filters.selectedAssignmentLabel(testState));
});
});
});
describe('mapDispatchToProps', () => {
test('fetchGrades', () => {
expect(mapDispatchToProps.fetchGrades).toEqual(fetchGrades);
});
test('setFilters', () => {
expect(mapDispatchToProps.setFilter).toEqual(actions.app.setLocalFilter);
});
test('updateAssignmentLimits', () => {
expect(
mapDispatchToProps.updateAssignmentLimits,
).toEqual(
actions.filters.update.assignmentLimits,
);
});
});
});

View File

@@ -7,7 +7,13 @@ exports[`AssignmentTypeFilter Component snapshots SelectGroup disabled if no ass
<SelectGroup
disabled={true}
id="assignment-types"
label="Assignment Types"
label={
<FormattedMessage
defaultMessage="Assignment Types"
description="Assignment Types filter select label in Gradebook Filters"
id="gradebook.GradebookFilters.assignmentTypesLabel"
/>
}
onChange={[MockFunction handleChange]}
options={
Array [
@@ -40,7 +46,13 @@ exports[`AssignmentTypeFilter Component snapshots smoke test 1`] = `
<SelectGroup
disabled={false}
id="assignment-types"
label="Assignment Types"
label={
<FormattedMessage
defaultMessage="Assignment Types"
description="Assignment Types filter select label in Gradebook Filters"
id="gradebook.GradebookFilters.assignmentTypesLabel"
/>
}
onChange={[MockFunction handleChange]}
options={
Array [

View File

@@ -3,10 +3,13 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import * as gradesActions from 'data/actions/grades';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import selectors from 'data/selectors';
import actions from 'data/actions';
import SelectGroup from '../SelectGroup';
import messages from '../messages';
export class AssignmentTypeFilter extends React.Component {
constructor(props) {
@@ -35,7 +38,7 @@ export class AssignmentTypeFilter extends React.Component {
<div className="student-filters">
<SelectGroup
id="assignment-types"
label="Assignment Types"
label={<FormattedMessage {...messages.assignmentTypes} />}
value={this.props.selectedAssignmentType}
onChange={this.handleChange}
disabled={this.props.assignmentFilterOptions.length === 0}
@@ -72,7 +75,7 @@ export const mapStateToProps = (state) => ({
});
export const mapDispatchToProps = {
filterAssignmentType: gradesActions.filterAssignmentType,
filterAssignmentType: actions.filters.update.assignmentType,
};
export default connect(mapStateToProps, mapDispatchToProps)(AssignmentTypeFilter);

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { shallow } from 'enzyme';
import selectors from 'data/selectors';
import { filterAssignmentType } from 'data/actions/grades';
import actions from 'data/actions';
import {
AssignmentTypeFilter,
@@ -128,7 +128,7 @@ describe('AssignmentTypeFilter', () => {
describe('mapDispatchToProps', () => {
test('filterAssignmentType', () => {
expect(mapDispatchToProps.filterAssignmentType).toEqual(
filterAssignmentType,
actions.filters.update.assignmentType,
);
});
});

View File

@@ -6,16 +6,26 @@ exports[`CourseGradeFilter Component snapshots basic snapshot 1`] = `
className="grade-filter-inputs"
>
<PercentGroup
disabled={false}
id="minimum-grade"
label="Min Grade"
label={
<FormattedMessage
defaultMessage="Min Grade"
description="Min-grade filter select label in Gradebook Filters"
id="gradebook.GradebookFilters.minGradeFilterLabel"
/>
}
onChange={[MockFunction handleUpdateMin]}
value="5"
/>
<PercentGroup
disabled={false}
id="maximum-grade"
label="Max Grade"
label={
<FormattedMessage
defaultMessage="Max Grade"
description="Max-grade filter select label in Gradebook Filters"
id="gradebook.GradebookFilters.maxGradeFilterLabel"
/>
}
onChange={[MockFunction handleUpdateMax]}
value="92"
/>

View File

@@ -0,0 +1,103 @@
/* eslint-disable react/sort-comp, react/button-has-type, import/no-named-as-default */
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { Button } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import selectors from 'data/selectors';
import actions from 'data/actions';
import thunkActions from 'data/thunkActions';
import messages from '../messages';
import PercentGroup from '../PercentGroup';
export class CourseGradeFilter extends React.Component {
constructor(props) {
super(props);
this.handleApplyClick = this.handleApplyClick.bind(this);
this.handleUpdateMin = this.handleUpdateMin.bind(this);
this.handleUpdateMax = this.handleUpdateMax.bind(this);
this.updateCourseGradeFilters = this.updateCourseGradeFilters.bind(this);
}
handleApplyClick() {
if (this.props.areLimitsValid) {
this.updateCourseGradeFilters();
}
}
handleUpdateMin({ target: { value } }) {
this.props.setLocalFilter({ courseGradeMin: value });
}
handleUpdateMax({ target: { value } }) {
this.props.setLocalFilter({ courseGradeMax: value });
}
updateCourseGradeFilters() {
this.props.updateFilter(this.props.localCourseLimits);
this.props.fetchGrades();
this.props.updateQueryParams(this.props.localCourseLimits);
}
render() {
const {
localCourseLimits: { courseGradeMin, courseGradeMax },
} = this.props;
return (
<>
<div className="grade-filter-inputs">
<PercentGroup
id="minimum-grade"
label={<FormattedMessage {...messages.minGrade} />}
value={courseGradeMin}
onChange={this.handleUpdateMin}
/>
<PercentGroup
id="maximum-grade"
label={<FormattedMessage {...messages.maxGrade} />}
value={courseGradeMax}
onChange={this.handleUpdateMax}
/>
</div>
<div className="grade-filter-action">
<Button
variant="outline-secondary"
onClick={this.handleApplyClick}
>
Apply
</Button>
</div>
</>
);
}
}
CourseGradeFilter.propTypes = {
updateQueryParams: PropTypes.func.isRequired,
// Redux
areLimitsValid: PropTypes.bool.isRequired,
fetchGrades: PropTypes.func.isRequired,
localCourseLimits: PropTypes.shape({
courseGradeMin: PropTypes.string.isRequired,
courseGradeMax: PropTypes.string.isRequired,
}).isRequired,
setLocalFilter: PropTypes.func.isRequired,
updateFilter: PropTypes.func.isRequired,
};
export const mapStateToProps = (state) => ({
areLimitsValid: selectors.app.areCourseGradeFiltersValid(state),
localCourseLimits: selectors.app.courseGradeLimits(state),
});
export const mapDispatchToProps = {
fetchGrades: thunkActions.grades.fetchGrades,
setLocalFilter: actions.app.setLocalFilter,
updateFilter: actions.filters.update.courseGradeLimits,
};
export default connect(mapStateToProps, mapDispatchToProps)(CourseGradeFilter);

View File

@@ -0,0 +1,150 @@
/* eslint-disable import/no-named-as-default */
import React from 'react';
import { shallow } from 'enzyme';
import actions from 'data/actions';
import selectors from 'data/selectors';
import { fetchGrades } from 'data/thunkActions/grades';
import {
CourseGradeFilter,
mapStateToProps,
mapDispatchToProps,
} from '.';
jest.mock('@edx/paragon', () => ({
Button: () => 'Button',
}));
jest.mock('../PercentGroup', () => 'PercentGroup');
jest.mock('data/thunkActions/grades', () => ({
fetchGrades: jest.fn(),
}));
jest.mock('data/selectors', () => ({
__esModule: true,
default: {
app: {
areCourseGradeFiltersValid: jest.fn(state => ({ areCourseGradeFiltersValid: state })),
courseGradeLimits: jest.fn(state => ({ courseGradeLimits: state })),
},
},
}));
describe('CourseGradeFilter', () => {
let props = {
localCourseLimits: {
courseGradeMin: '5',
courseGradeMax: '92',
},
areLimitsValid: true,
};
beforeEach(() => {
props = {
...props,
fetchGrades: jest.fn(),
setLocalFilter: jest.fn(),
updateQueryParams: jest.fn(),
updateFilter: jest.fn(),
};
});
describe('Component', () => {
describe('snapshots', () => {
test('basic snapshot', () => {
const el = shallow(<CourseGradeFilter {...props} />);
el.instance().handleUpdateMin = jest.fn().mockName(
'handleUpdateMin',
);
el.instance().handleUpdateMax = jest.fn().mockName(
'handleUpdateMax',
);
el.instance().handleApplyClick = jest.fn().mockName(
'handleApplyClick',
);
expect(el.instance().render()).toMatchSnapshot();
});
});
describe('behavior', () => {
let el;
const testVal = 'TESTvalue';
beforeEach(() => {
el = shallow(<CourseGradeFilter {...props} />);
});
describe('handleApplyClick', () => {
beforeEach(() => {
el.instance().updateCourseGradeFilters = jest.fn();
});
it('calls updateCourseGradeFilters is limits are valid', () => {
el.instance().handleApplyClick();
expect(el.instance().updateCourseGradeFilters).toHaveBeenCalledWith();
});
it('does not call updateCourseGradeFilters if limits are not valid', () => {
el.setProps({ areLimitsValid: false });
el.instance().handleApplyClick();
expect(el.instance().updateCourseGradeFilters).not.toHaveBeenCalled();
});
});
describe('updateCourseGradeFilters', () => {
beforeEach(() => {
el.instance().updateCourseGradeFilters();
});
it('calls props.updateFilter with selection', () => {
expect(props.updateFilter).toHaveBeenCalledWith(props.localCourseLimits);
});
it('calls props.getUserGrades with selection', () => {
expect(props.fetchGrades).toHaveBeenCalledWith();
});
it('updates query params with courseGradeMin and courseGradeMax', () => {
expect(props.updateQueryParams).toHaveBeenCalledWith(props.localCourseLimits);
});
});
describe('handleUpdateMin', () => {
it('calls props.setCourseGradeMin with event value', () => {
el.instance().handleUpdateMin(
{ target: { value: testVal } },
);
expect(props.setLocalFilter).toHaveBeenCalledWith({
courseGradeMin: testVal,
});
});
});
describe('handleUpdateMax', () => {
it('calls props.setCourseGradeMax with event value', () => {
el.instance().handleUpdateMax(
{ target: { value: testVal } },
);
expect(props.setLocalFilter).toHaveBeenCalledWith({
courseGradeMax: testVal,
});
});
});
});
});
describe('mapStateToProps', () => {
const testState = { peanut: 'butter', jelly: 'time' };
let mapped;
beforeEach(() => {
mapped = mapStateToProps(testState);
});
test('areLimitsValid from app.areCourseGradeFiltersValid', () => {
expect(mapped.areLimitsValid).toEqual(selectors.app.areCourseGradeFiltersValid(testState));
});
test('localCourseLimits from app.courseGradeLimits', () => {
expect(mapped.localCourseLimits).toEqual(selectors.app.courseGradeLimits(testState));
});
});
describe('mapDispatchToProps', () => {
test('fetchGrades from thunkActions.grades.fetchGrades', () => {
expect(mapDispatchToProps.fetchGrades).toEqual(fetchGrades);
});
test('setLocalFilter from actions.app.setLocalFilter', () => {
expect(mapDispatchToProps.setLocalFilter).toEqual(actions.app.setLocalFilter);
});
test('updateFilter from actions.filters.update.courseGradeLimits', () => {
expect(mapDispatchToProps.updateFilter).toEqual(actions.filters.update.courseGradeLimits);
});
});
});

View File

@@ -0,0 +1,6 @@
.filter-sidebar-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 15px;
}

View File

@@ -30,7 +30,7 @@ PercentGroup.defaultProps = {
};
PercentGroup.propTypes = {
id: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
label: PropTypes.node.isRequired,
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
disabled: PropTypes.bool,

View File

@@ -23,7 +23,7 @@ const SelectGroup = ({
);
SelectGroup.propTypes = {
id: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
label: PropTypes.node.isRequired,
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
disabled: PropTypes.bool,

View File

@@ -47,7 +47,7 @@ exports[`StudentGroupsFilter Component snapshots Cohorts group disabled if no co
</option>,
]
}
value="Cohorts"
value="cohorT3"
/>
</React.Fragment>
`;
@@ -168,3 +168,23 @@ Array [
</option>,
]
`;
exports[`StudentGroupsFilter optionFactory returns a list of options with a default first entry 1`] = `
Array [
<option
value="All-Ponies"
>
All-Ponies
</option>,
<option
value="RDash"
>
RDash
</option>,
<option
value="PPie"
>
PPie
</option>,
]
`;

View File

@@ -0,0 +1,152 @@
/* eslint-disable react/sort-comp, react/button-has-type, import/no-named-as-default */
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import actions from 'data/actions';
import selectors from 'data/selectors';
import thunkActions from 'data/thunkActions';
import messages from '../messages';
import SelectGroup from '../SelectGroup';
export const optionFactory = ({ data, defaultOption, key }) => [
<option value={defaultOption} key="0">{defaultOption}</option>,
...data.map(
entry => (<option key={entry[key]} value={entry.name}>{entry.name}</option>),
),
];
export class StudentGroupsFilter extends React.Component {
constructor(props) {
super(props);
this.mapCohortsEntries = this.mapCohortsEntries.bind(this);
this.mapTracksEntries = this.mapTracksEntries.bind(this);
this.updateCohorts = this.updateCohorts.bind(this);
this.updateTracks = this.updateTracks.bind(this);
}
mapCohortsEntries() {
return optionFactory({
data: this.props.cohorts,
defaultOption: this.translate(messages.cohortAll),
key: 'id',
});
}
mapTracksEntries() {
return optionFactory({
data: this.props.tracks,
defaultOption: this.translate(messages.trackAll),
key: 'slug',
});
}
selectedTrackSlugFromEvent({ target: { value } }) {
const selectedTrackItem = this.props.tracksByName[value];
return selectedTrackItem ? selectedTrackItem.slug : null;
}
selectedCohortIdFromEvent({ target: { value } }) {
const selectedCohortItem = this.props.cohortsByName[value];
return selectedCohortItem ? selectedCohortItem.id.toString() : null;
}
updateTracks(event) {
const track = this.selectedTrackSlugFromEvent(event);
this.props.updateQueryParams({ track });
this.props.updateTrack(track);
this.props.fetchGrades();
}
updateCohorts(event) {
const cohort = this.selectedCohortIdFromEvent(event);
this.props.updateQueryParams({ cohort });
this.props.updateCohort(cohort);
this.props.fetchGrades();
}
translate(message) {
return this.props.intl.formatMessage(message);
}
render() {
return (
<>
<SelectGroup
id="Tracks"
label={this.translate(messages.tracks)}
value={this.props.selectedTrackEntry.name}
onChange={this.updateTracks}
options={this.mapTracksEntries()}
/>
<SelectGroup
id="Cohorts"
label={this.translate(messages.cohorts)}
value={this.props.selectedCohortEntry.name}
disabled={this.props.cohorts.length === 0}
onChange={this.updateCohorts}
options={this.mapCohortsEntries()}
/>
</>
);
}
}
StudentGroupsFilter.defaultProps = {
cohorts: [],
cohortsByName: {},
selectedCohortEntry: { name: '' },
selectedTrackEntry: { name: '' },
tracks: [],
tracksByName: {},
};
StudentGroupsFilter.propTypes = {
updateQueryParams: PropTypes.func.isRequired,
// injected
intl: intlShape.isRequired,
// redux
cohorts: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string,
id: PropTypes.number,
})),
cohortsByName: PropTypes.objectOf(PropTypes.shape({
name: PropTypes.string,
id: PropTypes.number,
})),
fetchGrades: PropTypes.func.isRequired,
selectedTrackEntry: PropTypes.shape({ name: PropTypes.string }),
selectedCohortEntry: PropTypes.shape({ name: PropTypes.string }),
tracks: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string,
slug: PropTypes.string,
})),
tracksByName: PropTypes.objectOf(PropTypes.shape({
name: PropTypes.string,
slug: PropTypes.string,
})),
updateCohort: PropTypes.func.isRequired,
updateTrack: PropTypes.func.isRequired,
};
export const mapStateToProps = (state) => ({
cohorts: selectors.cohorts.allCohorts(state),
cohortsByName: selectors.cohorts.cohortsByName(state),
selectedCohortEntry: selectors.root.selectedCohortEntry(state),
selectedTrackEntry: selectors.root.selectedTrackEntry(state),
tracks: selectors.tracks.allTracks(state),
tracksByName: selectors.tracks.tracksByName(state),
});
export const mapDispatchToProps = {
fetchGrades: thunkActions.grades.fetchGrades,
updateCohort: actions.filters.update.cohort,
updateTrack: actions.filters.update.track,
};
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(StudentGroupsFilter));

View File

@@ -3,24 +3,45 @@
import React from 'react';
import { shallow } from 'enzyme';
import { fetchGrades } from 'data/actions/grades';
import { fetchGrades } from 'data/thunkActions/grades';
import actions from 'data/actions';
import selectors from 'data/selectors';
import {
optionFactory,
StudentGroupsFilter,
mapStateToProps,
mapDispatchToProps,
} from '.';
jest.mock('data/selectors', () => ({
__esModule: true,
default: {
root: {
selectedCohortEntry: jest.fn(state => ({ selectedCohortEntry: state })),
selectedTrackEntry: jest.fn(state => ({ selectedTrackEntry: state })),
},
cohorts: {
allCohorts: jest.fn(state => ({ allCohorts: state })),
cohortsByName: jest.fn(state => ({ cohortsByName: state })),
},
tracks: {
allTracks: jest.fn(state => ({ allTracks: state })),
tracksByName: jest.fn(state => ({ tracksByName: state })),
},
},
}));
jest.mock('data/thunkActions/grades', () => ({
fetchGrades: jest.fn(),
}));
describe('StudentGroupsFilter', () => {
let props = {
courseId: '12345',
cohorts: [
{ name: 'cohorT1', id: 8001 },
{ name: 'cohorT2', id: 8002 },
{ name: 'cohorT3', id: 8003 },
],
selectedAssignmentType: 'assignMent type 1',
selectedCohort: '8003',
selectedTrack: 'TracK2_slug',
tracks: [
{ name: 'TracK1', slug: 'TracK1_slug' },
{ name: 'TracK2', slug: 'TracK2_slug' },
@@ -28,15 +49,40 @@ describe('StudentGroupsFilter', () => {
],
};
beforeEach(() => {
props = {
...props,
getUserGrades: jest.fn(),
updateQueryParams: jest.fn(),
};
describe('optionFactory', () => {
it('returns a list of options with a default first entry', () => {
const data = [{ cMark: 'rainbow', name: 'RDash' }, { cMark: 'balloons', name: 'PPie' }];
const defaultOption = 'All-Ponies';
const key = 'cMark';
const options = optionFactory({ data, defaultOption, key });
expect(options).toMatchSnapshot();
});
});
describe('Component', () => {
beforeEach(() => {
props = {
...props,
intl: { formatMessage: (msg) => msg.defaultMessage },
cohortsByName: {
[props.cohorts[0].name]: props.cohorts[0],
[props.cohorts[1].name]: props.cohorts[1],
[props.cohorts[2].name]: props.cohorts[2],
},
tracksByName: {
[props.tracks[0].name]: props.tracks[0],
[props.tracks[1].name]: props.tracks[1],
[props.tracks[2].name]: props.tracks[2],
},
fetchGrades: jest.fn(),
selectedCohortEntry: props.cohorts[2],
selectedTrackEntry: props.tracks[1],
updateQueryParams: jest.fn(),
updateCohort: jest.fn().mockName('updateCohort'),
updateTrack: jest.fn().mockName('updateTrack'),
};
});
describe('snapshots', () => {
let el;
beforeEach(() => {
@@ -72,34 +118,6 @@ describe('StudentGroupsFilter', () => {
beforeEach(() => {
el = shallow(<StudentGroupsFilter {...props} />);
});
describe('mapSelectedCohortEntry', () => {
it('returns the name of the cohort with the same numerical id', () => {
// Because selectedCohort is the id of cohorts[2]
expect(el.instance().mapSelectedCohortEntry()).toEqual(
props.cohorts[2].name,
);
});
it('returns "Cohorts" if no cohort is found', () => {
el.setProps({ selectedCohort: '999' });
expect(el.instance().mapSelectedCohortEntry()).toEqual(
'Cohorts',
);
});
});
describe('mapSelectedTrackEntry', () => {
it('returns the name of the track with the selected slug', () => {
// Because selectedTrack is the slug of tracks[1]
expect(el.instance().mapSelectedTrackEntry()).toEqual(
props.tracks[1].name,
);
});
it('returns "Tracks" if no track is found', () => {
el.setProps({ selectedTrack: 'FAKE' });
expect(el.instance().mapSelectedTrackEntry()).toEqual(
'Tracks',
);
});
});
describe('selectedCohortIdFromEvent', () => {
it('returns the id of the cohort with the name matching the event', () => {
expect(
@@ -142,13 +160,11 @@ describe('StudentGroupsFilter', () => {
).mockReturnValue(selectedSlug);
el.instance().updateTracks({ target: {} });
});
it('calls getUserGrades with selection', () => {
expect(props.getUserGrades).toHaveBeenCalledWith(
props.courseId,
props.selectedCohort,
selectedSlug,
props.selectedAssignmentType,
);
it('calls updateTrack with new value', () => {
expect(props.updateTrack).toHaveBeenCalledWith(selectedSlug);
});
it('calls fetchGrades', () => {
expect(props.fetchGrades).toHaveBeenCalledWith();
});
it('updates queryParams with track value', () => {
expect(props.updateQueryParams).toHaveBeenCalledWith({
@@ -166,13 +182,11 @@ describe('StudentGroupsFilter', () => {
).mockReturnValue(selectedId);
el.instance().updateCohorts({ target: {} });
});
it('calls getUserGrades with selection', () => {
expect(props.getUserGrades).toHaveBeenCalledWith(
props.courseId,
selectedId,
props.selectedTrack,
props.selectedAssignmentType,
);
it('calls updateCohort with new value', () => {
expect(props.updateCohort).toHaveBeenCalledWith(selectedId);
});
it('calls fetchGrades', () => {
expect(props.fetchGrades).toHaveBeenCalledWith();
});
it('updates queryParams with cohort value', () => {
expect(props.updateQueryParams).toHaveBeenCalledWith({
@@ -183,56 +197,43 @@ describe('StudentGroupsFilter', () => {
});
});
describe('mapStateToProps', () => {
const state = {
cohorts: { results: ['some', 'cohorts'] },
filters: {
cohort: 'COHort',
track: 'TRacK',
assignmentType: 'TYPe',
},
tracks: { results: ['a', 'few', 'tracks'] },
};
describe('cohorts', () => {
test('drawn from cohorts.results', () => {
expect(mapStateToProps(state).cohorts).toEqual(
state.cohorts.results,
);
});
const testState = { h: 'e', l: 'l', o: 'oooooooooo' };
let mappedProps;
beforeAll(() => {
mappedProps = mapStateToProps(testState);
});
describe('selectedAssignmentType', () => {
test('drawn from filters.assignmentType', () => {
expect(mapStateToProps(state).selectedAssignmentType).toEqual(
state.filters.assignmentType,
);
});
test('cohorts from selectors.cohorts.allCohorts', () => {
expect(mappedProps.cohorts).toEqual(selectors.cohorts.allCohorts(testState));
});
describe('selectedCohort', () => {
test('drawn from filters.cohort', () => {
expect(mapStateToProps(state).selectedCohort).toEqual(
state.filters.cohort,
);
});
test('cohortsByName from selectors.cohorts.cohortsByName', () => {
expect(mappedProps.cohortsByName).toEqual(selectors.cohorts.cohortsByName(testState));
});
describe('selectedTrack', () => {
test('drawn from filters.track', () => {
expect(mapStateToProps(state).selectedTrack).toEqual(
state.filters.track,
);
});
test('selectedCohortEntry from selectors.root.selectedCohortEntry', () => {
expect(
mappedProps.selectedCohortEntry,
).toEqual(selectors.root.selectedCohortEntry(testState));
});
describe('tracks', () => {
test('drawn from tracks.results', () => {
expect(mapStateToProps(state).tracks).toEqual(
state.tracks.results,
);
});
test('selectedTrackEntry from selectors.root.selectedTrackEntry', () => {
expect(
mappedProps.selectedTrackEntry,
).toEqual(selectors.root.selectedTrackEntry(testState));
});
test('tracks from selectors.tracks.allTracks', () => {
expect(mappedProps.tracks).toEqual(selectors.tracks.allTracks(testState));
});
test('tracksByName from selectors.tracks.tracksByName', () => {
expect(mappedProps.tracksByName).toEqual(selectors.tracks.tracksByName(testState));
});
});
describe('mapDispatchToProps', () => {
describe('getUserGrades', () => {
test('from fetchGrades', () => {
expect(mapDispatchToProps.getUserGrades).toEqual(fetchGrades);
});
test('fetchGrades from thunkActions.grades.fetchGrades', () => {
expect(mapDispatchToProps.fetchGrades).toEqual(fetchGrades);
});
test('updateCohort from actions.filters.update.cohort', () => {
expect(mapDispatchToProps.updateCohort).toEqual(actions.filters.update.cohort);
});
test('updateTrack from actions.filters.update.track', () => {
expect(mapDispatchToProps.updateTrack).toEqual(actions.filters.update.track);
});
});
});

View File

@@ -0,0 +1,98 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GradebookFilters Component snapshots basic snapshot 1`] = `
<React.Fragment>
<div
className="filter-sidebar-header"
>
<h2>
<Icon
className="fa fa-filter"
/>
</h2>
<IconButton
alt="Close Filters"
aria-label="Close Filters"
className="p-1"
iconAs="Icon"
onClick={[MockFunction this.props.closeMenu]}
src="paragon.icons.Close"
/>
</div>
<Collapsible
className="filter-group mb-3"
defaultOpen={true}
title={
<FormattedMessage
defaultMessage="Assignments"
description="Assignment filter group label in Gradebook Filters"
id="gradebook.GradebookFilters.assignmentsFilterLabel"
/>
}
>
<div>
<Connect(AssignmentTypeFilter)
updateQueryParams={[MockFunction]}
/>
<Connect(AssignmentFilter)
updateQueryParams={[MockFunction]}
/>
<Connect(AssignmentGradeFilter)
updateQueryParams={[MockFunction]}
/>
</div>
</Collapsible>
<Collapsible
className="filter-group mb-3"
defaultOpen={true}
title={
<FormattedMessage
defaultMessage="Overall Grade"
description="Overall Grade filter group label in Gradebook Filters"
id="gradebook.GradebookFilters.overallGradeFilterLabel"
/>
}
>
<Connect(CourseGradeFilter)
updateQueryParams={[MockFunction]}
/>
</Collapsible>
<Collapsible
className="filter-group mb-3"
defaultOpen={true}
title={
<FormattedMessage
defaultMessage="Student Groups"
description="Student Groups filter group label in Gradebook Filters"
id="gradebook.GradebookFilters.studentGroupsFilterLabel"
/>
}
>
<InjectIntl(ShimmedIntlComponent)
updateQueryParams={[MockFunction]}
/>
</Collapsible>
<Collapsible
className="filter-group mb-3"
defaultOpen={true}
title={
<FormattedMessage
defaultMessage="Include Course Team Members"
description="Include Course Team Members filter label in Gradebook Filters"
id="gradebook.GradebookFilters.includeCourseTeamMembersFilterLabel"
/>
}
>
<Checkbox
checked={true}
onChange={[MockFunction handleIncludeTeamMembersChange]}
>
<FormattedMessage
defaultMessage="Include Course Team Members"
description="Include Course Team Members filter label in Gradebook Filters"
id="gradebook.GradebookFilters.includeCourseTeamMembersFilterLabel"
/>
</Checkbox>
</Collapsible>
</React.Fragment>
`;

View File

@@ -3,11 +3,20 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Collapsible, Form } from '@edx/paragon';
import {
Collapsible,
Icon,
IconButton,
Form,
} from '@edx/paragon';
import { Close } from '@edx/paragon/icons';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import * as filterActions from 'data/actions/filters';
import actions from 'data/actions';
import selectors from 'data/selectors';
import thunkActions from 'data/thunkActions';
import messages from './messages';
import AssignmentTypeFilter from './AssignmentTypeFilter';
import AssignmentFilter from './AssignmentFilter';
import AssignmentGradeFilter from './AssignmentGradeFilter';
@@ -27,65 +36,61 @@ export class GradebookFilters extends React.Component {
const includeCourseRoleMembers = event.target.checked;
this.setState({ includeCourseRoleMembers });
this.props.updateIncludeCourseRoleMembers(includeCourseRoleMembers);
this.props.fetchGrades();
this.props.updateQueryParams({ includeCourseRoleMembers });
}
collapsibleGroup = (title, content) => (
<Collapsible title={title} defaultOpen className="filter-group mb-3">
<Collapsible
title={<FormattedMessage {...title} />}
defaultOpen
className="filter-group mb-3"
>
{content}
</Collapsible>
);
render() {
const {
courseId,
filterValues,
setFilters,
intl,
updateQueryParams,
} = this.props;
return (
<>
{this.collapsibleGroup('Assignments', (
<div className="filter-sidebar-header">
<h2><Icon className="fa fa-filter" /></h2>
<IconButton
className="p-1"
onClick={this.props.closeMenu}
iconAs={Icon}
src={Close}
alt={intl.formatMessage(messages.closeFilters)}
aria-label={intl.formatMessage(messages.closeFilters)}
/>
</div>
{this.collapsibleGroup(messages.assignments, (
<div>
<AssignmentTypeFilter
updateQueryParams={updateQueryParams}
/>
<AssignmentFilter
courseId={courseId}
updateQueryParams={updateQueryParams}
/>
<AssignmentGradeFilter
{...{
courseId,
filterValues,
setFilters,
updateQueryParams,
}}
/>
<AssignmentTypeFilter updateQueryParams={updateQueryParams} />
<AssignmentFilter updateQueryParams={updateQueryParams} />
<AssignmentGradeFilter updateQueryParams={updateQueryParams} />
</div>
))}
{this.collapsibleGroup('Overall Grade', (
<CourseGradeFilter
{...{
filterValues,
setFilters,
courseId,
updateQueryParams,
}}
/>
{this.collapsibleGroup(messages.overallGrade, (
<CourseGradeFilter updateQueryParams={updateQueryParams} />
))}
{this.collapsibleGroup('Student Groups', (
<StudentGroupsFilter
courseId={courseId}
updateQueryParams={updateQueryParams}
/>
{this.collapsibleGroup(messages.studentGroups, (
<StudentGroupsFilter updateQueryParams={updateQueryParams} />
))}
{this.collapsibleGroup('Include Course Team Members', (
{this.collapsibleGroup(messages.includeCourseTeamMembers, (
<Form.Checkbox
checked={this.state.includeCourseRoleMembers}
onChange={this.handleIncludeTeamMembersChange}
>
Include Course Team Members
<FormattedMessage {...messages.includeCourseTeamMembers} />
</Form.Checkbox>
))}
</>
@@ -96,17 +101,14 @@ GradebookFilters.defaultProps = {
includeCourseRoleMembers: false,
};
GradebookFilters.propTypes = {
courseId: PropTypes.string.isRequired,
filterValues: PropTypes.shape({
assignmentGradeMin: PropTypes.string,
assignmentGradeMax: PropTypes.string,
courseGradeMin: PropTypes.string,
courseGradeMax: PropTypes.string,
}).isRequired,
setFilters: PropTypes.func.isRequired,
updateQueryParams: PropTypes.func.isRequired,
// injected
intl: intlShape.isRequired,
// redux
closeMenu: PropTypes.func.isRequired,
fetchGrades: PropTypes.func.isRequired,
includeCourseRoleMembers: PropTypes.bool,
updateIncludeCourseRoleMembers: PropTypes.func.isRequired,
updateQueryParams: PropTypes.func.isRequired,
};
export const mapStateToProps = (state) => ({
@@ -114,7 +116,9 @@ export const mapStateToProps = (state) => ({
});
export const mapDispatchToProps = {
updateIncludeCourseRoleMembers: filterActions.updateIncludeCourseRoleMembers,
closeMenu: thunkActions.app.filterMenu.close,
fetchGrades: thunkActions.grades.fetchGrades,
updateIncludeCourseRoleMembers: actions.filters.update.includeCourseRoleMembers,
};
export default connect(mapStateToProps, mapDispatchToProps)(GradebookFilters);
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(GradebookFilters));

View File

@@ -0,0 +1,71 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
assignments: {
id: 'gradebook.GradebookFilters.assignmentsFilterLabel',
defaultMessage: 'Assignments',
description: 'Assignment filter group label in Gradebook Filters',
},
overallGrade: {
id: 'gradebook.GradebookFilters.overallGradeFilterLabel',
defaultMessage: 'Overall Grade',
description: 'Overall Grade filter group label in Gradebook Filters',
},
studentGroups: {
id: 'gradebook.GradebookFilters.studentGroupsFilterLabel',
defaultMessage: 'Student Groups',
description: 'Student Groups filter group label in Gradebook Filters',
},
includeCourseTeamMembers: {
id: 'gradebook.GradebookFilters.includeCourseTeamMembersFilterLabel',
defaultMessage: 'Include Course Team Members',
description: 'Include Course Team Members filter label in Gradebook Filters',
},
assignment: {
id: 'gradebook.GradebookFilters.assignmentFilterLabel',
defaultMessage: 'Assignment',
description: 'Assignment filter select label in Gradebook Filters',
},
assignmentTypes: {
id: 'gradebook.GradebookFilters.assignmentTypesLabel',
defaultMessage: 'Assignment Types',
description: 'Assignment Types filter select label in Gradebook Filters',
},
maxGrade: {
id: 'gradebook.GradebookFilters.maxGradeFilterLabel',
defaultMessage: 'Max Grade',
description: 'Max-grade filter select label in Gradebook Filters',
},
minGrade: {
id: 'gradebook.GradebookFilters.minGradeFilterLabel',
defaultMessage: 'Min Grade',
description: 'Min-grade filter select label in Gradebook Filters',
},
cohorts: {
id: 'gradebook.GradebookFilters.cohorts',
defaultMessage: 'Cohorts',
description: 'Cohorts filter select label in Gradebook Filters',
},
cohortAll: {
id: 'gradebook.GradebookFilters.cohortsAll',
defaultMessage: 'Cohort-All',
description: 'Cohorts filter select default in Gradebook Filters',
},
tracks: {
id: 'gradebook.GradebookFilters.tracks',
defaultMessage: 'Tracks',
description: 'Tracks filter select label in Gradebook Filters',
},
trackAll: {
id: 'gradebook.GradebookFilters.trackAll',
defaultMessage: 'Track-All',
description: 'Tracks filter select default in Gradebook Filters',
},
closeFilters: {
id: 'gradebook.GradebookFilters.closeFilters',
defaultMessage: 'Close Filters',
description: 'Button label for Close button in Gradebook Filters',
},
});
export default messages;

View File

@@ -1,7 +1,9 @@
import React from 'react';
import { shallow } from 'enzyme';
import { updateIncludeCourseRoleMembers } from 'data/actions/filters';
import actions from 'data/actions';
import selectors from 'data/selectors';
import thunkActions from 'data/thunkActions';
import {
GradebookFilters,
@@ -14,26 +16,41 @@ jest.mock('@edx/paragon', () => ({
Form: {
Checkbox: 'Checkbox',
},
Icon: 'Icon',
IconButton: 'IconButton',
}));
jest.mock('@edx/paragon/icons', () => ({
Close: 'paragon.icons.Close',
}));
jest.mock('data/selectors', () => ({
__esModule: true,
default: {
filters: {
includeCourseRoleMembers: jest.fn((state) => ({ includeCourseRoleMembers: state })),
},
},
}));
jest.mock('data/thunkActions', () => ({
__esModule: true,
default: {
app: { filterMenu: { close: jest.fn() } },
grades: { fetchGrades: jest.fn() },
},
}));
describe('GradebookFilters', () => {
let props = {
courseId: '12345',
filterValues: {
assignmentGradeMin: '10',
assignmentGradeMax: '90',
courseGradeMin: '20',
courseGradeMax: '80',
},
includeCourseRoleMembers: true,
};
beforeEach(() => {
props = {
...props,
updateQueryParams: jest.fn(),
intl: { formatMessage: (msg) => msg.defaultMessage },
closeMenu: jest.fn().mockName('this.props.closeMenu'),
fetchGrades: jest.fn(),
updateIncludeCourseRoleMembers: jest.fn(),
setFilters: jest.fn(),
updateQueryParams: jest.fn(),
};
});
@@ -82,24 +99,23 @@ describe('GradebookFilters', () => {
});
});
describe('mapStateToProps', () => {
const state = {
filters: {
includeCourseRoleMembers: 'plz do',
},
};
describe('includeCourseRoleMembers', () => {
it('is drawn from filters.includeCourseRoleMembers', () => {
expect(mapStateToProps(state).includeCourseRoleMembers).toEqual(
state.filters.includeCourseRoleMembers,
const testState = { A: 'laska' };
test('includeCourseRoleMembers from filters.includeCourseRoleMembers', () => {
expect(
mapStateToProps(testState).includeCourseRoleMembers,
).toEqual(selectors.filters.includeCourseRoleMembers(testState));
});
});
describe('mapDispatchToProps', () => {
test('fetchGrades from thunkActions.grades.fetchGrades', () => {
expect(mapDispatchToProps.fetchGrades).toEqual(thunkActions.grades.fetchGrades);
});
describe('updateIncludeCourseRoleMembers', () => {
test('from actions.filters.update.includeCourseRoleMembers', () => {
expect(mapDispatchToProps.updateIncludeCourseRoleMembers).toEqual(
actions.filters.update.includeCourseRoleMembers,
);
});
});
});
describe('mapDispatchToProps', () => {
test('updateIncludeCourseRoleMembers', () => {
expect(mapDispatchToProps.updateIncludeCourseRoleMembers).toEqual(
updateIncludeCourseRoleMembers,
);
});
});
});

View File

@@ -0,0 +1,137 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GradebookHeader component snapshots default values (grades frozen, cannot view). unauthorized warning, but no grades frozen warning 1`] = `
<div
className="gradebook-header"
>
<a
className="mb-3"
href="http://localhost:18000/courses/fakeID/instructor"
>
<span
aria-hidden="true"
>
&lt;&lt;
</span>
<FormattedMessage
defaultMessage="Back to Dashboard"
description="Button text to take user back to LMS dashboard in Gradebook Header"
id="gradebook.GradebookHeader.backButton"
/>
</a>
<h1>
<FormattedMessage
defaultMessage="Gradebook"
description="Top-level app title in Gradebook Header component"
id="gradebook.GradebookHeader.appLabel"
/>
</h1>
<h3>
fakeID
</h3>
<div
className="alert alert-warning"
role="alert"
>
<FormattedMessage
defaultMessage="You are not authorized to view the gradebook for this course."
description="Warning message in Gradebook Header when user is not allowed to view the app"
id="gradebook.GradebookHeader.unauthorizedWarning"
/>
</div>
</div>
`;
exports[`GradebookHeader component snapshots grades frozen, can view. grades frozen warning but no unauthorized warning 1`] = `
<div
className="gradebook-header"
>
<a
className="mb-3"
href="http://localhost:18000/courses/fakeID/instructor"
>
<span
aria-hidden="true"
>
&lt;&lt;
</span>
<FormattedMessage
defaultMessage="Back to Dashboard"
description="Button text to take user back to LMS dashboard in Gradebook Header"
id="gradebook.GradebookHeader.backButton"
/>
</a>
<h1>
<FormattedMessage
defaultMessage="Gradebook"
description="Top-level app title in Gradebook Header component"
id="gradebook.GradebookHeader.appLabel"
/>
</h1>
<h3>
fakeID
</h3>
<div
className="alert alert-warning"
role="alert"
>
<FormattedMessage
defaultMessage="The grades for this course are now frozen. Editing of grades is no longer allowed."
description="Warning message in Gradebook Header for frozen messages"
id="gradebook.GradebookHeader.frozenWarning"
/>
</div>
</div>
`;
exports[`GradebookHeader component snapshots grades frozen, cannot view unauthorized warning, and grades frozen warning. 1`] = `
<div
className="gradebook-header"
>
<a
className="mb-3"
href="http://localhost:18000/courses/fakeID/instructor"
>
<span
aria-hidden="true"
>
&lt;&lt;
</span>
<FormattedMessage
defaultMessage="Back to Dashboard"
description="Button text to take user back to LMS dashboard in Gradebook Header"
id="gradebook.GradebookHeader.backButton"
/>
</a>
<h1>
<FormattedMessage
defaultMessage="Gradebook"
description="Top-level app title in Gradebook Header component"
id="gradebook.GradebookHeader.appLabel"
/>
</h1>
<h3>
fakeID
</h3>
<div
className="alert alert-warning"
role="alert"
>
<FormattedMessage
defaultMessage="The grades for this course are now frozen. Editing of grades is no longer allowed."
description="Warning message in Gradebook Header for frozen messages"
id="gradebook.GradebookHeader.frozenWarning"
/>
</div>
<div
className="alert alert-warning"
role="alert"
>
<FormattedMessage
defaultMessage="You are not authorized to view the gradebook for this course."
description="Warning message in Gradebook Header when user is not allowed to view the app"
id="gradebook.GradebookHeader.unauthorizedWarning"
/>
</div>
</div>
`;

View File

@@ -0,0 +1,67 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { configuration } from 'config';
import selectors from 'data/selectors';
import messages from './messages';
export class GradebookHeader extends React.Component {
lmsInstructorDashboardUrl = courseId => (
`${configuration.LMS_BASE_URL}/courses/${courseId}/instructor`
);
render() {
return (
<div className="gradebook-header">
<a
href={this.lmsInstructorDashboardUrl(this.props.courseId)}
className="mb-3"
>
<span aria-hidden="true">{'<< '}</span>
<FormattedMessage {...messages.backToDashboard} />
</a>
<h1>
<FormattedMessage {...messages.gradebook} />
</h1>
<h3>{this.props.courseId}</h3>
{this.props.areGradesFrozen
&& (
<div className="alert alert-warning" role="alert">
<FormattedMessage {...messages.frozenWarning} />
</div>
)}
{(this.props.canUserViewGradebook === false) && (
<div className="alert alert-warning" role="alert">
<FormattedMessage {...messages.unauthorizedWarning} />
</div>
)}
</div>
);
}
}
GradebookHeader.defaultProps = {
// redux
courseId: '',
areGradesFrozen: false,
canUserViewGradebook: false,
};
GradebookHeader.propTypes = {
// redux
courseId: PropTypes.string,
areGradesFrozen: PropTypes.bool,
canUserViewGradebook: PropTypes.bool,
};
export const mapStateToProps = (state) => ({
courseId: selectors.app.courseId(state),
areGradesFrozen: selectors.assignmentTypes.areGradesFrozen(state),
canUserViewGradebook: selectors.roles.canUserViewGradebook(state),
});
export default connect(mapStateToProps)(GradebookHeader);

View File

@@ -0,0 +1,26 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
backToDashboard: {
id: 'gradebook.GradebookHeader.backButton',
defaultMessage: 'Back to Dashboard',
description: 'Button text to take user back to LMS dashboard in Gradebook Header',
},
gradebook: {
id: 'gradebook.GradebookHeader.appLabel',
defaultMessage: 'Gradebook',
description: 'Top-level app title in Gradebook Header component',
},
frozenWarning: {
id: 'gradebook.GradebookHeader.frozenWarning',
defaultMessage: 'The grades for this course are now frozen. Editing of grades is no longer allowed.',
description: 'Warning message in Gradebook Header for frozen messages',
},
unauthorizedWarning: {
id: 'gradebook.GradebookHeader.unauthorizedWarning',
defaultMessage: 'You are not authorized to view the gradebook for this course.',
description: 'Warning message in Gradebook Header when user is not allowed to view the app',
},
});
export default messages;

View File

@@ -0,0 +1,63 @@
import React from 'react';
import { shallow } from 'enzyme';
import selectors from 'data/selectors';
import { GradebookHeader, mapStateToProps } from '.';
jest.mock('@edx/frontend-platform/i18n', () => ({
defineMessages: messages => messages,
FormattedMessage: 'FormattedMessage',
}));
jest.mock('data/selectors', () => ({
__esModule: true,
default: {
app: { courseId: jest.fn(state => ({ courseId: state })) },
assignmentTypes: { areGradesFrozen: jest.fn(state => ({ areGradesFrozen: state })) },
roles: { canUserViewGradebook: jest.fn(state => ({ canUserViewGradebook: state })) },
},
}));
const courseId = 'fakeID';
describe('GradebookHeader component', () => {
describe('snapshots', () => {
describe('default values (grades frozen, cannot view).', () => {
test('unauthorized warning, but no grades frozen warning', () => {
const props = { courseId, areGradesFrozen: false, canUserViewGradebook: false };
expect(shallow(<GradebookHeader {...props} />)).toMatchSnapshot();
});
});
describe('grades frozen, cannot view', () => {
test('unauthorized warning, and grades frozen warning.', () => {
const props = { courseId, areGradesFrozen: true, canUserViewGradebook: false };
expect(shallow(<GradebookHeader {...props} />)).toMatchSnapshot();
});
});
describe('grades frozen, can view.', () => {
test('grades frozen warning but no unauthorized warning', () => {
const props = { courseId, areGradesFrozen: true, canUserViewGradebook: true };
expect(shallow(<GradebookHeader {...props} />)).toMatchSnapshot();
});
});
});
describe('mapStateToProps', () => {
let mapped;
const testState = { a: 'test', example: 'state' };
beforeEach(() => {
mapped = mapStateToProps(testState);
});
test('courseId from app.courseId', () => {
expect(mapped.courseId).toEqual(selectors.app.courseId(testState));
});
test('areGradesFrozen from assignmentTypes selector', () => {
expect(
mapped.areGradesFrozen,
).toEqual(selectors.assignmentTypes.areGradesFrozen(testState));
});
test('canUserViewGradebook from roles selector', () => {
expect(
mapped.canUserViewGradebook,
).toEqual(selectors.roles.canUserViewGradebook(testState));
});
});
});

View File

@@ -0,0 +1,108 @@
/* eslint-disable react/sort-comp, react/button-has-type */
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { StatefulButton, Icon } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { StrictDict } from 'utils';
import actions from 'data/actions';
import selectors from 'data/selectors';
import messages from './messages';
export const basicButtonProps = () => ({
variant: 'outline-primary',
icons: {
default: <Icon className="fa fa-download mr-2" />,
pending: <Icon className="fa fa-spinner fa-spin mr-2" />,
},
disabledStates: ['pending'],
className: 'ml-2',
});
export const buttonStates = StrictDict({
pending: 'pending',
default: 'default',
});
/**
* <BulkManagementControls />
* Provides download buttons for Bulk Management and Intervention reports, only if
* showBulkManagement is set in redus.
*/
export class BulkManagementControls extends React.Component {
constructor(props) {
super(props);
this.buttonProps = this.buttonProps.bind(this);
this.handleClickDownloadInterventions = this.handleClickDownloadInterventions.bind(this);
this.handleClickExportGrades = this.handleClickExportGrades.bind(this);
}
buttonProps(label) {
return {
labels: { default: label, pending: label },
state: this.props.showSpinner ? 'pending' : 'default',
...basicButtonProps(),
};
}
handleClickDownloadInterventions() {
this.props.downloadInterventionReport();
window.location.assign(this.props.interventionExportUrl);
}
// At present, we don't store label and value in google analytics. By setting the label
// property of the below events, I want to verify that we can set the label of google anlatyics
// The following properties of a google analytics event are:
// category (used), name(used), label(not used), value(not used)
handleClickExportGrades() {
this.props.downloadBulkGradesReport();
window.location.assign(this.props.gradeExportUrl);
}
render() {
return this.props.showBulkManagement && (
<div>
<StatefulButton
{...this.buttonProps(<FormattedMessage {...messages.bulkManagement} />)}
onClick={this.handleClickExportGrades}
/>
<StatefulButton
{...this.buttonProps(<FormattedMessage {...messages.interventions} />)}
onClick={this.handleClickDownloadInterventions}
/>
</div>
);
}
}
BulkManagementControls.defaultProps = {
showBulkManagement: false,
showSpinner: false,
};
BulkManagementControls.propTypes = {
// redux
downloadBulkGradesReport: PropTypes.func.isRequired,
downloadInterventionReport: PropTypes.func.isRequired,
gradeExportUrl: PropTypes.string.isRequired,
interventionExportUrl: PropTypes.string.isRequired,
showSpinner: PropTypes.bool,
showBulkManagement: PropTypes.bool,
};
export const mapStateToProps = (state) => ({
gradeExportUrl: selectors.root.gradeExportUrl(state),
interventionExportUrl: selectors.root.interventionExportUrl(state),
showBulkManagement: selectors.root.showBulkManagement(state),
showSpinner: selectors.root.shouldShowSpinner(state),
});
export const mapDispatchToProps = {
downloadBulkGradesReport: actions.grades.downloadReport.bulkGrades,
downloadInterventionReport: actions.grades.downloadReport.intervention,
};
export default connect(mapStateToProps, mapDispatchToProps)(BulkManagementControls);

View File

@@ -0,0 +1,168 @@
import React from 'react';
import { shallow } from 'enzyme';
import actions from 'data/actions';
import selectors from 'data/selectors';
import {
BulkManagementControls,
basicButtonProps,
buttonStates,
mapStateToProps,
mapDispatchToProps,
} from './BulkManagementControls';
jest.mock('data/selectors', () => ({
__esModule: true,
default: {
root: {
gradeExportUrl: (state) => ({ gradeExportUrl: state }),
interventionExportUrl: (state) => ({ interventionExportUrl: state }),
showBulkManagement: (state) => ({ showBulkManagement: state }),
shouldShowSpinner: (state) => ({ showSpinner: state }),
},
},
}));
jest.mock('data/actions', () => ({
__esModule: true,
default: {
grades: {
downloadReport: {
bulkGrades: jest.fn(),
intervention: jest.fn(),
},
},
},
}));
describe('BulkManagementControls', () => {
describe('component', () => {
let el;
let props = {
gradeExportUrl: 'gradesGoHere',
interventionExportUrl: 'interventionsGoHere',
};
beforeEach(() => {
props = {
...props,
downloadBulkGradesReport: jest.fn(),
downloadInterventionReport: jest.fn(),
};
});
test('snapshot - empty if showBulkManagement is not truthy', () => {
expect(shallow(<BulkManagementControls {...props} />)).toEqual({});
});
test('snapshot - buttonProps for each button ("Bulk Management" and "Interventions")', () => {
el = shallow(<BulkManagementControls {...props} showBulkManagement />);
jest.spyOn(el.instance(), 'buttonProps').mockImplementation(
value => ({ buttonProps: value }),
);
jest.spyOn(el.instance(), 'handleClickExportGrades').mockName('this.handleClickExportGrades');
jest.spyOn(
el.instance(),
'handleClickDownloadInterventions',
).mockName('this.handleClickDownloadInterventions');
});
describe('behavior', () => {
const oldWindowLocation = window.location;
beforeAll(() => {
delete window.location;
window.location = Object.defineProperties(
{},
{
...Object.getOwnPropertyDescriptors(oldWindowLocation),
assign: {
configurable: true,
value: jest.fn(),
},
},
);
});
beforeEach(() => {
window.location.assign.mockReset();
el = shallow(<BulkManagementControls {...props} showBulkManagement />);
});
afterAll(() => {
// restore `window.location` to the `jsdom` `Location` object
window.location = oldWindowLocation;
});
describe('buttonProps', () => {
test('loads default and pending labels based on passed string', () => {
const label = 'Fake Label';
const { labels, state, ...rest } = el.instance().buttonProps(label);
expect(rest).toEqual(basicButtonProps());
expect(labels).toEqual({ default: label, pending: label });
});
test('loads pending state if props.showSpinner', () => {
const label = 'Fake Label';
el.setProps({ showSpinner: true });
const { labels, state, ...rest } = el.instance().buttonProps(label);
expect(state).toEqual(buttonStates.pending);
expect(rest).toEqual(basicButtonProps());
});
test('loads default state if not props.showSpinner', () => {
const label = 'Fake Label';
const { labels, state, ...rest } = el.instance().buttonProps(label);
expect(state).toEqual(buttonStates.default);
expect(rest).toEqual(basicButtonProps());
});
});
describe('handleClickDownloadInterventions', () => {
const assertions = [
'calls props.downloadInterventionReport',
'sets location to props.interventionsExportUrl',
];
it(assertions.join(' and '), () => {
el.instance().handleClickDownloadInterventions();
expect(props.downloadInterventionReport).toHaveBeenCalled();
expect(window.location.assign).toHaveBeenCalledWith(props.interventionExportUrl);
});
});
describe('handleClickExportGrades', () => {
const assertions = [
'calls props.downloadBulkGradesReport',
'sets location to props.gradeExportUrl',
];
it(assertions.join(' and '), () => {
el.instance().handleClickExportGrades();
expect(props.downloadBulkGradesReport).toHaveBeenCalled();
expect(window.location.assign).toHaveBeenCalledWith(props.gradeExportUrl);
});
});
});
});
describe('mapStateToProps', () => {
let mapped;
const testState = { do: 'not', test: 'me' };
beforeEach(() => {
mapped = mapStateToProps(testState);
});
test('gradeExportUrl from root.gradeExportUrl', () => {
expect(mapped.gradeExportUrl).toEqual(selectors.root.gradeExportUrl(testState));
});
test('interventionExportUrl from root.interventionExportUrl', () => {
expect(mapped.interventionExportUrl).toEqual(selectors.root.interventionExportUrl(testState));
});
test('showBulkManagement from root.showBulkManagement', () => {
expect(mapped.showBulkManagement).toEqual(selectors.root.showBulkManagement(testState));
});
test('showSpinner from root.shouldShowSpinner', () => {
expect(mapped.showSpinner).toEqual(selectors.root.shouldShowSpinner(testState));
});
});
describe('mapDispatchToProps', () => {
test('downloadBulkGradesReport from actions.grades.downloadReport.bulkGrades', () => {
expect(
mapDispatchToProps.downloadBulkGradesReport,
).toEqual(actions.grades.downloadReport.bulkGrades);
});
test('downloadInterventionReport from actions.grades.downloadReport.invervention', () => {
expect(
mapDispatchToProps.downloadInterventionReport,
).toEqual(actions.grades.downloadReport.intervention);
});
});
});

View File

@@ -0,0 +1,26 @@
import React from 'react';
import PropTypes from 'prop-types';
/**
* HistoryHeader
* simple display container for an individual history table header
* @param {string} id - header id
* @param {string} label - header label
* @param {string} value - header value
*/
const HistoryHeader = ({ id, label, value }) => (
<div>
<div className={`grade-history-header grade-history-${id}`}>{label}: </div>
<div>{value}</div>
</div>
);
HistoryHeader.defaultProps = {
value: null,
};
HistoryHeader.propTypes = {
id: PropTypes.string.isRequired,
label: PropTypes.node.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};
export default HistoryHeader;

View File

@@ -0,0 +1,17 @@
import React from 'react';
import { shallow } from 'enzyme';
import HistoryHeader from './HistoryHeader';
describe('HistoryHeader', () => {
const props = {
id: 'water',
label: 'Brita',
value: 'hydration',
};
describe('Component', () => {
test('snapshot', () => {
expect(shallow(<HistoryHeader {...props} />)).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,68 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import selectors from 'data/selectors';
import messages from './messages';
import HistoryHeader from './HistoryHeader';
/**
* <ModalHeaders />
* Provides a list of HistoryHeaders for the student name, assignment,
* original grade, and current override grade.
*/
export const ModalHeaders = ({
modalState,
originalGrade,
currentGrade,
}) => (
<div>
<HistoryHeader
id="assignment"
label={<FormattedMessage {...messages.assignmentHeader} />}
value={modalState.assignmentName}
/>
<HistoryHeader
id="student"
label={<FormattedMessage {...messages.studentHeader} />}
value={modalState.updateUserName}
/>
<HistoryHeader
id="original-grade"
label={<FormattedMessage {...messages.originalGradeHeader} />}
value={originalGrade}
/>
<HistoryHeader
id="current-grade"
label={<FormattedMessage {...messages.currentGradeHeader} />}
value={currentGrade}
/>
</div>
);
ModalHeaders.defaultProps = {
currentGrade: null,
originalGrade: null,
};
ModalHeaders.propTypes = {
// redux
currentGrade: PropTypes.number,
originalGrade: PropTypes.number,
modalState: PropTypes.shape({
assignmentName: PropTypes.string.isRequired,
updateUserName: PropTypes.string,
}).isRequired,
};
export const mapStateToProps = (state) => ({
modalState: {
assignmentName: selectors.app.modalState.assignmentName(state),
updateUserName: selectors.app.modalState.updateUserName(state),
},
currentGrade: selectors.grades.gradeOverrideCurrentEarnedGradedOverride(state),
originalGrade: selectors.grades.gradeOriginalEarnedGraded(state),
});
export default connect(mapStateToProps)(ModalHeaders);

View File

@@ -0,0 +1,94 @@
import React from 'react';
import { shallow } from 'enzyme';
import selectors from 'data/selectors';
import {
ModalHeaders,
mapStateToProps,
} from './ModalHeaders';
jest.mock('./HistoryHeader', () => 'HistoryHeader');
jest.mock('data/selectors', () => ({
__esModule: true,
default: {
app: {
editUpdateData: jest.fn(state => ({ editUpdateData: state })),
modalState: {
assignmentName: jest.fn(state => ({ assignmentName: state })),
updateUserName: jest.fn(state => ({ updateUserName: state })),
},
},
grades: {
gradeOverrideCurrentEarnedGradedOverride: jest.fn(state => ({ currentGrade: state })),
gradeOriginalEarnedGraded: jest.fn(state => ({ originalGrade: state })),
},
},
}));
describe('ModalHeaders', () => {
let el;
const props = {
currentGrade: 2,
originalGrade: 20,
modalState: {
assignmentName: 'Qwerty',
updateUserName: 'Uiop',
},
};
describe('Component', () => {
describe('snapshots', () => {
beforeEach(() => {
});
describe('gradeOverrideHistoryError is and empty and open is true', () => {
test('modal open and StatusAlert showing', () => {
el = shallow(<ModalHeaders {...props} />);
expect(el).toMatchSnapshot();
});
});
describe('gradeOverrideHistoryError is empty and open is false', () => {
test('modal closed and StatusAlert closed', () => {
el = shallow(
<ModalHeaders {...props} open={false} gradeOverrideHistoryError="" />,
);
expect(el).toMatchSnapshot();
});
});
});
});
describe('mapStateToProps', () => {
const testState = { he: 'lives in a', pineapple: 'under the sea' };
let mapped;
beforeEach(() => {
mapped = mapStateToProps(testState);
});
describe('modalState', () => {
test('assignmentName from app.modalState.assignmentName', () => {
expect(
mapped.modalState.assignmentName,
).toEqual(selectors.app.modalState.assignmentName(testState));
});
test('updateUserName from app.modalState.updateUserName', () => {
expect(
mapped.modalState.updateUserName,
).toEqual(selectors.app.modalState.updateUserName(testState));
});
});
describe('originalGrade', () => {
test('from grades.gradeOverrideCurrentEarnedGradedOverride', () => {
expect(mapped.currentGrade).toEqual(
selectors.grades.gradeOverrideCurrentEarnedGradedOverride(testState),
);
});
});
describe('originalGrade', () => {
test('from grades.gradeOriginalEarnedGrades', () => {
expect(mapped.originalGrade).toEqual(
selectors.grades.gradeOriginalEarnedGraded(testState),
);
});
});
});
});

View File

@@ -0,0 +1,64 @@
/* eslint-disable react/sort-comp, react/button-has-type */
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Form } from '@edx/paragon';
import selectors from 'data/selectors';
import actions from 'data/actions';
/**
* <AdjustedGradeInput />
* Input control for adjusting the grade of a unit
* displays an "/ ${possibleGrade} if there is one in the data model.
*/
export class AdjustedGradeInput extends React.Component {
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
}
onChange = ({ target }) => {
this.props.setModalState({ adjustedGradeValue: target.value });
};
render() {
return (
<span>
<Form.Control
type="text"
name="adjustedGradeValue"
value={this.props.value}
onChange={this.onChange}
/>
{this.props.possibleGrade && ` / ${this.props.possibleGrade}`}
</span>
);
}
}
AdjustedGradeInput.defaultProps = {
possibleGrade: null,
};
AdjustedGradeInput.propTypes = {
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]).isRequired,
possibleGrade: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
setModalState: PropTypes.func.isRequired,
};
export const mapStateToProps = (state) => ({
possibleGrade: selectors.root.editModalPossibleGrade(state),
value: selectors.app.modalState.adjustedGradeValue(state),
});
export const mapDispatchToProps = {
setModalState: actions.app.setModalState,
};
export default connect(mapStateToProps, mapDispatchToProps)(AdjustedGradeInput);

View File

@@ -0,0 +1,92 @@
import React from 'react';
import { shallow } from 'enzyme';
import actions from 'data/actions';
import selectors from 'data/selectors';
import {
AdjustedGradeInput,
mapStateToProps,
mapDispatchToProps,
} from './AdjustedGradeInput';
jest.mock('@edx/paragon', () => ({
Form: { Control: () => 'Form.Control' },
}));
jest.mock('data/selectors', () => ({
__esModule: true,
default: {
root: {
editModalPossibleGrade: jest.fn(state => ({ updateUserName: state })),
},
app: {
modalState: { adjustedGradeValue: jest.fn(state => ({ adjustedGradeValue: state })) },
},
},
}));
jest.mock('data/actions', () => ({
__esModule: true,
default: {
app: { setModalState: jest.fn() },
},
}));
describe('AdjustedGradeInput', () => {
let el;
let props = {
value: 1,
possibleGrade: 5,
};
beforeEach(() => {
props = {
...props,
setModalState: jest.fn(),
};
});
describe('Component', () => {
beforeEach(() => {
el = shallow(<AdjustedGradeInput {...props} />);
});
describe('snapshots', () => {
test('displays input control and "out of possible grade" label', () => {
el.instance().onChange = jest.fn().mockName('this.onChange');
expect(el.instance().render()).toMatchSnapshot();
});
});
describe('behavior', () => {
describe('onChange', () => {
it('calls props.setModalState event target value', () => {
const value = 42;
el.instance().onChange({ target: { value } });
expect(props.setModalState).toHaveBeenCalledWith({
adjustedGradeValue: value,
});
});
});
});
});
describe('mapStateToProps', () => {
const testState = { like: 'no one', ever: 'was' };
let mapped;
beforeEach(() => {
mapped = mapStateToProps(testState);
});
describe('modalState', () => {
test('possibleGrade from root.editModalPossibleGrade', () => {
expect(
mapped.possibleGrade,
).toEqual(selectors.root.editModalPossibleGrade(testState));
});
test('updateUserName from app.modalState.updateUserName', () => {
expect(
mapped.value,
).toEqual(selectors.app.modalState.adjustedGradeValue(testState));
});
});
});
describe('mapDispatchToProps', () => {
test('setModalState from actions.app.setModalState', () => {
expect(mapDispatchToProps.setModalState).toEqual(actions.app.setModalState);
});
});
});

View File

@@ -0,0 +1,55 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Form } from '@edx/paragon';
import selectors from 'data/selectors';
import actions from 'data/actions';
/**
* <ReasonInput />
* Input control for the "reason for change" field in the Edit modal.
*/
export class ReasonInput extends React.Component {
constructor(props) {
super(props);
this.ref = React.createRef();
this.onChange = this.onChange.bind(this);
}
componentDidMount() {
this.ref.current.focus();
}
onChange = (event) => {
this.props.setModalState({ reasonForChange: event.target.value });
};
render() {
return (
<Form.Control
type="text"
name="reasonForChange"
value={this.props.value}
onChange={this.onChange}
ref={this.ref}
/>
);
}
}
ReasonInput.propTypes = {
// redux
setModalState: PropTypes.func.isRequired,
value: PropTypes.string.isRequired,
};
export const mapStateToProps = (state) => ({
value: selectors.app.modalState.reasonForChange(state),
});
export const mapDispatchToProps = {
setModalState: actions.app.setModalState,
};
export default connect(mapStateToProps, mapDispatchToProps)(ReasonInput);

View File

@@ -0,0 +1,90 @@
import React from 'react';
import { shallow } from 'enzyme';
import actions from 'data/actions';
import selectors from 'data/selectors';
import {
ReasonInput,
mapStateToProps,
mapDispatchToProps,
} from './ReasonInput';
jest.mock('@edx/paragon', () => ({
Form: { Control: () => 'Form.Control' },
}));
jest.mock('data/selectors', () => ({
__esModule: true,
default: {
app: {
modalState: { reasonForChange: jest.fn(state => ({ reasonForChange: state })) },
},
},
}));
jest.mock('data/actions', () => ({
__esModule: true,
default: {
app: { setModalState: jest.fn() },
},
}));
describe('ReasonInput', () => {
let el;
let props = {
value: 'did not answer the question',
};
beforeEach(() => {
props = {
...props,
setModalState: jest.fn(),
};
});
describe('Component', () => {
beforeEach(() => {
el = shallow(<ReasonInput {...props} />, { disableLifecycleMethods: true });
});
describe('snapshots', () => {
test('displays reason for change input control', () => {
el.instance().onChange = jest.fn().mockName('this.onChange');
expect(el.instance().render()).toMatchSnapshot();
});
});
describe('behavior', () => {
describe('onChange', () => {
it('calls props.setModalState event target value', () => {
const value = 42;
el.instance().onChange({ target: { value } });
expect(props.setModalState).toHaveBeenCalledWith({
reasonForChange: value,
});
});
});
describe('componentDidMount', () => {
it('focuses the input ref', () => {
const focus = jest.fn();
expect(el.instance().ref).toEqual({ current: null });
el.instance().ref.current = { focus };
el.instance().componentDidMount();
expect(el.instance().ref.current.focus).toHaveBeenCalledWith();
});
});
});
});
describe('mapStateToProps', () => {
const testState = { to: { catchThem: 'my real test', trainThem: 'my cause!' } };
let mapped;
beforeEach(() => {
mapped = mapStateToProps(testState);
});
describe('modalState', () => {
test('value from app.modalState.reasonForChange', () => {
expect(mapped.value).toEqual(selectors.app.modalState.reasonForChange(testState));
});
});
});
describe('mapDispatchToProps', () => {
test('setModalState from actions.app.setModalState', () => {
expect(mapDispatchToProps.setModalState).toEqual(actions.app.setModalState);
});
});
});

View File

@@ -0,0 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AdjustedGradeInput Component snapshots displays input control and "out of possible grade" label 1`] = `
<span>
<Control
name="adjustedGradeValue"
onChange={[MockFunction this.onChange]}
type="text"
value={1}
/>
/ 5
</span>
`;

View File

@@ -0,0 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ReasonInput Component snapshots displays reason for change input control 1`] = `
<Control
name="reasonForChange"
onChange={[MockFunction this.onChange]}
type="text"
value="did not answer the question"
/>
`;

View File

@@ -0,0 +1,63 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`OverrideTable Component snapshots basic snapshot shows a row for each entry and one editable row 1`] = `
<Table
columns={
Array [
Object {
"key": "date",
"label": <FormattedMessage
defaultMessage="Date"
description="Edit Modal Override Table Date column header"
id="gradebook.GradesTab.EditModal.Overrides.dateHeader"
/>,
},
Object {
"key": "grader",
"label": <FormattedMessage
defaultMessage="Grader"
description="Edit Modal Override Table Grader column header"
id="gradebook.GradesTab.EditModal.Overrides.graderHeader"
/>,
},
Object {
"key": "reason",
"label": <FormattedMessage
defaultMessage="Reason"
description="Edit Modal Override Table Reason column header"
id="gradebook.GradesTab.EditModal.Overrides.reasonHeader"
/>,
},
Object {
"key": "adjustedGrade",
"label": <FormattedMessage
defaultMessage="Adjusted grade"
description="Edit Modal Override Table Adjusted grade column header"
id="gradebook.GradesTab.EditModal.Overrides.adjustedGradeHeader"
/>,
},
]
}
data={
Array [
Object {
"adjustedGrade": 0,
"date": "yesterday",
"grader": "me",
"reason": "you ate my sandwich",
},
Object {
"adjustedGrade": 20,
"date": "today",
"grader": "me",
"reason": "you brought me a new sandwich",
},
Object {
"adjustedGrade": <AdjustedGradeInput />,
"date": "todaaaaaay",
"reason": <ReasonInput />,
},
]
}
/>
`;

View File

@@ -0,0 +1,72 @@
/* eslint-disable react/sort-comp, react/button-has-type, import/no-named-as-default */
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Table } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { gradeOverrideHistoryColumns as columns } from 'data/constants/app';
import selectors from 'data/selectors';
import messages from './messages';
import ReasonInput from './ReasonInput';
import AdjustedGradeInput from './AdjustedGradeInput';
/**
* <OverrideTable />
* Table containing previous grade override entries, and an "edit" row
* with todays date, an AdjustedGradeInput and a ReasonInput
*/
export const OverrideTable = ({
hide,
gradeOverrides,
todaysDate,
}) => {
if (hide) {
return null;
}
return (
<Table
columns={[
{ label: <FormattedMessage {...messages.dateHeader} />, key: columns.date },
{ label: <FormattedMessage {...messages.graderHeader} />, key: columns.grader },
{ label: <FormattedMessage {...messages.reasonHeader} />, key: columns.reason },
{
label: <FormattedMessage {...messages.adjustedGradeHeader} />,
key: columns.adjustedGrade,
},
]}
data={[
...gradeOverrides,
{
adjustedGrade: <AdjustedGradeInput />,
date: todaysDate,
reason: <ReasonInput />,
},
]}
/>
);
};
OverrideTable.defaultProps = {
gradeOverrides: [],
};
OverrideTable.propTypes = {
// redux
gradeOverrides: PropTypes.arrayOf(PropTypes.shape({
date: PropTypes.string,
grader: PropTypes.string,
reason: PropTypes.string,
adjustedGrade: PropTypes.number,
})),
hide: PropTypes.bool.isRequired,
todaysDate: PropTypes.string.isRequired,
};
export const mapStateToProps = (state) => ({
hide: selectors.grades.hasOverrideErrors(state),
gradeOverrides: selectors.grades.gradeOverrides(state),
todaysDate: selectors.app.modalState.todaysDate(state),
});
export default connect(mapStateToProps)(OverrideTable);

View File

@@ -0,0 +1,26 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
adjustedGradeHeader: {
id: 'gradebook.GradesTab.EditModal.Overrides.adjustedGradeHeader',
defaultMessage: 'Adjusted grade',
description: 'Edit Modal Override Table Adjusted grade column header',
},
dateHeader: {
id: 'gradebook.GradesTab.EditModal.Overrides.dateHeader',
defaultMessage: 'Date',
description: 'Edit Modal Override Table Date column header',
},
graderHeader: {
id: 'gradebook.GradesTab.EditModal.Overrides.graderHeader',
defaultMessage: 'Grader',
description: 'Edit Modal Override Table Grader column header',
},
reasonHeader: {
id: 'gradebook.GradesTab.EditModal.Overrides.reasonHeader',
defaultMessage: 'Reason',
description: 'Edit Modal Override Table Reason column header',
},
});
export default messages;

View File

@@ -0,0 +1,81 @@
import React from 'react';
import { shallow } from 'enzyme';
import selectors from 'data/selectors';
import {
OverrideTable,
mapStateToProps,
} from '.';
jest.mock('@edx/paragon', () => ({ Table: () => 'Table' }));
jest.mock('./ReasonInput', () => 'ReasonInput');
jest.mock('./AdjustedGradeInput', () => 'AdjustedGradeInput');
jest.mock('data/selectors', () => ({
__esModule: true,
default: {
app: {
modalState: {
todaysDate: jest.fn(state => ({ todaysDate: state })),
},
},
grades: {
hasOverrideErrors: jest.fn(state => ({ hasOverrideErrors: state })),
gradeOverrides: jest.fn(state => ({ gradeOverrides: state })),
},
},
}));
describe('OverrideTable', () => {
const props = {
gradeOverrides: [
{
date: 'yesterday',
grader: 'me',
reason: 'you ate my sandwich',
adjustedGrade: 0,
},
{
date: 'today',
grader: 'me',
reason: 'you brought me a new sandwich',
adjustedGrade: 20,
},
],
hide: false,
todaysDate: 'todaaaaaay',
};
describe('Component', () => {
describe('snapshots', () => {
it('returns null if hide is true', () => {
expect(shallow(<OverrideTable {...props} hide />)).toEqual({});
});
describe('basic snapshot', () => {
test('shows a row for each entry and one editable row', () => {
expect(shallow(<OverrideTable {...props} />)).toMatchSnapshot();
});
});
});
});
describe('mapStateToProps', () => {
const testState = { I: 'wanna', be: 'the', very: 'best' };
let mapped;
beforeEach(() => {
mapped = mapStateToProps(testState);
});
describe('modalState', () => {
test('hide from grades.hasOverrideErrors', () => {
expect(mapped.hide).toEqual(selectors.grades.hasOverrideErrors(testState));
});
test('gradeOverrides from grades.gradeOverrides', () => {
expect(mapped.gradeOverrides).toEqual(selectors.grades.gradeOverrides(testState));
});
test('todaysData from app.modalState.todaysDate', () => {
expect(mapped.todaysDate).toEqual(selectors.app.modalState.todaysDate(testState));
});
});
});
});

View File

@@ -0,0 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`HistoryHeader Component snapshot 1`] = `
<div>
<div
className="grade-history-header grade-history-water"
>
Brita
:
</div>
<div>
hydration
</div>
</div>
`;

View File

@@ -0,0 +1,99 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ModalHeaders Component snapshots gradeOverrideHistoryError is and empty and open is true modal open and StatusAlert showing 1`] = `
<div>
<HistoryHeader
id="assignment"
label={
<FormattedMessage
defaultMessage="Assignment"
description="Edit Modal Assignment header"
id="gradebook.GradesTab.EditModal.headers.assignment"
/>
}
value="Qwerty"
/>
<HistoryHeader
id="student"
label={
<FormattedMessage
defaultMessage="Student"
description="Edit Modal Student header"
id="gradebook.GradesTab.EditModal.headers.student"
/>
}
value="Uiop"
/>
<HistoryHeader
id="original-grade"
label={
<FormattedMessage
defaultMessage="Original Grade"
description="Edit Modal Original Grade header"
id="gradebook.GradesTab.EditModal.headers.originalGrade"
/>
}
value={20}
/>
<HistoryHeader
id="current-grade"
label={
<FormattedMessage
defaultMessage="Current Grade"
description="Edit Modal Current Grade header"
id="gradebook.GradesTab.EditModal.headers.currentGrade"
/>
}
value={2}
/>
</div>
`;
exports[`ModalHeaders Component snapshots gradeOverrideHistoryError is empty and open is false modal closed and StatusAlert closed 1`] = `
<div>
<HistoryHeader
id="assignment"
label={
<FormattedMessage
defaultMessage="Assignment"
description="Edit Modal Assignment header"
id="gradebook.GradesTab.EditModal.headers.assignment"
/>
}
value="Qwerty"
/>
<HistoryHeader
id="student"
label={
<FormattedMessage
defaultMessage="Student"
description="Edit Modal Student header"
id="gradebook.GradesTab.EditModal.headers.student"
/>
}
value="Uiop"
/>
<HistoryHeader
id="original-grade"
label={
<FormattedMessage
defaultMessage="Original Grade"
description="Edit Modal Original Grade header"
id="gradebook.GradesTab.EditModal.headers.originalGrade"
/>
}
value={20}
/>
<HistoryHeader
id="current-grade"
label={
<FormattedMessage
defaultMessage="Current Grade"
description="Edit Modal Current Grade header"
id="gradebook.GradesTab.EditModal.headers.currentGrade"
/>
}
value={2}
/>
</div>
`;

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