Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2540bc3a0 | ||
|
|
ffb5a765e2 | ||
|
|
952e543217 | ||
|
|
45a1da9f5e | ||
|
|
022515d1d2 | ||
|
|
2d737aae7f | ||
|
|
4c4db14eac | ||
|
|
911cea6a0e | ||
|
|
a52ddfd9bd | ||
|
|
8175ba897a | ||
|
|
cfda72b2e2 | ||
|
|
4483a734bc | ||
|
|
db1903cdce | ||
|
|
71851b13a6 | ||
|
|
6efa31092d | ||
|
|
c3541a3d79 | ||
|
|
dad01fcd78 | ||
|
|
30e6eed60d | ||
|
|
4e718f85de | ||
|
|
a211547a1d | ||
|
|
784e9afccf | ||
|
|
4b23d8c4e4 | ||
|
|
6d02e63d08 | ||
|
|
b1feed2443 | ||
|
|
cabf4e3f27 | ||
|
|
78a40d47c1 | ||
|
|
18a6840037 | ||
|
|
3276496523 | ||
|
|
7ab55175b5 | ||
|
|
72e82005c0 | ||
|
|
c4df727178 | ||
|
|
642be093c7 | ||
|
|
86939a2559 | ||
|
|
8ed18f3d69 | ||
|
|
061746da9f | ||
|
|
de77aa5f0c | ||
|
|
7034d10536 | ||
|
|
4ce7311809 | ||
|
|
e76f5b6937 | ||
|
|
f8fc794458 | ||
|
|
a5069edd94 |
@@ -4,7 +4,8 @@ import React from 'react';
|
|||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { initialize, getConfig, subscribe, APP_READY } from '@edx/frontend-platform';
|
import { initialize, getConfig, subscribe, APP_READY } from '@edx/frontend-platform';
|
||||||
import { AppContext, AppProvider } from '@edx/frontend-platform/react';
|
import { AppContext, AppProvider } from '@edx/frontend-platform/react';
|
||||||
import Header from '@edx/frontend-component-header';
|
// import Header from '@edx/frontend-component-header';
|
||||||
|
import { LearningHeader as Header } from '@edx/frontend-component-header';
|
||||||
|
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
|
||||||
|
|||||||
208
package-lock.json
generated
208
package-lock.json
generated
@@ -9,23 +9,31 @@
|
|||||||
"version": "1.0.0-semantically-released",
|
"version": "1.0.0-semantically-released",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@edx/paragon": "20.44.0",
|
"@edx/paragon": "20.45.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "6.3.0",
|
"@fortawesome/fontawesome-svg-core": "6.3.0",
|
||||||
"@fortawesome/free-brands-svg-icons": "6.3.0",
|
"@fortawesome/free-brands-svg-icons": "6.3.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "6.3.0",
|
"@fortawesome/free-regular-svg-icons": "6.3.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "6.3.0",
|
"@fortawesome/free-solid-svg-icons": "6.3.0",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
|
"@reduxjs/toolkit": "1.9.5",
|
||||||
|
"axios-mock-adapter": "1.21.5",
|
||||||
"babel-polyfill": "6.26.0",
|
"babel-polyfill": "6.26.0",
|
||||||
|
"classnames": "2.3.2",
|
||||||
|
"lodash": "4.17.21",
|
||||||
|
"react-redux": "7.2.9",
|
||||||
"react-responsive": "8.2.0",
|
"react-responsive": "8.2.0",
|
||||||
"react-transition-group": "4.4.5"
|
"react-router-dom": "5.3.4",
|
||||||
|
"react-transition-group": "4.4.5",
|
||||||
|
"rosie": "2.1.0",
|
||||||
|
"timeago.js": "4.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
|
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
|
||||||
"@edx/browserslist-config": "^1.1.1",
|
"@edx/browserslist-config": "^1.1.1",
|
||||||
"@edx/frontend-build": "12.8.51",
|
"@edx/frontend-build": "12.8.57",
|
||||||
"@edx/frontend-platform": "4.5.1",
|
"@edx/frontend-platform": "4.5.1",
|
||||||
"@edx/reactifex": "^2.1.1",
|
"@edx/reactifex": "^2.1.1",
|
||||||
"@testing-library/dom": "9.3.0",
|
"@testing-library/dom": "9.3.1",
|
||||||
"@testing-library/jest-dom": "5.16.5",
|
"@testing-library/jest-dom": "5.16.5",
|
||||||
"@testing-library/react": "10.4.9",
|
"@testing-library/react": "10.4.9",
|
||||||
"enzyme": "3.11.0",
|
"enzyme": "3.11.0",
|
||||||
@@ -37,7 +45,6 @@
|
|||||||
"react": "16.14.0",
|
"react": "16.14.0",
|
||||||
"react-dom": "16.14.0",
|
"react-dom": "16.14.0",
|
||||||
"react-redux": "7.2.9",
|
"react-redux": "7.2.9",
|
||||||
"react-router-dom": "5.3.4",
|
|
||||||
"react-test-renderer": "16.14.0",
|
"react-test-renderer": "16.14.0",
|
||||||
"redux": "4.2.1",
|
"redux": "4.2.1",
|
||||||
"redux-saga": "1.2.3"
|
"redux-saga": "1.2.3"
|
||||||
@@ -2144,9 +2151,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@edx/frontend-build": {
|
"node_modules/@edx/frontend-build": {
|
||||||
"version": "12.8.51",
|
"version": "12.8.57",
|
||||||
"resolved": "https://registry.npmjs.org/@edx/frontend-build/-/frontend-build-12.8.51.tgz",
|
"resolved": "https://registry.npmjs.org/@edx/frontend-build/-/frontend-build-12.8.57.tgz",
|
||||||
"integrity": "sha512-UsOJ7kN/ECxTDxGYbigGOLQnNtTSmwXqJTed3AUh292U3LEobqho8/C9z2j18xyXhbLY8TJlCWrPkhPFtqFOug==",
|
"integrity": "sha512-mbWyGtF351pKs2/wtN0B16xor5ZowDw70ddBPqE5sasbIlWQw82Yx8Wie6vBjDAweCNDffOCKuIycYJYilWO8Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/cli": "7.22.5",
|
"@babel/cli": "7.22.5",
|
||||||
@@ -2182,7 +2189,7 @@
|
|||||||
"file-loader": "6.2.0",
|
"file-loader": "6.2.0",
|
||||||
"html-webpack-plugin": "5.5.3",
|
"html-webpack-plugin": "5.5.3",
|
||||||
"identity-obj-proxy": "3.0.0",
|
"identity-obj-proxy": "3.0.0",
|
||||||
"image-minimizer-webpack-plugin": "3.8.2",
|
"image-minimizer-webpack-plugin": "3.8.3",
|
||||||
"jest": "26.6.3",
|
"jest": "26.6.3",
|
||||||
"mini-css-extract-plugin": "1.6.2",
|
"mini-css-extract-plugin": "1.6.2",
|
||||||
"postcss": "8.4.24",
|
"postcss": "8.4.24",
|
||||||
@@ -2192,7 +2199,7 @@
|
|||||||
"react-dev-utils": "12.0.1",
|
"react-dev-utils": "12.0.1",
|
||||||
"react-refresh": "0.14.0",
|
"react-refresh": "0.14.0",
|
||||||
"resolve-url-loader": "5.0.0",
|
"resolve-url-loader": "5.0.0",
|
||||||
"sass": "1.63.3",
|
"sass": "1.63.6",
|
||||||
"sass-loader": "13.3.2",
|
"sass-loader": "13.3.2",
|
||||||
"sharp": "^0.32.0",
|
"sharp": "^0.32.0",
|
||||||
"source-map-loader": "^4.0.1",
|
"source-map-loader": "^4.0.1",
|
||||||
@@ -2944,9 +2951,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@edx/frontend-build/node_modules/jest-snapshot/node_modules/semver": {
|
"node_modules/@edx/frontend-build/node_modules/jest-snapshot/node_modules/semver": {
|
||||||
"version": "7.5.1",
|
"version": "7.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz",
|
||||||
"integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==",
|
"integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lru-cache": "^6.0.0"
|
"lru-cache": "^6.0.0"
|
||||||
@@ -3248,9 +3255,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@edx/paragon": {
|
"node_modules/@edx/paragon": {
|
||||||
"version": "20.44.0",
|
"version": "20.45.0",
|
||||||
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.44.0.tgz",
|
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.45.0.tgz",
|
||||||
"integrity": "sha512-C1uC3RaRmlFANtHebFdZzVDM08vgFJRnHE3u97ix07e0ACSQDbVNoZ2H7JgBy8nqHz2JWGHPnvtpvPf5DAZsZQ==",
|
"integrity": "sha512-9lHcnSJ36sQ+bsYFhydf/Pvp3Bo5N3go8R3ORPTNtvYnwiKSfjlv11QpURC/xHobXsT2eYHiwl2gNmq1yP09BA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
||||||
"@fortawesome/react-fontawesome": "^0.1.18",
|
"@fortawesome/react-fontawesome": "^0.1.18",
|
||||||
@@ -3334,9 +3341,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@edx/reactifex": {
|
"node_modules/@edx/reactifex": {
|
||||||
"version": "2.1.1",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@edx/reactifex/-/reactifex-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@edx/reactifex/-/reactifex-2.2.0.tgz",
|
||||||
"integrity": "sha512-A/DfCPsNNRuWhhWCquInlfG6Pi//qcxAi0P2jY/UeOVAHoOLkA3L328UtHEuoZbncXT2E1H1EDlpfNrovo/nng==",
|
"integrity": "sha512-vyGDtx3BwCr6Gjbm4y6gJ8Bzc2TOSNBlBa2hMerz59HoXaot14MihxxiDU+JDNybGLLcKDBiK511bOi/77i1lw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
@@ -3370,9 +3377,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@edx/reactifex/node_modules/yargs": {
|
"node_modules/@edx/reactifex/node_modules/yargs": {
|
||||||
"version": "17.7.1",
|
"version": "17.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||||
"integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==",
|
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cliui": "^8.0.1",
|
"cliui": "^8.0.1",
|
||||||
@@ -5807,6 +5814,29 @@
|
|||||||
"integrity": "sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA==",
|
"integrity": "sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"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/@restart/context": {
|
"node_modules/@restart/context": {
|
||||||
"version": "2.1.4",
|
"version": "2.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@restart/context/-/context-2.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@restart/context/-/context-2.1.4.tgz",
|
||||||
@@ -6108,15 +6138,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@testing-library/dom": {
|
"node_modules/@testing-library/dom": {
|
||||||
"version": "9.3.0",
|
"version": "9.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.1.tgz",
|
||||||
"integrity": "sha512-Dffe68pGwI6WlLRYR2I0piIkyole9cSBH5jGQKCGMRpHW5RHCqAUaqc2Kv0tUyd4dU4DLPKhJIjyKOnjv4tuUw==",
|
"integrity": "sha512-0DGPd9AR3+iDTjGoMpxIkAsUihHZ3Ai6CneU6bRRrffXMgzCdlNk43jTrD2/5LT6CBb3MWTP8v510JzYtahD2w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.10.4",
|
"@babel/code-frame": "^7.10.4",
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@types/aria-query": "^5.0.1",
|
"@types/aria-query": "^5.0.1",
|
||||||
"aria-query": "^5.0.0",
|
"aria-query": "5.1.3",
|
||||||
"chalk": "^4.1.0",
|
"chalk": "^4.1.0",
|
||||||
"dom-accessibility-api": "^0.5.9",
|
"dom-accessibility-api": "^0.5.9",
|
||||||
"lz-string": "^1.5.0",
|
"lz-string": "^1.5.0",
|
||||||
@@ -6785,7 +6815,7 @@
|
|||||||
"version": "7.1.25",
|
"version": "7.1.25",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.25.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.25.tgz",
|
||||||
"integrity": "sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg==",
|
"integrity": "sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/hoist-non-react-statics": "^3.3.0",
|
"@types/hoist-non-react-statics": "^3.3.0",
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -7681,8 +7711,7 @@
|
|||||||
"node_modules/asynckit": {
|
"node_modules/asynckit": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/at-least-node": {
|
"node_modules/at-least-node": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@@ -7771,7 +7800,6 @@
|
|||||||
"version": "0.27.2",
|
"version": "0.27.2",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
|
||||||
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
|
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.14.9",
|
"follow-redirects": "^1.14.9",
|
||||||
"form-data": "^4.0.0"
|
"form-data": "^4.0.0"
|
||||||
@@ -7791,6 +7819,40 @@
|
|||||||
"url": "https://github.com/ArthurFiorette/axios-cache-interceptor?sponsor=1"
|
"url": "https://github.com/ArthurFiorette/axios-cache-interceptor?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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==",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"is-buffer": "^2.0.5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"axios": ">= 0.17.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/axios-mock-adapter/node_modules/is-buffer": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/axobject-query": {
|
"node_modules/axobject-query": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz",
|
||||||
@@ -9138,7 +9200,6 @@
|
|||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"delayed-stream": "~1.0.0"
|
"delayed-stream": "~1.0.0"
|
||||||
},
|
},
|
||||||
@@ -9868,7 +9929,6 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
@@ -11746,8 +11806,7 @@
|
|||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/fast-defer": {
|
"node_modules/fast-defer": {
|
||||||
"version": "1.1.7",
|
"version": "1.1.7",
|
||||||
@@ -12024,7 +12083,6 @@
|
|||||||
"version": "1.15.2",
|
"version": "1.15.2",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "individual",
|
"type": "individual",
|
||||||
@@ -12255,7 +12313,6 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"asynckit": "^0.4.0",
|
"asynckit": "^0.4.0",
|
||||||
"combined-stream": "^1.0.8",
|
"combined-stream": "^1.0.8",
|
||||||
@@ -12841,7 +12898,6 @@
|
|||||||
"version": "4.10.1",
|
"version": "4.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
|
||||||
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
|
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.1.2",
|
"@babel/runtime": "^7.1.2",
|
||||||
"loose-envify": "^1.2.0",
|
"loose-envify": "^1.2.0",
|
||||||
@@ -13200,12 +13256,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/image-minimizer-webpack-plugin": {
|
"node_modules/image-minimizer-webpack-plugin": {
|
||||||
"version": "3.8.2",
|
"version": "3.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/image-minimizer-webpack-plugin/-/image-minimizer-webpack-plugin-3.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/image-minimizer-webpack-plugin/-/image-minimizer-webpack-plugin-3.8.3.tgz",
|
||||||
"integrity": "sha512-l3nDq/c15y4ViTPtICG6lbFp77SoycSnR1hT/n3ER76uol//OpRptCDl7U1qiDSSEy2AcqPD1T7isRQ8TK27Cw==",
|
"integrity": "sha512-Ex0cjNJc2FUSuwN7WHNyxkIZINP0M9lrN+uWJznMcsehiM5Z7ELwk+SEkSGEookK1GUd2wf+09jy1PEH5a5XmQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"schema-utils": "^4.0.0",
|
"schema-utils": "^4.2.0",
|
||||||
"serialize-javascript": "^6.0.1"
|
"serialize-javascript": "^6.0.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -13268,9 +13324,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/image-minimizer-webpack-plugin/node_modules/schema-utils": {
|
"node_modules/image-minimizer-webpack-plugin/node_modules/schema-utils": {
|
||||||
"version": "4.0.1",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz",
|
||||||
"integrity": "sha512-lELhBAAly9NowEsX0yZBlw9ahZG+sK/1RJ21EpzdYHKEs13Vku3LJ+MIPhh4sMs0oCCeufZQEQbMekiA4vuVIQ==",
|
"integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/json-schema": "^7.0.9",
|
"@types/json-schema": "^7.0.9",
|
||||||
@@ -13296,7 +13352,6 @@
|
|||||||
"version": "9.0.21",
|
"version": "9.0.21",
|
||||||
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
|
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
|
||||||
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
|
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
|
||||||
"dev": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
"url": "https://opencollective.com/immer"
|
"url": "https://opencollective.com/immer"
|
||||||
@@ -18918,8 +18973,7 @@
|
|||||||
"node_modules/lodash": {
|
"node_modules/lodash": {
|
||||||
"version": "4.17.21",
|
"version": "4.17.21",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lodash.camelcase": {
|
"node_modules/lodash.camelcase": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
@@ -19178,7 +19232,6 @@
|
|||||||
"version": "1.52.0",
|
"version": "1.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
@@ -19187,7 +19240,6 @@
|
|||||||
"version": "2.1.35",
|
"version": "2.1.35",
|
||||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mime-db": "1.52.0"
|
"mime-db": "1.52.0"
|
||||||
},
|
},
|
||||||
@@ -20206,7 +20258,6 @@
|
|||||||
"version": "1.8.0",
|
"version": "1.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
|
||||||
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
|
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"isarray": "0.0.1"
|
"isarray": "0.0.1"
|
||||||
}
|
}
|
||||||
@@ -20214,8 +20265,7 @@
|
|||||||
"node_modules/path-to-regexp/node_modules/isarray": {
|
"node_modules/path-to-regexp/node_modules/isarray": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
||||||
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
|
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/path-type": {
|
"node_modules/path-type": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
@@ -21825,7 +21875,7 @@
|
|||||||
"version": "17.0.2",
|
"version": "17.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||||
"dev": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
"node_modules/react-lifecycles-compat": {
|
"node_modules/react-lifecycles-compat": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
@@ -21882,7 +21932,7 @@
|
|||||||
"version": "7.2.9",
|
"version": "7.2.9",
|
||||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz",
|
||||||
"integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==",
|
"integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.15.4",
|
"@babel/runtime": "^7.15.4",
|
||||||
"@types/react-redux": "^7.1.20",
|
"@types/react-redux": "^7.1.20",
|
||||||
@@ -21978,7 +22028,6 @@
|
|||||||
"version": "5.3.4",
|
"version": "5.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz",
|
||||||
"integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==",
|
"integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.13",
|
"@babel/runtime": "^7.12.13",
|
||||||
"history": "^4.9.0",
|
"history": "^4.9.0",
|
||||||
@@ -21998,7 +22047,6 @@
|
|||||||
"version": "5.3.4",
|
"version": "5.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz",
|
||||||
"integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==",
|
"integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.13",
|
"@babel/runtime": "^7.12.13",
|
||||||
"history": "^4.9.0",
|
"history": "^4.9.0",
|
||||||
@@ -22015,8 +22063,7 @@
|
|||||||
"node_modules/react-router/node_modules/react-is": {
|
"node_modules/react-router/node_modules/react-is": {
|
||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/react-style-singleton": {
|
"node_modules/react-style-singleton": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.1",
|
||||||
@@ -22270,7 +22317,6 @@
|
|||||||
"version": "4.2.1",
|
"version": "4.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
|
||||||
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
|
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.9.2"
|
"@babel/runtime": "^7.9.2"
|
||||||
}
|
}
|
||||||
@@ -22284,6 +22330,14 @@
|
|||||||
"@redux-saga/core": "^1.2.3"
|
"@redux-saga/core": "^1.2.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/redux-thunk": {
|
||||||
|
"version": "2.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz",
|
||||||
|
"integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"redux": "^4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/reflect.ownkeys": {
|
"node_modules/reflect.ownkeys": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz",
|
||||||
@@ -22472,6 +22526,11 @@
|
|||||||
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/reselect": {
|
||||||
|
"version": "4.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz",
|
||||||
|
"integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="
|
||||||
|
},
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.2",
|
"version": "1.22.2",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
|
||||||
@@ -22513,8 +22572,7 @@
|
|||||||
"node_modules/resolve-pathname": {
|
"node_modules/resolve-pathname": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
|
||||||
"integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==",
|
"integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/resolve-url": {
|
"node_modules/resolve-url": {
|
||||||
"version": "0.2.1",
|
"version": "0.2.1",
|
||||||
@@ -22597,6 +22655,14 @@
|
|||||||
"rimraf": "bin.js"
|
"rimraf": "bin.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rosie": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/rosie/-/rosie-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-Dbzdc+prLXZuB/suRptDnBUY29SdGvND3bLg6cll8n7PNqzuyCxSlRfrkn8PqjS9n4QVsiM7RCvxCkKAkTQRjA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/rst-selector-parser": {
|
"node_modules/rst-selector-parser": {
|
||||||
"version": "2.2.3",
|
"version": "2.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz",
|
||||||
@@ -23001,9 +23067,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sass": {
|
"node_modules/sass": {
|
||||||
"version": "1.63.3",
|
"version": "1.63.6",
|
||||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.63.3.tgz",
|
"resolved": "https://registry.npmjs.org/sass/-/sass-1.63.6.tgz",
|
||||||
"integrity": "sha512-ySdXN+DVpfwq49jG1+hmtDslYqpS7SkOR5GpF6o2bmb1RL/xS+wvPmegMvMywyfsmAV6p7TgwXYGrCZIFFbAHg==",
|
"integrity": "sha512-MJuxGMHzaOW7ipp+1KdELtqKbfAWbH7OLIdoSMnVe3EXPMTmxTmlaZDCTsgIpPCs3w99lLo9/zDKkOrJuT5byw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chokidar": ">=3.0.0 <4.0.0",
|
"chokidar": ">=3.0.0 <4.0.0",
|
||||||
@@ -24714,17 +24780,20 @@
|
|||||||
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
|
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"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/tiny-invariant": {
|
"node_modules/tiny-invariant": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz",
|
||||||
"integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==",
|
"integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/tiny-warning": {
|
"node_modules/tiny-warning": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
||||||
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==",
|
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/tmpl": {
|
"node_modules/tmpl": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
@@ -25366,8 +25435,7 @@
|
|||||||
"node_modules/value-equal": {
|
"node_modules/value-equal": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
|
||||||
"integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==",
|
"integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/vary": {
|
"node_modules/vary": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
|
|||||||
17
package.json
17
package.json
@@ -35,10 +35,10 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
|
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
|
||||||
"@edx/browserslist-config": "^1.1.1",
|
"@edx/browserslist-config": "^1.1.1",
|
||||||
"@edx/frontend-build": "12.8.51",
|
"@edx/frontend-build": "12.8.57",
|
||||||
"@edx/frontend-platform": "4.5.1",
|
"@edx/frontend-platform": "4.5.1",
|
||||||
"@edx/reactifex": "^2.1.1",
|
"@edx/reactifex": "^2.1.1",
|
||||||
"@testing-library/dom": "9.3.0",
|
"@testing-library/dom": "9.3.1",
|
||||||
"@testing-library/jest-dom": "5.16.5",
|
"@testing-library/jest-dom": "5.16.5",
|
||||||
"@testing-library/react": "10.4.9",
|
"@testing-library/react": "10.4.9",
|
||||||
"enzyme": "3.11.0",
|
"enzyme": "3.11.0",
|
||||||
@@ -50,21 +50,28 @@
|
|||||||
"react": "16.14.0",
|
"react": "16.14.0",
|
||||||
"react-dom": "16.14.0",
|
"react-dom": "16.14.0",
|
||||||
"react-redux": "7.2.9",
|
"react-redux": "7.2.9",
|
||||||
"react-router-dom": "5.3.4",
|
|
||||||
"react-test-renderer": "16.14.0",
|
"react-test-renderer": "16.14.0",
|
||||||
"redux": "4.2.1",
|
"redux": "4.2.1",
|
||||||
"redux-saga": "1.2.3"
|
"redux-saga": "1.2.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@edx/paragon": "20.44.0",
|
"@edx/paragon": "20.45.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "6.3.0",
|
"@fortawesome/fontawesome-svg-core": "6.3.0",
|
||||||
"@fortawesome/free-brands-svg-icons": "6.3.0",
|
"@fortawesome/free-brands-svg-icons": "6.3.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "6.3.0",
|
"@fortawesome/free-regular-svg-icons": "6.3.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "6.3.0",
|
"@fortawesome/free-solid-svg-icons": "6.3.0",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
|
"@reduxjs/toolkit": "1.9.5",
|
||||||
|
"axios-mock-adapter": "1.21.5",
|
||||||
"babel-polyfill": "6.26.0",
|
"babel-polyfill": "6.26.0",
|
||||||
|
"classnames": "2.3.2",
|
||||||
|
"lodash": "4.17.21",
|
||||||
|
"react-redux": "7.2.9",
|
||||||
"react-responsive": "8.2.0",
|
"react-responsive": "8.2.0",
|
||||||
"react-transition-group": "4.4.5"
|
"react-router-dom": "5.3.4",
|
||||||
|
"react-transition-group": "4.4.5",
|
||||||
|
"rosie": "2.1.0",
|
||||||
|
"timeago.js": "4.0.2"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@edx/frontend-platform": "^4.0.0",
|
"@edx/frontend-platform": "^4.0.0",
|
||||||
|
|||||||
@@ -2,24 +2,38 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
import TestRenderer from 'react-test-renderer';
|
import TestRenderer from 'react-test-renderer';
|
||||||
import { AppContext } from '@edx/frontend-platform/react';
|
import { AppContext, AppProvider } from '@edx/frontend-platform/react';
|
||||||
import { Context as ResponsiveContext } from 'react-responsive';
|
import { Context as ResponsiveContext } from 'react-responsive';
|
||||||
|
import { initializeMockApp } from '@edx/frontend-platform';
|
||||||
|
import store from './store';
|
||||||
|
|
||||||
import Header from './index';
|
import Header from './index';
|
||||||
|
|
||||||
const HeaderComponent = ({ width, contextValue }) => (
|
const HeaderComponent = ({ width, contextValue }) => (
|
||||||
<ResponsiveContext.Provider value={width}>
|
<ResponsiveContext.Provider value={width}>
|
||||||
<IntlProvider locale="en" messages={{}}>
|
<IntlProvider locale="en" messages={{}}>
|
||||||
<AppContext.Provider
|
<AppProvider store={store}>
|
||||||
value={contextValue}
|
<AppContext.Provider
|
||||||
>
|
value={contextValue}
|
||||||
<Header />
|
>
|
||||||
</AppContext.Provider>
|
<Header />
|
||||||
|
</AppContext.Provider>
|
||||||
|
</AppProvider>
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
</ResponsiveContext.Provider>
|
</ResponsiveContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
||||||
describe('<Header />', () => {
|
describe('<Header />', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
initializeMockApp({
|
||||||
|
authenticatedUser: {
|
||||||
|
userId: '123abc',
|
||||||
|
username: 'testuser',
|
||||||
|
administrator: false,
|
||||||
|
roles: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
it('renders correctly for anonymous desktop', () => {
|
it('renders correctly for anonymous desktop', () => {
|
||||||
const contextValue = {
|
const contextValue = {
|
||||||
authenticatedUser: null,
|
authenticatedUser: null,
|
||||||
|
|||||||
71
src/Notifications/NotificationRowItem.jsx
Normal file
71
src/Notifications/NotificationRowItem.jsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Icon } from '@edx/paragon';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import * as timeago from 'timeago.js';
|
||||||
|
import { getIconByType } from './utils';
|
||||||
|
import { markNotificationsAsRead } from './data/thunks';
|
||||||
|
import messages from './messages';
|
||||||
|
import timeLocale from '../common/time-locale';
|
||||||
|
|
||||||
|
const NotificationRowItem = ({
|
||||||
|
id, type, contentUrl, content, courseName, createdAt, lastRead,
|
||||||
|
}) => {
|
||||||
|
timeago.register('time-locale', timeLocale);
|
||||||
|
const intl = useIntl();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const handleMarkAsRead = useCallback(() => {
|
||||||
|
dispatch(markNotificationsAsRead(id));
|
||||||
|
}, [dispatch, id]);
|
||||||
|
|
||||||
|
const { icon: iconComponent, class: iconClass } = getIconByType(type);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
target="_blank"
|
||||||
|
className="d-flex mb-2 align-items-center text-decoration-none"
|
||||||
|
to={contentUrl}
|
||||||
|
onClick={handleMarkAsRead}
|
||||||
|
>
|
||||||
|
<Icon src={iconComponent} className={`${iconClass} mr-4 notification-icon`} />
|
||||||
|
<div className="d-flex w-100">
|
||||||
|
<div className="d-flex align-items-center w-100">
|
||||||
|
<div className="py-10px w-100 px-0 cursor-pointer">
|
||||||
|
<span
|
||||||
|
className="line-height-24 text-gray-700 mb-2 notification-item-content overflow-hidden content"
|
||||||
|
// eslint-disable-next-line react/no-danger
|
||||||
|
dangerouslySetInnerHTML={{ __html: content }}
|
||||||
|
/>
|
||||||
|
<div className="py-0 d-flex">
|
||||||
|
<span className="font-size-12 text-gray-500 line-height-20">
|
||||||
|
<span>{courseName}</span>
|
||||||
|
<span className="text-light-700 px-1.5">{intl.formatMessage(messages.fullStop)}</span>
|
||||||
|
<span>{timeago.format(createdAt, 'time-locale')}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!lastRead && (
|
||||||
|
<div className="d-flex py-1.5 px-1.5 ml-2 cursor-pointer">
|
||||||
|
<span className="bg-brand-500 rounded unread" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
NotificationRowItem.propTypes = {
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
type: PropTypes.string.isRequired,
|
||||||
|
contentUrl: PropTypes.string.isRequired,
|
||||||
|
content: PropTypes.node.isRequired,
|
||||||
|
courseName: PropTypes.string.isRequired,
|
||||||
|
createdAt: PropTypes.string.isRequired,
|
||||||
|
lastRead: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(NotificationRowItem);
|
||||||
81
src/Notifications/NotificationSections.jsx
Normal file
81
src/Notifications/NotificationSections.jsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
import { Button } from '@edx/paragon';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import isEmpty from 'lodash/isEmpty';
|
||||||
|
import messages from './messages';
|
||||||
|
import NotificationRowItem from './NotificationRowItem';
|
||||||
|
import { markAllNotificationsAsRead } from './data/thunks';
|
||||||
|
import { selectNotificationsByIds, selectPaginationData, selectSelectedAppName } from './data/selectors';
|
||||||
|
import { splitNotificationsByTime } from './utils';
|
||||||
|
import { updatePaginationRequest } from './data/slice';
|
||||||
|
|
||||||
|
const NotificationSections = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const selectedAppName = useSelector(selectSelectedAppName());
|
||||||
|
const notifications = useSelector(selectNotificationsByIds(selectedAppName));
|
||||||
|
const { currentPage, numPages } = useSelector(selectPaginationData());
|
||||||
|
const { today = [], earlier = [] } = useMemo(
|
||||||
|
() => splitNotificationsByTime(notifications),
|
||||||
|
[notifications],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMarkAllAsRead = useCallback(() => {
|
||||||
|
dispatch(markAllNotificationsAsRead(selectedAppName));
|
||||||
|
}, [dispatch, selectedAppName]);
|
||||||
|
|
||||||
|
const updatePagination = useCallback(() => {
|
||||||
|
dispatch(updatePaginationRequest());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const renderNotificationSection = (section, items) => {
|
||||||
|
if (isEmpty(items)) { return null; }
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pb-2">
|
||||||
|
<div className="d-flex justify-content-between align-items-center py-10px mb-2">
|
||||||
|
<span className="text-gray-500 line-height-10">
|
||||||
|
{section === 'today' && intl.formatMessage(messages.notificationTodayHeading)}
|
||||||
|
{section === 'earlier' && intl.formatMessage(messages.notificationEarlierHeading)}
|
||||||
|
</span>
|
||||||
|
{notifications?.length > 0 && (section === 'earlier' ? today.length === 0 : true) && (
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
className="text-info-500 font-size-14 line-height-10 text-decoration-none p-0 border-0"
|
||||||
|
onClick={handleMarkAllAsRead}
|
||||||
|
>
|
||||||
|
{intl.formatMessage(messages.notificationMarkAsRead)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{items.map((notification) => (
|
||||||
|
<NotificationRowItem
|
||||||
|
key={notification.id}
|
||||||
|
id={notification.id}
|
||||||
|
type={notification.type}
|
||||||
|
contentUrl={notification.contentUrl}
|
||||||
|
content={notification.content}
|
||||||
|
courseName={notification.courseName}
|
||||||
|
createdAt={notification.createdAt}
|
||||||
|
lastRead={notification.lastRead}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-4 px-4">
|
||||||
|
{renderNotificationSection('today', today)}
|
||||||
|
{renderNotificationSection('earlier', earlier)}
|
||||||
|
{currentPage < numPages && (
|
||||||
|
<Button variant="primary" className="w-100 bg-primary-500" onClick={updatePagination}>
|
||||||
|
{intl.formatMessage(messages.loadMoreNotifications)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(NotificationSections);
|
||||||
52
src/Notifications/NotificationTabs.jsx
Normal file
52
src/Notifications/NotificationTabs.jsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { Tab, Tabs } from '@edx/paragon';
|
||||||
|
import NotificationSections from './NotificationSections';
|
||||||
|
import { fetchNotificationList, markNotificationsAsSeen } from './data/thunks';
|
||||||
|
import {
|
||||||
|
selectNotificationTabs, selectNotificationTabsCount, selectPaginationData, selectSelectedAppName,
|
||||||
|
} from './data/selectors';
|
||||||
|
import { updateAppNameRequest } from './data/slice';
|
||||||
|
|
||||||
|
const NotificationTabs = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const selectedAppName = useSelector(selectSelectedAppName());
|
||||||
|
const notificationUnseenCounts = useSelector(selectNotificationTabsCount());
|
||||||
|
const notificationTabs = useSelector(selectNotificationTabs());
|
||||||
|
const { currentPage } = useSelector(selectPaginationData());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchNotificationList({ appName: selectedAppName, page: currentPage, pageSize: 10 }));
|
||||||
|
if (selectedAppName) { dispatch(markNotificationsAsSeen(selectedAppName)); }
|
||||||
|
}, [currentPage, selectedAppName]);
|
||||||
|
|
||||||
|
const handleActiveTab = useCallback((appName) => {
|
||||||
|
dispatch(updateAppNameRequest({ appName }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const tabArray = useMemo(() => notificationTabs?.map((appName) => (
|
||||||
|
<Tab
|
||||||
|
key={appName}
|
||||||
|
eventKey={appName}
|
||||||
|
title={appName}
|
||||||
|
notification={notificationUnseenCounts[appName]}
|
||||||
|
tabClassName="pt-0 pb-10px px-2.5 d-flex border-top-0 mb-0 align-items-center line-height-24 text-capitalize"
|
||||||
|
>
|
||||||
|
{appName === selectedAppName && (<NotificationSections />)}
|
||||||
|
</Tab>
|
||||||
|
)), [notificationUnseenCounts, selectedAppName, notificationTabs]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tabs
|
||||||
|
variant="tabs"
|
||||||
|
defaultActiveKey={selectedAppName}
|
||||||
|
onSelect={handleActiveTab}
|
||||||
|
className="px-2.5 text-primary-500"
|
||||||
|
>
|
||||||
|
{tabArray}
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(NotificationTabs);
|
||||||
1
src/Notifications/data/__factories__/index.js
Normal file
1
src/Notifications/data/__factories__/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import './notifications.factory';
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Factory } from 'rosie';
|
||||||
|
|
||||||
|
Factory.define('notificationsCount')
|
||||||
|
.attr('count', 45)
|
||||||
|
.attr('countByAppName', {
|
||||||
|
reminders: 10,
|
||||||
|
discussions: 20,
|
||||||
|
grades: 10,
|
||||||
|
authoring: 5,
|
||||||
|
})
|
||||||
|
.attr('showNotificationsTray', true);
|
||||||
|
|
||||||
|
Factory.define('notification')
|
||||||
|
.sequence('id')
|
||||||
|
.attr('type', 'post')
|
||||||
|
.sequence('content', ['id'], (idx, notificationId) => `<p><b>User ${idx}</b> posts <b>Hello and welcome to SC0x
|
||||||
|
${notificationId}!</b></p>`)
|
||||||
|
.attr('course_name', 'Supply Chain Analytics')
|
||||||
|
.sequence('content_url', (idx) => `https://example.com/${idx}`)
|
||||||
|
.attr('last_read', null)
|
||||||
|
.attr('last_seen', null)
|
||||||
|
.sequence('created_at', ['createdDate'], (idx, date) => date);
|
||||||
44
src/Notifications/data/api.js
Normal file
44
src/Notifications/data/api.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { getConfig, snakeCaseObject } from '@edx/frontend-platform';
|
||||||
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
|
|
||||||
|
export const getNotificationsCountApiUrl = () => `${getConfig().LMS_BASE_URL}/api/notifications/count/`;
|
||||||
|
export const getNotificationsApiUrl = () => `${getConfig().LMS_BASE_URL}/api/notifications/`;
|
||||||
|
export const markNotificationsSeenApiUrl = (appName) => `${getConfig().LMS_BASE_URL}/api/notifications/mark-notifications-unseen/${appName}/`;
|
||||||
|
export const markNotificationAsReadApiUrl = () => `${getConfig().LMS_BASE_URL}/api/notifications/read/`;
|
||||||
|
|
||||||
|
export async function getNotifications(appName, page, pageSize) {
|
||||||
|
const params = snakeCaseObject({ page, pageSize });
|
||||||
|
const { data } = await getAuthenticatedHttpClient().get(getNotificationsApiUrl(), { params });
|
||||||
|
|
||||||
|
const startIndex = (page - 1) * pageSize;
|
||||||
|
const endIndex = startIndex + pageSize;
|
||||||
|
|
||||||
|
const notifications = data.slice(startIndex, endIndex);
|
||||||
|
return { notifications, numPages: 2, currentPage: page };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getNotificationCounts() {
|
||||||
|
const { data } = await getAuthenticatedHttpClient().get(getNotificationsCountApiUrl());
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function markNotificationSeen(appName) {
|
||||||
|
const { data } = await getAuthenticatedHttpClient().put(`${markNotificationsSeenApiUrl(appName)}`);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function markAllNotificationRead(appName) {
|
||||||
|
const params = snakeCaseObject({ appName });
|
||||||
|
const { data } = await getAuthenticatedHttpClient().put(markNotificationAsReadApiUrl(), { params });
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function markNotificationRead(notificationId) {
|
||||||
|
const params = snakeCaseObject({ notificationId });
|
||||||
|
const { data } = await getAuthenticatedHttpClient().put(markNotificationAsReadApiUrl(), { params });
|
||||||
|
|
||||||
|
return { data, id: notificationId };
|
||||||
|
}
|
||||||
150
src/Notifications/data/api.test.js
Normal file
150
src/Notifications/data/api.test.js
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
|
import { Factory } from 'rosie';
|
||||||
|
|
||||||
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
|
import { initializeMockApp } from '@edx/frontend-platform/testing';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getNotificationsApiUrl, getNotificationsCountApiUrl, markNotificationAsReadApiUrl, markNotificationsSeenApiUrl,
|
||||||
|
getNotificationCounts, getNotifications, markNotificationSeen, markAllNotificationRead, markNotificationRead,
|
||||||
|
} from './api';
|
||||||
|
|
||||||
|
import './__factories__';
|
||||||
|
|
||||||
|
const notificationCountsApiUrl = getNotificationsCountApiUrl();
|
||||||
|
const notificationsApiUrl = getNotificationsApiUrl();
|
||||||
|
const markedAllNotificationsAsSeenApiUrl = markNotificationsSeenApiUrl('discussions');
|
||||||
|
const markedAllNotificationsAsReadApiUrl = markNotificationAsReadApiUrl();
|
||||||
|
|
||||||
|
let axiosMock = null;
|
||||||
|
|
||||||
|
describe('Notifications API', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
initializeMockApp({
|
||||||
|
authenticatedUser: {
|
||||||
|
userId: '123abc',
|
||||||
|
username: 'testuser',
|
||||||
|
administrator: false,
|
||||||
|
roles: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||||
|
Factory.resetAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
axiosMock.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Successfully get notification counts for different tabs.', async () => {
|
||||||
|
axiosMock.onGet(notificationCountsApiUrl).reply(200, (Factory.build('notificationsCount')));
|
||||||
|
|
||||||
|
const { count, countByAppName } = await getNotificationCounts();
|
||||||
|
|
||||||
|
expect(count).toEqual(45);
|
||||||
|
expect(countByAppName.reminders).toEqual(10);
|
||||||
|
expect(countByAppName.discussions).toEqual(20);
|
||||||
|
expect(countByAppName.grades).toEqual(10);
|
||||||
|
expect(countByAppName.authoring).toEqual(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{ statusCode: 404, message: 'Failed to get notification counts.' },
|
||||||
|
{ statusCode: 403, message: 'Denied to get notification counts.' },
|
||||||
|
])('%s for notification counts API.', async ({ statusCode, message }) => {
|
||||||
|
axiosMock.onGet(notificationCountsApiUrl).reply(statusCode, { message });
|
||||||
|
try {
|
||||||
|
await getNotificationCounts();
|
||||||
|
} catch (error) {
|
||||||
|
expect(error.response.status).toEqual(statusCode);
|
||||||
|
expect(error.response.data.message).toEqual(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Successfully get notifications.', async () => {
|
||||||
|
axiosMock.onGet(notificationsApiUrl).reply(
|
||||||
|
200,
|
||||||
|
(Factory.buildList('notification', 2, null, { createdDate: new Date().toISOString() })),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { notifications } = await getNotifications('discussions', 1, 10);
|
||||||
|
|
||||||
|
expect(notifications).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{ statusCode: 404, message: 'Failed to get notifications.' },
|
||||||
|
{ statusCode: 403, message: 'Denied to get notifications.' },
|
||||||
|
])('%s for notification API.', async ({ statusCode, message }) => {
|
||||||
|
axiosMock.onGet(notificationsApiUrl).reply(statusCode, { message });
|
||||||
|
try {
|
||||||
|
await getNotifications({ page: 1, pageSize: 10 });
|
||||||
|
} catch (error) {
|
||||||
|
expect(error.response.status).toEqual(statusCode);
|
||||||
|
expect(error.response.data.message).toEqual(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Successfully marked all notifications as seen for selected app.', async () => {
|
||||||
|
axiosMock.onPut(markedAllNotificationsAsSeenApiUrl).reply(200, { message: 'Notifications marked seen.' });
|
||||||
|
|
||||||
|
const { message } = await markNotificationSeen('discussions');
|
||||||
|
|
||||||
|
expect(message).toEqual('Notifications marked seen.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{ statusCode: 404, message: 'Failed to mark all notifications as seen for selected app.' },
|
||||||
|
{ statusCode: 403, message: 'Denied to mark all notifications as seen for selected app.' },
|
||||||
|
])('%s for notification mark as seen API.', async ({ statusCode, message }) => {
|
||||||
|
axiosMock.onPut(markedAllNotificationsAsSeenApiUrl).reply(statusCode, { message });
|
||||||
|
try {
|
||||||
|
await markNotificationSeen('discussions');
|
||||||
|
} catch (error) {
|
||||||
|
expect(error.response.status).toEqual(statusCode);
|
||||||
|
expect(error.response.data.message).toEqual(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Successfully marked all notifications as read for selected app.', async () => {
|
||||||
|
axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(200, { message: 'Notifications marked read.' });
|
||||||
|
|
||||||
|
const { message } = await markAllNotificationRead('discussions');
|
||||||
|
|
||||||
|
expect(message).toEqual('Notifications marked read.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{ statusCode: 404, message: 'Failed to mark all notifications as read for selected app.' },
|
||||||
|
{ statusCode: 403, message: 'Denied to mark all notifications as read for selected app.' },
|
||||||
|
])('%s for notification mark all as read API.', async ({ statusCode, message }) => {
|
||||||
|
axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(statusCode, { message });
|
||||||
|
try {
|
||||||
|
await markAllNotificationRead('discussions');
|
||||||
|
} catch (error) {
|
||||||
|
expect(error.response.status).toEqual(statusCode);
|
||||||
|
expect(error.response.data.message).toEqual(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Successfully marked notification as read.', async () => {
|
||||||
|
axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(200, { message: 'Notification marked read.' });
|
||||||
|
|
||||||
|
const { data } = await markNotificationRead(1);
|
||||||
|
|
||||||
|
expect(data.message).toEqual('Notification marked read.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{ statusCode: 404, message: 'Failed to mark notification as read.' },
|
||||||
|
{ statusCode: 403, message: 'Denied to mark notification as read.' },
|
||||||
|
])('%s for notification mark as read API.', async ({ statusCode, message }) => {
|
||||||
|
axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(statusCode, { message });
|
||||||
|
try {
|
||||||
|
await markAllNotificationRead(1);
|
||||||
|
} catch (error) {
|
||||||
|
expect(error.response.status).toEqual(statusCode);
|
||||||
|
expect(error.response.data.message).toEqual(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
11
src/Notifications/data/hook.js
Normal file
11
src/Notifications/data/hook.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { breakpoints, useWindowSize } from '@edx/paragon';
|
||||||
|
|
||||||
|
export function useIsOnMediumScreen() {
|
||||||
|
const windowSize = useWindowSize();
|
||||||
|
return breakpoints.large.maxWidth > windowSize.width && windowSize.width >= breakpoints.medium.minWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useIsOnLargeScreen() {
|
||||||
|
const windowSize = useWindowSize();
|
||||||
|
return windowSize.width >= breakpoints.extraLarge.minWidth;
|
||||||
|
}
|
||||||
1
src/Notifications/data/index.js
Normal file
1
src/Notifications/data/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './slice';
|
||||||
134
src/Notifications/data/notifications.json
Normal file
134
src/Notifications/data/notifications.json
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "post",
|
||||||
|
"content": "<p><b>SCM_Lead</b> posts <b>Hello and welcome to SC0x!</b></p>",
|
||||||
|
"course_name": "Supply Chain Analytics",
|
||||||
|
"content_url": "",
|
||||||
|
"last_read": null,
|
||||||
|
"last_seen": null,
|
||||||
|
"created_at": "2023-06-01T00:46:11.979531Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"type": "help",
|
||||||
|
"content": "<p><b>MITx_Learner</b> asked <b>What grade does a student need to get in order to pass the course and earn a certificate?</b></p>",
|
||||||
|
"course_name": "Supply Chain Analytics",
|
||||||
|
"content_url": "",
|
||||||
|
"last_read": null,
|
||||||
|
"last_seen": null,
|
||||||
|
"created_at": "2023-06-01T00:36:11.979531Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"type": "post",
|
||||||
|
"content": "<p><b>SCM_Lead</b> posts <b>Hello and welcome to SC0x!</b></p>",
|
||||||
|
"course_name": "Supply Chain Analytics",
|
||||||
|
"content_url": "",
|
||||||
|
"last_read": null,
|
||||||
|
"last_seen": null,
|
||||||
|
"created_at": "2023-06-01T00:46:11.979531Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"type": "respond",
|
||||||
|
"content": "<p><b>MITx_Learner</b> responded <b>Can't find linear regression in section 3 review</b></p>",
|
||||||
|
"course_name": "Supply Chain Analytics",
|
||||||
|
"content_url": "",
|
||||||
|
"last_read": null,
|
||||||
|
"last_seen": null,
|
||||||
|
"created_at": "2023-06-01T00:36:11.979531Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"type": "comment",
|
||||||
|
"content": "<p><b>MITx_Learner</b> commented on <b>MITx_Expert's</b> response on a post your following <b>Can't find linear regression in section 3 review</b></p>",
|
||||||
|
"course_name": "Supply Chain Analytics",
|
||||||
|
"content_url": "",
|
||||||
|
"last_read": null,
|
||||||
|
"last_seen": null,
|
||||||
|
"created_at": "2023-06-01T00:36:11.979531Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"type": "question",
|
||||||
|
"content": "<p><b>MITx_Learner</b> commented <b>Examples of quadratic equations in supply chains</b></p>",
|
||||||
|
"course_name": "Supply Chain Analytics",
|
||||||
|
"content_url": "",
|
||||||
|
"last_read": null,
|
||||||
|
"last_seen": null,
|
||||||
|
"created_at": "2023-06-01T00:36:11.979531Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"type": "answer",
|
||||||
|
"content": "<p><b>MITx_Expert</b> answered <b>Examples of quadratic equations in supply chains</b></p>",
|
||||||
|
"course_name": "Supply Chain Analytics",
|
||||||
|
"content_url": "",
|
||||||
|
"last_read": null,
|
||||||
|
"last_seen": null,
|
||||||
|
"created_at": "2023-06-05T00:36:11.979531Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"type": "comment",
|
||||||
|
"content": "<p><b>MITx_Learner</b> commented <b>Examples of quadratic equations in supply chains</b></p>",
|
||||||
|
"course_name": "Supply Chain Analytics",
|
||||||
|
"content_url": "",
|
||||||
|
"last_read": null,
|
||||||
|
"last_seen": null,
|
||||||
|
"created_at": "2023-06-01T00:36:11.979531Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"type": "comment",
|
||||||
|
"content": "<p><b>MITx_Learner</b> commented on <b>MITx_Expert's</b>what grade does a student need to get in order to pass the course and earn a certificate?</b></p>",
|
||||||
|
"course_name": "Supply Chain Analytics",
|
||||||
|
"content_url": "",
|
||||||
|
"last_read": null,
|
||||||
|
"last_seen": null,
|
||||||
|
"created_at": "2023-06-01T00:36:11.979531Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"type": "comment",
|
||||||
|
"content": "<p><b>MITx_Learner</b> commented on your response in <b>Convexity of f(x)=1/x , x>1</b></p>",
|
||||||
|
"course_name": "Supply Chain Analytics",
|
||||||
|
"content_url": "",
|
||||||
|
"last_read": null,
|
||||||
|
"last_seen": null,
|
||||||
|
"created_at": "2023-06-01T00:36:11.979531Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 11,
|
||||||
|
"type": "answer",
|
||||||
|
"content": "<p><b>SCM_Lead’s</b> response has been marked as answer in your post <b>Quiz in section 3 - Please explain the F-Significance value</b></p>",
|
||||||
|
"course_name": "Supply Chain Analytics",
|
||||||
|
"content_url": "",
|
||||||
|
"last_read": null,
|
||||||
|
"last_seen": null,
|
||||||
|
"created_at": "2023-06-01T00:36:11.979531Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 12,
|
||||||
|
"type": "endorsed",
|
||||||
|
"content": "<p>Your response has been endorsed in <b>Quiz in section 3 - Please explain the F-Significance value</b></p>",
|
||||||
|
"course_name": "Supply Chain Analytics",
|
||||||
|
"content_url": "",
|
||||||
|
"last_read": null,
|
||||||
|
"last_seen": null,
|
||||||
|
"created_at": "2023-06-01T00:36:11.979531Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 13,
|
||||||
|
"type": "reported",
|
||||||
|
"content": "<p><b>MITx Learner’s</b> post has been reported <b>“Here are the exam answers. Question 1 - CSA stands for Compliance Safety Ac...”</b></p>",
|
||||||
|
"course_name": "Supply Chain Analytics",
|
||||||
|
"content_url": "",
|
||||||
|
"last_read": null,
|
||||||
|
"last_seen": null,
|
||||||
|
"created_at": "2023-06-01T00:36:11.979531Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
164
src/Notifications/data/redux.test.js
Normal file
164
src/Notifications/data/redux.test.js
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
|
import { Factory } from 'rosie';
|
||||||
|
|
||||||
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
|
import { initializeMockApp } from '@edx/frontend-platform/testing';
|
||||||
|
|
||||||
|
import { initializeStore } from '../../store';
|
||||||
|
import executeThunk from '../../test-utils';
|
||||||
|
import {
|
||||||
|
getNotificationsApiUrl, getNotificationsCountApiUrl, markNotificationAsReadApiUrl, markNotificationsSeenApiUrl,
|
||||||
|
} from './api';
|
||||||
|
import {
|
||||||
|
fetchAppsNotificationCount, fetchNotificationList, markNotificationsAsRead, markAllNotificationsAsRead,
|
||||||
|
resetNotificationState, markNotificationsAsSeen,
|
||||||
|
} from './thunks';
|
||||||
|
|
||||||
|
import './__factories__';
|
||||||
|
|
||||||
|
const notificationCountsApiUrl = getNotificationsCountApiUrl();
|
||||||
|
const notificationsApiUrl = getNotificationsApiUrl();
|
||||||
|
const markedAllNotificationsAsReadApiUrl = markNotificationAsReadApiUrl();
|
||||||
|
const markedAllNotificationsAsSeenApiUrl = markNotificationsSeenApiUrl('discussions');
|
||||||
|
|
||||||
|
let axiosMock;
|
||||||
|
let store;
|
||||||
|
|
||||||
|
describe('Notification Redux', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
initializeMockApp({
|
||||||
|
authenticatedUser: {
|
||||||
|
userId: '123abc',
|
||||||
|
username: 'testuser',
|
||||||
|
administrator: false,
|
||||||
|
roles: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||||
|
Factory.resetAll();
|
||||||
|
store = initializeStore();
|
||||||
|
|
||||||
|
axiosMock.onGet(notificationCountsApiUrl).reply(200, (Factory.build('notificationsCount')));
|
||||||
|
axiosMock.onGet(notificationsApiUrl).reply(
|
||||||
|
200,
|
||||||
|
(Factory.buildList('notification', 2, null, { createdDate: new Date().toISOString() })),
|
||||||
|
);
|
||||||
|
await executeThunk(fetchAppsNotificationCount(), store.dispatch, store.getState);
|
||||||
|
await executeThunk(fetchNotificationList({ page: 1, pageSize: 10 }), store.dispatch, store.getState);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
axiosMock.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Successfully loaded initial notification states in the redux.', async () => {
|
||||||
|
executeThunk(resetNotificationState(), store.dispatch, store.getState);
|
||||||
|
|
||||||
|
const { notifications } = store.getState();
|
||||||
|
|
||||||
|
expect(notifications.notificationStatus).toEqual('idle');
|
||||||
|
expect(notifications.appName).toEqual('discussions');
|
||||||
|
expect(notifications.appsId).toHaveLength(0);
|
||||||
|
expect(notifications.apps).toEqual({});
|
||||||
|
expect(notifications.notifications).toEqual({});
|
||||||
|
expect(notifications.tabsCount).toEqual({});
|
||||||
|
expect(notifications.showNotificationsTray).toEqual(false);
|
||||||
|
expect(notifications.pagination.count).toEqual(10);
|
||||||
|
expect(notifications.pagination.numPages).toEqual(1);
|
||||||
|
expect(notifications.pagination.currentPage).toEqual(1);
|
||||||
|
expect(notifications.pagination.nextPage).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Successfully loaded notifications list in the redux.', async () => {
|
||||||
|
const { notifications: { notifications } } = store.getState();
|
||||||
|
|
||||||
|
expect(Object.keys(notifications)).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{ statusCode: 404, status: 'failed' },
|
||||||
|
{ statusCode: 403, status: 'denied' },
|
||||||
|
])('%s to load notifications list in the redux.', async ({ statusCode, status }) => {
|
||||||
|
axiosMock.onGet(notificationsApiUrl).reply(statusCode);
|
||||||
|
await executeThunk(fetchNotificationList({ page: 1, pageSize: 10 }), store.dispatch, store.getState);
|
||||||
|
|
||||||
|
const { notifications: { notificationStatus } } = store.getState();
|
||||||
|
|
||||||
|
expect(notificationStatus).toEqual(status);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Successfully loaded notification counts in the redux.', async () => {
|
||||||
|
const { notifications: { tabsCount } } = store.getState();
|
||||||
|
|
||||||
|
expect(tabsCount.count).toEqual(25);
|
||||||
|
expect(tabsCount.reminders).toEqual(10);
|
||||||
|
expect(tabsCount.discussions).toEqual(0);
|
||||||
|
expect(tabsCount.grades).toEqual(10);
|
||||||
|
expect(tabsCount.authoring).toEqual(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{ statusCode: 404, status: 'failed' },
|
||||||
|
{ statusCode: 403, status: 'denied' },
|
||||||
|
])('%s to load notification counts in the redux.', async ({ statusCode, status }) => {
|
||||||
|
axiosMock.onGet(notificationCountsApiUrl).reply(statusCode);
|
||||||
|
await executeThunk(fetchAppsNotificationCount(), store.dispatch, store.getState);
|
||||||
|
|
||||||
|
const { notifications: { notificationStatus } } = store.getState();
|
||||||
|
|
||||||
|
expect(notificationStatus).toEqual(status);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Successfully marked all notifications as seen for selected app.', async () => {
|
||||||
|
axiosMock.onPut(markedAllNotificationsAsSeenApiUrl).reply(200);
|
||||||
|
await executeThunk(markNotificationsAsSeen('discussions'), store.dispatch, store.getState);
|
||||||
|
|
||||||
|
expect(store.getState().notifications.notificationStatus).toEqual('successful');
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{ statusCode: 404, status: 'failed' },
|
||||||
|
{ statusCode: 403, status: 'denied' },
|
||||||
|
])('%s to mark all notifications as seen for selected app.', async ({ statusCode, status }) => {
|
||||||
|
axiosMock.onPut(markedAllNotificationsAsSeenApiUrl).reply(statusCode);
|
||||||
|
await executeThunk(markNotificationsAsSeen('discussions'), store.dispatch, store.getState);
|
||||||
|
|
||||||
|
const { notifications: { notificationStatus } } = store.getState();
|
||||||
|
|
||||||
|
expect(notificationStatus).toEqual(status);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Successfully marked all notifications as read for selected app in the redux.', async () => {
|
||||||
|
axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(200);
|
||||||
|
await executeThunk(markAllNotificationsAsRead('discussions'), store.dispatch, store.getState);
|
||||||
|
|
||||||
|
const { notifications: { notificationStatus, notifications } } = store.getState();
|
||||||
|
const firstNotification = Object.values(notifications)[0];
|
||||||
|
|
||||||
|
expect(notificationStatus).toEqual('successful');
|
||||||
|
expect(firstNotification.lastRead).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Successfully marked notification as read in the redux.', async () => {
|
||||||
|
axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(200);
|
||||||
|
await executeThunk(markNotificationsAsRead(1), store.dispatch, store.getState);
|
||||||
|
|
||||||
|
const { notifications: { notificationStatus, notifications } } = store.getState();
|
||||||
|
const firstNotification = Object.values(notifications)[0];
|
||||||
|
|
||||||
|
expect(notificationStatus).toEqual('successful');
|
||||||
|
expect(firstNotification.lastRead).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{ statusCode: 404, status: 'failed' },
|
||||||
|
{ statusCode: 403, status: 'denied' },
|
||||||
|
])('%s to marked notification as read in the redux.', async ({ statusCode, status }) => {
|
||||||
|
axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(statusCode);
|
||||||
|
await executeThunk(markNotificationsAsRead(1), store.dispatch, store.getState);
|
||||||
|
|
||||||
|
const { notifications: { notificationStatus } } = store.getState();
|
||||||
|
|
||||||
|
expect(notificationStatus).toEqual(status);
|
||||||
|
});
|
||||||
|
});
|
||||||
126
src/Notifications/data/selector.test.jsx
Normal file
126
src/Notifications/data/selector.test.jsx
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
|
import { Factory } from 'rosie';
|
||||||
|
|
||||||
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
|
import { initializeMockApp } from '@edx/frontend-platform/testing';
|
||||||
|
|
||||||
|
import { initializeStore } from '../../store';
|
||||||
|
import executeThunk from '../../test-utils';
|
||||||
|
import { getNotificationsApiUrl, getNotificationsCountApiUrl } from './api';
|
||||||
|
import {
|
||||||
|
selectNotifications,
|
||||||
|
selectNotificationsByIds,
|
||||||
|
selectNotificationStatus,
|
||||||
|
selectNotificationTabs,
|
||||||
|
selectNotificationTabsCount,
|
||||||
|
selectPaginationData,
|
||||||
|
selectSelectedAppName,
|
||||||
|
selectSelectedAppNotificationIds,
|
||||||
|
selectShowNotificationTray,
|
||||||
|
} from './selectors';
|
||||||
|
import { fetchAppsNotificationCount, fetchNotificationList } from './thunks';
|
||||||
|
|
||||||
|
import './__factories__';
|
||||||
|
|
||||||
|
const notificationCountsApiUrl = getNotificationsCountApiUrl();
|
||||||
|
const notificationsApiUrl = getNotificationsApiUrl();
|
||||||
|
|
||||||
|
let axiosMock;
|
||||||
|
let store;
|
||||||
|
|
||||||
|
describe('Notification Selectors', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
initializeMockApp({
|
||||||
|
authenticatedUser: {
|
||||||
|
userId: '123abc',
|
||||||
|
username: 'testuser',
|
||||||
|
administrator: false,
|
||||||
|
roles: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||||
|
Factory.resetAll();
|
||||||
|
store = initializeStore();
|
||||||
|
|
||||||
|
axiosMock.onGet(notificationCountsApiUrl).reply(200, (Factory.build('notificationsCount')));
|
||||||
|
axiosMock.onGet(notificationsApiUrl).reply(
|
||||||
|
200,
|
||||||
|
(Factory.buildList('notification', 2, null, { createdDate: new Date().toISOString() })),
|
||||||
|
);
|
||||||
|
await executeThunk(fetchAppsNotificationCount(), store.dispatch, store.getState);
|
||||||
|
await executeThunk(fetchNotificationList({ page: 1, pageSize: 10 }), store.dispatch, store.getState);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
axiosMock.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should return notification status.', async () => {
|
||||||
|
const state = store.getState();
|
||||||
|
const status = selectNotificationStatus()(state);
|
||||||
|
|
||||||
|
expect(status).toEqual('successful');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should return notification tabs count.', async () => {
|
||||||
|
const state = store.getState();
|
||||||
|
const tabsCount = selectNotificationTabsCount()(state);
|
||||||
|
|
||||||
|
expect(tabsCount.count).toEqual(25);
|
||||||
|
expect(tabsCount.reminders).toEqual(10);
|
||||||
|
expect(tabsCount.discussions).toEqual(0);
|
||||||
|
expect(tabsCount.grades).toEqual(10);
|
||||||
|
expect(tabsCount.authoring).toEqual(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should return notification tabs.', async () => {
|
||||||
|
const state = store.getState();
|
||||||
|
const tabs = selectNotificationTabs()(state);
|
||||||
|
|
||||||
|
expect(tabs).toHaveLength(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should return selected app notification ids.', async () => {
|
||||||
|
const state = store.getState();
|
||||||
|
const notificationIds = selectSelectedAppNotificationIds('discussions')(state);
|
||||||
|
|
||||||
|
expect(notificationIds).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should return show notification tray status.', async () => {
|
||||||
|
const state = store.getState();
|
||||||
|
const showNotificationTrayStatus = selectShowNotificationTray()(state);
|
||||||
|
|
||||||
|
expect(showNotificationTrayStatus).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should return notifications.', async () => {
|
||||||
|
const state = store.getState();
|
||||||
|
const notifications = selectNotifications()(state);
|
||||||
|
|
||||||
|
expect(Object.keys(notifications)).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should return notifications from Ids.', async () => {
|
||||||
|
const state = store.getState();
|
||||||
|
const notifications = selectNotificationsByIds('discussions')(state);
|
||||||
|
|
||||||
|
expect(notifications).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should return selected app name.', async () => {
|
||||||
|
const state = store.getState();
|
||||||
|
const appName = selectSelectedAppName()(state);
|
||||||
|
|
||||||
|
expect(appName).toEqual('discussions');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should return pagination data.', async () => {
|
||||||
|
const state = store.getState();
|
||||||
|
const paginationData = selectPaginationData()(state);
|
||||||
|
|
||||||
|
expect(paginationData.count).toEqual(10);
|
||||||
|
expect(paginationData.currentPage).toEqual(1);
|
||||||
|
expect(paginationData.numPages).toEqual(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
23
src/Notifications/data/selectors.js
Normal file
23
src/Notifications/data/selectors.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
export const selectNotificationStatus = () => state => state.notifications.notificationStatus;
|
||||||
|
|
||||||
|
export const selectNotificationTabsCount = () => state => state.notifications.tabsCount;
|
||||||
|
|
||||||
|
export const selectNotificationTabs = () => state => state.notifications.appsId;
|
||||||
|
|
||||||
|
export const selectSelectedAppNotificationIds = (appName) => state => state.notifications.apps[appName] ?? [];
|
||||||
|
|
||||||
|
export const selectShowNotificationTray = () => state => state.notifications.showNotificationsTray;
|
||||||
|
|
||||||
|
export const selectNotifications = () => state => state.notifications.notifications;
|
||||||
|
|
||||||
|
export const selectNotificationsByIds = (appName) => createSelector(
|
||||||
|
selectNotifications(),
|
||||||
|
selectSelectedAppNotificationIds(appName),
|
||||||
|
(notifications, notificationIds) => notificationIds.map((notificationId) => notifications[notificationId]) || [],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectSelectedAppName = () => state => state.notifications.appName;
|
||||||
|
|
||||||
|
export const selectPaginationData = () => state => state.notifications.pagination;
|
||||||
154
src/Notifications/data/slice.js
Normal file
154
src/Notifications/data/slice.js
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
/* eslint-disable no-param-reassign */
|
||||||
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
export const RequestStatus = {
|
||||||
|
IDLE: 'idle',
|
||||||
|
LOADING: 'in-progress',
|
||||||
|
LOADED: 'successful',
|
||||||
|
FAILED: 'failed',
|
||||||
|
DENIED: 'denied',
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
notificationStatus: 'idle',
|
||||||
|
appName: 'discussions',
|
||||||
|
appsId: [],
|
||||||
|
apps: {},
|
||||||
|
notifications: {},
|
||||||
|
tabsCount: {},
|
||||||
|
showNotificationsTray: false,
|
||||||
|
pagination: {
|
||||||
|
count: 10,
|
||||||
|
numPages: 1,
|
||||||
|
currentPage: 1,
|
||||||
|
nextPage: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const slice = createSlice({
|
||||||
|
name: 'notifications',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
fetchNotificationDenied: (state) => {
|
||||||
|
state.notificationStatus = RequestStatus.DENIED;
|
||||||
|
},
|
||||||
|
fetchNotificationFailure: (state) => {
|
||||||
|
state.notificationStatus = RequestStatus.FAILED;
|
||||||
|
},
|
||||||
|
fetchNotificationRequest: (state) => {
|
||||||
|
state.notificationStatus = RequestStatus.LOADING;
|
||||||
|
},
|
||||||
|
fetchNotificationSuccess: (state, { payload }) => {
|
||||||
|
const {
|
||||||
|
newNotificationIds, notificationsKeyValuePair, numPages, currentPage,
|
||||||
|
} = payload;
|
||||||
|
const existingNotificationIds = state.apps[state.appName];
|
||||||
|
|
||||||
|
state.apps[state.appName] = Array.from(new Set([...existingNotificationIds, ...newNotificationIds]));
|
||||||
|
state.notifications = { ...state.notifications, ...notificationsKeyValuePair };
|
||||||
|
state.tabsCount.count -= state.tabsCount[state.appName];
|
||||||
|
state.tabsCount[state.appName] = 0;
|
||||||
|
state.notificationStatus = RequestStatus.LOADED;
|
||||||
|
state.pagination.numPages = numPages;
|
||||||
|
state.pagination.currentPage = currentPage;
|
||||||
|
},
|
||||||
|
fetchNotificationsCountDenied: (state) => {
|
||||||
|
state.notificationStatus = RequestStatus.DENIED;
|
||||||
|
},
|
||||||
|
fetchNotificationsCountFailure: (state) => {
|
||||||
|
state.notificationStatus = RequestStatus.FAILED;
|
||||||
|
},
|
||||||
|
fetchNotificationsCountRequest: (state) => {
|
||||||
|
state.notificationStatus = RequestStatus.LOADING;
|
||||||
|
},
|
||||||
|
fetchNotificationsCountSuccess: (state, { payload }) => {
|
||||||
|
const {
|
||||||
|
countByAppName, appIds, apps, count, showNotificationsTray,
|
||||||
|
} = payload;
|
||||||
|
state.tabsCount = { count, ...countByAppName };
|
||||||
|
state.appsId = appIds;
|
||||||
|
state.apps = apps;
|
||||||
|
state.showNotificationsTray = showNotificationsTray;
|
||||||
|
state.notificationStatus = RequestStatus.LOADED;
|
||||||
|
},
|
||||||
|
markNotificationsAsSeenRequest: (state) => {
|
||||||
|
state.notificationStatus = RequestStatus.LOADING;
|
||||||
|
},
|
||||||
|
markNotificationsAsSeenSuccess: (state) => {
|
||||||
|
state.notificationStatus = RequestStatus.LOADED;
|
||||||
|
},
|
||||||
|
markNotificationsAsSeenDenied: (state) => {
|
||||||
|
state.notificationStatus = RequestStatus.DENIED;
|
||||||
|
},
|
||||||
|
markNotificationsAsSeenFailure: (state) => {
|
||||||
|
state.notificationStatus = RequestStatus.FAILED;
|
||||||
|
},
|
||||||
|
markAllNotificationsAsReadRequest: (state) => {
|
||||||
|
state.notificationStatus = RequestStatus.LOADING;
|
||||||
|
},
|
||||||
|
markAllNotificationsAsReadSuccess: (state) => {
|
||||||
|
const updatedNotifications = Object.fromEntries(
|
||||||
|
Object.entries(state.notifications).map(([key, notification]) => [
|
||||||
|
key, { ...notification, lastRead: new Date().toISOString() },
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
state.notifications = updatedNotifications;
|
||||||
|
state.notificationStatus = RequestStatus.LOADED;
|
||||||
|
},
|
||||||
|
markAllNotificationsAsReadDenied: (state) => {
|
||||||
|
state.notificationStatus = RequestStatus.DENIED;
|
||||||
|
},
|
||||||
|
markAllNotificationsAsReadFailure: (state) => {
|
||||||
|
state.notificationStatus = RequestStatus.FAILED;
|
||||||
|
},
|
||||||
|
markNotificationsAsReadRequest: (state) => {
|
||||||
|
state.notificationStatus = RequestStatus.LOADING;
|
||||||
|
},
|
||||||
|
markNotificationsAsReadSuccess: (state, { payload }) => {
|
||||||
|
const date = new Date().toISOString();
|
||||||
|
state.notifications[payload.id] = { ...state.notifications[payload.id], lastRead: date };
|
||||||
|
state.notificationStatus = RequestStatus.LOADED;
|
||||||
|
},
|
||||||
|
markNotificationsAsReadDenied: (state) => {
|
||||||
|
state.notificationStatus = RequestStatus.DENIED;
|
||||||
|
},
|
||||||
|
markNotificationsAsReadFailure: (state) => {
|
||||||
|
state.notificationStatus = RequestStatus.FAILED;
|
||||||
|
},
|
||||||
|
resetNotificationStateRequest: () => initialState,
|
||||||
|
updateAppNameRequest: (state, { payload }) => {
|
||||||
|
state.appName = payload.appName;
|
||||||
|
state.pagination.currentPage = 1;
|
||||||
|
},
|
||||||
|
updatePaginationRequest: (state) => {
|
||||||
|
state.pagination.currentPage += 1;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const {
|
||||||
|
fetchNotificationDenied,
|
||||||
|
fetchNotificationFailure,
|
||||||
|
fetchNotificationRequest,
|
||||||
|
fetchNotificationSuccess,
|
||||||
|
fetchNotificationsCountDenied,
|
||||||
|
fetchNotificationsCountFailure,
|
||||||
|
fetchNotificationsCountRequest,
|
||||||
|
fetchNotificationsCountSuccess,
|
||||||
|
markNotificationsAsSeenRequest,
|
||||||
|
markNotificationsAsSeenSuccess,
|
||||||
|
markNotificationsAsSeenFailure,
|
||||||
|
markNotificationsAsSeenDenied,
|
||||||
|
markAllNotificationsAsReadDenied,
|
||||||
|
markAllNotificationsAsReadRequest,
|
||||||
|
markAllNotificationsAsReadSuccess,
|
||||||
|
markAllNotificationsAsReadFailure,
|
||||||
|
markNotificationsAsReadDenied,
|
||||||
|
markNotificationsAsReadRequest,
|
||||||
|
markNotificationsAsReadSuccess,
|
||||||
|
markNotificationsAsReadFailure,
|
||||||
|
resetNotificationStateRequest,
|
||||||
|
updateAppNameRequest,
|
||||||
|
updatePaginationRequest,
|
||||||
|
} = slice.actions;
|
||||||
|
|
||||||
|
export const notificationsReducer = slice.reducer;
|
||||||
130
src/Notifications/data/thunks.js
Normal file
130
src/Notifications/data/thunks.js
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import { camelCaseObject } from '@edx/frontend-platform';
|
||||||
|
import {
|
||||||
|
fetchNotificationSuccess,
|
||||||
|
fetchNotificationRequest,
|
||||||
|
fetchNotificationFailure,
|
||||||
|
fetchNotificationDenied,
|
||||||
|
fetchNotificationsCountFailure,
|
||||||
|
fetchNotificationsCountRequest,
|
||||||
|
fetchNotificationsCountSuccess,
|
||||||
|
fetchNotificationsCountDenied,
|
||||||
|
markNotificationsAsSeenRequest,
|
||||||
|
markNotificationsAsSeenSuccess,
|
||||||
|
markNotificationsAsSeenFailure,
|
||||||
|
markNotificationsAsSeenDenied,
|
||||||
|
markNotificationsAsReadDenied,
|
||||||
|
resetNotificationStateRequest,
|
||||||
|
markAllNotificationsAsReadRequest,
|
||||||
|
markAllNotificationsAsReadSuccess,
|
||||||
|
markAllNotificationsAsReadFailure,
|
||||||
|
markAllNotificationsAsReadDenied,
|
||||||
|
markNotificationsAsReadRequest,
|
||||||
|
markNotificationsAsReadSuccess,
|
||||||
|
markNotificationsAsReadFailure,
|
||||||
|
} from './slice';
|
||||||
|
import {
|
||||||
|
getNotifications, getNotificationCounts, markNotificationSeen, markAllNotificationRead, markNotificationRead,
|
||||||
|
} from './api';
|
||||||
|
import { getHttpErrorStatus } from '../utils';
|
||||||
|
|
||||||
|
const normalizeNotificationCounts = ({ countByAppName, count, showNotificationsTray }) => {
|
||||||
|
const appIds = Object.keys(countByAppName);
|
||||||
|
const apps = appIds.reduce((acc, appId) => { acc[appId] = []; return acc; }, {});
|
||||||
|
return {
|
||||||
|
countByAppName, appIds, apps, count, showNotificationsTray,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeNotifications = ({ notifications }) => {
|
||||||
|
const newNotificationIds = notifications.map(notification => notification.id.toString());
|
||||||
|
const notificationsKeyValuePair = notifications.reduce((acc, obj) => { acc[obj.id] = obj; return acc; }, {});
|
||||||
|
return {
|
||||||
|
newNotificationIds, notificationsKeyValuePair,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchNotificationList = ({ appName, page, pageSize }) => (
|
||||||
|
async (dispatch) => {
|
||||||
|
try {
|
||||||
|
dispatch(fetchNotificationRequest({ appName }));
|
||||||
|
const data = await getNotifications(appName, page, pageSize);
|
||||||
|
const normalisedData = normalizeNotifications((camelCaseObject(data)));
|
||||||
|
dispatch(fetchNotificationSuccess({ ...normalisedData, numPages: data.numPages, currentPage: data.currentPage }));
|
||||||
|
} catch (error) {
|
||||||
|
if (getHttpErrorStatus(error) === 403) {
|
||||||
|
dispatch(fetchNotificationDenied(appName));
|
||||||
|
} else {
|
||||||
|
dispatch(fetchNotificationFailure(appName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const fetchAppsNotificationCount = () => (
|
||||||
|
async (dispatch) => {
|
||||||
|
try {
|
||||||
|
dispatch(fetchNotificationsCountRequest());
|
||||||
|
const data = await getNotificationCounts();
|
||||||
|
const normalisedData = normalizeNotificationCounts((camelCaseObject(data)));
|
||||||
|
dispatch(fetchNotificationsCountSuccess({ ...normalisedData }));
|
||||||
|
} catch (error) {
|
||||||
|
if (getHttpErrorStatus(error) === 403) {
|
||||||
|
dispatch(fetchNotificationsCountDenied());
|
||||||
|
} else {
|
||||||
|
dispatch(fetchNotificationsCountFailure());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const markAllNotificationsAsRead = (appName) => (
|
||||||
|
async (dispatch) => {
|
||||||
|
try {
|
||||||
|
dispatch(markAllNotificationsAsReadRequest({ appName }));
|
||||||
|
const data = await markAllNotificationRead(appName);
|
||||||
|
dispatch(markAllNotificationsAsReadSuccess(camelCaseObject(data)));
|
||||||
|
} catch (error) {
|
||||||
|
if (getHttpErrorStatus(error) === 403) {
|
||||||
|
dispatch(markAllNotificationsAsReadDenied());
|
||||||
|
} else {
|
||||||
|
dispatch(markAllNotificationsAsReadFailure());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const markNotificationsAsRead = (notificationId) => (
|
||||||
|
async (dispatch) => {
|
||||||
|
try {
|
||||||
|
dispatch(markNotificationsAsReadRequest({ notificationId }));
|
||||||
|
const data = await markNotificationRead(notificationId);
|
||||||
|
dispatch(markNotificationsAsReadSuccess(camelCaseObject(data)));
|
||||||
|
} catch (error) {
|
||||||
|
if (getHttpErrorStatus(error) === 403) {
|
||||||
|
dispatch(markNotificationsAsReadDenied());
|
||||||
|
} else {
|
||||||
|
dispatch(markNotificationsAsReadFailure());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const markNotificationsAsSeen = (appName) => (
|
||||||
|
async (dispatch) => {
|
||||||
|
try {
|
||||||
|
dispatch(markNotificationsAsSeenRequest({ appName }));
|
||||||
|
const data = await markNotificationSeen(appName);
|
||||||
|
dispatch(markNotificationsAsSeenSuccess(camelCaseObject(data)));
|
||||||
|
} catch (error) {
|
||||||
|
if (getHttpErrorStatus(error) === 403) {
|
||||||
|
dispatch(markNotificationsAsSeenDenied());
|
||||||
|
} else {
|
||||||
|
dispatch(markNotificationsAsSeenFailure());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const resetNotificationState = () => (
|
||||||
|
async (dispatch) => { dispatch(resetNotificationStateRequest()); }
|
||||||
|
);
|
||||||
101
src/Notifications/index.jsx
Normal file
101
src/Notifications/index.jsx
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import React, {
|
||||||
|
useCallback, useEffect, useRef, useState,
|
||||||
|
} from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import {
|
||||||
|
Badge, Icon, IconButton, OverlayTrigger, Popover,
|
||||||
|
} from '@edx/paragon';
|
||||||
|
import { NotificationsNone, Settings } from '@edx/paragon/icons';
|
||||||
|
import { selectNotificationTabsCount } from './data/selectors';
|
||||||
|
import { resetNotificationState } from './data/thunks';
|
||||||
|
import { useIsOnLargeScreen, useIsOnMediumScreen } from './data/hook';
|
||||||
|
import NotificationTabs from './NotificationTabs';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
const Notifications = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const popoverRef = useRef(null);
|
||||||
|
const buttonRef = useRef(null);
|
||||||
|
const [enableNotificationTray, setEnableNotificationTray] = useState(false);
|
||||||
|
const notificationCounts = useSelector(selectNotificationTabsCount());
|
||||||
|
const isOnMediumScreen = useIsOnMediumScreen();
|
||||||
|
const isOnLargeScreen = useIsOnLargeScreen();
|
||||||
|
|
||||||
|
const hideNotificationTray = useCallback(() => {
|
||||||
|
setEnableNotificationTray(prevState => !prevState);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleClickOutsideNotificationTray = useCallback((event) => {
|
||||||
|
if (!popoverRef.current?.contains(event.target) && !buttonRef.current?.contains(event.target)) {
|
||||||
|
setEnableNotificationTray(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('mousedown', handleClickOutsideNotificationTray);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutsideNotificationTray);
|
||||||
|
dispatch(resetNotificationState());
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OverlayTrigger
|
||||||
|
trigger="click"
|
||||||
|
key="bottom"
|
||||||
|
placement="bottom"
|
||||||
|
id="notificationTray"
|
||||||
|
show={enableNotificationTray}
|
||||||
|
overlay={(
|
||||||
|
<Popover
|
||||||
|
id="notificationTray"
|
||||||
|
data-testid="notificationTray"
|
||||||
|
className={classNames('overflow-auto rounded-0 border-0', {
|
||||||
|
'w-100': !isOnMediumScreen && !isOnLargeScreen,
|
||||||
|
'medium-screen': isOnMediumScreen,
|
||||||
|
'large-screen': isOnLargeScreen,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div ref={popoverRef}>
|
||||||
|
<Popover.Title as="h2" className="d-flex justify-content-between p-0 m-4 border-0 text-primary-500 font-size-18 line-height-24">
|
||||||
|
{intl.formatMessage(messages.notificationTitle)}
|
||||||
|
<Icon src={Settings} className="icon-size-20" />
|
||||||
|
</Popover.Title>
|
||||||
|
<Popover.Content className="notification-content p-0">
|
||||||
|
<NotificationTabs />
|
||||||
|
</Popover.Content>
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div ref={buttonRef}>
|
||||||
|
<IconButton
|
||||||
|
isActive={enableNotificationTray}
|
||||||
|
alt="notification bell icon"
|
||||||
|
onClick={hideNotificationTray}
|
||||||
|
src={NotificationsNone}
|
||||||
|
iconAs={Icon}
|
||||||
|
variant="light"
|
||||||
|
iconClassNames="text-primary-500"
|
||||||
|
className="ml-4 mr-1 my-3 notification-button"
|
||||||
|
/>
|
||||||
|
{notificationCounts?.count > 0 && (
|
||||||
|
<Badge
|
||||||
|
pill
|
||||||
|
variant="danger"
|
||||||
|
className="font-weight-normal px-1 notification-badge"
|
||||||
|
>
|
||||||
|
{notificationCounts.count}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</OverlayTrigger>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Notifications;
|
||||||
36
src/Notifications/messages.js
Normal file
36
src/Notifications/messages.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
notificationTitle: {
|
||||||
|
id: 'notification.title',
|
||||||
|
defaultMessage: 'Notifications',
|
||||||
|
description: 'Notifications',
|
||||||
|
},
|
||||||
|
notificationTodayHeading: {
|
||||||
|
id: 'notification.today.heading',
|
||||||
|
defaultMessage: 'Last 24 hours',
|
||||||
|
description: 'Today Notifications',
|
||||||
|
},
|
||||||
|
notificationEarlierHeading: {
|
||||||
|
id: 'notification.earlier.heading',
|
||||||
|
defaultMessage: 'Earlier',
|
||||||
|
description: 'Earlier Notifications',
|
||||||
|
},
|
||||||
|
notificationMarkAsRead: {
|
||||||
|
id: 'notification.mark.as.read',
|
||||||
|
defaultMessage: 'Mark all as read',
|
||||||
|
description: 'Mark all Notifications as read',
|
||||||
|
},
|
||||||
|
fullStop: {
|
||||||
|
id: 'notification.fullStop',
|
||||||
|
defaultMessage: '•',
|
||||||
|
description: 'Fullstop shown to users to indicate who edited a post.',
|
||||||
|
},
|
||||||
|
loadMoreNotifications: {
|
||||||
|
id: 'notification.load.more.notifications',
|
||||||
|
defaultMessage: 'Load more notifications',
|
||||||
|
description: 'Load more button to load more notifications',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default messages;
|
||||||
52
src/Notifications/utils.js
Normal file
52
src/Notifications/utils.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import {
|
||||||
|
CheckCircle, HelpOutline, QuestionAnswerOutline, Verified, Report, EditOutline, ThumbUpOutline, PostOutline,
|
||||||
|
} from '@edx/paragon/icons';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get HTTP Error status from generic error.
|
||||||
|
* @param error Generic caught error.
|
||||||
|
* @returns {number|null}
|
||||||
|
*/
|
||||||
|
export const getHttpErrorStatus = error => error?.customAttributes?.httpErrorStatus ?? error?.response?.status;
|
||||||
|
|
||||||
|
export const splitNotificationsByTime = (notificationList) => {
|
||||||
|
let splittedData = [];
|
||||||
|
if (notificationList.length > 0) {
|
||||||
|
const currentTime = Date.now();
|
||||||
|
const twentyFourHoursAgo = currentTime - (24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
splittedData = notificationList.reduce(
|
||||||
|
(result, notification) => {
|
||||||
|
if (notification) {
|
||||||
|
const objectTime = new Date(notification.createdAt).getTime();
|
||||||
|
if (objectTime >= twentyFourHoursAgo && objectTime <= currentTime) {
|
||||||
|
result.today.push(notification);
|
||||||
|
} else {
|
||||||
|
result.earlier.push(notification);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
{ today: [], earlier: [] },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const { today, earlier } = splittedData;
|
||||||
|
return { today, earlier };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getIconByType = (type) => {
|
||||||
|
const iconMap = {
|
||||||
|
post: { icon: PostOutline, class: 'text-primary-500' },
|
||||||
|
help: { icon: HelpOutline, class: 'text-primary-500' },
|
||||||
|
respond: { icon: QuestionAnswerOutline, class: 'text-primary-500' },
|
||||||
|
comment: { icon: QuestionAnswerOutline, class: 'text-primary-500' },
|
||||||
|
question: { icon: QuestionAnswerOutline, class: 'text-primary-500' },
|
||||||
|
answer: { icon: CheckCircle, class: 'text-success' },
|
||||||
|
endorsed: { icon: Verified, class: 'text-primary-500' },
|
||||||
|
reported: { icon: Report, class: 'text-danger-500' },
|
||||||
|
postLiked: { icon: ThumbUpOutline, class: 'text-primary-500' },
|
||||||
|
commentLiked: { icon: ThumbUpOutline, class: 'text-primary-500' },
|
||||||
|
edited: { icon: EditOutline, class: 'text-primary-500' },
|
||||||
|
};
|
||||||
|
return iconMap[type] || { icon: PostOutline, class: 'text-primary-500' };
|
||||||
|
};
|
||||||
18
src/common/time-locale.js
Normal file
18
src/common/time-locale.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
export default function timeLocale(number, index) {
|
||||||
|
return [
|
||||||
|
['just now', 'right now'],
|
||||||
|
['%ss', 'in %s seconds'],
|
||||||
|
['1m', 'in 1 minute'],
|
||||||
|
['%sm', 'in %s minutes'],
|
||||||
|
['1h', 'in 1 hour'],
|
||||||
|
['%sh', 'in %s hours'],
|
||||||
|
['1d', 'in 1 day'],
|
||||||
|
['%sd', 'in %s days'],
|
||||||
|
['1w', 'in 1 week'],
|
||||||
|
['%sw', 'in %s weeks'],
|
||||||
|
['4w', 'in 1 month'],
|
||||||
|
[`${number * 4}w`, 'in %s months'],
|
||||||
|
['1y', 'in 1 year'],
|
||||||
|
['%sy', 'in %s years'],
|
||||||
|
][index];
|
||||||
|
}
|
||||||
@@ -29,5 +29,11 @@
|
|||||||
"header.menu.account.label": "الحساب",
|
"header.menu.account.label": "الحساب",
|
||||||
"header.menu.orderHistory.label": "سجل الطلبيات",
|
"header.menu.orderHistory.label": "سجل الطلبيات",
|
||||||
"header.navigation.skipNavLink": "التخطي إلى المحتوى الرئيسي",
|
"header.navigation.skipNavLink": "التخطي إلى المحتوى الرئيسي",
|
||||||
"header.menu.signOut.label": "تسجيل الخروج"
|
"header.menu.signOut.label": "تسجيل الخروج",
|
||||||
|
"notification.title": "Notifications",
|
||||||
|
"notification.today.heading": "Last 24 hours",
|
||||||
|
"notification.earlier.heading": "Earlier",
|
||||||
|
"notification.mark.as.read": "Mark all as read",
|
||||||
|
"notification.fullStop": "•",
|
||||||
|
"notification.load.more.notifications": "Load more notifications"
|
||||||
}
|
}
|
||||||
@@ -29,5 +29,11 @@
|
|||||||
"header.menu.account.label": "Account",
|
"header.menu.account.label": "Account",
|
||||||
"header.menu.orderHistory.label": "Order History",
|
"header.menu.orderHistory.label": "Order History",
|
||||||
"header.navigation.skipNavLink": "Skip to main content.",
|
"header.navigation.skipNavLink": "Skip to main content.",
|
||||||
"header.menu.signOut.label": "Sign Out"
|
"header.menu.signOut.label": "Sign Out",
|
||||||
|
"notification.title": "Notifications",
|
||||||
|
"notification.today.heading": "Last 24 hours",
|
||||||
|
"notification.earlier.heading": "Earlier",
|
||||||
|
"notification.mark.as.read": "Mark all as read",
|
||||||
|
"notification.fullStop": "•",
|
||||||
|
"notification.load.more.notifications": "Load more notifications"
|
||||||
}
|
}
|
||||||
@@ -29,5 +29,11 @@
|
|||||||
"header.menu.account.label": "Cuenta",
|
"header.menu.account.label": "Cuenta",
|
||||||
"header.menu.orderHistory.label": "Historial de órdenes",
|
"header.menu.orderHistory.label": "Historial de órdenes",
|
||||||
"header.navigation.skipNavLink": "Dirígete al contenido principal.",
|
"header.navigation.skipNavLink": "Dirígete al contenido principal.",
|
||||||
"header.menu.signOut.label": "Cerrar sesión"
|
"header.menu.signOut.label": "Cerrar sesión",
|
||||||
|
"notification.title": "Notifications",
|
||||||
|
"notification.today.heading": "Last 24 hours",
|
||||||
|
"notification.earlier.heading": "Earlier",
|
||||||
|
"notification.mark.as.read": "Mark all as read",
|
||||||
|
"notification.fullStop": "•",
|
||||||
|
"notification.load.more.notifications": "Load more notifications"
|
||||||
}
|
}
|
||||||
@@ -29,5 +29,11 @@
|
|||||||
"header.menu.account.label": "Compte",
|
"header.menu.account.label": "Compte",
|
||||||
"header.menu.orderHistory.label": "Historique des commandes",
|
"header.menu.orderHistory.label": "Historique des commandes",
|
||||||
"header.navigation.skipNavLink": "Passer au contenu principal",
|
"header.navigation.skipNavLink": "Passer au contenu principal",
|
||||||
"header.menu.signOut.label": "Se déconnecter"
|
"header.menu.signOut.label": "Se déconnecter",
|
||||||
|
"notification.title": "Notifications",
|
||||||
|
"notification.today.heading": "Last 24 hours",
|
||||||
|
"notification.earlier.heading": "Earlier",
|
||||||
|
"notification.mark.as.read": "Mark all as read",
|
||||||
|
"notification.fullStop": "•",
|
||||||
|
"notification.load.more.notifications": "Load more notifications"
|
||||||
}
|
}
|
||||||
@@ -29,5 +29,11 @@
|
|||||||
"header.menu.account.label": "Compte",
|
"header.menu.account.label": "Compte",
|
||||||
"header.menu.orderHistory.label": "Historique des commandes",
|
"header.menu.orderHistory.label": "Historique des commandes",
|
||||||
"header.navigation.skipNavLink": "Passer au contenu principal.",
|
"header.navigation.skipNavLink": "Passer au contenu principal.",
|
||||||
"header.menu.signOut.label": "Se déconnecter"
|
"header.menu.signOut.label": "Se déconnecter",
|
||||||
|
"notification.title": "Notifications",
|
||||||
|
"notification.today.heading": "Dernières 24 heures",
|
||||||
|
"notification.earlier.heading": "Plus tôt",
|
||||||
|
"notification.mark.as.read": "tout marquer comme lu",
|
||||||
|
"notification.fullStop": "•",
|
||||||
|
"notification.load.more.notifications": "Charger plus de notifications"
|
||||||
}
|
}
|
||||||
@@ -29,5 +29,11 @@
|
|||||||
"header.menu.account.label": "Account",
|
"header.menu.account.label": "Account",
|
||||||
"header.menu.orderHistory.label": "Order History",
|
"header.menu.orderHistory.label": "Order History",
|
||||||
"header.navigation.skipNavLink": "Skip to main content.",
|
"header.navigation.skipNavLink": "Skip to main content.",
|
||||||
"header.menu.signOut.label": "Sign Out"
|
"header.menu.signOut.label": "Sign Out",
|
||||||
|
"notification.title": "Notifications",
|
||||||
|
"notification.today.heading": "Last 24 hours",
|
||||||
|
"notification.earlier.heading": "Earlier",
|
||||||
|
"notification.mark.as.read": "Mark all as read",
|
||||||
|
"notification.fullStop": "•",
|
||||||
|
"notification.load.more.notifications": "Load more notifications"
|
||||||
}
|
}
|
||||||
@@ -29,5 +29,11 @@
|
|||||||
"header.menu.account.label": "Account",
|
"header.menu.account.label": "Account",
|
||||||
"header.menu.orderHistory.label": "Order History",
|
"header.menu.orderHistory.label": "Order History",
|
||||||
"header.navigation.skipNavLink": "Skip to main content.",
|
"header.navigation.skipNavLink": "Skip to main content.",
|
||||||
"header.menu.signOut.label": "Sign Out"
|
"header.menu.signOut.label": "Sign Out",
|
||||||
|
"notification.title": "Notifications",
|
||||||
|
"notification.today.heading": "Last 24 hours",
|
||||||
|
"notification.earlier.heading": "Earlier",
|
||||||
|
"notification.mark.as.read": "Mark all as read",
|
||||||
|
"notification.fullStop": "•",
|
||||||
|
"notification.load.more.notifications": "Load more notifications"
|
||||||
}
|
}
|
||||||
@@ -29,5 +29,11 @@
|
|||||||
"header.menu.account.label": "Account",
|
"header.menu.account.label": "Account",
|
||||||
"header.menu.orderHistory.label": "Order History",
|
"header.menu.orderHistory.label": "Order History",
|
||||||
"header.navigation.skipNavLink": "Skip to main content.",
|
"header.navigation.skipNavLink": "Skip to main content.",
|
||||||
"header.menu.signOut.label": "Sign Out"
|
"header.menu.signOut.label": "Sign Out",
|
||||||
|
"notification.title": "Notifications",
|
||||||
|
"notification.today.heading": "Last 24 hours",
|
||||||
|
"notification.earlier.heading": "Earlier",
|
||||||
|
"notification.mark.as.read": "Mark all as read",
|
||||||
|
"notification.fullStop": "•",
|
||||||
|
"notification.load.more.notifications": "Load more notifications"
|
||||||
}
|
}
|
||||||
@@ -29,5 +29,11 @@
|
|||||||
"header.menu.account.label": "Account",
|
"header.menu.account.label": "Account",
|
||||||
"header.menu.orderHistory.label": "Order History",
|
"header.menu.orderHistory.label": "Order History",
|
||||||
"header.navigation.skipNavLink": "Skip to main content.",
|
"header.navigation.skipNavLink": "Skip to main content.",
|
||||||
"header.menu.signOut.label": "Sign Out"
|
"header.menu.signOut.label": "Sign Out",
|
||||||
|
"notification.title": "Notifications",
|
||||||
|
"notification.today.heading": "Last 24 hours",
|
||||||
|
"notification.earlier.heading": "Earlier",
|
||||||
|
"notification.mark.as.read": "Mark all as read",
|
||||||
|
"notification.fullStop": "•",
|
||||||
|
"notification.load.more.notifications": "Load more notifications"
|
||||||
}
|
}
|
||||||
@@ -29,5 +29,11 @@
|
|||||||
"header.menu.account.label": "Account",
|
"header.menu.account.label": "Account",
|
||||||
"header.menu.orderHistory.label": "Order History",
|
"header.menu.orderHistory.label": "Order History",
|
||||||
"header.navigation.skipNavLink": "Перейти до головного змісту.",
|
"header.navigation.skipNavLink": "Перейти до головного змісту.",
|
||||||
"header.menu.signOut.label": "Sign Out"
|
"header.menu.signOut.label": "Sign Out",
|
||||||
|
"notification.title": "Notifications",
|
||||||
|
"notification.today.heading": "Last 24 hours",
|
||||||
|
"notification.earlier.heading": "Earlier",
|
||||||
|
"notification.mark.as.read": "Mark all as read",
|
||||||
|
"notification.fullStop": "•",
|
||||||
|
"notification.load.more.notifications": "Load more notifications"
|
||||||
}
|
}
|
||||||
@@ -29,5 +29,11 @@
|
|||||||
"header.menu.account.label": "Account",
|
"header.menu.account.label": "Account",
|
||||||
"header.menu.orderHistory.label": "Order History",
|
"header.menu.orderHistory.label": "Order History",
|
||||||
"header.navigation.skipNavLink": "Skip to main content.",
|
"header.navigation.skipNavLink": "Skip to main content.",
|
||||||
"header.menu.signOut.label": "Sign Out"
|
"header.menu.signOut.label": "Sign Out",
|
||||||
|
"notification.title": "Notifications",
|
||||||
|
"notification.today.heading": "Last 24 hours",
|
||||||
|
"notification.earlier.heading": "Earlier",
|
||||||
|
"notification.mark.as.read": "Mark all as read",
|
||||||
|
"notification.fullStop": "•",
|
||||||
|
"notification.load.more.notifications": "Load more notifications"
|
||||||
}
|
}
|
||||||
139
src/index.scss
139
src/index.scss
@@ -1,7 +1,10 @@
|
|||||||
$spacer: 1rem;
|
$spacer: 1rem;
|
||||||
$blue: #007db8;
|
$blue: #007db8;
|
||||||
$white: #fff;
|
$white: #fff;
|
||||||
|
@import "@edx/brand/paragon/fonts.scss";
|
||||||
|
@import "@edx/brand/paragon/variables.scss";
|
||||||
|
@import "@edx/paragon/scss/core/core.scss";
|
||||||
|
@import "@edx/brand/paragon/overrides.scss";
|
||||||
@import './Menu/menu.scss';
|
@import './Menu/menu.scss';
|
||||||
|
|
||||||
.dropdown-item a {
|
.dropdown-item a {
|
||||||
@@ -27,7 +30,7 @@ $white: #fff;
|
|||||||
|
|
||||||
.learning-header {
|
.learning-header {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
.course-title-lockup {
|
.course-title-lockup {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
@@ -118,3 +121,135 @@ $white: #fff;
|
|||||||
border-radius: $rounded-pill;
|
border-radius: $rounded-pill;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
b {
|
||||||
|
color: #00262B !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-size-18 {
|
||||||
|
font-size: 18px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-size-12 {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-size-14 {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.py-10px {
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pb-10px {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-height-24 {
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-height-20 {
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-height-10 {
|
||||||
|
line-height: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-size-20 {
|
||||||
|
width: 20px !important;
|
||||||
|
height: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cursor-pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-button {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-icon{
|
||||||
|
height: 23.33px !important;
|
||||||
|
width: 23.33px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-badge {
|
||||||
|
position: absolute;
|
||||||
|
margin-top: 18px;
|
||||||
|
margin-left: -21px;
|
||||||
|
border: 2px solid #FFFFFF;
|
||||||
|
font-size: 9px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover {
|
||||||
|
max-height: calc(100% - 68px);
|
||||||
|
min-height: 1220px;
|
||||||
|
filter: none;
|
||||||
|
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.15), 0px 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
|
||||||
|
&.medium-screen {
|
||||||
|
min-width: 24.313rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.large-screen {
|
||||||
|
min-width: 34.313rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-toggle::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandable {
|
||||||
|
position: relative !important;
|
||||||
|
margin-left: 4px;
|
||||||
|
padding: 2px 5px;
|
||||||
|
border-radius: 10rem;
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-toggle {
|
||||||
|
font-size: 14px;
|
||||||
|
padding-top: 0px !important;
|
||||||
|
padding-bottom: 12px !important;
|
||||||
|
|
||||||
|
div {
|
||||||
|
min-height: 6px !important;
|
||||||
|
min-width: 6px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-content {
|
||||||
|
.notification-item-content {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
b {
|
||||||
|
color: #00262B;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.unread {
|
||||||
|
height: 10px;
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,16 +1,31 @@
|
|||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faUserCircle } from '@fortawesome/free-solid-svg-icons';
|
import { faUserCircle } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import { Dropdown } from '@edx/paragon';
|
import { Dropdown } from '@edx/paragon';
|
||||||
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
import Notifications from '../Notifications';
|
||||||
|
import { selectShowNotificationTray, selectNotificationStatus } from '../Notifications/data/selectors';
|
||||||
|
import { fetchAppsNotificationCount } from '../Notifications/data/thunks';
|
||||||
|
import { RequestStatus } from '../Notifications/data/slice';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
const AuthenticatedUserDropdown = ({ intl, username }) => {
|
const AuthenticatedUserDropdown = ({ intl, username }) => {
|
||||||
|
const showNotificationsTray = useSelector(selectShowNotificationTray());
|
||||||
|
const notificationStatus = useSelector(selectNotificationStatus());
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (notificationStatus === RequestStatus.IDLE) {
|
||||||
|
dispatch(fetchAppsNotificationCount());
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [notificationStatus]);
|
||||||
|
|
||||||
const dashboardMenuItem = (
|
const dashboardMenuItem = (
|
||||||
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/dashboard`}>
|
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/dashboard`}>
|
||||||
{intl.formatMessage(messages.dashboard)}
|
{intl.formatMessage(messages.dashboard)}
|
||||||
@@ -19,8 +34,9 @@ const AuthenticatedUserDropdown = ({ intl, username }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<a className="text-gray-700 mr-3" href={`${getConfig().SUPPORT_URL}`}>{intl.formatMessage(messages.help)}</a>
|
<a className="text-gray-700" href={`${getConfig().SUPPORT_URL}`}>{intl.formatMessage(messages.help)}</a>
|
||||||
<Dropdown className="user-dropdown">
|
{showNotificationsTray && <Notifications />}
|
||||||
|
<Dropdown className="user-dropdown ml-3">
|
||||||
<Dropdown.Toggle variant="outline-primary">
|
<Dropdown.Toggle variant="outline-primary">
|
||||||
<FontAwesomeIcon icon={faUserCircle} className="d-md-none" size="lg" />
|
<FontAwesomeIcon icon={faUserCircle} className="d-md-none" size="lg" />
|
||||||
<span data-hj-suppress className="d-none d-md-inline">
|
<span data-hj-suppress className="d-none d-md-inline">
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import React, { useContext } from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import { AppContext } from '@edx/frontend-platform/react';
|
import { AppContext, AppProvider } from '@edx/frontend-platform/react';
|
||||||
|
|
||||||
import AnonymousUserMenu from './AnonymousUserMenu';
|
import AnonymousUserMenu from './AnonymousUserMenu';
|
||||||
import AuthenticatedUserDropdown from './AuthenticatedUserDropdown';
|
import AuthenticatedUserDropdown from './AuthenticatedUserDropdown';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
import store from '../store';
|
||||||
|
|
||||||
const LinkedLogo = ({
|
const LinkedLogo = ({
|
||||||
href,
|
href,
|
||||||
@@ -40,24 +41,26 @@ const LearningHeader = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="learning-header">
|
<AppProvider store={store}>
|
||||||
<a className="sr-only sr-only-focusable" href="#main-content">{intl.formatMessage(messages.skipNavLink)}</a>
|
<header className="learning-header">
|
||||||
<div className="container-xl py-2 d-flex align-items-center">
|
<a className="sr-only sr-only-focusable" href="#main-content">{intl.formatMessage(messages.skipNavLink)}</a>
|
||||||
{headerLogo}
|
<div className="container-xl py-2 d-flex align-items-center">
|
||||||
<div className="flex-grow-1 course-title-lockup" style={{ lineHeight: 1 }}>
|
{headerLogo}
|
||||||
<span className="d-block small m-0">{courseOrg} {courseNumber}</span>
|
<div className="flex-grow-1 course-title-lockup" style={{ lineHeight: 1 }}>
|
||||||
<span className="d-block m-0 font-weight-bold course-title">{courseTitle}</span>
|
<span className="d-block small m-0">{courseOrg} {courseNumber}</span>
|
||||||
|
<span className="d-block m-0 font-weight-bold course-title">{courseTitle}</span>
|
||||||
|
</div>
|
||||||
|
{showUserDropdown && authenticatedUser && (
|
||||||
|
<AuthenticatedUserDropdown
|
||||||
|
username={authenticatedUser.username}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showUserDropdown && !authenticatedUser && (
|
||||||
|
<AnonymousUserMenu />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{showUserDropdown && authenticatedUser && (
|
</header>
|
||||||
<AuthenticatedUserDropdown
|
</AppProvider>
|
||||||
username={authenticatedUser.username}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{showUserDropdown && !authenticatedUser && (
|
|
||||||
<AnonymousUserMenu />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ describe('Header', () => {
|
|||||||
|
|
||||||
it('displays user button', () => {
|
it('displays user button', () => {
|
||||||
render(<Header />);
|
render(<Header />);
|
||||||
expect(screen.getByRole('button')).toHaveTextContent(authenticatedUser.username);
|
expect(screen.getByText(authenticatedUser.username)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays course data', () => {
|
it('displays course data', () => {
|
||||||
|
|||||||
16
src/store.js
Normal file
16
src/store.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
import { notificationsReducer } from './Notifications/data';
|
||||||
|
|
||||||
|
export function initializeStore(preloadedState = undefined) {
|
||||||
|
return configureStore({
|
||||||
|
reducer: {
|
||||||
|
notifications: notificationsReducer,
|
||||||
|
},
|
||||||
|
preloadedState,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = initializeStore();
|
||||||
|
|
||||||
|
export default store;
|
||||||
6
src/test-utils.js
Normal file
6
src/test-utils.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
const executeThunk = async (thunk, dispatch, getState) => {
|
||||||
|
await thunk(dispatch, getState);
|
||||||
|
await new Promise(setImmediate);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default executeThunk;
|
||||||
Reference in New Issue
Block a user