Compare commits

...

8 Commits

Author SHA1 Message Date
Syed Ali Abbas Zaidi
b24568f0bd chore: bump frontend-platform (#1209) 2023-10-18 11:13:06 +05:00
renovate[bot]
5604def491 chore(deps): update dependency @edx/frontend-build to v12.9.17 2023-10-04 21:51:00 +00:00
Syed Ali Abbas Zaidi
b788b969c3 feat: upgrade react router to v6 (#1128)
* feat: upgrade react router to v6

* fix: all test cases affected by react router upgrade

* refactor: fix navigations

* fix: test cases affectewd due to lib-special-exams

* refactor: improve code coverage
2023-10-04 17:34:53 -04:00
Zachary Hancock
b7a3d5640a fix: special exams version fix (#1196) 2023-09-27 13:12:20 -04:00
Zachary Hancock
3a21d8c807 feat: update exams library (#1188) 2023-09-25 13:46:29 -04:00
alangsto
81442bebe9 feat: update learning assistant version (#1195) 2023-09-21 14:44:58 -04:00
alangsto
168ed1e184 feat: upgrade learning assistant version (#1187) 2023-09-18 13:27:57 -04:00
Ben Warzeski
c8e32c3f46 feat: allow override of plugin.modal height (#1184) 2023-09-15 10:12:47 -04:00
52 changed files with 1283 additions and 859 deletions

1
.env
View File

@@ -46,3 +46,4 @@ TERMS_OF_SERVICE_URL=''
TWITTER_HASHTAG=''
TWITTER_URL=''
USER_INFO_COOKIE_NAME=''
OPTIMIZELY_FULL_STACK_SDK_KEY=''

View File

@@ -48,3 +48,4 @@ USER_INFO_COOKIE_NAME='edx-user-info'
SESSION_COOKIE_DOMAIN='localhost'
CHAT_RESPONSE_URL='http://localhost:18000/api/learning_assistant/v1/course_id'
PRIVACY_POLICY_URL='http://localhost:18000/privacy'
OPTIMIZELY_FULL_STACK_SDK_KEY=''

864
package-lock.json generated
View File

@@ -17,11 +17,11 @@
"license": "AGPL-3.0",
"dependencies": {
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
"@edx/frontend-component-footer": "12.1.2",
"@edx/frontend-component-header": "4.4.4",
"@edx/frontend-lib-learning-assistant": "^1.11.1",
"@edx/frontend-lib-special-exams": "2.20.1",
"@edx/frontend-platform": "4.6.0",
"@edx/frontend-component-footer": "12.2.1",
"@edx/frontend-component-header": "4.6.0",
"@edx/frontend-lib-learning-assistant": "^1.14.0",
"@edx/frontend-lib-special-exams": "2.23.2",
"@edx/frontend-platform": "5.5.2",
"@edx/paragon": "20.46.0",
"@edx/react-unit-test-utils": "npm:@edx/react-unit-test-utils@1.7.0",
"@fortawesome/fontawesome-svg-core": "1.3.0",
@@ -42,8 +42,8 @@
"react-dom": "17.0.2",
"react-helmet": "6.1.0",
"react-redux": "7.2.9",
"react-router": "5.2.1",
"react-router-dom": "5.3.0",
"react-router": "6.15.0",
"react-router-dom": "6.15.0",
"react-share": "4.4.1",
"redux": "4.1.2",
"regenerator-runtime": "0.13.11",
@@ -2227,9 +2227,9 @@
}
},
"node_modules/@edx/frontend-build": {
"version": "12.9.10",
"resolved": "https://registry.npmjs.org/@edx/frontend-build/-/frontend-build-12.9.10.tgz",
"integrity": "sha512-g1tlmonkNO+RxWKVBiRwYO9prFzqS6FuQCE1TIHwjPptWicPEBaVr/m6YoU6kW7CWqFXufCb2ESippQKfLBkKw==",
"version": "12.9.17",
"resolved": "https://registry.npmjs.org/@edx/frontend-build/-/frontend-build-12.9.17.tgz",
"integrity": "sha512-P4w/jp456o5EQ5l0WVl4JIRHnC5xbG+M1Ce7xX1JKex4YDN3lmoBuiaguPg3n6CBDsNKEiBr48n4u+ug+5UTCw==",
"dependencies": {
"@babel/cli": "7.22.5",
"@babel/core": "7.22.5",
@@ -2243,8 +2243,8 @@
"@edx/new-relic-source-map-webpack-plugin": "2.1.0",
"@fullhuman/postcss-purgecss": "5.0.0",
"@pmmmwh/react-refresh-webpack-plugin": "0.5.11",
"@svgr/webpack": "8.0.1",
"autoprefixer": "10.4.15",
"@svgr/webpack": "8.1.0",
"autoprefixer": "10.4.16",
"babel-jest": "26.6.3",
"babel-loader": "9.1.3",
"babel-plugin-react-intl": "7.9.4",
@@ -2269,21 +2269,21 @@
"image-minimizer-webpack-plugin": "3.8.3",
"jest": "26.6.3",
"mini-css-extract-plugin": "1.6.2",
"postcss": "8.4.28",
"postcss-custom-media": "10.0.0",
"postcss": "8.4.30",
"postcss-custom-media": "10.0.1",
"postcss-loader": "7.3.3",
"postcss-rtlcss": "4.0.7",
"postcss-rtlcss": "4.0.8",
"react-dev-utils": "12.0.1",
"react-refresh": "0.14.0",
"resolve-url-loader": "5.0.0",
"sass": "1.65.1",
"sass-loader": "13.3.2",
"sharp": "0.32.5",
"sharp": "0.32.6",
"source-map-loader": "4.0.1",
"style-loader": "3.3.3",
"url-loader": "4.1.1",
"webpack": "5.88.2",
"webpack-bundle-analyzer": "4.9.0",
"webpack-bundle-analyzer": "4.9.1",
"webpack-cli": "5.1.4",
"webpack-dev-server": "4.15.1",
"webpack-merge": "5.9.0"
@@ -3181,75 +3181,75 @@
}
},
"node_modules/@edx/frontend-component-footer": {
"version": "12.1.2",
"resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-12.1.2.tgz",
"integrity": "sha512-f1lM1WTpdiD4vjrbE5pMC8J11ObWI9w7upBBk+xqP3Iv27+py9Sr4CJVhQVRwqvIYr6heurvv9UxECbZ0X3alg==",
"version": "12.2.1",
"resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-12.2.1.tgz",
"integrity": "sha512-0ZeuFsnToS7h7qI4yXo6FKVz+c4vDyqP2nhPMR3xm3xgPOmHvf8KNL6ES/YGb+ptPYb64ZxT2iNBP6DY0wF3uQ==",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "6.4.0",
"@fortawesome/free-brands-svg-icons": "6.4.0",
"@fortawesome/free-regular-svg-icons": "6.4.0",
"@fortawesome/free-solid-svg-icons": "6.4.0",
"@fortawesome/fontawesome-svg-core": "6.4.2",
"@fortawesome/free-brands-svg-icons": "6.4.2",
"@fortawesome/free-regular-svg-icons": "6.4.2",
"@fortawesome/free-solid-svg-icons": "6.4.2",
"@fortawesome/react-fontawesome": "0.2.0"
},
"peerDependencies": {
"@edx/frontend-platform": "^4.0.0",
"@edx/frontend-platform": "^4.0.0 || ^5.0.0",
"prop-types": "^15.5.10",
"react": "^16.9.0 || ^17.0.0",
"react-dom": "^16.9.0 || ^17.0.0"
}
},
"node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/fontawesome-common-types": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.0.tgz",
"integrity": "sha512-HNii132xfomg5QVZw0HwXXpN22s7VBHQBv9CeOu9tfJnhsWQNd2lmTNi8CSrnw5B+5YOmzu1UoPAyxaXsJ6RgQ==",
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz",
"integrity": "sha512-1DgP7f+XQIJbLFCTX1V2QnxVmpLdKdzzo2k8EmvDOePfchaIGQ9eCHj2up3/jNEbZuBqel5OxiaOJf37TWauRA==",
"hasInstallScript": true,
"engines": {
"node": ">=6"
}
},
"node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/fontawesome-svg-core": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.0.tgz",
"integrity": "sha512-Bertv8xOiVELz5raB2FlXDPKt+m94MQ3JgDfsVbrqNpLU9+UE2E18GKjLKw+d3XbeYPqg1pzyQKGsrzbw+pPaw==",
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.2.tgz",
"integrity": "sha512-gjYDSKv3TrM2sLTOKBc5rH9ckje8Wrwgx1CxAPbN5N3Fm4prfi7NsJVWd1jklp7i5uSCVwhZS5qlhMXqLrpAIg==",
"hasInstallScript": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.4.0"
"@fortawesome/fontawesome-common-types": "6.4.2"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/free-brands-svg-icons": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.4.0.tgz",
"integrity": "sha512-qvxTCo0FQ5k2N+VCXb/PZQ+QMhqRVM4OORiO6MXdG6bKolIojGU/srQ1ptvKk0JTbRgaJOfL2qMqGvBEZG7Z6g==",
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.4.2.tgz",
"integrity": "sha512-LKOwJX0I7+mR/cvvf6qIiqcERbdnY+24zgpUSouySml+5w8B4BJOx8EhDR/FTKAu06W12fmUIcv6lzPSwYKGGg==",
"hasInstallScript": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.4.0"
"@fortawesome/fontawesome-common-types": "6.4.2"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/free-regular-svg-icons": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.4.0.tgz",
"integrity": "sha512-ZfycI7D0KWPZtf7wtMFnQxs8qjBXArRzczABuMQqecA/nXohquJ5J/RCR77PmY5qGWkxAZDxpnUFVXKwtY/jPw==",
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.4.2.tgz",
"integrity": "sha512-0+sIUWnkgTVVXVAPQmW4vxb9ZTHv0WstOa3rBx9iPxrrrDH6bNLsDYuwXF9b6fGm+iR7DKQvQshUH/FJm3ed9Q==",
"hasInstallScript": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.4.0"
"@fortawesome/fontawesome-common-types": "6.4.2"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/free-solid-svg-icons": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.0.tgz",
"integrity": "sha512-kutPeRGWm8V5dltFP1zGjQOEAzaLZj4StdQhWVZnfGFCvAPVvHh8qk5bRrU4KXnRRRNni5tKQI9PBAdI6MP8nQ==",
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.2.tgz",
"integrity": "sha512-sYwXurXUEQS32fZz9hVCUUv/xu49PEJEyUOsA51l6PU/qVgfbTb2glsTEaJngVVT8VqBATRIdh7XVgV1JF1LkA==",
"hasInstallScript": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.4.0"
"@fortawesome/fontawesome-common-types": "6.4.2"
},
"engines": {
"node": ">=6"
@@ -3268,39 +3268,32 @@
}
},
"node_modules/@edx/frontend-component-header": {
"version": "4.4.4",
"resolved": "https://registry.npmjs.org/@edx/frontend-component-header/-/frontend-component-header-4.4.4.tgz",
"integrity": "sha512-7C2cr9A/5uIt7zOLtM650MT66zO2Q/O+AY1WufrdH2D/YiSoBTL+rFPPv39PHpDe5zZ8Wq0LyFLm/pUeTWDQIQ==",
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-component-header/-/frontend-component-header-4.6.0.tgz",
"integrity": "sha512-zZuMgHQWfFMTquVb4iL/iQMwKRRgts8CFFLyL8R6vQL1WfHd21hndhKii2kp9lBnIJgrilIfF79RsbImb5L0og==",
"dependencies": {
"@edx/paragon": "20.45.5",
"@fortawesome/fontawesome-svg-core": "6.4.0",
"@fortawesome/free-brands-svg-icons": "6.4.0",
"@fortawesome/free-regular-svg-icons": "6.4.0",
"@fortawesome/free-solid-svg-icons": "6.4.0",
"@edx/paragon": "20.46.2",
"@fortawesome/fontawesome-svg-core": "6.4.2",
"@fortawesome/free-brands-svg-icons": "6.4.2",
"@fortawesome/free-regular-svg-icons": "6.4.2",
"@fortawesome/free-solid-svg-icons": "6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@reduxjs/toolkit": "1.9.5",
"axios-mock-adapter": "1.21.5",
"babel-polyfill": "6.26.0",
"classnames": "2.3.2",
"lodash": "4.17.21",
"react-redux": "7.2.9",
"react-responsive": "8.2.0",
"react-router-dom": "5.3.4",
"react-transition-group": "4.4.5",
"rosie": "2.1.0",
"timeago.js": "4.0.2"
"react-transition-group": "4.4.5"
},
"peerDependencies": {
"@edx/frontend-platform": "^4.0.0",
"@edx/frontend-platform": "^4.0.0 || ^5.0.0",
"prop-types": "^15.5.10",
"react": "^16.9.0 || ^17.0.0",
"react-dom": "^16.9.0 || ^17.0.0"
}
},
"node_modules/@edx/frontend-component-header/node_modules/@edx/paragon": {
"version": "20.45.5",
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.45.5.tgz",
"integrity": "sha512-7GsGPKyxtjFo3Xnj+uQ4vx/Khz7S6srHe8MqcsYCMx2mJ8fulPN2JFm84m+0o1CSwHaL469wBPONI4KCa+vfrA==",
"version": "20.46.2",
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.46.2.tgz",
"integrity": "sha512-px+KS/BV1CbiMKgfVgUofyjJi4CHUCUOLRukJbT66VPPqWP4Xon5Rns6uohoratPXMg2kNN46v2L8wIwqKQ4Lw==",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.1.1",
"@fortawesome/react-fontawesome": "^0.1.18",
@@ -3347,57 +3340,57 @@
}
},
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/fontawesome-common-types": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.0.tgz",
"integrity": "sha512-HNii132xfomg5QVZw0HwXXpN22s7VBHQBv9CeOu9tfJnhsWQNd2lmTNi8CSrnw5B+5YOmzu1UoPAyxaXsJ6RgQ==",
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz",
"integrity": "sha512-1DgP7f+XQIJbLFCTX1V2QnxVmpLdKdzzo2k8EmvDOePfchaIGQ9eCHj2up3/jNEbZuBqel5OxiaOJf37TWauRA==",
"hasInstallScript": true,
"engines": {
"node": ">=6"
}
},
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/fontawesome-svg-core": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.0.tgz",
"integrity": "sha512-Bertv8xOiVELz5raB2FlXDPKt+m94MQ3JgDfsVbrqNpLU9+UE2E18GKjLKw+d3XbeYPqg1pzyQKGsrzbw+pPaw==",
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.2.tgz",
"integrity": "sha512-gjYDSKv3TrM2sLTOKBc5rH9ckje8Wrwgx1CxAPbN5N3Fm4prfi7NsJVWd1jklp7i5uSCVwhZS5qlhMXqLrpAIg==",
"hasInstallScript": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.4.0"
"@fortawesome/fontawesome-common-types": "6.4.2"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/free-brands-svg-icons": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.4.0.tgz",
"integrity": "sha512-qvxTCo0FQ5k2N+VCXb/PZQ+QMhqRVM4OORiO6MXdG6bKolIojGU/srQ1ptvKk0JTbRgaJOfL2qMqGvBEZG7Z6g==",
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.4.2.tgz",
"integrity": "sha512-LKOwJX0I7+mR/cvvf6qIiqcERbdnY+24zgpUSouySml+5w8B4BJOx8EhDR/FTKAu06W12fmUIcv6lzPSwYKGGg==",
"hasInstallScript": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.4.0"
"@fortawesome/fontawesome-common-types": "6.4.2"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/free-regular-svg-icons": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.4.0.tgz",
"integrity": "sha512-ZfycI7D0KWPZtf7wtMFnQxs8qjBXArRzczABuMQqecA/nXohquJ5J/RCR77PmY5qGWkxAZDxpnUFVXKwtY/jPw==",
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.4.2.tgz",
"integrity": "sha512-0+sIUWnkgTVVXVAPQmW4vxb9ZTHv0WstOa3rBx9iPxrrrDH6bNLsDYuwXF9b6fGm+iR7DKQvQshUH/FJm3ed9Q==",
"hasInstallScript": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.4.0"
"@fortawesome/fontawesome-common-types": "6.4.2"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/free-solid-svg-icons": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.0.tgz",
"integrity": "sha512-kutPeRGWm8V5dltFP1zGjQOEAzaLZj4StdQhWVZnfGFCvAPVvHh8qk5bRrU4KXnRRRNni5tKQI9PBAdI6MP8nQ==",
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.2.tgz",
"integrity": "sha512-sYwXurXUEQS32fZz9hVCUUv/xu49PEJEyUOsA51l6PU/qVgfbTb2glsTEaJngVVT8VqBATRIdh7XVgV1JF1LkA==",
"hasInstallScript": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.4.0"
"@fortawesome/fontawesome-common-types": "6.4.2"
},
"engines": {
"node": ">=6"
@@ -3415,29 +3408,6 @@
"react": ">=16.3"
}
},
"node_modules/@edx/frontend-component-header/node_modules/@reduxjs/toolkit": {
"version": "1.9.5",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz",
"integrity": "sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==",
"dependencies": {
"immer": "^9.0.21",
"redux": "^4.2.1",
"redux-thunk": "^2.4.2",
"reselect": "^4.1.8"
},
"peerDependencies": {
"react": "^16.9.0 || ^17.0.0 || ^18",
"react-redux": "^7.2.1 || ^8.0.2"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"react-redux": {
"optional": true
}
}
},
"node_modules/@edx/frontend-component-header/node_modules/axios-mock-adapter": {
"version": "1.21.5",
"resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.21.5.tgz",
@@ -3476,24 +3446,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@edx/frontend-component-header/node_modules/history": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
"dependencies": {
"@babel/runtime": "^7.1.2",
"loose-envify": "^1.2.0",
"resolve-pathname": "^3.0.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0",
"value-equal": "^1.0.1"
}
},
"node_modules/@edx/frontend-component-header/node_modules/isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="
},
"node_modules/@edx/frontend-component-header/node_modules/minimatch": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
@@ -3505,67 +3457,10 @@
"node": ">=10"
}
},
"node_modules/@edx/frontend-component-header/node_modules/path-to-regexp": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
"dependencies": {
"isarray": "0.0.1"
}
},
"node_modules/@edx/frontend-component-header/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/@edx/frontend-component-header/node_modules/react-router": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz",
"integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==",
"dependencies": {
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1",
"path-to-regexp": "^1.7.0",
"prop-types": "^15.6.2",
"react-is": "^16.6.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
},
"peerDependencies": {
"react": ">=15"
}
},
"node_modules/@edx/frontend-component-header/node_modules/react-router-dom": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz",
"integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==",
"dependencies": {
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
"react-router": "5.3.4",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
},
"peerDependencies": {
"react": ">=15"
}
},
"node_modules/@edx/frontend-component-header/node_modules/redux": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
"dependencies": {
"@babel/runtime": "^7.9.2"
}
},
"node_modules/@edx/frontend-lib-learning-assistant": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@edx/frontend-lib-learning-assistant/-/frontend-lib-learning-assistant-1.11.1.tgz",
"integrity": "sha512-vl93osyrEF5AxNwjNes643+AszUzpWGhVOm3ipGsvLyqgckIY6yQGD4WTwJnBLDBSTbStGjp5JLQvtlE0we/dQ==",
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-lib-learning-assistant/-/frontend-lib-learning-assistant-1.14.0.tgz",
"integrity": "sha512-IJMNtcrfVx/vqIbcBcuvXox+cK1g47uef2SF99bqEI9ewQzIw/XjPPundYzk6M8pYU+VTNqp78PBiQuPt2sSmQ==",
"dependencies": {
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
"@fortawesome/fontawesome-svg-core": "1.2.36",
@@ -3573,22 +3468,23 @@
"@fortawesome/free-regular-svg-icons": "5.15.4",
"@fortawesome/free-solid-svg-icons": "5.15.4",
"@fortawesome/react-fontawesome": "0.2.0",
"@optimizely/react-sdk": "^2.9.2",
"core-js": "3.31.1",
"prop-types": "15.8.1",
"react-markdown": "^8.0.5"
"react-markdown": "^8.0.5",
"uuid": "9.0.0"
},
"peerDependencies": {
"@edx/frontend-platform": "^4.3.0",
"@edx/frontend-platform": "^4.3.0 || ^5.0.0",
"@edx/paragon": "20.46.0",
"@reduxjs/toolkit": "1.8.1",
"react": "16.14.0 || ^17.0.0",
"react-dom": "16.14.0 || ^17.0.0",
"react-redux": "7.2.9",
"react-router": "5.2.1",
"react-router-dom": "5.3.0",
"react-router": "5.2.1 || ^6.0.0",
"react-router-dom": "5.3.0 || ^6.0.0",
"redux": "4.1.2",
"regenerator-runtime": "0.13.11",
"uuid": "9.0.0"
"regenerator-runtime": "0.13.11"
}
},
"node_modules/@edx/frontend-lib-learning-assistant/node_modules/@fortawesome/fontawesome-common-types": {
@@ -3635,9 +3531,9 @@
}
},
"node_modules/@edx/frontend-lib-special-exams": {
"version": "2.20.1",
"resolved": "https://registry.npmjs.org/@edx/frontend-lib-special-exams/-/frontend-lib-special-exams-2.20.1.tgz",
"integrity": "sha512-AF0ubUIklG25hjuhFdtHpSruNfzV8azkAUw+47RP+3ffRdTlJq8JcNT0BWHOsZL0+DKKE2xtwwaVL5gY31R1vg==",
"version": "2.23.2",
"resolved": "https://registry.npmjs.org/@edx/frontend-lib-special-exams/-/frontend-lib-special-exams-2.23.2.tgz",
"integrity": "sha512-sOdZ+qd5qZ3gnzWa53daJEhW+TtgFQNhE+M6+G4epias63NxlrtYOTTHbDXqh76CIE8EbTOB4ne3xtm0fLjEwg==",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "1.2.34",
"@fortawesome/free-brands-svg-icons": "5.11.2",
@@ -3648,15 +3544,15 @@
"eventemitter3": "^4.0.7"
},
"peerDependencies": {
"@edx/frontend-platform": "^4.2.0",
"@edx/frontend-platform": "^4.2.0 || ^5.0.0",
"@edx/paragon": "^19.4.1 || ^20.22.4",
"@reduxjs/toolkit": "^1.5.1",
"prop-types": "^15.7.2",
"react": "^16.14.0 || ^17.0.0",
"react-dom": "^16.14.0 || ^17.0.0",
"react-redux": "^7.1.3",
"react-router": "^5.1.2",
"react-router-dom": "^5.1.2",
"react-router": "^5.1.2 || ^6.0.0",
"react-router-dom": "^5.1.2 || ^6.0.0",
"redux": "^4.0.5"
}
},
@@ -3727,9 +3623,9 @@
}
},
"node_modules/@edx/frontend-platform": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-4.6.0.tgz",
"integrity": "sha512-NZ1I3BgUZl7bqvDwSnnL+LxqZOdOUGZU55KiwvknqiKU8RS5Lx9tc4arp+NcX1u58xy/Xbinv+mriSO6PPxQNQ==",
"version": "5.5.2",
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-5.5.2.tgz",
"integrity": "sha512-cbUvWcFL/mTc7eypBS/BnCojgWDcJCe3h3ffb3GD7F+Y4ysrFBJYf031qPcgmWNUrN30452dR7r1+sqE7uVvYA==",
"dependencies": {
"@cospired/i18n-iso-languages": "4.1.0",
"@formatjs/intl-pluralrules": "4.3.3",
@@ -3756,13 +3652,13 @@
"transifex-utils.js": "i18n/scripts/transifex-utils.js"
},
"peerDependencies": {
"@edx/frontend-build": ">= 8.1.0",
"@edx/paragon": ">= 10.0.0 < 21.0.0",
"@edx/frontend-build": ">= 8.1.0 || ^12.9.0-alpha.1",
"@edx/paragon": ">= 10.0.0 < 22.0.0",
"prop-types": "^15.7.2",
"react": "^16.9.0 || ^17.0.0",
"react-dom": "^16.9.0 || ^17.0.0",
"react-redux": "^7.1.1",
"react-router-dom": "^5.0.1",
"react-router-dom": "^6.0.0",
"redux": "^4.0.4"
}
},
@@ -3915,6 +3811,46 @@
"react": "^16.9.0 || ^17.0.0"
}
},
"node_modules/@edx/react-unit-test-utils/node_modules/@edx/frontend-platform": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-4.6.0.tgz",
"integrity": "sha512-NZ1I3BgUZl7bqvDwSnnL+LxqZOdOUGZU55KiwvknqiKU8RS5Lx9tc4arp+NcX1u58xy/Xbinv+mriSO6PPxQNQ==",
"dependencies": {
"@cospired/i18n-iso-languages": "4.1.0",
"@formatjs/intl-pluralrules": "4.3.3",
"@formatjs/intl-relativetimeformat": "10.0.1",
"axios": "0.27.2",
"axios-cache-interceptor": "0.10.7",
"form-urlencoded": "4.1.4",
"glob": "7.2.3",
"history": "4.10.1",
"i18n-iso-countries": "4.3.1",
"jwt-decode": "3.1.2",
"localforage": "1.10.0",
"localforage-memoryStorageDriver": "0.9.2",
"lodash.camelcase": "4.3.0",
"lodash.memoize": "4.1.2",
"lodash.merge": "4.6.2",
"lodash.snakecase": "4.1.1",
"pubsub-js": "1.9.4",
"react-intl": "^5.25.0",
"universal-cookie": "4.0.4"
},
"bin": {
"intl-imports.js": "i18n/scripts/intl-imports.js",
"transifex-utils.js": "i18n/scripts/transifex-utils.js"
},
"peerDependencies": {
"@edx/frontend-build": ">= 8.1.0",
"@edx/paragon": ">= 10.0.0 < 21.0.0",
"prop-types": "^15.7.2",
"react": "^16.9.0 || ^17.0.0",
"react-dom": "^16.9.0 || ^17.0.0",
"react-redux": "^7.1.1",
"react-router-dom": "^5.0.1",
"redux": "^4.0.4"
}
},
"node_modules/@edx/react-unit-test-utils/node_modules/@testing-library/dom": {
"version": "9.3.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.1.tgz",
@@ -3962,6 +3898,15 @@
}
}
},
"node_modules/@edx/react-unit-test-utils/node_modules/axios": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
"dependencies": {
"follow-redirects": "^1.14.9",
"form-data": "^4.0.0"
}
},
"node_modules/@edx/react-unit-test-utils/node_modules/core-js": {
"version": "3.6.5",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz",
@@ -3973,6 +3918,78 @@
"url": "https://opencollective.com/core-js"
}
},
"node_modules/@edx/react-unit-test-utils/node_modules/history": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
"dependencies": {
"@babel/runtime": "^7.1.2",
"loose-envify": "^1.2.0",
"resolve-pathname": "^3.0.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0",
"value-equal": "^1.0.1"
}
},
"node_modules/@edx/react-unit-test-utils/node_modules/isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
"peer": true
},
"node_modules/@edx/react-unit-test-utils/node_modules/path-to-regexp": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
"peer": true,
"dependencies": {
"isarray": "0.0.1"
}
},
"node_modules/@edx/react-unit-test-utils/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"peer": true
},
"node_modules/@edx/react-unit-test-utils/node_modules/react-router": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz",
"integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1",
"path-to-regexp": "^1.7.0",
"prop-types": "^15.6.2",
"react-is": "^16.6.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
},
"peerDependencies": {
"react": ">=15"
}
},
"node_modules/@edx/react-unit-test-utils/node_modules/react-router-dom": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz",
"integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
"react-router": "5.3.4",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
},
"peerDependencies": {
"react": ">=15"
}
},
"node_modules/@edx/reactifex": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@edx/reactifex/-/reactifex-2.2.0.tgz",
@@ -5561,6 +5578,135 @@
"node": ">= 8"
}
},
"node_modules/@optimizely/js-sdk-logging": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@optimizely/js-sdk-logging/-/js-sdk-logging-0.3.1.tgz",
"integrity": "sha512-K71Jf283FP0E4oXehcXTTM3gvgHZHr7FUrIsw//0mdJlotHJT4Nss4hE0CWPbBxO7LJAtwNnO+VIA/YOcO4vHg==",
"dependencies": {
"@optimizely/js-sdk-utils": "^0.4.0"
}
},
"node_modules/@optimizely/js-sdk-utils": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@optimizely/js-sdk-utils/-/js-sdk-utils-0.4.0.tgz",
"integrity": "sha512-QG2oytnITW+VKTJK+l0RxjaS5VrA6W+AZMzpeg4LCB4Rn4BEKtF+EcW/5S1fBDLAviGq/0TLpkjM3DlFkJ9/Gw==",
"dependencies": {
"uuid": "^3.3.2"
}
},
"node_modules/@optimizely/js-sdk-utils/node_modules/uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
"bin": {
"uuid": "bin/uuid"
}
},
"node_modules/@optimizely/optimizely-sdk": {
"version": "4.9.4",
"resolved": "https://registry.npmjs.org/@optimizely/optimizely-sdk/-/optimizely-sdk-4.9.4.tgz",
"integrity": "sha512-aYxndR6RahnLdX7SQR1YO2dklfNjbCGUUvRaYJZ50LIsDqhkAu426vxHwYO+V+QJxqipypPG5SVdG1m32AgDvw==",
"dependencies": {
"@optimizely/js-sdk-datafile-manager": "^0.9.5",
"@optimizely/js-sdk-event-processor": "^0.9.2",
"@optimizely/js-sdk-logging": "^0.3.1",
"json-schema": "^0.4.0",
"murmurhash": "0.0.2",
"uuid": "^3.3.2"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/@optimizely/optimizely-sdk/node_modules/@optimizely/js-sdk-datafile-manager": {
"version": "0.9.5",
"resolved": "https://registry.npmjs.org/@optimizely/js-sdk-datafile-manager/-/js-sdk-datafile-manager-0.9.5.tgz",
"integrity": "sha512-O4ujr1nBBAQBtx8YoKNpzzaEZgsE+aU4dxubT17ePqv/YVUWE+JOY21tSRrqZy/BlbbyzL+ElT8hrGB5ZzVoIQ==",
"dependencies": {
"@optimizely/js-sdk-logging": "^0.3.1",
"@optimizely/js-sdk-utils": "^0.4.0",
"decompress-response": "^4.2.1"
},
"engines": {
"node": ">=8.0.0"
},
"peerDependencies": {
"@react-native-async-storage/async-storage": "^1.2.0"
},
"peerDependenciesMeta": {
"@react-native-async-storage/async-storage": {
"optional": true
}
}
},
"node_modules/@optimizely/optimizely-sdk/node_modules/@optimizely/js-sdk-event-processor": {
"version": "0.9.5",
"resolved": "https://registry.npmjs.org/@optimizely/js-sdk-event-processor/-/js-sdk-event-processor-0.9.5.tgz",
"integrity": "sha512-g5zqAjJuexxgbNvn7dacFkQXQxH3+OtjELfmSswvhxP9EHkyNR0ZdQF/kBxFxr335F2/RRPvAJ9tQBPkwaBg8g==",
"dependencies": {
"@optimizely/js-sdk-logging": "^0.3.1",
"@optimizely/js-sdk-utils": "^0.4.0"
},
"peerDependencies": {
"@react-native-async-storage/async-storage": "^1.2.0",
"@react-native-community/netinfo": "5.9.4"
},
"peerDependenciesMeta": {
"@react-native-async-storage/async-storage": {
"optional": true
},
"@react-native-community/netinfo": {
"optional": true
}
}
},
"node_modules/@optimizely/optimizely-sdk/node_modules/decompress-response": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz",
"integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==",
"dependencies": {
"mimic-response": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@optimizely/optimizely-sdk/node_modules/mimic-response": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz",
"integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@optimizely/optimizely-sdk/node_modules/uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
"bin": {
"uuid": "bin/uuid"
}
},
"node_modules/@optimizely/react-sdk": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/@optimizely/react-sdk/-/react-sdk-2.9.2.tgz",
"integrity": "sha512-//OozC59dr5Lsss2H9Jnyb35FMTF8Z+CMFi89kVs1U1Fy1sKOXK7Web1hw18DBZctwKfbb8Sl+Yw7Pgmo3P2fA==",
"dependencies": {
"@optimizely/js-sdk-logging": "^0.3.1",
"@optimizely/optimizely-sdk": "^4.9.1",
"hoist-non-react-statics": "^3.3.0",
"prop-types": "^15.6.2",
"utility-types": "^2.1.0 || ^3.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@pact-foundation/pact": {
"version": "11.0.2",
"resolved": "https://registry.npmjs.org/@pact-foundation/pact/-/pact-11.0.2.tgz",
@@ -5692,9 +5838,9 @@
}
},
"node_modules/@polka/url": {
"version": "1.0.0-next.21",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz",
"integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g=="
"version": "1.0.0-next.23",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.23.tgz",
"integrity": "sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg=="
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
@@ -5727,6 +5873,14 @@
}
}
},
"node_modules/@remix-run/router": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.8.0.tgz",
"integrity": "sha512-mrfKqIHnSZRyIzBcanNJmVQELTnX+qagEDlcKO90RgRBVOZGSGvZKeDihTRfWcqoDn5N/NkUcwWTccnpN18Tfg==",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@restart/context": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@restart/context/-/context-2.1.4.tgz",
@@ -5860,9 +6014,9 @@
}
},
"node_modules/@svgr/babel-plugin-transform-react-native-svg": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.0.0.tgz",
"integrity": "sha512-UKrY3860AQICgH7g+6h2zkoxeVEPLYwX/uAjmqo4PIq2FIHppwhIqZstIyTz0ZtlwreKR41O3W3BzsBBiJV2Aw==",
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz",
"integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==",
"engines": {
"node": ">=14"
},
@@ -5890,9 +6044,9 @@
}
},
"node_modules/@svgr/babel-preset": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.0.0.tgz",
"integrity": "sha512-KLcjiZychInVrhs86OvcYPLTFu9L5XV2vj0XAaE1HwE3J3jLmIzRY8ttdeAg/iFyp8nhavJpafpDZTt+1LIpkQ==",
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz",
"integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==",
"dependencies": {
"@svgr/babel-plugin-add-jsx-attribute": "8.0.0",
"@svgr/babel-plugin-remove-jsx-attribute": "8.0.0",
@@ -5900,7 +6054,7 @@
"@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0",
"@svgr/babel-plugin-svg-dynamic-title": "8.0.0",
"@svgr/babel-plugin-svg-em-dimensions": "8.0.0",
"@svgr/babel-plugin-transform-react-native-svg": "8.0.0",
"@svgr/babel-plugin-transform-react-native-svg": "8.1.0",
"@svgr/babel-plugin-transform-svg-component": "8.0.0"
},
"engines": {
@@ -5915,12 +6069,12 @@
}
},
"node_modules/@svgr/core": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.0.0.tgz",
"integrity": "sha512-aJKtc+Pie/rFYsVH/unSkDaZGvEeylNv/s2cP+ta9/rYWxRVvoV/S4Qw65Kmrtah4CBK5PM6ISH9qUH7IJQCng==",
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz",
"integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==",
"dependencies": {
"@babel/core": "^7.21.3",
"@svgr/babel-preset": "8.0.0",
"@svgr/babel-preset": "8.1.0",
"camelcase": "^6.2.0",
"cosmiconfig": "^8.1.3",
"snake-case": "^3.0.4"
@@ -5950,12 +6104,12 @@
}
},
"node_modules/@svgr/plugin-jsx": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.0.1.tgz",
"integrity": "sha512-bfCFb+4ZsM3UuKP2t7KmDwn6YV8qVn9HIQJmau6xeQb/iV65Rpi7NBNBWA2hcCd4GKoCqG8hpaaDk5FDR0eH+g==",
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz",
"integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==",
"dependencies": {
"@babel/core": "^7.21.3",
"@svgr/babel-preset": "8.0.0",
"@svgr/babel-preset": "8.1.0",
"@svgr/hast-util-to-babel-ast": "8.0.0",
"svg-parser": "^2.0.4"
},
@@ -5971,9 +6125,9 @@
}
},
"node_modules/@svgr/plugin-svgo": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-8.0.1.tgz",
"integrity": "sha512-29OJ1QmJgnohQHDAgAuY2h21xWD6TZiXji+hnx+W635RiXTAlHTbjrZDktfqzkN0bOeQEtNe+xgq73/XeWFfSg==",
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-8.1.0.tgz",
"integrity": "sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA==",
"dependencies": {
"cosmiconfig": "^8.1.3",
"deepmerge": "^4.3.1",
@@ -5991,18 +6145,18 @@
}
},
"node_modules/@svgr/webpack": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-8.0.1.tgz",
"integrity": "sha512-zSoeKcbCmfMXjA11uDuCJb+1LWNb3vy6Qw/VHj0Nfcl3UuqwuoZWknHsBIhCWvi4wU9vPui3aq054qjVyZqY4A==",
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-8.1.0.tgz",
"integrity": "sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA==",
"dependencies": {
"@babel/core": "^7.21.3",
"@babel/plugin-transform-react-constant-elements": "^7.21.3",
"@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.21.0",
"@svgr/core": "8.0.0",
"@svgr/plugin-jsx": "8.0.1",
"@svgr/plugin-svgo": "8.0.1"
"@svgr/core": "8.1.0",
"@svgr/plugin-jsx": "8.1.0",
"@svgr/plugin-svgo": "8.1.0"
},
"engines": {
"node": ">=14"
@@ -7290,9 +7444,9 @@
}
},
"node_modules/autoprefixer": {
"version": "10.4.15",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.15.tgz",
"integrity": "sha512-KCuPB8ZCIqFdA4HwKXsvz7j6gvSDNhDP7WnUjBleRkKjPdvCmHFuQ77ocavI8FT6NdvlBnE2UFr2H4Mycn8Vew==",
"version": "10.4.16",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz",
"integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==",
"funding": [
{
"type": "opencollective",
@@ -7309,8 +7463,8 @@
],
"dependencies": {
"browserslist": "^4.21.10",
"caniuse-lite": "^1.0.30001520",
"fraction.js": "^4.2.0",
"caniuse-lite": "^1.0.30001538",
"fraction.js": "^4.3.6",
"normalize-range": "^0.1.2",
"picocolors": "^1.0.0",
"postcss-value-parser": "^4.2.0"
@@ -7345,9 +7499,9 @@
}
},
"node_modules/axios": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
"integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.5.0.tgz",
"integrity": "sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==",
"peer": true,
"dependencies": {
"follow-redirects": "^1.15.0",
@@ -8071,9 +8225,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001525",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001525.tgz",
"integrity": "sha512-/3z+wB4icFt3r0USMwxujAqRvaD/B7rvGTsKhbhSQErVrJvkZCLhgNLJxU8MevahQVH6hCU9FsHdNUFbiwmE7Q==",
"version": "1.0.30001543",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001543.tgz",
"integrity": "sha512-qxdO8KPWPQ+Zk6bvNpPeQIOH47qZSYdFZd6dXQzb2KzhnSXju4Kd7H1PkSJx6NICSMgo/IhRZRhhfPTHYpJUCA==",
"funding": [
{
"type": "opencollective",
@@ -11592,15 +11746,15 @@
}
},
"node_modules/fraction.js": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.1.tgz",
"integrity": "sha512-/KxoyCnPM0GwYI4NN0Iag38Tqt+od3/mLuguepLgCAKPn0ZhC544nssAW0tG2/00zXEYl9W+7hwAIpLHo6Oc7Q==",
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.6.tgz",
"integrity": "sha512-n2aZ9tNfYDwaHhvFTkhFErqOMIb8uyzSQ+vGJBjZyanAKZVbGUQ1sngfk9FdkBw7G26O7AgNjLcecLffD1c7eg==",
"engines": {
"node": "*"
},
"funding": {
"type": "patreon",
"url": "https://www.patreon.com/infusion"
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/fragment-cache": {
@@ -16519,9 +16673,9 @@
}
},
"node_modules/jquery": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.0.tgz",
"integrity": "sha512-umpJ0/k8X0MvD1ds0P9SfowREz2LenHsQaxSohMZ5OMNEU2r0tf8pdeEFTHMFxWVxKNyU9rTtK3CWzUCTKJUeQ==",
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
"integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==",
"peer": true
},
"node_modules/js-base64": {
@@ -16655,6 +16809,11 @@
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
},
"node_modules/json-schema": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -16900,6 +17059,11 @@
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="
},
"node_modules/lodash.escape": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz",
"integrity": "sha512-nXEOnb/jK9g0DYMr1/Xvq6l5xMD7GDG55+GSYIYmS0G4tBk/hURD4JR9WCavs04t33WmJx9kCyp9vJ+mr4BOUw=="
},
"node_modules/lodash.filter": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz",
@@ -16915,6 +17079,11 @@
"resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz",
"integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ=="
},
"node_modules/lodash.invokemap": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.invokemap/-/lodash.invokemap-4.6.0.tgz",
"integrity": "sha512-CfkycNtMqgUlfjfdh2BhKO/ZXrP8ePOX5lEU/g0R3ItJcnuxWDwokMGKx1hWcfOikmyOVx6X9IwWnDGlgKl61w=="
},
"node_modules/lodash.isfunction": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.8.tgz",
@@ -16965,6 +17134,11 @@
"resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz",
"integrity": "sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q=="
},
"node_modules/lodash.pullall": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.pullall/-/lodash.pullall-4.2.0.tgz",
"integrity": "sha512-VhqxBKH0ZxPpLhiu68YD1KnHmbhQJQctcipvmFnqIBDYzcIHzf3Zpu0tpeOKtR4x76p9yohc506eGdOjTmyIBg=="
},
"node_modules/lodash.reduce": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz",
@@ -17763,20 +17937,6 @@
"node": ">=4"
}
},
"node_modules/mini-create-react-context": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz",
"integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==",
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
"dependencies": {
"@babel/runtime": "^7.12.1",
"tiny-warning": "^1.0.3"
},
"peerDependencies": {
"prop-types": "^15.0.0",
"react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
}
},
"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",
@@ -17883,6 +18043,14 @@
"multicast-dns": "cli.js"
}
},
"node_modules/murmurhash": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/murmurhash/-/murmurhash-0.0.2.tgz",
"integrity": "sha512-LKlwdZKWzvCQpMszb2HO5leJ7P9T4m5XuDKku8bM0uElrzqK9cn0+iozwQS8jO4SNjrp4w7olalgd8WgsIjhWA==",
"engines": {
"node": "*"
}
},
"node_modules/mute-stream": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
@@ -19018,9 +19186,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.28",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.28.tgz",
"integrity": "sha512-Z7V5j0cq8oEKyejIKfpD8b4eBy9cwW2JWPk0+fB1HOAMsfHbnAXLLS+PfVWlzMSLQaWttKDt607I0XHmpE67Vw==",
"version": "8.4.30",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.30.tgz",
"integrity": "sha512-7ZEao1g4kd68l97aWG/etQKPKq07us0ieSZ2TnFDk11i0ZfDW2AwKHYU8qv4MZKqN2fdBfg+7q0ES06UA73C1g==",
"funding": [
{
"type": "opencollective",
@@ -19092,9 +19260,9 @@
}
},
"node_modules/postcss-custom-media": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-10.0.0.tgz",
"integrity": "sha512-NxDn7C6GJ7X8TsWOa8MbCdq9rLERRLcPfQSp856k1jzMreL8X9M6iWk35JjPRIb9IfRnVohmxAylDRx7n4Rv4g==",
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-10.0.1.tgz",
"integrity": "sha512-fil7cosvzlIAYmZJPtNFcTH0Er7a3GveEK4q5Y/L24eWQHmiw8Fv/E5DMkVpdbNjkGzJxrvowOSt/Il9HZ06VQ==",
"funding": [
{
"type": "github",
@@ -19106,10 +19274,10 @@
}
],
"dependencies": {
"@csstools/cascade-layer-name-parser": "^1.0.3",
"@csstools/css-parser-algorithms": "^2.3.0",
"@csstools/css-tokenizer": "^2.1.1",
"@csstools/media-query-list-parser": "^2.1.2"
"@csstools/cascade-layer-name-parser": "^1.0.4",
"@csstools/css-parser-algorithms": "^2.3.1",
"@csstools/css-tokenizer": "^2.2.0",
"@csstools/media-query-list-parser": "^2.1.4"
},
"engines": {
"node": "^14 || ^16 || >=18"
@@ -19529,9 +19697,9 @@
}
},
"node_modules/postcss-rtlcss": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/postcss-rtlcss/-/postcss-rtlcss-4.0.7.tgz",
"integrity": "sha512-HUh9rtPOrAiPs0uYTebWixSN3PEO6evQ+bj6OmYG59v+pUSPmsfvbIm2VAySvTaHNbURez5beMkCXU/sQz+w0A==",
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/postcss-rtlcss/-/postcss-rtlcss-4.0.8.tgz",
"integrity": "sha512-CR2sY889PHnX6K8rjW9FG4Qvm9UJsIekDakMtEYGH3zgFp9XADMeaKcA0hPOmkClNh0jWbkaPBm0jZ6fHmqkJQ==",
"dependencies": {
"rtlcss": "4.1.0"
},
@@ -20497,86 +20665,35 @@
}
},
"node_modules/react-router": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.1.tgz",
"integrity": "sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==",
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.15.0.tgz",
"integrity": "sha512-NIytlzvzLwJkCQj2HLefmeakxxWHWAP+02EGqWEZy+DgfHHKQMUoBBjUQLOtFInBMhWtb3hiUy6MfFgwLjXhqg==",
"dependencies": {
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1",
"mini-create-react-context": "^0.4.0",
"path-to-regexp": "^1.7.0",
"prop-types": "^15.6.2",
"react-is": "^16.6.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
"@remix-run/router": "1.8.0"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"react": ">=15"
"react": ">=16.8"
}
},
"node_modules/react-router-dom": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.0.tgz",
"integrity": "sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ==",
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.15.0.tgz",
"integrity": "sha512-aR42t0fs7brintwBGAv2+mGlCtgtFQeOzK0BM1/OiqEzRejOZtpMZepvgkscpMUnKb8YO84G7s3LsHnnDNonbQ==",
"dependencies": {
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
"react-router": "5.2.1",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
"@remix-run/router": "1.8.0",
"react-router": "6.15.0"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"react": ">=15"
"react": ">=16.8",
"react-dom": ">=16.8"
}
},
"node_modules/react-router-dom/node_modules/history": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
"dependencies": {
"@babel/runtime": "^7.1.2",
"loose-envify": "^1.2.0",
"resolve-pathname": "^3.0.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0",
"value-equal": "^1.0.1"
}
},
"node_modules/react-router/node_modules/history": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
"dependencies": {
"@babel/runtime": "^7.1.2",
"loose-envify": "^1.2.0",
"resolve-pathname": "^3.0.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0",
"value-equal": "^1.0.1"
}
},
"node_modules/react-router/node_modules/isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="
},
"node_modules/react-router/node_modules/path-to-regexp": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
"dependencies": {
"isarray": "0.0.1"
}
},
"node_modules/react-router/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/react-shallow-renderer": {
"version": "16.15.0",
"resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz",
@@ -21206,6 +21323,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/rosie/-/rosie-2.1.0.tgz",
"integrity": "sha512-Dbzdc+prLXZuB/suRptDnBUY29SdGvND3bLg6cll8n7PNqzuyCxSlRfrkn8PqjS9n4QVsiM7RCvxCkKAkTQRjA==",
"dev": true,
"engines": {
"node": ">=10"
}
@@ -21969,9 +22087,9 @@
"integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA=="
},
"node_modules/sharp": {
"version": "0.32.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.5.tgz",
"integrity": "sha512-0dap3iysgDkNaPOaOL4X/0akdu0ma62GcdC2NBQ+93eqpePdDdr2/LM0sFdDSMmN7yS+odyZtPsb7tx/cYBKnQ==",
"version": "0.32.6",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz",
"integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==",
"hasInstallScript": true,
"dependencies": {
"color": "^4.2.3",
@@ -22133,13 +22251,13 @@
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
},
"node_modules/sirv": {
"version": "1.0.19",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.19.tgz",
"integrity": "sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==",
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.3.tgz",
"integrity": "sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA==",
"dependencies": {
"@polka/url": "^1.0.0-next.20",
"mrmime": "^1.0.0",
"totalist": "^1.0.0"
"totalist": "^3.0.0"
},
"engines": {
"node": ">= 10"
@@ -23443,11 +23561,6 @@
"resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="
},
"node_modules/timeago.js": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/timeago.js/-/timeago.js-4.0.2.tgz",
"integrity": "sha512-a7wPxPdVlQL7lqvitHGGRsofhdwtkoSXPGATFuSOA2i1ZNQEPLrGnj68vOp2sOJTCFAQVXPeNMX/GctBaO9L2w=="
},
"node_modules/timers-ext": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz",
@@ -23554,9 +23667,9 @@
}
},
"node_modules/totalist": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz",
"integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==",
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
"engines": {
"node": ">=6"
}
@@ -24227,6 +24340,14 @@
"resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz",
"integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA=="
},
"node_modules/utility-types": {
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz",
"integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==",
"engines": {
"node": ">= 4"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@@ -24443,19 +24564,26 @@
}
},
"node_modules/webpack-bundle-analyzer": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.9.0.tgz",
"integrity": "sha512-+bXGmO1LyiNx0i9enBu3H8mv42sj/BJWhZNFwjz92tVnBa9J3JMGo2an2IXlEleoDOPn/Hofl5hr/xCpObUDtw==",
"version": "4.9.1",
"resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.9.1.tgz",
"integrity": "sha512-jnd6EoYrf9yMxCyYDPj8eutJvtjQNp8PHmni/e/ulydHBWhT5J3menXt3HEkScsu9YqMAcG4CfFjs3rj5pVU1w==",
"dependencies": {
"@discoveryjs/json-ext": "0.5.7",
"acorn": "^8.0.4",
"acorn-walk": "^8.0.0",
"chalk": "^4.1.0",
"commander": "^7.2.0",
"escape-string-regexp": "^4.0.0",
"gzip-size": "^6.0.0",
"lodash": "^4.17.20",
"is-plain-object": "^5.0.0",
"lodash.debounce": "^4.0.8",
"lodash.escape": "^4.0.1",
"lodash.flatten": "^4.4.0",
"lodash.invokemap": "^4.6.0",
"lodash.pullall": "^4.2.0",
"lodash.uniqby": "^4.7.0",
"opener": "^1.5.2",
"sirv": "^1.0.7",
"picocolors": "^1.0.0",
"sirv": "^2.0.3",
"ws": "^7.3.1"
},
"bin": {
@@ -24473,6 +24601,14 @@
"node": ">= 10"
}
},
"node_modules/webpack-bundle-analyzer/node_modules/is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/webpack-cli": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz",

View File

@@ -30,11 +30,11 @@
},
"dependencies": {
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
"@edx/frontend-component-footer": "12.1.2",
"@edx/frontend-component-header": "4.4.4",
"@edx/frontend-lib-learning-assistant": "^1.11.1",
"@edx/frontend-lib-special-exams": "2.20.1",
"@edx/frontend-platform": "4.6.0",
"@edx/frontend-component-footer": "12.2.1",
"@edx/frontend-component-header": "4.6.0",
"@edx/frontend-lib-learning-assistant": "^1.14.0",
"@edx/frontend-lib-special-exams": "2.23.2",
"@edx/frontend-platform": "5.5.2",
"@edx/paragon": "20.46.0",
"@edx/react-unit-test-utils": "npm:@edx/react-unit-test-utils@1.7.0",
"@fortawesome/fontawesome-svg-core": "1.3.0",
@@ -55,8 +55,8 @@
"react-dom": "17.0.2",
"react-helmet": "6.1.0",
"react-redux": "7.2.9",
"react-router": "5.2.1",
"react-router-dom": "5.3.0",
"react-router": "6.15.0",
"react-router-dom": "6.15.0",
"react-share": "4.4.1",
"redux": "4.1.2",
"regenerator-runtime": "0.13.11",

33
src/constants.js Normal file
View File

@@ -0,0 +1,33 @@
export const DECODE_ROUTES = {
ACCESS_DENIED: '/course/:courseId/access-denied',
HOME: '/course/:courseId/home',
LIVE: '/course/:courseId/live',
DATES: '/course/:courseId/dates',
DISCUSSION: '/course/:courseId/discussion/:path/*',
PROGRESS: [
'/course/:courseId/progress/:targetUserId/',
'/course/:courseId/progress',
],
COURSE_END: '/course/:courseId/course-end',
COURSEWARE: [
'/course/:courseId/:sequenceId/:unitId',
'/course/:courseId/:sequenceId',
'/course/:courseId',
],
REDIRECT_HOME: 'home/:courseId',
REDIRECT_SURVEY: 'survey/:courseId',
};
export const ROUTES = {
UNSUBSCRIBE: '/goal-unsubscribe/:token',
REDIRECT: '/redirect/*',
DASHBOARD: 'dashboard',
CONSENT: 'consent',
};
export const REDIRECT_MODES = {
DASHBOARD_REDIRECT: 'dashboard-redirect',
CONSENT_REDIRECT: 'consent-redirect',
HOME_REDIRECT: 'home-redirect',
SURVEY_REDIRECT: 'survey-redirect',
};

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { Route } from 'react-router';
import { Routes, Route } from 'react-router-dom';
import MockAdapter from 'axios-mock-adapter';
import { Factory } from 'rosie';
import { getConfig, history } from '@edx/frontend-platform';
@@ -32,11 +32,16 @@ describe('DatesTab', () => {
component = (
<AppProvider store={store}>
<UserMessagesProvider>
<Route path="/course/:courseId/dates">
<TabContainer tab="dates" fetch={fetchDatesTab} slice="courseHome">
<DatesTab />
</TabContainer>
</Route>
<Routes>
<Route
path="/course/:courseId/dates"
element={(
<TabContainer tab="dates" fetch={fetchDatesTab} slice="courseHome">
<DatesTab />
</TabContainer>
)}
/>
</Routes>
</UserMessagesProvider>
</AppProvider>
);

View File

@@ -2,21 +2,20 @@ import { getConfig } from '@edx/frontend-platform';
import { injectIntl } from '@edx/frontend-platform/i18n';
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import { generatePath, useHistory } from 'react-router';
import { useParams } from 'react-router-dom';
import { useParams, generatePath, useNavigate } from 'react-router-dom';
import { useIFrameHeight, useIFramePluginEvents } from '../../generic/hooks';
const DiscussionTab = () => {
const { courseId } = useSelector(state => state.courseHome);
const { path } = useParams();
const [originalPath] = useState(path);
const history = useHistory();
const navigate = useNavigate();
const [, iFrameHeight] = useIFrameHeight();
useIFramePluginEvents({
'discussions.navigate': (payload) => {
const basePath = generatePath('/course/:courseId/discussion', { courseId });
history.push(`${basePath}/${payload.path}`);
navigate(`${basePath}/${payload.path}`);
},
});
const discussionsUrl = `${getConfig().DISCUSSIONS_MFE_BASE_URL}/${courseId}/${originalPath}`;

View File

@@ -4,7 +4,7 @@ import { AppProvider } from '@edx/frontend-platform/react';
import { render } from '@testing-library/react';
import MockAdapter from 'axios-mock-adapter';
import React from 'react';
import { Route } from 'react-router';
import { Route, Routes } from 'react-router-dom';
import { Factory } from 'rosie';
import { UserMessagesProvider } from '../../generic/user-messages';
import {
@@ -30,11 +30,16 @@ describe('DiscussionTab', () => {
component = (
<AppProvider store={store}>
<UserMessagesProvider>
<Route path="/course/:courseId/discussion">
<TabContainer tab="discussion" fetch={fetchDiscussionTab} slice="courseHome">
<DiscussionTab />
</TabContainer>
</Route>
<Routes>
<Route
path="/course/:courseId/discussion"
element={(
<TabContainer tab="discussion" fetch={fetchDiscussionTab} slice="courseHome">
<DiscussionTab />
</TabContainer>
)}
/>
</Routes>
</UserMessagesProvider>
</AppProvider>
);

View File

@@ -1,7 +1,9 @@
import React from 'react';
import { Route } from 'react-router';
import {
MemoryRouter, Route, Routes,
} from 'react-router-dom';
import MockAdapter from 'axios-mock-adapter';
import { getConfig, history } from '@edx/frontend-platform';
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { AppProvider } from '@edx/frontend-platform/react';
import { render, screen } from '@testing-library/react';
@@ -24,13 +26,16 @@ describe('GoalUnsubscribe', () => {
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
store = initializeStore();
component = (
<AppProvider store={store}>
<AppProvider store={store} wrapWithRouter={false}>
<UserMessagesProvider>
<Route path="/goal-unsubscribe/:token" component={GoalUnsubscribe} />
<MemoryRouter initialEntries={['/goal-unsubscribe/TOKEN']}>
<Routes>
<Route path="/goal-unsubscribe/:token" element={<GoalUnsubscribe />} />
</Routes>
</MemoryRouter>
</UserMessagesProvider>
</AppProvider>
);
history.push('/goal-unsubscribe/TOKEN'); // so we can pull token from url
});
it('starts with a spinner', () => {

View File

@@ -1,9 +1,8 @@
import React, { useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { useLocation, useNavigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { history } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button } from '@edx/paragon';
import { AlertList } from '../../generic/user-messages';
@@ -67,6 +66,7 @@ const OutlineTab = ({ intl }) => {
} = useModel('coursewareMeta', courseId);
const [expandAll, setExpandAll] = useState(false);
const navigate = useNavigate();
const eventProperties = {
org_key: org,
@@ -115,8 +115,10 @@ const OutlineTab = ({ intl }) => {
// Deleting the course_start query param as it only needs to be set once
// whenever passed in query params.
currentParams.delete('start_course');
history.replace({
search: currentParams.toString(),
navigate({
pathname: location.pathname,
search: `?${currentParams.toString()}`,
replace: true,
});
}
}, [location.search]);

View File

@@ -1,7 +1,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { history } from '@edx/frontend-platform';
import { createSelector } from '@reduxjs/toolkit';
import { defaultMemoize as memoize } from 'reselect';
@@ -17,45 +16,46 @@ import { TabPage } from '../tab-page';
import Course from './course';
import { handleNextSectionCelebration } from './course/celebration';
import withParamsAndNavigation from './utils';
// Look at where this is called in componentDidUpdate for more info about its usage
const checkResumeRedirect = memoize((courseStatus, courseId, sequenceId, firstSequenceId) => {
const checkResumeRedirect = memoize((courseStatus, courseId, sequenceId, firstSequenceId, navigate) => {
if (courseStatus === 'loaded' && !sequenceId) {
// Note that getResumeBlock is just an API call, not a redux thunk.
getResumeBlock(courseId).then((data) => {
// This is a replace because we don't want this change saved in the browser's history.
if (data.sectionId && data.unitId) {
history.replace(`/course/${courseId}/${data.sectionId}/${data.unitId}`);
navigate(`/course/${courseId}/${data.sectionId}/${data.unitId}`, { replace: true });
} else if (firstSequenceId) {
history.replace(`/course/${courseId}/${firstSequenceId}`);
navigate(`/course/${courseId}/${firstSequenceId}`, { replace: true });
}
});
}
});
// Look at where this is called in componentDidUpdate for more info about its usage
const checkSectionUnitToUnitRedirect = memoize((courseStatus, courseId, sequenceStatus, section, unitId) => {
const checkSectionUnitToUnitRedirect = memoize((courseStatus, courseId, sequenceStatus, section, unitId, navigate) => {
if (courseStatus === 'loaded' && sequenceStatus === 'failed' && section && unitId) {
history.replace(`/course/${courseId}/${unitId}`);
navigate(`/course/${courseId}/${unitId}`, { replace: true });
}
});
// Look at where this is called in componentDidUpdate for more info about its usage
const checkSectionToSequenceRedirect = memoize((courseStatus, courseId, sequenceStatus, section, unitId) => {
const checkSectionToSequenceRedirect = memoize((courseStatus, courseId, sequenceStatus, section, unitId, navigate) => {
if (courseStatus === 'loaded' && sequenceStatus === 'failed' && section && !unitId) {
// If the section is non-empty, redirect to its first sequence.
if (section.sequenceIds && section.sequenceIds[0]) {
history.replace(`/course/${courseId}/${section.sequenceIds[0]}`);
navigate(`/course/${courseId}/${section.sequenceIds[0]}`, { replace: true });
// Otherwise, just go to the course root, letting the resume redirect take care of things.
} else {
history.replace(`/course/${courseId}`);
navigate(`/course/${courseId}`, { replace: true });
}
}
});
// Look at where this is called in componentDidUpdate for more info about its usage
const checkUnitToSequenceUnitRedirect = memoize(
(courseStatus, courseId, sequenceStatus, sequenceMightBeUnit, sequenceId, section, routeUnitId) => {
(courseStatus, courseId, sequenceStatus, sequenceMightBeUnit, sequenceId, section, routeUnitId, navigate) => {
if (courseStatus === 'loaded' && sequenceStatus === 'failed' && !section && !routeUnitId) {
if (sequenceMightBeUnit) {
// If the sequence failed to load as a sequence, but it is marked as a possible unit, then
@@ -64,60 +64,62 @@ const checkUnitToSequenceUnitRedirect = memoize(
getSequenceForUnitDeprecated(courseId, unitId).then(
parentId => {
if (parentId) {
history.replace(`/course/${courseId}/${parentId}/${unitId}`);
navigate(`/course/${courseId}/${parentId}/${unitId}`, { replace: true });
} else {
history.replace(`/course/${courseId}`);
navigate(`/course/${courseId}`, { replace: true });
}
},
() => { // error case
history.replace(`/course/${courseId}`);
navigate(`/course/${courseId}`, { replace: true });
},
);
} else {
// Invalid sequence that isn't a unit either. Redirect up to main course.
history.replace(`/course/${courseId}`);
navigate(`/course/${courseId}`, { replace: true });
}
}
},
);
// Look at where this is called in componentDidUpdate for more info about its usage
const checkSequenceToSequenceUnitRedirect = memoize((courseId, sequenceStatus, sequence, unitId) => {
const checkSequenceToSequenceUnitRedirect = memoize((courseId, sequenceStatus, sequence, unitId, navigate) => {
if (sequenceStatus === 'loaded' && sequence.id && !unitId) {
if (sequence.unitIds !== undefined && sequence.unitIds.length > 0) {
const nextUnitId = sequence.unitIds[sequence.activeUnitIndex];
// This is a replace because we don't want this change saved in the browser's history.
history.replace(`/course/${courseId}/${sequence.id}/${nextUnitId}`);
navigate(`/course/${courseId}/${sequence.id}/${nextUnitId}`, { replace: true });
}
}
});
// Look at where this is called in componentDidUpdate for more info about its usage
const checkSequenceUnitMarkerToSequenceUnitRedirect = memoize((courseId, sequenceStatus, sequence, unitId) => {
if (sequenceStatus !== 'loaded' || !sequence.id) {
return;
}
const hasUnits = sequence.unitIds?.length > 0;
if (unitId === 'first') {
if (hasUnits) {
const firstUnitId = sequence.unitIds[0];
history.replace(`/course/${courseId}/${sequence.id}/${firstUnitId}`);
} else {
// No units... go to general sequence page
history.replace(`/course/${courseId}/${sequence.id}`);
const checkSequenceUnitMarkerToSequenceUnitRedirect = memoize(
(courseId, sequenceStatus, sequence, unitId, navigate) => {
if (sequenceStatus !== 'loaded' || !sequence.id) {
return;
}
} else if (unitId === 'last') {
if (hasUnits) {
const lastUnitId = sequence.unitIds[sequence.unitIds.length - 1];
history.replace(`/course/${courseId}/${sequence.id}/${lastUnitId}`);
} else {
const hasUnits = sequence.unitIds?.length > 0;
if (unitId === 'first') {
if (hasUnits) {
const firstUnitId = sequence.unitIds[0];
navigate(`/course/${courseId}/${sequence.id}/${firstUnitId}`, { replace: true });
} else {
// No units... go to general sequence page
history.replace(`/course/${courseId}/${sequence.id}`);
navigate(`/course/${courseId}/${sequence.id}`, { replace: true });
}
} else if (unitId === 'last') {
if (hasUnits) {
const lastUnitId = sequence.unitIds[sequence.unitIds.length - 1];
navigate(`/course/${courseId}/${sequence.id}/${lastUnitId}`, { replace: true });
} else {
// No units... go to general sequence page
navigate(`/course/${courseId}/${sequence.id}`, { replace: true });
}
}
}
});
},
);
class CoursewareContainer extends Component {
checkSaveSequencePosition = memoize((unitId) => {
@@ -145,12 +147,8 @@ class CoursewareContainer extends Component {
componentDidMount() {
const {
match: {
params: {
courseId: routeCourseId,
sequenceId: routeSequenceId,
},
},
routeCourseId,
routeSequenceId,
} = this.props;
// Load data whenever the course or sequence ID changes.
this.checkFetchCourse(routeCourseId);
@@ -167,13 +165,10 @@ class CoursewareContainer extends Component {
sequence,
firstSequenceId,
sectionViaSequenceId,
match: {
params: {
courseId: routeCourseId,
sequenceId: routeSequenceId,
unitId: routeUnitId,
},
},
routeCourseId,
routeSequenceId,
routeUnitId,
navigate,
} = this.props;
// Load data whenever the course or sequence ID changes.
@@ -202,7 +197,7 @@ class CoursewareContainer extends Component {
// Check resume redirect:
// /course/:courseId -> /course/:courseId/:sequenceId/:unitId
// based on sequence/unit where user was last active.
checkResumeRedirect(courseStatus, courseId, sequenceId, firstSequenceId);
checkResumeRedirect(courseStatus, courseId, sequenceId, firstSequenceId, navigate);
// Check section-unit to unit redirect:
// /course/:courseId/:sectionId/:unitId -> /course/:courseId/:unitId
@@ -215,42 +210,40 @@ class CoursewareContainer extends Component {
// otherwise, we could get stuck in a redirect loop, since a sequence that failed to load
// would endlessly redirect to itself through `checkSectionUnitToUnitRedirect`
// and `checkUnitToSequenceUnitRedirect`.
checkSectionUnitToUnitRedirect(courseStatus, courseId, sequenceStatus, sectionViaSequenceId, routeUnitId);
checkSectionUnitToUnitRedirect(courseStatus, courseId, sequenceStatus, sectionViaSequenceId, routeUnitId, navigate);
// Check section to sequence redirect:
// /course/:courseId/:sectionId -> /course/:courseId/:sequenceId
// by redirecting to the first sequence within the section.
checkSectionToSequenceRedirect(courseStatus, courseId, sequenceStatus, sectionViaSequenceId, routeUnitId);
checkSectionToSequenceRedirect(courseStatus, courseId, sequenceStatus, sectionViaSequenceId, routeUnitId, navigate);
// Check unit to sequence-unit redirect:
// /course/:courseId/:unitId -> /course/:courseId/:sequenceId/:unitId
// by filling in the ID of the parent sequence of :unitId.
checkUnitToSequenceUnitRedirect((
courseStatus, courseId, sequenceStatus, sequenceMightBeUnit, sequenceId, sectionViaSequenceId, routeUnitId
courseStatus, courseId, sequenceStatus, sequenceMightBeUnit,
sequenceId, sectionViaSequenceId, routeUnitId, navigate
));
// Check sequence to sequence-unit redirect:
// /course/:courseId/:sequenceId -> /course/:courseId/:sequenceId/:unitId
// by filling in the ID the most-recently-active unit in the sequence, OR
// the ID of the first unit the sequence if none is active.
checkSequenceToSequenceUnitRedirect(courseId, sequenceStatus, sequence, routeUnitId);
checkSequenceToSequenceUnitRedirect(courseId, sequenceStatus, sequence, routeUnitId, navigate);
// Check sequence-unit marker to sequence-unit redirect:
// /course/:courseId/:sequenceId/first -> /course/:courseId/:sequenceId/:unitId
// /course/:courseId/:sequenceId/last -> /course/:courseId/:sequenceId/:unitId
// by filling in the ID the first or last unit in the sequence.
// "Sequence unit marker" is an invented term used only in this component.
checkSequenceUnitMarkerToSequenceUnitRedirect(courseId, sequenceStatus, sequence, routeUnitId);
checkSequenceUnitMarkerToSequenceUnitRedirect(courseId, sequenceStatus, sequence, routeUnitId, navigate);
}
handleUnitNavigationClick = () => {
const {
courseId, sequenceId,
match: {
params: {
unitId: routeUnitId,
},
},
courseId,
sequenceId,
routeUnitId,
} = this.props;
this.props.checkBlockCompletion(courseId, sequenceId, routeUnitId);
@@ -279,11 +272,7 @@ class CoursewareContainer extends Component {
courseStatus,
courseId,
sequenceId,
match: {
params: {
unitId: routeUnitId,
},
},
routeUnitId,
} = this.props;
return (
@@ -326,13 +315,9 @@ const courseShape = PropTypes.shape({
});
CoursewareContainer.propTypes = {
match: PropTypes.shape({
params: PropTypes.shape({
courseId: PropTypes.string.isRequired,
sequenceId: PropTypes.string,
unitId: PropTypes.string,
}).isRequired,
}).isRequired,
routeCourseId: PropTypes.string.isRequired,
routeSequenceId: PropTypes.string,
routeUnitId: PropTypes.string,
courseId: PropTypes.string,
sequenceId: PropTypes.string,
firstSequenceId: PropTypes.string,
@@ -348,11 +333,14 @@ CoursewareContainer.propTypes = {
checkBlockCompletion: PropTypes.func.isRequired,
fetchCourse: PropTypes.func.isRequired,
fetchSequence: PropTypes.func.isRequired,
navigate: PropTypes.func.isRequired,
};
CoursewareContainer.defaultProps = {
courseId: null,
sequenceId: null,
routeSequenceId: null,
routeUnitId: null,
firstSequenceId: null,
nextSequence: null,
previousSequence: null,
@@ -467,4 +455,4 @@ export default connect(mapStateToProps, {
saveSequencePosition,
fetchCourse,
fetchSequence,
})(CoursewareContainer);
})(withParamsAndNavigation(CoursewareContainer));

View File

@@ -5,13 +5,16 @@ import { waitForElementToBeRemoved, fireEvent } from '@testing-library/dom';
import '@testing-library/jest-dom/extend-expect';
import { render, screen } from '@testing-library/react';
import React from 'react';
import { Route, Switch } from 'react-router';
import {
BrowserRouter, MemoryRouter, Route, Routes,
} from 'react-router-dom';
import { Factory } from 'rosie';
import MockAdapter from 'axios-mock-adapter';
import { UserMessagesProvider } from '../generic/user-messages';
import tabMessages from '../tab-page/messages';
import { initializeMockApp, waitFor } from '../setupTest';
import { DECODE_ROUTES } from '../constants';
import CoursewareContainer from './CoursewareContainer';
import { buildSimpleCourseBlocks, buildBinaryCourseBlocks } from '../shared/data/__factories__/courseBlocks.factory';
@@ -80,18 +83,16 @@ describe('CoursewareContainer', () => {
store = initializeStore();
component = (
<AppProvider store={store}>
<AppProvider store={store} wrapWithRouter={false}>
<UserMessagesProvider>
<Switch>
<Route
path={[
'/course/:courseId/:sequenceId/:unitId',
'/course/:courseId/:sequenceId',
'/course/:courseId',
]}
component={CoursewareContainer}
/>
</Switch>
<Routes>
{DECODE_ROUTES.COURSEWARE.map((route) => (
<Route
path={route}
element={<CoursewareContainer />}
/>
))}
</Routes>
</UserMessagesProvider>
</AppProvider>
);
@@ -151,7 +152,7 @@ describe('CoursewareContainer', () => {
}
async function loadContainer() {
const { container } = render(component);
const { container } = render(<BrowserRouter>{component}</BrowserRouter>);
// Wait for the page spinner to be removed, such that we can wait for our main
// content to load before making any assertions.
await waitForElementToBeRemoved(screen.getByRole('status'));
@@ -160,7 +161,7 @@ describe('CoursewareContainer', () => {
it('should initialize to show a spinner', () => {
history.push('/course/abc123');
render(component);
render(<MemoryRouter initialEntries={['/course/abc123']}>{component}</MemoryRouter>);
const spinner = screen.getByRole('status');

View File

@@ -1,56 +1,44 @@
import React from 'react';
import { Switch, useRouteMatch } from 'react-router';
import { getConfig } from '@edx/frontend-platform';
import { Routes, Route } from 'react-router-dom';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { PageRoute } from '@edx/frontend-platform/react';
import { PageWrap } from '@edx/frontend-platform/react';
import queryString from 'query-string';
import PageLoading from '../generic/PageLoading';
import DecodePageRoute from '../decode-page-route';
import { DECODE_ROUTES, REDIRECT_MODES, ROUTES } from '../constants';
import RedirectPage from './RedirectPage';
const CoursewareRedirectLandingPage = () => {
const { path } = useRouteMatch();
return (
<div className="flex-grow-1">
<PageLoading srMessage={(
<FormattedMessage
id="learn.redirect.interstitial.message"
description="The screen-reader message when a page is about to redirect"
defaultMessage="Redirecting..."
/>
)}
const CoursewareRedirectLandingPage = () => (
<div className="flex-grow-1">
<PageLoading srMessage={(
<FormattedMessage
id="learn.redirect.interstitial.message"
description="The screen-reader message when a page is about to redirect"
defaultMessage="Redirecting..."
/>
)}
/>
<Switch>
<DecodePageRoute
path={`${path}/survey/:courseId`}
render={({ match }) => {
global.location.assign(`${getConfig().LMS_BASE_URL}/courses/${match.params.courseId}/survey`);
}}
/>
<PageRoute
path={`${path}/dashboard`}
render={({ location }) => {
global.location.assign(`${getConfig().LMS_BASE_URL}/dashboard${location.search}`);
}}
/>
<PageRoute
path={`${path}/consent/`}
render={({ location }) => {
const { consentPath } = queryString.parse(location.search);
global.location.assign(`${getConfig().LMS_BASE_URL}${consentPath}`);
}}
/>
<DecodePageRoute
path={`${path}/home/:courseId`}
render={({ match }) => {
global.location.assign(`/course/${match.params.courseId}/home`);
}}
/>
</Switch>
</div>
);
};
<Routes>
<Route
path={DECODE_ROUTES.REDIRECT_SURVEY}
element={<DecodePageRoute><RedirectPage pattern="/courses/:courseId/survey" mode={REDIRECT_MODES.SURVEY_REDIRECT} /></DecodePageRoute>}
/>
<Route
path={ROUTES.DASHBOARD}
element={<PageWrap><RedirectPage pattern="/dashboard" mode={REDIRECT_MODES.DASHBOARD_REDIRECT} /></PageWrap>}
/>
<Route
path={ROUTES.CONSENT}
element={<PageWrap><RedirectPage mode={REDIRECT_MODES.CONSENT_REDIRECT} /></PageWrap>}
/>
<Route
path={DECODE_ROUTES.REDIRECT_HOME}
element={<DecodePageRoute><RedirectPage pattern="/course/:courseId/home" mode={REDIRECT_MODES.HOME_REDIRECT} /></DecodePageRoute>}
/>
</Routes>
</div>
);
export default CoursewareRedirectLandingPage;

View File

@@ -1,19 +1,12 @@
import React from 'react';
import { Router } from 'react-router';
import { createMemoryHistory } from 'history';
import { MemoryRouter as Router } from 'react-router-dom';
import { render, initializeMockApp } from '../setupTest';
import CoursewareRedirectLandingPage from './CoursewareRedirectLandingPage';
const redirectUrl = jest.fn();
jest.mock('@edx/frontend-platform/analytics');
jest.mock('react-router', () => ({
...jest.requireActual('react-router'),
useRouteMatch: () => ({
path: '/redirect',
}),
}));
jest.mock('../decode-page-route', () => jest.fn(({ children }) => <div>{children}</div>));
describe('CoursewareRedirectLandingPage', () => {
beforeEach(async () => {
@@ -23,12 +16,8 @@ describe('CoursewareRedirectLandingPage', () => {
});
it('Redirects to correct consent URL', () => {
const history = createMemoryHistory({
initialEntries: ['/redirect/consent/?consentPath=%2Fgrant_data_sharing_consent'],
});
render(
<Router history={history}>
<Router initialEntries={['/consent/?consentPath=%2Fgrant_data_sharing_consent']}>
<CoursewareRedirectLandingPage />
</Router>,
);
@@ -37,12 +26,8 @@ describe('CoursewareRedirectLandingPage', () => {
});
it('Redirects to correct consent URL', () => {
const history = createMemoryHistory({
initialEntries: ['/redirect/home/course-v1:edX+DemoX+Demo_Course'],
});
render(
<Router history={history}>
<Router initialEntries={['/home/course-v1:edX+DemoX+Demo_Course']}>
<CoursewareRedirectLandingPage />
</Router>,
);

View File

@@ -0,0 +1,45 @@
import PropTypes from 'prop-types';
import {
generatePath, useParams, useLocation,
} from 'react-router-dom';
import { getConfig } from '@edx/frontend-platform';
import queryString from 'query-string';
import { REDIRECT_MODES } from '../constants';
const RedirectPage = ({
pattern, mode,
}) => {
const { courseId } = useParams();
const location = useLocation();
const { consentPath } = queryString.parse(location?.search);
const BASE_URL = getConfig().LMS_BASE_URL;
switch (mode) {
case REDIRECT_MODES.DASHBOARD_REDIRECT:
global.location.assign(`${BASE_URL}${pattern}${location?.search}`);
break;
case REDIRECT_MODES.CONSENT_REDIRECT:
global.location.assign(`${BASE_URL}${consentPath}`);
break;
case REDIRECT_MODES.HOME_REDIRECT:
global.location.assign(generatePath(pattern, { courseId }));
break;
default:
global.location.assign(`${BASE_URL}${generatePath(pattern, { courseId })}`);
}
return null;
};
RedirectPage.propTypes = {
pattern: PropTypes.string,
mode: PropTypes.string.isRequired,
};
RedirectPage.defaultProps = {
pattern: null,
};
export default RedirectPage;

View File

@@ -0,0 +1,69 @@
import React from 'react';
import { render } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { getConfig } from '@edx/frontend-platform';
import RedirectPage from './RedirectPage';
import { REDIRECT_MODES } from '../constants';
const BASE_URL = getConfig().LMS_BASE_URL;
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useParams: () => ({
courseId: 'course-id-123',
}),
useLocation: () => ({
search: '?consentPath=/some-path',
}),
}));
describe('RedirectPage component', () => {
beforeEach(() => {
Object.defineProperty(window, 'location', {
writable: true,
value: { assign: jest.fn() },
});
jest.clearAllMocks();
});
it('should handle DASHBOARD_REDIRECT correctly', () => {
render(
<MemoryRouter>
<RedirectPage mode={REDIRECT_MODES.DASHBOARD_REDIRECT} pattern="/dashboard" />
</MemoryRouter>,
);
expect(global.location.assign).toHaveBeenCalledWith(`${BASE_URL}/dashboard?consentPath=/some-path`);
});
it('should handle CONSENT_REDIRECT correctly', () => {
render(
<MemoryRouter>
<RedirectPage mode={REDIRECT_MODES.CONSENT_REDIRECT} />
</MemoryRouter>,
);
expect(global.location.assign).toHaveBeenCalledWith(`${BASE_URL}/some-path`);
});
it('should handle HOME_REDIRECT correctly', () => {
render(
<MemoryRouter>
<RedirectPage mode={REDIRECT_MODES.HOME_REDIRECT} pattern="/course/:courseId/home" />
</MemoryRouter>,
);
expect(global.location.assign).toHaveBeenCalledWith('/course/course-id-123/home');
});
it('should handle the default case correctly', () => {
render(
<MemoryRouter>
<RedirectPage pattern="/default/:courseId" />
</MemoryRouter>,
);
expect(global.location.assign).toHaveBeenCalledWith(`${BASE_URL}/default/course-id-123`);
});
});

View File

@@ -15,6 +15,10 @@ import { executeThunk } from '../../utils';
import * as thunks from '../data/thunks';
jest.mock('@edx/frontend-platform/analytics');
jest.mock('@edx/frontend-lib-special-exams/dist/data/thunks.js', () => ({
...jest.requireActual('@edx/frontend-lib-special-exams/dist/data/thunks.js'),
checkExamEntry: () => jest.fn(),
}));
const recordFirstSectionCelebration = jest.fn();
// eslint-disable-next-line no-import-assign
@@ -65,11 +69,11 @@ describe('Course', () => {
const [firstSequenceId] = Object.keys(state.models.sequences);
mockData.sequenceId = firstSequenceId;
await render(<Course {...mockData} />, { store: testStore });
await render(<Course {...mockData} />, { store: testStore, wrapWithRouter: true });
};
it('loads learning sequence', async () => {
render(<Course {...mockData} />);
render(<Course {...mockData} />, { wrapWithRouter: true });
expect(screen.getByRole('navigation', { name: 'breadcrumb' })).toBeInTheDocument();
expect(await screen.findByText('Loading learning sequence...')).toBeInTheDocument();
@@ -102,7 +106,7 @@ describe('Course', () => {
};
// Set up LocalStorage for testing.
handleNextSectionCelebration(sequenceId, sequenceId, testData.unitId);
render(<Course {...testData} />, { store: testStore });
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
const firstSectionCelebrationModal = screen.getByRole('dialog');
expect(firstSectionCelebrationModal).toBeInTheDocument();
@@ -120,7 +124,7 @@ describe('Course', () => {
sequenceId,
unitId: Object.values(models.units)[0].id,
};
render(<Course {...testData} />, { store: testStore });
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
const weeklyGoalCelebrationModal = screen.getByRole('dialog');
expect(weeklyGoalCelebrationModal).toBeInTheDocument();
@@ -128,7 +132,7 @@ describe('Course', () => {
});
it('displays notification trigger and toggles active class on click', async () => {
render(<Course {...mockData} />);
render(<Course {...mockData} />, { wrapWithRouter: true });
const notificationTrigger = screen.getByRole('button', { name: /Show notification tray/i });
expect(notificationTrigger).toBeInTheDocument();
@@ -181,7 +185,7 @@ describe('Course', () => {
it('handles click to open/close notification tray', async () => {
sessionStorage.clear();
render(<Course {...mockData} />);
render(<Course {...mockData} />, { wrapWithRouter: true });
expect(sessionStorage.getItem(`notificationTrayStatus.${mockData.courseId}`)).toBe('"open"');
const notificationShowButton = await screen.findByRole('button', { name: /Show notification tray/i });
expect(screen.queryByRole('region', { name: /notification tray/i })).toHaveClass('d-none');
@@ -192,7 +196,7 @@ describe('Course', () => {
it('handles reload persisting notification tray status', async () => {
sessionStorage.clear();
render(<Course {...mockData} />);
render(<Course {...mockData} />, { wrapWithRouter: true });
const notificationShowButton = await screen.findByRole('button', { name: /Show notification tray/i });
fireEvent.click(notificationShowButton);
expect(sessionStorage.getItem(`notificationTrayStatus.${mockData.courseId}`)).toBe('"closed"');
@@ -216,7 +220,7 @@ describe('Course', () => {
// set sessionStorage for a different course before rendering Course
sessionStorage.setItem(`notificationTrayStatus.${courseMetadataSecondCourse.id}`, '"open"');
render(<Course {...mockData} />);
render(<Course {...mockData} />, { wrapWithRouter: true });
expect(sessionStorage.getItem(`notificationTrayStatus.${mockData.courseId}`)).toBe('"open"');
const notificationShowButton = await screen.findByRole('button', { name: /Show notification tray/i });
fireEvent.click(notificationShowButton);
@@ -244,7 +248,7 @@ describe('Course', () => {
sequenceId,
unitId: Object.values(models.units)[1].id, // Corner cases are already covered in `Sequence` tests.
};
render(<Course {...testData} />, { store: testStore });
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
loadUnit();
await waitFor(() => expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument());
@@ -276,7 +280,7 @@ describe('Course', () => {
previousSequenceHandler,
unitNavigationHandler,
};
render(<Course {...testData} />, { store: testStore });
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
loadUnit();
await waitFor(() => expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument());
@@ -305,7 +309,7 @@ describe('Course', () => {
courseId: courseMetadata.id,
sequenceId: sequenceBlocks[0].id,
};
render(<Course {...testData} />, { store: testStore });
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
await waitFor(() => expect(screen.getByText('Some random banner text to display.')).toBeInTheDocument());
});
@@ -339,7 +343,7 @@ describe('Course', () => {
courseId: testCourseMetadata.id,
sequenceId: sequenceBlocks[0].id,
};
render(<Course {...testData} />, { store: testStore });
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
await waitFor(() => expect(screen.getByText('Your score is 100%. You have passed the entrance exam.')).toBeInTheDocument());
});
@@ -373,7 +377,7 @@ describe('Course', () => {
courseId: testCourseMetadata.id,
sequenceId: sequenceBlocks[0].id,
};
render(<Course {...testData} />, { store: testStore });
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
await waitFor(() => expect(screen.getByText('To access course materials, you must score 70% or higher on this exam. Your current score is 30%.')).toBeInTheDocument());
});
});

View File

@@ -159,6 +159,7 @@ const CourseBreadcrumbs = ({
<Link
className="flex-shrink-0 text-primary"
to={`/course/${courseId}/home`}
replace
>
<FontAwesomeIcon icon={faHome} className="mr-2" />
<FormattedMessage

View File

@@ -26,6 +26,12 @@ jest.mock('react-redux', () => ({
Provider: ({ children }) => children,
useSelector: () => 'loaded',
}));
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
Link: jest.fn().mockImplementation(({ to, children }) => (
<a href={to}>{children}</a>
)),
}));
useModels.mockImplementation((name) => {
if (name === 'sections') {

View File

@@ -1,12 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
import { history } from '@edx/frontend-platform';
import { Dropdown } from '@edx/paragon';
import {
sendTrackingLogEvent,
sendTrackEvent,
} from '@edx/frontend-platform/analytics';
import { useNavigate } from 'react-router-dom';
const JumpNavMenuItem = ({
title,
@@ -17,6 +17,8 @@ const JumpNavMenuItem = ({
isDefault,
onClick,
}) => {
const navigate = useNavigate();
function logEvent(targetUrl) {
const eventName = 'edx.ui.lms.jump_nav.selected';
const payload = {
@@ -38,7 +40,7 @@ const JumpNavMenuItem = ({
function handleClick(e) {
const url = destinationUrl();
logEvent(url);
history.push(url);
navigate(url);
if (onClick) { onClick(e); }
}

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { screen, render } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import JumpNavMenuItem from './JumpNavMenuItem';
import { fireEvent } from '../../setupTest';
@@ -26,9 +27,11 @@ const mockData = {
};
describe('JumpNavMenuItem', () => {
render(
<JumpNavMenuItem
{...mockData}
/>,
<BrowserRouter>
<JumpNavMenuItem
{...mockData}
/>
</BrowserRouter>,
);
it('renders menu Item as expected with button and Text and handles clicks', () => {
expect(screen.queryAllByRole('button')).toHaveLength(1);

View File

@@ -4,7 +4,7 @@ import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button } from '@edx/paragon';
import { useSelector } from 'react-redux';
import { Redirect } from 'react-router-dom';
import { Navigate } from 'react-router-dom';
import CourseCelebration from './CourseCelebration';
import CourseInProgress from './CourseInProgress';
@@ -58,7 +58,7 @@ const CourseExit = ({ intl }) => {
} else if (mode === COURSE_EXIT_MODES.celebration) {
body = (<CourseCelebration />);
} else {
return (<Redirect to={`/course/${courseId}`} />);
return (<Navigate to={`/course/${courseId}`} replace />);
}
return (

View File

@@ -51,7 +51,7 @@ describe('Course Exit Pages', () => {
async function fetchAndRender(component) {
await executeThunk(fetchCourse(courseId), store.dispatch);
render(component, { store });
render(component, { store, wrapWithRouter: true });
}
beforeEach(() => {

View File

@@ -11,6 +11,10 @@ import Sequence from './Sequence';
import { fetchSequenceFailure } from '../../data/slice';
jest.mock('@edx/frontend-platform/analytics');
jest.mock('@edx/frontend-lib-special-exams/dist/data/thunks.js', () => ({
...jest.requireActual('@edx/frontend-lib-special-exams/dist/data/thunks.js'),
checkExamEntry: () => jest.fn(),
}));
describe('Sequence', () => {
let mockData;
@@ -42,7 +46,10 @@ describe('Sequence', () => {
it('renders correctly without data', async () => {
const testStore = await initializeTestStore({ excludeFetchCourse: true, excludeFetchSequence: true }, false);
render(<Sequence {...mockData} {...{ unitId: undefined, sequenceId: undefined }} />, { store: testStore });
render(
<Sequence {...mockData} {...{ unitId: undefined, sequenceId: undefined }} />,
{ store: testStore, wrapWithRouter: true },
);
expect(screen.getByText('There is no content here.')).toBeInTheDocument();
expect(screen.queryByRole('button')).not.toBeInTheDocument();
@@ -71,7 +78,7 @@ describe('Sequence', () => {
}, false);
const { container } = render(
<Sequence {...mockData} {...{ sequenceId: sequenceBlocks[0].id }} />,
{ store: testStore },
{ store: testStore, wrapWithRouter: true },
);
await waitFor(() => expect(screen.queryByText('Loading locked content messaging...')).toBeInTheDocument());
@@ -104,7 +111,7 @@ describe('Sequence', () => {
}, false);
render(
<Sequence {...mockData} {...{ sequenceId: sequenceBlocks[0].id }} />,
{ store: testStore },
{ store: testStore, wrapWithRouter: true },
);
await waitFor(() => {
@@ -121,13 +128,13 @@ describe('Sequence', () => {
it('displays error message on sequence load failure', async () => {
const testStore = await initializeTestStore({ excludeFetchCourse: true, excludeFetchSequence: true }, false);
testStore.dispatch(fetchSequenceFailure({ sequenceId: mockData.sequenceId }));
render(<Sequence {...mockData} />, { store: testStore });
render(<Sequence {...mockData} />, { store: testStore, wrapWithRouter: true });
expect(screen.getByText('There was an error loading this course.')).toBeInTheDocument();
});
it('handles loading unit', async () => {
render(<Sequence {...mockData} />);
render(<Sequence {...mockData} />, { wrapWithRouter: true });
expect(await screen.findByText('Loading learning sequence...')).toBeInTheDocument();
// `Previous`, `Bookmark` and `Close Tray` buttons
expect(screen.getAllByRole('button')).toHaveLength(3);
@@ -167,7 +174,7 @@ describe('Sequence', () => {
sequenceId: sequenceBlocks[1].id,
previousSequenceHandler: jest.fn(),
};
render(<Sequence {...testData} />, { store: testStore });
render(<Sequence {...testData} />, { store: testStore, wrapWithRouter: true });
expect(await screen.findByText('Loading learning sequence...')).toBeInTheDocument();
const sequencePreviousButton = screen.getByRole('link', { name: /previous/i });
@@ -203,7 +210,7 @@ describe('Sequence', () => {
sequenceId: sequenceBlocks[0].id,
nextSequenceHandler: jest.fn(),
};
render(<Sequence {...testData} />, { store: testStore });
render(<Sequence {...testData} />, { store: testStore, wrapWithRouter: true });
expect(await screen.findByText('Loading learning sequence...')).toBeInTheDocument();
const sequenceNextButton = screen.getByRole('link', { name: /next/i });
@@ -241,7 +248,7 @@ describe('Sequence', () => {
previousSequenceHandler: jest.fn(),
nextSequenceHandler: jest.fn(),
};
render(<Sequence {...testData} />, { store: testStore });
render(<Sequence {...testData} />, { store: testStore, wrapWithRouter: true });
await waitFor(() => expect(screen.queryByText('Loading learning sequence...')).toBeInTheDocument());
fireEvent.click(screen.getByRole('link', { name: /previous/i }));
@@ -265,7 +272,7 @@ describe('Sequence', () => {
unitNavigationHandler: jest.fn(),
previousSequenceHandler: jest.fn(),
};
render(<Sequence {...testData} />, { store: testStore });
render(<Sequence {...testData} />, { store: testStore, wrapWithRouter: true });
loadUnit();
await waitFor(() => expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument());
@@ -284,7 +291,7 @@ describe('Sequence', () => {
unitNavigationHandler: jest.fn(),
nextSequenceHandler: jest.fn(),
};
render(<Sequence {...testData} />, { store: testStore });
render(<Sequence {...testData} />, { store: testStore, wrapWithRouter: true });
loadUnit();
await waitFor(() => expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument());
@@ -326,7 +333,7 @@ describe('Sequence', () => {
nextSequenceHandler: jest.fn(),
};
render(<Sequence {...testData} />, { store: innerTestStore });
render(<Sequence {...testData} />, { store: innerTestStore, wrapWithRouter: true });
loadUnit();
await waitFor(() => expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument());
@@ -374,7 +381,7 @@ describe('Sequence', () => {
sequenceId: sequenceBlocks[0].id,
unitNavigationHandler: jest.fn(),
};
render(<Sequence {...testData} />, { store: testStore });
render(<Sequence {...testData} />, { store: testStore, wrapWithRouter: true });
await waitFor(() => expect(screen.queryByText('Loading learning sequence...')).toBeInTheDocument());
fireEvent.click(screen.getByRole('link', { name: targetUnit.display_name }));
@@ -401,13 +408,13 @@ describe('Sequence', () => {
describe('notification feature', () => {
it('renders notification tray in sequence', async () => {
render(<SidebarWrapper contextValue={{ courseId: mockData.courseId, currentSidebar: 'NOTIFICATIONS', toggleSidebar: () => null }} />);
render(<SidebarWrapper contextValue={{ courseId: mockData.courseId, currentSidebar: 'NOTIFICATIONS', toggleSidebar: () => null }} />, { wrapWithRouter: true });
expect(await screen.findByText('Notifications')).toBeInTheDocument();
});
it('handles click on notification tray close button', async () => {
const toggleNotificationTray = jest.fn();
render(<SidebarWrapper contextValue={{ courseId: mockData.courseId, currentSidebar: 'NOTIFICATIONS', toggleSidebar: toggleNotificationTray }} />);
render(<SidebarWrapper contextValue={{ courseId: mockData.courseId, currentSidebar: 'NOTIFICATIONS', toggleSidebar: toggleNotificationTray }} />, { wrapWithRouter: true });
const notificationCloseIconButton = await screen.findByRole('button', { name: /Close notification tray/i });
fireEvent.click(notificationCloseIconButton);
expect(toggleNotificationTray).toHaveBeenCalledTimes(1);
@@ -415,7 +422,7 @@ describe('Sequence', () => {
it('does not render notification tray in sequence by default if in responsive view', async () => {
global.innerWidth = breakpoints.medium.maxWidth;
const { container } = render(<Sequence {...mockData} />);
const { container } = render(<Sequence {...mockData} />, { wrapWithRouter: true });
// unable to test the absence of 'Notifications' by finding it by text, using the class of the tray instead:
expect(container).not.toHaveClass('notification-tray-container');
});

View File

@@ -19,13 +19,13 @@ describe('Sequence Content', () => {
});
it('displays loading message', () => {
render(<SequenceContent {...mockData} />);
render(<SequenceContent {...mockData} />, { wrapWithRouter: true });
expect(screen.getByText('Loading learning sequence...')).toBeInTheDocument();
});
it('displays messages for the locked content', async () => {
const { gatedContent } = store.getState().models.sequences[mockData.sequenceId];
const { container } = render(<SequenceContent {...mockData} gated />);
const { container } = render(<SequenceContent {...mockData} gated />, { wrapWithRouter: true });
expect(screen.getByText('Loading locked content messaging...')).toBeInTheDocument();
expect(await screen.findByText('Content Locked')).toBeInTheDocument();
@@ -38,7 +38,7 @@ describe('Sequence Content', () => {
});
it('displays message for no content', () => {
render(<SequenceContent {...mockData} unitId={null} />);
render(<SequenceContent {...mockData} unitId={null} />, { wrapWithRouter: true });
expect(screen.getByText('There is no content here.')).toBeInTheDocument();
});
});

View File

@@ -74,7 +74,7 @@ const ContentIFrame = ({
<iframe title={title} {...contentIFrameProps} data-testid={testIDs.contentIFrame} />
</div>
)}
{modalOptions.open && (
{modalOptions.isOpen && (
<Modal
body={modalOptions.body
? <div className="unit-modal">{ modalOptions.body }</div>
@@ -84,7 +84,7 @@ const ContentIFrame = ({
allow={IFRAME_FEATURE_POLICY}
frameBorder="0"
src={modalOptions.url}
style={{ width: '100%', height: '100vh' }}
style={{ width: '100%', height: modalOptions.height }}
/>
)}
dialogClassName="modal-lti"

View File

@@ -29,16 +29,17 @@ const iframeBehavior = {
const modalOptions = {
closed: {
open: false,
isOpen: false,
},
withBody: {
body: 'test-body',
open: true,
isOpen: true,
},
withUrl: {
open: true,
isOpen: true,
title: 'test-modal-title',
url: 'test-modal-url',
height: 'test-height',
},
};
@@ -83,7 +84,7 @@ describe('ContentIFrame Component', () => {
});
describe('output', () => {
let component;
describe('shouldShowContent', () => {
describe('if shouldShowContent', () => {
describe('if not hasLoaded', () => {
it('displays errorPage if showError', () => {
hooks.useIFrameBehavior.mockReturnValueOnce({ ...iframeBehavior, showError: true });
@@ -121,7 +122,7 @@ describe('ContentIFrame Component', () => {
});
});
});
describe('not shouldShowContent', () => {
describe('if not shouldShowContent', () => {
it('does not show PageLoading, ErrorPage, or unit-iframe-wrapper', () => {
el = shallow(<ContentIFrame {...{ ...props, shouldShowContent: false }} />);
expect(el.instance.findByType(PageLoading).length).toEqual(0);
@@ -129,13 +130,13 @@ describe('ContentIFrame Component', () => {
expect(el.instance.findByTestId(testIDs.contentIFrame).length).toEqual(0);
});
});
it('does not display modal if modalOptions returns open: false', () => {
it('does not display modal if modalOptions returns isOpen: false', () => {
el = shallow(<ContentIFrame {...props} />);
expect(el.instance.findByType(Modal).length).toEqual(0);
});
describe('if modalOptions.open', () => {
describe('if modalOptions.isOpen', () => {
const testModalOpenAndHandleClose = () => {
test('Modal component is open, with handleModalClose from hook', () => {
test('Modal component isOpen, with handleModalClose from hook', () => {
expect(component.props.onClose).toEqual(modalIFrameData.handleModalClose);
});
};
@@ -164,7 +165,7 @@ describe('ContentIFrame Component', () => {
allow={IFRAME_FEATURE_POLICY}
frameBorder="0"
src={modalOptions.withUrl.url}
style={{ width: '100%', height: '100vh' }}
style={{ width: '100%', height: modalOptions.withUrl.height }}
/>,
);
});

View File

@@ -5,28 +5,32 @@ import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils/dist';
import { useEventListener } from '../../../../../generic/hooks';
export const stateKeys = StrictDict({
modalOptions: 'modalOptions',
isOpen: 'isOpen',
options: 'options',
});
export const DEFAULT_HEIGHT = '100vh';
const useModalIFrameBehavior = () => {
const [modalOptions, setModalOptions] = useKeyedState(stateKeys.modalOptions, ({ open: false }));
const [isOpen, setIsOpen] = useKeyedState(stateKeys.isOpen, false);
const [options, setOptions] = useKeyedState(stateKeys.options, { height: DEFAULT_HEIGHT });
const receiveMessage = React.useCallback(({ data }) => {
const { type, payload } = data;
if (type === 'plugin.modal') {
payload.open = true;
setModalOptions(payload);
setOptions((current) => ({ ...current, ...payload }));
setIsOpen(true);
}
}, []);
useEventListener('message', receiveMessage);
const handleModalClose = () => {
setModalOptions({ open: false });
setIsOpen(false);
};
return {
handleModalClose,
modalOptions,
modalOptions: { isOpen, ...options },
};
};

View File

@@ -2,7 +2,7 @@ import { mockUseKeyedState } from '@edx/react-unit-test-utils';
import { useEventListener } from '../../../../../generic/hooks';
import { messageTypes } from '../constants';
import useModalIFrameBehavior, { stateKeys } from './useModalIFrameData';
import useModalIFrameBehavior, { stateKeys, DEFAULT_HEIGHT } from './useModalIFrameData';
jest.mock('react', () => ({
...jest.requireActual('react'),
@@ -20,31 +20,49 @@ describe('useModalIFrameBehavior', () => {
state.mock();
});
describe('behavior', () => {
it('initializes modalOptions to closed', () => {
it('initializes isOpen to false', () => {
useModalIFrameBehavior();
state.expectInitializedWith(stateKeys.modalOptions, { open: false });
state.expectInitializedWith(stateKeys.isOpen, false);
});
it('initializes options with default height', () => {
useModalIFrameBehavior();
state.expectInitializedWith(stateKeys.options, { height: DEFAULT_HEIGHT });
});
describe('eventListener', () => {
it('consumes modal events and opens sets modal options with open: true', () => {
const oldOptions = { some: 'old', options: 'yeah' };
state.mockVals({
[stateKeys.isOpen]: false,
[stateKeys.options]: oldOptions,
});
useModalIFrameBehavior();
expect(useEventListener).toHaveBeenCalled();
const { cb, prereqs } = useEventListener.mock.calls[0][1];
expect(prereqs).toEqual([]);
const payload = { test: 'values' };
cb({ data: { type: messageTypes.modal, payload } });
expect(state.setState.modalOptions).toHaveBeenCalledWith({ ...payload, open: true });
expect(state.setState.isOpen).toHaveBeenCalledWith(true);
expect(state.setState.options).toHaveBeenCalled();
const [[setOptionsCb]] = state.setState.options.mock.calls;
expect(setOptionsCb(oldOptions)).toEqual({ ...oldOptions, ...payload });
});
});
});
describe('output', () => {
test('handleModalClose sets modal options to closed', () => {
useModalIFrameBehavior().handleModalClose();
state.expectSetStateCalledWith(stateKeys.modalOptions, { open: false });
state.expectSetStateCalledWith(stateKeys.isOpen, false);
});
it('forwards modalOptions from state value', () => {
it('forwards modalOptions from state values', () => {
const modalOptions = { test: 'options' };
state.mockVal(stateKeys.modalOptions, modalOptions);
expect(useModalIFrameBehavior().modalOptions).toEqual(modalOptions);
state.mockVals({
[stateKeys.options]: modalOptions,
[stateKeys.isOpen]: true,
});
expect(useModalIFrameBehavior().modalOptions).toEqual({
...modalOptions,
isOpen: true,
});
});
});
});

View File

@@ -1,9 +1,9 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useNavigate } from 'react-router-dom';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faLock } from '@fortawesome/free-solid-svg-icons';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { history } from '@edx/frontend-platform';
import { Button } from '@edx/paragon';
import messages from './messages';
@@ -11,8 +11,9 @@ import messages from './messages';
const ContentLock = ({
intl, courseId, prereqSectionName, prereqId, sequenceTitle,
}) => {
const navigate = useNavigate();
const handleClick = useCallback(() => {
history.push(`/course/${courseId}/${prereqId}`);
navigate(`/course/${courseId}/${prereqId}`);
}, [courseId, prereqId]);
return (

View File

@@ -1,10 +1,16 @@
import React from 'react';
import { history } from '@edx/frontend-platform';
import {
render, screen, fireEvent, initializeMockApp,
} from '../../../../setupTest';
import ContentLock from './ContentLock';
const mockNavigate = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: () => mockNavigate,
}));
describe('Content Lock', () => {
const mockData = {
courseId: 'test-course-id',
@@ -19,7 +25,7 @@ describe('Content Lock', () => {
});
it('displays sequence title along with lock icon', () => {
const { container } = render(<ContentLock {...mockData} />);
const { container } = render(<ContentLock {...mockData} />, { wrapWithRouter: true });
const lockIcon = container.querySelector('svg');
expect(lockIcon).toHaveClass('fa-lock');
@@ -28,16 +34,15 @@ describe('Content Lock', () => {
it('displays prerequisite name', () => {
const prereqText = `You must complete the prerequisite: '${mockData.prereqSectionName}' to access this content.`;
render(<ContentLock {...mockData} />);
render(<ContentLock {...mockData} />, { wrapWithRouter: true });
expect(screen.getByText(prereqText)).toBeInTheDocument();
});
it('handles click', () => {
history.push = jest.fn();
render(<ContentLock {...mockData} />);
render(<ContentLock {...mockData} />, { wrapWithRouter: true });
fireEvent.click(screen.getByRole('button'));
expect(history.push).toHaveBeenCalledWith(`/course/${mockData.courseId}/${mockData.prereqId}`);
expect(mockNavigate).toHaveBeenCalledWith(`/course/${mockData.courseId}/${mockData.prereqId}`);
});
});

View File

@@ -1,16 +1,18 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useDispatch } from 'react-redux';
import { getConfig, history } from '@edx/frontend-platform';
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { ActionRow, Alert, Button } from '@edx/paragon';
import { useNavigate } from 'react-router-dom';
import { useModel } from '../../../../generic/model-store';
import { saveIntegritySignature } from '../../../data';
import messages from './messages';
const HonorCode = ({ intl, courseId }) => {
const navigate = useNavigate();
const dispatch = useDispatch();
const {
isMasquerading,
@@ -20,7 +22,7 @@ const HonorCode = ({ intl, courseId }) => {
const siteName = getConfig().SITE_NAME;
const honorCodeUrl = `${getConfig().TERMS_OF_SERVICE_URL}#honor-code`;
const handleCancel = () => history.push(`/course/${courseId}/home`);
const handleCancel = () => navigate(`/course/${courseId}/home`);
const handleAgree = () => dispatch(
// If the request is made by a staff user masquerading as a specific learner,

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { getConfig, history } from '@edx/frontend-platform';
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import MockAdapter from 'axios-mock-adapter';
import { Factory } from 'rosie';
@@ -9,12 +9,12 @@ import {
} from '../../../../setupTest';
import HonorCode from './HonorCode';
const mockNavigate = jest.fn();
initializeMockApp();
jest.mock('@edx/frontend-platform', () => ({
...jest.requireActual('@edx/frontend-platform'),
history: {
push: jest.fn(),
},
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: () => mockNavigate,
}));
describe('Honor Code', () => {
@@ -38,15 +38,15 @@ describe('Honor Code', () => {
it('cancel button links to course home ', async () => {
await setupStoreState();
render(<HonorCode {...mockData} />);
render(<HonorCode {...mockData} />, { wrapWithRouter: true });
const cancelButton = screen.getByText('Cancel');
fireEvent.click(cancelButton);
expect(history.push).toHaveBeenCalledWith(`/course/${mockData.courseId}/home`);
expect(mockNavigate).toHaveBeenCalledWith(`/course/${mockData.courseId}/home`);
});
it('calls to save integrity_signature when agreeing', async () => {
await setupStoreState({ username: authenticatedUser.username });
render(<HonorCode {...mockData} />);
render(<HonorCode {...mockData} />, { wrapWithRouter: true });
const agreeButton = screen.getByText('I agree');
fireEvent.click(agreeButton);
await waitFor(() => {
@@ -63,7 +63,7 @@ describe('Honor Code', () => {
username: authenticatedUser.username,
},
);
render(<HonorCode {...mockData} />);
render(<HonorCode {...mockData} />, { wrapWithRouter: true });
const agreeButton = screen.getByText('I agree');
fireEvent.click(agreeButton);
await waitFor(() => {
@@ -80,7 +80,7 @@ describe('Honor Code', () => {
username: 'otheruser',
},
);
render(<HonorCode {...mockData} />);
render(<HonorCode {...mockData} />, { wrapWithRouter: true });
const agreeButton = screen.getByText('I agree');
fireEvent.click(agreeButton);
await waitFor(() => {

View File

@@ -33,13 +33,13 @@ describe('Sequence Navigation', () => {
it('is empty while loading', async () => {
const testStore = await initializeTestStore({ excludeFetchSequence: true }, false);
const { container } = render(<SequenceNavigation {...mockData} />, { store: testStore });
const { container } = render(<SequenceNavigation {...mockData} />, { store: testStore, wrapWithRouter: true });
expect(container).toBeEmptyDOMElement();
});
it('renders empty div without unitId', () => {
const { container } = render(<SequenceNavigation {...mockData} unitId={undefined} />);
const { container } = render(<SequenceNavigation {...mockData} unitId={undefined} />, { wrapWithRouter: true });
expect(getByText(container, (content, element) => (
element.tagName.toLowerCase() === 'div' && element.getAttribute('style')))).toBeEmptyDOMElement();
});
@@ -61,7 +61,7 @@ describe('Sequence Navigation', () => {
sequenceId: sequenceBlocks[0].id,
onNavigate: jest.fn(),
};
render(<SequenceNavigation {...testData} />, { store: testStore });
render(<SequenceNavigation {...testData} />, { store: testStore, wrapWithRouter: true });
const unitButton = screen.getByTitle(unitBlocks[1].display_name);
fireEvent.click(unitButton);
@@ -74,7 +74,7 @@ describe('Sequence Navigation', () => {
it('renders correctly and handles unit button clicks', () => {
const onNavigate = jest.fn();
render(<SequenceNavigation {...mockData} {...{ onNavigate }} />);
render(<SequenceNavigation {...mockData} {...{ onNavigate }} />, { wrapWithRouter: true });
const unitButtons = screen.getAllByRole('link', { name: /\d+/ });
expect(unitButtons).toHaveLength(unitButtons.length);
@@ -83,7 +83,7 @@ describe('Sequence Navigation', () => {
});
it('has both navigation buttons enabled for a non-corner unit of the sequence', () => {
render(<SequenceNavigation {...mockData} />);
render(<SequenceNavigation {...mockData} />, { wrapWithRouter: true });
screen.getAllByRole('link', { name: /previous|next/i }).forEach(button => {
expect(button).toBeEnabled();
@@ -91,7 +91,7 @@ describe('Sequence Navigation', () => {
});
it('has the "Previous" button disabled for the first unit of the sequence', () => {
render(<SequenceNavigation {...mockData} unitId={unitBlocks[0].id} />);
render(<SequenceNavigation {...mockData} unitId={unitBlocks[0].id} />, { wrapWithRouter: true });
expect(screen.getByRole('button', { name: /previous/i })).toBeDisabled();
expect(screen.getByRole('link', { name: /next/i })).toBeEnabled();
@@ -106,7 +106,7 @@ describe('Sequence Navigation', () => {
render(
<SequenceNavigation {...testData} unitId={unitBlocks[unitBlocks.length - 1].id} />,
{ store: testStore },
{ store: testStore, wrapWithRouter: true },
);
expect(screen.getByRole('link', { name: /previous/i })).toBeEnabled();
@@ -122,7 +122,7 @@ describe('Sequence Navigation', () => {
render(
<SequenceNavigation {...testData} unitId={unitBlocks[unitBlocks.length - 1].id} />,
{ store: testStore },
{ store: testStore, wrapWithRouter: true },
);
expect(screen.getByRole('link', { name: /previous/i })).toBeEnabled();
@@ -143,7 +143,7 @@ describe('Sequence Navigation', () => {
render(
<SequenceNavigation {...testData} unitId={unitBlocks[unitBlocks.length - 1].id} />,
{ store: testStore },
{ store: testStore, wrapWithRouter: true },
);
expect(screen.getByRole('link', { name: /previous/i })).toBeEnabled();
@@ -153,7 +153,7 @@ describe('Sequence Navigation', () => {
it('handles "Previous" and "Next" click', () => {
const previousHandler = jest.fn();
const nextHandler = jest.fn();
render(<SequenceNavigation {...mockData} {...{ previousHandler, nextHandler }} />);
render(<SequenceNavigation {...mockData} {...{ previousHandler, nextHandler }} />, { wrapWithRouter: true });
fireEvent.click(screen.getByRole('link', { name: /previous/i }));
expect(previousHandler).toHaveBeenCalledTimes(1);

View File

@@ -40,7 +40,10 @@ describe('Sequence Navigation Dropdown', () => {
unitBlocks.forEach((unit, index) => {
it(`marks unit ${index + 1} as active`, async () => {
const { container } = render(<SequenceNavigationDropdown {...mockData} unitId={unit.id} />);
const { container } = render(
<SequenceNavigationDropdown {...mockData} unitId={unit.id} />,
{ wrapWithRouter: true },
);
const dropdownToggle = container.querySelector('.dropdown-toggle');
await act(async () => {
await fireEvent.click(dropdownToggle);
@@ -59,7 +62,10 @@ describe('Sequence Navigation Dropdown', () => {
it('handles the clicks', () => {
const onNavigate = jest.fn();
const { container } = render(<SequenceNavigationDropdown {...mockData} onNavigate={onNavigate} />);
const { container } = render(
<SequenceNavigationDropdown {...mockData} onNavigate={onNavigate} />,
{ wrapWithRouter: true },
);
const dropdownToggle = container.querySelector('.dropdown-toggle');
act(() => {

View File

@@ -41,7 +41,7 @@ describe('Sequence Navigation Tabs', () => {
it('renders unit buttons', () => {
useIndexOfLastVisibleChild.mockReturnValue([0, null, null]);
render(<SequenceNavigationTabs {...mockData} />);
render(<SequenceNavigationTabs {...mockData} />, { wrapWithRouter: true });
expect(screen.getAllByRole('link')).toHaveLength(unitBlocks.length);
});
@@ -50,7 +50,7 @@ describe('Sequence Navigation Tabs', () => {
let container = null;
await act(async () => {
useIndexOfLastVisibleChild.mockReturnValue([-1, null, null]);
const booyah = render(<SequenceNavigationTabs {...mockData} />);
const booyah = render(<SequenceNavigationTabs {...mockData} />, { wrapWithRouter: true });
container = booyah.container;
const dropdownToggle = container.querySelector('.dropdown-toggle');

View File

@@ -32,12 +32,12 @@ describe('Unit Button', () => {
});
it('hides title by default', () => {
render(<UnitButton {...mockData} />);
render(<UnitButton {...mockData} />, { wrapWithRouter: true });
expect(screen.getByRole('link')).not.toHaveTextContent(unit.display_name);
});
it('shows title', () => {
render(<UnitButton {...mockData} showTitle />);
render(<UnitButton {...mockData} showTitle />, { wrapWithRouter: true });
expect(screen.getByRole('link')).toHaveTextContent(unit.display_name);
});
@@ -49,7 +49,7 @@ describe('Unit Button', () => {
});
it('shows completion for completed unit', () => {
const { container } = render(<UnitButton {...mockData} unitId={completedUnit.id} />);
const { container } = render(<UnitButton {...mockData} unitId={completedUnit.id} />, { wrapWithRouter: true });
const buttonIcons = container.querySelectorAll('svg');
expect(buttonIcons).toHaveLength(2);
expect(buttonIcons[1]).toHaveClass('fa-check');
@@ -70,7 +70,7 @@ describe('Unit Button', () => {
});
it('shows bookmark', () => {
const { container } = render(<UnitButton {...mockData} unitId={bookmarkedUnit.id} />);
const { container } = render(<UnitButton {...mockData} unitId={bookmarkedUnit.id} />, { wrapWithRouter: true });
const buttonIcons = container.querySelectorAll('svg');
expect(buttonIcons).toHaveLength(3);
expect(buttonIcons[2]).toHaveClass('fa-bookmark');
@@ -78,7 +78,7 @@ describe('Unit Button', () => {
it('handles the click', () => {
const onClick = jest.fn();
render(<UnitButton {...mockData} onClick={onClick} />);
render(<UnitButton {...mockData} onClick={onClick} />, { wrapWithRouter: true });
fireEvent.click(screen.getByRole('link'));
expect(onClick).toHaveBeenCalledTimes(1);
});

View File

@@ -32,7 +32,7 @@ describe('Unit Navigation', () => {
unitId=""
onClickPrevious={() => {}}
onClickNext={() => {}}
/>);
/>, { wrapWithRouter: true });
// Only "Previous" and "Next" buttons should be rendered.
expect(screen.getAllByRole('link')).toHaveLength(2);
@@ -46,7 +46,7 @@ describe('Unit Navigation', () => {
{...mockData}
onClickPrevious={onClickPrevious}
onClickNext={onClickNext}
/>);
/>, { wrapWithRouter: true });
fireEvent.click(screen.getByRole('link', { name: /previous/i }));
expect(onClickPrevious).toHaveBeenCalledTimes(1);
@@ -56,7 +56,7 @@ describe('Unit Navigation', () => {
});
it('has the navigation buttons enabled for the non-corner unit in the sequence', () => {
render(<UnitNavigation {...mockData} />);
render(<UnitNavigation {...mockData} />, { wrapWithRouter: true });
screen.getAllByRole('link').forEach(button => {
expect(button).toBeEnabled();
@@ -64,7 +64,7 @@ describe('Unit Navigation', () => {
});
it('has the "Previous" button disabled for the first unit in the sequence', () => {
render(<UnitNavigation {...mockData} unitId={unitBlocks[0].id} />);
render(<UnitNavigation {...mockData} unitId={unitBlocks[0].id} />, { wrapWithRouter: true });
expect(screen.getByRole('button', { name: /previous/i })).toBeDisabled();
expect(screen.getByRole('link', { name: /next/i })).toBeEnabled();
@@ -79,7 +79,7 @@ describe('Unit Navigation', () => {
render(
<UnitNavigation {...testData} unitId={unitBlocks[unitBlocks.length - 1].id} />,
{ store: testStore },
{ store: testStore, wrapWithRouter: true },
);
expect(screen.getByRole('link', { name: /previous/i })).toBeEnabled();
@@ -95,7 +95,7 @@ describe('Unit Navigation', () => {
render(
<UnitNavigation {...testData} unitId={unitBlocks[unitBlocks.length - 1].id} />,
{ store: testStore },
{ store: testStore, wrapWithRouter: true },
);
expect(screen.getByRole('link', { name: /previous/i })).toBeEnabled();
@@ -116,7 +116,7 @@ describe('Unit Navigation', () => {
render(
<UnitNavigation {...testData} unitId={unitBlocks[unitBlocks.length - 1].id} />,
{ store: testStore },
{ store: testStore, wrapWithRouter: true },
);
expect(screen.getByRole('link', { name: /previous/i })).toBeEnabled();

22
src/courseware/utils.jsx Normal file
View File

@@ -0,0 +1,22 @@
import React from 'react';
import { useNavigate, useParams } from 'react-router-dom';
const withParamsAndNavigation = WrappedComponent => {
const WithParamsNavigationComponent = props => {
const { courseId, sequenceId, unitId } = useParams();
const navigate = useNavigate();
return (
<WrappedComponent
routeCourseId={courseId}
routeSequenceId={sequenceId}
routeUnitId={unitId}
navigate={navigate}
{...props}
/>
);
};
return WithParamsNavigationComponent;
};
export default withParamsAndNavigation;

View File

@@ -2,15 +2,16 @@
exports[`DecodePageRoute should not modify the url if it does not need to be decoded 1`] = `
<div>
PageRoute: {
"computedMatch": {
"path": "/course/:courseId/home",
"url": "/course/course-v1:edX+DemoX+Demo_Course/home",
"isExact": true,
"params": {
"courseId": "course-v1:edX+DemoX+Demo_Course"
}
}
PageWrap: {
"children": [
" ",
[
" ",
[],
" "
],
" "
]
}
</div>
`;

View File

@@ -1,7 +1,15 @@
import PropTypes from 'prop-types';
import { PageRoute } from '@edx/frontend-platform/react';
import { PageWrap } from '@edx/frontend-platform/react';
import React from 'react';
import { useHistory, generatePath } from 'react-router';
import {
generatePath, useMatch, Navigate,
} from 'react-router-dom';
import { DECODE_ROUTES } from '../constants';
const ROUTES = [].concat(
...Object.values(DECODE_ROUTES).map(value => (Array.isArray(value) ? value : [value])),
);
export const decodeUrl = (encodedUrl) => {
const decodedUrl = decodeURIComponent(encodedUrl);
@@ -11,10 +19,16 @@ export const decodeUrl = (encodedUrl) => {
return decodeUrl(decodedUrl);
};
const DecodePageRoute = (props) => {
const history = useHistory();
if (props.computedMatch) {
const { url, path, params } = props.computedMatch;
const DecodePageRoute = ({ children }) => {
let computedMatch = null;
ROUTES.forEach((route) => {
const matchedRoute = useMatch(route);
if (matchedRoute) { computedMatch = matchedRoute; }
});
if (computedMatch) {
const { pathname, pattern, params } = computedMatch;
Object.keys(params).forEach((param) => {
// only decode params not the entire url.
@@ -22,28 +36,19 @@ const DecodePageRoute = (props) => {
params[param] = decodeUrl(params[param]);
});
const newUrl = generatePath(path, params);
const newUrl = generatePath(pattern.path, params);
// if the url get decoded, reroute to the decoded url
if (newUrl !== url) {
history.replace(newUrl);
if (newUrl !== pathname) {
return <Navigate to={newUrl} replace />;
}
}
return <PageRoute {...props} />;
return <PageWrap> {children} </PageWrap>;
};
DecodePageRoute.propTypes = {
computedMatch: PropTypes.shape({
url: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
// eslint-disable-next-line react/forbid-prop-types
params: PropTypes.any,
}),
};
DecodePageRoute.defaultProps = {
computedMatch: null,
children: PropTypes.node.isRequired,
};
export default DecodePageRoute;

View File

@@ -1,7 +1,8 @@
import React from 'react';
import { render } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import { Router, matchPath } from 'react-router';
import {
MemoryRouter as Router, matchPath, Routes, Route, mockNavigate,
} from 'react-router-dom';
import DecodePageRoute, { decodeUrl } from '.';
const decodedCourseId = 'course-v1:edX+DemoX+Demo_Course';
@@ -15,84 +16,90 @@ const deepEncodedCourseId = (() => {
})();
jest.mock('@edx/frontend-platform/react', () => ({
PageRoute: (props) => `PageRoute: ${JSON.stringify(props, null, 2)}`,
PageWrap: (props) => `PageWrap: ${JSON.stringify(props, null, 2)}`,
}));
jest.mock('../constants', () => ({
DECODE_ROUTES: {
MOCK_ROUTE_1: '/course/:courseId/home',
MOCK_ROUTE_2: `/course/:courseId/${encodeURIComponent('some+thing')}/:unitId`,
},
}));
const renderPage = (props) => {
const memHistory = createMemoryHistory({
initialEntries: [props?.path],
});
jest.mock('react-router-dom', () => {
const mockNavigation = jest.fn();
const history = {
...memHistory,
replace: jest.fn(),
// eslint-disable-next-line react/prop-types
const Navigate = ({ to }) => {
mockNavigation(to);
return <div />;
};
return {
...jest.requireActual('react-router-dom'),
Navigate,
mockNavigate: mockNavigation,
};
});
const renderPage = (props) => {
const { container } = render(
<Router history={history}>
<DecodePageRoute computedMatch={props} />
<Router initialEntries={[props?.pathname]}>
<Routes>
<Route path={props?.pattern?.path} element={<DecodePageRoute> {[]} </DecodePageRoute>} />
</Routes>
</Router>,
);
return {
container,
history,
props,
};
return { container };
};
describe('DecodePageRoute', () => {
it('should not modify the url if it does not need to be decoded', () => {
const props = matchPath(`/course/${decodedCourseId}/home`, {
path: '/course/:courseId/home',
});
const { container, history } = renderPage(props);
afterEach(() => {
mockNavigate.mockClear();
});
expect(props.url).toContain(decodedCourseId);
expect(history.replace).not.toHaveBeenCalled();
it('should not modify the url if it does not need to be decoded', () => {
const props = matchPath({
path: '/course/:courseId/home',
}, `/course/${decodedCourseId}/home`);
const { container } = renderPage(props);
expect(props.pathname).toContain(decodedCourseId);
expect(mockNavigate).not.toHaveBeenCalled();
expect(container).toMatchSnapshot();
});
it('should decode the url and replace the history if necessary', () => {
const props = matchPath(`/course/${encodedCourseId}/home`, {
const props = matchPath({
path: '/course/:courseId/home',
});
const { history } = renderPage(props);
}, `/course/${encodedCourseId}/home`);
renderPage(props);
expect(props.url).not.toContain(decodedCourseId);
expect(props.url).toContain(encodedCourseId);
expect(history.replace.mock.calls[0][0]).toContain(decodedCourseId);
expect(props.pathname).not.toContain(decodedCourseId);
expect(props.pathname).toContain(encodedCourseId);
expect(mockNavigate).toHaveBeenCalledWith(`/course/${decodedCourseId}/home`);
});
it('should decode the url multiple times if necessary', () => {
const props = matchPath(`/course/${deepEncodedCourseId}/home`, {
const props = matchPath({
path: '/course/:courseId/home',
});
const { history } = renderPage(props);
}, `/course/${deepEncodedCourseId}/home`);
renderPage(props);
expect(props.url).not.toContain(decodedCourseId);
expect(props.url).toContain(deepEncodedCourseId);
expect(history.replace.mock.calls[0][0]).toContain(decodedCourseId);
expect(props.pathname).not.toContain(decodedCourseId);
expect(props.pathname).toContain(deepEncodedCourseId);
expect(mockNavigate).toHaveBeenCalledWith(`/course/${decodedCourseId}/home`);
});
it('should only decode the url params and not the entire url', () => {
const decodedUnitId = 'some+thing';
const encodedUnitId = encodeURIComponent(decodedUnitId);
const props = matchPath(`/course/${deepEncodedCourseId}/${encodedUnitId}/${encodedUnitId}`, {
const props = matchPath({
path: `/course/:courseId/${encodedUnitId}/:unitId`,
});
const { history } = renderPage(props);
}, `/course/${deepEncodedCourseId}/${encodedUnitId}/${encodedUnitId}`);
renderPage(props);
const decodedUrls = history.replace.mock.calls[0][0].split('/');
// unitId get decoded
expect(decodedUrls.pop()).toContain(decodedUnitId);
// path remain encoded
expect(decodedUrls.pop()).toContain(encodedUnitId);
// courseId get decoded
expect(decodedUrls.pop()).toContain(decodedCourseId);
expect(mockNavigate).toHaveBeenCalledWith(`/course/${decodedCourseId}/${encodedUnitId}/${decodedUnitId}`);
});
});

View File

@@ -1,9 +1,8 @@
import React, { useEffect } from 'react';
import { LearningHeader as Header } from '@edx/frontend-component-header';
import Footer from '@edx/frontend-component-footer';
import { useParams } from 'react-router-dom';
import { useParams, Navigate } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { Redirect } from 'react-router';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import useActiveEnterpriseAlert from '../alerts/active-enteprise-alert';
import { AlertList } from './user-messages';
@@ -38,7 +37,7 @@ const CourseAccessErrorPage = ({ intl }) => {
);
}
if (courseStatus === LOADED) {
return (<Redirect to={`/redirect/home/${courseId}`} />);
return <Navigate to={`/redirect/home/${courseId}`} replace />;
}
return (
<>

View File

@@ -1,11 +1,13 @@
import React from 'react';
import { history } from '@edx/frontend-platform';
import { Route } from 'react-router';
import { Routes, Route } from 'react-router-dom';
import { initializeTestStore, render, screen } from '../setupTest';
import CourseAccessErrorPage from './CourseAccessErrorPage';
const mockDispatch = jest.fn();
const mockNavigate = jest.fn();
let mockCourseStatus;
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useDispatch: () => mockDispatch,
@@ -14,6 +16,10 @@ jest.mock('react-redux', () => ({
jest.mock('./PageLoading', () => function () {
return <div data-testid="page-loading" />;
});
jest.mock('react-router-dom', () => ({
...(jest.requireActual('react-router-dom')),
useNavigate: () => mockNavigate,
}));
describe('CourseAccessErrorPage', () => {
let courseId;
@@ -28,33 +34,36 @@ describe('CourseAccessErrorPage', () => {
it('Displays loading in start on page rendering', () => {
mockCourseStatus = 'loading';
render(
<Route path="/course/:courseId/access-denied">
<CourseAccessErrorPage />
</Route>,
<Routes>
<Route path="/course/:courseId/access-denied" element={<CourseAccessErrorPage />} />
</Routes>,
{ wrapWithRouter: true },
);
expect(screen.getByTestId('page-loading')).toBeInTheDocument();
expect(history.location.pathname).toBe(accessDeniedUrl);
expect(window.location.pathname).toBe(accessDeniedUrl);
});
it('Redirect user to homepage if user has access', () => {
mockCourseStatus = 'loaded';
render(
<Route path="/course/:courseId/access-denied">
<CourseAccessErrorPage />
</Route>,
<Routes>
<Route path="/course/:courseId/access-denied" element={<CourseAccessErrorPage />} />
</Routes>,
{ wrapWithRouter: true },
);
expect(history.location.pathname).toBe('/redirect/home/course-v1:edX+DemoX+Demo_Course');
expect(window.location.pathname).toBe('/redirect/home/course-v1:edX+DemoX+Demo_Course');
});
it('For access denied it should render access denied page', () => {
mockCourseStatus = 'denied';
render(
<Route path="/course/:courseId/access-denied">
<CourseAccessErrorPage />
</Route>,
<Routes>
<Route path="/course/:courseId/access-denied" element={<CourseAccessErrorPage />} />
</Routes>,
{ wrapWithRouter: true },
);
expect(screen.getByTestId('access-denied-main')).toBeInTheDocument();
expect(history.location.pathname).toBe(accessDeniedUrl);
expect(window.location.pathname).toBe(accessDeniedUrl);
});
});

View File

@@ -1,4 +1,4 @@
import { Redirect, useLocation } from 'react-router-dom';
import { Navigate, useLocation } from 'react-router-dom';
import PropTypes from 'prop-types';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
@@ -16,10 +16,10 @@ const PathFixesProvider = ({ children }) => {
// We only check for spaces. That's not the only kind of character that is escaped in URLs, but it would always be
// present for our cases, and I believe it's the only one we use normally.
if (location.pathname.includes(' ')) {
if (location.pathname.includes(' ') || location.pathname.includes('%20')) {
const newLocation = {
...location,
pathname: location.pathname.replaceAll(' ', '+'),
pathname: (location.pathname.replaceAll(' ', '+')).replaceAll('%20', '+'),
};
sendTrackEvent('edx.ui.lms.path_fixed', {
@@ -29,7 +29,7 @@ const PathFixesProvider = ({ children }) => {
search: location.search,
});
return (<Redirect to={newLocation} />);
return (<Navigate to={newLocation} replace />);
}
return children; // pass through

View File

@@ -1,5 +1,7 @@
import React from 'react';
import { MemoryRouter, Route } from 'react-router-dom';
import {
MemoryRouter, Route, Routes, useLocation,
} from 'react-router-dom';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
@@ -19,16 +21,20 @@ describe('PathFixesProvider', () => {
});
function buildAndRender(path) {
const LocationComponent = () => {
testLocation = useLocation();
return null;
};
render(
<MemoryRouter initialEntries={[path]}>
<PathFixesProvider>
<Route
path="*"
render={routeProps => {
testLocation = routeProps.location;
return null;
}}
/>
<Routes>
<Route
path="*"
element={<LocationComponent />}
/>
</Routes>
</PathFixesProvider>
</MemoryRouter>,
);

View File

@@ -6,10 +6,10 @@ import {
mergeConfig,
getConfig,
} from '@edx/frontend-platform';
import { AppProvider, ErrorPage, PageRoute } from '@edx/frontend-platform/react';
import { AppProvider, ErrorPage, PageWrap } from '@edx/frontend-platform/react';
import React from 'react';
import ReactDOM from 'react-dom';
import { Switch } from 'react-router-dom';
import { Routes, Route } from 'react-router-dom';
import { Helmet } from 'react-helmet';
import { fetchDiscussionTab, fetchLiveTab } from './course-home/data/thunks';
@@ -36,6 +36,7 @@ import PathFixesProvider from './generic/path-fixes';
import LiveTab from './course-home/live-tab/LiveTab';
import CourseAccessErrorPage from './generic/CourseAccessErrorPage';
import DecodePageRoute from './decode-page-route';
import { DECODE_ROUTES, ROUTES } from './constants';
subscribe(APP_READY, () => {
ReactDOM.render(
@@ -46,59 +47,91 @@ subscribe(APP_READY, () => {
<PathFixesProvider>
<NoticesProvider>
<UserMessagesProvider>
<Switch>
<PageRoute exact path="/goal-unsubscribe/:token" component={GoalUnsubscribe} />
<PageRoute path="/redirect" component={CoursewareRedirectLandingPage} />
<DecodePageRoute path="/course/:courseId/access-denied" component={CourseAccessErrorPage} />
<DecodePageRoute path="/course/:courseId/home">
<TabContainer tab="outline" fetch={fetchOutlineTab} slice="courseHome">
<OutlineTab />
</TabContainer>
</DecodePageRoute>
<DecodePageRoute path="/course/:courseId/live">
<TabContainer tab="lti_live" fetch={fetchLiveTab} slice="courseHome">
<LiveTab />
</TabContainer>
</DecodePageRoute>
<DecodePageRoute path="/course/:courseId/dates">
<TabContainer tab="dates" fetch={fetchDatesTab} slice="courseHome">
<DatesTab />
</TabContainer>
</DecodePageRoute>
<DecodePageRoute path="/course/:courseId/discussion/:path*">
<TabContainer tab="discussion" fetch={fetchDiscussionTab} slice="courseHome">
<DiscussionTab />
</TabContainer>
</DecodePageRoute>
<DecodePageRoute
path={[
'/course/:courseId/progress/:targetUserId/',
'/course/:courseId/progress',
]}
render={({ match }) => (
<TabContainer
tab="progress"
fetch={(courseId) => fetchProgressTab(courseId, match.params.targetUserId)}
slice="courseHome"
>
<ProgressTab />
</TabContainer>
<Routes>
<Route path={ROUTES.UNSUBSCRIBE} element={<PageWrap><GoalUnsubscribe /></PageWrap>} />
<Route path={ROUTES.REDIRECT} element={<PageWrap><CoursewareRedirectLandingPage /></PageWrap>} />
<Route
path={DECODE_ROUTES.ACCESS_DENIED}
element={<DecodePageRoute><CourseAccessErrorPage /></DecodePageRoute>}
/>
<Route
path={DECODE_ROUTES.HOME}
element={(
<DecodePageRoute>
<TabContainer tab="outline" fetch={fetchOutlineTab} slice="courseHome">
<OutlineTab />
</TabContainer>
</DecodePageRoute>
)}
/>
<Route
path={DECODE_ROUTES.LIVE}
element={(
<DecodePageRoute>
<TabContainer tab="lti_live" fetch={fetchLiveTab} slice="courseHome">
<LiveTab />
</TabContainer>
</DecodePageRoute>
)}
/>
<DecodePageRoute path="/course/:courseId/course-end">
<TabContainer tab="courseware" fetch={fetchCourse} slice="courseware">
<CourseExit />
</TabContainer>
</DecodePageRoute>
<DecodePageRoute
path={[
'/course/:courseId/:sequenceId/:unitId',
'/course/:courseId/:sequenceId',
'/course/:courseId',
]}
component={CoursewareContainer}
<Route
path={DECODE_ROUTES.DATES}
element={(
<DecodePageRoute>
<TabContainer tab="dates" fetch={fetchDatesTab} slice="courseHome">
<DatesTab />
</TabContainer>
</DecodePageRoute>
)}
/>
</Switch>
<Route
path={DECODE_ROUTES.DISCUSSION}
element={(
<DecodePageRoute>
<TabContainer tab="discussion" fetch={fetchDiscussionTab} slice="courseHome">
<DiscussionTab />
</TabContainer>
</DecodePageRoute>
)}
/>
{DECODE_ROUTES.PROGRESS.map((route) => (
<Route
path={route}
element={(
<DecodePageRoute>
<TabContainer
tab="progress"
fetch={fetchProgressTab}
slice="courseHome"
isProgressTab
>
<ProgressTab />
</TabContainer>
</DecodePageRoute>
)}
/>
))}
<Route
path={DECODE_ROUTES.COURSE_END}
element={(
<DecodePageRoute>
<TabContainer tab="courseware" fetch={fetchCourse} slice="courseware">
<CourseExit />
</TabContainer>
</DecodePageRoute>
)}
/>
{DECODE_ROUTES.COURSEWARE.map((route) => (
<Route
path={route}
element={(
<DecodePageRoute>
<CoursewareContainer />
</DecodePageRoute>
)}
/>
))}
</Routes>
</UserMessagesProvider>
</NoticesProvider>
</PathFixesProvider>

View File

@@ -3,7 +3,7 @@
* @jest-environment jsdom
*/
import React from 'react';
import { Route, Switch } from 'react-router';
import { Route, Routes } from 'react-router-dom';
import { Factory } from 'rosie';
import { getConfig, history } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
@@ -26,6 +26,7 @@ import { buildSimpleCourseBlocks } from '../shared/data/__factories__/courseBloc
import { buildOutlineFromBlocks } from '../courseware/data/__factories__/learningSequencesOutline.factory';
import { UserMessagesProvider } from '../generic/user-messages';
import { DECODE_ROUTES } from '../constants';
initializeMockApp();
jest.mock('@edx/frontend-platform/analytics');
@@ -62,7 +63,7 @@ describe('Course Home Tours', () => {
<LoadedTabPage courseId={courseId} activeTabSlug="outline">
<OutlineTab />
</LoadedTabPage>,
{ store },
{ store, wrapWithRouter: true },
);
}
@@ -213,16 +214,14 @@ describe('Courseware Tour', () => {
component = (
<AppProvider store={store}>
<UserMessagesProvider>
<Switch>
<Route
path={[
'/course/:courseId/:sequenceId/:unitId',
'/course/:courseId/:sequenceId',
'/course/:courseId',
]}
component={CoursewareContainer}
/>
</Switch>
<Routes>
{DECODE_ROUTES.COURSEWARE.map((route) => (
<Route
path={route}
element={<CoursewareContainer />}
/>
))}
</Routes>
</UserMessagesProvider>
</AppProvider>
);

View File

@@ -169,13 +169,14 @@ function render(
ui,
{
store = null,
wrapWithRouter = false,
...renderOptions
} = {},
) {
const Wrapper = ({ children }) => (
// eslint-disable-next-line react/jsx-filename-extension
<IntlProvider locale="en">
<AppProvider store={store || globalStore}>
<AppProvider store={store || globalStore} wrapWithRouter={wrapWithRouter}>
<UserMessagesProvider>
{children}
</UserMessagesProvider>

View File

@@ -12,15 +12,21 @@ const TabContainer = (props) => {
fetch,
slice,
tab,
isProgressTab,
} = props;
const { courseId: courseIdFromUrl } = useParams();
const { courseId: courseIdFromUrl, targetUserId } = useParams();
const dispatch = useDispatch();
useEffect(() => {
// The courseId from the URL is the course we WANT to load.
dispatch(fetch(courseIdFromUrl));
if (isProgressTab) {
dispatch(fetch(courseIdFromUrl, targetUserId));
} else {
dispatch(fetch(courseIdFromUrl));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [courseIdFromUrl]);
}, [courseIdFromUrl, targetUserId]);
// The courseId from the store is the course we HAVE loaded. If the URL changes,
// we don't want the application to adjust to it until it has actually loaded the new data.
@@ -47,6 +53,11 @@ TabContainer.propTypes = {
fetch: PropTypes.func.isRequired,
slice: PropTypes.string.isRequired,
tab: PropTypes.string.isRequired,
isProgressTab: PropTypes.bool,
};
TabContainer.defaultProps = {
isProgressTab: false,
};
export default TabContainer;

View File

@@ -1,6 +1,5 @@
import React from 'react';
import { history } from '@edx/frontend-platform';
import { Route } from 'react-router';
import { Route, Routes, MemoryRouter } from 'react-router-dom';
import { initializeTestStore, render, screen } from '../setupTest';
import { TabContainer } from './index';
@@ -31,13 +30,19 @@ describe('Tab Container', () => {
});
it('renders correctly', () => {
history.push(`/course/${courseId}`);
render(
<Route path="/course/:courseId">
<TabContainer {...mockData}>
children={[]}
</TabContainer>
</Route>,
<MemoryRouter initialEntries={[`/course/${courseId}`]}>
<Routes>
<Route
path="/course/:courseId"
element={(
<TabContainer {...mockData}>
children={[]}
</TabContainer>
)}
/>
</Routes>
</MemoryRouter>,
);
expect(mockFetch).toHaveBeenCalledTimes(1);
@@ -49,22 +54,25 @@ describe('Tab Container', () => {
it('Should handle passing in a targetUserId', () => {
const targetUserId = '1';
history.push(`/course/${courseId}/progress/${targetUserId}/`);
render(
<Route
path="/course/:courseId/progress/:targetUserId/"
render={({ match }) => (
<TabContainer
fetch={() => mockFetch(match.params.courseId, match.params.targetUserId)}
tab="dummy"
slice="courseHome"
>
children={[]}
</TabContainer>
)}
/>,
<MemoryRouter initialEntries={[`/course/${courseId}/progress/${targetUserId}/`]}>
<Routes>
<Route
path="/course/:courseId/progress/:targetUserId/"
element={(
<TabContainer
fetch={mockFetch}
tab="dummy"
slice="courseHome"
isProgressTab
>
children={[]}
</TabContainer>
)}
/>
</Routes>
</MemoryRouter>,
);
expect(mockFetch).toHaveBeenCalledTimes(1);

View File

@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useDispatch, useSelector } from 'react-redux';
import { Redirect } from 'react-router';
import { Navigate } from 'react-router-dom';
import Footer from '@edx/frontend-component-footer';
import { Toast } from '@edx/paragon';
@@ -41,7 +41,7 @@ const TabPage = ({ intl, ...props }) => {
if (courseStatus === 'denied') {
const redirectUrl = getAccessDeniedRedirectUrl(courseId, activeTabSlug, courseAccess, start);
if (redirectUrl) {
return (<Redirect to={redirectUrl} />);
return (<Navigate to={redirectUrl} replace />);
}
}