Compare commits

..

4 Commits

Author SHA1 Message Date
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
136 changed files with 3491 additions and 1211 deletions

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

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

874
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "@edx/frontend-app-gradebook",
"version": "1.4.36",
"version": "1.4.43",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -18156,7 +18156,7 @@
"dependencies": {
"JSONStream": {
"version": "1.3.5",
"resolved": false,
"resolved": "",
"integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==",
"dev": true,
"requires": {
@@ -18166,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": {
@@ -18181,7 +18181,7 @@
},
"agentkeepalive": {
"version": "3.5.2",
"resolved": false,
"resolved": "",
"integrity": "sha512-e0L/HNe6qkQ7H19kTlRRqUibEAwDK5AFk6y3PtMsuut2VAH6+Q4xZml1tNDJD7kSAyqmbG/K08K5WEJYtUrSlQ==",
"dev": true,
"requires": {
@@ -18190,7 +18190,7 @@
},
"ajv": {
"version": "5.5.2",
"resolved": false,
"resolved": "",
"integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
"dev": true,
"requires": {
@@ -18202,7 +18202,7 @@
},
"ansi-align": {
"version": "2.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=",
"dev": true,
"requires": {
@@ -18211,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": {
@@ -18226,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": {
@@ -18260,7 +18260,7 @@
"dependencies": {
"readable-stream": {
"version": "2.3.6",
"resolved": false,
"resolved": "",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"dev": true,
"requires": {
@@ -18275,7 +18275,7 @@
},
"string_decoder": {
"version": "1.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
@@ -18286,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": {
@@ -18301,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,
@@ -18341,7 +18341,7 @@
},
"bin-links": {
"version": "1.1.8",
"resolved": false,
"resolved": "",
"integrity": "sha512-KgmVfx+QqggqP9dA3iIc5pA4T1qEEEL+hOhOhNPaUm77OTrJoOXE/C05SJLNJe6m/2wUK7F1tDSou7n5TfCDzQ==",
"dev": true,
"requires": {
@@ -18355,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": {
@@ -18376,7 +18376,7 @@
},
"brace-expansion": {
"version": "1.1.11",
"resolved": false,
"resolved": "",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"requires": {
@@ -18386,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": {
@@ -18433,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": {
@@ -18468,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": {
@@ -18489,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": {
@@ -18505,7 +18505,7 @@
},
"cli-table3": {
"version": "0.5.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==",
"dev": true,
"requires": {
@@ -18516,7 +18516,7 @@
},
"cliui": {
"version": "5.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
"dev": true,
"requires": {
@@ -18527,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": {
@@ -18550,7 +18550,7 @@
},
"strip-ansi": {
"version": "5.2.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"dev": true,
"requires": {
@@ -18561,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": {
@@ -18577,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": {
@@ -18598,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": {
@@ -18621,7 +18621,7 @@
},
"combined-stream": {
"version": "1.0.6",
"resolved": false,
"resolved": "",
"integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=",
"dev": true,
"requires": {
@@ -18630,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": {
@@ -18648,7 +18648,7 @@
"dependencies": {
"readable-stream": {
"version": "2.3.6",
"resolved": false,
"resolved": "",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"dev": true,
"requires": {
@@ -18663,7 +18663,7 @@
},
"string_decoder": {
"version": "1.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
@@ -18674,7 +18674,7 @@
},
"config-chain": {
"version": "1.1.12",
"resolved": false,
"resolved": "",
"integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==",
"dev": true,
"requires": {
@@ -18684,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": {
@@ -18698,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": {
@@ -18718,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
}
@@ -18732,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": {
@@ -18747,7 +18747,7 @@
},
"cross-spawn": {
"version": "5.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
"dev": true,
"requires": {
@@ -18758,7 +18758,7 @@
"dependencies": {
"lru-cache": {
"version": "4.1.5",
"resolved": false,
"resolved": "",
"integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
"dev": true,
"requires": {
@@ -18768,7 +18768,7 @@
},
"yallist": {
"version": "2.1.2",
"resolved": false,
"resolved": "",
"integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
"dev": true
}
@@ -18776,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": {
@@ -18797,7 +18797,7 @@
},
"debug": {
"version": "3.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"dev": true,
"requires": {
@@ -18806,7 +18806,7 @@
"dependencies": {
"ms": {
"version": "2.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"dev": true
}
@@ -18814,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": {
@@ -18847,7 +18847,7 @@
},
"define-properties": {
"version": "1.1.3",
"resolved": false,
"resolved": "",
"integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
"dev": true,
"requires": {
@@ -18856,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": {
@@ -18890,7 +18890,7 @@
},
"dot-prop": {
"version": "4.2.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==",
"dev": true,
"requires": {
@@ -18899,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": {
@@ -18923,7 +18923,7 @@
"dependencies": {
"readable-stream": {
"version": "2.3.6",
"resolved": false,
"resolved": "",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"dev": true,
"requires": {
@@ -18938,7 +18938,7 @@
},
"string_decoder": {
"version": "1.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
@@ -18949,7 +18949,7 @@
},
"ecc-jsbn": {
"version": "0.1.2",
"resolved": false,
"resolved": "",
"integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
"dev": true,
"optional": true,
@@ -18960,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": {
@@ -18981,7 +18981,7 @@
},
"end-of-stream": {
"version": "1.4.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==",
"dev": true,
"requires": {
@@ -18990,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": {
@@ -19011,7 +19011,7 @@
},
"es-abstract": {
"version": "1.12.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==",
"dev": true,
"requires": {
@@ -19024,7 +19024,7 @@
},
"es-to-primitive": {
"version": "1.2.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==",
"dev": true,
"requires": {
@@ -19035,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": {
@@ -19050,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": {
@@ -19071,7 +19071,7 @@
"dependencies": {
"get-stream": {
"version": "3.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=",
"dev": true
}
@@ -19079,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": {
@@ -19125,7 +19125,7 @@
"dependencies": {
"readable-stream": {
"version": "2.3.6",
"resolved": false,
"resolved": "",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"dev": true,
"requires": {
@@ -19140,7 +19140,7 @@
},
"string_decoder": {
"version": "1.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
@@ -19151,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": {
@@ -19168,7 +19168,7 @@
},
"from2": {
"version": "2.3.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=",
"dev": true,
"requires": {
@@ -19178,7 +19178,7 @@
"dependencies": {
"readable-stream": {
"version": "2.3.6",
"resolved": false,
"resolved": "",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"dev": true,
"requires": {
@@ -19193,7 +19193,7 @@
},
"string_decoder": {
"version": "1.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
@@ -19204,7 +19204,7 @@
},
"fs-minipass": {
"version": "1.2.7",
"resolved": false,
"resolved": "",
"integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==",
"dev": true,
"requires": {
@@ -19213,7 +19213,7 @@
"dependencies": {
"minipass": {
"version": "2.9.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
"dev": true,
"requires": {
@@ -19225,7 +19225,7 @@
},
"fs-vacuum": {
"version": "1.2.10",
"resolved": false,
"resolved": "",
"integrity": "sha1-t2Kb7AekAxolSP35n17PHMizHjY=",
"dev": true,
"requires": {
@@ -19236,7 +19236,7 @@
},
"fs-write-stream-atomic": {
"version": "1.0.10",
"resolved": false,
"resolved": "",
"integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=",
"dev": true,
"requires": {
@@ -19248,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": {
@@ -19269,7 +19269,7 @@
},
"string_decoder": {
"version": "1.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
@@ -19280,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": {
@@ -19308,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": {
@@ -19327,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": {
@@ -19352,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
}
@@ -19366,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": {
@@ -19381,7 +19381,7 @@
},
"getpass": {
"version": "0.1.7",
"resolved": false,
"resolved": "",
"integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
"dev": true,
"requires": {
@@ -19390,7 +19390,7 @@
},
"glob": {
"version": "7.1.6",
"resolved": false,
"resolved": "",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"dev": true,
"requires": {
@@ -19404,7 +19404,7 @@
},
"global-dirs": {
"version": "0.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=",
"dev": true,
"requires": {
@@ -19413,7 +19413,7 @@
},
"got": {
"version": "6.7.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=",
"dev": true,
"requires": {
@@ -19432,7 +19432,7 @@
"dependencies": {
"get-stream": {
"version": "3.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=",
"dev": true
}
@@ -19440,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": {
@@ -19462,7 +19462,7 @@
},
"has": {
"version": "1.0.3",
"resolved": false,
"resolved": "",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"requires": {
@@ -19471,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": {
@@ -19511,7 +19511,7 @@
},
"http-signature": {
"version": "1.2.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
"dev": true,
"requires": {
@@ -19522,7 +19522,7 @@
},
"https-proxy-agent": {
"version": "2.2.4",
"resolved": false,
"resolved": "",
"integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==",
"dev": true,
"requires": {
@@ -19532,7 +19532,7 @@
},
"humanize-ms": {
"version": "1.2.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=",
"dev": true,
"requires": {
@@ -19541,7 +19541,7 @@
},
"iconv-lite": {
"version": "0.4.23",
"resolved": false,
"resolved": "",
"integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
"dev": true,
"requires": {
@@ -19550,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": {
@@ -19565,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": {
@@ -19593,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": {
@@ -19621,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": {
@@ -19648,7 +19648,7 @@
"dependencies": {
"ci-info": {
"version": "1.6.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==",
"dev": true
}
@@ -19656,7 +19656,7 @@
},
"is-cidr": {
"version": "3.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-8Xnnbjsb0x462VoYiGlhEi+drY8SFwrHiSYuzc/CEwco55vkehTaxAyIjEdpi3EMvLPPJAJi9FlzP+h+03gp0Q==",
"dev": true,
"requires": {
@@ -19665,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": {
@@ -19680,7 +19680,7 @@
},
"is-installed-globally": {
"version": "0.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=",
"dev": true,
"requires": {
@@ -19690,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": {
@@ -19711,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": {
@@ -19726,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": {
@@ -19747,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": {
@@ -19820,7 +19820,7 @@
},
"latest-version": {
"version": "3.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=",
"dev": true,
"requires": {
@@ -19829,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": {
@@ -19858,7 +19858,7 @@
},
"libnpm": {
"version": "3.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-d7jU5ZcMiTfBqTUJVZ3xid44fE5ERBm9vBnmhp2ECD2Ls+FNXWxHSkO7gtvrnbLO78gwPdNPz1HpsF3W4rjkBQ==",
"dev": true,
"requires": {
@@ -19886,7 +19886,7 @@
},
"libnpmaccess": {
"version": "3.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha512-01512AK7MqByrI2mfC7h5j8N9V4I7MHJuk9buo8Gv+5QgThpOgpjB7sQBDDkeZqRteFb1QM/6YNdHfG7cDvfAQ==",
"dev": true,
"requires": {
@@ -19898,7 +19898,7 @@
},
"libnpmconfig": {
"version": "1.2.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-9esX8rTQAHqarx6qeZqmGQKBNZR5OIbl/Ayr0qQDy3oXja2iFVQQI81R6GZ2a02bSNZ9p3YOGX1O6HHCb1X7kA==",
"dev": true,
"requires": {
@@ -19909,7 +19909,7 @@
"dependencies": {
"find-up": {
"version": "3.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
"dev": true,
"requires": {
@@ -19918,7 +19918,7 @@
},
"locate-path": {
"version": "3.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
"dev": true,
"requires": {
@@ -19928,7 +19928,7 @@
},
"p-limit": {
"version": "2.2.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==",
"dev": true,
"requires": {
@@ -19937,7 +19937,7 @@
},
"p-locate": {
"version": "3.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
"dev": true,
"requires": {
@@ -19946,7 +19946,7 @@
},
"p-try": {
"version": "2.2.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"dev": true
}
@@ -19954,7 +19954,7 @@
},
"libnpmhook": {
"version": "5.0.3",
"resolved": false,
"resolved": "",
"integrity": "sha512-UdNLMuefVZra/wbnBXECZPefHMGsVDTq5zaM/LgKNE9Keyl5YXQTnGAzEo+nFOpdRqTWI9LYi4ApqF9uVCCtuA==",
"dev": true,
"requires": {
@@ -19966,7 +19966,7 @@
},
"libnpmorg": {
"version": "1.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-0sRUXLh+PLBgZmARvthhYXQAWn0fOsa6T5l3JSe2n9vKG/lCVK4nuG7pDsa7uMq+uTt2epdPK+a2g6btcY11Ww==",
"dev": true,
"requires": {
@@ -19978,7 +19978,7 @@
},
"libnpmpublish": {
"version": "1.1.2",
"resolved": false,
"resolved": "",
"integrity": "sha512-2yIwaXrhTTcF7bkJKIKmaCV9wZOALf/gsTDxVSu/Gu/6wiG3fA8ce8YKstiWKTxSFNC0R7isPUb6tXTVFZHt2g==",
"dev": true,
"requires": {
@@ -19995,7 +19995,7 @@
},
"libnpmsearch": {
"version": "2.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha512-VTBbV55Q6fRzTdzziYCr64+f8AopQ1YZ+BdPOv16UegIEaE8C0Kch01wo4s3kRTFV64P121WZJwgmBwrq68zYg==",
"dev": true,
"requires": {
@@ -20006,7 +20006,7 @@
},
"libnpmteam": {
"version": "1.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha512-p420vM28Us04NAcg1rzgGW63LMM6rwe+6rtZpfDxCcXxM0zUTLl7nPFEnRF3JfFBF5skF/yuZDUthTsHgde8QA==",
"dev": true,
"requires": {
@@ -20018,7 +20018,7 @@
},
"libnpx": {
"version": "10.2.4",
"resolved": false,
"resolved": "",
"integrity": "sha512-BPc0D1cOjBeS8VIBKUu5F80s6njm0wbVt7CsGMrIcJ+SI7pi7V0uVPGpEMH9H5L8csOcclTxAXFE2VAsJXUhfA==",
"dev": true,
"requires": {
@@ -20034,7 +20034,7 @@
},
"lock-verify": {
"version": "2.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-vcLpxnGvrqisKvLQ2C2v0/u7LVly17ak2YSgoK4PrdsYBXQIax19vhKiLfvKNFx7FRrpTnitrpzF/uuCMuorIg==",
"dev": true,
"requires": {
@@ -20044,7 +20044,7 @@
},
"lockfile": {
"version": "1.0.4",
"resolved": false,
"resolved": "",
"integrity": "sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA==",
"dev": true,
"requires": {
@@ -20053,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": {
@@ -20069,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": {
@@ -20090,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": {
@@ -20153,7 +20153,7 @@
},
"make-dir": {
"version": "1.3.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==",
"dev": true,
"requires": {
@@ -20162,7 +20162,7 @@
},
"make-fetch-happen": {
"version": "5.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha512-07JHC0r1ykIoruKO8ifMXu+xEU8qOXDFETylktdug6vJDACnP+HKevOu3PXyNPzFyTSlz8vrBYlBO1JZRe8Cag==",
"dev": true,
"requires": {
@@ -20181,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": {
@@ -20202,7 +20202,7 @@
},
"minimatch": {
"version": "3.0.4",
"resolved": false,
"resolved": "",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"requires": {
@@ -20211,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": {
@@ -20226,7 +20226,7 @@
"dependencies": {
"minipass": {
"version": "2.9.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
"dev": true,
"requires": {
@@ -20238,7 +20238,7 @@
},
"mississippi": {
"version": "3.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==",
"dev": true,
"requires": {
@@ -20256,7 +20256,7 @@
},
"mkdirp": {
"version": "0.5.5",
"resolved": false,
"resolved": "",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"dev": true,
"requires": {
@@ -20265,7 +20265,7 @@
"dependencies": {
"minimist": {
"version": "1.2.5",
"resolved": false,
"resolved": "",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true
}
@@ -20273,7 +20273,7 @@
},
"move-concurrently": {
"version": "1.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=",
"dev": true,
"requires": {
@@ -20287,7 +20287,7 @@
"dependencies": {
"aproba": {
"version": "1.2.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
"dev": true
}
@@ -20295,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": {
@@ -20318,7 +20318,7 @@
},
"node-gyp": {
"version": "5.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-OUTryc5bt/P8zVgNUmC6xdXiDJxLMAW8cF5tLQOT9E5sOQj+UeQxnnPy74K3CLCa/SOjjBlbuzDLR8ANwA+wmw==",
"dev": true,
"requires": {
@@ -20337,7 +20337,7 @@
},
"nopt": {
"version": "4.0.3",
"resolved": false,
"resolved": "",
"integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==",
"dev": true,
"requires": {
@@ -20347,7 +20347,7 @@
},
"normalize-package-data": {
"version": "2.5.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
"dev": true,
"requires": {
@@ -20359,7 +20359,7 @@
"dependencies": {
"resolve": {
"version": "1.10.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==",
"dev": true,
"requires": {
@@ -20370,7 +20370,7 @@
},
"npm-audit-report": {
"version": "1.3.3",
"resolved": false,
"resolved": "",
"integrity": "sha512-8nH/JjsFfAWMvn474HB9mpmMjrnKb1Hx/oTAdjv4PT9iZBvBxiZ+wtDUapHCJwLqYGQVPaAfs+vL5+5k9QndXw==",
"dev": true,
"requires": {
@@ -20380,7 +20380,7 @@
},
"npm-bundled": {
"version": "1.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==",
"dev": true,
"requires": {
@@ -20389,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": {
@@ -20404,7 +20404,7 @@
},
"npm-lifecycle": {
"version": "3.1.5",
"resolved": false,
"resolved": "",
"integrity": "sha512-lDLVkjfZmvmfvpvBzA4vzee9cn+Me4orq0QF8glbswJVEbIcSNWib7qGOffolysc3teCqbbPZZkzbr3GQZTL1g==",
"dev": true,
"requires": {
@@ -20420,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": {
@@ -20444,7 +20444,7 @@
},
"npm-packlist": {
"version": "1.4.8",
"resolved": false,
"resolved": "",
"integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==",
"dev": true,
"requires": {
@@ -20455,7 +20455,7 @@
},
"npm-pick-manifest": {
"version": "3.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha512-wNprTNg+X5nf+tDi+hbjdHhM4bX+mKqv6XmPh7B5eG+QY9VARfQPfCEH013H5GqfNj6ee8Ij2fg8yk0mzps1Vw==",
"dev": true,
"requires": {
@@ -20466,7 +20466,7 @@
},
"npm-profile": {
"version": "4.0.4",
"resolved": false,
"resolved": "",
"integrity": "sha512-Ta8xq8TLMpqssF0H60BXS1A90iMoM6GeKwsmravJ6wYjWwSzcYBTdyWa3DZCYqPutacBMEm7cxiOkiIeCUAHDQ==",
"dev": true,
"requires": {
@@ -20477,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": {
@@ -20492,7 +20492,7 @@
"dependencies": {
"safe-buffer": {
"version": "5.2.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true
}
@@ -20500,7 +20500,7 @@
},
"npm-run-path": {
"version": "2.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=",
"dev": true,
"requires": {
@@ -20509,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": {
@@ -20527,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": {
@@ -20561,7 +20561,7 @@
},
"once": {
"version": "1.4.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"requires": {
@@ -20570,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": {
@@ -20598,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": {
@@ -20616,7 +20616,7 @@
},
"pacote": {
"version": "9.5.12",
"resolved": false,
"resolved": "",
"integrity": "sha512-BUIj/4kKbwWg4RtnBncXPJd15piFSVNpTzY0rysSr3VnMowTYgkGKcaHrbReepAkjTr8lH2CVWRi58Spg2CicQ==",
"dev": true,
"requires": {
@@ -20654,7 +20654,7 @@
"dependencies": {
"minipass": {
"version": "2.9.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
"dev": true,
"requires": {
@@ -20666,7 +20666,7 @@
},
"parallel-transform": {
"version": "1.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=",
"dev": true,
"requires": {
@@ -20677,7 +20677,7 @@
"dependencies": {
"readable-stream": {
"version": "2.3.6",
"resolved": false,
"resolved": "",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"dev": true,
"requires": {
@@ -20692,7 +20692,7 @@
},
"string_decoder": {
"version": "1.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
@@ -20703,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": {
@@ -20773,7 +20773,7 @@
"dependencies": {
"retry": {
"version": "0.10.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=",
"dev": true
}
@@ -20781,7 +20781,7 @@
},
"promzard": {
"version": "0.3.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-JqXW7ox97kyxIggwWs+5O6OCqe4=",
"dev": true,
"requires": {
@@ -20790,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": {
@@ -20805,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": {
@@ -20833,7 +20833,7 @@
},
"pumpify": {
"version": "1.5.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==",
"dev": true,
"requires": {
@@ -20844,7 +20844,7 @@
"dependencies": {
"pump": {
"version": "2.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==",
"dev": true,
"requires": {
@@ -20856,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": {
@@ -20885,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": {
@@ -20903,7 +20903,7 @@
},
"read": {
"version": "1.0.7",
"resolved": false,
"resolved": "",
"integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=",
"dev": true,
"requires": {
@@ -20912,7 +20912,7 @@
},
"read-cmd-shim": {
"version": "1.0.5",
"resolved": false,
"resolved": "",
"integrity": "sha512-v5yCqQ/7okKoZZkBQUAfTsQ3sVJtXdNfbPnI5cceppoxEVLYA3k+VtV2omkeo8MS94JCy4fSiUwlRBAwCVRPUA==",
"dev": true,
"requires": {
@@ -20921,7 +20921,7 @@
},
"read-installed": {
"version": "4.0.3",
"resolved": false,
"resolved": "",
"integrity": "sha1-/5uLZ/GH0eTCm5/rMfayI6zRkGc=",
"dev": true,
"requires": {
@@ -20936,7 +20936,7 @@
},
"read-package-json": {
"version": "2.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-dAiqGtVc/q5doFz6096CcnXhpYk0ZN8dEKVkGLU0CsASt8SrgF6SF7OTKAYubfvFhWaqofl+Y8HK19GR8jwW+A==",
"dev": true,
"requires": {
@@ -20949,7 +20949,7 @@
},
"read-package-tree": {
"version": "5.3.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-mLUDsD5JVtlZxjSlPPx1RETkNjjvQYuweKwNVt1Sn8kP5Jh44pvYuUHCp6xSVDZWbNxVxG5lyZJ921aJH61sTw==",
"dev": true,
"requires": {
@@ -20960,7 +20960,7 @@
},
"readable-stream": {
"version": "3.6.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"requires": {
@@ -20971,7 +20971,7 @@
},
"readdir-scoped-modules": {
"version": "1.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==",
"dev": true,
"requires": {
@@ -20983,7 +20983,7 @@
},
"registry-auth-token": {
"version": "3.4.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==",
"dev": true,
"requires": {
@@ -20993,7 +20993,7 @@
},
"registry-url": {
"version": "3.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=",
"dev": true,
"requires": {
@@ -21002,7 +21002,7 @@
},
"request": {
"version": "2.88.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
"dev": true,
"requires": {
@@ -21030,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": {
@@ -21063,7 +21063,7 @@
},
"run-queue": {
"version": "1.0.3",
"resolved": false,
"resolved": "",
"integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=",
"dev": true,
"requires": {
@@ -21072,7 +21072,7 @@
"dependencies": {
"aproba": {
"version": "1.2.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
"dev": true
}
@@ -21080,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": {
@@ -21107,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": {
@@ -21122,7 +21122,7 @@
},
"shebang-command": {
"version": "1.2.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
"dev": true,
"requires": {
@@ -21131,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": {
@@ -21165,7 +21165,7 @@
},
"socks-proxy-agent": {
"version": "4.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg==",
"dev": true,
"requires": {
@@ -21175,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": {
@@ -21186,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": {
@@ -21202,7 +21202,7 @@
"dependencies": {
"from2": {
"version": "1.3.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-iEE7qqX5pZfP3pIh2GmGzTwGHf0=",
"dev": true,
"requires": {
@@ -21212,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": {
@@ -21230,7 +21230,7 @@
},
"string_decoder": {
"version": "0.10.31",
"resolved": false,
"resolved": "",
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
"dev": true
}
@@ -21238,7 +21238,7 @@
},
"spdx-correct": {
"version": "3.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==",
"dev": true,
"requires": {
@@ -21248,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": {
@@ -21264,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": {
@@ -21293,7 +21293,7 @@
},
"ssri": {
"version": "6.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==",
"dev": true,
"requires": {
@@ -21302,7 +21302,7 @@
},
"stream-each": {
"version": "1.2.2",
"resolved": false,
"resolved": "",
"integrity": "sha512-mc1dbFhGBxvTM3bIWmAAINbqiuAk9TATcfIQC8P+/+HJefgaiTlMn2dHvkX8qlI12KeYKSQ1Ua9RrIqrn1VPoA==",
"dev": true,
"requires": {
@@ -21312,7 +21312,7 @@
},
"stream-iterate": {
"version": "1.2.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-K9fHcpbBcCpGSIuK1B95hl7s1OE=",
"dev": true,
"requires": {
@@ -21322,7 +21322,7 @@
"dependencies": {
"readable-stream": {
"version": "2.3.6",
"resolved": false,
"resolved": "",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"dev": true,
"requires": {
@@ -21337,7 +21337,7 @@
},
"string_decoder": {
"version": "1.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
@@ -21348,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": {
@@ -21370,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": {
@@ -21393,7 +21393,7 @@
},
"string_decoder": {
"version": "1.3.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"dev": true,
"requires": {
@@ -21402,7 +21402,7 @@
"dependencies": {
"safe-buffer": {
"version": "5.2.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==",
"dev": true
}
@@ -21410,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": {
@@ -21425,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": {
@@ -21446,7 +21446,7 @@
},
"tar": {
"version": "4.4.13",
"resolved": false,
"resolved": "",
"integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==",
"dev": true,
"requires": {
@@ -21461,7 +21461,7 @@
"dependencies": {
"minipass": {
"version": "2.9.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
"dev": true,
"requires": {
@@ -21473,7 +21473,7 @@
},
"term-size": {
"version": "1.2.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=",
"dev": true,
"requires": {
@@ -21482,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": {
@@ -21504,7 +21504,7 @@
"dependencies": {
"readable-stream": {
"version": "2.3.6",
"resolved": false,
"resolved": "",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"dev": true,
"requires": {
@@ -21519,7 +21519,7 @@
},
"string_decoder": {
"version": "1.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
@@ -21530,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": {
@@ -21552,7 +21552,7 @@
},
"tunnel-agent": {
"version": "0.6.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
"dev": true,
"requires": {
@@ -21561,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": {
@@ -21595,7 +21595,7 @@
},
"unique-slug": {
"version": "2.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-22Z258fMBimHj/GWCXx4hVrp9Ks=",
"dev": true,
"requires": {
@@ -21604,7 +21604,7 @@
},
"unique-string": {
"version": "1.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=",
"dev": true,
"requires": {
@@ -21613,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": {
@@ -21643,7 +21643,7 @@
},
"url-parse-lax": {
"version": "1.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=",
"dev": true,
"requires": {
@@ -21652,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": {
@@ -21673,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": {
@@ -21689,7 +21689,7 @@
},
"validate-npm-package-name": {
"version": "3.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-X6kS2B630MdK/BQN5zF/DKffQ34=",
"dev": true,
"requires": {
@@ -21698,7 +21698,7 @@
},
"verror": {
"version": "1.10.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
"dev": true,
"requires": {
@@ -21709,7 +21709,7 @@
},
"wcwidth": {
"version": "1.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=",
"dev": true,
"requires": {
@@ -21718,7 +21718,7 @@
},
"which": {
"version": "1.3.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
"dev": true,
"requires": {
@@ -21727,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": {
@@ -21742,7 +21742,7 @@
"dependencies": {
"string-width": {
"version": "1.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"dev": true,
"requires": {
@@ -21755,7 +21755,7 @@
},
"widest-line": {
"version": "2.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==",
"dev": true,
"requires": {
@@ -21764,7 +21764,7 @@
},
"worker-farm": {
"version": "1.7.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==",
"dev": true,
"requires": {
@@ -21773,7 +21773,7 @@
},
"wrap-ansi": {
"version": "5.1.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
"dev": true,
"requires": {
@@ -21784,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": {
@@ -21807,7 +21807,7 @@
},
"strip-ansi": {
"version": "5.2.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"dev": true,
"requires": {
@@ -21818,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": {
@@ -21835,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": {
@@ -21878,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": {
@@ -21893,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": {
@@ -21909,7 +21909,7 @@
},
"p-limit": {
"version": "2.3.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"dev": true,
"requires": {
@@ -21918,7 +21918,7 @@
},
"p-locate": {
"version": "3.0.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
"dev": true,
"requires": {
@@ -21927,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": {
@@ -21944,7 +21944,7 @@
},
"strip-ansi": {
"version": "5.2.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"dev": true,
"requires": {
@@ -21955,7 +21955,7 @@
},
"yargs-parser": {
"version": "15.0.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==",
"dev": true,
"requires": {
@@ -21965,7 +21965,7 @@
"dependencies": {
"camelcase": {
"version": "5.3.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"dev": true
}
@@ -24779,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",

View File

@@ -1,6 +1,6 @@
{
"name": "@edx/frontend-app-gradebook",
"version": "1.4.39",
"version": "1.4.43",
"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",
@@ -75,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;

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

@@ -1,20 +1,22 @@
/* 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 * as appConstants from 'data/constants/app';
import selectors from 'data/selectors';
const { messages: { BulkManagementTab: messages } } = appConstants;
import messages from './messages';
/**
* <BulkManagementAlerts />
* Alerts to display at the top of the BulkManagement tab
*/
export const BulkManagementAlerts = ({ bulkImportError, uploadSuccess }) => (
export const BulkManagementAlerts = ({
bulkImportError,
uploadSuccess,
}) => (
<>
<Alert
variant="danger"
@@ -28,7 +30,7 @@ export const BulkManagementAlerts = ({ bulkImportError, uploadSuccess }) => (
show={uploadSuccess}
dismissible={false}
>
{messages.successDialog}
<FormattedMessage {...messages.successDialog} />
</Alert>
</>
);

View File

@@ -1,12 +1,17 @@
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 * as appConstants from 'data/constants/app';
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',
}));
@@ -61,8 +66,8 @@ describe('BulkManagementAlerts', () => {
});
test('open success alert with messages.successDialog content', () => {
expect(el.childAt(1).is(Alert)).toEqual(true);
expect(el.childAt(1).children().text()).toEqual(
appConstants.messages.BulkManagementTab.successDialog,
expect(el.childAt(1).children().getElement()).toEqual(
<FormattedMessage {...messages.successDialog} />,
);
expect(el.childAt(1).props().show).toEqual(true);
});

View File

@@ -3,6 +3,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import {
Button,
Form,
@@ -10,11 +12,9 @@ import {
FormGroup,
} from '@edx/paragon';
import { messages } from 'data/constants/app';
import selectors from 'data/selectors';
import thunkActions from 'data/thunkActions';
const { csvUploadLabel, importBtnText } = messages.BulkManagementTab;
import messages from './messages';
/**
* <FileUploadForm />
@@ -56,14 +56,15 @@ export class FileUploadForm extends React.Component {
}
render() {
const { gradeExportUrl } = this.props;
return (
<>
<Form action={this.props.gradeExportUrl} method="post">
<Form action={gradeExportUrl} method="post">
<FormGroup controlId="csv">
<FormControl
className="d-none"
type="file"
label={csvUploadLabel}
label={<FormattedMessage {...messages.csvUploadLabel} />}
onChange={this.handleFileInputChange}
ref={this.fileInputRef}
/>
@@ -71,7 +72,7 @@ export class FileUploadForm extends React.Component {
</Form>
<Button variant="primary" onClick={this.handleClickImportGrades}>
{importBtnText}
<FormattedMessage {...messages.importBtnText} />
</Button>
</>
);

View File

@@ -1,23 +1,25 @@
/* eslint-disable import/no-named-as-default */
import React from 'react';
import { shallow, mount } from 'enzyme';
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 * as appConstants from 'data/constants/app';
import { FileUploadForm, mapStateToProps, mapDispatchToProps } from './FileUploadForm';
const {
messages: { BulkManagementTab: messages },
} = appConstants;
import messages from './messages';
jest.mock('@edx/frontend-platform/i18n', () => ({
defineMessages: m => m,
FormattedMessage: () => 'FormattedMessage',
}));
jest.mock('data/selectors', () => ({
__esModule: true,
default: {
@@ -39,10 +41,16 @@ jest.mock('data/thunkActions', () => ({
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',
@@ -71,87 +79,105 @@ describe('FileUploadForm', () => {
});
describe('render', () => {
beforeEach(() => {
el = mount(<FileUploadForm {...props} />);
el = TestRenderer.create(
<FileUploadForm {...props} />,
{ createNodeMock: () => mockRef },
);
inst = el.root;
});
describe('alert form', () => {
let form;
beforeEach(() => {
form = el.find(Form);
form = inst.findByType(Form);
});
test('post action points to gradeExportUrl', () => {
expect(form.props().action).toEqual(props.gradeExportUrl);
expect(form.props().method).toEqual('post');
expect(form.props.action).toEqual(props.gradeExportUrl);
expect(form.props.method).toEqual('post');
});
describe('file input', () => {
let formGroup;
beforeEach(() => {
formGroup = el.find(FormGroup);
formGroup = inst.findByType(FormGroup);
});
test('group with controlId="csv"', () => {
expect(formGroup.props().controlId).toEqual('csv');
expect(formGroup.props.controlId).toEqual('csv');
});
test('file control with onChange from handleFileInputChange', () => {
const control = el.find(FormControl);
const control = inst.findByType(FormControl);
expect(
control.props().onChange,
).toEqual(el.instance().handleFileInputChange);
control.props.onChange,
).toEqual(el.getInstance().handleFileInputChange);
});
test('fileInputRef points to control', () => {
expect(el.find(FormControl).getElement().ref).toBe(el.instance().fileInputRef);
expect(
// eslint-disable-next-line no-underscore-dangle
inst.findByType(FormControl)._fiber.ref,
).toEqual(el.getInstance().fileInputRef);
});
});
});
describe('import button', () => {
let btn;
beforeEach(() => {
btn = el.find(Button);
btn = inst.findByType(Button);
});
test('handleClickImportGrade on click', () => {
expect(btn.props().onClick).toEqual(el.instance().handleClickImportGrades);
expect(btn.props.onClick).toEqual(el.getInstance().handleClickImportGrades);
});
test('text from messages.importBtn', () => {
expect(btn.children().text()).toEqual(messages.importBtnText);
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 = mount(<FileUploadForm {...props} />);
fileInput = jest.spyOn(el.instance(), 'fileInput', 'get');
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.instance().handleClickImportGrades();
el.getInstance().handleClickImportGrades();
expect(mockRef.click).not.toHaveBeenCalled();
});
it('calls fileInput.click if is loaded', () => {
const click = jest.fn();
fileInput.mockReturnValue({ click });
el.instance().handleClickImportGrades();
expect(click).toHaveBeenCalled();
el.getInstance().handleClickImportGrades();
expect(mockRef.click).toHaveBeenCalled();
});
});
describe('handleClickImportGrades', () => {
it('does nothing if file input has not loaded with files', () => {
fileInput.mockReturnValue(null);
el.instance().handleFileInputChange();
el.getInstance().handleFileInputChange();
expect(props.submitFileUploadFormData).not.toHaveBeenCalled();
fileInput.mockReturnValue({ files: [] });
el.instance().handleFileInputChange();
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.instance(), 'formData', 'get').mockReturnValue(formData);
jest.spyOn(el.getInstance(), 'formData', 'get').mockReturnValue(formData);
const submit = jest.fn(() => ({ then: (thenCB) => { thenCB(); } }));
el.setProps({
submitFileUploadFormData: submit,
});
el.instance().handleFileInputChange();
el.update(<FileUploadForm {...props} submitFileUploadFormData={submit} />);
el.getInstance().handleFileInputChange();
expect(submit).toHaveBeenCalledWith(formData);
expect(el.instance().fileInput.value).toEqual(null);
expect(el.getInstance().fileInput.value).toEqual(null);
});
});
describe('formData', () => {
@@ -161,7 +187,7 @@ describe('FileUploadForm', () => {
fileInput.mockReturnValue({ files: [file], value });
const expected = new FormData();
expected.append('csv', file);
expect([...el.instance().formData.entries()]).toEqual([...expected.entries()]);
expect([...el.getInstance().formData.entries()]).toEqual([...expected.entries()]);
});
});
});

View File

@@ -3,11 +3,14 @@ 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, messages } from 'data/constants/app';
import { bulkManagementColumns } from 'data/constants/app';
import selectors from 'data/selectors';
import ResultsSummary from './ResultsSummary';
import messages from './messages';
export const mapHistoryRows = ({
resultsSummary,
@@ -21,19 +24,19 @@ export const mapHistoryRows = ({
...rest,
});
const { hints } = messages.BulkManagementTab;
/**
* <HistoryTable />
* Table with history of bulk management uploads, including a results summary which
* displays total, skipped, and failed uploads
*/
export const HistoryTable = ({ bulkManagementHistory }) => (
export const HistoryTable = ({
bulkManagementHistory,
}) => (
<>
<p>
{hints[0]}
<FormattedMessage {...messages.hint1} />
<br />
{hints[1]}
<FormattedMessage {...messages.hint2} />
</p>
<Table
@@ -55,7 +58,6 @@ HistoryTable.propTypes = {
timeUploaded: PropTypes.string.isRequired,
resultsSummary: PropTypes.shape({
rowId: PropTypes.number.isRequired,
courseId: PropTypes.string.isRequired,
text: PropTypes.string.isRequired,
}),
})),

View File

@@ -2,13 +2,19 @@
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, messages } from 'data/constants/app';
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',
}));
@@ -61,9 +67,9 @@ describe('HistoryTable', () => {
});
test('hints with break in between', () => {
const hints = el.find('p');
expect(hints.childAt(0).text()).toEqual(messages.BulkManagementTab.hints[0]);
expect(hints.childAt(0).getElement()).toEqual(<FormattedMessage {...messages.hint1} />);
expect(hints.childAt(1).is('br')).toEqual(true);
expect(hints.childAt(2).text()).toEqual(messages.BulkManagementTab.hints[1]);
expect(hints.childAt(2).getElement()).toEqual(<FormattedMessage {...messages.hint2} />);
});
describe('history table', () => {
let table;

View File

@@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
import { Hyperlink, Icon } from '@edx/paragon';
import { Download } from '@edx/paragon/icons';
import { bulkGradesUrlByCourseAndRow } from 'data/constants/api';
import lms from 'data/services/lms';
/**
* <ResultsSummary {...{ courseId, rowId, text }} />
@@ -15,12 +15,11 @@ import { bulkGradesUrlByCourseAndRow } from 'data/constants/api';
* @param {string} text - summary string
*/
const ResultsSummary = ({
courseId,
rowId,
text,
}) => (
<Hyperlink
href={bulkGradesUrlByCourseAndRow(courseId, rowId)}
href={lms.urls.bulkGradesUrlByRow(rowId)}
destination="www.edx.org"
target="_blank"
rel="noopener noreferrer"
@@ -32,7 +31,6 @@ const ResultsSummary = ({
);
ResultsSummary.propTypes = {
courseId: PropTypes.string.isRequired,
rowId: PropTypes.number.isRequired,
text: PropTypes.string.isRequired,
};

View File

@@ -4,7 +4,7 @@ import { shallow } from 'enzyme';
import { Icon } from '@edx/paragon';
import { Download } from '@edx/paragon/icons';
import * as api from 'data/constants/api';
import lms from 'data/services/lms';
import ResultsSummary from './ResultsSummary';
jest.mock('@edx/paragon', () => ({
@@ -14,13 +14,14 @@ jest.mock('@edx/paragon', () => ({
jest.mock('@edx/paragon/icons', () => ({
Download: 'DownloadIcon',
}));
jest.mock('data/constants/api', () => ({
bulkGradesUrlByCourseAndRow: jest.fn((courseId, rowId) => ({ url: { courseId, rowId } })),
jest.mock('data/services/lms', () => ({
urls: {
bulkGradesUrlByRow: jest.fn((rowId) => ({ url: { rowId } })),
},
}));
describe('ResultsSummary component', () => {
const props = {
courseId: 'classy',
rowId: 42,
text: 'texty',
};
@@ -41,7 +42,7 @@ describe('ResultsSummary component', () => {
expect(el.props().rel).toEqual('noopener noreferrer');
});
test('Hyperlink has href to bulkGradesUrl', () => {
expect(el.props().href).toEqual(api.bulkGradesUrlByCourseAndRow(props.courseId, props.rowId));
expect(el.props().href).toEqual(lms.urls.bulkGradesUrlByRow(props.rowId));
});
test('displays Download Icon and text', () => {
const icon = el.childAt(0);

View File

@@ -12,7 +12,11 @@ exports[`BulkManagementAlerts component no errer, no upload success snapshot - b
show={false}
variant="success"
>
CSV processing. File uploads may take several minutes to complete.
<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>
`;
@@ -31,7 +35,11 @@ exports[`BulkManagementAlerts component no errer, no upload success snapshot - d
show={true}
variant="success"
>
CSV processing. File uploads may take several minutes to complete.
<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

@@ -16,7 +16,13 @@ exports[`FileUploadForm component snapshot snapshot - loads export form w/ alert
<ForwardRef
as="input"
className="d-none"
label="Upload Grade CSV"
label={
<FormattedMessage
defaultMessage="Upload Grade CSV"
description="Button in BulkManagementTab Alerts"
id="gradebook.BulkManagementTab.csvUploadLabel"
/>
}
onChange={[MockFunction this.handleFileInputChange]}
plaintext={false}
type="file"
@@ -29,7 +35,11 @@ exports[`FileUploadForm component snapshot snapshot - loads export form w/ alert
onClick={[MockFunction this.handleClickImportGrades]}
variant="primary"
>
Import Grades
<FormattedMessage
defaultMessage="Import Grades"
description="Button in BulkManagement Tab File Upload Form"
id="gradebook.BulkManagementTab.importBtnText"
/>
</ForwardRef>
</React.Fragment>
`;

View File

@@ -44,9 +44,17 @@ Array [
exports[`HistoryTable component snapshot snapshot - loads hints display, formatted table 1`] = `
<Fragment>
<p>
Results appear in the table below.
<FormattedMessage
defaultMessage="Results appear in the table below."
description="Hint text on BulkManagement Tab History Table"
id="gradebook.BulkManagementTab.hint1"
/>
<br />
Grade processing may take a few seconds.
<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"

View File

@@ -6,7 +6,6 @@ exports[`ResultsSummary component snapshot - safe hyperlink with bulkGradesUrl w
href={
Object {
"url": Object {
"courseId": "classy",
"rowId": 42,
},
}

View File

@@ -3,7 +3,11 @@
exports[`BulkManagementTab component snapshot snapshot - loads heading from messages.BulkManagementTab.heading, <BulkManagementAlerts />, <FileUploadForm />, <HistoryTable /> 1`] = `
<div>
<h4>
Use this feature by downloading a CSV for bulk management, overriding grades locally, and coming back here to upload.
<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 />

View File

@@ -1,7 +1,8 @@
/* 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 'data/constants/app';
import messages from './messages';
import BulkManagementAlerts from './BulkManagementAlerts';
import FileUploadForm from './FileUploadForm';
import HistoryTable from './HistoryTable';
@@ -12,7 +13,7 @@ import HistoryTable from './HistoryTable';
*/
export const BulkManagementTab = () => (
<div>
<h4>{messages.BulkManagementTab.heading}</h4>
<h4><FormattedMessage {...(messages.heading)} /></h4>
<BulkManagementAlerts />
<FileUploadForm />
<HistoryTable />

View File

@@ -1,12 +1,13 @@
/* eslint-disable import/no-named-as-default */
import React from 'react';
import { shallow } from 'enzyme';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { messages } from 'data/constants/app';
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');
@@ -30,7 +31,11 @@ describe('BulkManagementTab', () => {
});
test('heading - h4 loaded from messages', () => {
const heading = el.find('h4');
expect(heading.text()).toEqual(messages.BulkManagementTab.heading);
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);

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

@@ -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,10 +3,13 @@ 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 actions from 'data/actions';
import thunkActions from 'data/thunkActions';
import messages from '../messages';
import SelectGroup from '../SelectGroup';
const { fetchGradesIfAssignmentGradeFiltersSet } = thunkActions.grades;
@@ -46,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}
@@ -64,7 +67,6 @@ AssignmentFilter.defaultProps = {
AssignmentFilter.propTypes = {
updateQueryParams: PropTypes.func.isRequired,
// redux
assignmentFilterOptions: PropTypes.arrayOf(PropTypes.shape({
label: PropTypes.string,

View File

@@ -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,
@@ -87,6 +86,30 @@ describe('AssignmentFilter', () => {
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();
});
});
});
});
describe('snapshots', () => {

View File

@@ -7,14 +7,26 @@ 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="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="98"
/>
@@ -42,14 +54,26 @@ 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="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="98"
/>

View File

@@ -3,12 +3,14 @@ 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 {
@@ -34,19 +36,21 @@ export class AssignmentGradeFilter extends React.Component {
}
render() {
const { assignmentGradeMin, assignmentGradeMax } = this.props.localAssignmentLimits;
const {
localAssignmentLimits: { assignmentGradeMax, assignmentGradeMin },
} = this.props;
return (
<div className="grade-filter-inputs">
<PercentGroup
id="assignmentGradeMin"
label="Min Grade"
label={<FormattedMessage {...messages.minGrade} />}
value={assignmentGradeMin}
disabled={!this.props.selectedAssignment}
onChange={this.handleSetMin}
/>
<PercentGroup
id="assignmentGradeMax"
label="Max Grade"
label={<FormattedMessage {...messages.maxGrade} />}
value={assignmentGradeMax}
disabled={!this.props.selectedAssignment}
onChange={this.handleSetMax}

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,9 +3,13 @@ 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 actions from 'data/actions';
import SelectGroup from '../SelectGroup';
import messages from '../messages';
export class AssignmentTypeFilter extends React.Component {
constructor(props) {
@@ -34,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}

View File

@@ -7,13 +7,25 @@ exports[`CourseGradeFilter Component snapshots basic snapshot 1`] = `
>
<PercentGroup
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
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

@@ -2,13 +2,15 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import {
Button,
} from '@edx/paragon';
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 {
@@ -41,19 +43,21 @@ export class CourseGradeFilter extends React.Component {
}
render() {
const { courseGradeMin, courseGradeMax } = this.props.localCourseLimits;
const {
localCourseLimits: { courseGradeMin, courseGradeMax },
} = this.props;
return (
<>
<div className="grade-filter-inputs">
<PercentGroup
id="minimum-grade"
label="Min Grade"
label={<FormattedMessage {...messages.minGrade} />}
value={courseGradeMin}
onChange={this.handleUpdateMin}
/>
<PercentGroup
id="maximum-grade"
label="Max Grade"
label={<FormattedMessage {...messages.maxGrade} />}
value={courseGradeMax}
onChange={this.handleUpdateMax}
/>

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

@@ -3,10 +3,13 @@ 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 }) => [
@@ -28,7 +31,7 @@ export class StudentGroupsFilter extends React.Component {
mapCohortsEntries() {
return optionFactory({
data: this.props.cohorts,
defaultOption: 'Cohort-All',
defaultOption: this.translate(messages.cohortAll),
key: 'id',
});
}
@@ -36,7 +39,7 @@ export class StudentGroupsFilter extends React.Component {
mapTracksEntries() {
return optionFactory({
data: this.props.tracks,
defaultOption: 'Track-All',
defaultOption: this.translate(messages.trackAll),
key: 'slug',
});
}
@@ -65,19 +68,23 @@ export class StudentGroupsFilter extends React.Component {
this.props.fetchGrades();
}
translate(message) {
return this.props.intl.formatMessage(message);
}
render() {
return (
<>
<SelectGroup
id="Tracks"
label="Tracks"
label={this.translate(messages.tracks)}
value={this.props.selectedTrackEntry.name}
onChange={this.updateTracks}
options={this.mapTracksEntries()}
/>
<SelectGroup
id="Cohorts"
label="Cohorts"
label={this.translate(messages.cohorts)}
value={this.props.selectedCohortEntry.name}
disabled={this.props.cohorts.length === 0}
onChange={this.updateCohorts}
@@ -100,6 +107,9 @@ StudentGroupsFilter.defaultProps = {
StudentGroupsFilter.propTypes = {
updateQueryParams: PropTypes.func.isRequired,
// injected
intl: intlShape.isRequired,
// redux
cohorts: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string,
@@ -139,4 +149,4 @@ export const mapDispatchToProps = {
updateTrack: actions.filters.update.track,
};
export default connect(mapStateToProps, mapDispatchToProps)(StudentGroupsFilter);
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(StudentGroupsFilter));

View File

@@ -63,6 +63,7 @@ describe('StudentGroupsFilter', () => {
beforeEach(() => {
props = {
...props,
intl: { formatMessage: (msg) => msg.defaultMessage },
cohortsByName: {
[props.cohorts[0].name]: props.cohorts[0],
[props.cohorts[1].name]: props.cohorts[1],

View File

@@ -22,7 +22,13 @@ exports[`GradebookFilters Component snapshots basic snapshot 1`] = `
<Collapsible
className="filter-group mb-3"
defaultOpen={true}
title="Assignments"
title={
<FormattedMessage
defaultMessage="Assignments"
description="Assignment filter group label in Gradebook Filters"
id="gradebook.GradebookFilters.assignmentsFilterLabel"
/>
}
>
<div>
<Connect(AssignmentTypeFilter)
@@ -39,7 +45,13 @@ exports[`GradebookFilters Component snapshots basic snapshot 1`] = `
<Collapsible
className="filter-group mb-3"
defaultOpen={true}
title="Overall Grade"
title={
<FormattedMessage
defaultMessage="Overall Grade"
description="Overall Grade filter group label in Gradebook Filters"
id="gradebook.GradebookFilters.overallGradeFilterLabel"
/>
}
>
<Connect(CourseGradeFilter)
updateQueryParams={[MockFunction]}
@@ -48,22 +60,38 @@ exports[`GradebookFilters Component snapshots basic snapshot 1`] = `
<Collapsible
className="filter-group mb-3"
defaultOpen={true}
title="Student Groups"
title={
<FormattedMessage
defaultMessage="Student Groups"
description="Student Groups filter group label in Gradebook Filters"
id="gradebook.GradebookFilters.studentGroupsFilterLabel"
/>
}
>
<Connect(StudentGroupsFilter)
<InjectIntl(ShimmedIntlComponent)
updateQueryParams={[MockFunction]}
/>
</Collapsible>
<Collapsible
className="filter-group mb-3"
defaultOpen={true}
title="Include Course Team Members"
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]}
>
Include Course Team Members
<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

@@ -10,11 +10,13 @@ import {
Form,
} from '@edx/paragon';
import { Close } from '@edx/paragon/icons';
import { FormattedMessage, 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 AssignmentTypeFilter from './AssignmentTypeFilter';
import AssignmentFilter from './AssignmentFilter';
import AssignmentGradeFilter from './AssignmentGradeFilter';
@@ -39,13 +41,18 @@ export class GradebookFilters extends React.Component {
}
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 {
intl,
updateQueryParams,
} = this.props;
return (
@@ -57,12 +64,12 @@ export class GradebookFilters extends React.Component {
onClick={this.props.closeMenu}
iconAs={Icon}
src={Close}
alt="Close Filters"
aria-label="Close Filters"
alt={intl.formatMessage(messages.closeFilters)}
aria-label={intl.formatMessage(messages.closeFilters)}
/>
</div>
{this.collapsibleGroup('Assignments', (
{this.collapsibleGroup(messages.assignments, (
<div>
<AssignmentTypeFilter updateQueryParams={updateQueryParams} />
<AssignmentFilter updateQueryParams={updateQueryParams} />
@@ -70,20 +77,20 @@ export class GradebookFilters extends React.Component {
</div>
))}
{this.collapsibleGroup('Overall Grade', (
{this.collapsibleGroup(messages.overallGrade, (
<CourseGradeFilter updateQueryParams={updateQueryParams} />
))}
{this.collapsibleGroup('Student Groups', (
{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>
))}
</>
@@ -95,6 +102,8 @@ GradebookFilters.defaultProps = {
};
GradebookFilters.propTypes = {
updateQueryParams: PropTypes.func.isRequired,
// injected
intl: intlShape.isRequired,
// redux
closeMenu: PropTypes.func.isRequired,
fetchGrades: PropTypes.func.isRequired,
@@ -112,4 +121,4 @@ export const mapDispatchToProps = {
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

@@ -46,6 +46,7 @@ describe('GradebookFilters', () => {
beforeEach(() => {
props = {
...props,
intl: { formatMessage: (msg) => msg.defaultMessage },
closeMenu: jest.fn().mockName('this.props.closeMenu'),
fetchGrades: jest.fn(),
updateIncludeCourseRoleMembers: jest.fn(),

View File

@@ -13,20 +13,31 @@ exports[`GradebookHeader component snapshots default values (grades frozen, cann
>
&lt;&lt;
</span>
Back to Dashboard
<FormattedMessage
defaultMessage="Back to Dashboard"
description="Button text to take user back to LMS dashboard in Gradebook Header"
id="gradebook.GradebookHeader.backButton"
/>
</a>
<h1>
Gradebook
<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"
>
You are not authorized to view the gradebook for this course.
<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>
`;
@@ -44,20 +55,31 @@ exports[`GradebookHeader component snapshots grades frozen, can view. grades fro
>
&lt;&lt;
</span>
Back to Dashboard
<FormattedMessage
defaultMessage="Back to Dashboard"
description="Button text to take user back to LMS dashboard in Gradebook Header"
id="gradebook.GradebookHeader.backButton"
/>
</a>
<h1>
Gradebook
<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"
>
The grades for this course are now frozen. Editing of grades is no longer allowed.
<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>
`;
@@ -75,26 +97,41 @@ exports[`GradebookHeader component snapshots grades frozen, cannot view unauthor
>
&lt;&lt;
</span>
Back to Dashboard
<FormattedMessage
defaultMessage="Back to Dashboard"
description="Button text to take user back to LMS dashboard in Gradebook Header"
id="gradebook.GradebookHeader.backButton"
/>
</a>
<h1>
Gradebook
<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"
>
The grades for this course are now frozen. Editing of grades is no longer allowed.
<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"
>
You are not authorized to view the gradebook for this course.
<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

@@ -2,9 +2,13 @@ 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`
@@ -17,19 +21,22 @@ export class GradebookHeader extends React.Component {
href={this.lmsInstructorDashboardUrl(this.props.courseId)}
className="mb-3"
>
<span aria-hidden="true">{'<< '}</span> Back to Dashboard
<span aria-hidden="true">{'<< '}</span>
<FormattedMessage {...messages.backToDashboard} />
</a>
<h1>Gradebook</h1>
<h3> {this.props.courseId}</h3>
<h1>
<FormattedMessage {...messages.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.
<FormattedMessage {...messages.frozenWarning} />
</div>
)}
{(this.props.canUserViewGradebook === false) && (
<div className="alert alert-warning" role="alert">
You are not authorized to view the gradebook for this course.
<FormattedMessage {...messages.unauthorizedWarning} />
</div>
)}
</div>

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

@@ -4,6 +4,11 @@ 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: {

View File

@@ -4,11 +4,14 @@ 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: {
@@ -63,11 +66,11 @@ export class BulkManagementControls extends React.Component {
return this.props.showBulkManagement && (
<div>
<StatefulButton
{...this.buttonProps('Bulk Management')}
{...this.buttonProps(<FormattedMessage {...messages.bulkManagement} />)}
onClick={this.handleClickExportGrades}
/>
<StatefulButton
{...this.buttonProps('Interventions')}
{...this.buttonProps(<FormattedMessage {...messages.interventions} />)}
onClick={this.handleClickDownloadInterventions}
/>
</div>

View File

@@ -19,7 +19,7 @@ HistoryHeader.defaultProps = {
};
HistoryHeader.propTypes = {
id: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
label: PropTypes.node.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};

View File

@@ -2,7 +2,11 @@ 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';
/**
@@ -18,22 +22,22 @@ export const ModalHeaders = ({
<div>
<HistoryHeader
id="assignment"
label="Assignment"
label={<FormattedMessage {...messages.assignmentHeader} />}
value={modalState.assignmentName}
/>
<HistoryHeader
id="student"
label="Student"
label={<FormattedMessage {...messages.studentHeader} />}
value={modalState.updateUserName}
/>
<HistoryHeader
id="original-grade"
label="Original Grade"
label={<FormattedMessage {...messages.originalGradeHeader} />}
value={originalGrade}
/>
<HistoryHeader
id="current-grade"
label="Current Grade"
label={<FormattedMessage {...messages.currentGradeHeader} />}
value={currentGrade}
/>
</div>

View File

@@ -6,19 +6,35 @@ exports[`OverrideTable Component snapshots basic snapshot shows a row for each e
Array [
Object {
"key": "date",
"label": "Date",
"label": <FormattedMessage
defaultMessage="Date"
description="Edit Modal Override Table Date column header"
id="gradebook.GradesTab.EditModal.Overrides.dateHeader"
/>,
},
Object {
"key": "grader",
"label": "Grader",
"label": <FormattedMessage
defaultMessage="Grader"
description="Edit Modal Override Table Grader column header"
id="gradebook.GradesTab.EditModal.Overrides.graderHeader"
/>,
},
Object {
"key": "reason",
"label": "Reason",
"label": <FormattedMessage
defaultMessage="Reason"
description="Edit Modal Override Table Reason column header"
id="gradebook.GradesTab.EditModal.Overrides.reasonHeader"
/>,
},
Object {
"key": "adjustedGrade",
"label": "Adjusted grade",
"label": <FormattedMessage
defaultMessage="Adjusted grade"
description="Edit Modal Override Table Adjusted grade column header"
id="gradebook.GradesTab.EditModal.Overrides.adjustedGradeHeader"
/>,
},
]
}

View File

@@ -4,18 +4,15 @@ 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';
const GRADE_OVERRIDE_HISTORY_COLUMNS = [
{ label: 'Date', key: 'date' },
{ label: 'Grader', key: 'grader' },
{ label: 'Reason', key: 'reason' },
{ label: 'Adjusted grade', key: 'adjustedGrade' },
];
/**
* <OverrideTable />
* Table containing previous grade override entries, and an "edit" row
@@ -31,7 +28,15 @@ export const OverrideTable = ({
}
return (
<Table
columns={GRADE_OVERRIDE_HISTORY_COLUMNS}
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,
{

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

@@ -4,22 +4,46 @@ exports[`ModalHeaders Component snapshots gradeOverrideHistoryError is and empty
<div>
<HistoryHeader
id="assignment"
label="Assignment"
label={
<FormattedMessage
defaultMessage="Assignment"
description="Edit Modal Assignment header"
id="gradebook.GradesTab.EditModal.headers.assignment"
/>
}
value="Qwerty"
/>
<HistoryHeader
id="student"
label="Student"
label={
<FormattedMessage
defaultMessage="Student"
description="Edit Modal Student header"
id="gradebook.GradesTab.EditModal.headers.student"
/>
}
value="Uiop"
/>
<HistoryHeader
id="original-grade"
label="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="Current Grade"
label={
<FormattedMessage
defaultMessage="Current Grade"
description="Edit Modal Current Grade header"
id="gradebook.GradesTab.EditModal.headers.currentGrade"
/>
}
value={2}
/>
</div>
@@ -29,22 +53,46 @@ exports[`ModalHeaders Component snapshots gradeOverrideHistoryError is empty and
<div>
<HistoryHeader
id="assignment"
label="Assignment"
label={
<FormattedMessage
defaultMessage="Assignment"
description="Edit Modal Assignment header"
id="gradebook.GradesTab.EditModal.headers.assignment"
/>
}
value="Qwerty"
/>
<HistoryHeader
id="student"
label="Student"
label={
<FormattedMessage
defaultMessage="Student"
description="Edit Modal Student header"
id="gradebook.GradesTab.EditModal.headers.student"
/>
}
value="Uiop"
/>
<HistoryHeader
id="original-grade"
label="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="Current Grade"
label={
<FormattedMessage
defaultMessage="Current Grade"
description="Edit Modal Current Grade header"
id="gradebook.GradesTab.EditModal.headers.currentGrade"
/>
}
value={2}
/>
</div>

View File

@@ -13,10 +13,18 @@ exports[`EditMoal Component snapshots gradeOverrideHistoryError is and empty and
/>
<OverrideTable />
<div>
Showing most recent actions (max 5). To see more, please contact support.
<FormattedMessage
defaultMessage="Showing most recent actions (max 5). To see more, please contact support"
description="Edit Modal visibility hint message"
id="gradebook.GradesTab.EditModal.contactSupport"
/>
</div>
<div>
Note: Once you save, your changes will be visible to students.
<FormattedMessage
defaultMessage="Note: Once you save, your changes will be visible to students."
description="Edit Modal saved changes effect hint"
id="gradebook.GradesTab.EditModal.saveVisibility"
/>
</div>
</div>
}
@@ -26,14 +34,30 @@ exports[`EditMoal Component snapshots gradeOverrideHistoryError is and empty and
onClick={[MockFunction this.handleAdjustedGradeClick]}
variant="primary"
>
Save Grade
<FormattedMessage
defaultMessage="Save Grades"
description="Edit Modal Save button label"
id="gradebook.GradesTab.EditModal.saveGrade"
/>
</Button>,
]
}
closeText="Cancel"
closeText={
<FormattedMessage
defaultMessage="Cancel"
description="Edit Modal close button text"
id="gradebook.GradesTab.EditModal.closeText"
/>
}
onClose={[MockFunction this.closeAssignmentModal]}
open={true}
title="Edit Grades"
title={
<FormattedMessage
defaultMessage="Edit Grades"
description="Edit Modal title"
id="gradebook.GradesTab.EditModal.title"
/>
}
/>
`;
@@ -50,10 +74,18 @@ exports[`EditMoal Component snapshots gradeOverrideHistoryError is empty and ope
/>
<OverrideTable />
<div>
Showing most recent actions (max 5). To see more, please contact support.
<FormattedMessage
defaultMessage="Showing most recent actions (max 5). To see more, please contact support"
description="Edit Modal visibility hint message"
id="gradebook.GradesTab.EditModal.contactSupport"
/>
</div>
<div>
Note: Once you save, your changes will be visible to students.
<FormattedMessage
defaultMessage="Note: Once you save, your changes will be visible to students."
description="Edit Modal saved changes effect hint"
id="gradebook.GradesTab.EditModal.saveVisibility"
/>
</div>
</div>
}
@@ -63,13 +95,29 @@ exports[`EditMoal Component snapshots gradeOverrideHistoryError is empty and ope
onClick={[MockFunction this.handleAdjustedGradeClick]}
variant="primary"
>
Save Grade
<FormattedMessage
defaultMessage="Save Grades"
description="Edit Modal Save button label"
id="gradebook.GradesTab.EditModal.saveGrade"
/>
</Button>,
]
}
closeText="Cancel"
closeText={
<FormattedMessage
defaultMessage="Cancel"
description="Edit Modal close button text"
id="gradebook.GradesTab.EditModal.closeText"
/>
}
onClose={[MockFunction this.closeAssignmentModal]}
open={false}
title="Edit Grades"
title={
<FormattedMessage
defaultMessage="Edit Grades"
description="Edit Modal title"
id="gradebook.GradesTab.EditModal.title"
/>
}
/>
`;

View File

@@ -8,11 +8,13 @@ import {
Modal,
StatusAlert,
} 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 OverrideTable from './OverrideTable';
import ModalHeaders from './ModalHeaders';
@@ -46,8 +48,8 @@ export class EditModal extends React.Component {
return (
<Modal
open={this.props.open}
title="Edit Grades"
closeText="Cancel"
title={<FormattedMessage {...messages.title} />}
closeText={<FormattedMessage {...messages.closeText} />}
body={(
<div>
<ModalHeaders />
@@ -58,15 +60,13 @@ export class EditModal extends React.Component {
dismissible={false}
/>
<OverrideTable />
<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><FormattedMessage {...messages.visibility} /></div>
<div><FormattedMessage {...messages.saveVisibility} /></div>
</div>
)}
buttons={[
<Button variant="primary" onClick={this.handleAdjustedGradeClick}>
Save Grade
<FormattedMessage {...messages.saveGrade} />
</Button>,
]}
onClose={this.closeAssignmentModal}

View File

@@ -0,0 +1,51 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
assignmentHeader: {
id: 'gradebook.GradesTab.EditModal.headers.assignment',
defaultMessage: 'Assignment',
description: 'Edit Modal Assignment header',
},
currentGradeHeader: {
id: 'gradebook.GradesTab.EditModal.headers.currentGrade',
defaultMessage: 'Current Grade',
description: 'Edit Modal Current Grade header',
},
originalGradeHeader: {
id: 'gradebook.GradesTab.EditModal.headers.originalGrade',
defaultMessage: 'Original Grade',
description: 'Edit Modal Original Grade header',
},
studentHeader: {
id: 'gradebook.GradesTab.EditModal.headers.student',
defaultMessage: 'Student',
description: 'Edit Modal Student header',
},
title: {
id: 'gradebook.GradesTab.EditModal.title',
defaultMessage: 'Edit Grades',
description: 'Edit Modal title',
},
closeText: {
id: 'gradebook.GradesTab.EditModal.closeText',
defaultMessage: 'Cancel',
description: 'Edit Modal close button text',
},
visibility: {
id: 'gradebook.GradesTab.EditModal.contactSupport',
defaultMessage: 'Showing most recent actions (max 5). To see more, please contact support',
description: 'Edit Modal visibility hint message',
},
saveVisibility: {
id: 'gradebook.GradesTab.EditModal.saveVisibility',
defaultMessage: 'Note: Once you save, your changes will be visible to students.',
description: 'Edit Modal saved changes effect hint',
},
saveGrade: {
id: 'gradebook.GradesTab.EditModal.saveGrade',
defaultMessage: 'Save Grades',
description: 'Edit Modal Save button label',
},
});
export default messages;

View File

@@ -3,6 +3,7 @@ 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';
@@ -15,7 +16,6 @@ import selectors from 'data/selectors';
* @param {string} filterName - api filter name (for redux connector)
*/
export const FilterBadge = ({
handleClose,
config: {
displayName,
isDefault,
@@ -23,11 +23,15 @@ export const FilterBadge = ({
value,
connectedFilters,
},
handleClose,
}) => !isDefault && (
<div>
<span className="badge badge-info">
<span>
{displayName}{!hideValue && `: ${value}`}
<FormattedMessage {...displayName} />
</span>
<span>
{!hideValue ? `: ${value}` : ''}
</span>
<Button
className="btn-info"
@@ -48,7 +52,9 @@ FilterBadge.propTypes = {
// redux
config: PropTypes.shape({
connectedFilters: PropTypes.arrayOf(PropTypes.string),
displayName: PropTypes.string.isRequired,
displayName: PropTypes.shape({
defaultMessage: PropTypes.string,
}).isRequired,
isDefault: PropTypes.bool.isRequired,
hideValue: PropTypes.bool,
value: PropTypes.oneOfType([

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { shallow } from 'enzyme';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Button } from '@edx/paragon';
import selectors from 'data/selectors';
@@ -20,7 +21,9 @@ jest.mock('data/selectors', () => ({
describe('FilterBadge', () => {
describe('component', () => {
const config = {
displayName: 'a common name',
displayName: {
defaultMessage: 'a common name',
},
isDefault: false,
hideValue: false,
value: 'a common value',
@@ -58,7 +61,11 @@ describe('FilterBadge', () => {
expect(el).toMatchSnapshot();
});
it('shows displayName but not value in span', () => {
expect(el.find('span.badge').childAt(0).text()).toEqual(config.displayName);
expect(el.find('span.badge').childAt(0).getElement()).toEqual(
<span>
<FormattedMessage {...config.displayName} />
</span>,
);
});
it('calls a handleClose event for connected filters on button click', () => {
expect(el.find(Button).props().onClick).toEqual(handleClose(config.connectedFilters));
@@ -72,8 +79,15 @@ describe('FilterBadge', () => {
expect(el).toMatchSnapshot();
});
it('shows displayName and value in span', () => {
expect(el.find('span.badge').childAt(0).text()).toEqual(
`${config.displayName}: ${config.value}`,
expect(el.find('span.badge').childAt(0).getElement()).toEqual(
<span>
<FormattedMessage {...config.displayName} />
</span>,
);
expect(el.find('span.badge').childAt(1).getElement()).toEqual(
<span>
{`: ${config.value}`}
</span>,
);
});
it('calls a handleClose event for connected filters on button click', () => {

View File

@@ -8,7 +8,11 @@ exports[`FilterBadge component with non-default value (active) if hideValue is f
className="badge badge-info"
>
<span>
a common name
<FormattedMessage
defaultMessage="a common name"
/>
</span>
<span>
: a common value
</span>
<Button
@@ -40,8 +44,11 @@ exports[`FilterBadge component with non-default value (active) if hideValue is t
className="badge badge-info"
>
<span>
a common name
<FormattedMessage
defaultMessage="a common name"
/>
</span>
<span />
<Button
aria-label="close"
className="btn-info"

View File

@@ -7,8 +7,9 @@ import {
OverlayTrigger,
Tooltip,
} from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Headings } from 'data/constants/grades';
import messages from './messages';
export const totalGradePercentageMessage = 'Total Grade values are always displayed as a percentage.';
@@ -23,12 +24,21 @@ const TotalGradeLabelReplacement = () => (
trigger={['hover', 'focus']}
key="left-basic"
placement="left"
overlay={(<Tooltip id="course-grade-tooltip">{totalGradePercentageMessage}</Tooltip>)}
overlay={(
<Tooltip id="course-grade-tooltip">
<FormattedMessage {...messages.totalGradePercentage} />
</Tooltip>
)}
>
<div>
{Headings.totalGrade}
<FormattedMessage {...messages.totalGradeHeading} />
<div id="courseGradeTooltipIcon">
<Icon className="fa fa-info-circle" screenReaderText={totalGradePercentageMessage} />
<Icon
className="fa fa-info-circle"
screenReaderText={(
<FormattedMessage {...messages.totalGradePercentage} />
)}
/>
</div>
</div>
</OverlayTrigger>
@@ -41,8 +51,12 @@ const TotalGradeLabelReplacement = () => (
*/
const UsernameLabelReplacement = () => (
<div>
<div>Username</div>
<div className="font-weight-normal student-key">Student Key*</div>
<div>
<FormattedMessage {...messages.usernameHeading} />
</div>
<div className="font-weight-normal student-key">
<FormattedMessage {...messages.studentKeyLabel} />
</div>
</div>
);

View File

@@ -4,7 +4,11 @@ exports[`LabelReplacements TotalGradeLabelReplacement displays overlay tooltip 1
<Tooltip
id="course-grade-tooltip"
>
Total Grade values are always displayed as a percentage.
<FormattedMessage
defaultMessage="Total Grade values are always displayed as a percentage"
description="Gradebook table message that total grades are displayed in percent format"
id="gradebook.GradesTab.table.totalGradePercentage"
/>
</Tooltip>
`;
@@ -16,7 +20,11 @@ exports[`LabelReplacements TotalGradeLabelReplacement snapshot 1`] = `
<Tooltip
id="course-grade-tooltip"
>
Total Grade values are always displayed as a percentage.
<FormattedMessage
defaultMessage="Total Grade values are always displayed as a percentage"
description="Gradebook table message that total grades are displayed in percent format"
id="gradebook.GradesTab.table.totalGradePercentage"
/>
</Tooltip>
}
placement="left"
@@ -28,13 +36,23 @@ exports[`LabelReplacements TotalGradeLabelReplacement snapshot 1`] = `
}
>
<div>
Total Grade (%)
<FormattedMessage
defaultMessage="Total Grade (%)"
description="Gradebook table total grade column header"
id="gradebook.GradesTab.table.headings.totalGrade"
/>
<div
id="courseGradeTooltipIcon"
>
<Icon
className="fa fa-info-circle"
screenReaderText="Total Grade values are always displayed as a percentage."
screenReaderText={
<FormattedMessage
defaultMessage="Total Grade values are always displayed as a percentage"
description="Gradebook table message that total grades are displayed in percent format"
id="gradebook.GradesTab.table.totalGradePercentage"
/>
}
/>
</div>
</div>
@@ -45,12 +63,20 @@ exports[`LabelReplacements TotalGradeLabelReplacement snapshot 1`] = `
exports[`LabelReplacements UsernameLabelReplacement snapshot 1`] = `
<div>
<div>
Username
<FormattedMessage
defaultMessage="Username"
description="Gradebook table username column header"
id="gradebook.GradesTab.table.headings.username"
/>
</div>
<div
className="font-weight-normal student-key"
>
Student Key*
<FormattedMessage
defaultMessage="Student Key*"
description="Gradebook table Student Key label"
id="gradebook.GradesTab.table.labels.studentKey"
/>
</div>
</div>
`;

View File

@@ -16,7 +16,11 @@ exports[`GradebookTable component snapshot - fields1 and 2 between email and tot
},
Object {
"key": "Email",
"label": "Email",
"label": <FormattedMessage
defaultMessage="Email"
description="Gradebook table email column header"
id="gradebook.GradesTab.table.headings.email"
/>,
},
Object {
"key": "field1",

View File

@@ -4,10 +4,12 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Table } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Headings } from 'data/constants/grades';
import selectors from 'data/selectors';
import { Headings } from 'data/constants/grades';
import messages from './messages';
import Fields from './Fields';
import LabelReplacements from './LabelReplacements';
import GradeButton from './GradeButton';
@@ -28,14 +30,17 @@ export class GradebookTable extends React.Component {
}
mapHeaders(heading) {
const replacement = {
[Headings.totalGrade]: <LabelReplacements.TotalGradeLabelReplacement />,
[Headings.username]: <LabelReplacements.UsernameLabelReplacement />,
}[heading];
return {
label: replacement !== undefined ? replacement : heading,
key: heading,
};
let label;
if (heading === Headings.totalGrade) {
label = <LabelReplacements.TotalGradeLabelReplacement />;
} else if (heading === Headings.username) {
label = <LabelReplacements.UsernameLabelReplacement />;
} else if (heading === Headings.email) {
label = <FormattedMessage {...messages.emailHeading} />;
} else {
label = heading;
}
return { label, key: heading };
}
mapRows(entry) {

View File

@@ -0,0 +1,36 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
emailHeading: {
id: 'gradebook.GradesTab.table.headings.email',
defaultMessage: 'Email',
description: 'Gradebook table email column header',
},
totalGradeHeading: {
id: 'gradebook.GradesTab.table.headings.totalGrade',
defaultMessage: 'Total Grade (%)',
description: 'Gradebook table total grade column header',
},
usernameHeading: {
id: 'gradebook.GradesTab.table.headings.username',
defaultMessage: 'Username',
description: 'Gradebook table username column header',
},
studentKeyLabel: {
id: 'gradebook.GradesTab.table.labels.studentKey',
defaultMessage: 'Student Key*',
description: 'Gradebook table Student Key label',
},
usernameLabel: {
id: 'gradebook.GradesTab.table.labels.username',
defaultMessage: 'Username',
description: 'Gradebook table username label',
},
totalGradePercentage: {
id: 'gradebook.GradesTab.table.totalGradePercentage',
defaultMessage: 'Total Grade values are always displayed as a percentage',
description: 'Gradebook table message that total grades are displayed in percent format',
},
});
export default messages;

View File

@@ -2,11 +2,13 @@ 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 { Headings } from 'data/constants/grades';
import LabelReplacements from './LabelReplacements';
import Fields from './Fields';
import messages from './messages';
import { GradebookTable, mapStateToProps } from '.';
jest.mock('@edx/paragon', () => ({
@@ -94,7 +96,7 @@ describe('GradebookTable', () => {
test('email sets key and label from header', () => {
const heading = headings[1];
expect(heading.key).toEqual(Headings.email);
expect(heading.label).toEqual(Headings.email);
expect(heading.label).toEqual(<FormattedMessage {...messages.emailHeading} />);
});
test('subsections set key and label from header', () => {
expect(headings[2]).toEqual({ key: fields.field1, label: fields.field1 });

View File

@@ -19,7 +19,11 @@ exports[`PageButtons component snapshots buttons enabled with both endpoints pro
}
variant="outline-primary"
>
Previous Page
<FormattedMessage
defaultMessage="Previous Page"
description="Grades tab Previous Page button text"
id="gradebook.GradesTab.PageButtons.prevPage"
/>
</Button>
<Button
disabled={false}
@@ -31,7 +35,11 @@ exports[`PageButtons component snapshots buttons enabled with both endpoints pro
}
variant="outline-primary"
>
Next Page
<FormattedMessage
defaultMessage="Next Page"
description="Grades tab Next Page button text"
id="gradebook.GradesTab.PageButtons.nextPage"
/>
</Button>
</div>
`;
@@ -55,7 +63,11 @@ exports[`PageButtons component snapshots nextPage disabled if not provided 1`] =
}
variant="outline-primary"
>
Previous Page
<FormattedMessage
defaultMessage="Previous Page"
description="Grades tab Previous Page button text"
id="gradebook.GradesTab.PageButtons.prevPage"
/>
</Button>
<Button
disabled={true}
@@ -67,7 +79,11 @@ exports[`PageButtons component snapshots nextPage disabled if not provided 1`] =
}
variant="outline-primary"
>
Next Page
<FormattedMessage
defaultMessage="Next Page"
description="Grades tab Next Page button text"
id="gradebook.GradesTab.PageButtons.nextPage"
/>
</Button>
</div>
`;
@@ -91,7 +107,11 @@ exports[`PageButtons component snapshots prevPage disabled if not provided 1`] =
}
variant="outline-primary"
>
Previous Page
<FormattedMessage
defaultMessage="Previous Page"
description="Grades tab Previous Page button text"
id="gradebook.GradesTab.PageButtons.prevPage"
/>
</Button>
<Button
disabled={false}
@@ -103,7 +123,11 @@ exports[`PageButtons component snapshots prevPage disabled if not provided 1`] =
}
variant="outline-primary"
>
Next Page
<FormattedMessage
defaultMessage="Next Page"
description="Grades tab Next Page button text"
id="gradebook.GradesTab.PageButtons.nextPage"
/>
</Button>
</div>
`;

View File

@@ -3,9 +3,11 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Button } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import selectors from 'data/selectors';
import thunkActions from 'data/thunkActions';
import messages from './messages';
export class PageButtons extends React.Component {
constructor(props) {
@@ -34,7 +36,7 @@ export class PageButtons extends React.Component {
disabled={!this.props.prevPage}
onClick={this.getPrevGrades}
>
Previous Page
<FormattedMessage {...messages.prevPage} />
</Button>
<Button
style={{ margin: '20px' }}
@@ -42,7 +44,7 @@ export class PageButtons extends React.Component {
disabled={!this.props.nextPage}
onClick={this.getNextGrades}
>
Next Page
<FormattedMessage {...messages.nextPage} />
</Button>
</div>
);

View File

@@ -0,0 +1,16 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
prevPage: {
id: 'gradebook.GradesTab.PageButtons.prevPage',
defaultMessage: 'Previous Page',
description: 'Grades tab Previous Page button text',
},
nextPage: {
id: 'gradebook.GradesTab.PageButtons.nextPage',
defaultMessage: 'Next Page',
description: 'Grades tab Next Page button text',
},
});
export default messages;

View File

@@ -3,29 +3,37 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { FormControl, FormGroup, FormLabel } from '@edx/paragon';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import actions from 'data/actions';
import selectors from 'data/selectors';
import messages from './messages';
/**
* <ScoreViewInput />
* redux-connected select control for grade format (percent vs absolute)
*/
export const ScoreViewInput = ({ format, toggleFormat }) => (
export const ScoreViewInput = ({ format, intl, toggleFormat }) => (
<FormGroup controlId="ScoreView">
<FormLabel>Score View:</FormLabel>
<FormLabel><FormattedMessage {...messages.scoreView} />:</FormLabel>
<FormControl
as="select"
value={format}
onChange={toggleFormat}
>
<option value="percent">Percent</option>
<option value="absolute">Absolute</option>
<option value="percent">{intl.formatMessage(messages.percent)}</option>
<option value="absolute">{intl.formatMessage(messages.absolute)}</option>
</FormControl>
</FormGroup>
);
ScoreViewInput.defaultProps = {
format: 'percent',
};
ScoreViewInput.propTypes = {
format: PropTypes.string.isRequired,
// injected
intl: intlShape.isRequired,
// redux
format: PropTypes.string,
toggleFormat: PropTypes.func.isRequired,
};
@@ -37,4 +45,4 @@ export const mapDispatchToProps = {
toggleFormat: actions.grades.toggleGradeFormat,
};
export default connect(() => ({}), mapDispatchToProps)(ScoreViewInput);
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ScoreViewInput));

View File

@@ -35,6 +35,7 @@ describe('ScoreViewInput', () => {
let el;
beforeEach(() => {
props.toggleFormat = jest.fn();
props.intl = { formatMessage: (msg) => msg.defaultMessage };
el = shallow(<ScoreViewInput {...props} />);
});
const assertions = [

View File

@@ -3,11 +3,14 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Button, Icon, SearchField } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import actions from 'data/actions';
import selectors from 'data/selectors';
import thunkActions from 'data/thunkActions';
import messages from './messages';
/**
* 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.
@@ -32,25 +35,25 @@ export class SearchControls extends React.Component {
render() {
return (
<>
<h4>Step 1: Filter the Grade Report</h4>
<h4><FormattedMessage {...messages.filterStepHeading} /></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
<Icon className="fa fa-filter" /> <FormattedMessage {...messages.editFilters} />
</Button>
<div>
<SearchField
onSubmit={this.props.fetchGrades}
inputLabel="Search for a learner"
inputLabel={<FormattedMessage {...messages.searchLabel} />}
onChange={this.onChange}
onClear={this.onClear}
value={this.props.searchValue}
/>
<small className="form-text text-muted search-help-text">
Search by username, email, or student key
<FormattedMessage {...messages.searchHint} />
</small>
</div>
</div>

View File

@@ -3,12 +3,11 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { StatusAlert } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import selectors from 'data/selectors';
import actions from 'data/actions';
export const maxCourseGradeInvalidMessage = 'Maximum course grade value must be between 0 and 100. ';
export const minCourseGradeInvalidMessage = 'Minimum course grade value must be between 0 and 100. ';
import messages from './messages';
export class StatusAlerts extends React.Component {
get isCourseGradeFilterAlertOpen() {
@@ -18,15 +17,24 @@ export class StatusAlerts extends React.Component {
);
}
get minValidityMessage() {
return (this.props.limitValidity.isMinValid)
? ''
: <FormattedMessage {...messages.minGradeInvalid} />;
}
get maxValidityMessage() {
return (this.props.limitValidity.isMaxValid)
? ''
: <FormattedMessage {...messages.maxGradeInvalid} />;
}
get courseGradeFilterAlertDialogText() {
let dialogText = '';
if (!this.props.limitValidity.isMinValid) {
dialogText += minCourseGradeInvalidMessage;
}
if (!this.props.limitValidity.isMaxValid) {
dialogText += maxCourseGradeInvalidMessage;
}
return dialogText;
return (
<>
{this.minValidityMessage}{this.maxValidityMessage}
</>
);
}
render() {
@@ -34,7 +42,7 @@ export class StatusAlerts extends React.Component {
<>
<StatusAlert
alertType="success"
dialog="The grade has been successfully edited. You may see a slight delay before updates appear in the Gradebook."
dialog={<FormattedMessage {...messages.editSuccessAlert} />}
onClose={this.props.handleCloseSuccessBanner}
open={this.props.showSuccessBanner}
/>

View File

@@ -1,14 +1,15 @@
import React from 'react';
import { shallow } from 'enzyme';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import actions from 'data/actions';
import selectors from 'data/selectors';
import messages from './messages';
import {
StatusAlerts,
mapDispatchToProps,
mapStateToProps,
maxCourseGradeInvalidMessage,
minCourseGradeInvalidMessage,
} from './StatusAlerts';
jest.mock('@edx/paragon', () => ({
@@ -77,18 +78,24 @@ describe('StatusAlerts', () => {
!isMinValid || !isMaxValid,
);
if (!isMaxValid) {
if (!isMinValid) {
expect(el.instance().courseGradeFilterAlertDialogText).toEqual(
<>
<FormattedMessage {...messages.minGradeInvalid} />
<FormattedMessage {...messages.maxGradeInvalid} />
</>,
);
} else {
expect(
el.instance().courseGradeFilterAlertDialogText,
// eslint-disable-next-line react/jsx-curly-brace-presence
).toEqual(<>{''}<FormattedMessage {...messages.maxGradeInvalid} /></>);
}
} else if (!isMinValid) {
expect(
el.instance().courseGradeFilterAlertDialogText,
).toEqual(
expect.stringContaining(maxCourseGradeInvalidMessage),
);
}
if (!isMinValid) {
expect(
el.instance().courseGradeFilterAlertDialogText,
).toEqual(
expect.stringContaining(minCourseGradeInvalidMessage),
);
// eslint-disable-next-line react/jsx-curly-brace-presence
).toEqual(<><FormattedMessage {...messages.minGradeInvalid} />{''}</>);
}
});
});

View File

@@ -3,6 +3,8 @@ 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';
/**
@@ -18,9 +20,15 @@ export const UsersLabel = ({
}
const bold = (val) => (<span className="font-weight-bold">{val}</span>);
return (
<>
Showing {bold(filteredUsersCount)} of {bold(totalUsersCount)} total learners
</>
<FormattedMessage
id="gradebook.GradesTab.usersVisibilityLabel'"
defaultMessage="Showing {filteredUsers} of {totalUsers} total learners"
description="Users visibility label"
values={{
filteredUsers: bold(filteredUsersCount),
totalUsers: bold(totalUsersCount),
}}
/>
);
};
UsersLabel.propTypes = {

View File

@@ -5,7 +5,12 @@ exports[`ScoreViewInput component snapshot - select box with percent and absolut
controlId="ScoreView"
>
<FormLabel>
Score View:
<FormattedMessage
defaultMessage="Score View"
description="Score format select dropdown label"
id="gradebook.GradesTab.scoreViewLabel"
/>
:
</FormLabel>
<FormControl
as="select"

View File

@@ -3,7 +3,11 @@
exports[`SearchControls Component Snapshots basic snapshot 1`] = `
<React.Fragment>
<h4>
Step 1: Filter the Grade Report
<FormattedMessage
defaultMessage="Step 1: Filter the Grade Report"
description="Filter controls container heading string"
id="gradebook.GradesTab.filterHeading"
/>
</h4>
<div
className="d-flex justify-content-between"
@@ -16,11 +20,22 @@ exports[`SearchControls Component Snapshots basic snapshot 1`] = `
<Icon
className="fa fa-filter"
/>
Edit Filters
<FormattedMessage
defaultMessage="Edit Filters"
description="Button text on Grades tab to open/close the Filters tab"
id="gradebook.GradesTab.editFilterLabel"
/>
</Button>
<div>
<SearchField
inputLabel="Search for a learner"
inputLabel={
<FormattedMessage
defaultMessage="Search for a learner"
description="Search description label"
id="gradebook.GradesTab.search.label"
/>
}
onChange={[MockFunction onChange]}
onClear={[MockFunction onClear]}
onSubmit={[MockFunction fetchGrades]}
@@ -29,7 +44,11 @@ exports[`SearchControls Component Snapshots basic snapshot 1`] = `
<small
className="form-text text-muted search-help-text"
>
Search by username, email, or student key
<FormattedMessage
defaultMessage="Search by username, email, or student key"
description="Search hint label"
id="gradebook.GradesTab.search.hint"
/>
</small>
</div>
</div>

View File

@@ -4,7 +4,13 @@ exports[`StatusAlerts snapshots basic snapshot 1`] = `
<React.Fragment>
<StatusAlert
alertType="success"
dialog="The grade has been successfully edited. You may see a slight delay before updates appear in the Gradebook."
dialog={
<FormattedMessage
defaultMessage="The grade has been successfully edited. You may see a slight delay before updates appear in the Gradebook."
description="Alert text for successful edit action"
id="gradebook.GradesTab.editSuccessAlert"
/>
}
onClose={[MockFunction handleCloseSuccessBanner]}
open={true}
/>

View File

@@ -1,19 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UsersLabel component snapshot - displays label with number of filtered users out of total 1`] = `
<Fragment>
Showing
<span
className="font-weight-bold"
>
23
</span>
of
<span
className="font-weight-bold"
>
140
</span>
total learners
</Fragment>
<FormattedMessage
defaultMessage="Showing {filteredUsers} of {totalUsers} total learners"
description="Users visibility label"
id="gradebook.GradesTab.usersVisibilityLabel'"
values={
Object {
"filteredUsers": <span
className="font-weight-bold"
>
23
</span>,
"totalUsers": <span
className="font-weight-bold"
>
140
</span>,
}
}
/>
`;

View File

@@ -9,7 +9,11 @@ exports[`GradesTab Component snapshots basic snapshot 1`] = `
/>
<StatusAlerts />
<h4>
Step 2: View or Modify Individual Grades
<FormattedMessage
defaultMessage="Step 2: View or Modify Individual Grades"
description="Alert text for invalid minimum course grade"
id="gradebook.GradesTab.gradebookStepHeading"
/>
</h4>
<UsersLabel />
<div
@@ -21,7 +25,12 @@ exports[`GradesTab Component snapshots basic snapshot 1`] = `
<GradebookTable />
<PageButtons />
<p>
* available for learners in the Master's track only
*
<FormattedMessage
defaultMessage="available for learners in the Master's track only"
description="Masters feature availability hint on Grades Tab"
id="gradebook.GradesTab.mastersHint"
/>
</p>
<EditModal />
</React.Fragment>

View File

@@ -3,6 +3,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import actions from 'data/actions';
import thunkActions from 'data/thunkActions';
@@ -17,6 +19,7 @@ import StatusAlerts from './StatusAlerts';
import SpinnerIcon from './SpinnerIcon';
import ScoreViewInput from './ScoreViewInput';
import UsersLabel from './UsersLabel';
import messages from './messages';
export class GradesTab extends React.Component {
constructor(props) {
@@ -43,7 +46,7 @@ export class GradesTab extends React.Component {
<FilterBadges handleClose={this.handleFilterBadgeClose} />
<StatusAlerts />
<h4>Step 2: View or Modify Individual Grades</h4>
<h4><FormattedMessage {...messages.gradebookStepHeading} /></h4>
<UsersLabel />
<div className="d-flex justify-content-between align-items-center mb-2">
@@ -54,7 +57,7 @@ export class GradesTab extends React.Component {
<GradebookTable />
<PageButtons />
<p>* available for learners in the Master&apos;s track only</p>
<p>* <FormattedMessage {...messages.mastersHint} /></p>
<EditModal />
</>
);

View File

@@ -0,0 +1,76 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
bulkManagement: {
id: 'gradebook.GradesTab.BulkManagementControls.bulkManagementLabel',
defaultMessage: 'Bulk Management',
description: 'Button text for bulk grades download control in GradesTab',
},
interventions: {
id: 'gradebook.GradesTab.BulkManagementControls.interventionsLabel',
defaultMessage: 'Interventions',
description: 'Button text for intervention report download control in GradesTab',
},
scoreView: {
id: 'gradebook.GradesTab.scoreViewLabel',
defaultMessage: 'Score View',
description: 'Score format select dropdown label',
},
absolute: {
id: 'gradebook.GradesTab.absoluteOption',
defaultMessage: 'Absolute',
description: 'Score format select dropdown option',
},
percent: {
id: 'gradebook.GradesTab.percentOption',
defaultMessage: 'Percent',
description: 'Score format select dropdown option',
},
filterStepHeading: {
id: 'gradebook.GradesTab.filterHeading',
defaultMessage: 'Step 1: Filter the Grade Report',
description: 'Filter controls container heading string',
},
editFilters: {
id: 'gradebook.GradesTab.editFilterLabel',
defaultMessage: 'Edit Filters',
description: 'Button text on Grades tab to open/close the Filters tab',
},
searchLabel: {
id: 'gradebook.GradesTab.search.label',
defaultMessage: 'Search for a learner',
description: 'Search description label',
},
searchHint: {
id: 'gradebook.GradesTab.search.hint',
defaultMessage: 'Search by username, email, or student key',
description: 'Search hint label',
},
editSuccessAlert: {
id: 'gradebook.GradesTab.editSuccessAlert',
defaultMessage: 'The grade has been successfully edited. You may see a slight delay before updates appear in the Gradebook.',
description: 'Alert text for successful edit action',
},
maxGradeInvalid: {
id: 'gradebook.GradesTab.maxCourseGradeInvalid',
defaultMessage: 'Maximum course grade must be between 0 and 100',
description: 'Alert text for invalid maximum course grade',
},
minGradeInvalid: {
id: 'gradebook.GradesTab.minCourseGradeInvalid',
defaultMessage: 'Minimum course grade must be between 0 and 100',
description: 'Alert text for invalid minimum course grade',
},
gradebookStepHeading: {
id: 'gradebook.GradesTab.gradebookStepHeading',
defaultMessage: 'Step 2: View or Modify Individual Grades',
description: 'Alert text for invalid minimum course grade',
},
mastersHint: {
id: 'gradebook.GradesTab.mastersHint',
defaultMessage: "available for learners in the Master's track only",
description: 'Masters feature availability hint on Grades Tab',
},
});
export default messages;

View File

@@ -6,6 +6,7 @@ import thunkActions from 'data/thunkActions';
import {
GradesTab,
mapStateToProps,
mapDispatchToProps,
} from '.';
@@ -78,6 +79,9 @@ describe('GradesTab', () => {
});
});
});
test('mapStateToProps is empty', () => {
expect(mapStateToProps({ some: 'state' })).toEqual({});
});
describe('mapDispatchToProps', () => {
describe('fetchGrades', () => {
test('from thunkActions.grades.fetchGrades', () => {

View File

@@ -1,19 +1,22 @@
import { createAction } from '@reduxjs/toolkit';
export const options = {
year: 'numeric',
month: 'long',
day: 'numeric',
timeZone: 'UTC',
};
export const timeOptions = {
hour: '2-digit',
minute: '2-digit',
timeZone: 'UTC',
timeZoneName: 'short',
};
const formatDateForDisplay = (inputDate) => {
const options = {
year: 'numeric',
month: 'long',
day: 'numeric',
timeZone: 'UTC',
};
const timeOptions = {
hour: '2-digit',
minute: '2-digit',
timeZone: 'UTC',
timeZoneName: 'short',
};
return `${inputDate.toLocaleDateString('en-US', options)} at ${inputDate.toLocaleTimeString('en-US', timeOptions)}`;
const date = inputDate.toLocaleDateString('en-US', options);
const time = inputDate.toLocaleTimeString('en-US', timeOptions);
return `${date} at ${time}`;
};
const sortAlphaAsc = (gradeRowA, gradeRowB) => {

View File

@@ -0,0 +1,34 @@
import { createAction } from '@reduxjs/toolkit';
import * as utils from './utils';
jest.mock('@reduxjs/toolkit', () => ({
createAction: (key, ...args) => ({ action: key, args }),
}));
describe('redux action utils', () => {
describe('formatDateForDisplay', () => {
it('returns the datetime as a formatted string', () => {
expect(utils.formatDateForDisplay(new Date('Jun 3 2021 11:59 AM EDT'))).toEqual(
'June 3, 2021 at 03:59 PM UTC',
);
});
});
describe('sortAlphaAsc', () => {
it('returns sorting value (-1, 0, 1) by uppercase username', () => {
const sort = (v1, v2) => utils.sortAlphaAsc({ username: v1 }, { username: v2 });
expect(sort('aName', 'ANAme')).toEqual(0);
expect(sort('aName', 'laterName')).toEqual(-1);
expect(sort('laterName', 'aName')).toEqual(1);
});
});
describe('createActionFactory', () => {
it('returns an action creator with the data key', () => {
const dataKey = 'part-of-the-model';
const actionKey = 'an-action';
const args = ['some', 'args'];
expect(utils.createActionFactory(dataKey)(actionKey, ...args)).toEqual(
createAction(`${dataKey}/${actionKey}`, ...args),
);
});
});
});

View File

@@ -1,14 +0,0 @@
import { configuration } from 'config';
export const baseUrl = `${configuration.LMS_BASE_URL}/api`;
/**
* bulkGradesUrlByCourseAndRow(courseId, rowId)
* returns the bulkGrades url with the given courseId and rowId.
* @param {string} courseId - course identifier
* @param {string} rowId - row/error identifier
* @return {string} - bulk grades fetch url
*/
export const bulkGradesUrlByCourseAndRow = (courseId, rowId) => (
`${baseUrl}/bulkGrades/course/${courseId}/?error_id=${rowId}`
);

View File

@@ -1,4 +1,7 @@
import { StrictDict } from 'utils';
import { getConfig } from '@edx/frontend-platform';
export const routePath = `${getConfig().PUBLIC_PATH}:courseId`;
export const modalFieldKeys = StrictDict({
adjustedGradePossible: 'adjustedGradePossible',
@@ -49,6 +52,13 @@ export const bulkManagementColumns = [
},
];
export const gradeOverrideHistoryColumns = StrictDict({
adjustedGrade: 'adjustedGrade',
date: 'date',
grader: 'grader',
reason: 'reason',
});
/**
* Display strings for various app components.
* Note: this is a temporary storage location for these strings, before we put them in

View File

@@ -1,5 +1,7 @@
import { StrictDict } from 'utils';
import messages from './filters.messages';
export const filters = StrictDict({
assignment: 'assignment',
assignmentGrade: 'assignmentGrade',
@@ -28,34 +30,34 @@ const initialFilters = {
export const filterConfig = StrictDict({
[filters.assignment]: {
displayName: 'Assignment',
displayName: messages[filters.assignment],
connectedFilters: ['assignment', 'assignmentGradeMax', 'assignmentGradeMax'],
},
[filters.assignmentType]: {
displayName: 'Assignment Type',
displayName: messages[filters.assignmentType],
connectedFilters: ['assignmentType'],
},
[filters.assignmentGrade]: {
displayName: 'Assignment Grade',
filterOrder: ['courseGradeMin', 'courseGradeMax'],
connectedFilters: ['courseGradeMax', 'courseGradeMin'],
displayName: messages[filters.assignmentGrade],
filterOrder: ['assignmentGradeMin', 'assignmentGradeMax'],
connectedFilters: ['assignmentGradeMax', 'assignmentGradeMin'],
},
[filters.cohort]: {
displayName: 'Cohort',
displayName: messages[filters.cohort],
connectedFilters: ['cohort'],
},
[filters.courseGrade]: {
displayName: 'Course Grade',
displayName: messages[filters.courseGrade],
filterOrder: ['courseGradeMin', 'courseGradeMax'],
connectedFilters: ['courseGradeMax', 'courseGradeMin'],
},
[filters.includeCourseRoleMembers]: {
displayName: 'Includeing Course Team Members',
displayName: messages[filters.includeCourseRoleMembers],
connectedFilters: ['includeCourseRoleMembers'],
hideValue: true,
},
[filters.track]: {
displayName: 'Track',
displayName: messages[filters.track],
connectedFilters: ['track'],
},
});

View File

@@ -0,0 +1,41 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
assignment: {
id: 'gradebook.GradesTab.FilterBadges.assignment',
defaultMessage: 'Assignment',
description: 'Assignment FilterBadge label',
},
assignmentGrade: {
id: 'gradebook.GradesTab.FilterBadges.assignmentGrade',
defaultMessage: 'Assignment Grade',
description: 'Assignment Grade FilterBadge label',
},
assignmentType: {
id: 'gradebook.GradesTab.FilterBadges.assignmentType',
defaultMessage: 'Assignment Type',
description: 'Assignment Type FilterBadge label',
},
cohort: {
id: 'gradebook.GradesTab.FilterBadges.cohort',
defaultMessage: 'Cohort',
description: 'Cohort FilterBadge label',
},
courseGrade: {
id: 'gradebook.GradesTab.FilterBadges.courseGrade',
defaultMessage: 'Course Grade',
description: 'Course Grade FilterBadge label',
},
includeCourseRoleMembers: {
id: 'gradebook.GradesTab.FilterBadges.includeCourseRoleMembers',
defaultMessage: 'Include Course Team Members',
description: 'Include Course Team Members FilterBadge label',
},
track: {
id: 'gradebook.GradesTab.FilterBadges.track',
defaultMessage: 'Track',
description: 'Track FilterBadge label',
},
});
export default messages;

View File

@@ -42,6 +42,37 @@ describe('app reducer', () => {
).toEqual({ ...testingState, courseId: testValue });
});
});
describe('appActions.filterMenu.startTransition', () => {
it('sets filterMenu.transitioning to true', () => {
expect(
app(testingState, appActions.filterMenu.startTransition()),
).toEqual({
...testingState,
filterMenu: { ...testingState.filterMenu, transitioning: true },
});
});
});
describe('appActions.filterMenu.endTransition', () => {
it('sets filterMenu.transitioning to false', () => {
const transitioningState = {
...testingState,
filterMenu: { ...testingState.filterMenu, transitioning: true },
};
expect(
app(transitioningState, appActions.filterMenu.endTransition()),
).toEqual(testingState);
});
});
describe('appActions.filterMenu.toggle', () => {
it('toggles filterMenu.open', () => {
const openState = {
...testingState,
filterMenu: { ...testingState.filterMenu, open: true },
};
expect(app(testingState, appActions.filterMenu.toggle())).toEqual(openState);
expect(app(openState, appActions.filterMenu.toggle())).toEqual(testingState);
});
});
describe('appActions.setLocalFilter', () => {
it('loads filter values from the payload', () => {
expect(

View File

@@ -30,17 +30,13 @@ const reducer = (state = initialState, { type: actionType, payload }) => {
assignmentGradeMax: payload.assignmentGradeMax,
assignmentGradeMin: payload.assignmentGradeMin,
};
case actions.update.assignmentType.toString():
return {
...state,
assignmentType: payload,
assignment: (
(
payload !== ''
&& (state.assignment || {}).type !== payload
) ? '' : state.assignment
),
};
case actions.update.assignmentType.toString(): {
const newState = { ...state, assignmentType: payload };
if (payload !== '' && state.assignment && payload !== state.assignment.type) {
newState.assignment = '';
}
return newState;
}
case actions.update.cohort.toString():
return { ...state, cohort: payload };
case actions.update.courseGradeLimits.toString():

View File

@@ -9,6 +9,7 @@ import grades from './grades';
import roles from './roles';
import tracks from './tracks';
/* istanbul ignore next */
const rootReducer = combineReducers({
app,
assignmentTypes,

View File

@@ -105,11 +105,11 @@ export const headingMapper = (category, label = 'All') => {
filter = filters.byLabel;
}
const { username, email, totalGrade } = Headings;
const fillerLabels = (entry) => entry.filter(filter).map(s => s.label);
const filteredLabels = (entry) => entry.filter(filter).map(s => s.label);
return (entry) => (
entry
? [username, email, ...fillerLabels(entry), totalGrade]
? [username, email, ...filteredLabels(entry), totalGrade]
: []
);
};
@@ -133,7 +133,6 @@ export const transformHistoryEntry = ({
originalFilename,
resultsSummary: {
rowId: id,
courseId,
text: module.getRowsProcessed(data),
},
...rest,
@@ -188,7 +187,7 @@ export const allGrades = ({ grades: { results } }) => results;
*/
export const bulkImportError = ({ grades: { bulkManagement } }) => (
(!!bulkManagement && bulkManagement.errorMessages)
? `Errors while processing: ${bulkManagement.errorMessages.join(', ')}`
? `Errors while processing: ${bulkManagement.errorMessages.join('; ')};`
: ''
);

View File

@@ -87,6 +87,44 @@ describe('grades selectors', () => {
describe('grade formatters', () => {
const selectedAssignment = { assignmentId: 'block-v1:edX+type@sequential+block@abcde' };
describe('formatGradeOverrideForDisplay', () => {
it('maps history entries with formatted date, grader, reason, and adjusted grade', () => {
const historyArray = [
{
history_date: 'Jan 01 2021',
history_user: 'Grog',
override_reason: 'rage',
earned_graded_override: 0,
},
{
history_date: 'Jan 02 2021',
history_user: 'Keyleth',
override_reason: 'nature',
earned_graded_override: 10,
},
{
history_date: 'Jan 03 2021',
history_user: 'Pike',
override_reason: 'Sarenrae',
earned_graded_override: 9001,
},
];
const mapped = selectors.formatGradeOverrideForDisplay(historyArray);
const testEntry = (index) => {
const entry = historyArray[index];
expect(mapped[index]).toEqual({
date: formatDateForDisplay(new Date(entry.history_date)),
grader: entry.history_user,
reason: entry.override_reason,
adjustedGrade: entry.earned_graded_override,
});
};
testEntry(0);
testEntry(1);
testEntry(2);
});
});
describe('formatMinAssignmentGrade', () => {
const modifiedGrade = '1';
const selector = selectors.formatMinAssignmentGrade;
@@ -200,7 +238,6 @@ describe('grades selectors', () => {
it('summarizes processed rows', () => {
expect(output.resultsSummary).toEqual({
text: selectors.getRowsProcessed(rawEntry.data),
courseId: rawEntry.unique_id,
rowId: rawEntry.id,
});
});
@@ -276,7 +313,7 @@ describe('grades selectors', () => {
expect(
selectors.bulkImportError({ grades: { bulkManagement: { errorMessages } } }),
).toEqual(
`Errors while processing: ${errorMessages[0]}, ${errorMessages[1]}`,
`Errors while processing: ${errorMessages[0]}; ${errorMessages[1]};`,
);
});
});

View File

@@ -1,7 +1,6 @@
/* eslint-disable import/no-named-as-default-member, import/no-self-import */
import { StrictDict } from 'utils';
import LmsApiService from 'data/services/LmsApiService';
import lms from 'data/services/lms';
import * as filterConstants from 'data/constants/filters';
import * as module from '.';
@@ -122,13 +121,13 @@ export const getHeadings = (state) => grades.headingMapper(
/**
* gradeExportUrl(state, options)
* Returns the output of getGradeExportCsvUrl, applying the current includeCourseRoleMembers
* Returns the output of getGradeCsvUrl, applying the current includeCourseRoleMembers
* filter.
* @param {object} state - redux state
* @return {string} - generated grade export url
*/
export const gradeExportUrl = (state) => (
LmsApiService.getGradeExportCsvUrl(app.courseId(state), {
lms.urls.gradeCsvUrl({
...module.lmsApiServiceArgs(state),
excludeCourseRoles: filters.includeCourseRoleMembers(state) ? '' : 'all',
})
@@ -141,8 +140,7 @@ export const gradeExportUrl = (state) => (
* @return {string} - generated intervention export url
*/
export const interventionExportUrl = (state) => (
LmsApiService.getInterventionExportCsvUrl(
app.courseId(state),
lms.urls.interventionExportCsvUrl(
module.lmsApiServiceArgs(state),
)
);

View File

@@ -1,17 +1,16 @@
/* eslint-disable import/no-named-as-default-member */
/* eslint-disable import/no-named-as-default-member, import/no-named-as-default */
import * as filterConstants from '../constants/filters';
import selectors from '.';
import * as moduleSelectors from '.';
import { minGrade, maxGrade } from './grades';
jest.mock('../services/LmsApiService', () => ({
__esModule: true,
default: {
getGradeExportCsvUrl: jest.fn(
(...args) => ({ getGradeExportCsvUrl: { args } }),
jest.mock('data/services/lms', () => ({
urls: {
gradeCsvUrl: jest.fn(
(...args) => ({ gradeCsvUrl: { args } }),
),
getInterventionExportCsvUrl: jest.fn(
(...args) => ({ getInterventionExportCsvUrl: { args } }),
interventionExportCsvUrl: jest.fn(
(...args) => ({ interventionExportCsvUrl: { args } }),
),
},
}));
@@ -344,8 +343,8 @@ describe('root selectors', () => {
it('calls the API service with the right args, excluding all course roles', () => {
selectors.filters.includeCourseRoleMembers.mockReturnValue(undefined);
expect(selector(testState)).toEqual({
getGradeExportCsvUrl: {
args: [testCourseId, { lmsArgs: testState, excludeCourseRoles: 'all' }],
gradeCsvUrl: {
args: [{ lmsArgs: testState, excludeCourseRoles: 'all' }],
},
});
});
@@ -354,8 +353,8 @@ describe('root selectors', () => {
it('calls the API service with the right args, including course roles', () => {
selectors.filters.includeCourseRoleMembers.mockReturnValue(true);
expect(selector(testState)).toEqual({
getGradeExportCsvUrl: {
args: [testCourseId, { lmsArgs: testState, excludeCourseRoles: '' }],
gradeCsvUrl: {
args: [{ lmsArgs: testState, excludeCourseRoles: '' }],
},
});
});
@@ -369,8 +368,8 @@ describe('root selectors', () => {
expect(
moduleSelectors.interventionExportUrl(testState),
).toEqual({
getInterventionExportCsvUrl: {
args: [testCourseId, { lmsArgs: testState }],
interventionExportCsvUrl: {
args: [{ lmsArgs: testState }],
},
});
moduleSelectors.lmsApiServiceArgs = lmsApiServiceArgs;

View File

@@ -1,143 +0,0 @@
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { configuration } from '../../config';
class LmsApiService {
static baseUrl = configuration.LMS_BASE_URL;
static pageSize = 25
static fetchGradebookData(courseId, searchText, cohort, track, options = {}) {
const queryParams = {};
queryParams.page_size = LmsApiService.pageSize;
if (searchText) {
queryParams.user_contains = searchText;
}
if (cohort) {
queryParams.cohort_id = cohort;
}
if (track) {
queryParams.enrollment_mode = track;
}
if (options.assignmentGradeMax || options.assignmentGradeMin) {
if (!options.assignment) {
throw new Error('Gradebook LMS API requires assignment to be set to filter by min/max assig. grade');
}
queryParams.assignment = options.assignment;
if (options.assignmentGradeMin) {
queryParams.assignment_grade_min = options.assignmentGradeMin;
}
if (options.assignmentGradeMax) {
queryParams.assignment_grade_max = options.assignmentGradeMax;
}
}
if (options.courseGradeMin) {
queryParams.course_grade_min = options.courseGradeMin;
}
if (options.courseGradeMax) {
queryParams.course_grade_max = options.courseGradeMax;
}
if (!options.includeCourseRoleMembers) {
queryParams.excluded_course_roles = ['all'];
}
const queryParamString = Object.keys(queryParams)
.map(attr => `${attr}=${encodeURIComponent(queryParams[attr])}`)
.join('&');
const gradebookUrl = `${LmsApiService.baseUrl}/api/grades/v1/gradebook/${courseId}/?${queryParamString}`;
return getAuthenticatedHttpClient().get(gradebookUrl);
}
static updateGradebookData(courseId, updateData) {
/*
updateData is expected to be a list of objects with the keys 'user_id' (an integer),
'usage_id' (a string) and 'grade', which is an object with the keys:
'earned_all_override',
'possible_all_override',
'earned_graded_override',
and 'possible_graded_override',
each of which should be an integer.
Example:
[
{
"user_id": 9,
"usage_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions",
"grade": {
"earned_all_override": 11,
"possible_all_override": 11,
"earned_graded_override": 11,
"possible_graded_override": 11,
"comment": "reason for override"
}
}
]
*/
const gradebookUrl = `${LmsApiService.baseUrl}/api/grades/v1/gradebook/${courseId}/bulk-update`;
return getAuthenticatedHttpClient().post(gradebookUrl, updateData);
}
static fetchTracks(courseId) {
const trackUrl = `${LmsApiService.baseUrl}/api/enrollment/v1/course/${courseId}?include_expired=1`;
return getAuthenticatedHttpClient().get(trackUrl);
}
static fetchCohorts(courseId) {
const cohortsUrl = `${LmsApiService.baseUrl}/courses/${courseId}/cohorts/`;
return getAuthenticatedHttpClient().get(cohortsUrl);
}
static fetchAssignmentTypes(courseId) {
const assignmentTypesUrl = `${LmsApiService.baseUrl}/api/grades/v1/gradebook/${courseId}/grading-info?graded_only=true`;
return getAuthenticatedHttpClient().get(assignmentTypesUrl);
}
static fetchUserRoles(courseId) {
const rolesUrl = `${LmsApiService.baseUrl}/api/enrollment/v1/roles/?course_id=${encodeURIComponent(courseId)}`;
return getAuthenticatedHttpClient().get(rolesUrl);
}
static getGradeExportCsvUrl(courseId, options = {}) {
const queryParams = ['track', 'cohort', 'assignment', 'assignmentType', 'assignmentGradeMax',
'assignmentGradeMin', 'courseGradeMin', 'courseGradeMax', 'excludedCourseRoles']
.filter(opt => options[opt]
&& options[opt] !== 'All')
.map(opt => `${opt}=${encodeURIComponent(options[opt])}`)
.join('&');
return `${LmsApiService.baseUrl}/api/bulk_grades/course/${courseId}/?${queryParams}`;
}
static getInterventionExportCsvUrl(courseId, options = {}) {
const queryParams = ['track', 'cohort', 'assignment', 'assignmentType', 'assignmentGradeMax',
'assignmentGradeMin', 'courseGradeMin', 'courseGradeMax']
.filter(opt => options[opt]
&& options[opt] !== 'All')
.map(opt => `${opt}=${encodeURIComponent(options[opt])}`)
.join('&');
return `${LmsApiService.baseUrl}/api/bulk_grades/course/${courseId}/intervention?${queryParams}`;
}
static getGradeImportCsvUrl = LmsApiService.getGradeExportCsvUrl;
static uploadGradeCsv(courseId, formData) {
const fileUploadUrl = LmsApiService.getGradeImportCsvUrl(courseId);
return getAuthenticatedHttpClient().post(fileUploadUrl, formData).then((result) => {
if (result.status === 200 && !result.data.error_messages.length) {
return result.data;
}
return Promise.reject(result);
});
}
static fetchGradeBulkOperationHistory(courseId) {
const url = `${LmsApiService.baseUrl}/api/bulk_grades/course/${courseId}/history/`;
return getAuthenticatedHttpClient().get(url).then(response => response.data).catch(() => Promise.reject(Error('unhandled response error')));
}
static fetchGradeOverrideHistory(subsectionId, userId) {
const historyUrl = `${LmsApiService.baseUrl}/api/grades/v1/subsection/${subsectionId}/?user_id=${userId}&history_record_limit=5`;
return getAuthenticatedHttpClient().get(historyUrl);
}
}
export default LmsApiService;

View File

@@ -0,0 +1,120 @@
import { StrictDict } from 'utils';
import urls, {
gradeCsvUrl,
sectionOverrideHistoryUrl,
} from './urls';
import { pageSize, paramKeys } from './constants';
import messages from './messages';
import * as utils from './utils';
const { get, post, stringifyUrl } = utils;
/*********************************************************************************
* GET Actions
*********************************************************************************/
const assignmentTypes = () => get(urls.assignmentTypes);
const cohorts = () => get(urls.cohorts);
const roles = () => get(urls.roles);
const tracks = () => get(urls.tracks);
/**
* fetch.gradebookData(searchText, cohort, track, options)
* fetches updated gradebook data based on current filter selections.
* Raises an error if assignment grade limits are set, but not assignment.
* @param {string} searchText - search text filter
* @param {nunber} cohort - selected cohort filter
* @param {string} track - selected track filter
* @param {object} options - additional optional filter values
* @return {Promise} - get response
*/
const gradebookData = (searchText, cohort, track, options = {}) => {
if ((options.assignmentGradeMax || options.assignmentGradeMin) && !options.assignment) {
throw new Error(messages.errors.missingAssignment);
}
const queryParams = {
[paramKeys.pageSize]: pageSize,
[paramKeys.userContains]: searchText,
[paramKeys.cohortId]: cohort,
[paramKeys.enrollmentMode]: track,
[paramKeys.courseGradeMax]: options.courseGradeMax,
[paramKeys.courseGradeMin]: options.courseGradeMin,
[paramKeys.excludedCourseRoles]: options.includeCourseRoleMembers ? null : ['all'],
[paramKeys.assignment]: options.assignment,
[paramKeys.assignmentGradeMax]: options.assignmentGradeMax,
[paramKeys.assignmentGradeMin]: options.assignmentGradeMin,
};
return get(stringifyUrl(urls.gradebook, queryParams));
};
/**
* fetch.gradeBulkOperationHistory()
* fetches bulk operation history and raises an error if the operation fails
* @return {Promise} - get response
*/
const gradeBulkOperationHistory = () => get(urls.bulkHistory)
.then(response => response.data)
.catch(() => Promise.reject(Error(messages.errors.unhandledResponse)));
/**
* fetch.gradeOverrideHistory(subsectionId, userId)
* fetches grade override history for a given user on a given subsection
* @param {string} subsectionId - subsection identifier
* @param {string} userId - user identifier
* @return {Promise} - get response
*/
const gradeOverrideHistory = (subsectionId, userId) => (
get(sectionOverrideHistoryUrl(subsectionId, userId))
);
/*********************************************************************************
* POST Actions
*********************************************************************************/
/**
* updateGradebookData(updateData)
* sends an update message with new grades overrides
* @param {object[]} updateData
* {
* user_id: <int>,
* usage_id: <string>
* grade: {
* earned_all_override: <int>
* possible_all_override: <int>
* earned_graded_override: <int>
* possible_graded_override: <int>
* }
* }
* @return {Promise} - post response
*/
const updateGradebookData = (updateData) => post(urls.bulkUpdate, updateData);
/**
* uploadGradeCsv(formData)
* Posts form data to grade csv url. On success, forwards response data.
* Reject promise with result on failure.
* @param {object} formData - new grade data
* @return {Promise} - post response
*/
const uploadGradeCsv = (formData) => (
post(gradeCsvUrl(), formData).then((result) => {
if (result.status === 200 && !result.data.error_messages.length) {
return result.data;
}
return Promise.reject(result);
})
);
export default StrictDict({
fetch: StrictDict({
assignmentTypes,
cohorts,
gradebookData,
gradeBulkOperationHistory,
gradeOverrideHistory,
roles,
tracks,
}),
updateGradebookData,
uploadGradeCsv,
});

View File

@@ -0,0 +1,234 @@
import api from './api';
import { pageSize, paramKeys } from './constants';
import messages from './messages';
import urls, { gradeCsvUrl, sectionOverrideHistoryUrl } from './urls';
import * as utils from './utils';
jest.mock('./urls', () => ({
__esModule: true,
default: jest.requireActual('./urls').default,
gradeCsvUrl: (...args) => ({ gradeCsvUrl: args }),
sectionOverrideHistoryUrl: (...args) => `sectionOverrideHistoryUrl(${args})`,
}));
jest.mock('./utils', () => ({
get: jest.fn(),
post: jest.fn(),
stringifyUrl: jest.fn(),
}));
describe('lms service api', () => {
describe('get actions', () => {
const mockGet = promiseFn => {
jest.spyOn(utils, 'get').mockImplementation(
url => new Promise(promiseFn(url)),
);
};
const resolveFn = (url) => (resolve) => resolve({ data: url });
const rejectFn = (url) => (resolve, reject) => reject(url);
const testSimpleFetch = (method, expectedUrl, description) => {
mockGet(resolveFn);
test(description, () => (
method().then(({ data }) => { expect(data).toEqual(expectedUrl); })
));
};
describe('fetch.assignmentTypes', () => {
testSimpleFetch(
api.fetch.assignmentTypes,
urls.assignmentTypes,
'fetches from urls.assignmentTypes',
);
});
describe('fetch.cohorts', () => {
testSimpleFetch(
api.fetch.cohorts,
urls.cohorts,
'fetches from urls.cohorts',
);
});
describe('fetch.roles', () => {
testSimpleFetch(
api.fetch.roles,
urls.roles,
'fetches from urls.roles',
);
});
describe('fetch.tracks', () => {
testSimpleFetch(
api.fetch.tracks,
urls.tracks,
'fetches from urls.tracks',
);
});
describe('fetch.gradebookData', () => {
const searchText = 'some user';
const cohort = 2;
const track = 'masters';
const options = {
courseGradeMax: 90,
courseGradeMin: 10,
includeCourseRoleMembers: true,
assignment: 'some work',
assignmentGradeMax: 95,
assignmentGradeMin: 5,
};
it('throws an error if either assignmentGrade limit is set, but no assignment', () => {
mockGet(resolveFn);
expect(() => {
api.fetch.gradebookData(
searchText,
cohort,
track,
{ ...options, assignmentGradeMax: null, assignment: null },
);
}).toThrow(Error(messages.errors.missingAssignment));
expect(() => {
api.fetch.gradebookData(
searchText,
cohort,
track,
{ ...options, assignmentGradeMin: null, assignment: null },
);
}).toThrow(Error(messages.errors.missingAssignment));
});
describe('fetches from urls.gradebook with queryParams loaded from options', () => {
beforeEach(() => {
mockGet(resolveFn);
});
test('loads only passed values if options is empty', () => (
api.fetch.gradebookData(searchText, cohort, track).then(({ data }) => {
expect(data).toEqual(utils.stringifyUrl(urls.gradebook, {
[paramKeys.pageSize]: pageSize,
[paramKeys.userContains]: searchText,
[paramKeys.cohortId]: cohort,
[paramKeys.enrollmentMode]: track,
[paramKeys.courseGradeMax]: undefined,
[paramKeys.courseGradeMin]: undefined,
[paramKeys.excludedCourseRoles]: undefined,
[paramKeys.assignment]: undefined,
[paramKeys.assignmentGradeMax]: undefined,
[paramKeys.assignmentGradeMin]: undefined,
}));
})
));
test('loads ["all"] for excludedCorseRoles if not includeCourseRoles', () => (
api.fetch.gradebookData(searchText, cohort, track, options).then(({ data }) => {
expect(data).toEqual(utils.stringifyUrl(urls.gradebook, {
[paramKeys.pageSize]: pageSize,
[paramKeys.userContains]: searchText,
[paramKeys.cohortId]: cohort,
[paramKeys.enrollmentMode]: track,
[paramKeys.courseGradeMax]: options.courseGradeMax,
[paramKeys.courseGradeMin]: options.courseGradeMin,
[paramKeys.excludedCourseRoles]: ['all'],
[paramKeys.assignment]: options.assignment,
[paramKeys.assignmentGradeMax]: options.assignmentGradeMax,
[paramKeys.assignmentGradeMin]: options.assignmentGradeMin,
}));
})
));
test('loads null for excludedCorseRoles if includeCourseRoles', () => (
api.fetch.gradebookData(searchText, cohort, track, options).then(({ data }) => {
expect(data).toEqual(utils.stringifyUrl(urls.gradebook, {
[paramKeys.pageSize]: pageSize,
[paramKeys.userContains]: searchText,
[paramKeys.cohortId]: cohort,
[paramKeys.enrollmentMode]: track,
[paramKeys.courseGradeMax]: options.courseGradeMax,
[paramKeys.courseGradeMin]: options.courseGradeMin,
[paramKeys.excludedCourseRoles]: null,
[paramKeys.assignment]: options.assignment,
[paramKeys.assignmentGradeMax]: options.assignmentGradeMax,
[paramKeys.assignmentGradeMin]: options.assignmentGradeMin,
}));
})
));
});
});
describe('gradeBulkOperationHistory', () => {
describe('success', () => {
beforeEach(() => {
mockGet(resolveFn);
});
it('fetches from urls.bulkHistory and returns the data', () => (
api.fetch.gradeBulkOperationHistory().then(url => {
expect(url).toEqual(urls.bulkHistory);
})
));
});
describe('failure', () => {
beforeEach(() => {
mockGet(rejectFn);
});
it('rejects with unhandledResponse Error', () => (
api.fetch.gradeBulkOperationHistory().catch(error => {
expect(error).toEqual(Error(messages.errors.unhandledResponse));
})
));
});
});
describe('gradeOverrideHistory', () => {
const subsectionId = 'a subsection';
const userId = 'Thomas';
beforeEach(() => {
mockGet(resolveFn);
});
test('gets from urls.sectionOverrideHistoryUrl with passed subseciton and user ids', () => (
api.fetch.gradeOverrideHistory(subsectionId, userId).then(({ data }) => {
expect(data).toEqual(sectionOverrideHistoryUrl(subsectionId, userId));
})
));
});
});
describe('post actions', () => {
const mockPost = promiseFn => {
jest.spyOn(utils, 'post').mockImplementation(
(url, callback) => new Promise(promiseFn(url, callback)),
);
};
const resolveFn = (url, data) => (resolve) => resolve({ data: { url, data } });
describe('updateGradebookData', () => {
const updateData = { some: 'update data' };
beforeEach(() => {
mockPost(resolveFn);
});
test('posts to urls.bulkUpdate with passed data', () => (
api.updateGradebookData(updateData).then(({ data }) => {
expect(data).toEqual({ url: urls.bulkUpdate, data: updateData });
})
));
});
describe('uploadGradeCsv', () => {
describe('200 status with no error_messages', () => {
const response = {
status: 200,
data: {
error_messages: [],
other: 'data',
},
};
const formData = { some: 'form Data' };
beforeEach(() => {
mockPost(() => (resolve) => { resolve(response); });
});
it('posts formData to gradeCsvUrl and returns the data from response', () => (
api.uploadGradeCsv(formData).then(result => {
expect(result).toEqual(response.data);
})
));
});
describe('non-200 status', () => {
const formData = { some: 'form Data' };
beforeEach(() => {
mockPost((url, data) => (resolve) => { resolve({ url, data }); });
});
it('posts formData to gradeCsvUrl and returns the data from response', () => (
api.uploadGradeCsv(formData).catch(result => {
expect(result).toEqual({ url: gradeCsvUrl(), data: formData });
})
));
});
});
});
});

View File

@@ -0,0 +1,17 @@
import { StrictDict } from 'utils';
export const pageSize = 25;
export const historyRecordLimit = 5;
export const paramKeys = StrictDict({
cohortId: 'cohort_id',
pageSize: 'page_size',
userContains: 'user_contains',
enrollmentMode: 'enrollment_mode',
assignment: 'assignment',
assignmentGradeMin: 'assignment_grade_min',
assignmentGradeMax: 'assignment_grade_max',
courseGradeMin: 'course_grade_min',
courseGradeMax: 'course_grade_max',
excludedCourseRoles: 'excluded_course_roles',
});

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