Compare commits

...

4 Commits

Author SHA1 Message Date
ayesha waris
0ce451cfd2 feat: integrated notifications tray with backend apis (#363)
* feat: integrated notifications tray with backend apis

* test: updates test cases

* refactor: loader added and resolves minor nits

* test: fixes test cases related to pagination

* refactor: moved pagination to normalised data
2023-07-10 13:18:44 +05:00
renovate[bot]
42e831a693 chore(deps): update dependency @edx/frontend-build to v12.8.60 2023-07-10 07:38:50 +00:00
Jenkins
9af7eaf587 chore(i18n): update translations 2023-07-09 16:30:57 -04:00
Bilal Qamar
3fbbde1044 fix: updated frontend-build to bump eslint version (#364) 2023-07-06 18:14:00 +05:00
18 changed files with 312 additions and 221 deletions

226
package-lock.json generated
View File

@@ -30,7 +30,7 @@
"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.57", "@edx/frontend-build": "12.8.60",
"@edx/frontend-platform": "4.6.0", "@edx/frontend-platform": "4.6.0",
"@edx/reactifex": "^2.1.1", "@edx/reactifex": "^2.1.1",
"@testing-library/dom": "9.3.1", "@testing-library/dom": "9.3.1",
@@ -51,11 +51,21 @@
"redux-saga": "1.2.3" "redux-saga": "1.2.3"
}, },
"peerDependencies": { "peerDependencies": {
"@edx/frontend-platform": "^4.0.0",
"prop-types": "^15.5.10", "prop-types": "^15.5.10",
"react": "^16.9.0 || ^17.0.0", "react": "^16.9.0 || ^17.0.0",
"react-dom": "^16.9.0 || ^17.0.0" "react-dom": "^16.9.0 || ^17.0.0"
} }
}, },
"node_modules/@aashutoshrathi/word-wrap": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
"integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/@adobe/css-tools": { "node_modules/@adobe/css-tools": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.2.0.tgz", "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.2.0.tgz",
@@ -156,14 +166,14 @@
} }
}, },
"node_modules/@babel/eslint-parser": { "node_modules/@babel/eslint-parser": {
"version": "7.22.5", "version": "7.22.7",
"resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.22.5.tgz", "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.22.7.tgz",
"integrity": "sha512-C69RWYNYtrgIRE5CmTd77ZiLDXqgBipahJc/jHP3sLcAGj6AJzxNIuKNpVnICqbyK7X3pFUfEvL++rvtbQpZkQ==", "integrity": "sha512-LH6HJqjOyu/Qtp7LuSycZXK/CYXQ4ohdkliEaL1QTdtOXVdOVpTBKVxAo/+eeyt+x/2SRzB+zUPduVl+xiEvdg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1",
"eslint-visitor-keys": "^2.1.0", "@nicolo-ribaudo/semver-v6": "^6.3.3",
"semver": "^6.3.0" "eslint-visitor-keys": "^2.1.0"
}, },
"engines": { "engines": {
"node": "^10.13.0 || ^12.13.0 || >=14.0.0" "node": "^10.13.0 || ^12.13.0 || >=14.0.0"
@@ -2152,14 +2162,14 @@
} }
}, },
"node_modules/@edx/frontend-build": { "node_modules/@edx/frontend-build": {
"version": "12.8.57", "version": "12.8.60",
"resolved": "https://registry.npmjs.org/@edx/frontend-build/-/frontend-build-12.8.57.tgz", "resolved": "https://registry.npmjs.org/@edx/frontend-build/-/frontend-build-12.8.60.tgz",
"integrity": "sha512-mbWyGtF351pKs2/wtN0B16xor5ZowDw70ddBPqE5sasbIlWQw82Yx8Wie6vBjDAweCNDffOCKuIycYJYilWO8Q==", "integrity": "sha512-17AwuGoMzk/OTKoZ4Jl1MegwMWxYDKMiAkxl8x0TU2wFXy4fOz83lkm20hWYzN3olkDFEtTGDUx8b3yzR7pkoA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/cli": "7.22.5", "@babel/cli": "7.22.5",
"@babel/core": "7.22.5", "@babel/core": "7.22.5",
"@babel/eslint-parser": "7.22.5", "@babel/eslint-parser": "7.22.7",
"@babel/plugin-proposal-class-properties": "7.18.6", "@babel/plugin-proposal-class-properties": "7.18.6",
"@babel/plugin-proposal-object-rest-spread": "7.20.7", "@babel/plugin-proposal-object-rest-spread": "7.20.7",
"@babel/plugin-syntax-dynamic-import": "7.8.3", "@babel/plugin-syntax-dynamic-import": "7.8.3",
@@ -2172,7 +2182,7 @@
"@svgr/webpack": "8.0.1", "@svgr/webpack": "8.0.1",
"autoprefixer": "10.4.14", "autoprefixer": "10.4.14",
"babel-jest": "26.6.3", "babel-jest": "26.6.3",
"babel-loader": "9.1.2", "babel-loader": "9.1.3",
"babel-plugin-react-intl": "7.9.4", "babel-plugin-react-intl": "7.9.4",
"babel-plugin-transform-imports": "2.0.0", "babel-plugin-transform-imports": "2.0.0",
"babel-polyfill": "6.26.0", "babel-polyfill": "6.26.0",
@@ -2181,7 +2191,7 @@
"cssnano": "6.0.1", "cssnano": "6.0.1",
"dotenv": "8.6.0", "dotenv": "8.6.0",
"dotenv-webpack": "8.0.1", "dotenv-webpack": "8.0.1",
"eslint": "8.42.0", "eslint": "8.44.0",
"eslint-config-airbnb": "19.0.4", "eslint-config-airbnb": "19.0.4",
"eslint-plugin-import": "2.27.5", "eslint-plugin-import": "2.27.5",
"eslint-plugin-jsx-a11y": "6.7.1", "eslint-plugin-jsx-a11y": "6.7.1",
@@ -2193,7 +2203,7 @@
"image-minimizer-webpack-plugin": "3.8.3", "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.25",
"postcss-custom-media": "^9.1.2", "postcss-custom-media": "^9.1.2",
"postcss-loader": "7.3.3", "postcss-loader": "7.3.3",
"postcss-rtlcss": "4.0.6", "postcss-rtlcss": "4.0.6",
@@ -2952,9 +2962,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.2", "version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"lru-cache": "^6.0.0" "lru-cache": "^6.0.0"
@@ -3414,14 +3424,14 @@
} }
}, },
"node_modules/@eslint/eslintrc": { "node_modules/@eslint/eslintrc": {
"version": "2.0.3", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.0.tgz",
"integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==", "integrity": "sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"ajv": "^6.12.4", "ajv": "^6.12.4",
"debug": "^4.3.2", "debug": "^4.3.2",
"espree": "^9.5.2", "espree": "^9.6.0",
"globals": "^13.19.0", "globals": "^13.19.0",
"ignore": "^5.2.0", "ignore": "^5.2.0",
"import-fresh": "^3.2.1", "import-fresh": "^3.2.1",
@@ -3482,9 +3492,9 @@
} }
}, },
"node_modules/@eslint/js": { "node_modules/@eslint/js": {
"version": "8.42.0", "version": "8.44.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.42.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.44.0.tgz",
"integrity": "sha512-6SWlXpWU5AvId8Ac7zjzmIOqMOba/JWY8XZ4A7q7Gn1Vlfg/SFFIlrtHXt9nPn4op9ZPAkl91Jao+QQv3r/ukw==", "integrity": "sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0" "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -5504,6 +5514,15 @@
"eslint-scope": "5.1.1" "eslint-scope": "5.1.1"
} }
}, },
"node_modules/@nicolo-ribaudo/semver-v6": {
"version": "6.3.3",
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/semver-v6/-/semver-v6-6.3.3.tgz",
"integrity": "sha512-3Yc1fUTs69MG/uZbJlLSI3JISMn2UV2rg+1D/vROUqZyh3l6iYHCs7GMp+M40ZD7yOdDbYjJcU1oTJhrc+dGKg==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -7749,12 +7768,12 @@
} }
}, },
"node_modules/babel-loader": { "node_modules/babel-loader": {
"version": "9.1.2", "version": "9.1.3",
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.2.tgz", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.3.tgz",
"integrity": "sha512-mN14niXW43tddohGl8HPu5yfQq70iUThvFL/4QzESA7GcZoC0eVOhvWdQ8+3UlSjaDE9MVtsW9mxDY07W7VpVA==", "integrity": "sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"find-cache-dir": "^3.3.2", "find-cache-dir": "^4.0.0",
"schema-utils": "^4.0.0" "schema-utils": "^4.0.0"
}, },
"engines": { "engines": {
@@ -8902,12 +8921,6 @@
"integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==",
"dev": true "dev": true
}, },
"node_modules/commondir": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
"integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
"dev": true
},
"node_modules/component-emitter": { "node_modules/component-emitter": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
@@ -10328,15 +10341,15 @@
} }
}, },
"node_modules/eslint": { "node_modules/eslint": {
"version": "8.42.0", "version": "8.44.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.42.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.44.0.tgz",
"integrity": "sha512-ulg9Ms6E1WPf67PHaEY4/6E2tEn5/f7FXGzr3t9cBMugOmf1INYvuUwwh1aXQN4MfJ6a5K2iNwP3w4AColvI9A==", "integrity": "sha512-0wpHoUbDUHgNCyvFB5aXLiQVfK9B0at6gUvzy83k4kAsQ/u769TQDX6iKC+aO4upIHO9WSaA3QoXYQDHbNwf1A==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.4.0", "@eslint-community/regexpp": "^4.4.0",
"@eslint/eslintrc": "^2.0.3", "@eslint/eslintrc": "^2.1.0",
"@eslint/js": "8.42.0", "@eslint/js": "8.44.0",
"@humanwhocodes/config-array": "^0.11.10", "@humanwhocodes/config-array": "^0.11.10",
"@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8", "@nodelib/fs.walk": "^1.2.8",
@@ -10348,7 +10361,7 @@
"escape-string-regexp": "^4.0.0", "escape-string-regexp": "^4.0.0",
"eslint-scope": "^7.2.0", "eslint-scope": "^7.2.0",
"eslint-visitor-keys": "^3.4.1", "eslint-visitor-keys": "^3.4.1",
"espree": "^9.5.2", "espree": "^9.6.0",
"esquery": "^1.4.2", "esquery": "^1.4.2",
"esutils": "^2.0.2", "esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
@@ -10368,7 +10381,7 @@
"lodash.merge": "^4.6.2", "lodash.merge": "^4.6.2",
"minimatch": "^3.1.2", "minimatch": "^3.1.2",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
"optionator": "^0.9.1", "optionator": "^0.9.3",
"strip-ansi": "^6.0.1", "strip-ansi": "^6.0.1",
"strip-json-comments": "^3.1.0", "strip-json-comments": "^3.1.0",
"text-table": "^0.2.0" "text-table": "^0.2.0"
@@ -10818,12 +10831,12 @@
} }
}, },
"node_modules/espree": { "node_modules/espree": {
"version": "9.5.2", "version": "9.6.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.0.tgz",
"integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==", "integrity": "sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"acorn": "^8.8.0", "acorn": "^8.9.0",
"acorn-jsx": "^5.3.2", "acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^3.4.1" "eslint-visitor-keys": "^3.4.1"
}, },
@@ -11586,32 +11599,113 @@
"dev": true "dev": true
}, },
"node_modules/find-cache-dir": { "node_modules/find-cache-dir": {
"version": "3.3.2", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz",
"integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"commondir": "^1.0.1", "common-path-prefix": "^3.0.0",
"make-dir": "^3.0.2", "pkg-dir": "^7.0.0"
"pkg-dir": "^4.1.0"
}, },
"engines": { "engines": {
"node": ">=8" "node": ">=14.16"
}, },
"funding": { "funding": {
"url": "https://github.com/avajs/find-cache-dir?sponsor=1" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/find-cache-dir/node_modules/make-dir": { "node_modules/find-cache-dir/node_modules/find-up": {
"version": "3.1.0", "version": "6.3.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz",
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"semver": "^6.0.0" "locate-path": "^7.1.0",
"path-exists": "^5.0.0"
}, },
"engines": { "engines": {
"node": ">=8" "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/find-cache-dir/node_modules/locate-path": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz",
"integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==",
"dev": true,
"dependencies": {
"p-locate": "^6.0.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/find-cache-dir/node_modules/p-limit": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz",
"integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==",
"dev": true,
"dependencies": {
"yocto-queue": "^1.0.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/find-cache-dir/node_modules/p-locate": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz",
"integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==",
"dev": true,
"dependencies": {
"p-limit": "^4.0.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/find-cache-dir/node_modules/path-exists": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz",
"integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==",
"dev": true,
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
"node_modules/find-cache-dir/node_modules/pkg-dir": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz",
"integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==",
"dev": true,
"dependencies": {
"find-up": "^6.3.0"
},
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/find-cache-dir/node_modules/yocto-queue": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz",
"integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==",
"dev": true,
"engines": {
"node": ">=12.20"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
@@ -19398,17 +19492,17 @@
} }
}, },
"node_modules/optionator": { "node_modules/optionator": {
"version": "0.9.1", "version": "0.9.3",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
"integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@aashutoshrathi/word-wrap": "^1.2.3",
"deep-is": "^0.1.3", "deep-is": "^0.1.3",
"fast-levenshtein": "^2.0.6", "fast-levenshtein": "^2.0.6",
"levn": "^0.4.1", "levn": "^0.4.1",
"prelude-ls": "^1.2.1", "prelude-ls": "^1.2.1",
"type-check": "^0.4.0", "type-check": "^0.4.0"
"word-wrap": "^1.2.3"
}, },
"engines": { "engines": {
"node": ">= 0.8.0" "node": ">= 0.8.0"
@@ -19867,9 +19961,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.24", "version": "8.4.25",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.25.tgz",
"integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==", "integrity": "sha512-7taJ/8t2av0Z+sQEvNzCkpDynl0tX3uJMCODi6nT3PfASC7dYCWV9aQ+uiCf+KBD4SEFcu+GvJdGdwzQ6OSjCw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {

View File

@@ -35,7 +35,7 @@
"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.57", "@edx/frontend-build": "12.8.60",
"@edx/frontend-platform": "4.6.0", "@edx/frontend-platform": "4.6.0",
"@edx/reactifex": "^2.1.1", "@edx/reactifex": "^2.1.1",
"@testing-library/dom": "9.3.1", "@testing-library/dom": "9.3.1",

View File

@@ -1,21 +1,24 @@
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo } from 'react';
import { Button } from '@edx/paragon'; import { Button, Spinner } from '@edx/paragon';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
import isEmpty from 'lodash/isEmpty'; import isEmpty from 'lodash/isEmpty';
import messages from './messages'; import messages from './messages';
import NotificationRowItem from './NotificationRowItem'; import NotificationRowItem from './NotificationRowItem';
import { markAllNotificationsAsRead } from './data/thunks'; import { markAllNotificationsAsRead } from './data/thunks';
import { selectNotificationsByIds, selectPaginationData, selectSelectedAppName } from './data/selectors'; import {
selectNotificationsByIds, selectPaginationData, selectSelectedAppName, selectNotificationStatus,
} from './data/selectors';
import { splitNotificationsByTime } from './utils'; import { splitNotificationsByTime } from './utils';
import { updatePaginationRequest } from './data/slice'; import { updatePaginationRequest, RequestStatus } from './data/slice';
const NotificationSections = () => { const NotificationSections = () => {
const intl = useIntl(); const intl = useIntl();
const dispatch = useDispatch(); const dispatch = useDispatch();
const selectedAppName = useSelector(selectSelectedAppName()); const selectedAppName = useSelector(selectSelectedAppName());
const notificationRequestStatus = useSelector(selectNotificationStatus());
const notifications = useSelector(selectNotificationsByIds(selectedAppName)); const notifications = useSelector(selectNotificationsByIds(selectedAppName));
const { currentPage, numPages } = useSelector(selectPaginationData()); const { hasMorePages } = useSelector(selectPaginationData());
const { today = [], earlier = [] } = useMemo( const { today = [], earlier = [] } = useMemo(
() => splitNotificationsByTime(notifications), () => splitNotificationsByTime(notifications),
[notifications], [notifications],
@@ -70,15 +73,21 @@ const NotificationSections = () => {
<div className="mt-4 px-4" data-testid="notification-tray-section"> <div className="mt-4 px-4" data-testid="notification-tray-section">
{renderNotificationSection('today', today)} {renderNotificationSection('today', today)}
{renderNotificationSection('earlier', earlier)} {renderNotificationSection('earlier', earlier)}
{currentPage < numPages && ( {hasMorePages && notificationRequestStatus === RequestStatus.IN_PROGRESS ? (
<Button <div className="d-flex justify-content-center p-4">
variant="primary" <Spinner animation="border" variant="primary" size="lg" />
className="w-100 bg-primary-500" </div>
onClick={updatePagination} ) : (hasMorePages && notificationRequestStatus === RequestStatus.SUCCESSFUL
data-testid="load-more-notifications" && (
> <Button
{intl.formatMessage(messages.loadMoreNotifications)} variant="primary"
</Button> className="w-100 bg-primary-500"
onClick={updatePagination}
data-testid="load-more-notifications"
>
{intl.formatMessage(messages.loadMoreNotifications)}
</Button>
)
)} )}
</div> </div>
); );

View File

@@ -17,7 +17,7 @@ const NotificationTabs = () => {
const { currentPage } = useSelector(selectPaginationData()); const { currentPage } = useSelector(selectPaginationData());
useEffect(() => { useEffect(() => {
dispatch(fetchNotificationList({ appName: selectedAppName, page: currentPage, pageSize: 10 })); dispatch(fetchNotificationList({ appName: selectedAppName, page: currentPage }));
if (selectedAppName) { dispatch(markNotificationsAsSeen(selectedAppName)); } if (selectedAppName) { dispatch(markNotificationsAsSeen(selectedAppName)); }
}, [currentPage, selectedAppName]); }, [currentPage, selectedAppName]);

View File

@@ -4,7 +4,7 @@ Factory.define('notificationsCount')
.attr('count', 45) .attr('count', 45)
.attr('countByAppName', { .attr('countByAppName', {
reminders: 10, reminders: 10,
discussions: 20, discussion: 20,
grades: 10, grades: 10,
authoring: 5, authoring: 5,
}) })
@@ -13,10 +13,19 @@ Factory.define('notificationsCount')
Factory.define('notification') Factory.define('notification')
.sequence('id') .sequence('id')
.attr('type', 'post') .attr('type', 'post')
.sequence('content', ['id'], (idx, notificationId) => `<p><b>User ${idx}</b> posts <b>Hello and welcome to SC0x .sequence('content', ['id'], (idx, notificationId) => `<p><strong>User ${idx}</strong> posts <strong>Hello and welcome to SC0x
${notificationId}!</b></p>`) ${notificationId}!</strong></p>`)
.attr('course_name', 'Supply Chain Analytics') .attr('course_name', 'Supply Chain Analytics')
.sequence('content_url', (idx) => `https://example.com/${idx}`) .sequence('content_url', (idx) => `https://example.com/${idx}`)
.attr('last_read', null) .attr('last_read', null)
.attr('last_seen', null) .attr('last_seen', null)
.sequence('created_at', ['createdDate'], (idx, date) => date); .sequence('created_at', ['createdDate'], (idx, date) => date);
Factory.define('notificationsList')
.attr('next', null)
.attr('previous', null)
.attr('count', null, 2)
.attr('num_pages', null, 1)
.attr('current_page', null, 1)
.attr('start', null, 0)
.attr('results', ['results'], (results) => results || Factory.buildList('notification', 2, null, { createdDate: new Date().toISOString() }));

View File

@@ -2,19 +2,14 @@ import { getConfig, snakeCaseObject } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
export const getNotificationsCountApiUrl = () => `${getConfig().LMS_BASE_URL}/api/notifications/count/`; export const getNotificationsCountApiUrl = () => `${getConfig().LMS_BASE_URL}/api/notifications/count/`;
export const getNotificationsApiUrl = () => `${getConfig().LMS_BASE_URL}/api/notifications/`; export const getNotificationsListApiUrl = () => `${getConfig().LMS_BASE_URL}/api/notifications/`;
export const markNotificationsSeenApiUrl = (appName) => `${getConfig().LMS_BASE_URL}/api/notifications/mark-notifications-unseen/${appName}/`; export const markNotificationsSeenApiUrl = (appName) => `${getConfig().LMS_BASE_URL}/api/notifications/mark-seen/${appName}/`;
export const markNotificationAsReadApiUrl = () => `${getConfig().LMS_BASE_URL}/api/notifications/read/`; export const markNotificationAsReadApiUrl = () => `${getConfig().LMS_BASE_URL}/api/notifications/read/`;
export async function getNotifications(appName, page, pageSize) { export async function getNotificationsList(appName, page) {
const params = snakeCaseObject({ page, pageSize }); const params = snakeCaseObject({ appName, page });
const { data } = await getAuthenticatedHttpClient().get(getNotificationsApiUrl(), { params }); const { data } = await getAuthenticatedHttpClient().get(getNotificationsListApiUrl(), { params });
return data;
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() { export async function getNotificationCounts() {
@@ -31,14 +26,14 @@ export async function markNotificationSeen(appName) {
export async function markAllNotificationRead(appName) { export async function markAllNotificationRead(appName) {
const params = snakeCaseObject({ appName }); const params = snakeCaseObject({ appName });
const { data } = await getAuthenticatedHttpClient().put(markNotificationAsReadApiUrl(), { params }); const { data } = await getAuthenticatedHttpClient().patch(markNotificationAsReadApiUrl(), params);
return data; return data;
} }
export async function markNotificationRead(notificationId) { export async function markNotificationRead(notificationId) {
const params = snakeCaseObject({ notificationId }); const params = snakeCaseObject({ notificationId });
const { data } = await getAuthenticatedHttpClient().put(markNotificationAsReadApiUrl(), { params }); const { data } = await getAuthenticatedHttpClient().patch(markNotificationAsReadApiUrl(), params);
return { data, id: notificationId }; return { data, id: notificationId };
} }

View File

@@ -5,15 +5,15 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { initializeMockApp } from '@edx/frontend-platform/testing'; import { initializeMockApp } from '@edx/frontend-platform/testing';
import { import {
getNotificationsApiUrl, getNotificationsCountApiUrl, markNotificationAsReadApiUrl, markNotificationsSeenApiUrl, getNotificationsListApiUrl, getNotificationsCountApiUrl, markNotificationAsReadApiUrl, markNotificationsSeenApiUrl,
getNotificationCounts, getNotifications, markNotificationSeen, markAllNotificationRead, markNotificationRead, getNotificationCounts, getNotificationsList, markNotificationSeen, markAllNotificationRead, markNotificationRead,
} from './api'; } from './api';
import './__factories__'; import './__factories__';
const notificationCountsApiUrl = getNotificationsCountApiUrl(); const notificationCountsApiUrl = getNotificationsCountApiUrl();
const notificationsApiUrl = getNotificationsApiUrl(); const notificationsApiUrl = getNotificationsListApiUrl();
const markedAllNotificationsAsSeenApiUrl = markNotificationsSeenApiUrl('discussions'); const markedAllNotificationsAsSeenApiUrl = markNotificationsSeenApiUrl('discussion');
const markedAllNotificationsAsReadApiUrl = markNotificationAsReadApiUrl(); const markedAllNotificationsAsReadApiUrl = markNotificationAsReadApiUrl();
let axiosMock = null; let axiosMock = null;
@@ -43,7 +43,7 @@ describe('Notifications API', () => {
expect(count).toEqual(45); expect(count).toEqual(45);
expect(countByAppName.reminders).toEqual(10); expect(countByAppName.reminders).toEqual(10);
expect(countByAppName.discussions).toEqual(20); expect(countByAppName.discussion).toEqual(20);
expect(countByAppName.grades).toEqual(10); expect(countByAppName.grades).toEqual(10);
expect(countByAppName.authoring).toEqual(5); expect(countByAppName.authoring).toEqual(5);
}); });
@@ -62,14 +62,11 @@ describe('Notifications API', () => {
}); });
it('Successfully get notifications.', async () => { it('Successfully get notifications.', async () => {
axiosMock.onGet(notificationsApiUrl).reply( axiosMock.onGet(notificationsApiUrl).reply(200, (Factory.build('notificationsList')));
200,
(Factory.buildList('notification', 2, null, { createdDate: new Date().toISOString() })),
);
const { notifications } = await getNotifications('discussions', 1, 10); const notifications = await getNotificationsList('discussion', 1);
expect(notifications).toHaveLength(2); expect(notifications.results).toHaveLength(2);
}); });
it.each([ it.each([
@@ -78,7 +75,7 @@ describe('Notifications API', () => {
])('%s for notification API.', async ({ statusCode, message }) => { ])('%s for notification API.', async ({ statusCode, message }) => {
axiosMock.onGet(notificationsApiUrl).reply(statusCode, { message }); axiosMock.onGet(notificationsApiUrl).reply(statusCode, { message });
try { try {
await getNotifications({ page: 1, pageSize: 10 }); await getNotificationsList('discussion', 1);
} catch (error) { } catch (error) {
expect(error.response.status).toEqual(statusCode); expect(error.response.status).toEqual(statusCode);
expect(error.response.data.message).toEqual(message); expect(error.response.data.message).toEqual(message);
@@ -88,7 +85,7 @@ describe('Notifications API', () => {
it('Successfully marked all notifications as seen for selected app.', async () => { it('Successfully marked all notifications as seen for selected app.', async () => {
axiosMock.onPut(markedAllNotificationsAsSeenApiUrl).reply(200, { message: 'Notifications marked seen.' }); axiosMock.onPut(markedAllNotificationsAsSeenApiUrl).reply(200, { message: 'Notifications marked seen.' });
const { message } = await markNotificationSeen('discussions'); const { message } = await markNotificationSeen('discussion');
expect(message).toEqual('Notifications marked seen.'); expect(message).toEqual('Notifications marked seen.');
}); });
@@ -99,7 +96,7 @@ describe('Notifications API', () => {
])('%s for notification mark as seen API.', async ({ statusCode, message }) => { ])('%s for notification mark as seen API.', async ({ statusCode, message }) => {
axiosMock.onPut(markedAllNotificationsAsSeenApiUrl).reply(statusCode, { message }); axiosMock.onPut(markedAllNotificationsAsSeenApiUrl).reply(statusCode, { message });
try { try {
await markNotificationSeen('discussions'); await markNotificationSeen('discussion');
} catch (error) { } catch (error) {
expect(error.response.status).toEqual(statusCode); expect(error.response.status).toEqual(statusCode);
expect(error.response.data.message).toEqual(message); expect(error.response.data.message).toEqual(message);
@@ -107,9 +104,9 @@ describe('Notifications API', () => {
}); });
it('Successfully marked all notifications as read for selected app.', async () => { it('Successfully marked all notifications as read for selected app.', async () => {
axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(200, { message: 'Notifications marked read.' }); axiosMock.onPatch(markedAllNotificationsAsReadApiUrl).reply(200, { message: 'Notifications marked read.' });
const { message } = await markAllNotificationRead('discussions'); const { message } = await markAllNotificationRead('discussion');
expect(message).toEqual('Notifications marked read.'); expect(message).toEqual('Notifications marked read.');
}); });
@@ -118,9 +115,9 @@ describe('Notifications API', () => {
{ statusCode: 404, message: 'Failed to mark all notifications as read for selected app.' }, { 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.' }, { statusCode: 403, message: 'Denied to mark all notifications as read for selected app.' },
])('%s for notification mark all as read API.', async ({ statusCode, message }) => { ])('%s for notification mark all as read API.', async ({ statusCode, message }) => {
axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(statusCode, { message }); axiosMock.onPatch(markedAllNotificationsAsReadApiUrl).reply(statusCode, { message });
try { try {
await markAllNotificationRead('discussions'); await markAllNotificationRead('discussion');
} catch (error) { } catch (error) {
expect(error.response.status).toEqual(statusCode); expect(error.response.status).toEqual(statusCode);
expect(error.response.data.message).toEqual(message); expect(error.response.data.message).toEqual(message);
@@ -128,7 +125,7 @@ describe('Notifications API', () => {
}); });
it('Successfully marked notification as read.', async () => { it('Successfully marked notification as read.', async () => {
axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(200, { message: 'Notification marked read.' }); axiosMock.onPatch(markedAllNotificationsAsReadApiUrl).reply(200, { message: 'Notification marked read.' });
const { data } = await markNotificationRead(1); const { data } = await markNotificationRead(1);
@@ -139,7 +136,7 @@ describe('Notifications API', () => {
{ statusCode: 404, message: 'Failed to mark notification as read.' }, { statusCode: 404, message: 'Failed to mark notification as read.' },
{ statusCode: 403, message: 'Denied to mark notification as read.' }, { statusCode: 403, message: 'Denied to mark notification as read.' },
])('%s for notification mark as read API.', async ({ statusCode, message }) => { ])('%s for notification mark as read API.', async ({ statusCode, message }) => {
axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(statusCode, { message }); axiosMock.onPatch(markedAllNotificationsAsReadApiUrl).reply(statusCode, { message });
try { try {
await markAllNotificationRead(1); await markAllNotificationRead(1);
} catch (error) { } catch (error) {

View File

@@ -6,8 +6,9 @@ import { initializeMockApp } from '@edx/frontend-platform/testing';
import { initializeStore } from '../../store'; import { initializeStore } from '../../store';
import executeThunk from '../../test-utils'; import executeThunk from '../../test-utils';
import mockNotificationsResponse from '../test-utils';
import { import {
getNotificationsApiUrl, getNotificationsCountApiUrl, markNotificationAsReadApiUrl, markNotificationsSeenApiUrl, getNotificationsListApiUrl, getNotificationsCountApiUrl, markNotificationAsReadApiUrl, markNotificationsSeenApiUrl,
} from './api'; } from './api';
import { import {
fetchAppsNotificationCount, fetchNotificationList, markNotificationsAsRead, markAllNotificationsAsRead, fetchAppsNotificationCount, fetchNotificationList, markNotificationsAsRead, markAllNotificationsAsRead,
@@ -17,9 +18,9 @@ import {
import './__factories__'; import './__factories__';
const notificationCountsApiUrl = getNotificationsCountApiUrl(); const notificationCountsApiUrl = getNotificationsCountApiUrl();
const notificationsApiUrl = getNotificationsApiUrl(); const notificationsListApiUrl = getNotificationsListApiUrl();
const markedAllNotificationsAsReadApiUrl = markNotificationAsReadApiUrl(); const markedAllNotificationsAsReadApiUrl = markNotificationAsReadApiUrl();
const markedAllNotificationsAsSeenApiUrl = markNotificationsSeenApiUrl('discussions'); const markedAllNotificationsAsSeenApiUrl = markNotificationsSeenApiUrl('discussion');
let axiosMock; let axiosMock;
let store; let store;
@@ -38,13 +39,7 @@ describe('Notification Redux', () => {
Factory.resetAll(); Factory.resetAll();
store = initializeStore(); store = initializeStore();
axiosMock.onGet(notificationCountsApiUrl).reply(200, (Factory.build('notificationsCount'))); ({ store, axiosMock } = await mockNotificationsResponse());
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(() => { afterEach(() => {
@@ -57,30 +52,26 @@ describe('Notification Redux', () => {
const { notifications } = store.getState(); const { notifications } = store.getState();
expect(notifications.notificationStatus).toEqual('idle'); expect(notifications.notificationStatus).toEqual('idle');
expect(notifications.appName).toEqual('discussions'); expect(notifications.appName).toEqual('discussion');
expect(notifications.appsId).toHaveLength(0); expect(notifications.appsId).toHaveLength(0);
expect(notifications.apps).toEqual({}); expect(notifications.apps).toEqual({});
expect(notifications.notifications).toEqual({}); expect(notifications.notifications).toEqual({});
expect(notifications.tabsCount).toEqual({}); expect(notifications.tabsCount).toEqual({});
expect(notifications.showNotificationsTray).toEqual(false); expect(notifications.showNotificationsTray).toEqual(false);
expect(notifications.pagination.count).toEqual(10); expect(notifications.pagination).toEqual({});
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 () => { it('Successfully loaded notifications list in the redux.', async () => {
const { notifications: { notifications } } = store.getState(); const { notifications: { notifications } } = store.getState();
expect(Object.keys(notifications)).toHaveLength(10);
expect(Object.keys(notifications)).toHaveLength(2);
}); });
it.each([ it.each([
{ statusCode: 404, status: 'failed' }, { statusCode: 404, status: 'failed' },
{ statusCode: 403, status: 'denied' }, { statusCode: 403, status: 'denied' },
])('%s to load notifications list in the redux.', async ({ statusCode, status }) => { ])('%s to load notifications list in the redux.', async ({ statusCode, status }) => {
axiosMock.onGet(notificationsApiUrl).reply(statusCode); axiosMock.onGet(notificationsListApiUrl).reply(statusCode);
await executeThunk(fetchNotificationList({ page: 1, pageSize: 10 }), store.dispatch, store.getState); await executeThunk(fetchNotificationList({ page: 1 }), store.dispatch, store.getState);
const { notifications: { notificationStatus } } = store.getState(); const { notifications: { notificationStatus } } = store.getState();
@@ -92,7 +83,7 @@ describe('Notification Redux', () => {
expect(tabsCount.count).toEqual(25); expect(tabsCount.count).toEqual(25);
expect(tabsCount.reminders).toEqual(10); expect(tabsCount.reminders).toEqual(10);
expect(tabsCount.discussions).toEqual(0); expect(tabsCount.discussion).toEqual(0);
expect(tabsCount.grades).toEqual(10); expect(tabsCount.grades).toEqual(10);
expect(tabsCount.authoring).toEqual(5); expect(tabsCount.authoring).toEqual(5);
}); });
@@ -111,7 +102,7 @@ describe('Notification Redux', () => {
it('Successfully marked all notifications as seen for selected app.', async () => { it('Successfully marked all notifications as seen for selected app.', async () => {
axiosMock.onPut(markedAllNotificationsAsSeenApiUrl).reply(200); axiosMock.onPut(markedAllNotificationsAsSeenApiUrl).reply(200);
await executeThunk(markNotificationsAsSeen('discussions'), store.dispatch, store.getState); await executeThunk(markNotificationsAsSeen('discussion'), store.dispatch, store.getState);
expect(store.getState().notifications.notificationStatus).toEqual('successful'); expect(store.getState().notifications.notificationStatus).toEqual('successful');
}); });
@@ -121,7 +112,7 @@ describe('Notification Redux', () => {
{ statusCode: 403, status: 'denied' }, { statusCode: 403, status: 'denied' },
])('%s to mark all notifications as seen for selected app.', async ({ statusCode, status }) => { ])('%s to mark all notifications as seen for selected app.', async ({ statusCode, status }) => {
axiosMock.onPut(markedAllNotificationsAsSeenApiUrl).reply(statusCode); axiosMock.onPut(markedAllNotificationsAsSeenApiUrl).reply(statusCode);
await executeThunk(markNotificationsAsSeen('discussions'), store.dispatch, store.getState); await executeThunk(markNotificationsAsSeen('discussion'), store.dispatch, store.getState);
const { notifications: { notificationStatus } } = store.getState(); const { notifications: { notificationStatus } } = store.getState();
@@ -129,8 +120,8 @@ describe('Notification Redux', () => {
}); });
it('Successfully marked all notifications as read for selected app in the redux.', async () => { it('Successfully marked all notifications as read for selected app in the redux.', async () => {
axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(200); axiosMock.onPatch(markedAllNotificationsAsReadApiUrl).reply(200);
await executeThunk(markAllNotificationsAsRead('discussions'), store.dispatch, store.getState); await executeThunk(markAllNotificationsAsRead('discussion'), store.dispatch, store.getState);
const { notifications: { notificationStatus, notifications } } = store.getState(); const { notifications: { notificationStatus, notifications } } = store.getState();
const firstNotification = Object.values(notifications)[0]; const firstNotification = Object.values(notifications)[0];
@@ -140,7 +131,7 @@ describe('Notification Redux', () => {
}); });
it('Successfully marked notification as read in the redux.', async () => { it('Successfully marked notification as read in the redux.', async () => {
axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(200); axiosMock.onPatch(markedAllNotificationsAsReadApiUrl).reply(200);
await executeThunk(markNotificationsAsRead(1), store.dispatch, store.getState); await executeThunk(markNotificationsAsRead(1), store.dispatch, store.getState);
const { notifications: { notificationStatus, notifications } } = store.getState(); const { notifications: { notificationStatus, notifications } } = store.getState();
@@ -154,7 +145,7 @@ describe('Notification Redux', () => {
{ statusCode: 404, status: 'failed' }, { statusCode: 404, status: 'failed' },
{ statusCode: 403, status: 'denied' }, { statusCode: 403, status: 'denied' },
])('%s to marked notification as read in the redux.', async ({ statusCode, status }) => { ])('%s to marked notification as read in the redux.', async ({ statusCode, status }) => {
axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(statusCode); axiosMock.onPatch(markedAllNotificationsAsReadApiUrl).reply(statusCode);
await executeThunk(markNotificationsAsRead(1), store.dispatch, store.getState); await executeThunk(markNotificationsAsRead(1), store.dispatch, store.getState);
const { notifications: { notificationStatus } } = store.getState(); const { notifications: { notificationStatus } } = store.getState();

View File

@@ -5,8 +5,7 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { initializeMockApp } from '@edx/frontend-platform/testing'; import { initializeMockApp } from '@edx/frontend-platform/testing';
import { initializeStore } from '../../store'; import { initializeStore } from '../../store';
import executeThunk from '../../test-utils'; import mockNotificationsResponse from '../test-utils';
import { getNotificationsApiUrl, getNotificationsCountApiUrl } from './api';
import { import {
selectNotifications, selectNotifications,
selectNotificationsByIds, selectNotificationsByIds,
@@ -18,13 +17,9 @@ import {
selectSelectedAppNotificationIds, selectSelectedAppNotificationIds,
selectShowNotificationTray, selectShowNotificationTray,
} from './selectors'; } from './selectors';
import { fetchAppsNotificationCount, fetchNotificationList } from './thunks';
import './__factories__'; import './__factories__';
const notificationCountsApiUrl = getNotificationsCountApiUrl();
const notificationsApiUrl = getNotificationsApiUrl();
let axiosMock; let axiosMock;
let store; let store;
@@ -42,13 +37,7 @@ describe('Notification Selectors', () => {
Factory.resetAll(); Factory.resetAll();
store = initializeStore(); store = initializeStore();
axiosMock.onGet(notificationCountsApiUrl).reply(200, (Factory.build('notificationsCount'))); ({ store, axiosMock } = await mockNotificationsResponse());
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(() => { afterEach(() => {
@@ -68,7 +57,7 @@ describe('Notification Selectors', () => {
expect(tabsCount.count).toEqual(25); expect(tabsCount.count).toEqual(25);
expect(tabsCount.reminders).toEqual(10); expect(tabsCount.reminders).toEqual(10);
expect(tabsCount.discussions).toEqual(0); expect(tabsCount.discussion).toEqual(0);
expect(tabsCount.grades).toEqual(10); expect(tabsCount.grades).toEqual(10);
expect(tabsCount.authoring).toEqual(5); expect(tabsCount.authoring).toEqual(5);
}); });
@@ -82,9 +71,9 @@ describe('Notification Selectors', () => {
it('Should return selected app notification ids.', async () => { it('Should return selected app notification ids.', async () => {
const state = store.getState(); const state = store.getState();
const notificationIds = selectSelectedAppNotificationIds('discussions')(state); const notificationIds = selectSelectedAppNotificationIds('discussion')(state);
expect(notificationIds).toHaveLength(2); expect(notificationIds).toHaveLength(10);
}); });
it('Should return show notification tray status.', async () => { it('Should return show notification tray status.', async () => {
@@ -98,29 +87,29 @@ describe('Notification Selectors', () => {
const state = store.getState(); const state = store.getState();
const notifications = selectNotifications()(state); const notifications = selectNotifications()(state);
expect(Object.keys(notifications)).toHaveLength(2); expect(Object.keys(notifications)).toHaveLength(10);
}); });
it('Should return notifications from Ids.', async () => { it('Should return notifications from Ids.', async () => {
const state = store.getState(); const state = store.getState();
const notifications = selectNotificationsByIds('discussions')(state); const notifications = selectNotificationsByIds('discussion')(state);
expect(notifications).toHaveLength(2); expect(notifications).toHaveLength(10);
}); });
it('Should return selected app name.', async () => { it('Should return selected app name.', async () => {
const state = store.getState(); const state = store.getState();
const appName = selectSelectedAppName()(state); const appName = selectSelectedAppName()(state);
expect(appName).toEqual('discussions'); expect(appName).toEqual('discussion');
}); });
it('Should return pagination data.', async () => { it('Should return pagination data.', async () => {
const state = store.getState(); const state = store.getState();
const paginationData = selectPaginationData()(state); const paginationData = selectPaginationData()(state);
expect(paginationData.count).toEqual(10);
expect(paginationData.currentPage).toEqual(1); expect(paginationData.currentPage).toEqual(1);
expect(paginationData.numPages).toEqual(2); expect(paginationData.numPages).toEqual(2);
expect(paginationData.hasMorePages).toEqual(true);
}); });
}); });

View File

@@ -3,26 +3,21 @@ import { createSlice } from '@reduxjs/toolkit';
export const RequestStatus = { export const RequestStatus = {
IDLE: 'idle', IDLE: 'idle',
LOADING: 'in-progress', IN_PROGRESS: 'in-progress',
LOADED: 'successful', SUCCESSFUL: 'successful',
FAILED: 'failed', FAILED: 'failed',
DENIED: 'denied', DENIED: 'denied',
}; };
const initialState = { const initialState = {
notificationStatus: 'idle', notificationStatus: RequestStatus.IDLE,
appName: 'discussions', appName: 'discussion',
appsId: [], appsId: [],
apps: {}, apps: {},
notifications: {}, notifications: {},
tabsCount: {}, tabsCount: {},
showNotificationsTray: false, showNotificationsTray: false,
pagination: { pagination: {},
count: 10,
numPages: 1,
currentPage: 1,
nextPage: null,
},
}; };
const slice = createSlice({ const slice = createSlice({
name: 'notifications', name: 'notifications',
@@ -35,21 +30,19 @@ const slice = createSlice({
state.notificationStatus = RequestStatus.FAILED; state.notificationStatus = RequestStatus.FAILED;
}, },
fetchNotificationRequest: (state) => { fetchNotificationRequest: (state) => {
state.notificationStatus = RequestStatus.LOADING; state.notificationStatus = RequestStatus.IN_PROGRESS;
}, },
fetchNotificationSuccess: (state, { payload }) => { fetchNotificationSuccess: (state, { payload }) => {
const { const {
newNotificationIds, notificationsKeyValuePair, numPages, currentPage, newNotificationIds, notificationsKeyValuePair, pagination,
} = payload; } = payload;
const existingNotificationIds = state.apps[state.appName]; const existingNotificationIds = state.apps[state.appName];
state.apps[state.appName] = Array.from(new Set([...existingNotificationIds, ...newNotificationIds])); state.apps[state.appName] = Array.from(new Set([...existingNotificationIds, ...newNotificationIds]));
state.notifications = { ...state.notifications, ...notificationsKeyValuePair }; state.notifications = { ...state.notifications, ...notificationsKeyValuePair };
state.tabsCount.count -= state.tabsCount[state.appName]; state.tabsCount.count -= state.tabsCount[state.appName];
state.tabsCount[state.appName] = 0; state.tabsCount[state.appName] = 0;
state.notificationStatus = RequestStatus.LOADED; state.notificationStatus = RequestStatus.SUCCESSFUL;
state.pagination.numPages = numPages; state.pagination = pagination;
state.pagination.currentPage = currentPage;
}, },
fetchNotificationsCountDenied: (state) => { fetchNotificationsCountDenied: (state) => {
state.notificationStatus = RequestStatus.DENIED; state.notificationStatus = RequestStatus.DENIED;
@@ -58,7 +51,7 @@ const slice = createSlice({
state.notificationStatus = RequestStatus.FAILED; state.notificationStatus = RequestStatus.FAILED;
}, },
fetchNotificationsCountRequest: (state) => { fetchNotificationsCountRequest: (state) => {
state.notificationStatus = RequestStatus.LOADING; state.notificationStatus = RequestStatus.IN_PROGRESS;
}, },
fetchNotificationsCountSuccess: (state, { payload }) => { fetchNotificationsCountSuccess: (state, { payload }) => {
const { const {
@@ -68,13 +61,13 @@ const slice = createSlice({
state.appsId = appIds; state.appsId = appIds;
state.apps = apps; state.apps = apps;
state.showNotificationsTray = showNotificationsTray; state.showNotificationsTray = showNotificationsTray;
state.notificationStatus = RequestStatus.LOADED; state.notificationStatus = RequestStatus.SUCCESSFUL;
}, },
markNotificationsAsSeenRequest: (state) => { markNotificationsAsSeenRequest: (state) => {
state.notificationStatus = RequestStatus.LOADING; state.notificationStatus = RequestStatus.IN_PROGRESS;
}, },
markNotificationsAsSeenSuccess: (state) => { markNotificationsAsSeenSuccess: (state) => {
state.notificationStatus = RequestStatus.LOADED; state.notificationStatus = RequestStatus.SUCCESSFUL;
}, },
markNotificationsAsSeenDenied: (state) => { markNotificationsAsSeenDenied: (state) => {
state.notificationStatus = RequestStatus.DENIED; state.notificationStatus = RequestStatus.DENIED;
@@ -83,7 +76,7 @@ const slice = createSlice({
state.notificationStatus = RequestStatus.FAILED; state.notificationStatus = RequestStatus.FAILED;
}, },
markAllNotificationsAsReadRequest: (state) => { markAllNotificationsAsReadRequest: (state) => {
state.notificationStatus = RequestStatus.LOADING; state.notificationStatus = RequestStatus.IN_PROGRESS;
}, },
markAllNotificationsAsReadSuccess: (state) => { markAllNotificationsAsReadSuccess: (state) => {
const updatedNotifications = Object.fromEntries( const updatedNotifications = Object.fromEntries(
@@ -92,7 +85,7 @@ const slice = createSlice({
]), ]),
); );
state.notifications = updatedNotifications; state.notifications = updatedNotifications;
state.notificationStatus = RequestStatus.LOADED; state.notificationStatus = RequestStatus.SUCCESSFUL;
}, },
markAllNotificationsAsReadDenied: (state) => { markAllNotificationsAsReadDenied: (state) => {
state.notificationStatus = RequestStatus.DENIED; state.notificationStatus = RequestStatus.DENIED;
@@ -101,12 +94,12 @@ const slice = createSlice({
state.notificationStatus = RequestStatus.FAILED; state.notificationStatus = RequestStatus.FAILED;
}, },
markNotificationsAsReadRequest: (state) => { markNotificationsAsReadRequest: (state) => {
state.notificationStatus = RequestStatus.LOADING; state.notificationStatus = RequestStatus.IN_PROGRESS;
}, },
markNotificationsAsReadSuccess: (state, { payload }) => { markNotificationsAsReadSuccess: (state, { payload }) => {
const date = new Date().toISOString(); const date = new Date().toISOString();
state.notifications[payload.id] = { ...state.notifications[payload.id], lastRead: date }; state.notifications[payload.id] = { ...state.notifications[payload.id], lastRead: date };
state.notificationStatus = RequestStatus.LOADED; state.notificationStatus = RequestStatus.SUCCESSFUL;
}, },
markNotificationsAsReadDenied: (state) => { markNotificationsAsReadDenied: (state) => {
state.notificationStatus = RequestStatus.DENIED; state.notificationStatus = RequestStatus.DENIED;

View File

@@ -23,7 +23,7 @@ import {
markNotificationsAsReadFailure, markNotificationsAsReadFailure,
} from './slice'; } from './slice';
import { import {
getNotifications, getNotificationCounts, markNotificationSeen, markAllNotificationRead, markNotificationRead, getNotificationsList, getNotificationCounts, markNotificationSeen, markAllNotificationRead, markNotificationRead,
} from './api'; } from './api';
import { getHttpErrorStatus } from '../utils'; import { getHttpErrorStatus } from '../utils';
@@ -35,21 +35,26 @@ const normalizeNotificationCounts = ({ countByAppName, count, showNotificationsT
}; };
}; };
const normalizeNotifications = ({ notifications }) => { const normalizeNotifications = (data) => {
const newNotificationIds = notifications.map(notification => notification.id.toString()); const newNotificationIds = data.results.map(notification => notification.id.toString());
const notificationsKeyValuePair = notifications.reduce((acc, obj) => { acc[obj.id] = obj; return acc; }, {}); const notificationsKeyValuePair = data.results.reduce((acc, obj) => { acc[obj.id] = obj; return acc; }, {});
const pagination = {
numPages: data.numPages,
currentPage: data.currentPage,
hasMorePages: !!data.next,
};
return { return {
newNotificationIds, notificationsKeyValuePair, newNotificationIds, notificationsKeyValuePair, pagination,
}; };
}; };
export const fetchNotificationList = ({ appName, page, pageSize }) => ( export const fetchNotificationList = ({ appName, page }) => (
async (dispatch) => { async (dispatch) => {
try { try {
dispatch(fetchNotificationRequest({ appName })); dispatch(fetchNotificationRequest({ appName }));
const data = await getNotifications(appName, page, pageSize); const data = await getNotificationsList(appName, page);
const normalisedData = normalizeNotifications((camelCaseObject(data))); const normalisedData = normalizeNotifications((camelCaseObject(data)));
dispatch(fetchNotificationSuccess({ ...normalisedData, numPages: data.numPages, currentPage: data.currentPage })); dispatch(fetchNotificationSuccess({ ...normalisedData }));
} catch (error) { } catch (error) {
if (getHttpErrorStatus(error) === 403) { if (getHttpErrorStatus(error) === 403) {
dispatch(fetchNotificationDenied(appName)); dispatch(fetchNotificationDenied(appName));

View File

@@ -73,7 +73,7 @@ describe('Notification row item test cases.', () => {
); );
it('Successfully marked notification as read.', async () => { it('Successfully marked notification as read.', async () => {
axiosMock.onPut(markedNotificationAsReadApiUrl).reply(200, { message: 'Notification marked read.' }); axiosMock.onPatch(markedNotificationAsReadApiUrl).reply(200, { message: 'Notification marked read.' });
renderComponent(); renderComponent();
const bellIcon = screen.queryByTestId('notification-bell-icon'); const bellIcon = screen.queryByTestId('notification-bell-icon');

View File

@@ -14,9 +14,10 @@ import { AppContext, AppProvider } from '@edx/frontend-platform/react';
import AuthenticatedUserDropdown from '../learning-header/AuthenticatedUserDropdown'; import AuthenticatedUserDropdown from '../learning-header/AuthenticatedUserDropdown';
import { initializeStore } from '../store'; import { initializeStore } from '../store';
import { markNotificationAsReadApiUrl } from './data/api'; import { markNotificationAsReadApiUrl, markNotificationsSeenApiUrl, getNotificationsListApiUrl } from './data/api';
import mockNotificationsResponse from './test-utils'; import mockNotificationsResponse from './test-utils';
import { markNotificationsAsSeen, fetchNotificationList } from './data/thunks';
import executeThunk from '../test-utils';
import './data/__factories__'; import './data/__factories__';
const markedAllNotificationsAsReadApiUrl = markNotificationAsReadApiUrl(); const markedAllNotificationsAsReadApiUrl = markNotificationAsReadApiUrl();
@@ -69,7 +70,7 @@ describe('Notification sections test cases.', () => {
}); });
it('Successfully marked all notifications as read, removing the unread status.', async () => { it('Successfully marked all notifications as read, removing the unread status.', async () => {
axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(200, { message: 'Notifications marked read.' }); axiosMock.onPatch(markedAllNotificationsAsReadApiUrl).reply(200, { message: 'Notifications marked read.' });
renderComponent(); renderComponent();
const bellIcon = screen.queryByTestId('notification-bell-icon'); const bellIcon = screen.queryByTestId('notification-bell-icon');
@@ -83,16 +84,23 @@ describe('Notification sections test cases.', () => {
}); });
it('Successfully load more notifications by clicking on load more notification button.', async () => { it('Successfully load more notifications by clicking on load more notification button.', async () => {
axiosMock.onPut(markNotificationsSeenApiUrl('discussion')).reply(200);
await executeThunk(markNotificationsAsSeen('discussions'), store.dispatch, store.getState);
renderComponent(); renderComponent();
const bellIcon = screen.queryByTestId('notification-bell-icon'); const bellIcon = screen.queryByTestId('notification-bell-icon');
await act(async () => { fireEvent.click(bellIcon); }); await act(async () => { fireEvent.click(bellIcon); });
expect(screen.queryAllByTestId('notification-contents')).toHaveLength(10);
const loadMoreButton = screen.queryByTestId('load-more-notifications'); const loadMoreButton = screen.queryByTestId('load-more-notifications');
expect(screen.queryAllByTestId('notification-contents')).toHaveLength(10); axiosMock.onGet(getNotificationsListApiUrl()).reply(
await act(async () => { fireEvent.click(loadMoreButton); }); 200,
(Factory.build('notificationsList', { num_pages: 2, current_page: 2 })),
);
await executeThunk(fetchNotificationList({ appName: 'discussion', page: 2 }), store.dispatch, store.getState);
expect(screen.queryAllByTestId('notification-contents')).toHaveLength(16); await act(async () => { fireEvent.click(loadMoreButton); });
expect(screen.queryAllByTestId('notification-contents')).toHaveLength(12);
}); });
}); });

View File

@@ -59,7 +59,7 @@ describe('Notification Tabs test cases.', () => {
const selectedTab = tabs.find(tab => tab.getAttribute('aria-selected') === 'true'); const selectedTab = tabs.find(tab => tab.getAttribute('aria-selected') === 'true');
expect(tabs.length).toEqual(5); expect(tabs.length).toEqual(5);
expect(within(selectedTab).queryByText('discussions')).toBeInTheDocument(); expect(within(selectedTab).queryByText('discussion')).toBeInTheDocument();
expect(within(selectedTab).queryByRole('status')).not.toBeInTheDocument(); expect(within(selectedTab).queryByRole('status')).not.toBeInTheDocument();
}); });

View File

@@ -5,27 +5,28 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { initializeStore } from '../store'; import { initializeStore } from '../store';
import executeThunk from '../test-utils'; import executeThunk from '../test-utils';
import { getNotificationsApiUrl, getNotificationsCountApiUrl } from './data/api'; import { getNotificationsListApiUrl, getNotificationsCountApiUrl } from './data/api';
import { fetchAppsNotificationCount, fetchNotificationList } from './data/thunks'; import { fetchAppsNotificationCount, fetchNotificationList } from './data/thunks';
import './data/__factories__'; import './data/__factories__';
const notificationCountsApiUrl = getNotificationsCountApiUrl(); const notificationCountsApiUrl = getNotificationsCountApiUrl();
const notificationsApiUrl = getNotificationsApiUrl(); const notificationsApiUrl = getNotificationsListApiUrl();
export default async function mockNotificationsResponse() { export default async function mockNotificationsResponse() {
const store = initializeStore(); const store = initializeStore();
const axiosMock = new MockAdapter(getAuthenticatedHttpClient()); const axiosMock = new MockAdapter(getAuthenticatedHttpClient());
const notifications = (Factory.buildList('notification', 8, null, { createdDate: new Date().toISOString() }).concat(
Factory.buildList('notification', 2, null, { createdDate: '2023-06-01T00:46:11.979531Z' }),
));
axiosMock.onGet(notificationCountsApiUrl).reply(200, (Factory.build('notificationsCount'))); axiosMock.onGet(notificationCountsApiUrl).reply(200, (Factory.build('notificationsCount')));
axiosMock.onGet(notificationsApiUrl).reply( axiosMock.onGet(notificationsApiUrl).reply(200, (Factory.build('notificationsList', {
200, results: notifications,
(Factory.buildList('notification', 8, null, { createdDate: new Date().toISOString() }).concat( num_pages: 2,
Factory.buildList('notification', 8, null, { createdDate: '2023-06-01T00:46:11.979531Z' }), next: `${notificationsApiUrl}?app_name=discussion&page=2`,
)), })));
);
await executeThunk(fetchAppsNotificationCount(), store.dispatch, store.getState); await executeThunk(fetchAppsNotificationCount(), store.dispatch, store.getState);
await executeThunk(fetchNotificationList({ page: 1, pageSize: 10 }), store.dispatch, store.getState); await executeThunk(fetchNotificationList({ appName: 'discussion', page: 1 }), store.dispatch, store.getState);
return { store, axiosMock }; return { store, axiosMock };
} }

View File

@@ -30,10 +30,10 @@
"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.title": "Notificaciones",
"notification.today.heading": "Last 24 hours", "notification.today.heading": "Últimas 24 horas",
"notification.earlier.heading": "Earlier", "notification.earlier.heading": "Más temprano",
"notification.mark.as.read": "Mark all as read", "notification.mark.as.read": "Marcar todo como leído",
"notification.fullStop": "•", "notification.fullStop": "•",
"notification.load.more.notifications": "Load more notifications" "notification.load.more.notifications": "Cargar más notificaciones"
} }

View File

@@ -123,7 +123,7 @@ $white: #fff;
} }
.content { .content {
b { strong {
color: #00262B !important; color: #00262B !important;
font-weight: 500 !important; font-weight: 500 !important;
} }

View File

@@ -24,7 +24,7 @@ const AuthenticatedUserDropdown = ({ intl, username }) => {
dispatch(fetchAppsNotificationCount()); dispatch(fetchAppsNotificationCount());
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // 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`}>