diff --git a/package-lock.json b/package-lock.json
index 5b885e10..91de9b9e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,6 +20,7 @@
"@fortawesome/free-brands-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/react-fontawesome": "^0.1.15",
+ "@openedx/frontend-plugin-framework": "1.0.2",
"@openedx/paragon": "^21.11.3",
"@optimizely/react-sdk": "^2.9.2",
"@redux-beacon/segment": "^1.1.0",
@@ -2169,6 +2170,101 @@
"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": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/@edx/frontend-component-header/-/frontend-component-header-5.0.2.tgz",
+ "integrity": "sha512-73fNNc1X/tevb3/hw7+s22T+nPGlW1yXA7zsT9eRzdH7rBxONfp0Jz7yEdeBvTax9a96PaOht45DA6GX9eG4KA==",
+ "dependencies": {
+ "@fortawesome/fontawesome-svg-core": "6.5.1",
+ "@fortawesome/free-brands-svg-icons": "6.5.1",
+ "@fortawesome/free-regular-svg-icons": "6.5.1",
+ "@fortawesome/free-solid-svg-icons": "6.5.1",
+ "@fortawesome/react-fontawesome": "^0.2.0",
+ "axios-mock-adapter": "1.22.0",
+ "babel-polyfill": "6.26.0",
+ "react-responsive": "8.2.0",
+ "react-transition-group": "4.4.5"
+ },
+ "peerDependencies": {
+ "@edx/frontend-platform": "^7.0.0",
+ "@openedx/paragon": ">= 21.5.7 < 23.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/@fortawesome/fontawesome-common-types": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz",
+ "integrity": "sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A==",
+ "hasInstallScript": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@edx/frontend-component-header/node_modules/@fortawesome/fontawesome-svg-core": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.1.tgz",
+ "integrity": "sha512-MfRCYlQPXoLlpem+egxjfkEuP9UQswTrlCOsknus/NcMoblTH2g0jPrapbcIb04KGA7E2GZxbAccGZfWoYgsrQ==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": "6.5.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@edx/frontend-component-header/node_modules/@fortawesome/free-brands-svg-icons": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.5.1.tgz",
+ "integrity": "sha512-093l7DAkx0aEtBq66Sf19MgoZewv1zeY9/4C7vSKPO4qMwEsW/2VYTUTpBtLwfb9T2R73tXaRDPmE4UqLCYHfg==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": "6.5.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@edx/frontend-component-header/node_modules/@fortawesome/free-solid-svg-icons": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.1.tgz",
+ "integrity": "sha512-S1PPfU3mIJa59biTtXJz1oI0+KAXW6bkAb31XKhxdxtuXDiUIFsih4JR1v5BbxY7hVHsD1RKq+jRkVRaf773NQ==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": "6.5.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@edx/frontend-component-header/node_modules/@fortawesome/react-fontawesome": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.0.tgz",
+ "integrity": "sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw==",
+ "dependencies": {
+ "prop-types": "^15.8.1"
+ },
+ "peerDependencies": {
+ "@fortawesome/fontawesome-svg-core": "~1 || ~6",
+ "react": ">=16.3"
+ }
+ },
+ "node_modules/@edx/frontend-component-header/node_modules/@fortawesome/react-fontawesome/node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.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-enterprise-hotjar": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-enterprise-hotjar/-/frontend-enterprise-hotjar-3.0.0.tgz",
@@ -6479,6 +6575,222 @@
"node": ">=8"
}
},
+ "node_modules/@openedx/frontend-plugin-framework": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@openedx/frontend-plugin-framework/-/frontend-plugin-framework-1.0.2.tgz",
+ "integrity": "sha512-9KVFtVp14foXuZVzIN7EyXMha9fhbu4SwXBle81HFzuHEpm46+qxtiIOktH6b4TYh74SMzuSphgP30MCHr1P8Q==",
+ "dependencies": {
+ "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
+ "@edx/frontend-component-footer": "13.0.3",
+ "@edx/frontend-component-header": "5.0.2",
+ "@edx/frontend-platform": "^7.1.0",
+ "@fortawesome/fontawesome-svg-core": "1.2.36",
+ "@fortawesome/free-brands-svg-icons": "5.15.4",
+ "@fortawesome/free-regular-svg-icons": "5.15.4",
+ "@fortawesome/free-solid-svg-icons": "5.15.4",
+ "@fortawesome/react-fontawesome": "0.2.0",
+ "@openedx/paragon": "^21.0.0",
+ "classnames": "^2.3.2",
+ "core-js": "3.36.0",
+ "prop-types": "15.8.1",
+ "react": "17.0.2",
+ "react-dom": "17.0.2",
+ "react-error-boundary": "^4.0.11",
+ "react-redux": "7.2.9",
+ "react-router": "6.22.2",
+ "react-router-dom": "6.22.2",
+ "redux": "4.2.1",
+ "regenerator-runtime": "0.14.1"
+ }
+ },
+ "node_modules/@openedx/frontend-plugin-framework/node_modules/@edx/frontend-component-footer": {
+ "version": "13.0.3",
+ "resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-13.0.3.tgz",
+ "integrity": "sha512-09vX6qC7AcDwG02qhBzKr4x58hpe9FXZrA9ui2cJnsG53pKaNL+wvOSRtDUBNexCf+y/iPg+8RgR+4alkzhZhw==",
+ "dependencies": {
+ "@fortawesome/fontawesome-svg-core": "6.5.1",
+ "@fortawesome/free-brands-svg-icons": "6.5.1",
+ "@fortawesome/free-regular-svg-icons": "6.5.1",
+ "@fortawesome/free-solid-svg-icons": "6.5.1",
+ "@fortawesome/react-fontawesome": "0.2.0",
+ "lodash": "^4.17.21"
+ },
+ "peerDependencies": {
+ "@edx/frontend-platform": "^7.0.0",
+ "@openedx/paragon": ">= 21.11.3 < 23.0.0",
+ "prop-types": "^15.5.10",
+ "react": "^16.9.0 || ^17.0.0",
+ "react-dom": "^16.9.0 || ^17.0.0"
+ }
+ },
+ "node_modules/@openedx/frontend-plugin-framework/node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/fontawesome-common-types": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz",
+ "integrity": "sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A==",
+ "hasInstallScript": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@openedx/frontend-plugin-framework/node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/fontawesome-svg-core": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.1.tgz",
+ "integrity": "sha512-MfRCYlQPXoLlpem+egxjfkEuP9UQswTrlCOsknus/NcMoblTH2g0jPrapbcIb04KGA7E2GZxbAccGZfWoYgsrQ==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": "6.5.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@openedx/frontend-plugin-framework/node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/free-brands-svg-icons": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.5.1.tgz",
+ "integrity": "sha512-093l7DAkx0aEtBq66Sf19MgoZewv1zeY9/4C7vSKPO4qMwEsW/2VYTUTpBtLwfb9T2R73tXaRDPmE4UqLCYHfg==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": "6.5.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@openedx/frontend-plugin-framework/node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/free-regular-svg-icons": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.5.1.tgz",
+ "integrity": "sha512-m6ShXn+wvqEU69wSP84coxLbNl7sGVZb+Ca+XZq6k30SzuP3X4TfPqtycgUh9ASwlNh5OfQCd8pDIWxl+O+LlQ==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": "6.5.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@openedx/frontend-plugin-framework/node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/free-solid-svg-icons": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.1.tgz",
+ "integrity": "sha512-S1PPfU3mIJa59biTtXJz1oI0+KAXW6bkAb31XKhxdxtuXDiUIFsih4JR1v5BbxY7hVHsD1RKq+jRkVRaf773NQ==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": "6.5.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@openedx/frontend-plugin-framework/node_modules/@fortawesome/free-regular-svg-icons": {
+ "version": "5.15.4",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.15.4.tgz",
+ "integrity": "sha512-9VNNnU3CXHy9XednJ3wzQp6SwNwT3XaM26oS4Rp391GsxVYA+0oDR2J194YCIWf7jNRCYKjUCOduxdceLrx+xw==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": "^0.2.36"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@openedx/frontend-plugin-framework/node_modules/@fortawesome/react-fontawesome": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.0.tgz",
+ "integrity": "sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw==",
+ "dependencies": {
+ "prop-types": "^15.8.1"
+ },
+ "peerDependencies": {
+ "@fortawesome/fontawesome-svg-core": "~1 || ~6",
+ "react": ">=16.3"
+ }
+ },
+ "node_modules/@openedx/frontend-plugin-framework/node_modules/@remix-run/router": {
+ "version": "1.15.2",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.2.tgz",
+ "integrity": "sha512-+Rnav+CaoTE5QJc4Jcwh5toUpnVLKYbpU6Ys0zqbakqbaLQHeglLVHPfxOiQqdNmUy5C2lXz5dwC6tQNX2JW2Q==",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@openedx/frontend-plugin-framework/node_modules/core-js": {
+ "version": "3.36.0",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.36.0.tgz",
+ "integrity": "sha512-mt7+TUBbTFg5+GngsAxeKBTl5/VS0guFeJacYge9OmHb+m058UwwIm41SE9T4Den7ClatV57B6TYTuJ0CX1MAw==",
+ "hasInstallScript": true,
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
+ "node_modules/@openedx/frontend-plugin-framework/node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
+ "node_modules/@openedx/frontend-plugin-framework/node_modules/react-error-boundary": {
+ "version": "4.0.13",
+ "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.13.tgz",
+ "integrity": "sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5"
+ },
+ "peerDependencies": {
+ "react": ">=16.13.1"
+ }
+ },
+ "node_modules/@openedx/frontend-plugin-framework/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/@openedx/frontend-plugin-framework/node_modules/react-router": {
+ "version": "6.22.2",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.2.tgz",
+ "integrity": "sha512-YD3Dzprzpcq+tBMHBS822tCjnWD3iIZbTeSXMY9LPSG541EfoBGyZ3bS25KEnaZjLcmQpw2AVLkFyfgXY8uvcw==",
+ "dependencies": {
+ "@remix-run/router": "1.15.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/@openedx/frontend-plugin-framework/node_modules/react-router-dom": {
+ "version": "6.22.2",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.2.tgz",
+ "integrity": "sha512-WgqxD2qySEIBPZ3w0sHH+PUAiamDeszls9tzqMPBDA1YYVucTBXLU7+gtRfcSnhe92A3glPnvSxK2dhNoAVOIQ==",
+ "dependencies": {
+ "@remix-run/router": "1.15.2",
+ "react-router": "6.22.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
+ "node_modules/@openedx/frontend-plugin-framework/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/@openedx/frontend-plugin-framework/node_modules/regenerator-runtime": {
+ "version": "0.14.1",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
+ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
+ },
"node_modules/@openedx/paragon": {
"version": "21.12.3",
"resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-21.12.3.tgz",
@@ -9033,10 +9345,9 @@
}
},
"node_modules/axios-mock-adapter": {
- "version": "1.21.5",
- "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.21.5.tgz",
- "integrity": "sha512-5NI1V/VK+8+JeTF8niqOowuysA4b8mGzdlMN/QnTnoXbYh4HZSNiopsDclN2g/m85+G++IrEtUdZaQ3GnaMsSA==",
- "dev": true,
+ "version": "1.22.0",
+ "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.22.0.tgz",
+ "integrity": "sha512-dmI0KbkyAhntUR05YY96qg2H6gg0XMl2+qTW0xmYg6Up+BFBAJYRLROMXRdDEL06/Wqwa0TJThAYvFtSFdRCZw==",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"is-buffer": "^2.0.5"
@@ -15570,7 +15881,6 @@
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
"integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==",
- "dev": true,
"funding": [
{
"type": "github",
diff --git a/package.json b/package.json
index 1342a540..6705b2f6 100755
--- a/package.json
+++ b/package.json
@@ -33,6 +33,7 @@
"@edx/frontend-platform": "7.1.0",
"@edx/openedx-atlas": "^0.6.0",
"@edx/react-unit-test-utils": "2.0.0",
+ "@openedx/frontend-plugin-framework": "1.0.2",
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-brands-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
diff --git a/src/containers/CourseFilterControls/ActiveCourseFilters.jsx b/src/containers/CourseFilterControls/ActiveCourseFilters.jsx
index e4fad874..a121c749 100644
--- a/src/containers/CourseFilterControls/ActiveCourseFilters.jsx
+++ b/src/containers/CourseFilterControls/ActiveCourseFilters.jsx
@@ -4,16 +4,17 @@ import { useIntl } from '@edx/frontend-platform/i18n';
import { Button, Chip } from '@openedx/paragon';
import { CloseSmall } from '@openedx/paragon/icons';
+import { reduxHooks } from 'hooks';
import messages from './messages';
import './index.scss';
export const ActiveCourseFilters = ({
filters,
- setFilters,
handleRemoveFilter,
}) => {
const { formatMessage } = useIntl();
+ const clearFilters = reduxHooks.useClearFilters();
return (
{filters.map(filter => (
@@ -25,7 +26,7 @@ export const ActiveCourseFilters = ({
{formatMessage(messages[filter])}
))}
-
@@ -33,10 +34,6 @@ export const ActiveCourseFilters = ({
};
ActiveCourseFilters.propTypes = {
filters: PropTypes.arrayOf(PropTypes.string).isRequired,
- setFilters: PropTypes.shape({
- remove: PropTypes.func,
- clear: PropTypes.func,
- }).isRequired,
handleRemoveFilter: PropTypes.func.isRequired,
};
diff --git a/src/containers/CourseFilterControls/ActiveCourseFilters.test.jsx b/src/containers/CourseFilterControls/ActiveCourseFilters.test.jsx
index e920613c..faf74d44 100644
--- a/src/containers/CourseFilterControls/ActiveCourseFilters.test.jsx
+++ b/src/containers/CourseFilterControls/ActiveCourseFilters.test.jsx
@@ -6,10 +6,6 @@ import ActiveCourseFilters from './ActiveCourseFilters';
describe('ActiveCourseFilters', () => {
const props = {
filters: Object.values(FilterKeys),
- setFilters: {
- remove: jest.fn().mockName('setFilters.remove'),
- clear: jest.fn().mockName('setFilters.clear'),
- },
handleRemoveFilter: jest.fn().mockName('handleRemoveFilter'),
};
describe('snapshot', () => {
diff --git a/src/containers/CourseFilterControls/CourseFilterControls.jsx b/src/containers/CourseFilterControls/CourseFilterControls.jsx
index 46b72a90..4dd98186 100644
--- a/src/containers/CourseFilterControls/CourseFilterControls.jsx
+++ b/src/containers/CourseFilterControls/CourseFilterControls.jsx
@@ -27,7 +27,6 @@ export const CourseFilterControls = ({
sortBy,
setSortBy,
filters,
- setFilters,
}) => {
const { formatMessage } = useIntl();
const hasCourses = reduxHooks.useHasCourses();
@@ -41,7 +40,6 @@ export const CourseFilterControls = ({
handleSortChange,
} = useCourseFilterControlsData({
filters,
- setFilters,
setSortBy,
});
const { width } = useWindowSize();
@@ -112,10 +110,6 @@ CourseFilterControls.propTypes = {
sortBy: PropTypes.string.isRequired,
setSortBy: PropTypes.func.isRequired,
filters: PropTypes.arrayOf(PropTypes.string).isRequired,
- setFilters: PropTypes.shape({
- add: PropTypes.func.isRequired,
- remove: PropTypes.func.isRequired,
- }).isRequired,
};
export default CourseFilterControls;
diff --git a/src/containers/CourseFilterControls/CourseFilterControls.test.jsx b/src/containers/CourseFilterControls/CourseFilterControls.test.jsx
index 2526bdaa..7daee4e5 100644
--- a/src/containers/CourseFilterControls/CourseFilterControls.test.jsx
+++ b/src/containers/CourseFilterControls/CourseFilterControls.test.jsx
@@ -23,10 +23,6 @@ describe('CourseFilterControls', () => {
sortBy: 'test-sort-by',
setSortBy: jest.fn().mockName('setSortBy'),
filters: ['test-filter'],
- setFilters: {
- add: jest.fn().mockName('setFilters.add'),
- remove: jest.fn().mockName('setFilters.remove'),
- },
};
useCourseFilterControlsData.mockReturnValue({
diff --git a/src/containers/CourseFilterControls/__snapshots__/ActiveCourseFilters.test.jsx.snap b/src/containers/CourseFilterControls/__snapshots__/ActiveCourseFilters.test.jsx.snap
index ec20355d..9a9cf832 100644
--- a/src/containers/CourseFilterControls/__snapshots__/ActiveCourseFilters.test.jsx.snap
+++ b/src/containers/CourseFilterControls/__snapshots__/ActiveCourseFilters.test.jsx.snap
@@ -30,7 +30,7 @@ exports[`ActiveCourseFilters snapshot renders 1`] = `
Upgraded
Clear all
diff --git a/src/containers/CourseFilterControls/hooks.js b/src/containers/CourseFilterControls/hooks.js
index 22da132a..b1969019 100644
--- a/src/containers/CourseFilterControls/hooks.js
+++ b/src/containers/CourseFilterControls/hooks.js
@@ -3,6 +3,7 @@ import { useToggle } from '@openedx/paragon';
import { StrictDict } from 'utils';
import track from 'tracking';
+import { reduxHooks } from 'hooks';
import * as module from './hooks';
@@ -10,15 +11,25 @@ export const state = StrictDict({
target: (val) => React.useState(val), // eslint-disable-line
});
+/**
+ * Sets up a toggle for the modal as well as helper functions for handling changes to the form controls.
+ *
+ * @param {array} filters Currently active course filters
+ * @param {function} setSortBy Set function for sorting the course list
+ * @returns {object} data and functions for managing the CourseFilterControls component
+ */
export const useCourseFilterControlsData = ({
filters,
- setFilters,
setSortBy,
}) => {
const [isOpen, toggleOpen, toggleClose] = useToggle(false);
const [target, setTarget] = module.state.target(null);
+
+ const addFilter = reduxHooks.useAddFilter();
+ const removeFilter = reduxHooks.useRemoveFilter();
+
const handleFilterChange = ({ target: { checked, value } }) => {
- const update = checked ? setFilters.add : setFilters.remove;
+ const update = checked ? addFilter : removeFilter;
update(value);
};
const handleSortChange = ({ target: { value } }) => {
diff --git a/src/containers/CourseFilterControls/hooks.test.js b/src/containers/CourseFilterControls/hooks.test.js
index 4c2c86e9..eaff3851 100644
--- a/src/containers/CourseFilterControls/hooks.test.js
+++ b/src/containers/CourseFilterControls/hooks.test.js
@@ -1,6 +1,8 @@
import { useToggle } from '@openedx/paragon';
import { MockUseState } from 'testUtils';
+import { reduxHooks } from 'hooks';
+
import track from 'tracking';
import * as hooks from './hooks';
@@ -12,18 +14,28 @@ jest.mock('tracking', () => ({
},
}));
+jest.mock('hooks', () => ({
+ reduxHooks: {
+ useAddFilter: jest.fn(),
+ useRemoveFilter: jest.fn(),
+ },
+}));
+
const state = new MockUseState(hooks);
describe('CourseFilterControls hooks', () => {
let out;
const filters = ['a', 'b', 'c'];
const setSortBy = jest.fn();
- const setFilters = {
- add: jest.fn(),
- remove: jest.fn(),
- };
+
+ const removeFilter = jest.fn();
+ reduxHooks.useRemoveFilter.mockReturnValue(removeFilter);
+ const addFilter = jest.fn();
+ reduxHooks.useAddFilter.mockReturnValue(addFilter);
+
const toggleOpen = jest.fn();
const toggleClose = jest.fn();
+
describe('state values', () => {
state.testGetter(state.keys.target);
});
@@ -37,7 +49,6 @@ describe('CourseFilterControls hooks', () => {
state.mock();
out = hooks.useCourseFilterControlsData({
filters,
- setFilters,
setSortBy,
});
});
@@ -66,7 +77,6 @@ describe('CourseFilterControls hooks', () => {
state.mockVal(state.keys.target, 'foo');
out = hooks.useCourseFilterControlsData({
filters,
- setFilters,
setSortBy,
});
expect(out.isOpen).toEqual(true);
@@ -81,14 +91,14 @@ describe('CourseFilterControls hooks', () => {
value,
},
});
- expect(setFilters.add).toHaveBeenCalledWith(value);
+ expect(addFilter).toHaveBeenCalledWith(value);
out.handleFilterChange({
target: {
checked: false,
value,
},
});
- expect(setFilters.remove).toHaveBeenCalledWith(value);
+ expect(removeFilter).toHaveBeenCalledWith(value);
});
test('handle sort change', () => {
const value = 'a';
diff --git a/src/containers/CourseList/hooks.js b/src/containers/CourseList/hooks.js
index 2e7709b9..f585b22a 100644
--- a/src/containers/CourseList/hooks.js
+++ b/src/containers/CourseList/hooks.js
@@ -1,6 +1,6 @@
import React from 'react';
-import { useCheckboxSetValues, useWindowSize, breakpoints } from '@openedx/paragon';
+import { useWindowSize, breakpoints } from '@openedx/paragon';
import queryString from 'query-string';
import { ListPageSize, SortKeys } from 'data/constants/app';
@@ -18,31 +18,40 @@ export const state = StrictDict({
sortBy: (val) => React.useState(val), // eslint-disable-line
});
+/**
+ * Filters are fetched from the store and used to generate a list of "visible" courses.
+ * Other values returned and used for the layout of the CourseList component are:
+ * the current page number, the sorting method, and whether or not to enable filters and pagination.
+ *
+ * @returns data for the CourseList component
+ */
export const useCourseListData = () => {
- const [filters, setFilters] = useCheckboxSetValues([]);
- const [sortBy, setSortBy] = module.state.sortBy(SortKeys.enrolled);
+ const filters = reduxHooks.useFilters();
+ const removeFilter = reduxHooks.useRemoveFilter();
const pageNumber = reduxHooks.usePageNumber();
+ const setPageNumber = reduxHooks.useSetPageNumber();
+
+ const [sortBy, setSortBy] = module.state.sortBy(SortKeys.enrolled);
+
const querySearch = queryString.parse(window.location.search, { parseNumbers: true });
- const { numPages, visible } = reduxHooks.useCurrentCourseList({
+ const { numPages, visibleList } = reduxHooks.useCurrentCourseList({
sortBy,
filters,
pageSize: querySearch?.disable_pagination === 1 ? 0 : ListPageSize,
});
- const handleRemoveFilter = (filter) => () => setFilters.remove(filter);
- const setPageNumber = reduxHooks.useSetPageNumber();
+ const handleRemoveFilter = (filter) => () => removeFilter(filter);
return {
pageNumber,
numPages,
setPageNumber,
- visibleList: visible,
+ visibleList,
filterOptions: {
sortBy,
setSortBy,
filters,
- setFilters,
handleRemoveFilter,
},
showFilters: filters.length > 0,
diff --git a/src/containers/CourseList/hooks.test.js b/src/containers/CourseList/hooks.test.js
index 7ed4de3a..86789d18 100644
--- a/src/containers/CourseList/hooks.test.js
+++ b/src/containers/CourseList/hooks.test.js
@@ -1,5 +1,3 @@
-import * as paragon from '@openedx/paragon';
-
import queryString from 'query-string';
import { MockUseState } from 'testUtils';
@@ -12,6 +10,8 @@ jest.mock('hooks', () => ({
useCurrentCourseList: jest.fn(),
usePageNumber: jest.fn(() => 23),
useSetPageNumber: jest.fn(),
+ useFilters: jest.fn(),
+ useRemoveFilter: jest.fn(),
},
}));
@@ -24,20 +24,22 @@ const state = new MockUseState(hooks);
const testList = ['a', 'b'];
const testListData = {
numPages: 52,
- visible: testList,
+ visibleList: testList,
};
const testSortBy = 'fake sort option';
const testFilters = ['some', 'fake', 'filters'];
-const testSetFilters = { add: jest.fn(), remove: jest.fn() };
-const testCheckboxSetValues = [testFilters, testSetFilters];
+
const setPageNumber = jest.fn(val => ({ setPageNumber: val }));
reduxHooks.useSetPageNumber.mockReturnValue(setPageNumber);
+const removeFilter = jest.fn();
+reduxHooks.useRemoveFilter.mockReturnValue(removeFilter);
+reduxHooks.useFilters.mockReturnValue(['some', 'fake', 'filters']);
+
describe('CourseList hooks', () => {
let out;
reduxHooks.useCurrentCourseList.mockReturnValue(testListData);
- paragon.useCheckboxSetValues.mockImplementation(() => testCheckboxSetValues);
describe('state values', () => {
state.testGetter(state.keys.sortBy);
@@ -80,12 +82,12 @@ describe('CourseList hooks', () => {
});
test('numPages and visible list load from useCurrentCourseList hook', () => {
expect(out.numPages).toEqual(testListData.numPages);
- expect(out.visibleList).toEqual(testListData.visible);
+ expect(out.visibleList).toEqual(testListData.visibleList);
});
test('showFilters is true iff filters is not empty', () => {
expect(out.showFilters).toEqual(true);
state.mockVal(state.keys.sortBy, testSortBy);
- paragon.useCheckboxSetValues.mockReturnValueOnce([[], testSetFilters]);
+ reduxHooks.useFilters.mockReturnValueOnce([]);
out = hooks.useCourseListData();
// don't show filter when list is empty.
expect(out.showFilters).toEqual(false);
@@ -95,15 +97,14 @@ describe('CourseList hooks', () => {
expect(out.filterOptions.sortBy).toEqual(testSortBy);
expect(out.filterOptions.setSortBy).toEqual(state.setState.sortBy);
});
- test('filters and setFilters passed by useCheckboxSetValues', () => {
+ test('filters passed by useFilters hook', () => {
expect(out.filterOptions.filters).toEqual(testFilters);
- expect(out.filterOptions.setFilters).toEqual(testSetFilters);
});
test('handleRemoveFilter creates callback to call setFilter.remove', () => {
const cb = out.filterOptions.handleRemoveFilter(testFilters[0]);
- expect(testSetFilters.remove).not.toHaveBeenCalled();
+ expect(removeFilter).not.toHaveBeenCalled();
cb();
- expect(testSetFilters.remove).toHaveBeenCalledWith(testFilters[0]);
+ expect(removeFilter).toHaveBeenCalledWith(testFilters[0]);
});
test('setPageNumber dispatches setPageNumber action with passed value', () => {
expect(out.setPageNumber(2)).toEqual(setPageNumber(2));
diff --git a/src/containers/CourseList/index.jsx b/src/containers/CourseList/index.jsx
index 9ad21666..6ca98196 100644
--- a/src/containers/CourseList/index.jsx
+++ b/src/containers/CourseList/index.jsx
@@ -17,6 +17,11 @@ import messages from './messages';
import './index.scss';
+/**
+ * Renders the list of CourseCards, as well as the controls (CourseFilterControls) for modifying the list.
+ * Also houses the NoCoursesView to display if the user hasn't enrolled in any courses.
+ * @returns List of courses as CourseCards
+ */
export const CourseList = () => {
const { formatMessage } = useIntl();
const hasCourses = reduxHooks.useHasCourses();
@@ -28,6 +33,7 @@ export const CourseList = () => {
visibleList,
} = useCourseListData();
const isCollapsed = useIsCollapsed();
+
return (
diff --git a/src/data/redux/app/reducer.js b/src/data/redux/app/reducer.js
index a8df1ee4..a3f94e64 100644
--- a/src/data/redux/app/reducer.js
+++ b/src/data/redux/app/reducer.js
@@ -10,15 +10,17 @@ const initialState = {
enterpriseDashboard: {},
platformSettings: {},
suggestedCourses: [],
- filterState: {},
selectSessionModal: {},
+ filters: [],
};
export const cardId = (val) => `card-${val}`;
export const today = Date.now();
-// eslint-disable-next-line no-unused-vars
+/**
+ * Creates a redux slice with actions to load dashboard data and manage visual layout
+ */
const app = createSlice({
name: 'app',
initialState,
@@ -49,6 +51,22 @@ const app = createSlice({
selectSessionModal: { cardId: payload },
}),
setPageNumber: (state, { payload }) => ({ ...state, pageNumber: payload }),
+ setFilters: (state, { payload }) => ({
+ ...state,
+ filters: payload,
+ }),
+ addFilter: (state, { payload }) => ({
+ ...state,
+ filters: [...state.filters, payload],
+ }),
+ removeFilter: (state, { payload }) => ({
+ ...state,
+ filters: state.filters.filter(item => item !== payload),
+ }),
+ clearFilters: (state) => ({
+ ...state,
+ filters: [],
+ }),
},
});
diff --git a/src/data/redux/app/reducer.test.js b/src/data/redux/app/reducer.test.js
index 00a98c10..5d542223 100644
--- a/src/data/redux/app/reducer.test.js
+++ b/src/data/redux/app/reducer.test.js
@@ -14,12 +14,14 @@ describe('app reducer', () => {
it('returns initial state', () => {
expect(reducer(undefined, {})).toEqual(initialState);
});
+ const initialFilter = 'initial filter';
const testState = {
...initialState,
enrollments: [],
courseData: {
},
entitlement: [],
+ filters: [initialFilter],
};
describe('action handlers', () => {
describe('loadCourses', () => {
@@ -93,6 +95,30 @@ describe('app reducer', () => {
});
});
});
+ describe('filters', () => {
+ const newFilter = 'new filter';
+ let out;
+ beforeEach(() => {
+ out = reducer(testState, {});
+ });
+ it('overwrites the filters object when using setFilters', () => {
+ expect(out.filters).toEqual([initialFilter]);
+ out = reducer(testState, actions.setFilters([newFilter]));
+ expect(out.filters).toEqual([newFilter]);
+ });
+ it('adds a filter when using addFilter', () => {
+ out = reducer(testState, actions.addFilter(newFilter));
+ expect(out.filters).toEqual([initialFilter, newFilter]);
+ });
+ it('removes a filter when using removeFilter', () => {
+ out = reducer(testState, actions.removeFilter(initialFilter));
+ expect(out.filters).toEqual([]);
+ });
+ it('clears the filters when using clearFilters', () => {
+ out = reducer(testState, actions.clearFilters());
+ expect(out.filters).toEqual([]);
+ });
+ });
});
});
});
diff --git a/src/data/redux/app/selectors/currentList.js b/src/data/redux/app/selectors/currentList.js
index 4e22a77e..9c713eb6 100644
--- a/src/data/redux/app/selectors/currentList.js
+++ b/src/data/redux/app/selectors/currentList.js
@@ -50,7 +50,7 @@ export const visibleList = (state, {
};
}
return {
- visible: list.slice((pageNumber - 1) * pageSize, pageNumber * pageSize),
+ visibleList: list.slice((pageNumber - 1) * pageSize, pageNumber * pageSize),
numPages: Math.ceil(list.length / pageSize),
};
};
diff --git a/src/data/redux/app/selectors/currentList.test.js b/src/data/redux/app/selectors/currentList.test.js
index d8bff131..058d5619 100644
--- a/src/data/redux/app/selectors/currentList.test.js
+++ b/src/data/redux/app/selectors/currentList.test.js
@@ -171,7 +171,7 @@ describe('courseList selector module', () => {
});
it('returns visible page based on passed page size and stored pageNumber', () => {
// page 3, 2 per page. [0 1] [2 3] [4 5] ...
- expect(out.visible).toEqual([testList[4], testList[5]]);
+ expect(out.visibleList).toEqual([testList[4], testList[5]]);
});
it('returns number of pages based on page size and list length', () => {
expect(out.numPages).toEqual(6);
diff --git a/src/data/redux/app/selectors/simpleSelectors.js b/src/data/redux/app/selectors/simpleSelectors.js
index c8ce2a0f..0c576fc2 100644
--- a/src/data/redux/app/selectors/simpleSelectors.js
+++ b/src/data/redux/app/selectors/simpleSelectors.js
@@ -15,6 +15,7 @@ export const simpleSelectors = StrictDict({
enterpriseDashboard: mkSimpleSelector(app => app.enterpriseDashboard || {}),
selectSessionModal: mkSimpleSelector(app => app.selectSessionModal),
pageNumber: mkSimpleSelector(app => app.pageNumber),
+ filters: mkSimpleSelector(app => app.filters),
socialShareSettings: mkSimpleSelector(app => app.socialShareSettings),
});
diff --git a/src/data/redux/hooks/app.js b/src/data/redux/hooks/app.js
index dd74aa8b..06a5c352 100644
--- a/src/data/redux/hooks/app.js
+++ b/src/data/redux/hooks/app.js
@@ -9,6 +9,7 @@ const actions = redux.actions.app;
/** Simple Selectors **/
export const usePageNumber = () => useSelector(selectors.pageNumber);
+export const useFilters = () => useSelector(selectors.filters);
export const useEmailConfirmationData = () => useSelector(selectors.emailConfirmation);
export const useEnterpriseDashboardData = () => useSelector(selectors.enterpriseDashboard);
export const usePlatformSettingsData = () => useSelector(selectors.platformSettings);
@@ -77,6 +78,26 @@ export const useSetPageNumber = () => {
return (value) => dispatch(actions.setPageNumber(value));
};
+export const useSetFilters = () => {
+ const dispatch = useDispatch();
+ return (value) => dispatch(actions.setFilters(value));
+};
+
+export const useAddFilter = () => {
+ const dispatch = useDispatch();
+ return (value) => dispatch(actions.addFilter(value));
+};
+
+export const useRemoveFilter = () => {
+ const dispatch = useDispatch();
+ return (value) => dispatch(actions.removeFilter(value));
+};
+
+export const useClearFilters = () => {
+ const dispatch = useDispatch();
+ return (value) => dispatch(actions.clearFilters(value));
+};
+
export const useLoadData = () => {
const dispatch = useDispatch();
return ({ courses, ...globalData }) => {
diff --git a/src/data/redux/index.js b/src/data/redux/index.js
index 827570b1..24e6e10b 100644
--- a/src/data/redux/index.js
+++ b/src/data/redux/index.js
@@ -10,6 +10,14 @@ const modules = {
requests,
};
+/**
+ * Extracts keys from the modules object and the provided propName parameter to locate the
+ * corresponding object for that propName.
+ * Example: moduleProps('reducer') will return an aggregated object containing the reducer for each module
+ *
+ * @param {string} propName Used to locate the prop in each module
+ * @returns {object} Aggregated values for the provided propName
+ */
const moduleProps = (propName) => Object.keys(modules).reduce(
(obj, moduleKey) => {
const value = modules[moduleKey][propName];