Merge pull request #278 from open-craft/chris/FAL-3375-video-selection-gallery
[FAL-3375] Feat: Video selection gallery page
This commit is contained in:
989
package-lock.json
generated
989
package-lock.json
generated
@@ -21,6 +21,8 @@
|
||||
"codemirror": "^6.0.0",
|
||||
"fast-xml-parser": "^4.0.10",
|
||||
"lodash-es": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"moment-shortformat": "^2.1.0",
|
||||
"react-redux": "^7.2.8",
|
||||
"react-responsive": "8.2.0",
|
||||
"react-transition-group": "4.4.2",
|
||||
@@ -41,6 +43,7 @@
|
||||
"@edx/paragon": "^20.28.0",
|
||||
"@edx/reactifex": "^2.1.1",
|
||||
"@testing-library/dom": "^8.13.0",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^12.1.1",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"codecov": "3.8.3",
|
||||
@@ -66,6 +69,12 @@
|
||||
"react-dom": "^16.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@adobe/css-tools": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.2.0.tgz",
|
||||
"integrity": "sha512-E09FiIft46CmH5Qnjb0wsW54/YQd69LsxeKUOWawmws1XWvyFGURnAChH0mlr7YPFR1ofwvUQfcL0J3lMxXqPA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@babel/cli": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.16.0.tgz",
|
||||
@@ -5017,6 +5026,47 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/expect-utils": {
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.5.0.tgz",
|
||||
"integrity": "sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"jest-get-type": "^29.4.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/schemas": {
|
||||
"version": "29.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz",
|
||||
"integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@sinclair/typebox": "^0.25.16"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/types": {
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jest/types/-/types-29.5.0.tgz",
|
||||
"integrity": "sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jest/schemas": "^29.4.3",
|
||||
"@types/istanbul-lib-coverage": "^2.0.0",
|
||||
"@types/istanbul-reports": "^3.0.0",
|
||||
"@types/node": "*",
|
||||
"@types/yargs": "^17.0.8",
|
||||
"chalk": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/common": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.2.tgz",
|
||||
@@ -5416,6 +5466,12 @@
|
||||
"@babel/runtime": "^7.9.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@sinclair/typebox": {
|
||||
"version": "0.25.24",
|
||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz",
|
||||
"integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@svgr/babel-plugin-remove-jsx-attribute": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-6.5.0.tgz",
|
||||
@@ -6404,6 +6460,41 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/jest-dom": {
|
||||
"version": "5.16.5",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.16.5.tgz",
|
||||
"integrity": "sha512-N5ixQ2qKpi5OLYfwQmUb/5mSV9LneAcaUfp32pn4yCnpb8r/Yz0pXFPck21dIicKmi+ta5WRAknkZCfA8refMA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@adobe/css-tools": "^4.0.1",
|
||||
"@babel/runtime": "^7.9.2",
|
||||
"@types/testing-library__jest-dom": "^5.9.1",
|
||||
"aria-query": "^5.0.0",
|
||||
"chalk": "^3.0.0",
|
||||
"css.escape": "^1.5.1",
|
||||
"dom-accessibility-api": "^0.5.6",
|
||||
"lodash": "^4.17.15",
|
||||
"redent": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8",
|
||||
"npm": ">=6",
|
||||
"yarn": ">=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/jest-dom/node_modules/chalk": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
|
||||
"integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/react": {
|
||||
"version": "12.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-12.1.1.tgz",
|
||||
@@ -6543,6 +6634,57 @@
|
||||
"@types/istanbul-lib-coverage": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/istanbul-reports": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz",
|
||||
"integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/istanbul-lib-report": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest": {
|
||||
"version": "29.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.1.tgz",
|
||||
"integrity": "sha512-tEuVcHrpaixS36w7hpsfLBLpjtMRJUE09/MHXn923LOVojDwyC14cWcfc0rDs0VEfUyYmt/+iX1kxxp+gZMcaQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"expect": "^29.0.0",
|
||||
"pretty-format": "^29.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest/node_modules/ansi-styles": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest/node_modules/pretty-format": {
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz",
|
||||
"integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jest/schemas": "^29.4.3",
|
||||
"ansi-styles": "^5.0.0",
|
||||
"react-is": "^18.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest/node_modules/react-is": {
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
|
||||
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
|
||||
@@ -6615,6 +6757,21 @@
|
||||
"integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/stack-utils": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
|
||||
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/testing-library__jest-dom": {
|
||||
"version": "5.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.5.tgz",
|
||||
"integrity": "sha512-SBwbxYoyPIvxHbeHxTZX2Pe/74F/tX2/D3mMvzabdeJ25bBojfW0TyB8BHrbq/9zaaKICJZjLP+8r6AeZMFCuQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/jest": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/uglify-js": {
|
||||
"version": "3.17.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.17.1.tgz",
|
||||
@@ -6644,6 +6801,15 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/yargs": {
|
||||
"version": "17.0.24",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz",
|
||||
"integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/yargs-parser": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/yargs-parser": {
|
||||
"version": "21.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz",
|
||||
@@ -6894,18 +7060,6 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/anymatch/node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/aproba": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
|
||||
@@ -8464,15 +8618,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-jest/node_modules/@types/istanbul-reports": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz",
|
||||
"integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/istanbul-lib-report": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-jest/node_modules/@types/yargs": {
|
||||
"version": "15.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz",
|
||||
@@ -11442,6 +11587,12 @@
|
||||
"resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz",
|
||||
"integrity": "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q=="
|
||||
},
|
||||
"node_modules/css.escape": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
|
||||
"integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/cssnano": {
|
||||
"version": "5.0.12",
|
||||
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.0.12.tgz",
|
||||
@@ -12382,6 +12533,15 @@
|
||||
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/diff-sequences": {
|
||||
"version": "29.4.3",
|
||||
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz",
|
||||
"integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dir-glob": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||
@@ -14266,6 +14426,22 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/expect": {
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/expect/-/expect-29.5.0.tgz",
|
||||
"integrity": "sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jest/expect-utils": "^29.5.0",
|
||||
"jest-get-type": "^29.4.3",
|
||||
"jest-matcher-utils": "^29.5.0",
|
||||
"jest-message-util": "^29.5.0",
|
||||
"jest-util": "^29.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ext-list": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz",
|
||||
@@ -22393,6 +22569,170 @@
|
||||
"node": ">= 10.14.2"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-diff": {
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz",
|
||||
"integrity": "sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chalk": "^4.0.0",
|
||||
"diff-sequences": "^29.4.3",
|
||||
"jest-get-type": "^29.4.3",
|
||||
"pretty-format": "^29.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-diff/node_modules/ansi-styles": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-diff/node_modules/pretty-format": {
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz",
|
||||
"integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jest/schemas": "^29.4.3",
|
||||
"ansi-styles": "^5.0.0",
|
||||
"react-is": "^18.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-diff/node_modules/react-is": {
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
|
||||
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/jest-get-type": {
|
||||
"version": "29.4.3",
|
||||
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz",
|
||||
"integrity": "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-matcher-utils": {
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.5.0.tgz",
|
||||
"integrity": "sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chalk": "^4.0.0",
|
||||
"jest-diff": "^29.5.0",
|
||||
"jest-get-type": "^29.4.3",
|
||||
"pretty-format": "^29.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-matcher-utils/node_modules/ansi-styles": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-matcher-utils/node_modules/pretty-format": {
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz",
|
||||
"integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jest/schemas": "^29.4.3",
|
||||
"ansi-styles": "^5.0.0",
|
||||
"react-is": "^18.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-matcher-utils/node_modules/react-is": {
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
|
||||
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/jest-message-util": {
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.5.0.tgz",
|
||||
"integrity": "sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.12.13",
|
||||
"@jest/types": "^29.5.0",
|
||||
"@types/stack-utils": "^2.0.0",
|
||||
"chalk": "^4.0.0",
|
||||
"graceful-fs": "^4.2.9",
|
||||
"micromatch": "^4.0.4",
|
||||
"pretty-format": "^29.5.0",
|
||||
"slash": "^3.0.0",
|
||||
"stack-utils": "^2.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-message-util/node_modules/ansi-styles": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-message-util/node_modules/pretty-format": {
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz",
|
||||
"integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jest/schemas": "^29.4.3",
|
||||
"ansi-styles": "^5.0.0",
|
||||
"react-is": "^18.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-message-util/node_modules/react-is": {
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
|
||||
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/jest-message-util/node_modules/slash": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
||||
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-pnp-resolver": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz",
|
||||
@@ -22410,6 +22750,38 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/jest-util": {
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.5.0.tgz",
|
||||
"integrity": "sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jest/types": "^29.5.0",
|
||||
"@types/node": "*",
|
||||
"chalk": "^4.0.0",
|
||||
"ci-info": "^3.2.0",
|
||||
"graceful-fs": "^4.2.9",
|
||||
"picomatch": "^2.2.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-util/node_modules/ci-info": {
|
||||
"version": "3.8.0",
|
||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz",
|
||||
"integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/sibiraj-s"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/jest/node_modules/@babel/generator": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.5.tgz",
|
||||
@@ -22872,15 +23244,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/jest/node_modules/@types/istanbul-reports": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz",
|
||||
"integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/istanbul-lib-report": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/jest/node_modules/@types/normalize-package-data": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz",
|
||||
@@ -22893,12 +23256,6 @@
|
||||
"integrity": "sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/jest/node_modules/@types/stack-utils": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
|
||||
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/jest/node_modules/@types/yargs": {
|
||||
"version": "15.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz",
|
||||
@@ -23230,15 +23587,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/jest/node_modules/escape-string-regexp": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
|
||||
"integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/jest/node_modules/escodegen": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz",
|
||||
@@ -25135,18 +25483,6 @@
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest/node_modules/stack-utils": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
|
||||
"integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"escape-string-regexp": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/jest/node_modules/static-extend": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",
|
||||
@@ -25955,18 +26291,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch/node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
@@ -26008,6 +26332,15 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/min-indent": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
||||
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/mini-css-extract-plugin": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.6.2.tgz",
|
||||
@@ -26149,6 +26482,26 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.29.4",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/moment-shortformat": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/moment-shortformat/-/moment-shortformat-2.1.0.tgz",
|
||||
"integrity": "sha512-TBh8jH4cVQOnZU+fQXkyCgj74ti//0CTdQd5sQLDTZuHLMaUP3uFEhbfb3yrrLYNVzgoiUhyiIMf0rDdn5iTJg==",
|
||||
"deprecated": "Package is no longer maintained",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"moment": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/moo": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz",
|
||||
@@ -27812,6 +28165,18 @@
|
||||
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/pify": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
|
||||
@@ -29182,16 +29547,17 @@
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp/node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"node_modules/redent": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
|
||||
"integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
"dependencies": {
|
||||
"indent-string": "^4.0.0",
|
||||
"strip-indent": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/redux": {
|
||||
@@ -30234,6 +30600,27 @@
|
||||
"deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/stack-utils": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
|
||||
"integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"escape-string-regexp": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/stack-utils/node_modules/escape-string-regexp": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
|
||||
"integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
@@ -30729,6 +31116,18 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-indent": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
|
||||
"integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"min-indent": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-json-comments": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
||||
@@ -34547,6 +34946,12 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@adobe/css-tools": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.2.0.tgz",
|
||||
"integrity": "sha512-E09FiIft46CmH5Qnjb0wsW54/YQd69LsxeKUOWawmws1XWvyFGURnAChH0mlr7YPFR1ofwvUQfcL0J3lMxXqPA==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/cli": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.16.0.tgz",
|
||||
@@ -38196,6 +38601,38 @@
|
||||
"integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
|
||||
"dev": true
|
||||
},
|
||||
"@jest/expect-utils": {
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.5.0.tgz",
|
||||
"integrity": "sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"jest-get-type": "^29.4.3"
|
||||
}
|
||||
},
|
||||
"@jest/schemas": {
|
||||
"version": "29.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz",
|
||||
"integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sinclair/typebox": "^0.25.16"
|
||||
}
|
||||
},
|
||||
"@jest/types": {
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jest/types/-/types-29.5.0.tgz",
|
||||
"integrity": "sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jest/schemas": "^29.4.3",
|
||||
"@types/istanbul-lib-coverage": "^2.0.0",
|
||||
"@types/istanbul-reports": "^3.0.0",
|
||||
"@types/node": "*",
|
||||
"@types/yargs": "^17.0.8",
|
||||
"chalk": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"@lezer/common": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.2.tgz",
|
||||
@@ -38516,6 +38953,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@sinclair/typebox": {
|
||||
"version": "0.25.24",
|
||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz",
|
||||
"integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@svgr/babel-plugin-remove-jsx-attribute": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-6.5.0.tgz",
|
||||
@@ -39188,6 +39631,35 @@
|
||||
"pretty-format": "^27.0.2"
|
||||
}
|
||||
},
|
||||
"@testing-library/jest-dom": {
|
||||
"version": "5.16.5",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.16.5.tgz",
|
||||
"integrity": "sha512-N5ixQ2qKpi5OLYfwQmUb/5mSV9LneAcaUfp32pn4yCnpb8r/Yz0pXFPck21dIicKmi+ta5WRAknkZCfA8refMA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@adobe/css-tools": "^4.0.1",
|
||||
"@babel/runtime": "^7.9.2",
|
||||
"@types/testing-library__jest-dom": "^5.9.1",
|
||||
"aria-query": "^5.0.0",
|
||||
"chalk": "^3.0.0",
|
||||
"css.escape": "^1.5.1",
|
||||
"dom-accessibility-api": "^0.5.6",
|
||||
"lodash": "^4.17.15",
|
||||
"redent": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
|
||||
"integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@testing-library/react": {
|
||||
"version": "12.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-12.1.1.tgz",
|
||||
@@ -39309,6 +39781,50 @@
|
||||
"@types/istanbul-lib-coverage": "*"
|
||||
}
|
||||
},
|
||||
"@types/istanbul-reports": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz",
|
||||
"integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/istanbul-lib-report": "*"
|
||||
}
|
||||
},
|
||||
"@types/jest": {
|
||||
"version": "29.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.1.tgz",
|
||||
"integrity": "sha512-tEuVcHrpaixS36w7hpsfLBLpjtMRJUE09/MHXn923LOVojDwyC14cWcfc0rDs0VEfUyYmt/+iX1kxxp+gZMcaQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"expect": "^29.0.0",
|
||||
"pretty-format": "^29.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true
|
||||
},
|
||||
"pretty-format": {
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz",
|
||||
"integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jest/schemas": "^29.4.3",
|
||||
"ansi-styles": "^5.0.0",
|
||||
"react-is": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"react-is": {
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
|
||||
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@types/json-schema": {
|
||||
"version": "7.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
|
||||
@@ -39381,6 +39897,21 @@
|
||||
"integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/stack-utils": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
|
||||
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/testing-library__jest-dom": {
|
||||
"version": "5.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.5.tgz",
|
||||
"integrity": "sha512-SBwbxYoyPIvxHbeHxTZX2Pe/74F/tX2/D3mMvzabdeJ25bBojfW0TyB8BHrbq/9zaaKICJZjLP+8r6AeZMFCuQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/jest": "*"
|
||||
}
|
||||
},
|
||||
"@types/uglify-js": {
|
||||
"version": "3.17.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.17.1.tgz",
|
||||
@@ -39409,6 +39940,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@types/yargs": {
|
||||
"version": "17.0.24",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz",
|
||||
"integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/yargs-parser": "*"
|
||||
}
|
||||
},
|
||||
"@types/yargs-parser": {
|
||||
"version": "21.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz",
|
||||
@@ -39609,14 +40149,6 @@
|
||||
"requires": {
|
||||
"normalize-path": "^3.0.0",
|
||||
"picomatch": "^2.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"aproba": {
|
||||
@@ -40750,15 +41282,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/istanbul-reports": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz",
|
||||
"integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/istanbul-lib-report": "*"
|
||||
}
|
||||
},
|
||||
"@types/yargs": {
|
||||
"version": "15.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz",
|
||||
@@ -43103,6 +43626,12 @@
|
||||
"resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz",
|
||||
"integrity": "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q=="
|
||||
},
|
||||
"css.escape": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
|
||||
"integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
|
||||
"dev": true
|
||||
},
|
||||
"cssnano": {
|
||||
"version": "5.0.12",
|
||||
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.0.12.tgz",
|
||||
@@ -43745,6 +44274,12 @@
|
||||
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
|
||||
"dev": true
|
||||
},
|
||||
"diff-sequences": {
|
||||
"version": "29.4.3",
|
||||
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz",
|
||||
"integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==",
|
||||
"dev": true
|
||||
},
|
||||
"dir-glob": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||
@@ -45199,6 +45734,19 @@
|
||||
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||||
"dev": true
|
||||
},
|
||||
"expect": {
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/expect/-/expect-29.5.0.tgz",
|
||||
"integrity": "sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jest/expect-utils": "^29.5.0",
|
||||
"jest-get-type": "^29.4.3",
|
||||
"jest-matcher-utils": "^29.5.0",
|
||||
"jest-message-util": "^29.5.0",
|
||||
"jest-util": "^29.5.0"
|
||||
}
|
||||
},
|
||||
"ext-list": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz",
|
||||
@@ -51991,15 +52539,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/istanbul-reports": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz",
|
||||
"integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/istanbul-lib-report": "*"
|
||||
}
|
||||
},
|
||||
"@types/normalize-package-data": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz",
|
||||
@@ -52012,12 +52551,6 @@
|
||||
"integrity": "sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/stack-utils": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
|
||||
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/yargs": {
|
||||
"version": "15.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz",
|
||||
@@ -52289,12 +52822,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"escape-string-regexp": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
|
||||
"integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
|
||||
"dev": true
|
||||
},
|
||||
"escodegen": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz",
|
||||
@@ -53799,15 +54326,6 @@
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"stack-utils": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
|
||||
"integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"escape-string-regexp": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"static-extend": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",
|
||||
@@ -54095,6 +54613,134 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"jest-diff": {
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz",
|
||||
"integrity": "sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^4.0.0",
|
||||
"diff-sequences": "^29.4.3",
|
||||
"jest-get-type": "^29.4.3",
|
||||
"pretty-format": "^29.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true
|
||||
},
|
||||
"pretty-format": {
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz",
|
||||
"integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jest/schemas": "^29.4.3",
|
||||
"ansi-styles": "^5.0.0",
|
||||
"react-is": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"react-is": {
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
|
||||
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"jest-get-type": {
|
||||
"version": "29.4.3",
|
||||
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz",
|
||||
"integrity": "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==",
|
||||
"dev": true
|
||||
},
|
||||
"jest-matcher-utils": {
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.5.0.tgz",
|
||||
"integrity": "sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^4.0.0",
|
||||
"jest-diff": "^29.5.0",
|
||||
"jest-get-type": "^29.4.3",
|
||||
"pretty-format": "^29.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true
|
||||
},
|
||||
"pretty-format": {
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz",
|
||||
"integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jest/schemas": "^29.4.3",
|
||||
"ansi-styles": "^5.0.0",
|
||||
"react-is": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"react-is": {
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
|
||||
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"jest-message-util": {
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.5.0.tgz",
|
||||
"integrity": "sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.12.13",
|
||||
"@jest/types": "^29.5.0",
|
||||
"@types/stack-utils": "^2.0.0",
|
||||
"chalk": "^4.0.0",
|
||||
"graceful-fs": "^4.2.9",
|
||||
"micromatch": "^4.0.4",
|
||||
"pretty-format": "^29.5.0",
|
||||
"slash": "^3.0.0",
|
||||
"stack-utils": "^2.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true
|
||||
},
|
||||
"pretty-format": {
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz",
|
||||
"integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jest/schemas": "^29.4.3",
|
||||
"ansi-styles": "^5.0.0",
|
||||
"react-is": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"react-is": {
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
|
||||
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
|
||||
"dev": true
|
||||
},
|
||||
"slash": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
||||
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"jest-pnp-resolver": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz",
|
||||
@@ -54102,6 +54748,28 @@
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"jest-util": {
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.5.0.tgz",
|
||||
"integrity": "sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jest/types": "^29.5.0",
|
||||
"@types/node": "*",
|
||||
"chalk": "^4.0.0",
|
||||
"ci-info": "^3.2.0",
|
||||
"graceful-fs": "^4.2.9",
|
||||
"picomatch": "^2.2.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"ci-info": {
|
||||
"version": "3.8.0",
|
||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz",
|
||||
"integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -54478,12 +55146,6 @@
|
||||
"requires": {
|
||||
"fill-range": "^7.0.1"
|
||||
}
|
||||
},
|
||||
"picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -54513,6 +55175,12 @@
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz",
|
||||
"integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA=="
|
||||
},
|
||||
"min-indent": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
||||
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
|
||||
"dev": true
|
||||
},
|
||||
"mini-css-extract-plugin": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.6.2.tgz",
|
||||
@@ -54618,6 +55286,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.29.4",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
|
||||
},
|
||||
"moment-shortformat": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/moment-shortformat/-/moment-shortformat-2.1.0.tgz",
|
||||
"integrity": "sha512-TBh8jH4cVQOnZU+fQXkyCgj74ti//0CTdQd5sQLDTZuHLMaUP3uFEhbfb3yrrLYNVzgoiUhyiIMf0rDdn5iTJg==",
|
||||
"requires": {}
|
||||
},
|
||||
"moo": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz",
|
||||
@@ -55847,6 +56526,12 @@
|
||||
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
|
||||
"dev": true
|
||||
},
|
||||
"picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true
|
||||
},
|
||||
"pify": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
|
||||
@@ -56908,14 +57593,16 @@
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"redent": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
|
||||
"integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"indent-string": "^4.0.0",
|
||||
"strip-indent": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"redux": {
|
||||
@@ -57710,6 +58397,23 @@
|
||||
"integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==",
|
||||
"dev": true
|
||||
},
|
||||
"stack-utils": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
|
||||
"integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"escape-string-regexp": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"escape-string-regexp": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
|
||||
"integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
@@ -58073,6 +58777,15 @@
|
||||
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
|
||||
"dev": true
|
||||
},
|
||||
"strip-indent": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
|
||||
"integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"min-indent": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"strip-json-comments": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"@edx/paragon": "^20.28.0",
|
||||
"@edx/reactifex": "^2.1.1",
|
||||
"@testing-library/dom": "^8.13.0",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^12.1.1",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"codecov": "3.8.3",
|
||||
@@ -69,6 +70,8 @@
|
||||
"codemirror": "^6.0.0",
|
||||
"fast-xml-parser": "^4.0.10",
|
||||
"lodash-es": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"moment-shortformat": "^2.1.0",
|
||||
"react-redux": "^7.2.8",
|
||||
"react-responsive": "8.2.0",
|
||||
"react-transition-group": "4.4.2",
|
||||
|
||||
34
src/editors/VideoSelector.jsx
Normal file
34
src/editors/VideoSelector.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import VideoGallery from './containers/VideoGallery';
|
||||
import * as hooks from './hooks';
|
||||
|
||||
export const VideoSelector = ({
|
||||
learningContextId,
|
||||
lmsEndpointUrl,
|
||||
studioEndpointUrl,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
hooks.initializeApp({
|
||||
dispatch,
|
||||
data: {
|
||||
blockId: '',
|
||||
blockType: 'video',
|
||||
learningContextId,
|
||||
lmsEndpointUrl,
|
||||
studioEndpointUrl,
|
||||
},
|
||||
});
|
||||
return (
|
||||
<VideoGallery />
|
||||
);
|
||||
};
|
||||
|
||||
VideoSelector.propTypes = {
|
||||
learningContextId: PropTypes.string.isRequired,
|
||||
lmsEndpointUrl: PropTypes.string.isRequired,
|
||||
studioEndpointUrl: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default VideoSelector;
|
||||
40
src/editors/VideoSelector.test.jsx
Normal file
40
src/editors/VideoSelector.test.jsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { shallow } from 'enzyme';
|
||||
import * as hooks from './hooks';
|
||||
import VideoSelector from './VideoSelector';
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
initializeApp: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('./containers/VideoGallery', () => 'VideoGallery');
|
||||
|
||||
const props = {
|
||||
learningContextId: 'course-v1:edX+DemoX+Demo_Course',
|
||||
lmsEndpointUrl: 'evenfakerurl.com',
|
||||
studioEndpointUrl: 'fakeurl.com',
|
||||
};
|
||||
|
||||
const initData = {
|
||||
blockId: '',
|
||||
blockType: 'video',
|
||||
...props,
|
||||
};
|
||||
|
||||
describe('Video Selector', () => {
|
||||
describe('render', () => {
|
||||
test('rendering correctly with expected Input', () => {
|
||||
expect(shallow(<VideoSelector {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('calls initializeApp hook with dispatch, and passed data', () => {
|
||||
shallow(<VideoSelector {...props} />);
|
||||
expect(hooks.initializeApp).toHaveBeenCalledWith({
|
||||
dispatch: useDispatch(),
|
||||
data: initData,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
38
src/editors/VideoSelectorPage.jsx
Normal file
38
src/editors/VideoSelectorPage.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import ErrorBoundary from './sharedComponents/ErrorBoundary';
|
||||
import { VideoSelector } from './VideoSelector';
|
||||
import store from './data/store';
|
||||
|
||||
const VideoSelectorPage = ({
|
||||
courseId,
|
||||
lmsEndpointUrl,
|
||||
studioEndpointUrl,
|
||||
}) => (
|
||||
<ErrorBoundary>
|
||||
<Provider store={store}>
|
||||
<VideoSelector
|
||||
{...{
|
||||
learningContextId: courseId,
|
||||
lmsEndpointUrl,
|
||||
studioEndpointUrl,
|
||||
}}
|
||||
/>
|
||||
</Provider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
VideoSelectorPage.defaultProps = {
|
||||
courseId: null,
|
||||
lmsEndpointUrl: null,
|
||||
studioEndpointUrl: null,
|
||||
};
|
||||
|
||||
VideoSelectorPage.propTypes = {
|
||||
courseId: PropTypes.string,
|
||||
lmsEndpointUrl: PropTypes.string,
|
||||
studioEndpointUrl: PropTypes.string,
|
||||
};
|
||||
|
||||
export default VideoSelectorPage;
|
||||
25
src/editors/VideoSelectorPage.test.jsx
Normal file
25
src/editors/VideoSelectorPage.test.jsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import VideoSelectorPage from './VideoSelectorPage';
|
||||
|
||||
const props = {
|
||||
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
||||
lmsEndpointUrl: 'evenfakerurl.com',
|
||||
studioEndpointUrl: 'fakeurl.com',
|
||||
};
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
Provider: 'Provider',
|
||||
}));
|
||||
jest.mock('./VideoSelector', () => 'VideoSelector');
|
||||
|
||||
describe('Video Selector Page', () => {
|
||||
describe('snapshots', () => {
|
||||
test('rendering correctly with expected Input', () => {
|
||||
expect(shallow(<VideoSelectorPage {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
test('rendering with props to null', () => {
|
||||
expect(shallow(<VideoSelectorPage />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
3
src/editors/__snapshots__/VideoSelector.test.jsx.snap
Normal file
3
src/editors/__snapshots__/VideoSelector.test.jsx.snap
Normal file
@@ -0,0 +1,3 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Video Selector render rendering correctly with expected Input 1`] = `<VideoGallery />`;
|
||||
45
src/editors/__snapshots__/VideoSelectorPage.test.jsx.snap
Normal file
45
src/editors/__snapshots__/VideoSelectorPage.test.jsx.snap
Normal file
@@ -0,0 +1,45 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Video Selector Page snapshots rendering correctly with expected Input 1`] = `
|
||||
<ErrorBoundary>
|
||||
<Provider
|
||||
store={
|
||||
Object {
|
||||
"dispatch": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
Symbol(Symbol.observable): [Function],
|
||||
}
|
||||
}
|
||||
>
|
||||
<Component
|
||||
learningContextId="course-v1:edX+DemoX+Demo_Course"
|
||||
lmsEndpointUrl="evenfakerurl.com"
|
||||
studioEndpointUrl="fakeurl.com"
|
||||
/>
|
||||
</Provider>
|
||||
</ErrorBoundary>
|
||||
`;
|
||||
|
||||
exports[`Video Selector Page snapshots rendering with props to null 1`] = `
|
||||
<ErrorBoundary>
|
||||
<Provider
|
||||
store={
|
||||
Object {
|
||||
"dispatch": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
Symbol(Symbol.observable): [Function],
|
||||
}
|
||||
}
|
||||
>
|
||||
<Component
|
||||
learningContextId={null}
|
||||
lmsEndpointUrl={null}
|
||||
studioEndpointUrl={null}
|
||||
/>
|
||||
</Provider>
|
||||
</ErrorBoundary>
|
||||
`;
|
||||
@@ -5,6 +5,7 @@ exports[`EditorContainer component render snapshot: initialized. enable save and
|
||||
className="position-relative zindex-0"
|
||||
>
|
||||
<BaseModal
|
||||
bodyStyle={null}
|
||||
close={[MockFunction closeCancelConfirmModal]}
|
||||
confirmAction={
|
||||
<Button
|
||||
@@ -25,6 +26,8 @@ exports[`EditorContainer component render snapshot: initialized. enable save and
|
||||
</Button>
|
||||
}
|
||||
footerAction={null}
|
||||
headerComponent={null}
|
||||
isFullscreenScroll={true}
|
||||
isOpen={false}
|
||||
size="md"
|
||||
title="Exit the editor?"
|
||||
@@ -83,6 +86,7 @@ exports[`EditorContainer component render snapshot: not initialized. disable sav
|
||||
className="position-relative zindex-0"
|
||||
>
|
||||
<BaseModal
|
||||
bodyStyle={null}
|
||||
close={[MockFunction closeCancelConfirmModal]}
|
||||
confirmAction={
|
||||
<Button
|
||||
@@ -103,6 +107,8 @@ exports[`EditorContainer component render snapshot: not initialized. disable sav
|
||||
</Button>
|
||||
}
|
||||
footerAction={null}
|
||||
headerComponent={null}
|
||||
isFullscreenScroll={true}
|
||||
isOpen={false}
|
||||
size="md"
|
||||
title="Exit the editor?"
|
||||
|
||||
@@ -10,6 +10,9 @@ jest.mock('../../../../../data/redux', () => ({
|
||||
problemType: jest.fn(state => ({ problemType: state })),
|
||||
},
|
||||
},
|
||||
thunkActions: {
|
||||
video: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('AnswerOption', () => {
|
||||
|
||||
@@ -11,6 +11,11 @@ jest.mock('../../../../../data/redux', () => ({
|
||||
settings: jest.fn(state => ({ question: state })),
|
||||
},
|
||||
},
|
||||
thunkActions: {
|
||||
video: {
|
||||
importTranscript: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../../../../sharedComponents/TinyMceWidget/hooks', () => ({
|
||||
|
||||
@@ -21,6 +21,11 @@ jest.mock('../../../../../data/redux', () => ({
|
||||
question: jest.fn(state => ({ question: state })),
|
||||
},
|
||||
},
|
||||
thunkActions: {
|
||||
video: {
|
||||
importTranscript: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../../../../sharedComponents/TinyMceWidget/hooks', () => ({
|
||||
|
||||
@@ -16,6 +16,9 @@ jest.mock('../../../../../../data/redux', () => ({
|
||||
learningContextId: jest.fn(state => ({ learningContextId: state })),
|
||||
},
|
||||
},
|
||||
thunkActions: {
|
||||
video: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('ShowAnswerCard', () => {
|
||||
|
||||
@@ -5,6 +5,7 @@ exports[`SwitchToAdvancedEditorCard snapshot snapshot: SwitchToAdvancedEditorCar
|
||||
className="border border-light-700 shadow-none"
|
||||
>
|
||||
<BaseModal
|
||||
bodyStyle={null}
|
||||
close={[Function]}
|
||||
confirmAction={
|
||||
<Button
|
||||
@@ -21,6 +22,8 @@ exports[`SwitchToAdvancedEditorCard snapshot snapshot: SwitchToAdvancedEditorCar
|
||||
</Button>
|
||||
}
|
||||
footerAction={null}
|
||||
headerComponent={null}
|
||||
isFullscreenScroll={true}
|
||||
isOpen={false}
|
||||
size="md"
|
||||
title={
|
||||
|
||||
@@ -61,6 +61,11 @@ jest.mock('../../data/redux', () => ({
|
||||
isFinished: jest.fn((state, params) => ({ isFailed: { state, params } })),
|
||||
},
|
||||
},
|
||||
thunkActions: {
|
||||
video: {
|
||||
importTranscript: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('TextEditor', () => {
|
||||
|
||||
@@ -26,6 +26,7 @@ import { ErrorAlert } from '../../../../../../sharedComponents/ErrorAlerts/Error
|
||||
import { UploadErrorAlert } from '../../../../../../sharedComponents/ErrorAlerts/UploadErrorAlert';
|
||||
import CollapsibleFormWidget from '../CollapsibleFormWidget';
|
||||
import { ErrorContext } from '../../../../hooks';
|
||||
import { RequestKeys } from '../../../../../../data/constants/requests';
|
||||
|
||||
/**
|
||||
* Collapsible Form widget controlling video handouts
|
||||
@@ -38,6 +39,7 @@ export const HandoutWidget = ({
|
||||
handout,
|
||||
getHandoutDownloadUrl,
|
||||
updateField,
|
||||
isUploadError,
|
||||
}) => {
|
||||
const [error] = React.useContext(ErrorContext).handout;
|
||||
const { fileSizeError } = hooks.fileSizeError();
|
||||
@@ -59,7 +61,7 @@ export const HandoutWidget = ({
|
||||
>
|
||||
<FormattedMessage {...messages.fileSizeError} />
|
||||
</ErrorAlert>
|
||||
<UploadErrorAlert message={messages.uploadHandoutError} />
|
||||
<UploadErrorAlert isUploadError={isUploadError} message={messages.uploadHandoutError} />
|
||||
<FileInput fileInput={fileInput} />
|
||||
{handout ? (
|
||||
<Stack gap={3}>
|
||||
@@ -125,6 +127,7 @@ export const mapStateToProps = (state) => ({
|
||||
isLibrary: selectors.app.isLibrary(state),
|
||||
handout: selectors.video.handout(state),
|
||||
getHandoutDownloadUrl: selectors.video.getHandoutDownloadUrl(state),
|
||||
isUploadError: selectors.requests.isFailed(state, { requestKey: RequestKeys.uploadAsset }),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = (dispatch) => ({
|
||||
|
||||
@@ -24,6 +24,9 @@ jest.mock('../../../../../../data/redux', () => ({
|
||||
app: {
|
||||
isLibrary: jest.fn(args => ({ isLibrary: args })),
|
||||
},
|
||||
requests: {
|
||||
isFailed: jest.fn(args => ({ isFailed: args })),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
194
src/editors/containers/VideoGallery/hooks.js
Normal file
194
src/editors/containers/VideoGallery/hooks.js
Normal file
@@ -0,0 +1,194 @@
|
||||
import React from 'react';
|
||||
import * as module from './hooks';
|
||||
import messages from './messages';
|
||||
import {
|
||||
filterKeys,
|
||||
filterMessages,
|
||||
sortKeys,
|
||||
sortMessages,
|
||||
sortFunctions,
|
||||
} from './utils';
|
||||
|
||||
export const state = {
|
||||
highlighted: (val) => React.useState(val),
|
||||
searchString: (val) => React.useState(val),
|
||||
showSelectVideoError: (val) => React.useState(val),
|
||||
showSizeError: (val) => React.useState(val),
|
||||
sortBy: (val) => React.useState(val),
|
||||
filertBy: (val) => React.useState(val),
|
||||
hideSelectedVideos: (val) => React.useState(val),
|
||||
};
|
||||
|
||||
export const searchAndSortProps = () => {
|
||||
const [searchString, setSearchString] = module.state.searchString('');
|
||||
const [sortBy, setSortBy] = module.state.sortBy(sortKeys.dateNewest);
|
||||
const [filterBy, setFilterBy] = module.state.filertBy(filterKeys.videoStatus);
|
||||
const [hideSelectedVideos, setHideSelectedVideos] = module.state.hideSelectedVideos(false);
|
||||
|
||||
return {
|
||||
searchString,
|
||||
onSearchChange: (e) => setSearchString(e.target.value),
|
||||
clearSearchString: () => setSearchString(''),
|
||||
sortBy,
|
||||
onSortClick: (key) => () => setSortBy(key),
|
||||
sortKeys,
|
||||
sortMessages,
|
||||
filterBy,
|
||||
onFilterClick: (key) => () => setFilterBy(key),
|
||||
filterKeys,
|
||||
filterMessages,
|
||||
showSwitch: true,
|
||||
hideSelectedVideos,
|
||||
switchMessage: messages.hideSelectedCourseVideosSwitchLabel,
|
||||
onSwitchClick: () => setHideSelectedVideos(!hideSelectedVideos),
|
||||
};
|
||||
};
|
||||
|
||||
export const filterListBySearch = ({ searchString, videoList }) => (
|
||||
videoList.filter(({ displayName }) => displayName.toLowerCase().includes(searchString.toLowerCase()))
|
||||
);
|
||||
|
||||
export const filterListByStatus = ({ statusFilter, videoList }) => {
|
||||
if (statusFilter === filterKeys.videoStatus) {
|
||||
return videoList;
|
||||
}
|
||||
return videoList.filter(({ status }) => status === statusFilter);
|
||||
};
|
||||
|
||||
export const filterListByHideSelectedCourse = ({ videoList }) => (
|
||||
// TODO Missing to implement this
|
||||
videoList
|
||||
);
|
||||
|
||||
export const filterList = ({
|
||||
sortBy,
|
||||
filterBy,
|
||||
searchString,
|
||||
videos,
|
||||
}) => {
|
||||
let filteredList = module.filterListBySearch({
|
||||
searchString,
|
||||
videoList: videos,
|
||||
});
|
||||
filteredList = module.filterListByStatus({
|
||||
statusFilter: filterBy,
|
||||
videoList: filteredList,
|
||||
});
|
||||
filteredList = module.filterListByHideSelectedCourse({
|
||||
videoList: filteredList,
|
||||
});
|
||||
return filteredList.sort(sortFunctions[sortBy in sortKeys ? sortKeys[sortBy] : sortKeys.dateNewest]);
|
||||
};
|
||||
|
||||
export const videoListProps = ({ searchSortProps, videos }) => {
|
||||
const [highlighted, setHighlighted] = module.state.highlighted(null);
|
||||
const [
|
||||
showSelectVideoError,
|
||||
setShowSelectVideoError,
|
||||
] = module.state.showSelectVideoError(false);
|
||||
const [
|
||||
showSizeError,
|
||||
setShowSizeError,
|
||||
] = module.state.showSizeError(false);
|
||||
const filteredList = module.filterList({ ...searchSortProps, videos });
|
||||
return {
|
||||
galleryError: {
|
||||
show: showSelectVideoError,
|
||||
set: () => setShowSelectVideoError(true),
|
||||
dismiss: () => setShowSelectVideoError(false),
|
||||
message: messages.selectVideoError,
|
||||
},
|
||||
// TODO We need to update this message when implementing the video upload screen
|
||||
inputError: {
|
||||
show: showSizeError,
|
||||
set: () => setShowSizeError(true),
|
||||
dismiss: () => setShowSelectVideoError(false),
|
||||
message: messages.fileSizeError,
|
||||
},
|
||||
galleryProps: {
|
||||
galleryIsEmpty: Object.keys(filteredList).length === 0,
|
||||
searchIsEmpty: filteredList.length === 0,
|
||||
displayList: filteredList,
|
||||
highlighted,
|
||||
onHighlightChange: (e) => setHighlighted(e.target.value),
|
||||
emptyGalleryLabel: messages.emptyGalleryLabel,
|
||||
showIdsOnCards: true,
|
||||
height: '100%',
|
||||
},
|
||||
selectBtnProps: {
|
||||
onclick: () => {
|
||||
// TODO Update this when implementing the selection feature
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const fileInputProps = () => {
|
||||
// TODO [Update video] Implement this
|
||||
const ref = React.useRef();
|
||||
const click = () => ref.current.click();
|
||||
|
||||
return {
|
||||
click,
|
||||
addFile: () => {},
|
||||
ref,
|
||||
};
|
||||
};
|
||||
|
||||
export const buildVideos = ({ rawVideos }) => {
|
||||
let videos = [];
|
||||
const rawVideoList = Object.values(rawVideos);
|
||||
if (rawVideoList.length > 0) {
|
||||
videos = rawVideoList.map(video => ({
|
||||
id: video.edx_video_id,
|
||||
displayName: video.client_video_id,
|
||||
externalUrl: video.course_video_image_url,
|
||||
dateAdded: video.created,
|
||||
locked: false,
|
||||
thumbnail: video.course_video_image_url,
|
||||
status: video.status,
|
||||
statusBadgeVariant: module.getstatusBadgeVariant({ status: video.status }),
|
||||
duration: video.duration,
|
||||
transcripts: video.transcripts,
|
||||
}));
|
||||
}
|
||||
return videos;
|
||||
};
|
||||
|
||||
export const getstatusBadgeVariant = ({ status }) => {
|
||||
switch (status) {
|
||||
case filterKeys.failed:
|
||||
return 'danger';
|
||||
case filterKeys.uploading:
|
||||
case filterKeys.processing:
|
||||
return 'light';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const videoProps = ({ videos }) => {
|
||||
const searchSortProps = module.searchAndSortProps();
|
||||
const videoList = module.videoListProps({ searchSortProps, videos });
|
||||
const {
|
||||
galleryError,
|
||||
galleryProps,
|
||||
inputError,
|
||||
selectBtnProps,
|
||||
} = videoList;
|
||||
const fileInput = module.fileInputProps();
|
||||
|
||||
return {
|
||||
galleryError,
|
||||
inputError,
|
||||
fileInput,
|
||||
galleryProps,
|
||||
searchSortProps,
|
||||
selectBtnProps,
|
||||
};
|
||||
};
|
||||
|
||||
export default {
|
||||
videoProps,
|
||||
buildVideos,
|
||||
};
|
||||
253
src/editors/containers/VideoGallery/hooks.test.js
Normal file
253
src/editors/containers/VideoGallery/hooks.test.js
Normal file
@@ -0,0 +1,253 @@
|
||||
import * as hooks from './hooks';
|
||||
import { filterKeys, sortKeys } from './utils';
|
||||
import { MockUseState } from '../../../testUtils';
|
||||
import { keyStore } from '../../utils';
|
||||
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
useRef: jest.fn(val => ({ current: val })),
|
||||
useEffect: jest.fn(),
|
||||
useCallback: (cb, prereqs) => ({ cb, prereqs }),
|
||||
}));
|
||||
|
||||
jest.mock('react-redux', () => {
|
||||
const dispatchFn = jest.fn();
|
||||
return {
|
||||
...jest.requireActual('react-redux'),
|
||||
dispatch: dispatchFn,
|
||||
useDispatch: jest.fn(() => dispatchFn),
|
||||
};
|
||||
});
|
||||
|
||||
const state = new MockUseState(hooks);
|
||||
const hookKeys = keyStore(hooks);
|
||||
let hook;
|
||||
const testValue = 'testVALUEVALID';
|
||||
|
||||
describe('VideoGallery hooks', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('state hooks', () => {
|
||||
state.testGetter(state.keys.highlighted);
|
||||
state.testGetter(state.keys.searchString);
|
||||
state.testGetter(state.keys.showSelectVideoError);
|
||||
state.testGetter(state.keys.showSizeError);
|
||||
state.testGetter(state.keys.sortBy);
|
||||
state.testGetter(state.keys.filertBy);
|
||||
state.testGetter(state.keys.hideSelectedVideos);
|
||||
});
|
||||
describe('using state', () => {
|
||||
beforeEach(() => { state.mock(); });
|
||||
afterEach(() => { state.restore(); });
|
||||
|
||||
describe('searchAndSortProps', () => {
|
||||
beforeEach(() => {
|
||||
hook = hooks.searchAndSortProps();
|
||||
});
|
||||
it('returns searchString value, initialized to an empty string', () => {
|
||||
expect(state.stateVals.searchString).toEqual(hook.searchString);
|
||||
expect(state.stateVals.searchString).toEqual('');
|
||||
});
|
||||
it('returns highlighted value, initialized to dateNewest', () => {
|
||||
expect(state.stateVals.sortBy).toEqual(hook.sortBy);
|
||||
expect(state.stateVals.sortBy).toEqual(sortKeys.dateNewest);
|
||||
});
|
||||
test('onSearchChange sets searchString with event target value', () => {
|
||||
hook.onSearchChange({ target: { value: testValue } });
|
||||
expect(state.setState.searchString).toHaveBeenCalledWith(testValue);
|
||||
});
|
||||
test('clearSearchString sets search string to empty string', () => {
|
||||
hook.clearSearchString();
|
||||
expect(state.setState.searchString).toHaveBeenCalledWith('');
|
||||
});
|
||||
test('onSortClick takes a key and returns callback to set sortBY to that key', () => {
|
||||
hook.onSortClick(testValue);
|
||||
expect(state.setState.sortBy).not.toHaveBeenCalled();
|
||||
hook.onSortClick(testValue)();
|
||||
expect(state.setState.sortBy).toHaveBeenCalledWith(testValue);
|
||||
});
|
||||
});
|
||||
describe('filterListBySearch', () => {
|
||||
const matching = [
|
||||
'test',
|
||||
'TEst',
|
||||
'eeees',
|
||||
'essSSSS',
|
||||
];
|
||||
const notMatching = ['bad', 'other', 'bad stuff'];
|
||||
const searchString = 'eS';
|
||||
test('returns list filtered lowercase by displayName', () => {
|
||||
const filter = jest.fn(cb => ({ filter: cb }));
|
||||
hook = hooks.filterListBySearch({ searchString, videoList: { filter } });
|
||||
expect(filter).toHaveBeenCalled();
|
||||
const [[filterCb]] = filter.mock.calls;
|
||||
matching.forEach(val => expect(filterCb({ displayName: val })).toEqual(true));
|
||||
notMatching.forEach(val => expect(filterCb({ displayName: val })).toEqual(false));
|
||||
});
|
||||
});
|
||||
describe('buildVideos', () => {
|
||||
const rawVideos = [
|
||||
{
|
||||
edx_video_id: 'id_1',
|
||||
client_video_id: 'client_id_1',
|
||||
course_video_image_url: 'course_video_image_url_1',
|
||||
created: 'created_1',
|
||||
status: 'status_1',
|
||||
duration: 1,
|
||||
transcripts: [],
|
||||
},
|
||||
{
|
||||
edx_video_id: 'id_2',
|
||||
client_video_id: 'client_id_2',
|
||||
course_video_image_url: 'course_video_image_url_2',
|
||||
created: 'created_2',
|
||||
status: 'status_2',
|
||||
duration: 2,
|
||||
transcripts: [],
|
||||
},
|
||||
];
|
||||
const expectedValues = [
|
||||
{
|
||||
id: 'id_1',
|
||||
displayName: 'client_id_1',
|
||||
externalUrl: 'course_video_image_url_1',
|
||||
dateAdded: 'created_1',
|
||||
locked: false,
|
||||
thumbnail: 'course_video_image_url_1',
|
||||
status: 'status_1',
|
||||
statusBadgeVariant: null,
|
||||
duration: 1,
|
||||
transcripts: [],
|
||||
},
|
||||
{
|
||||
id: 'id_2',
|
||||
displayName: 'client_id_2',
|
||||
externalUrl: 'course_video_image_url_2',
|
||||
dateAdded: 'created_2',
|
||||
locked: false,
|
||||
thumbnail: 'course_video_image_url_2',
|
||||
status: 'status_2',
|
||||
statusBadgeVariant: null,
|
||||
duration: 2,
|
||||
transcripts: [],
|
||||
},
|
||||
];
|
||||
test('return the expected values', () => {
|
||||
const values = hooks.buildVideos({ rawVideos });
|
||||
expect(values).toEqual(expectedValues);
|
||||
});
|
||||
});
|
||||
describe('getstatusBadgeVariant', () => {
|
||||
test('return the expected values', () => {
|
||||
let value = hooks.getstatusBadgeVariant({ status: filterKeys.failed });
|
||||
expect(value).toEqual('danger');
|
||||
value = hooks.getstatusBadgeVariant({ status: filterKeys.uploading });
|
||||
expect(value).toEqual('light');
|
||||
value = hooks.getstatusBadgeVariant({ status: filterKeys.processing });
|
||||
expect(value).toEqual('light');
|
||||
value = hooks.getstatusBadgeVariant({ status: filterKeys.videoStatus });
|
||||
expect(value).toBeNull();
|
||||
value = hooks.getstatusBadgeVariant({ status: filterKeys.ready });
|
||||
expect(value).toBeNull();
|
||||
});
|
||||
});
|
||||
describe('videoListProps outputs', () => {
|
||||
const props = {
|
||||
searchSortProps: {
|
||||
searchString: 'Es',
|
||||
sortBy: sortKeys.dateNewest,
|
||||
filterBy: filterKeys.videoStatus,
|
||||
},
|
||||
videos: [
|
||||
{
|
||||
displayName: 'sOmEuiMAge',
|
||||
staTICUrl: '/assets/sOmEuiMAge',
|
||||
id: 'sOmEuiMAgeURl',
|
||||
},
|
||||
],
|
||||
};
|
||||
const filterList = (args) => ({ filterList: args });
|
||||
const load = () => {
|
||||
jest.spyOn(hooks, hookKeys.filterList).mockImplementationOnce(filterList);
|
||||
hook = hooks.videoListProps(props);
|
||||
};
|
||||
beforeEach(() => {
|
||||
load();
|
||||
});
|
||||
describe('galleryProps', () => {
|
||||
it('returns highlighted value, initialized to null', () => {
|
||||
expect(hook.galleryProps.highlighted).toEqual(state.stateVals.highlighted);
|
||||
expect(state.stateVals.highlighted).toEqual(null);
|
||||
});
|
||||
test('onHighlightChange sets highlighted with event target value', () => {
|
||||
hook.galleryProps.onHighlightChange({ target: { value: testValue } });
|
||||
expect(state.setState.highlighted).toHaveBeenCalledWith(testValue);
|
||||
});
|
||||
test('displayList returns filterListhook called with searchSortProps and videos', () => {
|
||||
expect(hook.galleryProps.displayList).toEqual(filterList({
|
||||
...props.searchSortProps,
|
||||
videos: props.videos,
|
||||
}));
|
||||
});
|
||||
});
|
||||
describe('galleryError', () => {
|
||||
test('show is initialized to false and returns properly', () => {
|
||||
const show = 'sHOWSelectiRROr';
|
||||
expect(hook.galleryError.show).toEqual(false);
|
||||
state.mockVal(state.keys.showSelectVideoError, show);
|
||||
hook = hooks.videoListProps(props);
|
||||
expect(hook.galleryError.show).toEqual(show);
|
||||
});
|
||||
test('set sets showSelectVideoError to true', () => {
|
||||
hook.galleryError.set();
|
||||
expect(state.setState.showSelectVideoError).toHaveBeenCalledWith(true);
|
||||
});
|
||||
test('dismiss sets showSelectVideoError to false', () => {
|
||||
hook.galleryError.dismiss();
|
||||
expect(state.setState.showSelectVideoError).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('videoProps', () => {
|
||||
const videoList = {
|
||||
galleryProps: 'some gallery props',
|
||||
selectBtnProps: 'some select btn props',
|
||||
};
|
||||
const searchAndSortProps = { search: 'props' };
|
||||
const fileInput = { file: 'input hooks' };
|
||||
const videos = { video: { staTICUrl: '/assets/sOmEuiMAge' } };
|
||||
const spies = {};
|
||||
beforeEach(() => {
|
||||
spies.videoList = jest.spyOn(hooks, hookKeys.videoListProps)
|
||||
.mockReturnValueOnce(videoList);
|
||||
spies.search = jest.spyOn(hooks, hookKeys.searchAndSortProps)
|
||||
.mockReturnValueOnce(searchAndSortProps);
|
||||
spies.file = jest.spyOn(hooks, hookKeys.fileInputProps)
|
||||
.mockReturnValueOnce(fileInput);
|
||||
hook = hooks.videoProps({ videos });
|
||||
});
|
||||
it('forwards fileInput as fileInput', () => {
|
||||
expect(hook.fileInput).toEqual(fileInput);
|
||||
expect(spies.file.mock.calls.length).toEqual(1);
|
||||
expect(spies.file).toHaveBeenCalled();
|
||||
});
|
||||
it('initializes videoList', () => {
|
||||
expect(spies.videoList.mock.calls.length).toEqual(1);
|
||||
expect(spies.videoList).toHaveBeenCalledWith({
|
||||
searchSortProps: searchAndSortProps,
|
||||
videos,
|
||||
});
|
||||
});
|
||||
it('forwards searchAndSortHooks as searchSortProps', () => {
|
||||
expect(hook.searchSortProps).toEqual(searchAndSortProps);
|
||||
expect(spies.search.mock.calls.length).toEqual(1);
|
||||
expect(spies.search).toHaveBeenCalled();
|
||||
});
|
||||
it('forwards galleryProps and selectBtnProps from the video list hooks', () => {
|
||||
expect(hook.galleryProps).toEqual(videoList.galleryProps);
|
||||
expect(hook.selectBtnProps).toEqual(videoList.selectBtnProps);
|
||||
});
|
||||
});
|
||||
});
|
||||
77
src/editors/containers/VideoGallery/index.jsx
Normal file
77
src/editors/containers/VideoGallery/index.jsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { selectors } from '../../data/redux';
|
||||
import hooks from './hooks';
|
||||
import SelectionModal from '../../sharedComponents/SelectionModal';
|
||||
import { acceptedImgKeys } from './utils';
|
||||
import messages from './messages';
|
||||
import { RequestKeys } from '../../data/constants/requests';
|
||||
|
||||
export const VideoGallery = ({
|
||||
// redux
|
||||
rawVideos,
|
||||
isLoaded,
|
||||
isFetchError,
|
||||
isUploadError,
|
||||
}) => {
|
||||
const videos = hooks.buildVideos({ rawVideos });
|
||||
const {
|
||||
galleryError,
|
||||
inputError,
|
||||
fileInput,
|
||||
galleryProps,
|
||||
searchSortProps,
|
||||
selectBtnProps,
|
||||
} = hooks.videoProps({ videos });
|
||||
|
||||
const modalMessages = {
|
||||
confirmMsg: messages.selectVideoButtonlabel,
|
||||
titleMsg: messages.titleLabel,
|
||||
uploadButtonMsg: messages.uploadButtonLabel,
|
||||
fetchError: messages.fetchVideosError,
|
||||
uploadError: messages.uploadVideoError,
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SelectionModal
|
||||
{...{
|
||||
isOpen: true,
|
||||
close: () => { /* TODO */ },
|
||||
size: 'fullscreen',
|
||||
isFullscreenScroll: false,
|
||||
galleryError,
|
||||
inputError,
|
||||
fileInput,
|
||||
galleryProps,
|
||||
searchSortProps,
|
||||
selectBtnProps,
|
||||
acceptedFiles: acceptedImgKeys,
|
||||
modalMessages,
|
||||
isLoaded,
|
||||
isUploadError,
|
||||
isFetchError,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
VideoGallery.propTypes = {
|
||||
rawVideos: PropTypes.shape({}).isRequired,
|
||||
isLoaded: PropTypes.bool.isRequired,
|
||||
isFetchError: PropTypes.bool.isRequired,
|
||||
isUploadError: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
rawVideos: selectors.app.videos(state),
|
||||
isLoaded: selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchVideos }),
|
||||
isFetchError: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchVideos }),
|
||||
isUploadError: selectors.requests.isFailed(state, { requestKey: RequestKeys.uploadVideo }),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(VideoGallery);
|
||||
87
src/editors/containers/VideoGallery/index.test.jsx
Normal file
87
src/editors/containers/VideoGallery/index.test.jsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import SelectionModal from '../../sharedComponents/SelectionModal';
|
||||
import hooks from './hooks';
|
||||
import * as module from '.';
|
||||
|
||||
jest.mock('../../sharedComponents/SelectionModal', () => 'SelectionModal');
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
buildVideos: jest.fn(() => []),
|
||||
videoProps: jest.fn(() => ({
|
||||
galleryError: {
|
||||
show: 'ShoWERror gAlLery',
|
||||
set: jest.fn(),
|
||||
dismiss: jest.fn(),
|
||||
message: {
|
||||
id: 'Gallery error id',
|
||||
defaultMessage: 'Gallery error',
|
||||
description: 'Gallery error',
|
||||
},
|
||||
},
|
||||
inputError: {
|
||||
show: 'ShoWERror inPUT',
|
||||
set: jest.fn(),
|
||||
dismiss: jest.fn(),
|
||||
message: {
|
||||
id: 'Input error id',
|
||||
defaultMessage: 'Input error',
|
||||
description: 'Input error',
|
||||
},
|
||||
},
|
||||
fileInput: {
|
||||
addFile: 'videoHooks.fileInput.addFile',
|
||||
click: 'videoHooks.fileInput.click',
|
||||
ref: 'videoHooks.fileInput.ref',
|
||||
},
|
||||
galleryProps: { gallery: 'props' },
|
||||
searchSortProps: { search: 'sortProps' },
|
||||
selectBtnProps: { select: 'btnProps' },
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('../../data/redux', () => ({
|
||||
selectors: {
|
||||
requests: {
|
||||
isLoaded: (state, { requestKey }) => ({ isLoaded: { state, requestKey } }),
|
||||
isFetchError: (state, { requestKey }) => ({ isFetchError: { state, requestKey } }),
|
||||
isUploadError: (state, { requestKey }) => ({ isUploadError: { state, requestKey } }),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('VideoGallery', () => {
|
||||
describe('component', () => {
|
||||
const props = {
|
||||
rawVideos: { sOmEaSsET: { staTICUrl: '/video/sOmEaSsET' } },
|
||||
isLoaded: false,
|
||||
isFetchError: false,
|
||||
isUploadError: false,
|
||||
};
|
||||
let el;
|
||||
const videoProps = hooks.videoProps();
|
||||
beforeEach(() => {
|
||||
el = shallow(<module.VideoGallery {...props} />);
|
||||
});
|
||||
it('provides confirm action, forwarding selectBtnProps from imgHooks', () => {
|
||||
expect(el.find(SelectionModal).props().selectBtnProps).toEqual(
|
||||
expect.objectContaining({ ...hooks.videoProps().selectBtnProps }),
|
||||
);
|
||||
});
|
||||
it('provides file upload button linked to fileInput.click', () => {
|
||||
expect(el.find(SelectionModal).props().fileInput.click).toEqual(
|
||||
videoProps.fileInput.click,
|
||||
);
|
||||
});
|
||||
it('provides a SearchSort component with searchSortProps from imgHooks', () => {
|
||||
expect(el.find(SelectionModal).props().searchSortProps).toEqual(videoProps.searchSortProps);
|
||||
});
|
||||
it('provides a Gallery component with galleryProps from imgHooks', () => {
|
||||
expect(el.find(SelectionModal).props().galleryProps).toEqual(videoProps.galleryProps);
|
||||
});
|
||||
it('provides a FileInput component with fileInput props from imgHooks', () => {
|
||||
expect(el.find(SelectionModal).props().fileInput).toMatchObject(videoProps.fileInput);
|
||||
});
|
||||
});
|
||||
});
|
||||
117
src/editors/containers/VideoGallery/messages.js
Normal file
117
src/editors/containers/VideoGallery/messages.js
Normal file
@@ -0,0 +1,117 @@
|
||||
export const messages = {
|
||||
// Gallery
|
||||
emptyGalleryLabel: {
|
||||
id: 'authoring.selectvideomodal.emptyGalleryLabel',
|
||||
defaultMessage:
|
||||
'No results found.',
|
||||
description: 'Label for when video gallery is empty.',
|
||||
},
|
||||
selectVideoButtonlabel: {
|
||||
id: 'authoring.selectvideomodal.selectvideo.label',
|
||||
defaultMessage: 'Select video',
|
||||
description: 'Label for Select video button',
|
||||
},
|
||||
titleLabel: {
|
||||
id: 'authoring.selectvideomodal.title.label',
|
||||
defaultMessage: 'Add video to your course',
|
||||
description: 'Title for the select video modal',
|
||||
},
|
||||
uploadButtonLabel: {
|
||||
id: 'authoring.selectvideomodal.upload.label',
|
||||
defaultMessage: 'Upload or embed a new video',
|
||||
description: 'Label for upload button',
|
||||
},
|
||||
|
||||
// Sort Dropdown
|
||||
sortByDateNewest: {
|
||||
id: 'authoring.selectvideomodal.sort.datenewest.label',
|
||||
defaultMessage: 'By date added (newest)',
|
||||
description: 'Dropdown label for sorting by date (newest)',
|
||||
},
|
||||
sortByDateOldest: {
|
||||
id: 'authoring.selectvideomodal.sort.dateoldest.label',
|
||||
defaultMessage: 'By date added (oldest)',
|
||||
description: 'Dropdown label for sorting by date (oldest)',
|
||||
},
|
||||
sortByNameAscending: {
|
||||
id: 'authoring.selectvideomodal.sort.nameascending.label',
|
||||
defaultMessage: 'By name (ascending)',
|
||||
description: 'Dropdown label for sorting by name (ascending)',
|
||||
},
|
||||
sortByNameDescending: {
|
||||
id: 'authoring.selectvideomodal.sort.namedescending.label',
|
||||
defaultMessage: 'By name (descending)',
|
||||
description: 'Dropdown label for sorting by name (descending)',
|
||||
},
|
||||
sortByDurationShortest: {
|
||||
id: 'authoring.selectvideomodal.sort.durationshortest.label',
|
||||
defaultMessage: 'By duration (shortest)',
|
||||
description: 'Dropdown label for sorting by duration (shortest)',
|
||||
},
|
||||
sortByDurationLongest: {
|
||||
id: 'authoring.selectvideomodal.sort.durationlongest.label',
|
||||
defaultMessage: 'By duration (longest)',
|
||||
description: 'Dropdown label for sorting by duration (longest)',
|
||||
},
|
||||
|
||||
// Filter Dropdown
|
||||
filterByVideoStatusNone: {
|
||||
id: 'authoring.selectvideomodal.filter.videostatusnone.label',
|
||||
defaultMessage: 'Video status',
|
||||
description: 'Dropdown label for filter by video status (none)',
|
||||
},
|
||||
filterByVideoStatusUploading: {
|
||||
id: 'authoring.selectvideomodal.filter.videostatusuploading.label',
|
||||
defaultMessage: 'Uploading',
|
||||
description: 'Dropdown label for filter by video status (uploading)',
|
||||
},
|
||||
filterByVideoStatusProcessing: {
|
||||
id: 'authoring.selectvideomodal.filter.videostatusprocessing.label',
|
||||
defaultMessage: 'Processing',
|
||||
description: 'Dropdown label for filter by video status (processing)',
|
||||
},
|
||||
filterByVideoStatusReady: {
|
||||
id: 'authoring.selectvideomodal.filter.videostatusready.label',
|
||||
defaultMessage: 'Ready',
|
||||
description: 'Dropdown label for filter by video status (ready)',
|
||||
},
|
||||
filterByVideoStatusFailed: {
|
||||
id: 'authoring.selectvideomodal.filter.videostatusfailed.label',
|
||||
defaultMessage: 'Failed',
|
||||
description: 'Dropdown label for filter by video status (failed)',
|
||||
},
|
||||
|
||||
// Hide switch
|
||||
hideSelectedCourseVideosSwitchLabel: {
|
||||
id: 'authoring.selectvideomodal.switch.hideselectedcoursevideos.label',
|
||||
defaultMessage: 'Hide selected course videos',
|
||||
description: 'Switch label for hide selected course videos',
|
||||
},
|
||||
|
||||
// Errors
|
||||
selectVideoError: {
|
||||
id: 'authoring.selectvideomodal.error.selectVideoError',
|
||||
defaultMessage: 'Select a video to continue.',
|
||||
description:
|
||||
'Message presented to user when clicking Next without selecting a video',
|
||||
},
|
||||
fileSizeError: {
|
||||
id: 'authoring.selectvideomodal.error.fileSizeError',
|
||||
defaultMessage:
|
||||
'Video must be 10 MB or less. Please resize image and try again.',
|
||||
description:
|
||||
'Message presented to user when file size of video is larger than 10 MB',
|
||||
},
|
||||
uploadVideoError: {
|
||||
id: 'authoring.selectvideomodal.error.uploadVideoError',
|
||||
defaultMessage: 'Failed to upload video. Please try again.',
|
||||
description: 'Message presented to user when video fails to upload',
|
||||
},
|
||||
fetchVideosError: {
|
||||
id: 'authoring.selectvideomodal.error.fetchVideosError',
|
||||
defaultMessage: 'Failed to obtain course videos. Please try again.',
|
||||
description: 'Message presented to user when videos are not found',
|
||||
},
|
||||
};
|
||||
|
||||
export default messages;
|
||||
63
src/editors/containers/VideoGallery/utils.js
Normal file
63
src/editors/containers/VideoGallery/utils.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import { StrictDict, keyStore } from '../../utils';
|
||||
import messages from './messages';
|
||||
|
||||
const messageKeys = keyStore(messages);
|
||||
|
||||
export const sortKeys = StrictDict({
|
||||
dateNewest: 'dateNewest',
|
||||
dateOldest: 'dateOldest',
|
||||
nameAscending: 'nameAscending',
|
||||
nameDescending: 'nameDescending',
|
||||
durationShortest: 'durationShortest',
|
||||
durationLongest: 'durationLongest',
|
||||
});
|
||||
|
||||
export const sortMessages = StrictDict({
|
||||
dateNewest: messages[messageKeys.sortByDateNewest],
|
||||
dateOldest: messages[messageKeys.sortByDateOldest],
|
||||
nameAscending: messages[messageKeys.sortByNameAscending],
|
||||
nameDescending: messages[messageKeys.sortByNameDescending],
|
||||
durationShortest: messages[messageKeys.sortByDurationShortest],
|
||||
durationLongest: messages[messageKeys.sortByDurationLongest],
|
||||
});
|
||||
|
||||
export const filterKeys = StrictDict({
|
||||
videoStatus: 'videoStatus',
|
||||
uploading: 'uploading',
|
||||
processing: 'processing',
|
||||
ready: 'ready',
|
||||
failed: 'failed',
|
||||
});
|
||||
|
||||
export const filterMessages = StrictDict({
|
||||
videoStatus: messages[messageKeys.filterByVideoStatusNone],
|
||||
uploading: messages[messageKeys.filterByVideoStatusUploading],
|
||||
processing: messages[messageKeys.filterByVideoStatusProcessing],
|
||||
ready: messages[messageKeys.filterByVideoStatusReady],
|
||||
failed: messages[messageKeys.filterByVideoStatusFailed],
|
||||
});
|
||||
|
||||
export const sortFunctions = StrictDict({
|
||||
dateNewest: (a, b) => b.dateAdded - a.dateAdded,
|
||||
dateOldest: (a, b) => a.dateAdded - b.dateAdded,
|
||||
nameAscending: (a, b) => {
|
||||
const nameA = a.displayName.toLowerCase();
|
||||
const nameB = b.displayName.toLowerCase();
|
||||
if (nameA < nameB) { return -1; }
|
||||
if (nameB < nameA) { return 1; }
|
||||
return b.dateAdded - a.dateAdded;
|
||||
},
|
||||
nameDescending: (a, b) => {
|
||||
const nameA = a.displayName.toLowerCase();
|
||||
const nameB = b.displayName.toLowerCase();
|
||||
if (nameA < nameB) { return 1; }
|
||||
if (nameB < nameA) { return -1; }
|
||||
return b.dateAdded - a.dateAdded;
|
||||
},
|
||||
durationShortest: (a, b) => a.duration - b.duration,
|
||||
durationLongest: (a, b) => b.duration - a.duration,
|
||||
});
|
||||
|
||||
export const acceptedImgKeys = StrictDict({
|
||||
mp4: '.mp4',
|
||||
});
|
||||
62
src/editors/containers/VideoGallery/utils.test.js
Normal file
62
src/editors/containers/VideoGallery/utils.test.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import { sortFunctions } from './utils';
|
||||
|
||||
describe('VideGallery utils', () => {
|
||||
describe('sortFunctions', () => {
|
||||
const dateA = {
|
||||
dateAdded: new Date('2023-03-30'),
|
||||
};
|
||||
const dateB = {
|
||||
dateAdded: new Date('2023-03-31'),
|
||||
};
|
||||
const nameA = {
|
||||
displayName: 'This is the Name A',
|
||||
dateAdded: new Date('2023-03-30'),
|
||||
};
|
||||
const nameB = {
|
||||
displayName: 'Hello World',
|
||||
dateAdded: new Date('2023-03-30'),
|
||||
};
|
||||
const nameC = {
|
||||
displayName: 'Hello World',
|
||||
dateAdded: new Date('2023-03-31'),
|
||||
};
|
||||
const durationA = {
|
||||
duration: 10,
|
||||
};
|
||||
const durationB = {
|
||||
duration: 100,
|
||||
};
|
||||
test('correct functionality of dateNewest', () => {
|
||||
expect(sortFunctions.dateNewest(dateA, dateB)).toBeGreaterThan(0);
|
||||
expect(sortFunctions.dateNewest(dateB, dateA)).toBeLessThan(0);
|
||||
expect(sortFunctions.dateNewest(dateA, dateA)).toEqual(0);
|
||||
});
|
||||
test('correct functionality of dateOldest', () => {
|
||||
expect(sortFunctions.dateOldest(dateA, dateB)).toBeLessThan(0);
|
||||
expect(sortFunctions.dateOldest(dateB, dateA)).toBeGreaterThan(0);
|
||||
expect(sortFunctions.dateOldest(dateA, dateA)).toEqual(0);
|
||||
});
|
||||
test('correct functionality of nameAscending', () => {
|
||||
expect(sortFunctions.nameAscending(nameA, nameB)).toEqual(1);
|
||||
expect(sortFunctions.nameAscending(nameB, nameA)).toEqual(-1);
|
||||
expect(sortFunctions.nameAscending(nameA, nameA)).toEqual(0);
|
||||
expect(sortFunctions.nameAscending(nameB, nameC)).toBeGreaterThan(0);
|
||||
});
|
||||
test('correct functionality of nameDescending', () => {
|
||||
expect(sortFunctions.nameDescending(nameA, nameB)).toEqual(-1);
|
||||
expect(sortFunctions.nameDescending(nameB, nameA)).toEqual(1);
|
||||
expect(sortFunctions.nameDescending(nameA, nameA)).toEqual(0);
|
||||
expect(sortFunctions.nameDescending(nameB, nameC)).toBeGreaterThan(0);
|
||||
});
|
||||
test('correct functionality of durationShortest', () => {
|
||||
expect(sortFunctions.durationShortest(durationA, durationB)).toBeLessThan(0);
|
||||
expect(sortFunctions.durationShortest(durationB, durationA)).toBeGreaterThan(0);
|
||||
expect(sortFunctions.durationShortest(durationA, durationA)).toEqual(0);
|
||||
});
|
||||
test('correct functionality of durationLongest', () => {
|
||||
expect(sortFunctions.durationLongest(durationA, durationB)).toBeGreaterThan(0);
|
||||
expect(sortFunctions.durationLongest(durationB, durationA)).toBeLessThan(0);
|
||||
expect(sortFunctions.durationLongest(durationA, durationA)).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -9,12 +9,14 @@ export const RequestStates = StrictDict({
|
||||
|
||||
export const RequestKeys = StrictDict({
|
||||
fetchAssets: 'fetchAssets',
|
||||
fetchVideos: 'fetchVideos',
|
||||
fetchBlock: 'fetchBlock',
|
||||
fetchImages: 'fetchImages',
|
||||
fetchUnit: 'fetchUnit',
|
||||
fetchStudioView: 'fetchStudioView',
|
||||
saveBlock: 'saveBlock',
|
||||
uploadAsset: 'uploadAsset',
|
||||
uploadVideo: 'uploadVideo',
|
||||
allowThumbnailUpload: 'allowThumbnailUpload',
|
||||
uploadThumbnail: 'uploadThumbnail',
|
||||
uploadTranscript: 'uploadTranscript',
|
||||
|
||||
@@ -16,6 +16,7 @@ const initialState = {
|
||||
studioEndpointUrl: null,
|
||||
lmsEndpointUrl: null,
|
||||
assets: {},
|
||||
videos: {},
|
||||
courseDetails: {},
|
||||
};
|
||||
|
||||
@@ -45,6 +46,7 @@ const app = createSlice({
|
||||
setSaveResponse: (state, { payload }) => ({ ...state, saveResponse: payload }),
|
||||
initializeEditor: (state) => ({ ...state, editorInitialized: true }),
|
||||
setAssets: (state, { payload }) => ({ ...state, assets: payload }),
|
||||
setVideos: (state, { payload }) => ({ ...state, videos: payload }),
|
||||
setCourseDetails: (state, { payload }) => ({ ...state, courseDetails: payload }),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -48,6 +48,7 @@ describe('app reducer', () => {
|
||||
['setBlockTitle', 'blockTitle'],
|
||||
['setSaveResponse', 'saveResponse'],
|
||||
['setAssets', 'assets'],
|
||||
['setVideos', 'videos'],
|
||||
['setCourseDetails', 'courseDetails'],
|
||||
].map(args => setterTest(...args));
|
||||
describe('setBlockValue', () => {
|
||||
|
||||
@@ -22,6 +22,7 @@ export const simpleSelectors = {
|
||||
unitUrl: mkSimpleSelector(app => app.unitUrl),
|
||||
blockTitle: mkSimpleSelector(app => app.blockTitle),
|
||||
assets: mkSimpleSelector(app => app.assets),
|
||||
videos: mkSimpleSelector(app => app.videos),
|
||||
};
|
||||
|
||||
export const returnUrl = createSelector(
|
||||
|
||||
@@ -48,6 +48,7 @@ describe('app selectors unit tests', () => {
|
||||
simpleKeys.blockTitle,
|
||||
simpleKeys.studioView,
|
||||
simpleKeys.assets,
|
||||
simpleKeys.videos,
|
||||
].map(testSimpleSelector);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,8 @@ const initialState = {
|
||||
[RequestKeys.deleteTranscript]: { status: RequestStates.inactive },
|
||||
[RequestKeys.fetchCourseDetails]: { status: RequestStates.inactive },
|
||||
[RequestKeys.fetchAssets]: { status: RequestStates.inactive },
|
||||
[RequestKeys.fetchVideos]: { status: RequestStates.inactive },
|
||||
[RequestKeys.uploadVideo]: { status: RequestStates.inactive },
|
||||
[RequestKeys.checkTranscriptsForImport]: { status: RequestStates.inactive },
|
||||
[RequestKeys.importTranscript]: { status: RequestStates.inactive },
|
||||
[RequestKeys.fetchVideoFeatures]: { status: RequestStates.inactive },
|
||||
|
||||
@@ -31,6 +31,12 @@ export const fetchAssets = () => (dispatch) => {
|
||||
}));
|
||||
};
|
||||
|
||||
export const fetchVideos = () => (dispatch) => {
|
||||
dispatch(requests.fetchVideos({
|
||||
onSuccess: (response) => dispatch(actions.app.setVideos(response.data.videos)),
|
||||
}));
|
||||
};
|
||||
|
||||
export const fetchCourseDetails = () => (dispatch) => {
|
||||
dispatch(requests.fetchCourseDetails({
|
||||
onSuccess: (response) => dispatch(actions.app.setCourseDetails(response)),
|
||||
@@ -50,6 +56,7 @@ export const initialize = (data) => (dispatch) => {
|
||||
dispatch(module.fetchUnit());
|
||||
dispatch(module.fetchStudioView());
|
||||
dispatch(module.fetchAssets());
|
||||
dispatch(module.fetchVideos());
|
||||
dispatch(module.fetchCourseDetails());
|
||||
};
|
||||
|
||||
@@ -74,11 +81,6 @@ export const uploadImage = ({ file, setSelection }) => (dispatch) => {
|
||||
}));
|
||||
};
|
||||
|
||||
export const fetchVideos = ({ onSuccess }) => (dispatch) => {
|
||||
dispatch(requests.fetchAssets({ onSuccess }));
|
||||
// onSuccess(mockData.mockVideoData);
|
||||
};
|
||||
|
||||
export default StrictDict({
|
||||
fetchBlock,
|
||||
fetchCourseDetails,
|
||||
|
||||
@@ -9,6 +9,7 @@ jest.mock('./requests', () => ({
|
||||
uploadAsset: (args) => ({ uploadAsset: args }),
|
||||
fetchStudioView: (args) => ({ fetchStudioView: args }),
|
||||
fetchAssets: (args) => ({ fetchAssets: args }),
|
||||
fetchVideos: (args) => ({ fetchVideos: args }),
|
||||
fetchCourseDetails: (args) => ({ fetchCourseDetails: args }),
|
||||
}));
|
||||
|
||||
@@ -105,12 +106,14 @@ describe('app thunkActions', () => {
|
||||
fetchUnit,
|
||||
fetchStudioView,
|
||||
fetchAssets,
|
||||
fetchVideos,
|
||||
fetchCourseDetails,
|
||||
} = thunkActions;
|
||||
thunkActions.fetchBlock = () => 'fetchBlock';
|
||||
thunkActions.fetchUnit = () => 'fetchUnit';
|
||||
thunkActions.fetchStudioView = () => 'fetchStudioView';
|
||||
thunkActions.fetchAssets = () => 'fetchAssets';
|
||||
thunkActions.fetchVideos = () => 'fetchVideos';
|
||||
thunkActions.fetchCourseDetails = () => 'fetchCourseDetails';
|
||||
thunkActions.initialize(testValue)(dispatch);
|
||||
expect(dispatch.mock.calls).toEqual([
|
||||
@@ -119,12 +122,14 @@ describe('app thunkActions', () => {
|
||||
[thunkActions.fetchUnit()],
|
||||
[thunkActions.fetchStudioView()],
|
||||
[thunkActions.fetchAssets()],
|
||||
[thunkActions.fetchVideos()],
|
||||
[thunkActions.fetchCourseDetails()],
|
||||
]);
|
||||
thunkActions.fetchBlock = fetchBlock;
|
||||
thunkActions.fetchUnit = fetchUnit;
|
||||
thunkActions.fetchStudioView = fetchStudioView;
|
||||
thunkActions.fetchAssets = fetchAssets;
|
||||
thunkActions.fetchVideos = fetchVideos;
|
||||
thunkActions.fetchCourseDetails = fetchCourseDetails;
|
||||
});
|
||||
});
|
||||
@@ -161,6 +166,15 @@ describe('app thunkActions', () => {
|
||||
expect(dispatch).toHaveBeenCalledWith(actions.app.setAssets(response));
|
||||
});
|
||||
});
|
||||
describe('fetchVideos', () => {
|
||||
it('dispatches fetchVideos action with setVideos for onSuccess param', () => {
|
||||
const response = { data: { videos: 'testRESPONSE' } };
|
||||
thunkActions.fetchVideos()(dispatch);
|
||||
const [[dispatchCall]] = dispatch.mock.calls;
|
||||
dispatchCall.fetchVideos.onSuccess(response);
|
||||
expect(dispatch).toHaveBeenCalledWith(actions.app.setVideos(response.data.videos));
|
||||
});
|
||||
});
|
||||
describe('uploadImage', () => {
|
||||
const setSelection = jest.fn();
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -136,6 +136,18 @@ export const fetchAssets = ({ ...rest }) => (dispatch, getState) => {
|
||||
}));
|
||||
};
|
||||
|
||||
export const fetchVideos = ({ ...rest }) => (dispatch, getState) => {
|
||||
dispatch(module.networkRequest({
|
||||
requestKey: RequestKeys.fetchVideos,
|
||||
promise: api
|
||||
.fetchVideos({
|
||||
studioEndpointUrl: selectors.app.studioEndpointUrl(getState()),
|
||||
learningContextId: selectors.app.learningContextId(getState()),
|
||||
}),
|
||||
...rest,
|
||||
}));
|
||||
};
|
||||
|
||||
export const allowThumbnailUpload = ({ ...rest }) => (dispatch, getState) => {
|
||||
dispatch(module.networkRequest({
|
||||
requestKey: RequestKeys.allowThumbnailUpload,
|
||||
@@ -289,6 +301,7 @@ export default StrictDict({
|
||||
fetchUnit,
|
||||
saveBlock,
|
||||
fetchAssets,
|
||||
fetchVideos,
|
||||
uploadAsset,
|
||||
allowThumbnailUpload,
|
||||
uploadThumbnail,
|
||||
|
||||
@@ -27,6 +27,7 @@ jest.mock('../../services/cms/api', () => ({
|
||||
fetchCourseDetails: (args) => args,
|
||||
saveBlock: (args) => args,
|
||||
fetchAssets: ({ id, url }) => ({ id, url }),
|
||||
fetchVideos: ({ id, url }) => ({ id, url }),
|
||||
uploadAsset: (args) => args,
|
||||
loadImages: jest.fn(),
|
||||
uploadThumbnail: (args) => args,
|
||||
@@ -265,6 +266,32 @@ describe('requests thunkActions module', () => {
|
||||
expect(loadImages).toHaveBeenCalledWith({ fetchAssets: expectedArgs });
|
||||
});
|
||||
});
|
||||
describe('fetchVideos', () => {
|
||||
const expectedArgs = {
|
||||
studioEndpointUrl: selectors.app.studioEndpointUrl(testState),
|
||||
learningContextId: selectors.app.learningContextId(testState),
|
||||
};
|
||||
let fetchVideos;
|
||||
let dispatchedAction;
|
||||
beforeEach(() => {
|
||||
fetchVideos = jest.fn((args) => new Promise((resolve) => {
|
||||
resolve({ data: { videos: { fetchVideos: args } } });
|
||||
}));
|
||||
jest.spyOn(api, apiKeys.fetchVideos).mockImplementationOnce(fetchVideos);
|
||||
requests.fetchVideos({ ...fetchParams, onSuccess, onFailure })(dispatch, () => testState);
|
||||
[[dispatchedAction]] = dispatch.mock.calls;
|
||||
});
|
||||
it('dispatches networkRequest', () => {
|
||||
expect(dispatchedAction.networkRequest).not.toEqual(undefined);
|
||||
});
|
||||
test('forwards onSuccess and onFailure', () => {
|
||||
expect(dispatchedAction.networkRequest.onSuccess).toEqual(onSuccess);
|
||||
expect(dispatchedAction.networkRequest.onFailure).toEqual(onFailure);
|
||||
});
|
||||
test('api.fetchVideos promise called with studioEndpointUrl and learningContextId', () => {
|
||||
expect(fetchVideos).toHaveBeenCalledWith(expectedArgs);
|
||||
});
|
||||
});
|
||||
describe('saveBlock', () => {
|
||||
const content = 'SoME HtMl CoNtent As String';
|
||||
testNetworkRequestAction({
|
||||
|
||||
@@ -18,6 +18,9 @@ export const apiMethods = {
|
||||
fetchAssets: ({ learningContextId, studioEndpointUrl }) => get(
|
||||
urls.courseAssets({ studioEndpointUrl, learningContextId }),
|
||||
),
|
||||
fetchVideos: ({ studioEndpointUrl, learningContextId }) => get(
|
||||
urls.courseVideos({ studioEndpointUrl, learningContextId }),
|
||||
),
|
||||
fetchCourseDetails: ({ studioEndpointUrl, learningContextId }) => get(
|
||||
urls.courseDetailsUrl({ studioEndpointUrl, learningContextId }),
|
||||
),
|
||||
|
||||
@@ -21,8 +21,11 @@ jest.mock('./urls', () => ({
|
||||
allowThumbnailUpload: jest.fn().mockName('urls.allowThumbnailUpload'),
|
||||
thumbnailUpload: jest.fn().mockName('urls.thumbnailUpload'),
|
||||
checkTranscriptsForImport: jest.fn().mockName('urls.checkTranscriptsForImport'),
|
||||
courseDetailsUrl: jest.fn().mockName('urls.courseDetailsUrl'),
|
||||
courseAdvanceSettings: jest.fn().mockName('urls.courseAdvanceSettings'),
|
||||
replaceTranscript: jest.fn().mockName('urls.replaceTranscript'),
|
||||
videoFeatures: jest.fn().mockName('urls.videoFeatures'),
|
||||
courseVideos: jest.fn().mockName('urls.courseVideos'),
|
||||
}));
|
||||
|
||||
jest.mock('./utils', () => ({
|
||||
@@ -73,6 +76,27 @@ describe('cms api', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchCourseDetails', () => {
|
||||
it('should call get with url.courseDetailsUrl', () => {
|
||||
apiMethods.fetchCourseDetails({ learningContextId, studioEndpointUrl });
|
||||
expect(get).toHaveBeenCalledWith(urls.courseDetailsUrl({ studioEndpointUrl, learningContextId }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchVideos', () => {
|
||||
it('should call get with url.courseVideos', () => {
|
||||
apiMethods.fetchVideos({ learningContextId, studioEndpointUrl });
|
||||
expect(get).toHaveBeenCalledWith(urls.courseVideos({ studioEndpointUrl, learningContextId }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchAdvancedSettings', () => {
|
||||
it('should call get with url.courseAdvanceSettings', () => {
|
||||
apiMethods.fetchAdvancedSettings({ learningContextId, studioEndpointUrl });
|
||||
expect(get).toHaveBeenCalledWith(urls.courseAdvanceSettings({ studioEndpointUrl, learningContextId }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeContent', () => {
|
||||
test('return value for blockType: html', () => {
|
||||
const content = 'Im baby palo santo ugh celiac fashion axe. La croix lo-fi venmo whatever. Beard man braid migas single-origin coffee forage ramps.';
|
||||
|
||||
@@ -66,3 +66,7 @@ export const courseAdvanceSettings = ({ studioEndpointUrl, learningContextId })
|
||||
export const videoFeatures = ({ studioEndpointUrl, learningContextId }) => (
|
||||
`${studioEndpointUrl}/video_features/${learningContextId}`
|
||||
);
|
||||
|
||||
export const courseVideos = ({ studioEndpointUrl, learningContextId }) => (
|
||||
`${studioEndpointUrl}/videos/${learningContextId}`
|
||||
);
|
||||
|
||||
@@ -13,7 +13,9 @@ import {
|
||||
courseDetailsUrl,
|
||||
checkTranscriptsForImport,
|
||||
replaceTranscript,
|
||||
courseAdvanceSettings,
|
||||
videoFeatures,
|
||||
courseVideos,
|
||||
} from './urls';
|
||||
|
||||
describe('cms url methods', () => {
|
||||
@@ -125,10 +127,22 @@ describe('cms url methods', () => {
|
||||
.toEqual(`${studioEndpointUrl}/transcripts/replace?data=${parameters}`);
|
||||
});
|
||||
});
|
||||
describe('courseAdvanceSettings', () => {
|
||||
it('returns url with studioEndpointUrl and learningContextId', () => {
|
||||
expect(courseAdvanceSettings({ studioEndpointUrl, learningContextId }))
|
||||
.toEqual(`${studioEndpointUrl}/api/contentstore/v0/advanced_settings/${learningContextId}`);
|
||||
});
|
||||
});
|
||||
describe('videoFeatures', () => {
|
||||
it('returns url with studioEndpointUrl and learningContextId', () => {
|
||||
expect(videoFeatures({ studioEndpointUrl, learningContextId }))
|
||||
.toEqual(`${studioEndpointUrl}/video_features/${learningContextId}`);
|
||||
});
|
||||
});
|
||||
describe('courseVideos', () => {
|
||||
it('returns url with studioEndpointUrl and learningContextId', () => {
|
||||
expect(courseVideos({ studioEndpointUrl, learningContextId }))
|
||||
.toEqual(`${studioEndpointUrl}/videos/${learningContextId}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,12 +10,20 @@ exports[`BaseModal ImageUploadModal template component snapshot 1`] = `
|
||||
size="lg"
|
||||
variant="default"
|
||||
>
|
||||
<ModalDialog.Header>
|
||||
<ModalDialog.Header
|
||||
style={
|
||||
Object {
|
||||
"zIndex": 10000,
|
||||
}
|
||||
}
|
||||
>
|
||||
<ModalDialog.Title>
|
||||
props.title node
|
||||
</ModalDialog.Title>
|
||||
</ModalDialog.Header>
|
||||
<ModalDialog.Body>
|
||||
<ModalDialog.Body
|
||||
style={null}
|
||||
>
|
||||
props.children node
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer>
|
||||
|
||||
@@ -14,9 +14,12 @@ export const BaseModal = ({
|
||||
close,
|
||||
title,
|
||||
children,
|
||||
headerComponent,
|
||||
confirmAction,
|
||||
footerAction,
|
||||
size,
|
||||
isFullscreenScroll,
|
||||
bodyStyle,
|
||||
}) => (
|
||||
<ModalDialog
|
||||
isOpen={isOpen}
|
||||
@@ -25,14 +28,15 @@ export const BaseModal = ({
|
||||
variant="default"
|
||||
hasCloseButton
|
||||
isFullscreenOnMobile
|
||||
isFullscreenScroll
|
||||
isFullscreenScroll={isFullscreenScroll}
|
||||
>
|
||||
<ModalDialog.Header>
|
||||
<ModalDialog.Header style={{ zIndex: 10000 }}>
|
||||
<ModalDialog.Title>
|
||||
{title}
|
||||
</ModalDialog.Title>
|
||||
{headerComponent}
|
||||
</ModalDialog.Header>
|
||||
<ModalDialog.Body>
|
||||
<ModalDialog.Body style={bodyStyle}>
|
||||
{children}
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer>
|
||||
@@ -50,7 +54,10 @@ export const BaseModal = ({
|
||||
|
||||
BaseModal.defaultProps = {
|
||||
footerAction: null,
|
||||
headerComponent: null,
|
||||
size: 'lg',
|
||||
isFullscreenScroll: true,
|
||||
bodyStyle: null,
|
||||
};
|
||||
|
||||
BaseModal.propTypes = {
|
||||
@@ -60,7 +67,10 @@ BaseModal.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
confirmAction: PropTypes.node.isRequired,
|
||||
footerAction: PropTypes.node,
|
||||
headerComponent: PropTypes.node,
|
||||
size: PropTypes.string,
|
||||
isFullscreenScroll: PropTypes.bool,
|
||||
bodyStyle: PropTypes.shape({}),
|
||||
};
|
||||
|
||||
export default BaseModal;
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Alert } from '@edx/paragon';
|
||||
import { Outline } from '@edx/paragon/icons';
|
||||
import { Error } from '@edx/paragon/icons';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import messages from './messages';
|
||||
@@ -42,7 +42,7 @@ export const ErrorAlert = ({
|
||||
return (
|
||||
<Alert
|
||||
variant="danger"
|
||||
icon={Outline}
|
||||
icon={Error}
|
||||
dismissible
|
||||
onClose={dismissAlert}
|
||||
>
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import ErrorAlert from './ErrorAlert';
|
||||
import { selectors } from '../../data/redux';
|
||||
import { RequestKeys } from '../../data/constants/requests';
|
||||
|
||||
export const FetchErrorAlert = ({
|
||||
message,
|
||||
// redux
|
||||
isFetchError,
|
||||
// inject
|
||||
}) => (
|
||||
<ErrorAlert
|
||||
isError={isFetchError}
|
||||
@@ -28,12 +23,8 @@ FetchErrorAlert.propTypes = {
|
||||
defaultMessage: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
}).isRequired,
|
||||
// redux
|
||||
isFetchError: PropTypes.bool.isRequired,
|
||||
|
||||
};
|
||||
export const mapStateToProps = (state) => ({
|
||||
isFetchError: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchAssets }),
|
||||
});
|
||||
export const mapDispatchToProps = {};
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(FetchErrorAlert);
|
||||
|
||||
export default FetchErrorAlert;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { FetchErrorAlert, mapStateToProps } from './FetchErrorAlert';
|
||||
import { selectors } from '../../data/redux';
|
||||
import { RequestKeys } from '../../data/constants/requests';
|
||||
import { FetchErrorAlert } from './FetchErrorAlert';
|
||||
|
||||
jest.mock('../../data/redux', () => ({
|
||||
selectors: {
|
||||
@@ -18,12 +16,4 @@ describe('FetchErrorAlert', () => {
|
||||
expect(shallow(<FetchErrorAlert isFetchError />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { A: 'pple', B: 'anana', C: 'ucumber' };
|
||||
test('isFetchError from requests.isFinished', () => {
|
||||
expect(
|
||||
mapStateToProps(testState).isFetchError,
|
||||
).toEqual(selectors.requests.isFailed(testState, { requestKey: RequestKeys.fetchAssets }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import ErrorAlert from './ErrorAlert';
|
||||
import { selectors } from '../../data/redux';
|
||||
import { RequestKeys } from '../../data/constants/requests';
|
||||
|
||||
export const UploadErrorAlert = ({
|
||||
message,
|
||||
// redux
|
||||
isUploadError,
|
||||
// inject
|
||||
}) => (
|
||||
<ErrorAlert
|
||||
isError={isUploadError}
|
||||
@@ -28,11 +23,7 @@ UploadErrorAlert.propTypes = {
|
||||
defaultMessage: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
}).isRequired,
|
||||
// redux
|
||||
isUploadError: PropTypes.bool.isRequired,
|
||||
};
|
||||
export const mapStateToProps = (state) => ({
|
||||
isUploadError: selectors.requests.isFailed(state, { requestKey: RequestKeys.uploadAsset }),
|
||||
});
|
||||
export const mapDispatchToProps = {};
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(UploadErrorAlert);
|
||||
|
||||
export default UploadErrorAlert;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { UploadErrorAlert, mapStateToProps } from './UploadErrorAlert';
|
||||
import { selectors } from '../../data/redux';
|
||||
import { RequestKeys } from '../../data/constants/requests';
|
||||
import { UploadErrorAlert } from './UploadErrorAlert';
|
||||
|
||||
jest.mock('../../data/redux', () => ({
|
||||
selectors: {
|
||||
@@ -18,12 +16,4 @@ describe('UploadErrorAlert', () => {
|
||||
expect(shallow(<UploadErrorAlert isUploadError />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { A: 'pple', B: 'anana', C: 'ucumber' };
|
||||
test('isUploadError from requests.isFinished', () => {
|
||||
expect(
|
||||
mapStateToProps(testState).isUploadError,
|
||||
).toEqual(selectors.requests.isFailed(testState, { requestKey: RequestKeys.uploadAsset }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
exports[`ImageSettingsModal render snapshot 1`] = `
|
||||
<BaseModal
|
||||
bodyStyle={null}
|
||||
close={[MockFunction props.close]}
|
||||
confirmAction={
|
||||
<Button
|
||||
@@ -35,6 +36,8 @@ exports[`ImageSettingsModal render snapshot 1`] = `
|
||||
</Button>
|
||||
}
|
||||
footerAction={null}
|
||||
headerComponent={null}
|
||||
isFullscreenScroll={true}
|
||||
isOpen={false}
|
||||
size="lg"
|
||||
title="Image Settings"
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Image, SelectableBox } from '@edx/paragon';
|
||||
import { FormattedMessage, FormattedDate, FormattedTime } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
export const GalleryCard = ({
|
||||
img,
|
||||
}) => (
|
||||
<SelectableBox className="card bg-white" key={img.externalUrl} type="radio" value={img.id}>
|
||||
<div className="card-div d-flex flex-row flex-nowrap">
|
||||
<Image
|
||||
style={{ width: '100px', height: '100px' }}
|
||||
src={img.externalUrl}
|
||||
/>
|
||||
<div className="img-text p-3">
|
||||
<h3>{img.displayName}</h3>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
{...messages.addedDate}
|
||||
values={{
|
||||
date: <FormattedDate value={img.dateAdded} />,
|
||||
time: <FormattedTime value={img.dateAdded} />,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</SelectableBox>
|
||||
);
|
||||
|
||||
GalleryCard.propTypes = {
|
||||
img: PropTypes.shape({
|
||||
contentType: PropTypes.string,
|
||||
displayName: PropTypes.string,
|
||||
externalUrl: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
dateAdded: PropTypes.number,
|
||||
locked: PropTypes.bool,
|
||||
portableUrl: PropTypes.string,
|
||||
thumbnail: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default GalleryCard;
|
||||
@@ -1,71 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
ActionRow, Dropdown, Form, Icon, IconButton,
|
||||
} from '@edx/paragon';
|
||||
import { Close, Search } from '@edx/paragon/icons';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { sortKeys, sortMessages } from './utils';
|
||||
import messages from './messages';
|
||||
|
||||
export const SearchSort = ({
|
||||
searchString,
|
||||
onSearchChange,
|
||||
clearSearchString,
|
||||
sortBy,
|
||||
onSortClick,
|
||||
// injected
|
||||
intl,
|
||||
}) => (
|
||||
<ActionRow>
|
||||
<Form.Group style={{ margin: 0 }}>
|
||||
<Form.Control
|
||||
autoFocus
|
||||
onChange={onSearchChange}
|
||||
placeholder={intl.formatMessage(messages.searchPlaceholder)}
|
||||
trailingElement={
|
||||
searchString
|
||||
? (
|
||||
<IconButton
|
||||
iconAs={Icon}
|
||||
invertColors
|
||||
isActive
|
||||
onClick={clearSearchString}
|
||||
size="sm"
|
||||
src={Close}
|
||||
/>
|
||||
)
|
||||
: <Icon src={Search} />
|
||||
}
|
||||
value={searchString}
|
||||
/>
|
||||
</Form.Group>
|
||||
<ActionRow.Spacer />
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle id="img-sort-button" variant="tertiary">
|
||||
<FormattedMessage {...sortMessages[sortBy]} />
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
{Object.keys(sortKeys).map(key => (
|
||||
<Dropdown.Item key={key} onClick={onSortClick(key)}>
|
||||
<FormattedMessage {...sortMessages[key]} />
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</ActionRow>
|
||||
);
|
||||
|
||||
SearchSort.propTypes = {
|
||||
searchString: PropTypes.string.isRequired,
|
||||
onSearchChange: PropTypes.func.isRequired,
|
||||
clearSearchString: PropTypes.func.isRequired,
|
||||
sortBy: PropTypes.string.isRequired,
|
||||
onSortClick: PropTypes.func.isRequired,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(SearchSort);
|
||||
@@ -1,43 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { Dropdown } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { formatMessage } from '../../../../testUtils';
|
||||
|
||||
import { sortKeys, sortMessages } from './utils';
|
||||
import { SearchSort } from './SearchSort';
|
||||
|
||||
describe('SearchSort component', () => {
|
||||
const props = {
|
||||
searchString: 'props.searchString',
|
||||
onSearchChange: jest.fn().mockName('props.onSearchChange'),
|
||||
clearSearchString: jest.fn().mockName('props.clearSearchString'),
|
||||
sortBy: sortKeys.dateOldest,
|
||||
onSortClick: jest.fn().mockName('props.onSortClick'),
|
||||
intl: { formatMessage },
|
||||
};
|
||||
describe('snapshots', () => {
|
||||
test('with search string (close button)', () => {
|
||||
expect(shallow(<SearchSort {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
test('without search string (search icon)', () => {
|
||||
expect(shallow(<SearchSort {...props} searchString="" />)).toMatchSnapshot();
|
||||
});
|
||||
test('adds a sort option for each sortKey', () => {
|
||||
const el = shallow(<SearchSort {...props} />);
|
||||
expect(el.find(Dropdown).containsMatchingElement(
|
||||
<FormattedMessage {...sortMessages.dateNewest} />,
|
||||
)).toEqual(true);
|
||||
expect(el.find(Dropdown).containsMatchingElement(
|
||||
<FormattedMessage {...sortMessages.dateOldest} />,
|
||||
)).toEqual(true);
|
||||
expect(el.find(Dropdown).containsMatchingElement(
|
||||
<FormattedMessage {...sortMessages.nameAscending} />,
|
||||
)).toEqual(true);
|
||||
expect(el.find(Dropdown).containsMatchingElement(
|
||||
<FormattedMessage {...sortMessages.nameDescending} />,
|
||||
)).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,152 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SearchSort component snapshots with search string (close button) 1`] = `
|
||||
<ActionRow>
|
||||
<Form.Group
|
||||
style={
|
||||
Object {
|
||||
"margin": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Form.Control
|
||||
autoFocus={true}
|
||||
onChange={[MockFunction props.onSearchChange]}
|
||||
placeholder="Search"
|
||||
trailingElement={
|
||||
<IconButton
|
||||
iconAs="Icon"
|
||||
invertColors={true}
|
||||
isActive={true}
|
||||
onClick={[MockFunction props.clearSearchString]}
|
||||
size="sm"
|
||||
src={[MockFunction icons.Close]}
|
||||
/>
|
||||
}
|
||||
value="props.searchString"
|
||||
/>
|
||||
</Form.Group>
|
||||
<ActionRow.Spacer />
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
id="img-sort-button"
|
||||
variant="tertiary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By date added (oldest)"
|
||||
description="Dropdown label for sorting by date (oldest)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.dateoldest.label"
|
||||
/>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item
|
||||
key="dateNewest"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By date added (newest)"
|
||||
description="Dropdown label for sorting by date (newest)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.datenewest.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="dateOldest"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By date added (oldest)"
|
||||
description="Dropdown label for sorting by date (oldest)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.dateoldest.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="nameAscending"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By name (ascending)"
|
||||
description="Dropdown label for sorting by name (ascending)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.nameascending.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="nameDescending"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By name (descending)"
|
||||
description="Dropdown label for sorting by name (descending)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.namedescending.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</ActionRow>
|
||||
`;
|
||||
|
||||
exports[`SearchSort component snapshots without search string (search icon) 1`] = `
|
||||
<ActionRow>
|
||||
<Form.Group
|
||||
style={
|
||||
Object {
|
||||
"margin": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Form.Control
|
||||
autoFocus={true}
|
||||
onChange={[MockFunction props.onSearchChange]}
|
||||
placeholder="Search"
|
||||
trailingElement={<Icon />}
|
||||
value=""
|
||||
/>
|
||||
</Form.Group>
|
||||
<ActionRow.Spacer />
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
id="img-sort-button"
|
||||
variant="tertiary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By date added (oldest)"
|
||||
description="Dropdown label for sorting by date (oldest)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.dateoldest.label"
|
||||
/>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item
|
||||
key="dateNewest"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By date added (newest)"
|
||||
description="Dropdown label for sorting by date (newest)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.datenewest.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="dateOldest"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By date added (oldest)"
|
||||
description="Dropdown label for sorting by date (oldest)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.dateoldest.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="nameAscending"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By name (ascending)"
|
||||
description="Dropdown label for sorting by name (ascending)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.nameascending.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="nameDescending"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By name (descending)"
|
||||
description="Dropdown label for sorting by name (descending)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.namedescending.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</ActionRow>
|
||||
`;
|
||||
@@ -1,189 +1,94 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SelectImageModal component snapshot 1`] = `
|
||||
<BaseModal
|
||||
close={[MockFunction props.close]}
|
||||
confirmAction={
|
||||
<Button
|
||||
select="btnProps"
|
||||
variant="primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Next"
|
||||
description="Label for Next button"
|
||||
id="authoring.texteditor.selectimagemodal.next.label"
|
||||
/>
|
||||
</Button>
|
||||
<SelectionModal
|
||||
acceptedFiles={
|
||||
Object {
|
||||
"gif": ".gif",
|
||||
"ico": ".ico",
|
||||
"jpeg": ".jpeg",
|
||||
"jpg": ".jpg",
|
||||
"png": ".png",
|
||||
"tif": ".tif",
|
||||
"tiff": ".tiff",
|
||||
}
|
||||
}
|
||||
footerAction={
|
||||
<Button
|
||||
onClick="imgHooks.fileInput.click"
|
||||
variant="link"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Upload a new image (10 MB max)"
|
||||
description="Label for upload button"
|
||||
id="authoring.texteditor.selectimagemodal.upload.label"
|
||||
/>
|
||||
</Button>
|
||||
close={[MockFunction props.close]}
|
||||
fileInput={
|
||||
Object {
|
||||
"addFile": "imgHooks.fileInput.addFile",
|
||||
"click": "imgHooks.fileInput.click",
|
||||
"ref": "imgHooks.fileInput.ref",
|
||||
}
|
||||
}
|
||||
galleryError={
|
||||
Object {
|
||||
"dismiss": [MockFunction],
|
||||
"message": Object {
|
||||
"defaultMessage": "Gallery error",
|
||||
"description": "Gallery error",
|
||||
"id": "Gallery error id",
|
||||
},
|
||||
"set": [MockFunction],
|
||||
"show": "ShoWERror gAlLery",
|
||||
}
|
||||
}
|
||||
galleryProps={
|
||||
Object {
|
||||
"gallery": "props",
|
||||
}
|
||||
}
|
||||
inputError={
|
||||
Object {
|
||||
"dismiss": [MockFunction],
|
||||
"message": Object {
|
||||
"defaultMessage": "Input error",
|
||||
"description": "Input error",
|
||||
"id": "Input error id",
|
||||
},
|
||||
"set": [MockFunction],
|
||||
"show": "ShoWERror inPUT",
|
||||
}
|
||||
}
|
||||
isOpen={true}
|
||||
title="Add an image"
|
||||
>
|
||||
<FetchErrorAlert
|
||||
message={
|
||||
Object {
|
||||
modalMessages={
|
||||
Object {
|
||||
"confirmMsg": Object {
|
||||
"defaultMessage": "Next",
|
||||
"description": "Label for Next button",
|
||||
"id": "authoring.texteditor.selectimagemodal.next.label",
|
||||
},
|
||||
"fetchError": Object {
|
||||
"defaultMessage": "Failed to obtain course images. Please try again.",
|
||||
"description": "Message presented to user when images are not found",
|
||||
"id": "authoring.texteditor.selectimagemodal.error.fetchImagesError",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<UploadErrorAlert
|
||||
message={
|
||||
Object {
|
||||
},
|
||||
"titleMsg": Object {
|
||||
"defaultMessage": "Add an image",
|
||||
"description": "Title for the select image modal",
|
||||
"id": "authoring.texteditor.selectimagemodal.title.label",
|
||||
},
|
||||
"uploadButtonMsg": Object {
|
||||
"defaultMessage": "Upload a new image (10 MB max)",
|
||||
"description": "Label for upload button",
|
||||
"id": "authoring.texteditor.selectimagemodal.upload.label",
|
||||
},
|
||||
"uploadError": Object {
|
||||
"defaultMessage": "Failed to upload image. Please try again.",
|
||||
"description": "Message presented to user when image fails to upload",
|
||||
"id": "authoring.texteditor.selectimagemodal.error.uploadImageError",
|
||||
}
|
||||
},
|
||||
}
|
||||
/>
|
||||
<ErrorAlert
|
||||
dismissError={[MockFunction]}
|
||||
hideHeading={true}
|
||||
isError="ShoWERror inPUT"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Images must be 10 MB or less. Please resize image and try again."
|
||||
description=" Message presented to user when file size of image is larger than 10 MB"
|
||||
id="authoring.texteditor.selectimagemodal.error.fileSizeError"
|
||||
/>
|
||||
</ErrorAlert>
|
||||
<ErrorAlert
|
||||
dismissError={[MockFunction]}
|
||||
hideHeading={true}
|
||||
isError="ShoWERror gAlLery"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Select an image to continue."
|
||||
description="Message presented to user when clicking Next without selecting an image"
|
||||
id="authoring.texteditor.selectimagemodal.error.selectImageError"
|
||||
/>
|
||||
</ErrorAlert>
|
||||
<Stack
|
||||
gap={3}
|
||||
>
|
||||
<SearchSort
|
||||
search="sortProps"
|
||||
/>
|
||||
<Gallery
|
||||
gallery="props"
|
||||
/>
|
||||
<FileInput
|
||||
acceptedFiles=".gif,.jpg,.jpeg,.png,.tif,.tiff,.ico"
|
||||
fileInput={
|
||||
Object {
|
||||
"addFile": "imgHooks.fileInput.addFile",
|
||||
"click": "imgHooks.fileInput.click",
|
||||
"ref": "imgHooks.fileInput.ref",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
</BaseModal>
|
||||
`;
|
||||
|
||||
exports[`SelectImageModal component snapshot: uploaded image not loaded, show spinner 1`] = `
|
||||
<BaseModal
|
||||
close={[MockFunction props.close]}
|
||||
confirmAction={
|
||||
<Button
|
||||
select="btnProps"
|
||||
variant="primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Next"
|
||||
description="Label for Next button"
|
||||
id="authoring.texteditor.selectimagemodal.next.label"
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
footerAction={
|
||||
<Button
|
||||
onClick="imgHooks.fileInput.click"
|
||||
variant="link"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Upload a new image (10 MB max)"
|
||||
description="Label for upload button"
|
||||
id="authoring.texteditor.selectimagemodal.upload.label"
|
||||
/>
|
||||
</Button>
|
||||
searchSortProps={
|
||||
Object {
|
||||
"search": "sortProps",
|
||||
}
|
||||
}
|
||||
isOpen={true}
|
||||
title="Add an image"
|
||||
>
|
||||
<FetchErrorAlert
|
||||
message={
|
||||
Object {
|
||||
"defaultMessage": "Failed to obtain course images. Please try again.",
|
||||
"description": "Message presented to user when images are not found",
|
||||
"id": "authoring.texteditor.selectimagemodal.error.fetchImagesError",
|
||||
}
|
||||
selectBtnProps={
|
||||
Object {
|
||||
"select": "btnProps",
|
||||
}
|
||||
/>
|
||||
<UploadErrorAlert
|
||||
message={
|
||||
Object {
|
||||
"defaultMessage": "Failed to upload image. Please try again.",
|
||||
"description": "Message presented to user when image fails to upload",
|
||||
"id": "authoring.texteditor.selectimagemodal.error.uploadImageError",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<ErrorAlert
|
||||
dismissError={[MockFunction]}
|
||||
hideHeading={true}
|
||||
isError="ShoWERror inPUT"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Images must be 10 MB or less. Please resize image and try again."
|
||||
description=" Message presented to user when file size of image is larger than 10 MB"
|
||||
id="authoring.texteditor.selectimagemodal.error.fileSizeError"
|
||||
/>
|
||||
</ErrorAlert>
|
||||
<ErrorAlert
|
||||
dismissError={[MockFunction]}
|
||||
hideHeading={true}
|
||||
isError="ShoWERror gAlLery"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Select an image to continue."
|
||||
description="Message presented to user when clicking Next without selecting an image"
|
||||
id="authoring.texteditor.selectimagemodal.error.selectImageError"
|
||||
/>
|
||||
</ErrorAlert>
|
||||
<Stack
|
||||
gap={3}
|
||||
>
|
||||
<SearchSort
|
||||
search="sortProps"
|
||||
/>
|
||||
<Spinner
|
||||
animation="border"
|
||||
className="mie-3"
|
||||
screenReaderText="loading..."
|
||||
/>
|
||||
<FileInput
|
||||
acceptedFiles=".gif,.jpg,.jpeg,.png,.tif,.tiff,.ico"
|
||||
fileInput={
|
||||
Object {
|
||||
"addFile": "imgHooks.fileInput.addFile",
|
||||
"click": "imgHooks.fileInput.click",
|
||||
"ref": "imgHooks.fileInput.ref",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
</BaseModal>
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -3,7 +3,8 @@ import { useDispatch } from 'react-redux';
|
||||
|
||||
import { thunkActions } from '../../../data/redux';
|
||||
import * as module from './hooks';
|
||||
import { sortFunctions, sortKeys } from './utils';
|
||||
import { sortFunctions, sortKeys, sortMessages } from './utils';
|
||||
import messages from './messages';
|
||||
|
||||
export const state = {
|
||||
highlighted: (val) => React.useState(val),
|
||||
@@ -22,6 +23,8 @@ export const searchAndSortHooks = () => {
|
||||
clearSearchString: () => setSearchString(''),
|
||||
sortBy,
|
||||
onSortClick: (key) => () => setSortBy(key),
|
||||
sortKeys,
|
||||
sortMessages,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -49,11 +52,13 @@ export const imgListHooks = ({ searchSortProps, setSelection, images }) => {
|
||||
show: showSelectImageError,
|
||||
set: () => setShowSelectImageError(true),
|
||||
dismiss: () => setShowSelectImageError(false),
|
||||
message: messages.selectImageError,
|
||||
},
|
||||
inputError: {
|
||||
show: showSizeError,
|
||||
set: () => setShowSizeError(true),
|
||||
dismiss: () => setShowSizeError(false),
|
||||
message: messages.fileSizeError,
|
||||
},
|
||||
images,
|
||||
galleryProps: {
|
||||
@@ -62,6 +67,7 @@ export const imgListHooks = ({ searchSortProps, setSelection, images }) => {
|
||||
displayList: list,
|
||||
highlighted,
|
||||
onHighlightChange: (e) => setHighlighted(e.target.value),
|
||||
emptyGalleryLabel: messages.emptyGalleryLabel,
|
||||
},
|
||||
// highlight by id
|
||||
selectBtnProps: {
|
||||
|
||||
@@ -1,27 +1,11 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Button, Stack, Spinner } from '@edx/paragon';
|
||||
import { Add } from '@edx/paragon/icons';
|
||||
import {
|
||||
FormattedMessage,
|
||||
injectIntl,
|
||||
intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { selectors } from '../../../data/redux';
|
||||
import { RequestKeys } from '../../../data/constants/requests';
|
||||
import { acceptedImgKeys } from './utils';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import hooks from './hooks';
|
||||
import { acceptedImgKeys } from './utils';
|
||||
import SelectionModal from '../../SelectionModal';
|
||||
import messages from './messages';
|
||||
import BaseModal from '../../BaseModal';
|
||||
import SearchSort from './SearchSort';
|
||||
import Gallery from './Gallery';
|
||||
import FileInput from '../../FileInput';
|
||||
import FetchErrorAlert from '../../ErrorAlerts/FetchErrorAlert';
|
||||
import UploadErrorAlert from '../../ErrorAlerts/UploadErrorAlert';
|
||||
import ErrorAlert from '../../ErrorAlerts/ErrorAlert';
|
||||
import { RequestKeys } from '../../../data/constants/requests';
|
||||
import { selectors } from '../../../data/redux';
|
||||
|
||||
export const SelectImageModal = ({
|
||||
isOpen,
|
||||
@@ -29,10 +13,10 @@ export const SelectImageModal = ({
|
||||
setSelection,
|
||||
clearSelection,
|
||||
images,
|
||||
// injected
|
||||
intl,
|
||||
// redux
|
||||
inputIsLoading,
|
||||
isLoaded,
|
||||
isFetchError,
|
||||
isUploadError,
|
||||
}) => {
|
||||
const {
|
||||
galleryError,
|
||||
@@ -43,53 +27,32 @@ export const SelectImageModal = ({
|
||||
selectBtnProps,
|
||||
} = hooks.imgHooks({ setSelection, clearSelection, images });
|
||||
|
||||
return (
|
||||
<BaseModal
|
||||
close={close}
|
||||
confirmAction={(
|
||||
<Button {...selectBtnProps} variant="primary">
|
||||
<FormattedMessage {...messages.nextButtonLabel} />
|
||||
</Button>
|
||||
)}
|
||||
isOpen={isOpen}
|
||||
footerAction={(
|
||||
<Button iconBefore={Add} onClick={fileInput.click} variant="link">
|
||||
<FormattedMessage {...messages.uploadButtonLabel} />
|
||||
</Button>
|
||||
)}
|
||||
title={intl.formatMessage(messages.titleLabel)}
|
||||
>
|
||||
{/* Error Alerts */}
|
||||
<FetchErrorAlert message={messages.fetchImagesError} />
|
||||
<UploadErrorAlert message={messages.uploadImageError} />
|
||||
<ErrorAlert
|
||||
dismissError={inputError.dismiss}
|
||||
hideHeading
|
||||
isError={inputError.show}
|
||||
>
|
||||
<FormattedMessage {...messages.fileSizeError} />
|
||||
</ErrorAlert>
|
||||
const modalMessages = {
|
||||
confirmMsg: messages.nextButtonLabel,
|
||||
titleMsg: messages.titleLabel,
|
||||
uploadButtonMsg: messages.uploadButtonLabel,
|
||||
fetchError: messages.fetchImagesError,
|
||||
uploadError: messages.uploadImageError,
|
||||
};
|
||||
|
||||
{/* User Feedback Alerts */}
|
||||
<ErrorAlert
|
||||
dismissError={galleryError.dismiss}
|
||||
hideHeading
|
||||
isError={galleryError.show}
|
||||
>
|
||||
<FormattedMessage {...messages.selectImageError} />
|
||||
</ErrorAlert>
|
||||
<Stack gap={3}>
|
||||
<SearchSort {...searchSortProps} />
|
||||
{!inputIsLoading ? <Gallery {...galleryProps} /> : (
|
||||
<Spinner
|
||||
animation="border"
|
||||
className="mie-3"
|
||||
screenReaderText={intl.formatMessage(messages.loading)}
|
||||
/>
|
||||
)}
|
||||
<FileInput fileInput={fileInput} acceptedFiles={Object.values(acceptedImgKeys).join()} />
|
||||
</Stack>
|
||||
</BaseModal>
|
||||
return (
|
||||
<SelectionModal
|
||||
{...{
|
||||
isOpen,
|
||||
close,
|
||||
galleryError,
|
||||
inputError,
|
||||
fileInput,
|
||||
galleryProps,
|
||||
searchSortProps,
|
||||
selectBtnProps,
|
||||
acceptedFiles: acceptedImgKeys,
|
||||
modalMessages,
|
||||
isLoaded,
|
||||
isFetchError,
|
||||
isUploadError,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -99,16 +62,18 @@ SelectImageModal.propTypes = {
|
||||
setSelection: PropTypes.func.isRequired,
|
||||
clearSelection: PropTypes.func.isRequired,
|
||||
images: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
// redux
|
||||
inputIsLoading: PropTypes.bool.isRequired,
|
||||
isLoaded: PropTypes.bool.isRequired,
|
||||
isFetchError: PropTypes.bool.isRequired,
|
||||
isUploadError: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
inputIsLoading: selectors.requests.isPending(state, { requestKey: RequestKeys.uploadAsset }),
|
||||
isLoaded: selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchAssets }),
|
||||
isFetchError: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchAssets }),
|
||||
isUploadError: selectors.requests.isFailed(state, { requestKey: RequestKeys.uploadAsset }),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {};
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(SelectImageModal));
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SelectImageModal);
|
||||
|
||||
@@ -2,22 +2,11 @@ import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { formatMessage } from '../../../../testUtils';
|
||||
import { RequestKeys } from '../../../data/constants/requests';
|
||||
import { selectors } from '../../../data/redux';
|
||||
import BaseModal from '../../BaseModal';
|
||||
import FileInput from '../../FileInput';
|
||||
import Gallery from './Gallery';
|
||||
import SearchSort from './SearchSort';
|
||||
import SelectionModal from '../../SelectionModal';
|
||||
import hooks from './hooks';
|
||||
import { SelectImageModal, mapStateToProps, mapDispatchToProps } from '.';
|
||||
import { SelectImageModal } from '.';
|
||||
|
||||
jest.mock('../../BaseModal', () => 'BaseModal');
|
||||
jest.mock('../../FileInput', () => 'FileInput');
|
||||
jest.mock('./Gallery', () => 'Gallery');
|
||||
jest.mock('./SearchSort', () => 'SearchSort');
|
||||
jest.mock('../../ErrorAlerts/FetchErrorAlert', () => 'FetchErrorAlert');
|
||||
jest.mock('../../ErrorAlerts/UploadErrorAlert', () => 'UploadErrorAlert');
|
||||
jest.mock('../..//ErrorAlerts/ErrorAlert', () => 'ErrorAlert');
|
||||
jest.mock('../../SelectionModal', () => 'SelectionModal');
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
imgHooks: jest.fn(() => ({
|
||||
@@ -25,11 +14,21 @@ jest.mock('./hooks', () => ({
|
||||
show: 'ShoWERror gAlLery',
|
||||
set: jest.fn(),
|
||||
dismiss: jest.fn(),
|
||||
message: {
|
||||
id: 'Gallery error id',
|
||||
defaultMessage: 'Gallery error',
|
||||
description: 'Gallery error',
|
||||
},
|
||||
},
|
||||
inputError: {
|
||||
show: 'ShoWERror inPUT',
|
||||
set: jest.fn(),
|
||||
dismiss: jest.fn(),
|
||||
message: {
|
||||
id: 'Input error id',
|
||||
defaultMessage: 'Input error',
|
||||
description: 'Input error',
|
||||
},
|
||||
},
|
||||
fileInput: {
|
||||
addFile: 'imgHooks.fileInput.addFile',
|
||||
@@ -58,7 +57,6 @@ describe('SelectImageModal', () => {
|
||||
setSelection: jest.fn().mockName('props.setSelection'),
|
||||
clearSelection: jest.fn().mockName('props.clearSelection'),
|
||||
intl: { formatMessage },
|
||||
inputIsLoading: false,
|
||||
};
|
||||
let el;
|
||||
const imgHooks = hooks.imgHooks();
|
||||
@@ -68,42 +66,24 @@ describe('SelectImageModal', () => {
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: uploaded image not loaded, show spinner', () => {
|
||||
props.inputIsLoading = true;
|
||||
expect(shallow(<SelectImageModal {...props} />)).toMatchSnapshot();
|
||||
props.inputIsLoading = false;
|
||||
});
|
||||
it('provides confirm action, forwarding selectBtnProps from imgHooks', () => {
|
||||
expect(el.find(BaseModal).props().confirmAction.props).toEqual(
|
||||
expect.objectContaining({ ...hooks.imgHooks().selectBtnProps, variant: 'primary' }),
|
||||
expect(el.find(SelectionModal).props().selectBtnProps).toEqual(
|
||||
expect.objectContaining({ ...hooks.imgHooks().selectBtnProps }),
|
||||
);
|
||||
});
|
||||
it('provides file upload button linked to fileInput.click', () => {
|
||||
expect(el.find(BaseModal).props().footerAction.props.onClick).toEqual(
|
||||
expect(el.find(SelectionModal).props().fileInput.click).toEqual(
|
||||
imgHooks.fileInput.click,
|
||||
);
|
||||
});
|
||||
it('provides a SearchSort component with searchSortProps from imgHooks', () => {
|
||||
expect(el.find(SearchSort).props()).toEqual(imgHooks.searchSortProps);
|
||||
expect(el.find(SelectionModal).props().searchSortProps).toEqual(imgHooks.searchSortProps);
|
||||
});
|
||||
it('provides a Gallery component with galleryProps from imgHooks', () => {
|
||||
expect(el.find(Gallery).props()).toEqual(imgHooks.galleryProps);
|
||||
expect(el.find(SelectionModal).props().galleryProps).toEqual(imgHooks.galleryProps);
|
||||
});
|
||||
it('provides a FileInput component with fileInput props from imgHooks', () => {
|
||||
expect(el.find(FileInput).props()).toMatchObject({ fileInput: imgHooks.fileInput });
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { some: 'testState' };
|
||||
test('loads inputIsLoading from requests.isPending selector for uploadAsset request', () => {
|
||||
expect(mapStateToProps(testState).inputIsLoading).toEqual(
|
||||
selectors.requests.isPending(testState, { requestKey: RequestKeys.uploadAsset }),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('is empty', () => {
|
||||
expect(mapDispatchToProps).toEqual({});
|
||||
expect(el.find(SelectionModal).props().fileInput).toMatchObject(imgHooks.fileInput);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,11 +17,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Add an image',
|
||||
description: 'Title for the select image modal',
|
||||
},
|
||||
searchPlaceholder: {
|
||||
id: 'authoring.texteditor.selectimagemodal.search.placeholder',
|
||||
defaultMessage: 'Search',
|
||||
description: 'Placeholder text for search bar',
|
||||
},
|
||||
|
||||
// Sort Dropdown
|
||||
sortByDateNewest: {
|
||||
@@ -46,27 +41,12 @@ const messages = defineMessages({
|
||||
},
|
||||
|
||||
// Gallery
|
||||
addedDate: {
|
||||
id: 'authoring.texteditor.selectimagemodal.addedDate.label',
|
||||
defaultMessage: 'Added {date} at {time}',
|
||||
description: 'File date-added string',
|
||||
},
|
||||
loading: {
|
||||
id: 'authoring.texteditor.selectimagemodal.spinner.readertext',
|
||||
defaultMessage: 'loading...',
|
||||
description: 'Gallery loading spinner screen-reader text',
|
||||
},
|
||||
emptyGalleryLabel: {
|
||||
id: 'authoring.texteditor.selectimagemodal.emptyGalleryLabel',
|
||||
defaultMessage:
|
||||
'No images found in your gallery. Please upload an image using the button below.',
|
||||
description: 'Label for when image gallery is empty.',
|
||||
},
|
||||
emptySearchLabel: {
|
||||
id: 'authoring.texteditor.selectimagemodal.emptySearchLabel',
|
||||
defaultMessage: 'No search results.',
|
||||
description: 'Label for when search returns nothing.',
|
||||
},
|
||||
|
||||
// Errors
|
||||
uploadImageError: {
|
||||
|
||||
@@ -1,55 +1,69 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
Scrollable, SelectableBox, Spinner,
|
||||
} from '@edx/paragon';
|
||||
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { selectors } from '../../../data/redux';
|
||||
import { RequestKeys } from '../../../data/constants/requests';
|
||||
import {
|
||||
FormattedMessage,
|
||||
injectIntl,
|
||||
intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './messages';
|
||||
import GalleryCard from './GalleryCard';
|
||||
|
||||
export const Gallery = ({
|
||||
show,
|
||||
galleryIsEmpty,
|
||||
searchIsEmpty,
|
||||
displayList,
|
||||
highlighted,
|
||||
onHighlightChange,
|
||||
emptyGalleryLabel,
|
||||
showIdsOnCards,
|
||||
height,
|
||||
isLoaded,
|
||||
// injected
|
||||
intl,
|
||||
// redux
|
||||
isLoaded,
|
||||
}) => {
|
||||
if (!show) {
|
||||
return null;
|
||||
}
|
||||
if (!isLoaded) {
|
||||
return (
|
||||
<Spinner
|
||||
animation="border"
|
||||
className="mie-3"
|
||||
screenReaderText={intl.formatMessage(messages.loading)}
|
||||
/>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
top: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}}
|
||||
>
|
||||
<Spinner
|
||||
animation="border"
|
||||
className="mie-3"
|
||||
screenReaderText={intl.formatMessage(messages.loading)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (galleryIsEmpty) {
|
||||
return (
|
||||
<div className="gallery p-4 bg-gray-100" style={{ height: '375px' }}>
|
||||
<FormattedMessage {...messages.emptyGalleryLabel} />
|
||||
<div className="gallery p-4 bg-gray-100" style={{ height, margin: '0 -1.5rem' }}>
|
||||
<FormattedMessage {...emptyGalleryLabel} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (searchIsEmpty) {
|
||||
return (
|
||||
<div className="gallery p-4 bg-gray-100" style={{ height: '375px' }}>
|
||||
<div className="gallery p-4 bg-gray-100" style={{ height, margin: '0 -1.5rem' }}>
|
||||
<FormattedMessage {...messages.emptySearchLabel} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Scrollable className="gallery bg-gray-100" style={{ height: '375px' }}>
|
||||
<Scrollable className="gallery bg-gray-100" style={{ height, margin: '0 -1.5rem' }}>
|
||||
<div className="p-4">
|
||||
<SelectableBox.Set
|
||||
columns={1}
|
||||
@@ -58,7 +72,7 @@ export const Gallery = ({
|
||||
type="radio"
|
||||
value={highlighted}
|
||||
>
|
||||
{displayList.map(img => <GalleryCard key={img.id} img={img} />)}
|
||||
{ displayList.map(asset => <GalleryCard key={asset.id} asset={asset} showId={showIdsOnCards} />) }
|
||||
</SelectableBox.Set>
|
||||
</div>
|
||||
</Scrollable>
|
||||
@@ -67,24 +81,23 @@ export const Gallery = ({
|
||||
|
||||
Gallery.defaultProps = {
|
||||
highlighted: '',
|
||||
showIdsOnCards: false,
|
||||
height: '375px',
|
||||
show: true,
|
||||
};
|
||||
Gallery.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
isLoaded: PropTypes.bool.isRequired,
|
||||
galleryIsEmpty: PropTypes.bool.isRequired,
|
||||
searchIsEmpty: PropTypes.bool.isRequired,
|
||||
displayList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
highlighted: PropTypes.string,
|
||||
onHighlightChange: PropTypes.func.isRequired,
|
||||
emptyGalleryLabel: PropTypes.shape({}).isRequired,
|
||||
showIdsOnCards: PropTypes.bool,
|
||||
height: PropTypes.string,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
// redux
|
||||
isLoaded: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const requestKey = RequestKeys.fetchAssets;
|
||||
export const mapStateToProps = (state) => ({
|
||||
isLoaded: selectors.requests.isFinished(state, { requestKey }),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {};
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(Gallery));
|
||||
export default injectIntl(Gallery);
|
||||
@@ -1,12 +1,10 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { formatMessage } from '../../../../testUtils';
|
||||
import { RequestKeys } from '../../../data/constants/requests';
|
||||
import { selectors } from '../../../data/redux';
|
||||
import { Gallery, mapStateToProps, mapDispatchToProps } from './Gallery';
|
||||
import { formatMessage } from '../../../testUtils';
|
||||
import { Gallery } from './Gallery';
|
||||
|
||||
jest.mock('../../../data/redux', () => ({
|
||||
jest.mock('../../data/redux', () => ({
|
||||
selectors: {
|
||||
requests: {
|
||||
isFinished: (state, { requestKey }) => ({ isFinished: { state, requestKey } }),
|
||||
@@ -39,18 +37,9 @@ describe('TextEditor Image Gallery component', () => {
|
||||
test('snapshot: loaded, show gallery', () => {
|
||||
expect(shallow(<Gallery {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { some: 'testState' };
|
||||
test('loads isLoaded from requests.isFinished selector for fetchAssets request', () => {
|
||||
expect(mapStateToProps(testState).isLoaded).toEqual(
|
||||
selectors.requests.isFinished(testState, { requestKey: RequestKeys.fetchAssets }),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('is empty', () => {
|
||||
expect(mapDispatchToProps).toEqual({});
|
||||
test('snapshot: not shot gallery', () => {
|
||||
const wrapper = shallow(<Gallery {...props} show={false} />);
|
||||
expect(wrapper.type()).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
83
src/editors/sharedComponents/SelectionModal/GalleryCard.jsx
Normal file
83
src/editors/sharedComponents/SelectionModal/GalleryCard.jsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
Badge,
|
||||
Image,
|
||||
SelectableBox,
|
||||
} from '@edx/paragon';
|
||||
import { FormattedMessage, FormattedDate, FormattedTime } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './messages';
|
||||
import { formatDuration } from '../../utils';
|
||||
import LanguageNamesWidget from '../../containers/VideoEditor/components/VideoSettingsModal/components/VideoPreviewWidget/LanguageNamesWidget';
|
||||
|
||||
export const GalleryCard = ({
|
||||
asset,
|
||||
}) => (
|
||||
<SelectableBox className="card bg-white" key={asset.externalUrl} type="radio" value={asset.id} style={{ padding: '10px 20px' }}>
|
||||
<div className="card-div d-flex flex-row flex-nowrap">
|
||||
<div style={{
|
||||
position: 'relative',
|
||||
width: '200px',
|
||||
height: '100px',
|
||||
margin: '16px 0 0 0',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
style={{ width: '200px', height: '100px' }}
|
||||
src={asset.externalUrl}
|
||||
/>
|
||||
{ asset.status && asset.statusBadgeVariant && (
|
||||
<Badge variant={asset.statusBadgeVariant} style={{ position: 'absolute', left: '6px', top: '6px' }}>
|
||||
{asset.status}
|
||||
</Badge>
|
||||
)}
|
||||
{ asset.duration >= 0 && (
|
||||
<Badge variant="dark" style={{ position: 'absolute', right: '6px', bottom: '6px' }}>
|
||||
{formatDuration(asset.duration)}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="card-text p-3">
|
||||
<h3>{asset.displayName}</h3>
|
||||
{ asset.transcripts && (
|
||||
<div style={{ margin: '0 0 5px 0' }}>
|
||||
<LanguageNamesWidget
|
||||
transcripts={asset.transcripts}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<p style={{ fontSize: '11px' }}>
|
||||
<FormattedMessage
|
||||
{...messages.addedDate}
|
||||
values={{
|
||||
date: <FormattedDate value={asset.dateAdded} />,
|
||||
time: <FormattedTime value={asset.dateAdded} />,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</SelectableBox>
|
||||
);
|
||||
|
||||
GalleryCard.propTypes = {
|
||||
asset: PropTypes.shape({
|
||||
contentType: PropTypes.string,
|
||||
displayName: PropTypes.string,
|
||||
externalUrl: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
dateAdded: PropTypes.number,
|
||||
locked: PropTypes.bool,
|
||||
portableUrl: PropTypes.string,
|
||||
thumbnail: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
duration: PropTypes.number,
|
||||
status: PropTypes.string,
|
||||
statusBadgeVariant: PropTypes.string,
|
||||
transcripts: PropTypes.array,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default GalleryCard;
|
||||
@@ -12,7 +12,7 @@ describe('GalleryCard component', () => {
|
||||
};
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<GalleryCard img={img} />);
|
||||
el = shallow(<GalleryCard asset={img} />);
|
||||
});
|
||||
test(`snapshot: dateAdded=${img.dateAdded}`, () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
133
src/editors/sharedComponents/SelectionModal/SearchSort.jsx
Normal file
133
src/editors/sharedComponents/SelectionModal/SearchSort.jsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
ActionRow, Dropdown, Form, Icon, IconButton,
|
||||
} from '@edx/paragon';
|
||||
import { Close, Search } from '@edx/paragon/icons';
|
||||
import {
|
||||
FormattedMessage,
|
||||
injectIntl,
|
||||
intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
export const SearchSort = ({
|
||||
searchString,
|
||||
onSearchChange,
|
||||
clearSearchString,
|
||||
sortBy,
|
||||
onSortClick,
|
||||
sortKeys,
|
||||
sortMessages,
|
||||
filterBy,
|
||||
onFilterClick,
|
||||
filterKeys,
|
||||
filterMessages,
|
||||
showSwitch,
|
||||
switchMessage,
|
||||
onSwitchClick,
|
||||
// injected
|
||||
intl,
|
||||
}) => (
|
||||
<ActionRow>
|
||||
<Form.Group style={{ margin: 0 }}>
|
||||
<Form.Control
|
||||
autoFocus
|
||||
onChange={onSearchChange}
|
||||
placeholder={intl.formatMessage(messages.searchPlaceholder)}
|
||||
trailingElement={
|
||||
searchString
|
||||
? (
|
||||
<IconButton
|
||||
iconAs={Icon}
|
||||
invertColors
|
||||
isActive
|
||||
onClick={clearSearchString}
|
||||
size="sm"
|
||||
src={Close}
|
||||
/>
|
||||
)
|
||||
: <Icon src={Search} />
|
||||
}
|
||||
value={searchString}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
{ !showSwitch && <ActionRow.Spacer /> }
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle id="gallery-sort-button" variant="tertiary">
|
||||
<FormattedMessage {...sortMessages[sortBy]} />
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
{Object.keys(sortKeys).map(key => (
|
||||
<Dropdown.Item key={key} onClick={onSortClick(key)}>
|
||||
<FormattedMessage {...sortMessages[key]} />
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
|
||||
{ filterKeys && filterMessages && (
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle id="gallery-filter-button" variant="tertiary">
|
||||
<FormattedMessage {...filterMessages[filterBy]} />
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
{Object.keys(filterKeys).map(key => (
|
||||
<Dropdown.Item key={key} onClick={onFilterClick(key)}>
|
||||
<FormattedMessage {...filterMessages[key]} />
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
)}
|
||||
|
||||
{ showSwitch && (
|
||||
<>
|
||||
<ActionRow.Spacer />
|
||||
<Form.SwitchSet
|
||||
name="switch"
|
||||
onChange={onSwitchClick}
|
||||
isInline
|
||||
>
|
||||
<Form.Switch value="switch-value" floatLabelLeft>
|
||||
<FormattedMessage {...switchMessage} />
|
||||
</Form.Switch>
|
||||
</Form.SwitchSet>
|
||||
</>
|
||||
)}
|
||||
|
||||
</ActionRow>
|
||||
);
|
||||
|
||||
SearchSort.defaultProps = {
|
||||
filterBy: '',
|
||||
onFilterClick: null,
|
||||
filterKeys: null,
|
||||
filterMessages: null,
|
||||
showSwitch: false,
|
||||
onSwitchClick: null,
|
||||
};
|
||||
|
||||
SearchSort.propTypes = {
|
||||
searchString: PropTypes.string.isRequired,
|
||||
onSearchChange: PropTypes.func.isRequired,
|
||||
clearSearchString: PropTypes.func.isRequired,
|
||||
sortBy: PropTypes.string.isRequired,
|
||||
onSortClick: PropTypes.func.isRequired,
|
||||
sortKeys: PropTypes.shape({}).isRequired,
|
||||
sortMessages: PropTypes.shape({}).isRequired,
|
||||
filterBy: PropTypes.string,
|
||||
onFilterClick: PropTypes.func,
|
||||
filterKeys: PropTypes.shape({}),
|
||||
filterMessages: PropTypes.shape({}),
|
||||
showSwitch: PropTypes.bool,
|
||||
switchMessage: PropTypes.shape({}).isRequired,
|
||||
onSwitchClick: PropTypes.func,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(SearchSort);
|
||||
101
src/editors/sharedComponents/SelectionModal/SearchSort.test.jsx
Normal file
101
src/editors/sharedComponents/SelectionModal/SearchSort.test.jsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { Dropdown } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { formatMessage } from '../../../testUtils';
|
||||
|
||||
import { sortKeys, sortMessages } from '../ImageUploadModal/SelectImageModal/utils';
|
||||
import { filterKeys, filterMessages } from '../../containers/VideoGallery/utils';
|
||||
import { SearchSort } from './SearchSort';
|
||||
|
||||
describe('SearchSort component', () => {
|
||||
describe('snapshots without filterKeys', () => {
|
||||
const props = {
|
||||
searchString: 'props.searchString',
|
||||
onSearchChange: jest.fn().mockName('props.onSearchChange'),
|
||||
clearSearchString: jest.fn().mockName('props.clearSearchString'),
|
||||
sortBy: sortKeys.dateOldest,
|
||||
sortKeys,
|
||||
sortMessages,
|
||||
onSortClick: jest.fn().mockName('props.onSortClick'),
|
||||
intl: { formatMessage },
|
||||
};
|
||||
test('with search string (close button)', () => {
|
||||
expect(shallow(<SearchSort {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
test('without search string (search icon)', () => {
|
||||
expect(shallow(<SearchSort {...props} searchString="" />)).toMatchSnapshot();
|
||||
});
|
||||
test('adds a sort option for each sortKey', () => {
|
||||
const el = shallow(<SearchSort {...props} />);
|
||||
expect(el.find(Dropdown).containsMatchingElement(
|
||||
<FormattedMessage {...sortMessages.dateNewest} />,
|
||||
)).toEqual(true);
|
||||
expect(el.find(Dropdown).containsMatchingElement(
|
||||
<FormattedMessage {...sortMessages.dateOldest} />,
|
||||
)).toEqual(true);
|
||||
expect(el.find(Dropdown).containsMatchingElement(
|
||||
<FormattedMessage {...sortMessages.nameAscending} />,
|
||||
)).toEqual(true);
|
||||
expect(el.find(Dropdown).containsMatchingElement(
|
||||
<FormattedMessage {...sortMessages.nameDescending} />,
|
||||
)).toEqual(true);
|
||||
});
|
||||
});
|
||||
describe('snapshots with filterKeys', () => {
|
||||
const props = {
|
||||
searchString: 'props.searchString',
|
||||
onSearchChange: jest.fn().mockName('props.onSearchChange'),
|
||||
clearSearchString: jest.fn().mockName('props.clearSearchString'),
|
||||
sortBy: sortKeys.dateOldest,
|
||||
sortKeys,
|
||||
sortMessages,
|
||||
filterKeys,
|
||||
filterMessages,
|
||||
showSwitch: true,
|
||||
onSortClick: jest.fn().mockName('props.onSortClick'),
|
||||
onFilterClick: jest.fn().mockName('props.onFilterClick'),
|
||||
intl: { formatMessage },
|
||||
};
|
||||
test('with search string (close button)', () => {
|
||||
expect(shallow(<SearchSort {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
test('without search string (search icon)', () => {
|
||||
expect(shallow(<SearchSort {...props} searchString="" />)).toMatchSnapshot();
|
||||
});
|
||||
test('adds a sort option for each sortKey', () => {
|
||||
const el = shallow(<SearchSort {...props} />);
|
||||
expect(el.find(Dropdown).containsMatchingElement(
|
||||
<FormattedMessage {...sortMessages.dateNewest} />,
|
||||
)).toEqual(true);
|
||||
expect(el.find(Dropdown).containsMatchingElement(
|
||||
<FormattedMessage {...sortMessages.dateOldest} />,
|
||||
)).toEqual(true);
|
||||
expect(el.find(Dropdown).containsMatchingElement(
|
||||
<FormattedMessage {...sortMessages.nameAscending} />,
|
||||
)).toEqual(true);
|
||||
expect(el.find(Dropdown).containsMatchingElement(
|
||||
<FormattedMessage {...sortMessages.nameDescending} />,
|
||||
)).toEqual(true);
|
||||
});
|
||||
test('adds a filter option for each filterKet', () => {
|
||||
const el = shallow(<SearchSort {...props} />);
|
||||
expect(el.find(Dropdown).containsMatchingElement(
|
||||
<FormattedMessage {...filterMessages.videoStatus} />,
|
||||
)).toEqual(true);
|
||||
expect(el.find(Dropdown).containsMatchingElement(
|
||||
<FormattedMessage {...filterMessages.uploading} />,
|
||||
)).toEqual(true);
|
||||
expect(el.find(Dropdown).containsMatchingElement(
|
||||
<FormattedMessage {...filterMessages.processing} />,
|
||||
)).toEqual(true);
|
||||
expect(el.find(Dropdown).containsMatchingElement(
|
||||
<FormattedMessage {...filterMessages.ready} />,
|
||||
)).toEqual(true);
|
||||
expect(el.find(Dropdown).containsMatchingElement(
|
||||
<FormattedMessage {...filterMessages.failed} />,
|
||||
)).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -6,14 +6,11 @@ exports[`TextEditor Image Gallery component component snapshot: loaded but no im
|
||||
style={
|
||||
Object {
|
||||
"height": "375px",
|
||||
"margin": "0 -1.5rem",
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="No images found in your gallery. Please upload an image using the button below."
|
||||
description="Label for when image gallery is empty."
|
||||
id="authoring.texteditor.selectimagemodal.emptyGalleryLabel"
|
||||
/>
|
||||
<FormattedMessage />
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -23,13 +20,14 @@ exports[`TextEditor Image Gallery component component snapshot: loaded but searc
|
||||
style={
|
||||
Object {
|
||||
"height": "375px",
|
||||
"margin": "0 -1.5rem",
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="No search results."
|
||||
description="Label for when search returns nothing."
|
||||
id="authoring.texteditor.selectimagemodal.emptySearchLabel"
|
||||
id="authoring.selectionmodal.emptySearchLabel"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
@@ -40,6 +38,7 @@ exports[`TextEditor Image Gallery component component snapshot: loaded, show gal
|
||||
style={
|
||||
Object {
|
||||
"height": "375px",
|
||||
"margin": "0 -1.5rem",
|
||||
}
|
||||
}
|
||||
>
|
||||
@@ -54,28 +53,31 @@ exports[`TextEditor Image Gallery component component snapshot: loaded, show gal
|
||||
value="props.highlighted"
|
||||
>
|
||||
<GalleryCard
|
||||
img={
|
||||
asset={
|
||||
Object {
|
||||
"id": 1,
|
||||
}
|
||||
}
|
||||
key="1"
|
||||
showId={false}
|
||||
/>
|
||||
<GalleryCard
|
||||
img={
|
||||
asset={
|
||||
Object {
|
||||
"id": 2,
|
||||
}
|
||||
}
|
||||
key="2"
|
||||
showId={false}
|
||||
/>
|
||||
<GalleryCard
|
||||
img={
|
||||
asset={
|
||||
Object {
|
||||
"id": 3,
|
||||
}
|
||||
}
|
||||
key="3"
|
||||
showId={false}
|
||||
/>
|
||||
</SelectableBox.Set>
|
||||
</div>
|
||||
@@ -83,9 +85,20 @@ exports[`TextEditor Image Gallery component component snapshot: loaded, show gal
|
||||
`;
|
||||
|
||||
exports[`TextEditor Image Gallery component component snapshot: not loaded, show spinner 1`] = `
|
||||
<Spinner
|
||||
animation="border"
|
||||
className="mie-3"
|
||||
screenReaderText="loading..."
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"left": "50%",
|
||||
"position": "absolute",
|
||||
"top": "50%",
|
||||
"transform": "translate(-50%, -50%)",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Spinner
|
||||
animation="border"
|
||||
className="mie-3"
|
||||
screenReaderText="loading..."
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
@@ -4,31 +4,53 @@ exports[`GalleryCard component snapshot: dateAdded=12345 1`] = `
|
||||
<SelectableBox
|
||||
className="card bg-white"
|
||||
key="props.img.externalUrl"
|
||||
style={
|
||||
Object {
|
||||
"padding": "10px 20px",
|
||||
}
|
||||
}
|
||||
type="radio"
|
||||
>
|
||||
<div
|
||||
className="card-div d-flex flex-row flex-nowrap"
|
||||
>
|
||||
<Image
|
||||
src="props.img.externalUrl"
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"height": "100px",
|
||||
"width": "100px",
|
||||
"margin": "16px 0 0 0",
|
||||
"position": "relative",
|
||||
"width": "200px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
>
|
||||
<Image
|
||||
src="props.img.externalUrl"
|
||||
style={
|
||||
Object {
|
||||
"height": "100px",
|
||||
"width": "200px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="img-text p-3"
|
||||
className="card-text p-3"
|
||||
>
|
||||
<h3>
|
||||
props.img.displayName
|
||||
</h3>
|
||||
<p>
|
||||
<p
|
||||
style={
|
||||
Object {
|
||||
"fontSize": "11px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Added {date} at {time}"
|
||||
description="File date-added string"
|
||||
id="authoring.texteditor.selectimagemodal.addedDate.label"
|
||||
id="authoring.selectionmodal.addedDate.label"
|
||||
values={
|
||||
Object {
|
||||
"date": <FormattedDate
|
||||
@@ -0,0 +1,437 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SearchSort component snapshots with filterKeys with search string (close button) 1`] = `
|
||||
<ActionRow>
|
||||
<Form.Group
|
||||
style={
|
||||
Object {
|
||||
"margin": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Form.Control
|
||||
autoFocus={true}
|
||||
onChange={[MockFunction props.onSearchChange]}
|
||||
placeholder="Search"
|
||||
trailingElement={
|
||||
<IconButton
|
||||
iconAs="Icon"
|
||||
invertColors={true}
|
||||
isActive={true}
|
||||
onClick={[MockFunction props.clearSearchString]}
|
||||
size="sm"
|
||||
src={[MockFunction icons.Close]}
|
||||
/>
|
||||
}
|
||||
value="props.searchString"
|
||||
/>
|
||||
</Form.Group>
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
id="gallery-sort-button"
|
||||
variant="tertiary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By date added (oldest)"
|
||||
description="Dropdown label for sorting by date (oldest)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.dateoldest.label"
|
||||
/>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item
|
||||
key="dateNewest"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By date added (newest)"
|
||||
description="Dropdown label for sorting by date (newest)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.datenewest.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="dateOldest"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By date added (oldest)"
|
||||
description="Dropdown label for sorting by date (oldest)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.dateoldest.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="nameAscending"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By name (ascending)"
|
||||
description="Dropdown label for sorting by name (ascending)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.nameascending.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="nameDescending"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By name (descending)"
|
||||
description="Dropdown label for sorting by name (descending)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.namedescending.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
id="gallery-filter-button"
|
||||
variant="tertiary"
|
||||
>
|
||||
<FormattedMessage />
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item
|
||||
key="videoStatus"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Video status"
|
||||
description="Dropdown label for filter by video status (none)"
|
||||
id="authoring.selectvideomodal.filter.videostatusnone.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="uploading"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Uploading"
|
||||
description="Dropdown label for filter by video status (uploading)"
|
||||
id="authoring.selectvideomodal.filter.videostatusuploading.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="processing"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Processing"
|
||||
description="Dropdown label for filter by video status (processing)"
|
||||
id="authoring.selectvideomodal.filter.videostatusprocessing.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="ready"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Ready"
|
||||
description="Dropdown label for filter by video status (ready)"
|
||||
id="authoring.selectvideomodal.filter.videostatusready.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="failed"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Failed"
|
||||
description="Dropdown label for filter by video status (failed)"
|
||||
id="authoring.selectvideomodal.filter.videostatusfailed.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
<ActionRow.Spacer />
|
||||
<Component
|
||||
isInline={true}
|
||||
name="switch"
|
||||
onChange={null}
|
||||
>
|
||||
<Component
|
||||
floatLabelLeft={true}
|
||||
value="switch-value"
|
||||
>
|
||||
<FormattedMessage />
|
||||
</Component>
|
||||
</Component>
|
||||
</ActionRow>
|
||||
`;
|
||||
|
||||
exports[`SearchSort component snapshots with filterKeys without search string (search icon) 1`] = `
|
||||
<ActionRow>
|
||||
<Form.Group
|
||||
style={
|
||||
Object {
|
||||
"margin": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Form.Control
|
||||
autoFocus={true}
|
||||
onChange={[MockFunction props.onSearchChange]}
|
||||
placeholder="Search"
|
||||
trailingElement={<Icon />}
|
||||
value=""
|
||||
/>
|
||||
</Form.Group>
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
id="gallery-sort-button"
|
||||
variant="tertiary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By date added (oldest)"
|
||||
description="Dropdown label for sorting by date (oldest)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.dateoldest.label"
|
||||
/>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item
|
||||
key="dateNewest"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By date added (newest)"
|
||||
description="Dropdown label for sorting by date (newest)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.datenewest.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="dateOldest"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By date added (oldest)"
|
||||
description="Dropdown label for sorting by date (oldest)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.dateoldest.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="nameAscending"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By name (ascending)"
|
||||
description="Dropdown label for sorting by name (ascending)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.nameascending.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="nameDescending"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By name (descending)"
|
||||
description="Dropdown label for sorting by name (descending)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.namedescending.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
id="gallery-filter-button"
|
||||
variant="tertiary"
|
||||
>
|
||||
<FormattedMessage />
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item
|
||||
key="videoStatus"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Video status"
|
||||
description="Dropdown label for filter by video status (none)"
|
||||
id="authoring.selectvideomodal.filter.videostatusnone.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="uploading"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Uploading"
|
||||
description="Dropdown label for filter by video status (uploading)"
|
||||
id="authoring.selectvideomodal.filter.videostatusuploading.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="processing"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Processing"
|
||||
description="Dropdown label for filter by video status (processing)"
|
||||
id="authoring.selectvideomodal.filter.videostatusprocessing.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="ready"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Ready"
|
||||
description="Dropdown label for filter by video status (ready)"
|
||||
id="authoring.selectvideomodal.filter.videostatusready.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="failed"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Failed"
|
||||
description="Dropdown label for filter by video status (failed)"
|
||||
id="authoring.selectvideomodal.filter.videostatusfailed.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
<ActionRow.Spacer />
|
||||
<Component
|
||||
isInline={true}
|
||||
name="switch"
|
||||
onChange={null}
|
||||
>
|
||||
<Component
|
||||
floatLabelLeft={true}
|
||||
value="switch-value"
|
||||
>
|
||||
<FormattedMessage />
|
||||
</Component>
|
||||
</Component>
|
||||
</ActionRow>
|
||||
`;
|
||||
|
||||
exports[`SearchSort component snapshots without filterKeys with search string (close button) 1`] = `
|
||||
<ActionRow>
|
||||
<Form.Group
|
||||
style={
|
||||
Object {
|
||||
"margin": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Form.Control
|
||||
autoFocus={true}
|
||||
onChange={[MockFunction props.onSearchChange]}
|
||||
placeholder="Search"
|
||||
trailingElement={
|
||||
<IconButton
|
||||
iconAs="Icon"
|
||||
invertColors={true}
|
||||
isActive={true}
|
||||
onClick={[MockFunction props.clearSearchString]}
|
||||
size="sm"
|
||||
src={[MockFunction icons.Close]}
|
||||
/>
|
||||
}
|
||||
value="props.searchString"
|
||||
/>
|
||||
</Form.Group>
|
||||
<ActionRow.Spacer />
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
id="gallery-sort-button"
|
||||
variant="tertiary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By date added (oldest)"
|
||||
description="Dropdown label for sorting by date (oldest)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.dateoldest.label"
|
||||
/>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item
|
||||
key="dateNewest"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By date added (newest)"
|
||||
description="Dropdown label for sorting by date (newest)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.datenewest.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="dateOldest"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By date added (oldest)"
|
||||
description="Dropdown label for sorting by date (oldest)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.dateoldest.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="nameAscending"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By name (ascending)"
|
||||
description="Dropdown label for sorting by name (ascending)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.nameascending.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="nameDescending"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By name (descending)"
|
||||
description="Dropdown label for sorting by name (descending)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.namedescending.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</ActionRow>
|
||||
`;
|
||||
|
||||
exports[`SearchSort component snapshots without filterKeys without search string (search icon) 1`] = `
|
||||
<ActionRow>
|
||||
<Form.Group
|
||||
style={
|
||||
Object {
|
||||
"margin": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Form.Control
|
||||
autoFocus={true}
|
||||
onChange={[MockFunction props.onSearchChange]}
|
||||
placeholder="Search"
|
||||
trailingElement={<Icon />}
|
||||
value=""
|
||||
/>
|
||||
</Form.Group>
|
||||
<ActionRow.Spacer />
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
id="gallery-sort-button"
|
||||
variant="tertiary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By date added (oldest)"
|
||||
description="Dropdown label for sorting by date (oldest)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.dateoldest.label"
|
||||
/>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item
|
||||
key="dateNewest"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By date added (newest)"
|
||||
description="Dropdown label for sorting by date (newest)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.datenewest.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="dateOldest"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By date added (oldest)"
|
||||
description="Dropdown label for sorting by date (oldest)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.dateoldest.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="nameAscending"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By name (ascending)"
|
||||
description="Dropdown label for sorting by name (ascending)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.nameascending.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="nameDescending"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By name (descending)"
|
||||
description="Dropdown label for sorting by name (descending)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.namedescending.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</ActionRow>
|
||||
`;
|
||||
155
src/editors/sharedComponents/SelectionModal/index.jsx
Normal file
155
src/editors/sharedComponents/SelectionModal/index.jsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Button, Stack } from '@edx/paragon';
|
||||
import { Add } from '@edx/paragon/icons';
|
||||
import {
|
||||
FormattedMessage,
|
||||
injectIntl,
|
||||
intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
|
||||
import BaseModal from '../BaseModal';
|
||||
import SearchSort from './SearchSort';
|
||||
import Gallery from './Gallery';
|
||||
import FileInput from '../FileInput';
|
||||
import ErrorAlert from '../ErrorAlerts/ErrorAlert';
|
||||
import FetchErrorAlert from '../ErrorAlerts/FetchErrorAlert';
|
||||
import UploadErrorAlert from '../ErrorAlerts/UploadErrorAlert';
|
||||
|
||||
export const SelectionModal = ({
|
||||
isOpen,
|
||||
close,
|
||||
size,
|
||||
isFullscreenScroll,
|
||||
galleryError,
|
||||
inputError,
|
||||
fileInput,
|
||||
galleryProps,
|
||||
searchSortProps,
|
||||
selectBtnProps,
|
||||
acceptedFiles,
|
||||
modalMessages,
|
||||
isLoaded,
|
||||
isFetchError,
|
||||
isUploadError,
|
||||
// injected
|
||||
intl,
|
||||
}) => {
|
||||
const {
|
||||
confirmMsg,
|
||||
uploadButtonMsg,
|
||||
titleMsg,
|
||||
fetchError,
|
||||
uploadError,
|
||||
} = modalMessages;
|
||||
|
||||
let background = '#FFFFFF';
|
||||
let showGallery = true;
|
||||
if (isLoaded && !isFetchError && !isUploadError && !inputError.show) {
|
||||
background = '#EBEBEB';
|
||||
} else if (isLoaded) {
|
||||
showGallery = false;
|
||||
}
|
||||
|
||||
const galleryPropsValues = {
|
||||
isLoaded,
|
||||
show: showGallery,
|
||||
...galleryProps,
|
||||
};
|
||||
return (
|
||||
<BaseModal
|
||||
close={close}
|
||||
confirmAction={(
|
||||
<Button {...selectBtnProps} variant="primary">
|
||||
<FormattedMessage {...confirmMsg} />
|
||||
</Button>
|
||||
)}
|
||||
isOpen={isOpen}
|
||||
size={size}
|
||||
isFullscreenScroll={isFullscreenScroll}
|
||||
footerAction={(
|
||||
<Button iconBefore={Add} onClick={fileInput.click} variant="link">
|
||||
<FormattedMessage {...uploadButtonMsg} />
|
||||
</Button>
|
||||
)}
|
||||
title={intl.formatMessage(titleMsg)}
|
||||
bodyStyle={{ background, padding: '9px 24px' }}
|
||||
headerComponent={(
|
||||
<div style={{ zIndex: 10000, margin: '18px 0' }}>
|
||||
<SearchSort {...searchSortProps} />
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
{/* Error Alerts */}
|
||||
<FetchErrorAlert isFetchError={isFetchError} message={fetchError} />
|
||||
<UploadErrorAlert isUploadError={isUploadError} message={uploadError} />
|
||||
<ErrorAlert
|
||||
dismissError={inputError.dismiss}
|
||||
hideHeading
|
||||
isError={inputError.show}
|
||||
>
|
||||
<FormattedMessage {...inputError.message} />
|
||||
</ErrorAlert>
|
||||
|
||||
{/* User Feedback Alerts */}
|
||||
<ErrorAlert
|
||||
dismissError={galleryError.dismiss}
|
||||
hideHeading
|
||||
isError={galleryError.show}
|
||||
>
|
||||
<FormattedMessage {...galleryError.message} />
|
||||
</ErrorAlert>
|
||||
<Stack gap={2}>
|
||||
<Gallery {...galleryPropsValues} />
|
||||
<FileInput fileInput={fileInput} acceptedFiles={Object.values(acceptedFiles).join()} />
|
||||
</Stack>
|
||||
</BaseModal>
|
||||
);
|
||||
};
|
||||
|
||||
SelectionModal.defaultProps = {
|
||||
size: 'lg',
|
||||
isFullscreenScroll: true,
|
||||
};
|
||||
|
||||
SelectionModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
close: PropTypes.func.isRequired,
|
||||
size: PropTypes.string,
|
||||
isFullscreenScroll: PropTypes.bool,
|
||||
galleryError: PropTypes.shape({
|
||||
dismiss: PropTypes.func.isRequired,
|
||||
show: PropTypes.bool.isRequired,
|
||||
set: PropTypes.func.isRequired,
|
||||
message: PropTypes.shape({}).isRequired,
|
||||
}).isRequired,
|
||||
inputError: PropTypes.shape({
|
||||
dismiss: PropTypes.func.isRequired,
|
||||
show: PropTypes.bool.isRequired,
|
||||
set: PropTypes.func.isRequired,
|
||||
message: PropTypes.shape({}).isRequired,
|
||||
}).isRequired,
|
||||
fileInput: PropTypes.shape({
|
||||
click: PropTypes.func.isRequired,
|
||||
addFile: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
galleryProps: PropTypes.shape({}).isRequired,
|
||||
searchSortProps: PropTypes.shape({}).isRequired,
|
||||
selectBtnProps: PropTypes.shape({}).isRequired,
|
||||
acceptedFiles: PropTypes.shape({}).isRequired,
|
||||
modalMessages: PropTypes.shape({
|
||||
confirmMsg: PropTypes.shape({}).isRequired,
|
||||
uploadButtonMsg: PropTypes.shape({}).isRequired,
|
||||
titleMsg: PropTypes.shape({}).isRequired,
|
||||
fetchError: PropTypes.shape({}).isRequired,
|
||||
uploadError: PropTypes.shape({}).isRequired,
|
||||
}).isRequired,
|
||||
isLoaded: PropTypes.bool.isRequired,
|
||||
isFetchError: PropTypes.bool.isRequired,
|
||||
isUploadError: PropTypes.bool.isRequired,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(SelectionModal);
|
||||
185
src/editors/sharedComponents/SelectionModal/index.test.jsx
Normal file
185
src/editors/sharedComponents/SelectionModal/index.test.jsx
Normal file
@@ -0,0 +1,185 @@
|
||||
import React from 'react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { formatMessage } from '../../../testUtils';
|
||||
import SelectionModal from '.';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
const props = {
|
||||
isOpen: jest.fn(),
|
||||
isClose: jest.fn(),
|
||||
size: 'fullscreen',
|
||||
isFullscreenScroll: false,
|
||||
galleryError: {
|
||||
show: false,
|
||||
set: jest.fn(),
|
||||
dismiss: jest.fn(),
|
||||
message: {
|
||||
id: 'Gallery error id',
|
||||
defaultMessage: 'Gallery error',
|
||||
description: 'Gallery error',
|
||||
},
|
||||
},
|
||||
inputError: {
|
||||
show: false,
|
||||
set: jest.fn(),
|
||||
dismiss: jest.fn(),
|
||||
message: {
|
||||
id: 'Input error id',
|
||||
defaultMessage: 'Input error',
|
||||
description: 'Input error',
|
||||
},
|
||||
},
|
||||
fileInput: {
|
||||
addFile: 'imgHooks.fileInput.addFile',
|
||||
click: 'imgHooks.fileInput.click',
|
||||
ref: 'imgHooks.fileInput.ref',
|
||||
},
|
||||
galleryProps: { gallery: 'props' },
|
||||
searchSortProps: { search: 'sortProps' },
|
||||
selectBtnProps: { select: 'btnProps' },
|
||||
acceptedFiles: { png: '.png' },
|
||||
modalMessages: {
|
||||
confirmMsg: {
|
||||
id: 'confirmMsg',
|
||||
defaultMessage: 'confirmMsg',
|
||||
description: 'confirmMsg',
|
||||
},
|
||||
uploadButtonMsg: {
|
||||
id: 'uploadButtonMsg',
|
||||
defaultMessage: 'uploadButtonMsg',
|
||||
description: 'uploadButtonMsg',
|
||||
},
|
||||
titleMsg: {
|
||||
id: 'titleMsg',
|
||||
defaultMessage: 'titleMsg',
|
||||
description: 'titleMsg',
|
||||
},
|
||||
fetchError: {
|
||||
id: 'fetchError',
|
||||
defaultMessage: 'fetchError',
|
||||
description: 'fetchError',
|
||||
},
|
||||
uploadError: {
|
||||
id: 'uploadError',
|
||||
defaultMessage: 'uploadError',
|
||||
description: 'uploadError',
|
||||
},
|
||||
},
|
||||
isLoaded: true,
|
||||
isFetchError: false,
|
||||
isUploadError: false,
|
||||
intl: { formatMessage },
|
||||
};
|
||||
|
||||
const mockGalleryFn = jest.fn();
|
||||
const mockFileInputFn = jest.fn();
|
||||
const mockFetchErrorAlertFn = jest.fn();
|
||||
const mockUploadErrorAlertFn = jest.fn();
|
||||
|
||||
jest.mock('../BaseModal', () => 'BaseModal');
|
||||
jest.mock('./SearchSort', () => 'SearchSort');
|
||||
jest.mock('./Gallery', () => (componentProps) => {
|
||||
mockGalleryFn(componentProps);
|
||||
return (<div>Gallery</div>);
|
||||
});
|
||||
jest.mock('../FileInput', () => (componentProps) => {
|
||||
mockFileInputFn(componentProps);
|
||||
return (<div>FileInput</div>);
|
||||
});
|
||||
jest.mock('../ErrorAlerts/ErrorAlert', () => () => (<div>ErrorAlert</div>));
|
||||
jest.mock('../ErrorAlerts/FetchErrorAlert', () => (componentProps) => {
|
||||
mockFetchErrorAlertFn(componentProps);
|
||||
return (<div>FetchErrorAlert</div>);
|
||||
});
|
||||
jest.mock('../ErrorAlerts/UploadErrorAlert', () => (componentProps) => {
|
||||
mockUploadErrorAlertFn(componentProps);
|
||||
return (<div>UploadErrorAlert</div>);
|
||||
});
|
||||
|
||||
describe('Selection Modal', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
test('rendering correctly with expected Input', async () => {
|
||||
render(
|
||||
<IntlProvider>
|
||||
<SelectionModal {...props} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
expect(screen.getByText('Gallery')).toBeInTheDocument();
|
||||
expect(screen.getByText('FileInput')).toBeInTheDocument();
|
||||
expect(screen.getByText('FetchErrorAlert')).toBeInTheDocument();
|
||||
expect(screen.getByText('UploadErrorAlert')).toBeInTheDocument();
|
||||
|
||||
expect(mockGalleryFn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
...props.galleryProps,
|
||||
isLoaded: props.isLoaded,
|
||||
show: true,
|
||||
}),
|
||||
);
|
||||
expect(mockFetchErrorAlertFn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
isFetchError: props.isFetchError,
|
||||
message: props.modalMessages.fetchError,
|
||||
}),
|
||||
);
|
||||
expect(mockUploadErrorAlertFn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
isUploadError: props.isUploadError,
|
||||
message: props.modalMessages.uploadError,
|
||||
}),
|
||||
);
|
||||
expect(mockFileInputFn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
acceptedFiles: '.png',
|
||||
fileInput: props.fileInput,
|
||||
}),
|
||||
);
|
||||
});
|
||||
test('rendering correctly with errors', () => {
|
||||
render(
|
||||
<IntlProvider>
|
||||
<SelectionModal {...props} isFetchError />
|
||||
</IntlProvider>,
|
||||
);
|
||||
expect(screen.getByText('Gallery')).toBeInTheDocument();
|
||||
expect(screen.getByText('FileInput')).toBeInTheDocument();
|
||||
expect(screen.getByText('FetchErrorAlert')).toBeInTheDocument();
|
||||
expect(screen.getByText('UploadErrorAlert')).toBeInTheDocument();
|
||||
|
||||
expect(mockFetchErrorAlertFn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
isFetchError: true,
|
||||
message: props.modalMessages.fetchError,
|
||||
}),
|
||||
);
|
||||
expect(mockGalleryFn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
...props.galleryProps,
|
||||
isLoaded: props.isLoaded,
|
||||
show: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
test('rendering correctly with loading', () => {
|
||||
render(
|
||||
<IntlProvider>
|
||||
<SelectionModal {...props} isLoaded={false} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
expect(screen.getByText('Gallery')).toBeInTheDocument();
|
||||
expect(screen.getByText('FileInput')).toBeInTheDocument();
|
||||
expect(screen.getByText('FetchErrorAlert')).toBeInTheDocument();
|
||||
expect(screen.getByText('UploadErrorAlert')).toBeInTheDocument();
|
||||
|
||||
expect(mockGalleryFn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
...props.galleryProps,
|
||||
isLoaded: false,
|
||||
show: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
24
src/editors/sharedComponents/SelectionModal/messages.js
Normal file
24
src/editors/sharedComponents/SelectionModal/messages.js
Normal file
@@ -0,0 +1,24 @@
|
||||
export const messages = {
|
||||
searchPlaceholder: {
|
||||
id: 'authoring.selectionmodal.search.placeholder',
|
||||
defaultMessage: 'Search',
|
||||
description: 'Placeholder text for search bar',
|
||||
},
|
||||
emptySearchLabel: {
|
||||
id: 'authoring.selectionmodal.emptySearchLabel',
|
||||
defaultMessage: 'No search results.',
|
||||
description: 'Label for when search returns nothing.',
|
||||
},
|
||||
loading: {
|
||||
id: 'authoring.selectionmodal.spinner.readertext',
|
||||
defaultMessage: 'loading...',
|
||||
description: 'Gallery loading spinner screen-reader text',
|
||||
},
|
||||
addedDate: {
|
||||
id: 'authoring.selectionmodal.addedDate.label',
|
||||
defaultMessage: 'Added {date} at {time}',
|
||||
description: 'File date-added string',
|
||||
},
|
||||
};
|
||||
|
||||
export default messages;
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
exports[`SourceCodeModal renders as expected with default behavior 1`] = `
|
||||
<BaseModal
|
||||
bodyStyle={null}
|
||||
close={[MockFunction]}
|
||||
confirmAction={
|
||||
<Button
|
||||
@@ -24,6 +25,8 @@ exports[`SourceCodeModal renders as expected with default behavior 1`] = `
|
||||
</Button>
|
||||
}
|
||||
footerAction={null}
|
||||
headerComponent={null}
|
||||
isFullscreenScroll={true}
|
||||
isOpen={false}
|
||||
size="xl"
|
||||
title="Edit Source Code"
|
||||
|
||||
18
src/editors/utils/formatDuration.js
Normal file
18
src/editors/utils/formatDuration.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import * as moment from 'moment-shortformat';
|
||||
|
||||
const formatDuration = (duration) => {
|
||||
const d = moment.duration(duration, 'seconds');
|
||||
if (d.hours() > 0) {
|
||||
return (
|
||||
`${d.hours().toString().padStart(2, '0')}:`
|
||||
+ `${d.minutes().toString().padStart(2, '0')}:`
|
||||
+ `${d.seconds().toString().padStart(2, '0')}`
|
||||
);
|
||||
}
|
||||
return (
|
||||
`${d.minutes().toString().padStart(2, '0')}:`
|
||||
+ `${d.seconds().toString().padStart(2, '0')}`
|
||||
);
|
||||
};
|
||||
|
||||
export default formatDuration;
|
||||
12
src/editors/utils/formatDuration.test.js
Normal file
12
src/editors/utils/formatDuration.test.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import formatDuration from './formatDuration';
|
||||
|
||||
describe('formatDuration', () => {
|
||||
test.each([
|
||||
[60, '01:00'],
|
||||
[35, '00:35'],
|
||||
[60 * 10 + 15, '10:15'],
|
||||
[60 * 60 + 60 * 15 + 13, '01:15:13'],
|
||||
])('correct functionality of formatDuration with duration as %p', (duration, expected) => {
|
||||
expect(formatDuration(duration)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -3,3 +3,4 @@ export { default as StrictDict } from './StrictDict';
|
||||
export { default as keyStore } from './keyStore';
|
||||
export { default as camelizeKeys } from './camelizeKeys';
|
||||
export { default as removeItemOnce } from './removeOnce';
|
||||
export { default as formatDuration } from './formatDuration';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Placeholder from './Placeholder';
|
||||
import messages from './i18n/index';
|
||||
import EditorPage from './editors/EditorPage';
|
||||
import VideoSelectorPage from './editors/VideoSelectorPage';
|
||||
|
||||
export { messages, EditorPage };
|
||||
export { messages, EditorPage, VideoSelectorPage };
|
||||
export default Placeholder;
|
||||
|
||||
Reference in New Issue
Block a user