Compare commits
21 Commits
aansari/si
...
Ayesha/INF
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed3addc021 | ||
|
|
b467298d9a | ||
|
|
b5d036a54d | ||
|
|
bc997108ef | ||
|
|
6ae5130c14 | ||
|
|
67d79cb3aa | ||
|
|
f31a0e71f3 | ||
|
|
9761787c89 | ||
|
|
e5a21f4a75 | ||
|
|
1d89e9556a | ||
|
|
b35632df64 | ||
|
|
b36c0266fd | ||
|
|
0d5df18ab2 | ||
|
|
c61435546d | ||
|
|
df4a3c2a73 | ||
|
|
ac635edcb8 | ||
|
|
c4f7115732 | ||
|
|
5cc8ba43fe | ||
|
|
68505821bb | ||
|
|
c6d953fe7b | ||
|
|
a479f5ae5b |
1143
package-lock.json
generated
1143
package-lock.json
generated
@@ -10,34 +10,34 @@
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||
"@edx/frontend-component-footer": "12.5.1",
|
||||
"@edx/frontend-component-header": "4.9.1",
|
||||
"@edx/frontend-platform": "4.6.3",
|
||||
"@edx/paragon": "20.44.0",
|
||||
"@reduxjs/toolkit": "1.8.0",
|
||||
"@edx/frontend-component-footer": "12.6.1",
|
||||
"@edx/frontend-component-header": "4.10.1",
|
||||
"@edx/frontend-platform": "5.6.1",
|
||||
"@edx/paragon": "20.46.3",
|
||||
"@reduxjs/toolkit": "1.9.7",
|
||||
"@tinymce/tinymce-react": "3.13.1",
|
||||
"babel-polyfill": "6.26.0",
|
||||
"classnames": "2.3.2",
|
||||
"core-js": "3.21.1",
|
||||
"dompurify": "^2.4.3",
|
||||
"formik": "2.2.9",
|
||||
"formik": "2.4.5",
|
||||
"lodash.snakecase": "4.1.1",
|
||||
"prop-types": "15.8.1",
|
||||
"raw-loader": "4.0.2",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-redux": "7.2.9",
|
||||
"react-router": "5.2.1",
|
||||
"react-router-dom": "5.3.0",
|
||||
"redux": "4.1.2",
|
||||
"regenerator-runtime": "0.13.9",
|
||||
"react-router": "6.18.0",
|
||||
"react-router-dom": "6.18.0",
|
||||
"redux": "4.2.1",
|
||||
"regenerator-runtime": "0.14.1",
|
||||
"timeago.js": "4.0.2",
|
||||
"tinymce": "5.10.7",
|
||||
"yup": "0.31.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/browserslist-config": "1.2.0",
|
||||
"@edx/frontend-build": "13.0.5",
|
||||
"@edx/frontend-build": "13.0.14",
|
||||
"@edx/reactifex": "1.1.0",
|
||||
"@testing-library/jest-dom": "5.17.0",
|
||||
"@testing-library/react": "12.1.5",
|
||||
@@ -48,7 +48,7 @@
|
||||
"glob": "7.2.0",
|
||||
"husky": "7.0.4",
|
||||
"jest": "27.5.1",
|
||||
"rosie": "2.1.0"
|
||||
"rosie": "2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@aashutoshrathi/word-wrap": {
|
||||
@@ -2057,9 +2057,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-build": {
|
||||
"version": "13.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-build/-/frontend-build-13.0.5.tgz",
|
||||
"integrity": "sha512-cGCw4deCTjLTt2kVoMKOOo+8HS+CSpRjlZBEln1Qfu/868PEB0IWM1E3c7d0rIlkR9kkt7s7WFpYxcs1fk7Ryw==",
|
||||
"version": "13.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-build/-/frontend-build-13.0.14.tgz",
|
||||
"integrity": "sha512-AR/2GvIecX4LxJT4QIoeeBbnUVjjpRnT2P6gaqO8zEeoAS9ugYRQmqvCCeKJnt7vGmEEcincKfWJQu5nfUGfdA==",
|
||||
"dependencies": {
|
||||
"@babel/cli": "7.22.5",
|
||||
"@babel/core": "7.22.5",
|
||||
@@ -2095,26 +2095,26 @@
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"express": "4.18.2",
|
||||
"file-loader": "6.2.0",
|
||||
"html-webpack-plugin": "5.5.3",
|
||||
"html-webpack-plugin": "5.5.4",
|
||||
"identity-obj-proxy": "3.0.0",
|
||||
"image-minimizer-webpack-plugin": "3.8.3",
|
||||
"jest": "26.6.3",
|
||||
"mini-css-extract-plugin": "1.6.2",
|
||||
"postcss": "8.4.31",
|
||||
"postcss": "8.4.32",
|
||||
"postcss-custom-media": "10.0.2",
|
||||
"postcss-loader": "7.3.3",
|
||||
"postcss-rtlcss": "4.0.8",
|
||||
"postcss-rtlcss": "4.0.9",
|
||||
"react-dev-utils": "12.0.1",
|
||||
"react-refresh": "0.14.0",
|
||||
"resolve-url-loader": "5.0.0",
|
||||
"sass": "1.65.1",
|
||||
"sass": "1.69.5",
|
||||
"sass-loader": "13.3.2",
|
||||
"sharp": "0.32.6",
|
||||
"sharp": "0.33.0",
|
||||
"source-map-loader": "4.0.1",
|
||||
"style-loader": "3.3.3",
|
||||
"url-loader": "4.1.1",
|
||||
"webpack": "5.89.0",
|
||||
"webpack-bundle-analyzer": "4.9.1",
|
||||
"webpack-bundle-analyzer": "4.10.1",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-dev-server": "4.15.1",
|
||||
"webpack-merge": "5.9.0"
|
||||
@@ -3126,15 +3126,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-footer": {
|
||||
"version": "12.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-12.5.1.tgz",
|
||||
"integrity": "sha512-bLXfSDyyf8z+n4VXkraQ98qhkc+ZXuvRy65kXUE3s560oDv0qdiKU054W8uPY6wtsdu4WQ50C/Mluxzd60UKUg==",
|
||||
"version": "12.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-12.6.1.tgz",
|
||||
"integrity": "sha512-Wjqv1kJ3KaQTFmVVhU2mNuKRSnocNt2j7bDd8RxbZU/UfUM6LxOy1CGbkCAG0xtCnc/n5+1zHJJ/aDX0ak7viA==",
|
||||
"dependencies": {
|
||||
"@edx/paragon": "^21.3.1",
|
||||
"@fortawesome/fontawesome-svg-core": "6.4.2",
|
||||
"@fortawesome/free-brands-svg-icons": "6.4.2",
|
||||
"@fortawesome/free-regular-svg-icons": "6.4.2",
|
||||
"@fortawesome/free-solid-svg-icons": "6.4.2",
|
||||
"@fortawesome/fontawesome-svg-core": "6.5.1",
|
||||
"@fortawesome/free-brands-svg-icons": "6.5.1",
|
||||
"@fortawesome/free-regular-svg-icons": "6.5.1",
|
||||
"@fortawesome/free-solid-svg-icons": "6.5.1",
|
||||
"@fortawesome/react-fontawesome": "0.2.0",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
@@ -3200,6 +3200,63 @@
|
||||
"react": ">=16.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/fontawesome-common-types": {
|
||||
"version": "6.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz",
|
||||
"integrity": "sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A==",
|
||||
"hasInstallScript": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/fontawesome-svg-core": {
|
||||
"version": "6.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.1.tgz",
|
||||
"integrity": "sha512-MfRCYlQPXoLlpem+egxjfkEuP9UQswTrlCOsknus/NcMoblTH2g0jPrapbcIb04KGA7E2GZxbAccGZfWoYgsrQ==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.5.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/free-brands-svg-icons": {
|
||||
"version": "6.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.5.1.tgz",
|
||||
"integrity": "sha512-093l7DAkx0aEtBq66Sf19MgoZewv1zeY9/4C7vSKPO4qMwEsW/2VYTUTpBtLwfb9T2R73tXaRDPmE4UqLCYHfg==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.5.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/free-regular-svg-icons": {
|
||||
"version": "6.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.5.1.tgz",
|
||||
"integrity": "sha512-m6ShXn+wvqEU69wSP84coxLbNl7sGVZb+Ca+XZq6k30SzuP3X4TfPqtycgUh9ASwlNh5OfQCd8pDIWxl+O+LlQ==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.5.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/free-solid-svg-icons": {
|
||||
"version": "6.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.1.tgz",
|
||||
"integrity": "sha512-S1PPfU3mIJa59biTtXJz1oI0+KAXW6bkAb31XKhxdxtuXDiUIFsih4JR1v5BbxY7hVHsD1RKq+jRkVRaf773NQ==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.5.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/react-fontawesome": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.0.tgz",
|
||||
@@ -3326,9 +3383,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-header": {
|
||||
"version": "4.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-component-header/-/frontend-component-header-4.9.1.tgz",
|
||||
"integrity": "sha512-OcCWkdRNihZhT5WkZcKRjHCeD883TACIUi27921g5LG2/7+++9EUG74JGtVDYklk0JCuhr0M9tD87YM4BySlaA==",
|
||||
"version": "4.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-component-header/-/frontend-component-header-4.10.1.tgz",
|
||||
"integrity": "sha512-bcQ+ebdy/lM2TfLVB+WhdWNsuc51cFVgt5UQuJcV5nw6ACYUkBVTLbGDT7J8797bsQe6Lywg7Cj9ykB+Dy78Kw==",
|
||||
"dependencies": {
|
||||
"@edx/paragon": "21.5.6",
|
||||
"@fortawesome/fontawesome-svg-core": "6.4.2",
|
||||
@@ -3529,9 +3586,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-platform": {
|
||||
"version": "4.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-4.6.3.tgz",
|
||||
"integrity": "sha512-vvmg2rWfjdOD9BKcHiFlV3n4kVGqMGUYS0UrIk8Dx7BYbb7It03q/twe5b2D3PHQwvNCTei9EgX8+Tn1QhkXBA==",
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-5.6.1.tgz",
|
||||
"integrity": "sha512-7MOIjGGYplVY7yHrSea90EkQ24UxKxRKU9FaihB41yUSL/Vin1txDuIn3059Xr+60QfIKRsym+LogXe9IZ47Dw==",
|
||||
"dependencies": {
|
||||
"@cospired/i18n-iso-languages": "4.1.0",
|
||||
"@formatjs/intl-pluralrules": "4.3.3",
|
||||
@@ -3564,7 +3621,7 @@
|
||||
"react": "^16.9.0 || ^17.0.0",
|
||||
"react-dom": "^16.9.0 || ^17.0.0",
|
||||
"react-redux": "^7.1.1",
|
||||
"react-router-dom": "^5.0.1",
|
||||
"react-router-dom": "^6.0.0",
|
||||
"redux": "^4.0.4"
|
||||
}
|
||||
},
|
||||
@@ -3618,9 +3675,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/paragon": {
|
||||
"version": "20.44.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.44.0.tgz",
|
||||
"integrity": "sha512-C1uC3RaRmlFANtHebFdZzVDM08vgFJRnHE3u97ix07e0ACSQDbVNoZ2H7JgBy8nqHz2JWGHPnvtpvPf5DAZsZQ==",
|
||||
"version": "20.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.46.3.tgz",
|
||||
"integrity": "sha512-cHxoxoOREVFbBqW9IRAtlIAQo1lcF9JJXkLoEw1Vam6oetKSa5Mc0SL5kykbV+1iRPP7kS8A0Csf5nRr0oolLQ==",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
||||
"@fortawesome/react-fontawesome": "^0.1.18",
|
||||
@@ -3663,9 +3720,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/paragon/node_modules/glob": {
|
||||
"version": "8.0.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz",
|
||||
"integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==",
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
|
||||
"integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
@@ -3681,9 +3738,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/paragon/node_modules/minimatch": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz",
|
||||
"integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==",
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
||||
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
@@ -3692,9 +3749,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/paragon/node_modules/uuid": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
|
||||
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
||||
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
@@ -3803,6 +3864,21 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "0.44.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.44.0.tgz",
|
||||
"integrity": "sha512-ZX/etZEZw8DR7zAB1eVQT40lNo0jeqpb6dCgOvctB6FIQ5PoXfMuNY8+ayQfu8tNQbAB8gQWSSJupR8NxeiZXw==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime/node_modules/tslib": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
|
||||
@@ -4256,6 +4332,437 @@
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
|
||||
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA=="
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-arm64": {
|
||||
"version": "0.33.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.0.tgz",
|
||||
"integrity": "sha512-070tEheekI1LJWTGPC9WlQEa5UoKTXzzlORBHMX4TbfUxMiL336YHR8vBEUNsjse0RJCX8dZ4ZXwT595aEF1ug==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"glibc": ">=2.26",
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
|
||||
"npm": ">=9.6.5",
|
||||
"pnpm": ">=7.1.0",
|
||||
"yarn": ">=3.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-arm64": "1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-x64": {
|
||||
"version": "0.33.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.0.tgz",
|
||||
"integrity": "sha512-pu/nvn152F3qbPeUkr+4e9zVvEhD3jhwzF473veQfMPkOYo9aoWXSfdZH/E6F+nYC3qvFjbxbvdDbUtEbghLqw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"glibc": ">=2.26",
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
|
||||
"npm": ">=9.6.5",
|
||||
"pnpm": ">=7.1.0",
|
||||
"yarn": ">=3.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-x64": "1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.0.tgz",
|
||||
"integrity": "sha512-VzYd6OwnUR81sInf3alj1wiokY50DjsHz5bvfnsFpxs5tqQxESoHtJO6xyksDs3RIkyhMWq2FufXo6GNSU9BMw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"macos": ">=11",
|
||||
"npm": ">=9.6.5",
|
||||
"pnpm": ">=7.1.0",
|
||||
"yarn": ">=3.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.0.tgz",
|
||||
"integrity": "sha512-dD9OznTlHD6aovRswaPNEy8dKtSAmNo4++tO7uuR4o5VxbVAOoEQ1uSmN4iFAdQneTHws1lkTZeiXPrcCkh6IA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"macos": ">=10.13",
|
||||
"npm": ">=9.6.5",
|
||||
"pnpm": ">=7.1.0",
|
||||
"yarn": ">=3.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.0.tgz",
|
||||
"integrity": "sha512-VwgD2eEikDJUk09Mn9Dzi1OW2OJFRQK+XlBTkUNmAWPrtj8Ly0yq05DFgu1VCMx2/DqCGQVi5A1dM9hTmxf3uw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"glibc": ">=2.28",
|
||||
"npm": ">=9.6.5",
|
||||
"pnpm": ">=7.1.0",
|
||||
"yarn": ">=3.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.0.tgz",
|
||||
"integrity": "sha512-xTYThiqEZEZc0PRU90yVtM3KE7lw1bKdnDQ9kCTHWbqWyHOe4NpPOtMGy27YnN51q0J5dqRrvicfPbALIOeAZA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"glibc": ">=2.26",
|
||||
"npm": ">=9.6.5",
|
||||
"pnpm": ">=7.1.0",
|
||||
"yarn": ">=3.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-s390x": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.0.tgz",
|
||||
"integrity": "sha512-o9E46WWBC6JsBlwU4QyU9578G77HBDT1NInd+aERfxeOPbk0qBZHgoDsQmA2v9TbqJRWzoBPx1aLOhprBMgPjw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"glibc": ">=2.28",
|
||||
"npm": ">=9.6.5",
|
||||
"pnpm": ">=7.1.0",
|
||||
"yarn": ">=3.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.0.tgz",
|
||||
"integrity": "sha512-naldaJy4hSVhWBgEjfdBY85CAa4UO+W1nx6a1sWStHZ7EUfNiuBTTN2KUYT5dH1+p/xij1t2QSXfCiFJoC5S/Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"glibc": ">=2.26",
|
||||
"npm": ">=9.6.5",
|
||||
"pnpm": ">=7.1.0",
|
||||
"yarn": ">=3.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.0.tgz",
|
||||
"integrity": "sha512-OdorplCyvmSAPsoJLldtLh3nLxRrkAAAOHsGWGDYfN0kh730gifK+UZb3dWORRa6EusNqCTjfXV4GxvgJ/nPDQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"musl": ">=1.2.2",
|
||||
"npm": ">=9.6.5",
|
||||
"pnpm": ">=7.1.0",
|
||||
"yarn": ">=3.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.0.tgz",
|
||||
"integrity": "sha512-FW8iK6rJrg+X2jKD0Ajhjv6y74lToIBEvkZhl42nZt563FfxkCYacrXZtd+q/sRQDypQLzY5WdLkVTbJoPyqNg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"musl": ">=1.2.2",
|
||||
"npm": ">=9.6.5",
|
||||
"pnpm": ">=7.1.0",
|
||||
"yarn": ">=3.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm": {
|
||||
"version": "0.33.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.0.tgz",
|
||||
"integrity": "sha512-4horD3wMFd5a0ddbDY8/dXU9CaOgHjEHALAddXgafoR5oWq5s8X61PDgsSeh4Qupsdo6ycfPPSSNBrfVQnwwrg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"glibc": ">=2.28",
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
|
||||
"npm": ">=9.6.5",
|
||||
"pnpm": ">=7.1.0",
|
||||
"yarn": ">=3.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm": "1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm64": {
|
||||
"version": "0.33.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.0.tgz",
|
||||
"integrity": "sha512-dcomVSrtgF70SyOr8RCOCQ8XGVThXwe71A1d8MGA+mXEVRJ/J6/TrCbBEJh9ddcEIIsrnrkolaEvYSHqVhswQw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"glibc": ">=2.26",
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
|
||||
"npm": ">=9.6.5",
|
||||
"pnpm": ">=7.1.0",
|
||||
"yarn": ">=3.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm64": "1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-s390x": {
|
||||
"version": "0.33.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.0.tgz",
|
||||
"integrity": "sha512-TiVJbx38J2rNVfA309ffSOB+3/7wOsZYQEOlKqOUdWD/nqkjNGrX+YQGz7nzcf5oy2lC+d37+w183iNXRZNngQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"glibc": ">=2.28",
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
|
||||
"npm": ">=9.6.5",
|
||||
"pnpm": ">=7.1.0",
|
||||
"yarn": ">=3.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-s390x": "1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-x64": {
|
||||
"version": "0.33.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.0.tgz",
|
||||
"integrity": "sha512-PaZM4Zi7/Ek71WgTdvR+KzTZpBqrQOFcPe7/8ZoPRlTYYRe43k6TWsf4GVH6XKRLMYeSp8J89RfAhBrSP4itNA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"glibc": ">=2.26",
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
|
||||
"npm": ">=9.6.5",
|
||||
"pnpm": ">=7.1.0",
|
||||
"yarn": ">=3.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-x64": "1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||
"version": "0.33.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.0.tgz",
|
||||
"integrity": "sha512-1QLbbN0zt+32eVrg7bb1lwtvEaZwlhEsY1OrijroMkwAqlHqFj6R33Y47s2XUv7P6Ie1PwCxK/uFnNqMnkd5kg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"musl": ">=1.2.2",
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
|
||||
"npm": ">=9.6.5",
|
||||
"pnpm": ">=7.1.0",
|
||||
"yarn": ">=3.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||
"version": "0.33.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.0.tgz",
|
||||
"integrity": "sha512-CecqgB/CnkvCWFhmfN9ZhPGMLXaEBXl4o7WtA6U3Ztrlh/s7FUKX4vNxpMSYLIrWuuzjiaYdfU3+Tdqh1xaHfw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"musl": ">=1.2.2",
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
|
||||
"npm": ">=9.6.5",
|
||||
"pnpm": ">=7.1.0",
|
||||
"yarn": ">=3.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-wasm32": {
|
||||
"version": "0.33.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.0.tgz",
|
||||
"integrity": "sha512-Hn4js32gUX9qkISlemZBUPuMs0k/xNJebUNl/L6djnU07B/HAA2KaxRVb3HvbU5fL242hLOcp0+tR+M8dvJUFw==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/runtime": "^0.44.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
|
||||
"npm": ">=9.6.5",
|
||||
"pnpm": ">=7.1.0",
|
||||
"yarn": ">=3.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-ia32": {
|
||||
"version": "0.33.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.0.tgz",
|
||||
"integrity": "sha512-5HfcsCZi3l5nPRF2q3bllMVMDXBqEWI3Q8KQONfzl0TferFE5lnsIG0A1YrntMAGqvkzdW6y1Ci1A2uTvxhfzg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
|
||||
"npm": ">=9.6.5",
|
||||
"pnpm": ">=7.1.0",
|
||||
"yarn": ">=3.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-x64": {
|
||||
"version": "0.33.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.0.tgz",
|
||||
"integrity": "sha512-i3DtP/2ce1yKFj4OzOnOYltOEL/+dp4dc4dJXJBv6god1AFTcmkaA99H/7SwOmkCOBQkbVvA3lCGm3/5nDtf9Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
|
||||
"npm": ">=9.6.5",
|
||||
"pnpm": ">=7.1.0",
|
||||
"yarn": ">=3.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@istanbuljs/load-nyc-config": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
|
||||
@@ -6008,18 +6515,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@reduxjs/toolkit": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.8.0.tgz",
|
||||
"integrity": "sha512-cdfHWfcvLyhBUDicoFwG1u32JqvwKDxLxDd7zSmSoFw/RhYLOygIRtmaMjPRUUHmVmmAGAvquLLsKKU/677kSQ==",
|
||||
"version": "1.9.7",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.7.tgz",
|
||||
"integrity": "sha512-t7v8ZPxhhKgOKtU+uyJT13lu4vL7az5aFi4IdoDs/eS548edn2M8Ik9h8fxgvMjGoAUVFSt6ZC1P5cWmQ014QQ==",
|
||||
"dependencies": {
|
||||
"immer": "^9.0.7",
|
||||
"redux": "^4.1.2",
|
||||
"redux-thunk": "^2.4.1",
|
||||
"reselect": "^4.1.5"
|
||||
"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.0.0-beta",
|
||||
"react-redux": "^7.2.1 || ^8.0.0-beta"
|
||||
"react": "^16.9.0 || ^17.0.0 || ^18",
|
||||
"react-redux": "^7.2.1 || ^8.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
@@ -6030,6 +6537,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@remix-run/router": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.11.0.tgz",
|
||||
"integrity": "sha512-BHdhcWgeiudl91HvVa2wxqZjSHbheSgIiDvxrF1VjFzBzpTtuDPkOdOi3Iqvc08kXtFkLjhbS+ML9aM8mJS+wQ==",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@restart/context": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@restart/context/-/context-2.1.4.tgz",
|
||||
@@ -7784,11 +8299,6 @@
|
||||
"deep-equal": "^2.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/b4a": {
|
||||
"version": "1.6.4",
|
||||
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz",
|
||||
"integrity": "sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw=="
|
||||
},
|
||||
"node_modules/babel-jest": {
|
||||
"version": "26.6.3",
|
||||
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-26.6.3.tgz",
|
||||
@@ -8744,11 +9254,6 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
|
||||
},
|
||||
"node_modules/chrome-trace-event": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz",
|
||||
@@ -9587,6 +10092,11 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/debounce": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
|
||||
"integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug=="
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
@@ -9624,20 +10134,6 @@
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/decompress-response": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||
"dependencies": {
|
||||
"mimic-response": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/dedent": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
|
||||
@@ -9677,14 +10173,6 @@
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
|
||||
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="
|
||||
},
|
||||
"node_modules/deep-extend": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/deep-is": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||
@@ -11308,14 +11796,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||
},
|
||||
"node_modules/expand-template": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
||||
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/expect": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/expect/-/expect-27.5.1.tgz",
|
||||
@@ -11600,11 +12080,6 @@
|
||||
"resolved": "https://registry.npmjs.org/fast-defer/-/fast-defer-1.1.7.tgz",
|
||||
"integrity": "sha512-tJ01ulDWT2WhqxMKS20nXX6wyX2iInBYpbN3GO7yjKwXMY4qvkdBRxak9IFwBLlFDESox+SwSvqMCZDfe1tqeg=="
|
||||
},
|
||||
"node_modules/fast-fifo": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
|
||||
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.2.12",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
|
||||
@@ -12192,9 +12667,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/formik": {
|
||||
"version": "2.2.9",
|
||||
"resolved": "https://registry.npmjs.org/formik/-/formik-2.2.9.tgz",
|
||||
"integrity": "sha512-LQLcISMmf1r5at4/gyJigGn0gOwFbeEAlji+N9InZF6LIMXnFNkO42sCI8Jt84YZggpD4cPWObAZaxpEFtSzNA==",
|
||||
"version": "2.4.5",
|
||||
"resolved": "https://registry.npmjs.org/formik/-/formik-2.4.5.tgz",
|
||||
"integrity": "sha512-Gxlht0TD3vVdzMDHwkiNZqJ7Mvg77xQNfmBRrNtvzcHZs72TJppSTDKHpImCMJZwcWPBJ8jSQQ95GJzXFf1nAQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -12202,13 +12677,14 @@
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@types/hoist-non-react-statics": "^3.3.1",
|
||||
"deepmerge": "^2.1.1",
|
||||
"hoist-non-react-statics": "^3.3.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"react-fast-compare": "^2.0.1",
|
||||
"tiny-warning": "^1.0.2",
|
||||
"tslib": "^1.10.0"
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
@@ -12222,6 +12698,11 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/formik/node_modules/tslib": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@@ -12261,11 +12742,6 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-constants": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
|
||||
@@ -12418,11 +12894,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/github-from-package": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
|
||||
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
|
||||
@@ -12831,9 +13302,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/html-webpack-plugin": {
|
||||
"version": "5.5.3",
|
||||
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.3.tgz",
|
||||
"integrity": "sha512-6YrDKTuqaP/TquFH7h4srYWsZx+x6k6+FbsTm0ziCwGHDP78Unr1r9F/H4+sGmMbX08GQcJ+K64x55b+7VM/jg==",
|
||||
"version": "5.5.4",
|
||||
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.4.tgz",
|
||||
"integrity": "sha512-3wNSaVVxdxcu0jd4FpQFoICdqgxs4zIQQvj+2yQKFfBOnLETQ6X5CDWdeasuGlSsooFlMkEioWDTqBv1wvw5Iw==",
|
||||
"dependencies": {
|
||||
"@types/html-minifier-terser": "^6.0.0",
|
||||
"html-minifier-terser": "^6.0.2",
|
||||
@@ -17426,9 +17897,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jquery": {
|
||||
"version": "3.6.1",
|
||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.1.tgz",
|
||||
"integrity": "sha512-opJeO4nCucVnsjiXOE+/PcCgYw9Gwpvs/a6B1LL/lQhwWwpbVEVYDZ1FokFr8PRc7ghYlrFPuyHuiiDNTQxmcw==",
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
|
||||
"integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
@@ -17727,21 +18198,6 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
||||
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="
|
||||
},
|
||||
"node_modules/lodash.escape": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz",
|
||||
"integrity": "sha512-nXEOnb/jK9g0DYMr1/Xvq6l5xMD7GDG55+GSYIYmS0G4tBk/hURD4JR9WCavs04t33WmJx9kCyp9vJ+mr4BOUw=="
|
||||
},
|
||||
"node_modules/lodash.flatten": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
|
||||
"integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g=="
|
||||
},
|
||||
"node_modules/lodash.invokemap": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.invokemap/-/lodash.invokemap-4.6.0.tgz",
|
||||
"integrity": "sha512-CfkycNtMqgUlfjfdh2BhKO/ZXrP8ePOX5lEU/g0R3ItJcnuxWDwokMGKx1hWcfOikmyOVx6X9IwWnDGlgKl61w=="
|
||||
},
|
||||
"node_modules/lodash.memoize": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
||||
@@ -17752,11 +18208,6 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
|
||||
},
|
||||
"node_modules/lodash.pullall": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.pullall/-/lodash.pullall-4.2.0.tgz",
|
||||
"integrity": "sha512-VhqxBKH0ZxPpLhiu68YD1KnHmbhQJQctcipvmFnqIBDYzcIHzf3Zpu0tpeOKtR4x76p9yohc506eGdOjTmyIBg=="
|
||||
},
|
||||
"node_modules/lodash.snakecase": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
|
||||
@@ -18061,17 +18512,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/mimic-response": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/min-indent": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
||||
@@ -18081,19 +18521,6 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/mini-create-react-context": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz",
|
||||
"integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.1",
|
||||
"tiny-warning": "^1.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"prop-types": "^15.0.0",
|
||||
"react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mini-css-extract-plugin": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.6.2.tgz",
|
||||
@@ -18150,11 +18577,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp-classic": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
|
||||
},
|
||||
"node_modules/mrmime": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz",
|
||||
@@ -18186,9 +18608,9 @@
|
||||
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
|
||||
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
|
||||
"version": "3.3.7",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -18223,11 +18645,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/napi-build-utils": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
|
||||
"integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg=="
|
||||
},
|
||||
"node_modules/natural-compare": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||
@@ -18265,52 +18682,6 @@
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
|
||||
},
|
||||
"node_modules/node-abi": {
|
||||
"version": "3.47.0",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.47.0.tgz",
|
||||
"integrity": "sha512-2s6B2CWZM//kPgwnuI0KrYwNjfdByE25zvAaEpq9IH4zcNsarH8Ihu/UuX6XMPEogDAxkuUFeZn60pXNHAqn3A==",
|
||||
"dependencies": {
|
||||
"semver": "^7.3.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/node-abi/node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/node-abi/node_modules/semver": {
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
||||
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/node-abi/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
|
||||
"integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA=="
|
||||
},
|
||||
"node_modules/node-forge": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
|
||||
@@ -19040,14 +19411,6 @@
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
|
||||
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
|
||||
"dependencies": {
|
||||
"isarray": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/path-type": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
||||
@@ -19253,9 +19616,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.31",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
|
||||
"version": "8.4.32",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz",
|
||||
"integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -19271,7 +19634,7 @@
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.6",
|
||||
"nanoid": "^3.3.7",
|
||||
"picocolors": "^1.0.0",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
@@ -19819,14 +20182,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-rtlcss": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/postcss-rtlcss/-/postcss-rtlcss-4.0.8.tgz",
|
||||
"integrity": "sha512-CR2sY889PHnX6K8rjW9FG4Qvm9UJsIekDakMtEYGH3zgFp9XADMeaKcA0hPOmkClNh0jWbkaPBm0jZ6fHmqkJQ==",
|
||||
"version": "4.0.9",
|
||||
"resolved": "https://registry.npmjs.org/postcss-rtlcss/-/postcss-rtlcss-4.0.9.tgz",
|
||||
"integrity": "sha512-dCNKEf+FgTv+EA3XI8ysg2RnpS5s3/iZmU+9qpCNFxHU/BhK+4hz7jyCsCAfo0CLnDrMPtaQENhwb+EGm1wh7Q==",
|
||||
"dependencies": {
|
||||
"rtlcss": "4.1.0"
|
||||
"rtlcss": "4.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4.21"
|
||||
@@ -19878,70 +20241,6 @@
|
||||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
|
||||
},
|
||||
"node_modules/prebuild-install": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz",
|
||||
"integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.0",
|
||||
"expand-template": "^2.0.3",
|
||||
"github-from-package": "0.0.0",
|
||||
"minimist": "^1.2.3",
|
||||
"mkdirp-classic": "^0.5.3",
|
||||
"napi-build-utils": "^1.0.1",
|
||||
"node-abi": "^3.3.0",
|
||||
"pump": "^3.0.0",
|
||||
"rc": "^1.2.7",
|
||||
"simple-get": "^4.0.0",
|
||||
"tar-fs": "^2.0.0",
|
||||
"tunnel-agent": "^0.6.0"
|
||||
},
|
||||
"bin": {
|
||||
"prebuild-install": "bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/prebuild-install/node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/prebuild-install/node_modules/tar-fs": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
|
||||
"integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
|
||||
"dependencies": {
|
||||
"chownr": "^1.1.1",
|
||||
"mkdirp-classic": "^0.5.2",
|
||||
"pump": "^3.0.0",
|
||||
"tar-stream": "^2.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/prebuild-install/node_modules/tar-stream": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||
"dependencies": {
|
||||
"bl": "^4.0.3",
|
||||
"end-of-stream": "^1.4.1",
|
||||
"fs-constants": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
|
||||
@@ -20191,11 +20490,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/queue-tick": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
|
||||
"integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag=="
|
||||
},
|
||||
"node_modules/randombytes": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||
@@ -20245,28 +20539,6 @@
|
||||
"webpack": "^4.0.0 || ^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rc": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
||||
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
||||
"dependencies": {
|
||||
"deep-extend": "^0.6.0",
|
||||
"ini": "~1.3.0",
|
||||
"minimist": "^1.2.0",
|
||||
"strip-json-comments": "~2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"rc": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/rc/node_modules/strip-json-comments": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
|
||||
@@ -20782,40 +21054,33 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.1.tgz",
|
||||
"integrity": "sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==",
|
||||
"version": "6.18.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.18.0.tgz",
|
||||
"integrity": "sha512-vk2y7Dsy8wI02eRRaRmOs9g2o+aE72YCx5q9VasT1N9v+lrdB79tIqrjMfByHiY5+6aYkH2rUa5X839nwWGPDg==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.13",
|
||||
"history": "^4.9.0",
|
||||
"hoist-non-react-statics": "^3.1.0",
|
||||
"loose-envify": "^1.3.1",
|
||||
"mini-create-react-context": "^0.4.0",
|
||||
"path-to-regexp": "^1.7.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"react-is": "^16.6.0",
|
||||
"tiny-invariant": "^1.0.2",
|
||||
"tiny-warning": "^1.0.0"
|
||||
"@remix-run/router": "1.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=15"
|
||||
"react": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.0.tgz",
|
||||
"integrity": "sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ==",
|
||||
"version": "6.18.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.18.0.tgz",
|
||||
"integrity": "sha512-Ubrue4+Ercc/BoDkFQfc6og5zRQ4A8YxSO3Knsne+eRbZ+IepAsK249XBH/XaFuOYOYr3L3r13CXTLvYt5JDjw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.13",
|
||||
"history": "^4.9.0",
|
||||
"loose-envify": "^1.3.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"react-router": "5.2.1",
|
||||
"tiny-invariant": "^1.0.2",
|
||||
"tiny-warning": "^1.0.0"
|
||||
"@remix-run/router": "1.11.0",
|
||||
"react-router": "6.18.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=15"
|
||||
"react": ">=16.8",
|
||||
"react-dom": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/react-style-singleton": {
|
||||
@@ -21032,9 +21297,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/redux": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-4.1.2.tgz",
|
||||
"integrity": "sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw==",
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
|
||||
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.9.2"
|
||||
}
|
||||
@@ -21064,9 +21329,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.13.9",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
|
||||
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
|
||||
},
|
||||
"node_modules/regenerator-transform": {
|
||||
"version": "0.15.2",
|
||||
@@ -21343,9 +21608,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rosie": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/rosie/-/rosie-2.1.0.tgz",
|
||||
"integrity": "sha512-Dbzdc+prLXZuB/suRptDnBUY29SdGvND3bLg6cll8n7PNqzuyCxSlRfrkn8PqjS9n4QVsiM7RCvxCkKAkTQRjA==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/rosie/-/rosie-2.1.1.tgz",
|
||||
"integrity": "sha512-2AXB7WrIZXtKMZ6Q/PlozqPF5nu/x7NEvRJZOblrJuprrPfm5gL8JVvJPj9aaib9F8IUALnLUFhzXrwEtnI5cQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@@ -21360,9 +21625,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rtlcss": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.1.0.tgz",
|
||||
"integrity": "sha512-W+N4hh0nVqVrrn3mRkHakxpB+c9cQ4CRT67O39kgA+1DjyhrdsqyCqIuHXyvWaXn4/835n+oX3fYJCi4+G/06A==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.1.1.tgz",
|
||||
"integrity": "sha512-/oVHgBtnPNcggP2aVXQjSy6N1mMAfHg4GSag0QtZBlD5bdDgAHwr4pydqJGd+SUCu9260+Pjqbjwtvu7EMH1KQ==",
|
||||
"dependencies": {
|
||||
"escalade": "^3.1.1",
|
||||
"picocolors": "^1.0.0",
|
||||
@@ -21620,9 +21885,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.65.1",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.65.1.tgz",
|
||||
"integrity": "sha512-9DINwtHmA41SEd36eVPQ9BJKpn7eKDQmUHmpI0y5Zv2Rcorrh0zS+cFrt050hdNbmmCNKTW3hV5mWfuegNRsEA==",
|
||||
"version": "1.69.5",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.69.5.tgz",
|
||||
"integrity": "sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==",
|
||||
"dependencies": {
|
||||
"chokidar": ">=3.0.0 <4.0.0",
|
||||
"immutable": "^4.0.0",
|
||||
@@ -21925,25 +22190,42 @@
|
||||
"integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA=="
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.32.6",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz",
|
||||
"integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==",
|
||||
"version": "0.33.0",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.0.tgz",
|
||||
"integrity": "sha512-99DZKudjm/Rmz+M0/26t4DKpXyywAOJaayGS9boEn7FvgtG0RYBi46uPE2c+obcJRtA3AZa0QwJot63gJQ1F0Q==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"color": "^4.2.3",
|
||||
"detect-libc": "^2.0.2",
|
||||
"node-addon-api": "^6.1.0",
|
||||
"prebuild-install": "^7.1.1",
|
||||
"semver": "^7.5.4",
|
||||
"simple-get": "^4.0.1",
|
||||
"tar-fs": "^3.0.4",
|
||||
"tunnel-agent": "^0.6.0"
|
||||
"semver": "^7.5.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.15.0"
|
||||
"libvips": ">=8.15.0",
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-darwin-arm64": "0.33.0",
|
||||
"@img/sharp-darwin-x64": "0.33.0",
|
||||
"@img/sharp-libvips-darwin-arm64": "1.0.0",
|
||||
"@img/sharp-libvips-darwin-x64": "1.0.0",
|
||||
"@img/sharp-libvips-linux-arm": "1.0.0",
|
||||
"@img/sharp-libvips-linux-arm64": "1.0.0",
|
||||
"@img/sharp-libvips-linux-s390x": "1.0.0",
|
||||
"@img/sharp-libvips-linux-x64": "1.0.0",
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.0.0",
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.0.0",
|
||||
"@img/sharp-linux-arm": "0.33.0",
|
||||
"@img/sharp-linux-arm64": "0.33.0",
|
||||
"@img/sharp-linux-s390x": "0.33.0",
|
||||
"@img/sharp-linux-x64": "0.33.0",
|
||||
"@img/sharp-linuxmusl-arm64": "0.33.0",
|
||||
"@img/sharp-linuxmusl-x64": "0.33.0",
|
||||
"@img/sharp-wasm32": "0.33.0",
|
||||
"@img/sharp-win32-ia32": "0.33.0",
|
||||
"@img/sharp-win32-x64": "0.33.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sharp/node_modules/lru-cache": {
|
||||
@@ -22027,49 +22309,6 @@
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
|
||||
},
|
||||
"node_modules/simple-concat": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
|
||||
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/simple-get": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
|
||||
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"decompress-response": "^6.0.0",
|
||||
"once": "^1.3.1",
|
||||
"simple-concat": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-swizzle": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
|
||||
@@ -22640,15 +22879,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/streamx": {
|
||||
"version": "2.15.1",
|
||||
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.1.tgz",
|
||||
"integrity": "sha512-fQMzy2O/Q47rgwErk/eGeLu/roaFWV0jVsogDmrszM9uIw8L5OA+t+V93MgYlufNptfjmYR1tOMWhei/Eh7TQA==",
|
||||
"dependencies": {
|
||||
"fast-fifo": "^1.1.0",
|
||||
"queue-tick": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
@@ -23039,26 +23269,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz",
|
||||
"integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==",
|
||||
"dependencies": {
|
||||
"mkdirp-classic": "^0.5.2",
|
||||
"pump": "^3.0.0",
|
||||
"tar-stream": "^3.1.5"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-stream": {
|
||||
"version": "3.1.6",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.6.tgz",
|
||||
"integrity": "sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==",
|
||||
"dependencies": {
|
||||
"b4a": "^1.6.4",
|
||||
"fast-fifo": "^1.2.0",
|
||||
"streamx": "^2.15.0"
|
||||
}
|
||||
},
|
||||
"node_modules/terminal-link": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz",
|
||||
@@ -23347,17 +23557,6 @@
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
},
|
||||
"node_modules/tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
|
||||
@@ -23901,23 +24100,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-bundle-analyzer": {
|
||||
"version": "4.9.1",
|
||||
"resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.9.1.tgz",
|
||||
"integrity": "sha512-jnd6EoYrf9yMxCyYDPj8eutJvtjQNp8PHmni/e/ulydHBWhT5J3menXt3HEkScsu9YqMAcG4CfFjs3rj5pVU1w==",
|
||||
"version": "4.10.1",
|
||||
"resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.1.tgz",
|
||||
"integrity": "sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ==",
|
||||
"dependencies": {
|
||||
"@discoveryjs/json-ext": "0.5.7",
|
||||
"acorn": "^8.0.4",
|
||||
"acorn-walk": "^8.0.0",
|
||||
"commander": "^7.2.0",
|
||||
"debounce": "^1.2.1",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"gzip-size": "^6.0.0",
|
||||
"html-escaper": "^2.0.2",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.escape": "^4.0.1",
|
||||
"lodash.flatten": "^4.4.0",
|
||||
"lodash.invokemap": "^4.6.0",
|
||||
"lodash.pullall": "^4.2.0",
|
||||
"lodash.uniqby": "^4.7.0",
|
||||
"opener": "^1.5.2",
|
||||
"picocolors": "^1.0.0",
|
||||
"sirv": "^2.0.3",
|
||||
|
||||
24
package.json
24
package.json
@@ -34,34 +34,34 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||
"@edx/frontend-component-footer": "12.5.1",
|
||||
"@edx/frontend-component-header": "4.9.1",
|
||||
"@edx/frontend-platform": "4.6.3",
|
||||
"@edx/paragon": "20.44.0",
|
||||
"@reduxjs/toolkit": "1.8.0",
|
||||
"@edx/frontend-component-footer": "12.6.1",
|
||||
"@edx/frontend-component-header": "4.10.1",
|
||||
"@edx/frontend-platform": "5.6.1",
|
||||
"@edx/paragon": "20.46.3",
|
||||
"@reduxjs/toolkit": "1.9.7",
|
||||
"@tinymce/tinymce-react": "3.13.1",
|
||||
"babel-polyfill": "6.26.0",
|
||||
"classnames": "2.3.2",
|
||||
"core-js": "3.21.1",
|
||||
"dompurify": "^2.4.3",
|
||||
"formik": "2.2.9",
|
||||
"formik": "2.4.5",
|
||||
"lodash.snakecase": "4.1.1",
|
||||
"prop-types": "15.8.1",
|
||||
"raw-loader": "4.0.2",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-redux": "7.2.9",
|
||||
"react-router": "5.2.1",
|
||||
"react-router-dom": "5.3.0",
|
||||
"redux": "4.1.2",
|
||||
"regenerator-runtime": "0.13.9",
|
||||
"react-router": "6.18.0",
|
||||
"react-router-dom": "6.18.0",
|
||||
"redux": "4.2.1",
|
||||
"regenerator-runtime": "0.14.1",
|
||||
"timeago.js": "4.0.2",
|
||||
"tinymce": "5.10.7",
|
||||
"yup": "0.31.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/browserslist-config": "1.2.0",
|
||||
"@edx/frontend-build": "13.0.5",
|
||||
"@edx/frontend-build": "13.0.14",
|
||||
"@edx/reactifex": "1.1.0",
|
||||
"@testing-library/jest-dom": "5.17.0",
|
||||
"@testing-library/react": "12.1.5",
|
||||
@@ -72,6 +72,6 @@
|
||||
"glob": "7.2.0",
|
||||
"husky": "7.0.4",
|
||||
"jest": "27.5.1",
|
||||
"rosie": "2.1.0"
|
||||
"rosie": "2.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
} from '../data/constants';
|
||||
import { selectCourseCohorts } from '../discussions/cohorts/data/selectors';
|
||||
import messages from '../discussions/posts/post-filter-bar/messages';
|
||||
import ActionItem from '../discussions/posts/post-filter-bar/PostFilterBar';
|
||||
import { ActionItem } from '../discussions/posts/post-filter-bar/PostFilterBar';
|
||||
|
||||
const FilterBar = ({
|
||||
intl,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { memo, useEffect } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import classNames from 'classnames';
|
||||
@@ -6,30 +6,25 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import withConditionalInContextRendering from '../../discussions/common/withConditionalInContextRendering';
|
||||
import { useCourseId } from '../../discussions/data/hooks';
|
||||
import { fetchTab } from './data/thunks';
|
||||
import Tabs from './tabs/Tabs';
|
||||
import messages from './messages';
|
||||
|
||||
import './navBar.scss';
|
||||
|
||||
const CourseTabsNavigation = ({ activeTab, className, rootSlug }) => {
|
||||
const CourseTabsNavigation = ({
|
||||
activeTab, className, courseId, rootSlug,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const intl = useIntl();
|
||||
const courseId = useCourseId();
|
||||
const tabs = useSelector(state => state.courseTabs.tabs);
|
||||
|
||||
useEffect(() => {
|
||||
if (courseId) {
|
||||
dispatch(fetchTab(courseId, rootSlug));
|
||||
}
|
||||
dispatch(fetchTab(courseId, rootSlug));
|
||||
}, [courseId]);
|
||||
|
||||
console.log('CourseTabsNavigation');
|
||||
|
||||
return (
|
||||
<div id="courseTabsNavigation" tabIndex="-1" className={classNames('course-tabs-navigation px-4', className)}>
|
||||
<div id="courseTabsNavigation" className={classNames('course-tabs-navigation px-4', className)}>
|
||||
{!!tabs.length && (
|
||||
<Tabs
|
||||
className="nav-underline-tabs"
|
||||
@@ -54,12 +49,13 @@ CourseTabsNavigation.propTypes = {
|
||||
activeTab: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
rootSlug: PropTypes.string,
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
CourseTabsNavigation.defaultProps = {
|
||||
activeTab: 'discussion',
|
||||
activeTab: undefined,
|
||||
className: null,
|
||||
rootSlug: 'outline',
|
||||
};
|
||||
|
||||
export default memo(withConditionalInContextRendering(CourseTabsNavigation, false));
|
||||
export default React.memo(CourseTabsNavigation);
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
const selectCourseTabs = state => state.courseTabs;
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
|
||||
export default selectCourseTabs;
|
||||
export const selectCourseTabs = state => state.courseTabs;
|
||||
|
||||
2
src/components/NavigationBar/index.js
Normal file
2
src/components/NavigationBar/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
export { default as CourseTabsNavigation } from './CourseTabsNavigation';
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, {
|
||||
useCallback, useEffect, useMemo, useRef, useState,
|
||||
useCallback, useContext, useEffect, useRef, useState,
|
||||
} from 'react';
|
||||
|
||||
import camelCase from 'lodash/camelCase';
|
||||
@@ -9,7 +9,7 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Icon, SearchField } from '@edx/paragon';
|
||||
import { Search as SearchIcon } from '@edx/paragon/icons';
|
||||
|
||||
import { useCurrentPage } from '../discussions/data/hooks';
|
||||
import { DiscussionContext } from '../discussions/common/context';
|
||||
import { setUsernameSearch } from '../discussions/learners/data';
|
||||
import { setSearchQuery } from '../discussions/posts/data';
|
||||
import postsMessages from '../discussions/posts/post-actions-bar/messages';
|
||||
@@ -18,7 +18,7 @@ import { setFilter as setTopicFilter } from '../discussions/topics/data/slices';
|
||||
const Search = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const page = useCurrentPage();
|
||||
const { page } = useContext(DiscussionContext);
|
||||
const postSearch = useSelector(({ threads }) => threads.filters.search);
|
||||
const topicSearch = useSelector(({ topics }) => topics.filter);
|
||||
const learnerSearch = useSelector(({ learners }) => learners.usernameSearch);
|
||||
@@ -26,15 +26,15 @@ const Search = () => {
|
||||
const isTopicSearch = 'topics'.includes(page);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const previousSearchValueRef = useRef('');
|
||||
let currentValue = '';
|
||||
|
||||
const currentValue = useMemo(() => {
|
||||
if (isPostSearch) {
|
||||
return postSearch;
|
||||
} if (isTopicSearch) {
|
||||
return topicSearch;
|
||||
}
|
||||
return learnerSearch;
|
||||
}, [postSearch, topicSearch, learnerSearch]);
|
||||
if (isPostSearch) {
|
||||
currentValue = postSearch;
|
||||
} else if (isTopicSearch) {
|
||||
currentValue = topicSearch;
|
||||
} else {
|
||||
currentValue = learnerSearch;
|
||||
}
|
||||
|
||||
const onClear = useCallback(() => {
|
||||
dispatch(setSearchQuery(''));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { Editor } from '@tinymce/tinymce-react';
|
||||
import { useLocation, useParams } from 'react-router';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
// TinyMCE so the global var exists
|
||||
// eslint-disable-next-line no-unused-vars,import/no-extraneous-dependencies
|
||||
import tinymce from 'tinymce/tinymce';
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
export const getApiBaseUrl = () => getConfig().LMS_BASE_URL;
|
||||
export const getFullUrl = (path) => (
|
||||
new URL(`${getConfig().PUBLIC_PATH.replace(/\/$/, '')}/${path}`, window.location.origin).href
|
||||
);
|
||||
|
||||
/**
|
||||
* Enum for thread types.
|
||||
@@ -137,25 +140,24 @@ export const DiscussionProvider = {
|
||||
OPEN_EDX: 'openedx',
|
||||
};
|
||||
|
||||
export const BASE_PATH = `${getConfig().PUBLIC_PATH}:courseId`;
|
||||
const BASE_PATH = '/:courseId';
|
||||
|
||||
export const Routes = {
|
||||
DISCUSSIONS: {
|
||||
PATH: BASE_PATH,
|
||||
},
|
||||
LEARNERS: {
|
||||
PATH: `${BASE_PATH}/learners`,
|
||||
POSTS: `${BASE_PATH}/learners/:learnerUsername/posts(/:postId)?`,
|
||||
PATH: `${BASE_PATH}/learners/:learnerUsername?`,
|
||||
POSTS: `${BASE_PATH}/learners/:learnerUsername/posts/:postId?`,
|
||||
POSTS_EDIT: `${BASE_PATH}/learners/:learnerUsername/posts/:postId/edit`,
|
||||
},
|
||||
POSTS: {
|
||||
PATH: `${BASE_PATH}/topics/:topicId`,
|
||||
MY_POSTS: `${BASE_PATH}/my-posts(/:postId)?`,
|
||||
ALL_POSTS: `${BASE_PATH}/posts(/:postId)?`,
|
||||
NEW_POST: [
|
||||
`${BASE_PATH}/topics/:topicId/posts/:postId`,
|
||||
`${BASE_PATH}/topics/:topicId`,
|
||||
`${BASE_PATH}`,
|
||||
],
|
||||
MY_POSTS: `${BASE_PATH}/my-posts/:postId?`,
|
||||
ALL_POSTS: `${BASE_PATH}/posts/:postId?`,
|
||||
EDIT_MY_POSTS: `${BASE_PATH}/my-posts/:postId/edit`,
|
||||
EDIT_ALL_POSTS: `${BASE_PATH}/posts/:postId/edit`,
|
||||
NEW_POST: `${BASE_PATH}/*`,
|
||||
EDIT_POST: [
|
||||
`${BASE_PATH}/category/:category/posts/:postId/edit`,
|
||||
`${BASE_PATH}/topics/:topicId/posts/:postId/edit`,
|
||||
@@ -166,19 +168,19 @@ export const Routes = {
|
||||
},
|
||||
COMMENTS: {
|
||||
PATH: [
|
||||
`${BASE_PATH}/category/:category/posts/:postId`,
|
||||
`${BASE_PATH}/topics/:topicId/posts/:postId`,
|
||||
`${BASE_PATH}/category/:category/posts/:postId?`,
|
||||
`${BASE_PATH}/topics/:topicId/posts/:postId?`,
|
||||
`${BASE_PATH}/posts/:postId`,
|
||||
`${BASE_PATH}/my-posts/:postId`,
|
||||
`${BASE_PATH}/learners/:learnerUsername/posts/:postId`,
|
||||
`${BASE_PATH}/learners/:learnerUsername/posts/:postId?`,
|
||||
],
|
||||
PAGE: `${BASE_PATH}/:page`,
|
||||
PAGE: `${BASE_PATH}/:page/*`,
|
||||
PAGES: {
|
||||
category: `${BASE_PATH}/category/:category/posts/:postId`,
|
||||
topics: `${BASE_PATH}/topics/:topicId/posts/:postId`,
|
||||
category: `${BASE_PATH}/category/:category/posts/:postId?`,
|
||||
topics: `${BASE_PATH}/topics/:topicId/posts/:postId?`,
|
||||
posts: `${BASE_PATH}/posts/:postId`,
|
||||
'my-posts': `${BASE_PATH}/my-posts/:postId`,
|
||||
learners: `${BASE_PATH}/learners/:learnerUsername/posts/:postId`,
|
||||
learners: `${BASE_PATH}/learners/:learnerUsername/posts/:postId?`,
|
||||
},
|
||||
},
|
||||
TOPICS: {
|
||||
@@ -189,9 +191,10 @@ export const Routes = {
|
||||
],
|
||||
ALL: `${BASE_PATH}/topics`,
|
||||
CATEGORY: `${BASE_PATH}/category/:category`,
|
||||
CATEGORY_POST: `${BASE_PATH}/category/:category/posts/:postId`,
|
||||
CATEGORY_POST: `${BASE_PATH}/category/:category/posts/:postId?`,
|
||||
CATEGORY_POST_EDIT: `${BASE_PATH}/category/:category/posts/:postId/edit`,
|
||||
TOPIC: `${BASE_PATH}/topics/:topicId`,
|
||||
TOPIC_POST: `${BASE_PATH}/topics/:topicId/posts/:postId`,
|
||||
TOPIC_POST: `${BASE_PATH}/topics/:topicId/posts/:postId?`,
|
||||
TOPIC_POST_EDIT: `${BASE_PATH}/topics/:topicId/posts/:postId/edit`,
|
||||
},
|
||||
};
|
||||
@@ -205,11 +208,12 @@ export const PostsPages = {
|
||||
};
|
||||
|
||||
export const ALL_ROUTES = []
|
||||
.concat([Routes.TOPICS.CATEGORY_POST, Routes.TOPICS.CATEGORY])
|
||||
.concat([Routes.TOPICS.CATEGORY_POST, `${Routes.TOPICS.CATEGORY}?`])
|
||||
.concat(Routes.COMMENTS.PATH)
|
||||
.concat(Routes.TOPICS.PATH)
|
||||
.concat(Routes.POSTS.EDIT_POST)
|
||||
.concat([Routes.POSTS.ALL_POSTS, Routes.POSTS.MY_POSTS])
|
||||
.concat([Routes.LEARNERS.POSTS, Routes.LEARNERS.PATH])
|
||||
.concat([Routes.DISCUSSIONS.PATH]);
|
||||
.concat([`${Routes.DISCUSSIONS.PATH}/*`]);
|
||||
|
||||
export const MAX_UPLOAD_FILE_SIZE = 1024;
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Report } from '@edx/paragon/icons';
|
||||
|
||||
import { AvatarOutlineAndLabelColors } from '../../data/constants';
|
||||
import {
|
||||
selectModerationSettings, selectUserHasModerationPrivileges, selectUserIsGroupTa, selectUserIsStaff,
|
||||
selectUserHasModerationPrivileges, selectUserIsGroupTa, selectUserIsStaff,
|
||||
} from '../data/selectors';
|
||||
import messages from '../post-comments/messages';
|
||||
import AlertBar from './AlertBar';
|
||||
@@ -29,7 +29,6 @@ const AlertBanner = ({
|
||||
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
|
||||
const userIsGroupTa = useSelector(selectUserIsGroupTa);
|
||||
const userIsGlobalStaff = useSelector(selectUserIsStaff);
|
||||
const { reasonCodesEnabled } = useSelector(selectModerationSettings);
|
||||
const userIsContentAuthor = getAuthenticatedUser().username === author;
|
||||
const canSeeReportedBanner = abuseFlagged;
|
||||
const canSeeLastEditOrClosedAlert = (userHasModerationPrivileges || userIsGroupTa
|
||||
@@ -45,7 +44,7 @@ const AlertBanner = ({
|
||||
{intl.formatMessage(messages.abuseFlaggedMessage)}
|
||||
</Alert>
|
||||
)}
|
||||
{reasonCodesEnabled && canSeeLastEditOrClosedAlert && (
|
||||
{ canSeeLastEditOrClosedAlert && (
|
||||
<>
|
||||
{lastEdit?.reason && (
|
||||
<AlertBar
|
||||
|
||||
@@ -90,7 +90,6 @@ describe.each([
|
||||
store = initializeStore({
|
||||
config: {
|
||||
hasModerationPrivileges: true,
|
||||
reasonCodesEnabled: true,
|
||||
},
|
||||
});
|
||||
const content = buildTestContent(type, props);
|
||||
|
||||
@@ -2,8 +2,7 @@ import React, { useContext, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { generatePath } from 'react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { generatePath, Link } from 'react-router-dom';
|
||||
import * as timeago from 'timeago.js';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
@@ -11,7 +10,6 @@ import { Icon, OverlayTrigger, Tooltip } from '@edx/paragon';
|
||||
import { Institution, School } from '@edx/paragon/icons';
|
||||
|
||||
import { Routes } from '../../data/constants';
|
||||
import { useShowLearnersTab } from '../data/hooks';
|
||||
import messages from '../messages';
|
||||
import { DiscussionContext } from './context';
|
||||
import timeLocale from './time-locale';
|
||||
@@ -46,12 +44,11 @@ const AuthorLabel = ({
|
||||
const showTextPrimary = !authorLabelMessage && !isRetiredUser && !alert;
|
||||
const className = classNames('d-flex align-items-center', { 'mb-0.5': !postOrComment }, labelColor);
|
||||
|
||||
const showUserNameAsLink = useShowLearnersTab()
|
||||
&& linkToProfile && author && author !== intl.formatMessage(messages.anonymous);
|
||||
const showUserNameAsLink = linkToProfile && author && author !== intl.formatMessage(messages.anonymous);
|
||||
|
||||
const authorName = useMemo(() => (
|
||||
<span
|
||||
className={classNames('mr-1.5 font-size-14 font-style font-weight-500', {
|
||||
className={classNames('mr-1.5 font-size-14 font-style font-weight-500 author-name', {
|
||||
'text-gray-700': isRetiredUser,
|
||||
'text-primary-500': !authorLabelMessage && !isRetiredUser,
|
||||
})}
|
||||
|
||||
@@ -53,7 +53,6 @@ describe('Author label', () => {
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`).reply(200, {
|
||||
learners_tab_enabled: true,
|
||||
has_moderation_privileges: true,
|
||||
});
|
||||
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/settings`).reply(200, {});
|
||||
|
||||
@@ -84,7 +84,6 @@ describe.each([
|
||||
store = initializeStore({
|
||||
config: {
|
||||
hasModerationPrivileges: true,
|
||||
reasonCodesEnabled: true,
|
||||
},
|
||||
});
|
||||
const content = buildTestContent(type, props);
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
} from '@testing-library/react';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { MemoryRouter, Route } from 'react-router';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { Factory } from 'rosie';
|
||||
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
@@ -60,15 +60,12 @@ async function mockAxiosReturnPagedCommentsResponses() {
|
||||
function renderComponent(postId) {
|
||||
const wrapper = render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<AppProvider store={store} wrapWithRouter={false}>
|
||||
<DiscussionContext.Provider
|
||||
value={{ courseId, postId }}
|
||||
value={{ courseId, postId, page: 'posts' }}
|
||||
>
|
||||
<MemoryRouter initialEntries={[`/${courseId}/posts/${postId}`]}>
|
||||
<DiscussionContent />
|
||||
<Route
|
||||
path="*"
|
||||
/>
|
||||
</MemoryRouter>
|
||||
</DiscussionContext.Provider>
|
||||
</AppProvider>
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { useEnableInContextSidebar } from '../data/hooks';
|
||||
|
||||
const withConditionalInContextRendering = (WrappedComponent, condition) => (
|
||||
function SidebarConditionalRenderer(props) {
|
||||
const enableInContextSidebar = useEnableInContextSidebar();
|
||||
|
||||
return enableInContextSidebar === condition && <WrappedComponent {...props} />;
|
||||
}
|
||||
);
|
||||
|
||||
export default withConditionalInContextRendering;
|
||||
@@ -4,16 +4,16 @@ import {
|
||||
} from 'react';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useHistory, useLocation, useRouteMatch } from 'react-router';
|
||||
import {
|
||||
matchPath, useLocation, useMatch, useNavigate,
|
||||
} from 'react-router-dom';
|
||||
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { breakpoints, useWindowSize } from '@edx/paragon';
|
||||
|
||||
import {
|
||||
ALL_ROUTES, BASE_PATH, RequestStatus, Routes,
|
||||
} from '../../data/constants';
|
||||
import { RequestStatus, Routes } from '../../data/constants';
|
||||
import { selectTopicsUnderCategory } from '../../data/selectors';
|
||||
import { fetchCourseBlocks } from '../../data/thunks';
|
||||
import { DiscussionContext } from '../common/context';
|
||||
@@ -22,7 +22,7 @@ import { threadsLoadingStatus } from '../posts/data/selectors';
|
||||
import { selectTopics } from '../topics/data/selectors';
|
||||
import tourCheckpoints from '../tours/constants';
|
||||
import { selectTours } from '../tours/data/selectors';
|
||||
import { fetchDiscussionTours, updateTourShowStatus } from '../tours/data/thunks';
|
||||
import { updateTourShowStatus } from '../tours/data/thunks';
|
||||
import messages from '../tours/messages';
|
||||
import { discussionsPath } from '../utils';
|
||||
import {
|
||||
@@ -31,8 +31,6 @@ import {
|
||||
selectIsCourseAdmin,
|
||||
selectIsCourseStaff,
|
||||
selectIsPostingEnabled,
|
||||
selectLearnersTabEnabled,
|
||||
selectModerationSettings,
|
||||
selectPostThreadCount,
|
||||
selectUserHasModerationPrivileges,
|
||||
selectUserIsGroupTa,
|
||||
@@ -55,22 +53,62 @@ export function useTotalTopicThreadCount() {
|
||||
}
|
||||
|
||||
export const useSidebarVisible = () => {
|
||||
const location = useLocation();
|
||||
const enableInContext = useSelector(selectEnableInContext);
|
||||
const isViewingTopics = useRouteMatch(Routes.TOPICS.ALL);
|
||||
const isViewingLearners = useRouteMatch(Routes.LEARNERS.PATH);
|
||||
const isViewingTopics = useMatch(Routes.TOPICS.ALL);
|
||||
const isViewingLearners = useMatch(`${Routes.LEARNERS.PATH}/*`);
|
||||
const isFiltered = useSelector(selectAreThreadsFiltered);
|
||||
const totalThreads = useSelector(selectPostThreadCount);
|
||||
const isThreadsEmpty = Boolean(useSelector(threadsLoadingStatus()) === RequestStatus.SUCCESSFUL && !totalThreads);
|
||||
const isIncontextTopicsView = Boolean(useRouteMatch(Routes.TOPICS.PATH) && enableInContext);
|
||||
const hideSidebar = Boolean(isThreadsEmpty && !isFiltered && !(isViewingTopics?.isExact || isViewingLearners));
|
||||
const matchInContextTopicView = Routes.TOPICS.PATH.find((route) => matchPath({ path: `${route}/*` }, location.pathname));
|
||||
const isInContextTopicsView = Boolean(matchInContextTopicView && enableInContext);
|
||||
const hideSidebar = Boolean(isThreadsEmpty && !isFiltered && !(isViewingTopics || isViewingLearners));
|
||||
|
||||
if (isIncontextTopicsView) {
|
||||
if (isInContextTopicsView) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !hideSidebar;
|
||||
};
|
||||
|
||||
export function useCourseDiscussionData(courseId) {
|
||||
const dispatch = useDispatch();
|
||||
const { authenticatedUser } = useContext(AppContext);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchBaseData() {
|
||||
await dispatch(fetchCourseConfig(courseId));
|
||||
await dispatch(fetchCourseBlocks(courseId, authenticatedUser.username));
|
||||
}
|
||||
|
||||
fetchBaseData();
|
||||
}, [courseId]);
|
||||
}
|
||||
|
||||
export function useRedirectToThread(courseId, enableInContextSidebar) {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const redirectToThread = useSelector(
|
||||
(state) => state.threads.redirectToThread,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// After posting a new thread we'd like to redirect users to it, the topic and post id are temporarily
|
||||
// stored in redirectToThread
|
||||
if (redirectToThread) {
|
||||
dispatch(clearRedirect());
|
||||
const newLocation = discussionsPath(Routes.COMMENTS.PAGES[enableInContextSidebar ? 'topics' : 'my-posts'], {
|
||||
courseId,
|
||||
postId: redirectToThread.threadId,
|
||||
topicId: redirectToThread.topicId,
|
||||
})(location);
|
||||
navigate({ ...newLocation });
|
||||
}
|
||||
}, [redirectToThread]);
|
||||
}
|
||||
|
||||
export function useIsOnDesktop() {
|
||||
const windowSize = useWindowSize();
|
||||
return windowSize.width >= breakpoints.medium.minWidth;
|
||||
@@ -123,18 +161,15 @@ export const useAlertBannerVisible = (
|
||||
) => {
|
||||
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
|
||||
const userIsGroupTa = useSelector(selectUserIsGroupTa);
|
||||
const { reasonCodesEnabled } = useSelector(selectModerationSettings);
|
||||
const userIsContentAuthor = getAuthenticatedUser().username === author;
|
||||
const canSeeLastEditOrClosedAlert = (userHasModerationPrivileges || userIsContentAuthor || userIsGroupTa);
|
||||
const canSeeReportedBanner = abuseFlagged;
|
||||
|
||||
return (
|
||||
(reasonCodesEnabled && canSeeLastEditOrClosedAlert && (lastEdit?.reason || closed)) || (canSeeReportedBanner)
|
||||
(canSeeLastEditOrClosedAlert && (lastEdit?.reason || closed)) || (canSeeReportedBanner)
|
||||
);
|
||||
};
|
||||
|
||||
export const useShowLearnersTab = () => useSelector(selectLearnersTabEnabled);
|
||||
|
||||
/**
|
||||
* React hook that gets the current topic ID from the current topic or category.
|
||||
* The topicId in the DiscussionContext only return the direct topicId from the URL.
|
||||
@@ -224,89 +259,3 @@ export const useDebounce = (value, delay) => {
|
||||
);
|
||||
return debouncedValue;
|
||||
};
|
||||
|
||||
export const useEnableInContextSidebar = () => {
|
||||
const location = useLocation();
|
||||
|
||||
return Boolean(new URLSearchParams(location.search).get('inContextSidebar') !== null);
|
||||
};
|
||||
|
||||
export const useCourseId = () => {
|
||||
const { params: { courseId } } = useRouteMatch(BASE_PATH);
|
||||
|
||||
return courseId;
|
||||
};
|
||||
|
||||
export const useCurrentPage = () => {
|
||||
const { params: { page } } = useRouteMatch(`${Routes.COMMENTS.PAGE}?`);
|
||||
|
||||
return page;
|
||||
};
|
||||
|
||||
export const usePostId = () => {
|
||||
const { params: { postId } } = useRouteMatch(ALL_ROUTES);
|
||||
|
||||
return postId;
|
||||
};
|
||||
|
||||
export const useLearnerUsername = () => {
|
||||
const { params: { learnerUsername } } = useRouteMatch(ALL_ROUTES);
|
||||
|
||||
return learnerUsername;
|
||||
};
|
||||
|
||||
export const useTopicId = () => {
|
||||
const { params: { topicId } } = useRouteMatch(ALL_ROUTES);
|
||||
|
||||
return topicId;
|
||||
};
|
||||
|
||||
export const useCategory = () => {
|
||||
const { params: { category } } = useRouteMatch(ALL_ROUTES);
|
||||
|
||||
return category;
|
||||
};
|
||||
|
||||
export function useRedirectToThread() {
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const courseId = useCourseId();
|
||||
const enableInContextSidebar = useEnableInContextSidebar();
|
||||
|
||||
const redirectToThread = useSelector(
|
||||
(state) => state.threads.redirectToThread,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// After posting a new thread we'd like to redirect users to it, the topic and post id are temporarily
|
||||
// stored in redirectToThread
|
||||
if (redirectToThread) {
|
||||
dispatch(clearRedirect());
|
||||
const newLocation = discussionsPath(Routes.COMMENTS.PAGES[enableInContextSidebar ? 'topics' : 'my-posts'], {
|
||||
courseId,
|
||||
postId: redirectToThread.threadId,
|
||||
topicId: redirectToThread.topicId,
|
||||
})(location);
|
||||
history.push(newLocation);
|
||||
}
|
||||
}, [redirectToThread]);
|
||||
}
|
||||
|
||||
export function useCourseDiscussionData() {
|
||||
const dispatch = useDispatch();
|
||||
const courseId = useCourseId();
|
||||
const { authenticatedUser } = useContext(AppContext);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchBaseData() {
|
||||
await Promise.all([
|
||||
dispatch(fetchCourseConfig(courseId)),
|
||||
dispatch(fetchCourseBlocks(courseId, authenticatedUser.username)),
|
||||
dispatch(fetchDiscussionTours()),
|
||||
]);
|
||||
}
|
||||
|
||||
fetchBaseData();
|
||||
}, [courseId]);
|
||||
}
|
||||
|
||||
@@ -14,8 +14,6 @@ export const selectUserIsGroupTa = state => state.config.isGroupTa;
|
||||
|
||||
export const selectConfigLoadingStatus = state => state.config.status;
|
||||
|
||||
export const selectLearnersTabEnabled = state => state.config.learnersTabEnabled;
|
||||
|
||||
export const selectUserRoles = state => state.config.userRoles;
|
||||
|
||||
export const selectDivisionSettings = state => state.config.settings;
|
||||
@@ -33,7 +31,6 @@ export const selectIsPostingEnabled = state => state.config.isPostingEnabled;
|
||||
export const selectModerationSettings = state => ({
|
||||
postCloseReasons: state.config.postCloseReasons,
|
||||
editReasons: state.config.editReasons,
|
||||
reasonCodesEnabled: state.config.reasonCodesEnabled,
|
||||
});
|
||||
|
||||
export const selectDiscussionProvider = state => state.config.provider;
|
||||
|
||||
@@ -16,7 +16,6 @@ const configSlice = createSlice({
|
||||
isCourseAdmin: false,
|
||||
isCourseStaff: false,
|
||||
isUserAdmin: false,
|
||||
learnersTabEnabled: false,
|
||||
isPostingEnabled: false,
|
||||
settings: {
|
||||
divisionScheme: 'none',
|
||||
@@ -24,7 +23,6 @@ const configSlice = createSlice({
|
||||
dividedInlineDiscussions: [],
|
||||
dividedCourseWideDiscussions: [],
|
||||
},
|
||||
reasonCodesEnabled: false,
|
||||
editReasons: [],
|
||||
postCloseReasons: [],
|
||||
enableInContext: false,
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { LearningHeader } from '@edx/frontend-component-header';
|
||||
|
||||
import selectCourseTabs from '../../components/NavigationBar/data/selectors';
|
||||
import withConditionalInContextRendering from '../common/withConditionalInContextRendering';
|
||||
|
||||
const CourseHeader = () => {
|
||||
const { courseNumber, courseTitle, org } = useSelector(selectCourseTabs);
|
||||
|
||||
console.log('CourseHeader', courseNumber, courseTitle, org);
|
||||
|
||||
return (courseNumber || courseTitle || org) && (
|
||||
<LearningHeader courseOrg={org} courseNumber={courseNumber} courseTitle={courseTitle} />
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withConditionalInContextRendering(CourseHeader, false));
|
||||
@@ -1,24 +0,0 @@
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { useEnableInContextSidebar } from '../data/hooks';
|
||||
import NavigationBar from '../navigation/navigation-bar/NavigationBar';
|
||||
import PostActionsBar from '../posts/post-actions-bar/PostActionsBar';
|
||||
|
||||
const DiscussionActionBar = () => {
|
||||
const enableInContextSidebar = useEnableInContextSidebar();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('d-flex flex-row justify-content-between navbar fixed-top', {
|
||||
'pl-4 pr-3 py-0': enableInContextSidebar,
|
||||
})}
|
||||
>
|
||||
<NavigationBar />
|
||||
<PostActionsBar />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(DiscussionActionBar);
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { lazy, Suspense } from 'react';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Route, Switch } from 'react-router';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
|
||||
import Spinner from '../../components/Spinner';
|
||||
import { Routes } from '../../data/constants';
|
||||
import { Routes as ROUTES } from '../../data/constants';
|
||||
|
||||
const PostEditor = lazy(() => import('../posts/post-editor/PostEditor'));
|
||||
const PostCommentsView = lazy(() => import('../post-comments/PostCommentsView'));
|
||||
@@ -16,20 +16,20 @@ const DiscussionContent = () => {
|
||||
<div className="d-flex bg-light-400 flex-column w-75 w-xs-100 w-xl-75 align-items-center">
|
||||
<div className="d-flex flex-column w-100">
|
||||
<Suspense fallback={(<Spinner />)}>
|
||||
{postEditorVisible ? (
|
||||
<Route path={Routes.POSTS.NEW_POST}>
|
||||
<PostEditor />
|
||||
</Route>
|
||||
) : (
|
||||
<Switch>
|
||||
<Route path={Routes.POSTS.EDIT_POST}>
|
||||
<PostEditor editExisting />
|
||||
</Route>
|
||||
<Route path={Routes.COMMENTS.PATH}>
|
||||
<PostCommentsView />
|
||||
</Route>
|
||||
</Switch>
|
||||
)}
|
||||
<Routes>
|
||||
{postEditorVisible ? (
|
||||
<Route path={ROUTES.POSTS.NEW_POST} element={<PostEditor />} />
|
||||
) : (
|
||||
<>
|
||||
{ROUTES.POSTS.EDIT_POST.map(route => (
|
||||
<Route key={route} path={route} element={<PostEditor editExisting />} />
|
||||
))}
|
||||
{ROUTES.COMMENTS.PATH.map(route => (
|
||||
<Route key={route} path={route} element={<PostCommentsView />} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import Footer from '@edx/frontend-component-footer';
|
||||
|
||||
import withConditionalInContextRendering from '../common/withConditionalInContextRendering';
|
||||
|
||||
const DiscussionFooter = () => <Footer />;
|
||||
|
||||
export default memo(withConditionalInContextRendering(DiscussionFooter, false));
|
||||
@@ -1,17 +0,0 @@
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import CourseTabsNavigation from '../../components/NavigationBar/CourseTabsNavigation';
|
||||
import CourseHeader from './CourseHeader';
|
||||
|
||||
const DiscussionHeader = () => {
|
||||
console.log('DiscussionHeader');
|
||||
|
||||
return (
|
||||
<>
|
||||
<CourseHeader />
|
||||
<CourseTabsNavigation />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(DiscussionHeader);
|
||||
@@ -1,53 +0,0 @@
|
||||
import React, { lazy, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { useEnableInContextSidebar } from '../data/hooks';
|
||||
import DiscussionsProductTour from '../tours/DiscussionsProductTour';
|
||||
import DiscussionActionBar from './DiscussionActionBar';
|
||||
import DiscussionFooter from './DiscussionFooter';
|
||||
import DiscussionHeader from './DiscussionHeader';
|
||||
import DiscussionSidebar from './DiscussionSidebar';
|
||||
import InfoPage from './InfoPage';
|
||||
import LayoutSwitcher from './LayoutSwitcher';
|
||||
import LegacyBreadcrumb from './LegacyBreadcrumb';
|
||||
|
||||
const DiscussionsRestrictionBanner = lazy(() => import('./DiscussionsRestrictionBanner'));
|
||||
|
||||
const DiscussionsLayout = ({ children }) => {
|
||||
const postActionBarRef = useRef(null);
|
||||
const enableInContextSidebar = useEnableInContextSidebar();
|
||||
|
||||
return (
|
||||
<>
|
||||
<DiscussionHeader />
|
||||
<main className="container-fluid d-flex flex-column p-0 w-100" id="main" tabIndex="-1">
|
||||
<div
|
||||
ref={postActionBarRef}
|
||||
className={classNames('header-action-bar', {
|
||||
'shadow-none border-light-300 border-bottom': enableInContextSidebar,
|
||||
})}
|
||||
>
|
||||
<DiscussionActionBar />
|
||||
<DiscussionsRestrictionBanner />
|
||||
</div>
|
||||
<LegacyBreadcrumb />
|
||||
<LayoutSwitcher
|
||||
sidebar={<DiscussionSidebar postActionBarRef={postActionBarRef} />}
|
||||
infoPage={<InfoPage />}
|
||||
>
|
||||
{children}
|
||||
</LayoutSwitcher>
|
||||
<DiscussionsProductTour />
|
||||
</main>
|
||||
<DiscussionFooter />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
DiscussionsLayout.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default React.memo(DiscussionsLayout);
|
||||
@@ -1,16 +1,21 @@
|
||||
import React, { lazy, Suspense } from 'react';
|
||||
import React, {
|
||||
lazy, Suspense, useContext, useEffect, useRef,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useSelector } from 'react-redux';
|
||||
import {
|
||||
Redirect, Route, Switch, useLocation,
|
||||
} from 'react-router';
|
||||
Navigate, Route, Routes,
|
||||
} from 'react-router-dom';
|
||||
|
||||
import { useWindowSize } from '@edx/paragon';
|
||||
|
||||
import Spinner from '../../components/Spinner';
|
||||
import { RequestStatus, Routes } from '../../data/constants';
|
||||
import { useEnableInContextSidebar, useShowLearnersTab } from '../data/hooks';
|
||||
import { RequestStatus, Routes as ROUTES } from '../../data/constants';
|
||||
import { DiscussionContext } from '../common/context';
|
||||
import { useContainerSize, useIsOnDesktop, useIsOnXLDesktop } from '../data/hooks';
|
||||
import { selectConfigLoadingStatus, selectEnableInContext } from '../data/selectors';
|
||||
import ResizableSidebar from './ResizableSidebar';
|
||||
|
||||
const TopicPostsView = lazy(() => import('../in-context-topics/TopicPostsView'));
|
||||
const InContextTopicsView = lazy(() => import('../in-context-topics/TopicsView'));
|
||||
@@ -19,71 +24,108 @@ const LearnersView = lazy(() => import('../learners/LearnersView'));
|
||||
const PostsView = lazy(() => import('../posts/PostsView'));
|
||||
const LegacyTopicsView = lazy(() => import('../topics/TopicsView'));
|
||||
|
||||
const DiscussionSidebar = ({ postActionBarRef }) => {
|
||||
const location = useLocation();
|
||||
const enableInContextSidebar = useEnableInContextSidebar();
|
||||
const DiscussionSidebar = ({ displaySidebar, postActionBarRef }) => {
|
||||
const isOnDesktop = useIsOnDesktop();
|
||||
const isOnXLDesktop = useIsOnXLDesktop();
|
||||
const { enableInContextSidebar } = useContext(DiscussionContext);
|
||||
const enableInContext = useSelector(selectEnableInContext);
|
||||
const configStatus = useSelector(selectConfigLoadingStatus);
|
||||
const redirectToLearnersTab = useShowLearnersTab();
|
||||
const sidebarRef = useRef(null);
|
||||
const postActionBarHeight = useContainerSize(postActionBarRef);
|
||||
const { height: windowHeight } = useWindowSize();
|
||||
|
||||
const memoizedRedirection = React.useMemo(() => (
|
||||
<Suspense fallback={(<Spinner />)}>
|
||||
<Switch>
|
||||
{enableInContext && !enableInContextSidebar && (
|
||||
<Route
|
||||
path={Routes.TOPICS.ALL}
|
||||
component={InContextTopicsView}
|
||||
exact
|
||||
/>
|
||||
)}
|
||||
{enableInContext && !enableInContextSidebar && (
|
||||
<Route
|
||||
path={[
|
||||
Routes.TOPICS.TOPIC,
|
||||
Routes.TOPICS.CATEGORY,
|
||||
Routes.TOPICS.TOPIC_POST,
|
||||
Routes.TOPICS.TOPIC_POST_EDIT,
|
||||
]}
|
||||
component={TopicPostsView}
|
||||
exact
|
||||
/>
|
||||
)}
|
||||
<Route
|
||||
path={[Routes.POSTS.ALL_POSTS, Routes.POSTS.MY_POSTS, Routes.POSTS.PATH, Routes.TOPICS.CATEGORY]}
|
||||
component={PostsView}
|
||||
/>
|
||||
<Route path={Routes.TOPICS.PATH} component={LegacyTopicsView} />
|
||||
{redirectToLearnersTab && (
|
||||
<Route path={Routes.LEARNERS.POSTS} component={LearnerPostsView} />
|
||||
)}
|
||||
{redirectToLearnersTab && (
|
||||
<Route path={Routes.LEARNERS.PATH} component={LearnersView} />
|
||||
)}
|
||||
{configStatus === RequestStatus.SUCCESSFUL && (
|
||||
<Redirect
|
||||
from={Routes.DISCUSSIONS.PATH}
|
||||
to={{
|
||||
...location,
|
||||
pathname: Routes.POSTS.ALL_POSTS,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Switch>
|
||||
</Suspense>
|
||||
), [enableInContext, enableInContextSidebar, configStatus, location, redirectToLearnersTab]);
|
||||
useEffect(() => {
|
||||
if (sidebarRef && postActionBarHeight && !enableInContextSidebar) {
|
||||
if (isOnDesktop) {
|
||||
sidebarRef.current.style.maxHeight = `${windowHeight - postActionBarHeight}px`;
|
||||
}
|
||||
sidebarRef.current.style.minHeight = `${windowHeight - postActionBarHeight}px`;
|
||||
sidebarRef.current.style.top = `${postActionBarHeight}px`;
|
||||
}
|
||||
}, [sidebarRef, postActionBarHeight, enableInContextSidebar]);
|
||||
|
||||
return (
|
||||
<ResizableSidebar postActionBarRef={postActionBarRef}>
|
||||
{memoizedRedirection}
|
||||
</ResizableSidebar>
|
||||
<div
|
||||
ref={sidebarRef}
|
||||
className={classNames('flex-column position-sticky', {
|
||||
'd-none': !displaySidebar,
|
||||
'd-flex overflow-auto box-shadow-centered-1': displaySidebar,
|
||||
'w-100': !isOnDesktop,
|
||||
'sidebar-desktop-width': isOnDesktop && !isOnXLDesktop,
|
||||
'w-25 sidebar-XL-width': isOnXLDesktop,
|
||||
'min-content-height': !enableInContextSidebar,
|
||||
})}
|
||||
data-testid="sidebar"
|
||||
>
|
||||
<Suspense fallback={(<Spinner />)}>
|
||||
<Routes>
|
||||
{enableInContext && !enableInContextSidebar && (
|
||||
<Route
|
||||
path={ROUTES.TOPICS.ALL}
|
||||
element={<InContextTopicsView />}
|
||||
/>
|
||||
)}
|
||||
{enableInContext && !enableInContextSidebar && (
|
||||
[
|
||||
ROUTES.TOPICS.TOPIC,
|
||||
ROUTES.TOPICS.CATEGORY,
|
||||
ROUTES.TOPICS.TOPIC_POST,
|
||||
ROUTES.TOPICS.TOPIC_POST_EDIT,
|
||||
].map((route) => (
|
||||
<Route
|
||||
key={route}
|
||||
path={route}
|
||||
element={<TopicPostsView />}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
{[
|
||||
ROUTES.POSTS.ALL_POSTS,
|
||||
ROUTES.POSTS.EDIT_ALL_POSTS,
|
||||
ROUTES.POSTS.MY_POSTS,
|
||||
ROUTES.POSTS.EDIT_MY_POSTS,
|
||||
ROUTES.TOPICS.CATEGORY,
|
||||
ROUTES.TOPICS.CATEGORY_POST,
|
||||
ROUTES.TOPICS.CATEGORY_POST_EDIT,
|
||||
ROUTES.TOPICS.TOPIC,
|
||||
ROUTES.TOPICS.TOPIC_POST,
|
||||
ROUTES.TOPICS.TOPIC_POST_EDIT,
|
||||
].map((route) => (
|
||||
<Route
|
||||
key={route}
|
||||
path={route}
|
||||
element={<PostsView />}
|
||||
/>
|
||||
))}
|
||||
{ROUTES.TOPICS.PATH.map(path => (
|
||||
<Route key={path} path={path} element={<LegacyTopicsView />} />
|
||||
))}
|
||||
{
|
||||
[ROUTES.LEARNERS.POSTS, ROUTES.LEARNERS.POSTS_EDIT].map((route) => (
|
||||
<Route key={route} path={route} element={<LearnerPostsView />} />
|
||||
))
|
||||
}
|
||||
<Route path={ROUTES.LEARNERS.PATH} element={<LearnersView />} />
|
||||
{configStatus === RequestStatus.SUCCESSFUL && (
|
||||
<Route path={`${ROUTES.DISCUSSIONS.PATH}/*`} element={<Navigate to="posts" />} />
|
||||
)}
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
DiscussionSidebar.propTypes = {
|
||||
displaySidebar: PropTypes.bool,
|
||||
postActionBarRef: PropTypes.oneOfType([
|
||||
PropTypes.func,
|
||||
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
|
||||
]).isRequired,
|
||||
]),
|
||||
};
|
||||
|
||||
DiscussionSidebar.defaultProps = {
|
||||
displaySidebar: false,
|
||||
postActionBarRef: null,
|
||||
};
|
||||
|
||||
export default React.memo(DiscussionSidebar);
|
||||
|
||||
@@ -3,7 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { Context as ResponsiveContext } from 'react-responsive';
|
||||
import { MemoryRouter } from 'react-router';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { Factory } from 'rosie';
|
||||
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
@@ -28,8 +28,8 @@ function renderComponent(displaySidebar = true, location = `/${courseId}/`) {
|
||||
const wrapper = render(
|
||||
<IntlProvider locale="en">
|
||||
<ResponsiveContext.Provider value={{ width: 1280 }}>
|
||||
<AppProvider store={store}>
|
||||
<DiscussionContext.Provider value={{ courseId }}>
|
||||
<AppProvider store={store} wrapWithRouter={false}>
|
||||
<DiscussionContext.Provider value={{ courseId, page: 'posts' }}>
|
||||
<MemoryRouter initialEntries={[location]}>
|
||||
<DiscussionSidebar displaySidebar={displaySidebar} postActionBarRef={null} />
|
||||
</MemoryRouter>
|
||||
|
||||
@@ -1,47 +1,161 @@
|
||||
import React, { lazy, Suspense, useMemo } from 'react';
|
||||
/* eslint-disable react/jsx-no-constructed-context-values */
|
||||
import React, { lazy, Suspense, useRef } from 'react';
|
||||
|
||||
import { useRouteMatch } from 'react-router';
|
||||
import classNames from 'classnames';
|
||||
import { useSelector } from 'react-redux';
|
||||
import {
|
||||
matchPath, Route, Routes, useLocation, useMatch,
|
||||
} from 'react-router-dom';
|
||||
|
||||
import { LearningHeader as Header } from '@edx/frontend-component-header';
|
||||
|
||||
import { Spinner } from '../../components';
|
||||
import { ALL_ROUTES } from '../../data/constants';
|
||||
import { selectCourseTabs } from '../../components/NavigationBar/data/selectors';
|
||||
import { ALL_ROUTES, DiscussionProvider, Routes as ROUTES } from '../../data/constants';
|
||||
import { DiscussionContext } from '../common/context';
|
||||
import {
|
||||
useCourseDiscussionData, useCurrentPage, useEnableInContextSidebar, useRedirectToThread,
|
||||
useCourseDiscussionData, useIsOnDesktop, useRedirectToThread, useSidebarVisible,
|
||||
} from '../data/hooks';
|
||||
import DiscussionLayout from './DiscussionLayout';
|
||||
import { selectDiscussionProvider, selectEnableInContext } from '../data/selectors';
|
||||
import { EmptyLearners, EmptyPosts, EmptyTopics } from '../empty-posts';
|
||||
import { EmptyTopic as InContextEmptyTopics } from '../in-context-topics/components';
|
||||
import messages from '../messages';
|
||||
import { selectPostEditorVisible } from '../posts/data/selectors';
|
||||
import useFeedbackWrapper from './FeedbackWrapper';
|
||||
|
||||
const Footer = lazy(() => import('@edx/frontend-component-footer'));
|
||||
const PostActionsBar = lazy(() => import('../posts/post-actions-bar/PostActionsBar'));
|
||||
const CourseTabsNavigation = lazy(() => import('../../components/NavigationBar/CourseTabsNavigation'));
|
||||
const LegacyBreadcrumbMenu = lazy(() => import('../navigation/breadcrumb-menu/LegacyBreadcrumbMenu'));
|
||||
const NavigationBar = lazy(() => import('../navigation/navigation-bar/NavigationBar'));
|
||||
const DiscussionsProductTour = lazy(() => import('../tours/DiscussionsProductTour'));
|
||||
const DiscussionsRestrictionBanner = lazy(() => import('./DiscussionsRestrictionBanner'));
|
||||
const DiscussionContent = lazy(() => import('./DiscussionContent'));
|
||||
const DiscussionSidebar = lazy(() => import('./DiscussionSidebar'));
|
||||
|
||||
const DiscussionsHome = () => {
|
||||
useCourseDiscussionData();
|
||||
useRedirectToThread();
|
||||
useFeedbackWrapper();
|
||||
const page = useCurrentPage();
|
||||
const enableInContextSidebar = useEnableInContextSidebar();
|
||||
const location = useLocation();
|
||||
const postActionBarRef = useRef(null);
|
||||
const postEditorVisible = useSelector(selectPostEditorVisible);
|
||||
const provider = useSelector(selectDiscussionProvider);
|
||||
const enableInContext = useSelector(selectEnableInContext);
|
||||
const { courseNumber, courseTitle, org } = useSelector(selectCourseTabs);
|
||||
const pageParams = useMatch(ROUTES.COMMENTS.PAGE)?.params;
|
||||
const page = pageParams?.page || null;
|
||||
const matchPattern = ALL_ROUTES.find((route) => matchPath({ path: route }, location.pathname));
|
||||
const { params } = useMatch(matchPattern);
|
||||
const isOnDesktop = useIsOnDesktop();
|
||||
let displaySidebar = useSidebarVisible();
|
||||
const enableInContextSidebar = Boolean(new URLSearchParams(location.search).get('inContextSidebar') !== null);
|
||||
const {
|
||||
params: {
|
||||
courseId, postId, topicId, category, learnerUsername,
|
||||
},
|
||||
} = useRouteMatch(ALL_ROUTES);
|
||||
courseId, postId, topicId, category, learnerUsername,
|
||||
} = params;
|
||||
|
||||
const contextValues = useMemo(() => ({
|
||||
page,
|
||||
courseId,
|
||||
postId,
|
||||
topicId,
|
||||
enableInContextSidebar,
|
||||
category,
|
||||
learnerUsername,
|
||||
}), [page, courseId, postId, topicId, enableInContextSidebar, category, learnerUsername]);
|
||||
useCourseDiscussionData(courseId);
|
||||
useRedirectToThread(courseId, enableInContextSidebar);
|
||||
useFeedbackWrapper();
|
||||
/* Display the content area if we are currently viewing/editing a post or creating one.
|
||||
If the window is larger than a particular size, show the sidebar for navigating between posts/topics.
|
||||
However, for smaller screens or embeds, only show the sidebar if the content area isn't displayed. */
|
||||
const displayContentArea = (postId || postEditorVisible || (learnerUsername && postId));
|
||||
if (displayContentArea) { displaySidebar = isOnDesktop; }
|
||||
|
||||
return (
|
||||
<Suspense fallback={(<Spinner />)}>
|
||||
<DiscussionLayout>
|
||||
<DiscussionContext.Provider value={contextValues}>
|
||||
<DiscussionContent />
|
||||
</DiscussionContext.Provider>
|
||||
</DiscussionLayout>
|
||||
<DiscussionContext.Provider value={{
|
||||
page,
|
||||
courseId,
|
||||
postId,
|
||||
topicId,
|
||||
enableInContextSidebar,
|
||||
category,
|
||||
learnerUsername,
|
||||
}}
|
||||
>
|
||||
{!enableInContextSidebar && (
|
||||
<Header courseOrg={org} courseNumber={courseNumber} courseTitle={courseTitle} />
|
||||
)}
|
||||
<main className="container-fluid d-flex flex-column p-0 w-100" id="main" tabIndex="-1">
|
||||
{!enableInContextSidebar && <CourseTabsNavigation activeTab="discussion" courseId={courseId} />}
|
||||
<div
|
||||
className={classNames('header-action-bar', {
|
||||
'shadow-none border-light-300 border-bottom': enableInContextSidebar,
|
||||
})}
|
||||
ref={postActionBarRef}
|
||||
>
|
||||
<div
|
||||
className={classNames('d-flex flex-row justify-content-between navbar fixed-top', {
|
||||
'pl-4 pr-2 py-0': enableInContextSidebar,
|
||||
})}
|
||||
>
|
||||
{!enableInContextSidebar && (
|
||||
<NavigationBar />
|
||||
)}
|
||||
<PostActionsBar />
|
||||
</div>
|
||||
<DiscussionsRestrictionBanner />
|
||||
</div>
|
||||
{provider === DiscussionProvider.LEGACY && (
|
||||
<Suspense fallback={(<Spinner />)}>
|
||||
<Routes>
|
||||
{[
|
||||
ROUTES.TOPICS.CATEGORY,
|
||||
ROUTES.TOPICS.CATEGORY_POST,
|
||||
ROUTES.TOPICS.CATEGORY_POST_EDIT,
|
||||
ROUTES.TOPICS.TOPIC,
|
||||
ROUTES.TOPICS.TOPIC_POST,
|
||||
ROUTES.TOPICS.TOPIC_POST_EDIT,
|
||||
].map((route) => (
|
||||
<Route
|
||||
key={route}
|
||||
path={route}
|
||||
element={<LegacyBreadcrumbMenu />}
|
||||
/>
|
||||
))}
|
||||
</Routes>
|
||||
</Suspense>
|
||||
)}
|
||||
<div className="d-flex flex-row position-relative">
|
||||
<Suspense fallback={(<Spinner />)}>
|
||||
<DiscussionSidebar displaySidebar={displaySidebar} postActionBarRef={postActionBarRef} />
|
||||
</Suspense>
|
||||
{displayContentArea && (
|
||||
<Suspense fallback={(<Spinner />)}>
|
||||
<DiscussionContent />
|
||||
</Suspense>
|
||||
)}
|
||||
{!displayContentArea && (
|
||||
<Routes>
|
||||
<>
|
||||
{ROUTES.TOPICS.PATH.map(route => (
|
||||
<Route
|
||||
key={route}
|
||||
path={`${route}/*`}
|
||||
element={(enableInContext || enableInContextSidebar) ? <InContextEmptyTopics /> : <EmptyTopics />}
|
||||
/>
|
||||
))}
|
||||
<Route
|
||||
path={ROUTES.POSTS.MY_POSTS}
|
||||
element={<EmptyPosts subTitleMessage={messages.emptyMyPosts} />}
|
||||
/>
|
||||
{[`${ROUTES.POSTS.PATH}/*`, ROUTES.POSTS.ALL_POSTS, ROUTES.LEARNERS.POSTS].map((route) => (
|
||||
<Route
|
||||
key={route}
|
||||
path={route}
|
||||
element={<EmptyPosts subTitleMessage={messages.emptyAllPosts} />}
|
||||
/>
|
||||
))}
|
||||
<Route path={ROUTES.LEARNERS.PATH} element={<EmptyLearners />} />
|
||||
</>
|
||||
</Routes>
|
||||
)}
|
||||
</div>
|
||||
{!enableInContextSidebar && (
|
||||
<DiscussionsProductTour />
|
||||
)}
|
||||
</main>
|
||||
{!enableInContextSidebar && <Footer />}
|
||||
</DiscussionContext.Provider>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import MockAdapter from 'axios-mock-adapter';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { Context as ResponsiveContext } from 'react-responsive';
|
||||
import { MemoryRouter } from 'react-router';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { Factory } from 'rosie';
|
||||
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
@@ -42,7 +42,7 @@ function renderComponent(location = `/${courseId}/`) {
|
||||
const wrapper = render(
|
||||
<IntlProvider locale="en">
|
||||
<ResponsiveContext.Provider value={{ width: 1280 }}>
|
||||
<AppProvider store={store}>
|
||||
<AppProvider store={store} wrapWithRouter={false}>
|
||||
<MemoryRouter initialEntries={[location]}>
|
||||
<DiscussionsHome />
|
||||
</MemoryRouter>
|
||||
@@ -198,9 +198,7 @@ describe('DiscussionsHome', () => {
|
||||
);
|
||||
|
||||
it('should display empty page message for empty learners list', async () => {
|
||||
axiosMock.onGet(getDiscussionsConfigUrl(courseId)).reply(200, {
|
||||
learners_tab_enabled: true,
|
||||
});
|
||||
axiosMock.onGet(getDiscussionsConfigUrl(courseId)).reply(200, {});
|
||||
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
|
||||
await renderComponent(`/${courseId}/learners`);
|
||||
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Route, Switch } from 'react-router';
|
||||
|
||||
import { Routes } from '../../data/constants';
|
||||
import { useEnableInContextSidebar, useShowLearnersTab } from '../data/hooks';
|
||||
import { selectEnableInContext } from '../data/selectors';
|
||||
import { EmptyLearners, EmptyPosts, EmptyTopics } from '../empty-posts';
|
||||
import { EmptyTopic as InContextEmptyTopics } from '../in-context-topics/components';
|
||||
import messages from '../messages';
|
||||
|
||||
const InfoPage = () => {
|
||||
const enableInContext = useSelector(selectEnableInContext);
|
||||
const isRedirectToLearners = useShowLearnersTab();
|
||||
const enableInContextSidebar = useEnableInContextSidebar();
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route
|
||||
path={Routes.TOPICS.PATH}
|
||||
component={(enableInContext || enableInContextSidebar) ? InContextEmptyTopics : EmptyTopics}
|
||||
/>
|
||||
<Route
|
||||
path={Routes.POSTS.MY_POSTS}
|
||||
render={routeProps => <EmptyPosts {...routeProps} subTitleMessage={messages.emptyMyPosts} />}
|
||||
/>
|
||||
<Route
|
||||
path={[Routes.POSTS.PATH, Routes.POSTS.ALL_POSTS, Routes.LEARNERS.POSTS]}
|
||||
render={routeProps => <EmptyPosts {...routeProps} subTitleMessage={messages.emptyAllPosts} />}
|
||||
/>
|
||||
{isRedirectToLearners && <Route path={Routes.LEARNERS.PATH} component={EmptyLearners} />}
|
||||
</Switch>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(InfoPage);
|
||||
@@ -1,40 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import {
|
||||
useIsOnDesktop, useLearnerUsername, usePostId, useSidebarVisible,
|
||||
} from '../data/hooks';
|
||||
import { selectPostEditorVisible } from '../posts/data/selectors';
|
||||
|
||||
const LayoutSwitcher = ({ children, sidebar, infoPage }) => {
|
||||
const postId = usePostId();
|
||||
const learnerUsername = useLearnerUsername();
|
||||
const isOnDesktop = useIsOnDesktop();
|
||||
let displaySidebar = useSidebarVisible();
|
||||
const postEditorVisible = useSelector(selectPostEditorVisible);
|
||||
|
||||
const displayContentArea = useMemo(() => {
|
||||
const isContentVisible = postId || postEditorVisible || (learnerUsername && postId);
|
||||
if (isContentVisible) {
|
||||
displaySidebar = isOnDesktop;
|
||||
}
|
||||
return isContentVisible;
|
||||
}, [postId, postEditorVisible, learnerUsername, isOnDesktop]);
|
||||
|
||||
return (
|
||||
<div className="d-flex flex-row position-relative">
|
||||
{displaySidebar && sidebar }
|
||||
{displayContentArea ? children : infoPage }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
LayoutSwitcher.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
sidebar: PropTypes.node.isRequired,
|
||||
infoPage: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default React.memo(LayoutSwitcher);
|
||||
@@ -1,23 +0,0 @@
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Route } from 'react-router-dom';
|
||||
|
||||
import { DiscussionProvider, Routes } from '../../data/constants';
|
||||
import { selectDiscussionProvider } from '../data/selectors';
|
||||
import LegacyBreadcrumbMenu from '../navigation/breadcrumb-menu/LegacyBreadcrumbMenu';
|
||||
|
||||
const LegacyBreadcrumb = () => {
|
||||
const provider = useSelector(selectDiscussionProvider);
|
||||
|
||||
return (
|
||||
provider === DiscussionProvider.LEGACY && (
|
||||
<Route
|
||||
path={[Routes.POSTS.PATH, Routes.TOPICS.CATEGORY]}
|
||||
component={LegacyBreadcrumbMenu}
|
||||
/>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(LegacyBreadcrumb);
|
||||
@@ -1,54 +0,0 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { useWindowSize } from '@edx/paragon';
|
||||
|
||||
import {
|
||||
useContainerSize, useEnableInContextSidebar, useIsOnDesktop, useIsOnXLDesktop,
|
||||
} from '../data/hooks';
|
||||
|
||||
const ResizableSidebar = ({ children, postActionBarRef }) => {
|
||||
const sidebarRef = useRef(null);
|
||||
const isOnDesktop = useIsOnDesktop();
|
||||
const isOnXLDesktop = useIsOnXLDesktop();
|
||||
const enableInContextSidebar = useEnableInContextSidebar();
|
||||
const postActionBarHeight = useContainerSize(postActionBarRef);
|
||||
const { height: windowHeight } = useWindowSize();
|
||||
|
||||
useEffect(() => {
|
||||
if (sidebarRef && postActionBarHeight && !enableInContextSidebar) {
|
||||
if (isOnDesktop) {
|
||||
sidebarRef.current.style.maxHeight = `${windowHeight - postActionBarHeight}px`;
|
||||
}
|
||||
sidebarRef.current.style.minHeight = `${windowHeight - postActionBarHeight}px`;
|
||||
sidebarRef.current.style.top = `${postActionBarHeight}px`;
|
||||
}
|
||||
}, [sidebarRef, postActionBarHeight, enableInContextSidebar]);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="sidebar"
|
||||
ref={sidebarRef}
|
||||
className={classNames('flex-column position-sticky d-flex overflow-auto box-shadow-centered-1', {
|
||||
'w-100': !isOnDesktop,
|
||||
'sidebar-desktop-width': isOnDesktop && !isOnXLDesktop,
|
||||
'w-25 sidebar-XL-width': isOnXLDesktop,
|
||||
'min-content-height': !enableInContextSidebar,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ResizableSidebar.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
postActionBarRef: PropTypes.oneOfType([
|
||||
PropTypes.func,
|
||||
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
|
||||
]).isRequired,
|
||||
};
|
||||
|
||||
export default React.memo(ResizableSidebar);
|
||||
@@ -26,7 +26,7 @@ function renderComponent(location = `/${courseId}/`) {
|
||||
return render(
|
||||
<IntlProvider locale="en">
|
||||
<ResponsiveContext.Provider value={{ width: 1280 }}>
|
||||
<AppProvider store={store}>
|
||||
<AppProvider store={store} wrapWithRouter={false}>
|
||||
<MemoryRouter initialEntries={[location]}>
|
||||
<EmptyPosts subTitleMessage={messages.emptyMyPosts} />
|
||||
</MemoryRouter>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useRouteMatch } from 'react-router';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { ALL_ROUTES } from '../../data/constants';
|
||||
import { useIsOnDesktop, useTotalTopicThreadCount } from '../data/hooks';
|
||||
import { selectTopicThreadCount } from '../data/selectors';
|
||||
import messages from '../messages';
|
||||
@@ -15,11 +14,11 @@ import EmptyPage from './EmptyPage';
|
||||
|
||||
const EmptyTopics = () => {
|
||||
const intl = useIntl();
|
||||
const match = useRouteMatch(ALL_ROUTES);
|
||||
const { topicId } = useParams();
|
||||
const dispatch = useDispatch();
|
||||
const isOnDesktop = useIsOnDesktop();
|
||||
const hasGlobalThreads = useTotalTopicThreadCount() > 0;
|
||||
const topicThreadCount = useSelector(selectTopicThreadCount(match.params.topicId));
|
||||
const topicThreadCount = useSelector(selectTopicThreadCount(topicId));
|
||||
|
||||
const addPost = useCallback(() => (
|
||||
dispatch(showPostEditor())
|
||||
@@ -35,7 +34,7 @@ const EmptyTopics = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (match.params.topicId) {
|
||||
if (topicId) {
|
||||
if (topicThreadCount > 0) {
|
||||
title = messages.noPostSelected;
|
||||
} else {
|
||||
|
||||
@@ -2,14 +2,14 @@ import { render, screen } from '@testing-library/react';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { Context as ResponsiveContext } from 'react-responsive';
|
||||
import { MemoryRouter } from 'react-router';
|
||||
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
||||
import { Factory } from 'rosie';
|
||||
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import { getApiBaseUrl } from '../../data/constants';
|
||||
import { getApiBaseUrl, Routes as ROUTES } from '../../data/constants';
|
||||
import { initializeStore } from '../../store';
|
||||
import { executeThunk } from '../../test-utils';
|
||||
import messages from '../messages';
|
||||
@@ -26,9 +26,12 @@ function renderComponent(location = `/${courseId}/topics/`) {
|
||||
return render(
|
||||
<IntlProvider locale="en">
|
||||
<ResponsiveContext.Provider value={{ width: 1280 }}>
|
||||
<AppProvider store={store}>
|
||||
<AppProvider store={store} wrapWithRouter={false}>
|
||||
<MemoryRouter initialEntries={[location]}>
|
||||
<EmptyTopics />
|
||||
<Routes>
|
||||
<Route path={ROUTES.TOPICS.ALL} element={<EmptyTopics />} />
|
||||
<Route path={ROUTES.TOPICS.TOPIC} element={<EmptyTopics />} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</AppProvider>
|
||||
</ResponsiveContext.Provider>
|
||||
|
||||
@@ -4,7 +4,9 @@ import {
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { generatePath, MemoryRouter, Route } from 'react-router';
|
||||
import {
|
||||
generatePath, MemoryRouter, Route, Routes, useLocation,
|
||||
} from 'react-router-dom';
|
||||
import { Factory } from 'rosie';
|
||||
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
@@ -12,7 +14,7 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import { PostActionsBar } from '../../components';
|
||||
import { Routes } from '../../data/constants';
|
||||
import { Routes as ROUTES } from '../../data/constants';
|
||||
import { initializeStore } from '../../store';
|
||||
import { executeThunk } from '../../test-utils';
|
||||
import { DiscussionContext } from '../common/context';
|
||||
@@ -35,16 +37,21 @@ let axiosMock;
|
||||
let lastLocation;
|
||||
let container;
|
||||
|
||||
const LocationComponent = () => {
|
||||
lastLocation = useLocation();
|
||||
return null;
|
||||
};
|
||||
|
||||
async function renderComponent({ topicId, category } = { }) {
|
||||
let path = `/${courseId}/topics`;
|
||||
if (topicId) {
|
||||
path = generatePath(Routes.POSTS.PATH, { courseId, topicId });
|
||||
path = generatePath(ROUTES.POSTS.PATH, { courseId, topicId });
|
||||
} else if (category) {
|
||||
path = generatePath(Routes.TOPICS.CATEGORY, { courseId, category });
|
||||
path = generatePath(ROUTES.TOPICS.CATEGORY, { courseId, category });
|
||||
}
|
||||
const wrapper = await render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<AppProvider store={store} wrapWithRouter={false}>
|
||||
<DiscussionContext.Provider value={{
|
||||
courseId,
|
||||
topicId,
|
||||
@@ -53,19 +60,35 @@ async function renderComponent({ topicId, category } = { }) {
|
||||
}}
|
||||
>
|
||||
<MemoryRouter initialEntries={[path]}>
|
||||
<Route exact path={[Routes.POSTS.PATH, Routes.TOPICS.CATEGORY]}>
|
||||
<TopicPostsView />
|
||||
</Route>
|
||||
<Route exact path={[Routes.TOPICS.ALL]}>
|
||||
<PostActionsBar />
|
||||
<TopicsView />
|
||||
</Route>
|
||||
<Route
|
||||
render={({ location }) => {
|
||||
lastLocation = location;
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
<Routes>
|
||||
{
|
||||
[
|
||||
ROUTES.POSTS.PATH,
|
||||
ROUTES.TOPICS.CATEGORY,
|
||||
].map((route) => (
|
||||
<Route
|
||||
key={route}
|
||||
path={route}
|
||||
element={(
|
||||
<>
|
||||
<TopicPostsView />
|
||||
<LocationComponent />
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
))
|
||||
}
|
||||
<Route
|
||||
path={ROUTES.TOPICS.ALL}
|
||||
element={(
|
||||
<>
|
||||
<PostActionsBar />
|
||||
<TopicsView />
|
||||
<LocationComponent />
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</DiscussionContext.Provider>
|
||||
</AppProvider>
|
||||
|
||||
@@ -5,7 +5,9 @@ import {
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { MemoryRouter, Route } from 'react-router';
|
||||
import {
|
||||
MemoryRouter, Route, Routes, useLocation,
|
||||
} from 'react-router-dom';
|
||||
import { Factory } from 'rosie';
|
||||
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
@@ -32,24 +34,21 @@ let axiosMock;
|
||||
let lastLocation;
|
||||
let container;
|
||||
|
||||
const LocationComponent = () => {
|
||||
lastLocation = useLocation();
|
||||
return null;
|
||||
};
|
||||
|
||||
function renderComponent() {
|
||||
const wrapper = render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<AppProvider store={store} wrapWithRouter={false}>
|
||||
<DiscussionContext.Provider value={{ courseId, category }}>
|
||||
<MemoryRouter initialEntries={[`/${courseId}/topics/`]}>
|
||||
<Route path="/:courseId/topics/">
|
||||
<TopicsView />
|
||||
</Route>
|
||||
<Route path="/:courseId/category/:category">
|
||||
<TopicPostsView />
|
||||
</Route>
|
||||
<Route
|
||||
render={({ location }) => {
|
||||
lastLocation = location;
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
<Routes>
|
||||
<Route path="/:courseId/topics/*" element={<><TopicsView /><LocationComponent /></>} />
|
||||
<Route path="/:courseId/category/:category" element={<><TopicPostsView /><LocationComponent /></>} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</DiscussionContext.Provider>
|
||||
</AppProvider>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Icon, IconButton, Spinner } from '@edx/paragon';
|
||||
@@ -12,7 +12,7 @@ import messages from '../messages';
|
||||
const BackButton = ({
|
||||
intl, path, title, loading,
|
||||
}) => {
|
||||
const history = useHistory();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -22,7 +22,7 @@ const BackButton = ({
|
||||
iconAs={Icon}
|
||||
style={{ padding: '18px' }}
|
||||
size="inline"
|
||||
onClick={() => history.push(path)}
|
||||
onClick={() => navigate(path)}
|
||||
alt={intl.formatMessage(messages.backAlt)}
|
||||
/>
|
||||
<div className="d-flex flex-fill justify-content-center align-items-center mr-4.5">
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import React, { useCallback, useContext } from 'react';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useRouteMatch } from 'react-router';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { ALL_ROUTES } from '../../../data/constants';
|
||||
import { DiscussionContext } from '../../common/context';
|
||||
import { useIsOnDesktop } from '../../data/hooks';
|
||||
import { selectPostThreadCount } from '../../data/selectors';
|
||||
@@ -16,11 +15,11 @@ import { selectCourseWareThreadsCount, selectTotalTopicsThreadsCount } from '../
|
||||
|
||||
const EmptyTopics = () => {
|
||||
const intl = useIntl();
|
||||
const match = useRouteMatch(ALL_ROUTES);
|
||||
const { category, topicId } = useParams();
|
||||
const dispatch = useDispatch();
|
||||
const isOnDesktop = useIsOnDesktop();
|
||||
const { enableInContextSidebar } = useContext(DiscussionContext);
|
||||
const courseWareThreadsCount = useSelector(selectCourseWareThreadsCount(match.params.category));
|
||||
const courseWareThreadsCount = useSelector(selectCourseWareThreadsCount(category));
|
||||
const topicThreadsCount = useSelector(selectPostThreadCount);
|
||||
// hasGlobalThreads is used to determine if there are any post available in courseware and non-courseware topics
|
||||
const hasGlobalThreads = useSelector(selectTotalTopicsThreadsCount) > 0;
|
||||
@@ -39,7 +38,7 @@ const EmptyTopics = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (match.params.topicId) {
|
||||
if (topicId) {
|
||||
if (topicThreadsCount > 0) {
|
||||
title = messages.noPostSelected;
|
||||
} else {
|
||||
@@ -48,7 +47,7 @@ const EmptyTopics = () => {
|
||||
subTitle = messages.emptyTopic;
|
||||
fullWidth = true;
|
||||
}
|
||||
} else if (match.params.category) {
|
||||
} else if (category) {
|
||||
if (enableInContextSidebar && topicThreadsCount > 0) {
|
||||
title = messages.noPostSelected;
|
||||
} else if (courseWareThreadsCount > 0) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { memo, useCallback, useEffect } from 'react';
|
||||
import React, { useCallback, useContext, useEffect } from 'react';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
@@ -6,15 +6,14 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Icon, SearchField } from '@edx/paragon';
|
||||
import { Search as SearchIcon } from '@edx/paragon/icons';
|
||||
|
||||
import { useCurrentPage } from '../../data/hooks';
|
||||
import { DiscussionContext } from '../../common/context';
|
||||
import postsMessages from '../../posts/post-actions-bar/messages';
|
||||
import { setFilter as setTopicFilter } from '../data/slices';
|
||||
|
||||
const TopicSearchBar = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const page = useCurrentPage();
|
||||
|
||||
const { page } = useContext(DiscussionContext);
|
||||
const topicSearch = useSelector(({ inContextTopics }) => inContextTopics.filter);
|
||||
let searchValue = '';
|
||||
|
||||
@@ -58,4 +57,4 @@ const TopicSearchBar = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(TopicSearchBar);
|
||||
export default TopicSearchBar;
|
||||
|
||||
@@ -2,8 +2,7 @@ import React, { useCallback, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useParams } from 'react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
@@ -41,7 +40,7 @@ const SectionBaseGroup = ({
|
||||
role="option"
|
||||
data-subsection-id={subsection.id}
|
||||
data-testid="subsection-group"
|
||||
to={sectionUrl(subsection.id)}
|
||||
to={sectionUrl(subsection.id)()}
|
||||
onClick={() => isSelected(subsection.id)}
|
||||
aria-current={isSelected(section.id) ? 'page' : undefined}
|
||||
tabIndex={(isSelected(subsection.id) || index === 0) ? 0 : -1}
|
||||
|
||||
@@ -5,8 +5,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useParams } from 'react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Icon, OverlayTrigger, Tooltip } from '@edx/paragon';
|
||||
@@ -42,7 +41,7 @@ const Topic = ({
|
||||
'border-light-400 border-bottom': showDivider,
|
||||
})}
|
||||
data-topic-id={topic.id}
|
||||
to={topicUrl}
|
||||
to={topicUrl()}
|
||||
onClick={() => isSelected(topic.id)}
|
||||
aria-current={isSelected(topic.id) ? 'page' : undefined}
|
||||
role="option"
|
||||
|
||||
@@ -4,7 +4,7 @@ import React, {
|
||||
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
@@ -35,7 +35,7 @@ import messages from './messages';
|
||||
const LearnerPostsView = () => {
|
||||
const intl = useIntl();
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const postsIds = useSelector(selectAllThreadsIds);
|
||||
@@ -83,7 +83,7 @@ const LearnerPostsView = () => {
|
||||
iconAs={Icon}
|
||||
style={{ padding: '18px' }}
|
||||
size="inline"
|
||||
onClick={() => history.push(discussionsPath(Routes.LEARNERS.PATH, { courseId })(location))}
|
||||
onClick={() => navigate({ ...discussionsPath(Routes.LEARNERS.PATH, { courseId })(location) })}
|
||||
alt={intl.formatMessage(messages.back)}
|
||||
/>
|
||||
<div className="text-primary-500 font-style font-weight-bold py-2.5">
|
||||
|
||||
@@ -6,7 +6,9 @@ import {
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { MemoryRouter, Route } from 'react-router';
|
||||
import {
|
||||
MemoryRouter, Route, Routes, useLocation,
|
||||
} from 'react-router-dom';
|
||||
import { Factory } from 'rosie';
|
||||
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
@@ -33,26 +35,26 @@ const username = 'abc123';
|
||||
let container;
|
||||
let lastLocation;
|
||||
|
||||
const LocationComponent = () => {
|
||||
lastLocation = useLocation();
|
||||
return null;
|
||||
};
|
||||
|
||||
async function renderComponent() {
|
||||
const wrapper = render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<AppProvider store={store} wrapWithRouter={false}>
|
||||
<DiscussionContext.Provider
|
||||
value={{
|
||||
learnerUsername: username,
|
||||
courseId,
|
||||
page: 'learners',
|
||||
}}
|
||||
>
|
||||
<MemoryRouter initialEntries={[`/${courseId}/learners/${username}/posts`]}>
|
||||
<Route path="/:courseId/learners/:learnerUsername/posts">
|
||||
<LearnerPostsView />
|
||||
</Route>
|
||||
<Route
|
||||
render={({ location }) => {
|
||||
lastLocation = location;
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
<Routes>
|
||||
<Route path="/:courseId/learners/:learnerUsername?/posts?" element={<><LearnerPostsView /><LocationComponent /></>} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</DiscussionContext.Provider>
|
||||
</AppProvider>
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import {
|
||||
Redirect, useLocation, useParams,
|
||||
} from 'react-router';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Spinner } from '@edx/paragon';
|
||||
|
||||
import SearchInfo from '../../components/SearchInfo';
|
||||
import { RequestStatus, Routes } from '../../data/constants';
|
||||
import { selectConfigLoadingStatus, selectLearnersTabEnabled } from '../data/selectors';
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import { selectConfigLoadingStatus } from '../data/selectors';
|
||||
import NoResults from '../posts/NoResults';
|
||||
import {
|
||||
learnersLoadingStatus,
|
||||
@@ -27,25 +25,21 @@ import messages from './messages';
|
||||
const LearnersView = () => {
|
||||
const intl = useIntl();
|
||||
const { courseId } = useParams();
|
||||
const location = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
const orderBy = useSelector(selectLearnerSorting());
|
||||
const nextPage = useSelector(selectLearnerNextPage());
|
||||
const loadingStatus = useSelector(learnersLoadingStatus());
|
||||
const usernameSearch = useSelector(selectUsernameSearch());
|
||||
const courseConfigLoadingStatus = useSelector(selectConfigLoadingStatus);
|
||||
const learnersTabEnabled = useSelector(selectLearnersTabEnabled);
|
||||
const learners = useSelector(selectAllLearners);
|
||||
|
||||
useEffect(() => {
|
||||
if (learnersTabEnabled) {
|
||||
if (usernameSearch) {
|
||||
dispatch(fetchLearners(courseId, { orderBy, usernameSearch }));
|
||||
} else {
|
||||
dispatch(fetchLearners(courseId, { orderBy }));
|
||||
}
|
||||
if (usernameSearch) {
|
||||
dispatch(fetchLearners(courseId, { orderBy, usernameSearch }));
|
||||
} else {
|
||||
dispatch(fetchLearners(courseId, { orderBy }));
|
||||
}
|
||||
}, [courseId, orderBy, learnersTabEnabled, usernameSearch]);
|
||||
}, [courseId, orderBy, usernameSearch]);
|
||||
|
||||
const loadPage = useCallback(async () => {
|
||||
if (nextPage) {
|
||||
@@ -63,12 +57,12 @@ const LearnersView = () => {
|
||||
|
||||
const renderLearnersList = useMemo(() => (
|
||||
(
|
||||
courseConfigLoadingStatus === RequestStatus.SUCCESSFUL && learnersTabEnabled && learners.map((learner) => (
|
||||
courseConfigLoadingStatus === RequestStatus.SUCCESSFUL && learners.map((learner) => (
|
||||
<LearnerCard learner={learner} key={learner.username} />
|
||||
))
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
) || <></>
|
||||
), [courseConfigLoadingStatus, learnersTabEnabled, learners]);
|
||||
), [courseConfigLoadingStatus, learners]);
|
||||
|
||||
return (
|
||||
<div className="d-flex flex-column border-right border-light-400">
|
||||
@@ -83,14 +77,6 @@ const LearnersView = () => {
|
||||
/>
|
||||
)}
|
||||
<div className="list-group list-group-flush learner" role="list">
|
||||
{courseConfigLoadingStatus === RequestStatus.SUCCESSFUL && !learnersTabEnabled && (
|
||||
<Redirect
|
||||
to={{
|
||||
...location,
|
||||
pathname: Routes.DISCUSSIONS.PATH,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{renderLearnersList}
|
||||
{loadingStatus === RequestStatus.IN_PROGRESS ? (
|
||||
<div className="d-flex justify-content-center p-4">
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { MemoryRouter, Route } from 'react-router';
|
||||
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
||||
import { Factory } from 'rosie';
|
||||
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
@@ -34,7 +34,7 @@ let container;
|
||||
function renderComponent() {
|
||||
const wrapper = render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<AppProvider store={store} wrapWithRouter={false}>
|
||||
<DiscussionContext.Provider value={{
|
||||
page: 'learners',
|
||||
learnerUsername: 'learner-1',
|
||||
@@ -42,10 +42,17 @@ function renderComponent() {
|
||||
}}
|
||||
>
|
||||
<MemoryRouter initialEntries={[`/${courseId}/`]}>
|
||||
<Route path="/:courseId/">
|
||||
<PostActionsBar />
|
||||
<LearnersView />
|
||||
</Route>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/:courseId/"
|
||||
element={(
|
||||
<>
|
||||
<PostActionsBar />
|
||||
<LearnersView />
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</DiscussionContext.Provider>
|
||||
</AppProvider>
|
||||
@@ -62,7 +69,6 @@ describe('LearnersView', () => {
|
||||
username: 'test_user',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
learnersTabEnabled: false,
|
||||
},
|
||||
});
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
@@ -99,7 +105,6 @@ describe('LearnersView', () => {
|
||||
|
||||
async function assignPrivilages(hasModerationPrivileges = false) {
|
||||
axiosMock.onGet(getDiscussionsConfigUrl(courseId)).reply(200, {
|
||||
learners_tab_enabled: true,
|
||||
user_is_privileged: true,
|
||||
hasModerationPrivileges,
|
||||
});
|
||||
@@ -107,13 +112,6 @@ describe('LearnersView', () => {
|
||||
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
|
||||
}
|
||||
|
||||
test('Learners tab is disabled by default', async () => {
|
||||
await setUpLearnerMockResponse();
|
||||
await renderComponent();
|
||||
|
||||
expect(screen.queryByText('learner-1')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Learners tab is enabled', async () => {
|
||||
await setUpLearnerMockResponse();
|
||||
await assignPrivilages();
|
||||
|
||||
@@ -5,12 +5,10 @@ import {
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { generatePath, MemoryRouter, Route } from 'react-router';
|
||||
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import { Routes } from '../../../data/constants';
|
||||
import { initializeStore } from '../../../store';
|
||||
import { DiscussionContext } from '../../common/context';
|
||||
import LearnerPostFilterBar from './LearnerPostFilterBar';
|
||||
@@ -18,32 +16,21 @@ import LearnerPostFilterBar from './LearnerPostFilterBar';
|
||||
let store;
|
||||
const username = 'abc123';
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const path = generatePath(
|
||||
Routes.LEARNERS.POSTS,
|
||||
{ courseId, learnerUsername: username },
|
||||
);
|
||||
|
||||
function renderComponent() {
|
||||
return render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<DiscussionContext.Provider
|
||||
value={{
|
||||
learnerUsername: username,
|
||||
courseId,
|
||||
}}
|
||||
>
|
||||
<MemoryRouter initialEntries={[path]}>
|
||||
<Route
|
||||
path={Routes.LEARNERS.POSTS}
|
||||
component={LearnerPostFilterBar}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
</DiscussionContext.Provider>
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
}
|
||||
const renderComponent = () => render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<DiscussionContext.Provider
|
||||
value={{
|
||||
learnerUsername: username,
|
||||
courseId,
|
||||
}}
|
||||
>
|
||||
<LearnerPostFilterBar />
|
||||
</DiscussionContext.Provider>
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
describe('LearnerPostFilterBar', () => {
|
||||
beforeEach(async () => {
|
||||
|
||||
@@ -18,7 +18,7 @@ const LearnerCard = ({ learner }) => {
|
||||
0: enableInContextSidebar ? 'in-context' : undefined,
|
||||
learnerUsername: learner.username,
|
||||
courseId,
|
||||
});
|
||||
})();
|
||||
|
||||
return (
|
||||
<Link
|
||||
|
||||
@@ -29,7 +29,7 @@ const BreadcrumbDropdown = ({
|
||||
key="null"
|
||||
active={!currentItem}
|
||||
as={Link}
|
||||
to={showAllPath}
|
||||
to={showAllPath()}
|
||||
>
|
||||
{showAllMsg}
|
||||
</Dropdown.Item>
|
||||
@@ -39,7 +39,7 @@ const BreadcrumbDropdown = ({
|
||||
key={itemLabelFunc(item)}
|
||||
active={itemActiveFunc(item)}
|
||||
as={Link}
|
||||
to={itemPathFunc(item)}
|
||||
to={itemPathFunc(item)()}
|
||||
>
|
||||
{itemLabelFunc(item)}
|
||||
</Dropdown.Item>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useRouteMatch } from 'react-router';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { Routes } from '../../../data/constants';
|
||||
import {
|
||||
@@ -15,12 +15,10 @@ import BreadcrumbDropdown from './BreadcrumbDropdown';
|
||||
|
||||
const LegacyBreadcrumbMenu = () => {
|
||||
const {
|
||||
params: {
|
||||
courseId,
|
||||
category,
|
||||
topicId: currentTopicId,
|
||||
},
|
||||
} = useRouteMatch([Routes.TOPICS.CATEGORY, Routes.TOPICS.TOPIC]);
|
||||
courseId,
|
||||
category,
|
||||
topicId: currentTopicId,
|
||||
} = useParams();
|
||||
const currentTopic = useSelector(selectTopic(currentTopicId));
|
||||
const currentCategory = category || currentTopic?.categoryId;
|
||||
const decodedCurrentCategory = String(currentCategory).replace('%23', '#');
|
||||
|
||||
@@ -5,14 +5,14 @@ import {
|
||||
} from '@testing-library/react';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { MemoryRouter, Route } from 'react-router';
|
||||
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
||||
import { Factory } from 'rosie';
|
||||
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import { getApiBaseUrl, Routes } from '../../../data/constants';
|
||||
import { getApiBaseUrl, Routes as ROUTES } from '../../../data/constants';
|
||||
import { initializeStore } from '../../../store';
|
||||
import { executeThunk } from '../../../test-utils';
|
||||
import { fetchCourseTopics } from '../../topics/data/thunks';
|
||||
@@ -28,15 +28,22 @@ let axiosMock;
|
||||
function renderComponent(path) {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<AppProvider store={store} wrapWithRouter={false}>
|
||||
<MemoryRouter initialEntries={[path]}>
|
||||
<Route
|
||||
path={[
|
||||
Routes.POSTS.PATH,
|
||||
Routes.TOPICS.CATEGORY,
|
||||
]}
|
||||
component={LegacyBreadcrumbMenu}
|
||||
/>
|
||||
<Routes>
|
||||
{
|
||||
[
|
||||
ROUTES.POSTS.PATH,
|
||||
ROUTES.TOPICS.CATEGORY,
|
||||
].map((route) => (
|
||||
<Route
|
||||
key={route}
|
||||
path={route}
|
||||
element={<LegacyBreadcrumbMenu />}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import React, { useContext, useMemo } from 'react';
|
||||
|
||||
import { matchPath } from 'react-router';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { matchPath, NavLink, useLocation } from 'react-router-dom';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Nav } from '@edx/paragon';
|
||||
|
||||
import { Routes } from '../../../data/constants';
|
||||
import withConditionalInContextRendering from '../../common/withConditionalInContextRendering';
|
||||
import { useCourseId, useShowLearnersTab } from '../../data/hooks';
|
||||
import { DiscussionContext } from '../../common/context';
|
||||
import { discussionsPath } from '../../utils';
|
||||
import messages from './messages';
|
||||
|
||||
const NavigationBar = () => {
|
||||
const intl = useIntl();
|
||||
const courseId = useCourseId();
|
||||
const showLearnersTab = useShowLearnersTab();
|
||||
const { courseId } = useContext(DiscussionContext);
|
||||
const location = useLocation();
|
||||
const isTopicsNavActive = Boolean(matchPath({ path: `${Routes.TOPICS.CATEGORY}/*` }, location.pathname));
|
||||
|
||||
const navLinks = useMemo(() => ([
|
||||
{
|
||||
@@ -28,41 +27,31 @@ const NavigationBar = () => {
|
||||
},
|
||||
{
|
||||
route: Routes.TOPICS.ALL,
|
||||
isActive: (match, location) => Boolean(matchPath(location.pathname, { path: Routes.TOPICS.PATH })),
|
||||
labelMessage: messages.allTopics,
|
||||
},
|
||||
{
|
||||
route: Routes.LEARNERS.PATH,
|
||||
labelMessage: messages.learners,
|
||||
},
|
||||
|
||||
]), []);
|
||||
|
||||
useMemo(() => {
|
||||
if (showLearnersTab) {
|
||||
navLinks.push({
|
||||
route: Routes.LEARNERS.PATH,
|
||||
labelMessage: messages.learners,
|
||||
});
|
||||
}
|
||||
}, [showLearnersTab]);
|
||||
console.log('NavigationBar');
|
||||
|
||||
const navLinksList = useMemo(() => (
|
||||
navLinks.map(link => (
|
||||
<Nav.Item key={link.route}>
|
||||
<Nav.Link
|
||||
key={link.route}
|
||||
as={NavLink}
|
||||
to={discussionsPath(link.route, { courseId })}
|
||||
isActive={link.isActive}
|
||||
>
|
||||
{intl.formatMessage(link.labelMessage)}
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
))
|
||||
), [navLinks]);
|
||||
|
||||
return (
|
||||
<Nav variant="pills" className="py-2 nav-button-group">
|
||||
{navLinksList}
|
||||
{navLinks.map(link => (
|
||||
<Nav.Item key={link.route}>
|
||||
<Nav.Link
|
||||
key={link.route}
|
||||
as={NavLink}
|
||||
to={discussionsPath(link.route, { courseId })()}
|
||||
className={isTopicsNavActive && link.route === Routes.TOPICS.ALL && 'active'}
|
||||
>
|
||||
{intl.formatMessage(link.labelMessage)}
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
))}
|
||||
</Nav>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withConditionalInContextRendering(NavigationBar, false));
|
||||
export default React.memo(NavigationBar);
|
||||
|
||||
@@ -2,14 +2,16 @@ import React, {
|
||||
Suspense, useCallback, useContext, useEffect, useState,
|
||||
} from 'react';
|
||||
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Icon, IconButton } from '@edx/paragon';
|
||||
import { ArrowBack } from '@edx/paragon/icons';
|
||||
|
||||
import Spinner from '../../components/Spinner';
|
||||
import { EndorsementStatus, PostsPages, ThreadType } from '../../data/constants';
|
||||
import {
|
||||
EndorsementStatus, PostsPages, ThreadType,
|
||||
} from '../../data/constants';
|
||||
import { useDispatchWithState } from '../../data/hooks';
|
||||
import { DiscussionContext } from '../common/context';
|
||||
import { useIsOnDesktop } from '../data/hooks';
|
||||
@@ -27,7 +29,7 @@ const CommentsView = React.lazy(() => import('./comments/CommentsView'));
|
||||
|
||||
const PostCommentsView = () => {
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const isOnDesktop = useIsOnDesktop();
|
||||
const [addingResponse, setAddingResponse] = useState(false);
|
||||
@@ -37,6 +39,9 @@ const PostCommentsView = () => {
|
||||
} = useContext(DiscussionContext);
|
||||
const commentsCount = useCommentsCount(postId);
|
||||
const { closed, id: threadId, type } = usePost(postId);
|
||||
const redirectUrl = discussionsPath(PostsPages[page], {
|
||||
courseId, learnerUsername, category, topicId,
|
||||
})(location);
|
||||
|
||||
useEffect(() => {
|
||||
if (!threadId) {
|
||||
@@ -89,9 +94,7 @@ const PostCommentsView = () => {
|
||||
variant="plain"
|
||||
className="px-0 line-height-24 py-0 my-1.5 border-0 font-weight-normal font-style text-primary-500"
|
||||
iconBefore={ArrowBack}
|
||||
onClick={() => history.push(discussionsPath(PostsPages[page], {
|
||||
courseId, learnerUsername, category, topicId,
|
||||
})(location))}
|
||||
onClick={() => navigate({ ...redirectUrl })}
|
||||
size="sm"
|
||||
>
|
||||
{intl.formatMessage(messages.backAlt)}
|
||||
@@ -106,9 +109,7 @@ const PostCommentsView = () => {
|
||||
style={{ padding: '18px' }}
|
||||
size="inline"
|
||||
className="ml-4 mt-4"
|
||||
onClick={() => history.push(discussionsPath(PostsPages[page], {
|
||||
courseId, learnerUsername, category, topicId,
|
||||
})(location))}
|
||||
onClick={() => navigate({ ...redirectUrl })}
|
||||
alt={intl.formatMessage(messages.backAlt)}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -3,7 +3,9 @@ import {
|
||||
} from '@testing-library/react';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { MemoryRouter, Route } from 'react-router';
|
||||
import {
|
||||
MemoryRouter, Route, Routes, useLocation,
|
||||
} from 'react-router-dom';
|
||||
import { Factory } from 'rosie';
|
||||
|
||||
import { camelCaseObject, initializeMockApp } from '@edx/frontend-platform';
|
||||
@@ -89,11 +91,10 @@ async function getThreadAPIResponse(attr = null) {
|
||||
await executeThunk(fetchThread(discussionPostId), store.dispatch, store.getState);
|
||||
}
|
||||
|
||||
async function setupCourseConfig(reasonCodesEnabled = true) {
|
||||
async function setupCourseConfig() {
|
||||
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`).reply(200, {
|
||||
has_moderation_privileges: true,
|
||||
isPostingEnabled: true,
|
||||
reason_codes_enabled: reasonCodesEnabled,
|
||||
editReasons: [
|
||||
{ code: 'reason-1', label: 'reason 1' },
|
||||
{ code: 'reason-2', label: 'reason 2' },
|
||||
@@ -107,22 +108,28 @@ async function setupCourseConfig(reasonCodesEnabled = true) {
|
||||
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
|
||||
}
|
||||
|
||||
function renderComponent(postId, isClosed = false) {
|
||||
const LocationComponent = () => {
|
||||
testLocation = useLocation();
|
||||
return null;
|
||||
};
|
||||
|
||||
function renderComponent(postId, isClosed = false, page = 'posts', path = `/${courseId}/posts/${postId}`) {
|
||||
const wrapper = render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<AppProvider store={store} wrapWithRouter={false}>
|
||||
<DiscussionContext.Provider
|
||||
value={{ courseId, postId, isClosed }}
|
||||
value={{
|
||||
courseId, postId, page, isClosed, topicId: 'topic-id',
|
||||
}}
|
||||
>
|
||||
<MemoryRouter initialEntries={[`/${courseId}/posts/${postId}`]}>
|
||||
<MemoryRouter initialEntries={[path]}>
|
||||
<DiscussionContent />
|
||||
<Route
|
||||
path="*"
|
||||
render={({ location }) => {
|
||||
testLocation = location;
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
<Routes>
|
||||
<Route
|
||||
path="*"
|
||||
element={<LocationComponent />}
|
||||
/>
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</DiscussionContext.Provider>
|
||||
</AppProvider>
|
||||
@@ -392,12 +399,12 @@ describe('ThreadView', () => {
|
||||
assertLastUpdateData({ edit_reason_code: 'reason-1' });
|
||||
});
|
||||
|
||||
it('should close the post directly if reason codes are not enabled', async () => {
|
||||
await setupCourseConfig(false);
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
it('should reopen the post', async () => {
|
||||
await setupCourseConfig();
|
||||
renderComponent(closedPostId);
|
||||
|
||||
const post = await screen.findByTestId('post-thread-1');
|
||||
const hoverCard = within(post).getByTestId('hover-card-thread-1');
|
||||
const post = screen.getByTestId('post-thread-2');
|
||||
const hoverCard = within(post).getByTestId('hover-card-thread-2');
|
||||
await act(async () => {
|
||||
fireEvent.click(
|
||||
within(hoverCard).getByRole('button', { name: /actions menu/i }),
|
||||
@@ -405,34 +412,12 @@ describe('ThreadView', () => {
|
||||
});
|
||||
expect(screen.queryByRole('dialog', { name: /close post/i })).not.toBeInTheDocument();
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /close/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /reopen/i }));
|
||||
});
|
||||
expect(screen.queryByRole('dialog', { name: /close post/i })).not.toBeInTheDocument();
|
||||
assertLastUpdateData({ closed: true });
|
||||
assertLastUpdateData({ closed: false });
|
||||
});
|
||||
|
||||
it.each([true, false])(
|
||||
'should reopen the post directly when reason codes enabled=%s',
|
||||
async (reasonCodesEnabled) => {
|
||||
await setupCourseConfig(reasonCodesEnabled);
|
||||
renderComponent(closedPostId);
|
||||
|
||||
const post = screen.getByTestId('post-thread-2');
|
||||
const hoverCard = within(post).getByTestId('hover-card-thread-2');
|
||||
await act(async () => {
|
||||
fireEvent.click(
|
||||
within(hoverCard).getByRole('button', { name: /actions menu/i }),
|
||||
);
|
||||
});
|
||||
expect(screen.queryByRole('dialog', { name: /close post/i })).not.toBeInTheDocument();
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /reopen/i }));
|
||||
});
|
||||
expect(screen.queryByRole('dialog', { name: /close post/i })).not.toBeInTheDocument();
|
||||
assertLastUpdateData({ closed: false });
|
||||
},
|
||||
);
|
||||
|
||||
it('should show the editor if the post is edited', async () => {
|
||||
await setupCourseConfig(false);
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
@@ -450,6 +435,28 @@ describe('ThreadView', () => {
|
||||
expect(testLocation.pathname).toBe(`/${courseId}/posts/${discussionPostId}/edit`);
|
||||
});
|
||||
|
||||
it('should show the editor if the post is edited on topics page', async () => {
|
||||
await setupCourseConfig(false);
|
||||
await waitFor(() => renderComponent(
|
||||
discussionPostId,
|
||||
false,
|
||||
'topics',
|
||||
`/${courseId}/topics/topic-id/posts/${discussionPostId}`,
|
||||
));
|
||||
|
||||
const post = await screen.findByTestId('post-thread-1');
|
||||
const hoverCard = within(post).getByTestId('hover-card-thread-1');
|
||||
await act(async () => {
|
||||
fireEvent.click(
|
||||
within(hoverCard).getByRole('button', { name: /actions menu/i }),
|
||||
);
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /edit/i }));
|
||||
});
|
||||
expect(testLocation.pathname).toBe(`/${courseId}/topics/topic-id/posts/${discussionPostId}/edit`);
|
||||
});
|
||||
|
||||
it('should allow pinning the post', async () => {
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
const post = await screen.findByTestId('post-thread-1');
|
||||
@@ -465,6 +472,20 @@ describe('ThreadView', () => {
|
||||
assertLastUpdateData({ pinned: false });
|
||||
});
|
||||
|
||||
it('should allow copying a link to the post', async () => {
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
const post = await screen.findByTestId('post-thread-1');
|
||||
const hoverCard = within(post).getByTestId('hover-card-thread-1');
|
||||
Object.assign(navigator, { clipboard: { writeText: jest.fn() } });
|
||||
await act(async () => {
|
||||
fireEvent.click(within(hoverCard).getByRole('button', { name: /actions menu/i }));
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.click(within(hoverCard).getByRole('button', { name: /copy link/i }));
|
||||
});
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(`http://localhost/${courseId}/posts/${discussionPostId}`);
|
||||
});
|
||||
|
||||
it('should allow reporting the post', async () => {
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
const post = await screen.findByTestId('post-thread-1');
|
||||
|
||||
@@ -36,7 +36,7 @@ const CommentsView = ({ endorsed }) => {
|
||||
|
||||
const handleDefinition = useCallback((message, commentsLength) => (
|
||||
<div
|
||||
className="mx-4 my-14px text-gray-700 font-style"
|
||||
className="comment-line mx-4 my-14px text-gray-700 font-style"
|
||||
role="heading"
|
||||
aria-level="2"
|
||||
>
|
||||
|
||||
@@ -40,10 +40,10 @@ const CommentEditor = ({
|
||||
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
|
||||
const userIsGroupTa = useSelector(selectUserIsGroupTa);
|
||||
const userIsStaff = useSelector(selectUserIsStaff);
|
||||
const { reasonCodesEnabled, editReasons } = useSelector(selectModerationSettings);
|
||||
const { editReasons } = useSelector(selectModerationSettings);
|
||||
const [submitting, dispatch] = useDispatchWithState();
|
||||
|
||||
const canDisplayEditReason = (reasonCodesEnabled && edit
|
||||
const canDisplayEditReason = (edit
|
||||
&& (userHasModerationPrivileges || userIsGroupTa || userIsStaff)
|
||||
&& author !== authenticatedUser.username
|
||||
);
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { selectAllThreadsIds } from './data/selectors';
|
||||
import PostsList from './PostsList';
|
||||
|
||||
const AllPostsList = () => {
|
||||
const postsIds = useSelector(selectAllThreadsIds);
|
||||
|
||||
return <PostsList postsIds={postsIds} topicsIds={null} />;
|
||||
};
|
||||
|
||||
export default memo(AllPostsList);
|
||||
@@ -1,22 +0,0 @@
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { selectCurrentCategoryGrouping, selectTopicsUnderCategory } from '../../data/selectors';
|
||||
import { useCategory, useEnableInContextSidebar } from '../data/hooks';
|
||||
import { selectAllThreadsIds, selectTopicThreadsIds } from './data/selectors';
|
||||
import PostsList from './PostsList';
|
||||
|
||||
const PostsView = () => {
|
||||
const category = useCategory();
|
||||
const enableInContextSidebar = useEnableInContextSidebar();
|
||||
|
||||
const groupedCategory = useSelector(selectCurrentCategoryGrouping)(category);
|
||||
// If grouping at subsection is enabled, only apply it when browsing discussions in context in the learning MFE.
|
||||
const topicIds = useSelector(selectTopicsUnderCategory)(enableInContextSidebar ? groupedCategory : category);
|
||||
const postsIds = useSelector(enableInContextSidebar ? selectAllThreadsIds : selectTopicThreadsIds(topicIds));
|
||||
|
||||
return <PostsList postsIds={postsIds} topicsIds={topicIds} />;
|
||||
};
|
||||
|
||||
export default memo(PostsView);
|
||||
@@ -1,90 +0,0 @@
|
||||
import React, {
|
||||
useCallback, useContext, useEffect, useMemo,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { Button, Spinner } from '@edx/paragon';
|
||||
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import { useCourseId, useCurrentPage } from '../data/hooks';
|
||||
import { selectConfigLoadingStatus, selectUserHasModerationPrivileges, selectUserIsStaff } from '../data/selectors';
|
||||
import { fetchUserPosts } from '../learners/data/thunks';
|
||||
import messages from '../messages';
|
||||
import { usePostList } from './data/hooks';
|
||||
import {
|
||||
selectThreadFilters, selectThreadNextPage, selectThreadSorting, threadsLoadingStatus,
|
||||
} from './data/selectors';
|
||||
import { fetchThreads } from './data/thunks';
|
||||
import NoResults from './NoResults';
|
||||
import { PostLink } from './post';
|
||||
|
||||
const PostsList = ({
|
||||
postsIds, topicsIds, isTopicTab, parentIsLoading,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const { authenticatedUser } = useContext(AppContext);
|
||||
const page = useCurrentPage();
|
||||
const courseId = useCourseId();
|
||||
const loadingStatus = useSelector(threadsLoadingStatus());
|
||||
const orderBy = useSelector(selectThreadSorting());
|
||||
const filters = useSelector(selectThreadFilters());
|
||||
const nextPage = useSelector(selectThreadNextPage());
|
||||
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
|
||||
const userIsStaff = useSelector(selectUserIsStaff);
|
||||
const configStatus = useSelector(selectConfigLoadingStatus);
|
||||
const sortedPostsIds = usePostList(postsIds);
|
||||
const showOwnPosts = page === 'my-posts';
|
||||
|
||||
const loadThreads = useCallback((topicIds, pageNum = undefined, isFilterChanged = false) => {
|
||||
const params = {
|
||||
orderBy,
|
||||
filters,
|
||||
page: pageNum,
|
||||
author: showOwnPosts ? authenticatedUser.username : null,
|
||||
countFlagged: (userHasModerationPrivileges || userIsStaff) || undefined,
|
||||
topicIds,
|
||||
isFilterChanged,
|
||||
};
|
||||
|
||||
if (showOwnPosts && filters.search === '') {
|
||||
dispatch(fetchUserPosts(courseId, params));
|
||||
} else {
|
||||
dispatch(fetchThreads(courseId, params));
|
||||
}
|
||||
}, [courseId, orderBy, filters, showOwnPosts, authenticatedUser.username, userHasModerationPrivileges, userIsStaff]);
|
||||
|
||||
return (
|
||||
loadingStatus === RequestStatus.IN_PROGRESS || parentIsLoading ? (
|
||||
<div className="d-flex justify-content-center p-4 mx-auto my-auto">
|
||||
<Spinner animation="border" variant="primary" size="lg" />
|
||||
</div>
|
||||
) : (
|
||||
nextPage && loadingStatus === RequestStatus.SUCCESSFUL && (
|
||||
<Button onClick={() => loadThreads(topicsIds, nextPage)} variant="primary" size="md">
|
||||
{intl.formatMessage(messages.loadMorePosts)}
|
||||
</Button>
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
PostsList.propTypes = {
|
||||
postsIds: PropTypes.arrayOf(PropTypes.string),
|
||||
topicsIds: PropTypes.arrayOf(PropTypes.string),
|
||||
isTopicTab: PropTypes.bool,
|
||||
parentIsLoading: PropTypes.bool,
|
||||
};
|
||||
|
||||
PostsList.defaultProps = {
|
||||
postsIds: [],
|
||||
topicsIds: undefined,
|
||||
isTopicTab: false,
|
||||
parentIsLoading: undefined,
|
||||
};
|
||||
|
||||
export default React.memo(PostsList);
|
||||
@@ -21,7 +21,7 @@ function renderComponent(location = `/${courseId}/`) {
|
||||
return render(
|
||||
<IntlProvider locale="en">
|
||||
<ResponsiveContext.Provider value={{ width: 1280 }}>
|
||||
<AppProvider store={store}>
|
||||
<AppProvider store={store} wrapWithRouter={false}>
|
||||
<MemoryRouter initialEntries={[location]}>
|
||||
<NoResults />
|
||||
</MemoryRouter>
|
||||
|
||||
@@ -10,7 +10,7 @@ import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { Button, Spinner } from '@edx/paragon';
|
||||
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import { useCourseId, useCurrentPage } from '../data/hooks';
|
||||
import { DiscussionContext } from '../common/context';
|
||||
import { selectConfigLoadingStatus, selectUserHasModerationPrivileges, selectUserIsStaff } from '../data/selectors';
|
||||
import { fetchUserPosts } from '../learners/data/thunks';
|
||||
import messages from '../messages';
|
||||
@@ -28,8 +28,7 @@ const PostsList = ({
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const { authenticatedUser } = useContext(AppContext);
|
||||
const page = useCurrentPage();
|
||||
const courseId = useCourseId();
|
||||
const { courseId, page } = useContext(DiscussionContext);
|
||||
const loadingStatus = useSelector(threadsLoadingStatus());
|
||||
const orderBy = useSelector(selectThreadSorting());
|
||||
const filters = useSelector(selectThreadFilters());
|
||||
@@ -81,8 +80,6 @@ const PostsList = ({
|
||||
))
|
||||
), [sortedPostsIds]);
|
||||
|
||||
console.log('sortedPostsIds', sortedPostsIds, loadingStatus === RequestStatus.IN_PROGRESS || parentIsLoading);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!parentIsLoading && postInstances}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import React, { memo, useCallback } from 'react';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import SearchInfo from '../../components/SearchInfo';
|
||||
import { setSearchQuery } from './data/slices';
|
||||
|
||||
const PostsSearchInfo = () => {
|
||||
const dispatch = useDispatch();
|
||||
const searchString = useSelector(({ threads }) => threads.filters.search);
|
||||
const resultsFound = useSelector(({ threads }) => threads.totalThreads);
|
||||
const textSearchRewrite = useSelector(({ threads }) => threads.textSearchRewrite);
|
||||
const loadingStatus = useSelector(({ threads }) => threads.status);
|
||||
|
||||
const handleOnClear = useCallback(() => {
|
||||
dispatch(setSearchQuery(''));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
searchString && (
|
||||
<SearchInfo
|
||||
count={resultsFound}
|
||||
text={searchString}
|
||||
loadingStatus={loadingStatus}
|
||||
onClear={handleOnClear}
|
||||
textSearchRewrite={textSearchRewrite}
|
||||
/>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(PostsSearchInfo);
|
||||
@@ -1,30 +1,103 @@
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import React, {
|
||||
useCallback, useContext, useEffect, useMemo,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useCategory, useTopicId } from '../data/hooks';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import SearchInfo from '../../components/SearchInfo';
|
||||
import { selectCurrentCategoryGrouping, selectTopicsUnderCategory } from '../../data/selectors';
|
||||
import { DiscussionContext } from '../common/context';
|
||||
import { selectEnableInContext } from '../data/selectors';
|
||||
import { selectTopics as selectInContextTopics } from '../in-context-topics/data/selectors';
|
||||
import { fetchCourseTopicsV3 } from '../in-context-topics/data/thunks';
|
||||
import { selectTopics } from '../topics/data/selectors';
|
||||
import { fetchCourseTopics } from '../topics/data/thunks';
|
||||
import { handleKeyDown } from '../utils';
|
||||
import { selectAllThreadsIds, selectTopicThreadsIds } from './data/selectors';
|
||||
import { setSearchQuery } from './data/slices';
|
||||
import PostFilterBar from './post-filter-bar/PostFilterBar';
|
||||
import AllPostsList from './AllPostsList';
|
||||
import CategoryPostsList from './CategoryPostsList';
|
||||
import PostsSearchInfo from './PostsSearchInfo';
|
||||
import TopicPostsList from './TopicPostsList';
|
||||
import PostsList from './PostsList';
|
||||
|
||||
const AllPostsList = () => {
|
||||
const postsIds = useSelector(selectAllThreadsIds);
|
||||
|
||||
return <PostsList postsIds={postsIds} topicsIds={null} />;
|
||||
};
|
||||
|
||||
const TopicPostsList = React.memo(({ topicId }) => {
|
||||
const postsIds = useSelector(selectTopicThreadsIds([topicId]));
|
||||
|
||||
return <PostsList postsIds={postsIds} topicsIds={[topicId]} isTopicTab />;
|
||||
});
|
||||
|
||||
TopicPostsList.propTypes = {
|
||||
topicId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const CategoryPostsList = React.memo(({ category }) => {
|
||||
const { enableInContextSidebar } = useContext(DiscussionContext);
|
||||
const groupedCategory = useSelector(selectCurrentCategoryGrouping)(category);
|
||||
// If grouping at subsection is enabled, only apply it when browsing discussions in context in the learning MFE.
|
||||
const topicIds = useSelector(selectTopicsUnderCategory)(enableInContextSidebar ? groupedCategory : category);
|
||||
const postsIds = useSelector(enableInContextSidebar ? selectAllThreadsIds : selectTopicThreadsIds(topicIds));
|
||||
|
||||
return <PostsList postsIds={postsIds} topicsIds={topicIds} />;
|
||||
});
|
||||
|
||||
CategoryPostsList.propTypes = {
|
||||
category: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const PostsView = () => {
|
||||
const topicId = useTopicId();
|
||||
const category = useCategory();
|
||||
const {
|
||||
topicId,
|
||||
category,
|
||||
courseId,
|
||||
enableInContextSidebar,
|
||||
} = useContext(DiscussionContext);
|
||||
const dispatch = useDispatch();
|
||||
const enableInContext = useSelector(selectEnableInContext);
|
||||
const searchString = useSelector(({ threads }) => threads.filters.search);
|
||||
const resultsFound = useSelector(({ threads }) => threads.totalThreads);
|
||||
const textSearchRewrite = useSelector(({ threads }) => threads.textSearchRewrite);
|
||||
const loadingStatus = useSelector(({ threads }) => threads.status);
|
||||
const topics = useSelector(enableInContext ? selectInContextTopics : selectTopics);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEmpty(topics)) {
|
||||
dispatch((enableInContext || enableInContextSidebar)
|
||||
? fetchCourseTopicsV3(courseId)
|
||||
: fetchCourseTopics(courseId));
|
||||
}
|
||||
}, [topics]);
|
||||
|
||||
const handleOnClear = useCallback(() => {
|
||||
dispatch(setSearchQuery(''));
|
||||
}, []);
|
||||
|
||||
const postsListComponent = useMemo(() => {
|
||||
if (topicId) {
|
||||
return <TopicPostsList />;
|
||||
return <TopicPostsList topicId={topicId} />;
|
||||
}
|
||||
if (category) {
|
||||
return <CategoryPostsList />;
|
||||
return <CategoryPostsList category={category} />;
|
||||
}
|
||||
return <AllPostsList />;
|
||||
}, [topicId, category]);
|
||||
|
||||
return (
|
||||
<div className="discussion-posts d-flex flex-column h-100">
|
||||
<PostsSearchInfo />
|
||||
{searchString && (
|
||||
<SearchInfo
|
||||
count={resultsFound}
|
||||
text={searchString}
|
||||
loadingStatus={loadingStatus}
|
||||
onClear={handleOnClear}
|
||||
textSearchRewrite={textSearchRewrite}
|
||||
/>
|
||||
)}
|
||||
<PostFilterBar />
|
||||
<div className="border-bottom border-light-400" />
|
||||
<div className="list-group list-group-flush flex-fill" role="list" onKeyDown={e => handleKeyDown(e)}>
|
||||
@@ -34,4 +107,4 @@ const PostsView = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(PostsView);
|
||||
export default PostsView;
|
||||
|
||||
@@ -5,15 +5,15 @@ import MockAdapter from 'axios-mock-adapter';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import {
|
||||
generatePath, MemoryRouter, Route, Switch,
|
||||
} from 'react-router';
|
||||
generatePath, MemoryRouter, Route, Routes,
|
||||
} from 'react-router-dom';
|
||||
import { Factory } from 'rosie';
|
||||
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import { getApiBaseUrl, Routes, ThreadType } from '../../data/constants';
|
||||
import { getApiBaseUrl, Routes as ROUTES, ThreadType } from '../../data/constants';
|
||||
import { initializeStore } from '../../store';
|
||||
import { executeThunk } from '../../test-utils';
|
||||
import { getCohortsApiUrl } from '../cohorts/data/api';
|
||||
@@ -39,24 +39,24 @@ const username = 'abc123';
|
||||
async function renderComponent({
|
||||
postId, topicId, category, myPosts, enableInContextSidebar = false,
|
||||
} = { myPosts: false }) {
|
||||
let path = generatePath(Routes.POSTS.ALL_POSTS, { courseId });
|
||||
let page;
|
||||
let path = generatePath(ROUTES.POSTS.ALL_POSTS, { courseId });
|
||||
let page = 'posts';
|
||||
if (postId) {
|
||||
path = generatePath(Routes.POSTS.ALL_POSTS, { courseId, postId });
|
||||
path = generatePath(ROUTES.POSTS.ALL_POSTS, { courseId, postId });
|
||||
page = 'posts';
|
||||
} else if (topicId) {
|
||||
path = generatePath(Routes.POSTS.PATH, { courseId, topicId });
|
||||
page = 'posts';
|
||||
path = generatePath(ROUTES.POSTS.PATH, { courseId, topicId });
|
||||
page = 'topics';
|
||||
} else if (category) {
|
||||
path = generatePath(Routes.TOPICS.CATEGORY, { courseId, category });
|
||||
path = generatePath(ROUTES.TOPICS.CATEGORY, { courseId, category });
|
||||
page = 'category';
|
||||
} else if (myPosts) {
|
||||
path = generatePath(Routes.POSTS.MY_POSTS, { courseId });
|
||||
path = generatePath(ROUTES.POSTS.MY_POSTS, { courseId });
|
||||
page = 'my-posts';
|
||||
}
|
||||
await render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<AppProvider store={store} wrapWithRouter={false}>
|
||||
<MemoryRouter initialEntries={[path]}>
|
||||
<DiscussionContext.Provider value={{
|
||||
courseId,
|
||||
@@ -67,15 +67,18 @@ async function renderComponent({
|
||||
enableInContextSidebar,
|
||||
}}
|
||||
>
|
||||
<Switch>
|
||||
<Route path={Routes.POSTS.MY_POSTS}>
|
||||
<PostsView />
|
||||
</Route>
|
||||
<Route
|
||||
path={[Routes.POSTS.PATH, Routes.POSTS.ALL_POSTS, Routes.TOPICS.CATEGORY]}
|
||||
component={PostsView}
|
||||
/>
|
||||
</Switch>
|
||||
<Routes>
|
||||
{
|
||||
[
|
||||
ROUTES.POSTS.PATH,
|
||||
ROUTES.POSTS.MY_POSTS,
|
||||
ROUTES.POSTS.ALL_POSTS,
|
||||
ROUTES.TOPICS.CATEGORY,
|
||||
].map((route) => (
|
||||
<Route key={route} path={route} element={<PostsView />} />
|
||||
))
|
||||
}
|
||||
</Routes>
|
||||
</DiscussionContext.Provider>
|
||||
</MemoryRouter>
|
||||
</AppProvider>
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import React, { memo, useEffect } from 'react';
|
||||
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { useCourseId, useEnableInContextSidebar, useTopicId } from '../data/hooks';
|
||||
import { selectEnableInContext } from '../data/selectors';
|
||||
import { selectTopics as selectInContextTopics } from '../in-context-topics/data/selectors';
|
||||
import { fetchCourseTopicsV3 } from '../in-context-topics/data/thunks';
|
||||
import { selectTopics } from '../topics/data/selectors';
|
||||
import { fetchCourseTopics } from '../topics/data/thunks';
|
||||
import { selectTopicThreadsIds } from './data/selectors';
|
||||
import PostsList from './PostsList';
|
||||
|
||||
const TopicPostsList = () => {
|
||||
const dispatch = useDispatch();
|
||||
const topicId = useTopicId();
|
||||
const courseId = useCourseId();
|
||||
const enableInContextSidebar = useEnableInContextSidebar();
|
||||
const enableInContext = useSelector(selectEnableInContext);
|
||||
const postsIds = useSelector(selectTopicThreadsIds([topicId]));
|
||||
const topics = useSelector(enableInContext ? selectInContextTopics : selectTopics);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEmpty(topics)) {
|
||||
dispatch((enableInContext || enableInContextSidebar)
|
||||
? fetchCourseTopicsV3(courseId)
|
||||
: fetchCourseTopics(courseId));
|
||||
}
|
||||
}, [courseId, topics]);
|
||||
|
||||
return <PostsList postsIds={postsIds} topicsIds={[topicId]} isTopicTab />;
|
||||
};
|
||||
|
||||
export default memo(TopicPostsList);
|
||||
@@ -26,6 +26,7 @@ Factory.define('thread')
|
||||
'type',
|
||||
'voted',
|
||||
'pinned',
|
||||
'copy_link',
|
||||
],
|
||||
author: 'test_user',
|
||||
author_label: 'Staff',
|
||||
|
||||
@@ -7,10 +7,20 @@ import { selectThreadsByIds } from './selectors';
|
||||
|
||||
export const usePostList = (ids) => {
|
||||
const posts = useSelector(selectThreadsByIds(ids));
|
||||
const pinnedPostsIds = [];
|
||||
const unpinnedPostsIds = [];
|
||||
|
||||
const sortedIds = useMemo(() => (
|
||||
[...posts].sort((a, b) => (b.pinned - a.pinned)).map((post) => post.id)
|
||||
), [posts]);
|
||||
const sortedIds = useMemo(() => {
|
||||
posts.forEach((post) => {
|
||||
if (post.pinned) {
|
||||
pinnedPostsIds.push(post.id);
|
||||
} else {
|
||||
unpinnedPostsIds.push(post.id);
|
||||
}
|
||||
});
|
||||
|
||||
return [...pinnedPostsIds, ...unpinnedPostsIds];
|
||||
}, [posts]);
|
||||
|
||||
return sortedIds;
|
||||
};
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@edx/paragon';
|
||||
|
||||
import { RequestStatus } from '../../../data/constants';
|
||||
import { useEnableInContextSidebar, useUserPostingEnabled } from '../../data/hooks';
|
||||
import { selectConfigLoadingStatus } from '../../data/selectors';
|
||||
import { showPostEditor } from '../data';
|
||||
import messages from './messages';
|
||||
|
||||
const AddPostButton = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const loadingStatus = useSelector(selectConfigLoadingStatus);
|
||||
const enableInContextSidebar = useEnableInContextSidebar();
|
||||
const isUserPrivilegedInPostingRestriction = useUserPostingEnabled();
|
||||
|
||||
const handleAddPost = useCallback(() => {
|
||||
dispatch(showPostEditor());
|
||||
}, []);
|
||||
|
||||
return (
|
||||
loadingStatus === RequestStatus.SUCCESSFUL && isUserPrivilegedInPostingRestriction && (
|
||||
<>
|
||||
{!enableInContextSidebar && <div className="border-right border-light-400 mx-3" />}
|
||||
<Button
|
||||
variant={enableInContextSidebar ? 'plain' : 'brand'}
|
||||
className={classNames(
|
||||
'my-0 font-style border-0 line-height-24',
|
||||
{ 'px-3 py-10px border-0': enableInContextSidebar },
|
||||
)}
|
||||
onClick={handleAddPost}
|
||||
size={enableInContextSidebar ? 'md' : 'sm'}
|
||||
>
|
||||
{intl.formatMessage(messages.addAPost)}
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default AddPostButton;
|
||||
@@ -1,47 +1,80 @@
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import React, { useCallback, useContext } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Icon, IconButton } from '@edx/paragon';
|
||||
import {
|
||||
Button, Icon, IconButton,
|
||||
} from '@edx/paragon';
|
||||
import { Close } from '@edx/paragon/icons';
|
||||
|
||||
import { useEnableInContextSidebar } from '../../data/hooks';
|
||||
import Search from '../../../components/Search';
|
||||
import { RequestStatus } from '../../../data/constants';
|
||||
import { DiscussionContext } from '../../common/context';
|
||||
import { useUserPostingEnabled } from '../../data/hooks';
|
||||
import { selectConfigLoadingStatus, selectEnableInContext } from '../../data/selectors';
|
||||
import { TopicSearchBar as IncontextSearch } from '../../in-context-topics/topic-search';
|
||||
import { postMessageToParent } from '../../utils';
|
||||
import AddPostButton from './AddPostButton';
|
||||
import { showPostEditor } from '../data';
|
||||
import messages from './messages';
|
||||
import SearchField from './SearchField';
|
||||
|
||||
import './actionBar.scss';
|
||||
|
||||
const PostActionsBar = () => {
|
||||
const intl = useIntl();
|
||||
const enableInContextSidebar = useEnableInContextSidebar();
|
||||
const dispatch = useDispatch();
|
||||
const loadingStatus = useSelector(selectConfigLoadingStatus);
|
||||
const enableInContext = useSelector(selectEnableInContext);
|
||||
const isUserPrivilegedInPostingRestriction = useUserPostingEnabled();
|
||||
const { enableInContextSidebar, page } = useContext(DiscussionContext);
|
||||
|
||||
const handleCloseInContext = useCallback(() => {
|
||||
postMessageToParent('learning.events.sidebar.close');
|
||||
}, []);
|
||||
|
||||
const handleAddPost = useCallback(() => {
|
||||
dispatch(showPostEditor());
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={classNames('d-flex justify-content-end flex-grow-1', { 'py-1': !enableInContextSidebar })}>
|
||||
<SearchField />
|
||||
{!enableInContextSidebar && (
|
||||
(enableInContext && ['topics', 'category'].includes(page))
|
||||
? <IncontextSearch />
|
||||
: <Search />
|
||||
)}
|
||||
{enableInContextSidebar && (
|
||||
<h4 className="d-flex flex-grow-1 font-weight-bold font-style my-0 py-10px align-self-center">
|
||||
{intl.formatMessage(messages.title)}
|
||||
</h4>
|
||||
)}
|
||||
<AddPostButton />
|
||||
{loadingStatus === RequestStatus.SUCCESSFUL && isUserPrivilegedInPostingRestriction && (
|
||||
<>
|
||||
{!enableInContextSidebar && <div className="border-right border-light-400 mx-3" />}
|
||||
<Button
|
||||
variant={enableInContextSidebar ? 'plain' : 'brand'}
|
||||
className={classNames(
|
||||
'my-0 font-style border-0 line-height-24',
|
||||
{ 'px-3 py-10px border-0': enableInContextSidebar },
|
||||
)}
|
||||
onClick={handleAddPost}
|
||||
size={enableInContextSidebar ? 'md' : 'sm'}
|
||||
>
|
||||
{intl.formatMessage(messages.addAPost)}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{enableInContextSidebar && (
|
||||
<>
|
||||
<div className="border-right border-light-300 mr-3 ml-1.5 my-10px" />
|
||||
<div className="justify-content-center mt-2.5 mx-3px">
|
||||
<div className="border-right border-light-300 mr-2 my-10px" />
|
||||
<div className="d-flex align-items-center justify-content-center">
|
||||
<IconButton
|
||||
src={Close}
|
||||
size="sm"
|
||||
iconAs={Icon}
|
||||
onClick={handleCloseInContext}
|
||||
alt={intl.formatMessage(messages.close)}
|
||||
iconClassNames="spinner-dimensions"
|
||||
className="spinner-dimensions"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
@@ -50,4 +83,4 @@ const PostActionsBar = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(PostActionsBar);
|
||||
export default PostActionsBar;
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import Search from '../../../components/Search';
|
||||
import withConditionalInContextRendering from '../../common/withConditionalInContextRendering';
|
||||
import { useCurrentPage } from '../../data/hooks';
|
||||
import { selectEnableInContext } from '../../data/selectors';
|
||||
import { TopicSearchBar as IncontextSearch } from '../../in-context-topics/topic-search';
|
||||
|
||||
const SearchField = () => {
|
||||
const enableInContext = useSelector(selectEnableInContext);
|
||||
const page = useCurrentPage();
|
||||
|
||||
return (
|
||||
enableInContext && ['topics', 'category'].includes(page) ? <IncontextSearch /> : <Search />
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withConditionalInContextRendering(SearchField, false));
|
||||
@@ -1,3 +1,4 @@
|
||||
.small-font {
|
||||
font-size: .875rem !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import PropTypes from 'prop-types';
|
||||
import { Formik } from 'formik';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useHistory, useLocation, useParams } from 'react-router-dom';
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
@@ -55,7 +55,7 @@ const PostEditor = ({
|
||||
editExisting,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
const editorRef = useRef(null);
|
||||
@@ -75,12 +75,12 @@ const PostEditor = ({
|
||||
const userIsGroupTa = useSelector(selectUserIsGroupTa);
|
||||
const settings = useSelector(selectDivisionSettings);
|
||||
const { allowAnonymous, allowAnonymousToPeers } = useSelector(selectAnonymousPostingConfig);
|
||||
const { reasonCodesEnabled, editReasons } = useSelector(selectModerationSettings);
|
||||
const { editReasons } = useSelector(selectModerationSettings);
|
||||
const userIsStaff = useSelector(selectUserIsStaff);
|
||||
const archivedTopics = useSelector(selectArchivedTopics);
|
||||
const postEditorId = `post-editor-${editExisting ? postId : 'new'}`;
|
||||
|
||||
const canDisplayEditReason = (reasonCodesEnabled && editExisting
|
||||
const canDisplayEditReason = (editExisting
|
||||
&& (userHasModerationPrivileges || userIsGroupTa || userIsStaff)
|
||||
&& post?.author !== authenticatedUser.username
|
||||
);
|
||||
@@ -126,7 +126,7 @@ const PostEditor = ({
|
||||
learnerUsername: post?.author,
|
||||
category,
|
||||
})(location);
|
||||
history.push(newLocation);
|
||||
navigate({ ...newLocation });
|
||||
}
|
||||
dispatch(hidePostEditor());
|
||||
}, [postId, topicId, post?.author, category, editExisting, commentsPagePath, location]);
|
||||
|
||||
@@ -7,14 +7,14 @@ import userEvent from '@testing-library/user-event';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { MemoryRouter, Route } from 'react-router';
|
||||
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
||||
import { Factory } from 'rosie';
|
||||
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import { getApiBaseUrl, Routes } from '../../../data/constants';
|
||||
import { getApiBaseUrl, Routes as ROUTES } from '../../../data/constants';
|
||||
import { initializeStore } from '../../../store';
|
||||
import { executeThunk } from '../../../test-utils';
|
||||
import { getCohortsApiUrl } from '../../cohorts/data/api';
|
||||
@@ -37,17 +37,19 @@ let axiosMock;
|
||||
let container;
|
||||
|
||||
async function renderComponent(editExisting = false, location = `/${courseId}/posts/`) {
|
||||
const path = editExisting ? Routes.POSTS.EDIT_POST : Routes.POSTS.NEW_POSTS;
|
||||
const paths = editExisting ? ROUTES.POSTS.EDIT_POST : [ROUTES.POSTS.NEW_POST];
|
||||
const wrapper = await render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<AppProvider store={store} wrapWithRouter={false}>
|
||||
<DiscussionContext.Provider
|
||||
value={{ courseId, category: null }}
|
||||
>
|
||||
<MemoryRouter initialEntries={[location]}>
|
||||
<Route path={path}>
|
||||
<PostEditor editExisting={editExisting} />
|
||||
</Route>
|
||||
<Routes>
|
||||
{paths.map((path) => (
|
||||
<Route path={path} element={<PostEditor editExisting={editExisting} />} />
|
||||
))}
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</DiscussionContext.Provider>
|
||||
</AppProvider>
|
||||
@@ -266,7 +268,18 @@ describe('PostEditor', () => {
|
||||
|
||||
test('cancel posting of existing post', async () => {
|
||||
const threadId = 'thread-1';
|
||||
await setupData();
|
||||
await setupData({
|
||||
editReasons: [
|
||||
{
|
||||
code: 'reason-1',
|
||||
label: 'Reason 1',
|
||||
},
|
||||
{
|
||||
code: 'reason-2',
|
||||
label: 'Reason 2',
|
||||
},
|
||||
],
|
||||
});
|
||||
await act(async () => {
|
||||
axiosMock.onGet(`${threadsApiUrl}${threadId}/`).reply(200, Factory.build('thread'));
|
||||
await executeThunk(fetchThread(threadId), store.dispatch, store.getState);
|
||||
@@ -292,7 +305,6 @@ describe('PostEditor', () => {
|
||||
config: {
|
||||
provider: 'legacy',
|
||||
hasModerationPrivileges: true,
|
||||
reasonCodesEnabled: true,
|
||||
editReasons: [
|
||||
{
|
||||
code: 'reason-1',
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Form, Icon } from '@edx/paragon';
|
||||
import { Check } from '@edx/paragon/icons';
|
||||
|
||||
const ActionItem = ({
|
||||
id, label, value, selected,
|
||||
}) => (
|
||||
<label
|
||||
htmlFor={id}
|
||||
style={{ cursor: 'pointer' }}
|
||||
className="focus border-bottom-0 d-flex align-items-center w-100 py-2 m-0 font-weight-500 filter-menu"
|
||||
data-testid={value === selected ? 'selected' : null}
|
||||
aria-checked={value === selected}
|
||||
tabIndex={value === selected ? '0' : '-1'}
|
||||
>
|
||||
<Icon src={Check} className={classNames('text-success mr-2', { invisible: value !== selected })} />
|
||||
<Form.Radio id={id} className="sr-only sr-only-focusable" value={value} tabIndex="0">
|
||||
{label}
|
||||
</Form.Radio>
|
||||
<span aria-hidden className="text-truncate">
|
||||
{label}
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
|
||||
ActionItem.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
selected: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default React.memo(ActionItem);
|
||||
@@ -1,81 +0,0 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { capitalize, isEmpty, toString } from 'lodash';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { Form, Spinner } from '@edx/paragon';
|
||||
|
||||
import { RequestStatus } from '../../../data/constants';
|
||||
import { selectCourseCohorts } from '../../cohorts/data/selectors';
|
||||
import { fetchCourseCohorts } from '../../cohorts/data/thunks';
|
||||
import { useCourseId } from '../../data/hooks';
|
||||
import { selectUserHasModerationPrivileges } from '../../data/selectors';
|
||||
import { selectThreadFilters } from '../data/selectors';
|
||||
import ActionItem from './ActionItem';
|
||||
import withFilterHandleChange from './withFilterHandleChange';
|
||||
|
||||
const CohortFilters = ({ handleSortFilterChange }) => {
|
||||
const dispatch = useDispatch();
|
||||
const courseId = useCourseId();
|
||||
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
|
||||
const currentFilters = useSelector(selectThreadFilters());
|
||||
const { status } = useSelector(state => state.cohorts);
|
||||
const cohorts = useSelector(selectCourseCohorts);
|
||||
|
||||
useEffect(() => {
|
||||
if (userHasModerationPrivileges && isEmpty(cohorts)) {
|
||||
dispatch(fetchCourseCohorts(courseId));
|
||||
}
|
||||
}, [userHasModerationPrivileges]);
|
||||
|
||||
const cohortsMenu = useMemo(() => (
|
||||
<>
|
||||
<ActionItem
|
||||
id="all-groups"
|
||||
label="All groups"
|
||||
value=""
|
||||
selected={currentFilters.cohort}
|
||||
/>
|
||||
{cohorts.map(cohort => (
|
||||
<ActionItem
|
||||
key={cohort.id}
|
||||
id={toString(cohort.id)}
|
||||
label={capitalize(cohort.name)}
|
||||
value={toString(cohort.id)}
|
||||
selected={currentFilters.cohort}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
), [cohorts, currentFilters.cohort]);
|
||||
|
||||
return (
|
||||
userHasModerationPrivileges && (
|
||||
<>
|
||||
<div className="border-bottom my-2" />
|
||||
{status === RequestStatus.IN_PROGRESS ? (
|
||||
<div className="d-flex justify-content-center p-4">
|
||||
<Spinner animation="border" variant="primary" size="lg" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="d-flex flex-row pt-2">
|
||||
<Form.RadioSet
|
||||
name="cohort"
|
||||
className="d-flex flex-column list-group list-group-flush w-100"
|
||||
value={currentFilters.cohort}
|
||||
onChange={handleSortFilterChange}
|
||||
>
|
||||
{cohortsMenu}
|
||||
</Form.RadioSet>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
CohortFilters.propTypes = {
|
||||
handleSortFilterChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default React.memo(withFilterHandleChange(CohortFilters));
|
||||
@@ -1,69 +0,0 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { capitalize, toString } from 'lodash';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Collapsible, Icon } from '@edx/paragon';
|
||||
import { Tune } from '@edx/paragon/icons';
|
||||
|
||||
import { selectCourseCohorts } from '../../cohorts/data/selectors';
|
||||
import { selectThreadFilters, selectThreadSorting } from '../data/selectors';
|
||||
import messages from './messages';
|
||||
|
||||
const CollapsibleFilter = ({ children }) => {
|
||||
const intl = useIntl();
|
||||
const currentSorting = useSelector(selectThreadSorting());
|
||||
const currentFilters = useSelector(selectThreadFilters());
|
||||
const cohorts = useSelector(selectCourseCohorts);
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
|
||||
const selectedCohort = useMemo(() => (
|
||||
cohorts.find(cohort => (
|
||||
toString(cohort.id) === currentFilters.cohort
|
||||
))
|
||||
), [cohorts, currentFilters.cohort]);
|
||||
|
||||
const handleToggle = useCallback(() => {
|
||||
setOpen(!isOpen);
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<Collapsible.Advanced
|
||||
open={isOpen}
|
||||
onToggle={handleToggle}
|
||||
className="filter-bar collapsible-card-lg border-0"
|
||||
>
|
||||
<Collapsible.Trigger className="collapsible-trigger border-0">
|
||||
<span className="text-primary-500 pr-4 font-style">
|
||||
{intl.formatMessage(messages.sortFilterStatus, {
|
||||
own: false,
|
||||
type: currentFilters.postType,
|
||||
sort: currentSorting,
|
||||
status: currentFilters.status,
|
||||
cohortType: selectedCohort?.name ? 'group' : 'all',
|
||||
cohort: capitalize(selectedCohort?.name),
|
||||
})}
|
||||
</span>
|
||||
<span id="icon-tune">
|
||||
<Collapsible.Visible whenClosed>
|
||||
<Icon src={Tune} />
|
||||
</Collapsible.Visible>
|
||||
<Collapsible.Visible whenOpen>
|
||||
<Icon src={Tune} />
|
||||
</Collapsible.Visible>
|
||||
</span>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Body className="collapsible-body px-4 pb-3 pt-0">
|
||||
{children}
|
||||
</Collapsible.Body>
|
||||
</Collapsible.Advanced>
|
||||
);
|
||||
};
|
||||
|
||||
CollapsibleFilter.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default React.memo(CollapsibleFilter);
|
||||
@@ -1,24 +1,317 @@
|
||||
import React from 'react';
|
||||
import React, {
|
||||
useCallback, useContext, useEffect, useMemo, useState,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Form } from '@edx/paragon';
|
||||
import classNames from 'classnames';
|
||||
import { capitalize, isEmpty, toString } from 'lodash';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import CohortFilters from './CohortFilters';
|
||||
import CollapsibleFilter from './CollapsibleFilter';
|
||||
import PostSortFilters from './PostSortFilters';
|
||||
import PostStatusFilters from './PostStatusFilters';
|
||||
import PostTypeFilters from './PostTypeFilters';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Collapsible, Form, Icon, Spinner,
|
||||
} from '@edx/paragon';
|
||||
import { Check, Tune } from '@edx/paragon/icons';
|
||||
|
||||
const PostFilterBar = () => (
|
||||
<CollapsibleFilter>
|
||||
<Form>
|
||||
<div className="d-flex flex-row py-2 justify-content-between">
|
||||
<PostTypeFilters />
|
||||
<PostStatusFilters />
|
||||
<PostSortFilters />
|
||||
</div>
|
||||
<CohortFilters />
|
||||
</Form>
|
||||
</CollapsibleFilter>
|
||||
);
|
||||
import {
|
||||
PostsStatusFilter, RequestStatus,
|
||||
ThreadOrdering, ThreadType,
|
||||
} from '../../../data/constants';
|
||||
import { selectCourseCohorts } from '../../cohorts/data/selectors';
|
||||
import { fetchCourseCohorts } from '../../cohorts/data/thunks';
|
||||
import { DiscussionContext } from '../../common/context';
|
||||
import { selectUserHasModerationPrivileges, selectUserIsGroupTa } from '../../data/selectors';
|
||||
import {
|
||||
setCohortFilter, setPostsTypeFilter, setSortedBy, setStatusFilter,
|
||||
} from '../data';
|
||||
import { selectThreadFilters, selectThreadSorting } from '../data/selectors';
|
||||
import messages from './messages';
|
||||
|
||||
export const ActionItem = React.memo(({
|
||||
id,
|
||||
label,
|
||||
value,
|
||||
selected,
|
||||
}) => (
|
||||
<label
|
||||
htmlFor={id}
|
||||
className="focus border-bottom-0 d-flex align-items-center w-100 py-2 m-0 font-weight-500 filter-menu"
|
||||
data-testid={value === selected ? 'selected' : null}
|
||||
style={{ cursor: 'pointer' }}
|
||||
aria-checked={value === selected}
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
||||
tabIndex={value === selected ? '0' : '-1'}
|
||||
>
|
||||
<Icon src={Check} className={classNames('text-success mr-2', { invisible: value !== selected })} />
|
||||
<Form.Radio id={id} className="sr-only sr-only-focusable" value={value} tabIndex="0">
|
||||
{label}
|
||||
</Form.Radio>
|
||||
<span aria-hidden className="text-truncate">
|
||||
{label}
|
||||
</span>
|
||||
</label>
|
||||
));
|
||||
|
||||
ActionItem.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
selected: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const PostFilterBar = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const { courseId } = useParams();
|
||||
const { page } = useContext(DiscussionContext);
|
||||
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
|
||||
const userIsGroupTa = useSelector(selectUserIsGroupTa);
|
||||
const currentSorting = useSelector(selectThreadSorting());
|
||||
const currentFilters = useSelector(selectThreadFilters());
|
||||
const { status } = useSelector(state => state.cohorts);
|
||||
const cohorts = useSelector(selectCourseCohorts);
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
|
||||
const selectedCohort = useMemo(() => (
|
||||
cohorts.find(cohort => (
|
||||
toString(cohort.id) === currentFilters.cohort
|
||||
))
|
||||
), [cohorts, currentFilters.cohort]);
|
||||
|
||||
const handleSortFilterChange = useCallback((event) => {
|
||||
const currentType = currentFilters.postType;
|
||||
const currentStatus = currentFilters.status;
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
} = event.currentTarget;
|
||||
const filterContentEventProperties = {
|
||||
statusFilter: currentStatus,
|
||||
threadTypeFilter: currentType,
|
||||
sortFilter: currentSorting,
|
||||
cohortFilter: selectedCohort,
|
||||
triggeredBy: name,
|
||||
};
|
||||
|
||||
if (name === 'type') {
|
||||
dispatch(setPostsTypeFilter(value));
|
||||
if (
|
||||
value === ThreadType.DISCUSSION && currentStatus === PostsStatusFilter.UNANSWERED
|
||||
) {
|
||||
// You can't filter discussions by unanswered
|
||||
dispatch(setStatusFilter(PostsStatusFilter.ALL));
|
||||
}
|
||||
filterContentEventProperties.threadTypeFilter = value;
|
||||
}
|
||||
|
||||
if (name === 'status') {
|
||||
dispatch(setStatusFilter(value));
|
||||
if (value === PostsStatusFilter.UNANSWERED && currentType !== ThreadType.QUESTION) {
|
||||
// You can't filter discussions by unanswered so switch type to questions
|
||||
dispatch(setPostsTypeFilter(ThreadType.QUESTION));
|
||||
}
|
||||
if (value === PostsStatusFilter.UNRESPONDED && currentType !== ThreadType.DISCUSSION) {
|
||||
// You can't filter questions by not responded so switch type to discussion
|
||||
dispatch(setPostsTypeFilter(ThreadType.DISCUSSION));
|
||||
}
|
||||
filterContentEventProperties.statusFilter = value;
|
||||
}
|
||||
|
||||
if (name === 'sort') {
|
||||
dispatch(setSortedBy(value));
|
||||
filterContentEventProperties.sortFilter = value;
|
||||
}
|
||||
|
||||
if (name === 'cohort') {
|
||||
dispatch(setCohortFilter(value));
|
||||
filterContentEventProperties.cohortFilter = value;
|
||||
}
|
||||
|
||||
sendTrackEvent('edx.forum.filter.content', filterContentEventProperties);
|
||||
}, [currentFilters, currentSorting, dispatch, selectedCohort]);
|
||||
|
||||
const handleToggle = useCallback(() => {
|
||||
setOpen(!isOpen);
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (userHasModerationPrivileges && isEmpty(cohorts)) {
|
||||
dispatch(fetchCourseCohorts(courseId));
|
||||
}
|
||||
}, [courseId, userHasModerationPrivileges]);
|
||||
|
||||
const renderCohortFilter = useMemo(() => (
|
||||
userHasModerationPrivileges && (
|
||||
<>
|
||||
<div className="border-bottom my-2" />
|
||||
{status === RequestStatus.IN_PROGRESS ? (
|
||||
<div className="d-flex justify-content-center p-4">
|
||||
<Spinner animation="border" variant="primary" size="lg" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="d-flex flex-row pt-2">
|
||||
<Form.RadioSet
|
||||
name="cohort"
|
||||
className="d-flex flex-column list-group list-group-flush w-100"
|
||||
value={currentFilters.cohort}
|
||||
onChange={handleSortFilterChange}
|
||||
>
|
||||
<ActionItem
|
||||
id="all-groups"
|
||||
label="All groups"
|
||||
value=""
|
||||
selected={currentFilters.cohort}
|
||||
/>
|
||||
{cohorts.map(cohort => (
|
||||
<ActionItem
|
||||
key={cohort.id}
|
||||
id={toString(cohort.id)}
|
||||
label={capitalize(cohort.name)}
|
||||
value={toString(cohort.id)}
|
||||
selected={currentFilters.cohort}
|
||||
/>
|
||||
))}
|
||||
</Form.RadioSet>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
), [cohorts, currentFilters.cohort, handleSortFilterChange, status, userHasModerationPrivileges]);
|
||||
|
||||
return (
|
||||
<Collapsible.Advanced
|
||||
open={isOpen}
|
||||
onToggle={handleToggle}
|
||||
className="filter-bar collapsible-card-lg border-0"
|
||||
>
|
||||
<Collapsible.Trigger className="collapsible-trigger border-0">
|
||||
<span className="text-primary-500 pr-4 font-style">
|
||||
{intl.formatMessage(messages.sortFilterStatus, {
|
||||
own: false,
|
||||
type: currentFilters.postType,
|
||||
sort: currentSorting,
|
||||
status: currentFilters.status,
|
||||
cohortType: selectedCohort?.name ? 'group' : 'all',
|
||||
cohort: capitalize(selectedCohort?.name),
|
||||
})}
|
||||
</span>
|
||||
<span id="icon-tune">
|
||||
<Collapsible.Visible whenClosed>
|
||||
<Icon src={Tune} />
|
||||
</Collapsible.Visible>
|
||||
<Collapsible.Visible whenOpen>
|
||||
<Icon src={Tune} />
|
||||
</Collapsible.Visible>
|
||||
</span>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Body className="collapsible-body px-4 pb-3 pt-0">
|
||||
<Form>
|
||||
<div className="d-flex flex-row py-2 justify-content-between">
|
||||
<Form.RadioSet
|
||||
name="type"
|
||||
className="d-flex flex-column list-group list-group-flush"
|
||||
value={currentFilters.postType}
|
||||
onChange={handleSortFilterChange}
|
||||
>
|
||||
<ActionItem
|
||||
id="type-all"
|
||||
label={intl.formatMessage(messages.allPosts)}
|
||||
value={ThreadType.ALL}
|
||||
selected={currentFilters.postType}
|
||||
/>
|
||||
<ActionItem
|
||||
id="type-discussions"
|
||||
label={intl.formatMessage(messages.filterDiscussions)}
|
||||
value={ThreadType.DISCUSSION}
|
||||
selected={currentFilters.postType}
|
||||
/>
|
||||
<ActionItem
|
||||
id="type-questions"
|
||||
label={intl.formatMessage(messages.filterQuestions)}
|
||||
value={ThreadType.QUESTION}
|
||||
selected={currentFilters.postType}
|
||||
/>
|
||||
</Form.RadioSet>
|
||||
<Form.RadioSet
|
||||
name="status"
|
||||
className="d-flex flex-column list-group list-group-flush"
|
||||
value={currentFilters.status}
|
||||
onChange={handleSortFilterChange}
|
||||
>
|
||||
<ActionItem
|
||||
id="status-any"
|
||||
label={intl.formatMessage(messages.filterAnyStatus)}
|
||||
value={PostsStatusFilter.ALL}
|
||||
selected={currentFilters.status}
|
||||
/>
|
||||
<ActionItem
|
||||
id="status-unread"
|
||||
label={intl.formatMessage(messages.filterUnread)}
|
||||
value={PostsStatusFilter.UNREAD}
|
||||
selected={currentFilters.status}
|
||||
/>
|
||||
{page !== 'my-posts' && (
|
||||
<ActionItem
|
||||
id="status-following"
|
||||
label={intl.formatMessage(messages.filterFollowing)}
|
||||
value={PostsStatusFilter.FOLLOWING}
|
||||
selected={currentFilters.status}
|
||||
/>
|
||||
)}
|
||||
{(userHasModerationPrivileges || userIsGroupTa) && (
|
||||
<ActionItem
|
||||
id="status-reported"
|
||||
label={intl.formatMessage(messages.filterReported)}
|
||||
value={PostsStatusFilter.REPORTED}
|
||||
selected={currentFilters.status}
|
||||
/>
|
||||
)}
|
||||
<ActionItem
|
||||
id="status-unanswered"
|
||||
label={intl.formatMessage(messages.filterUnanswered)}
|
||||
value={PostsStatusFilter.UNANSWERED}
|
||||
selected={currentFilters.status}
|
||||
/>
|
||||
<ActionItem
|
||||
id="status-unresponded"
|
||||
label={intl.formatMessage(messages.filterUnresponded)}
|
||||
value={PostsStatusFilter.UNRESPONDED}
|
||||
selected={currentFilters.status}
|
||||
/>
|
||||
</Form.RadioSet>
|
||||
<Form.RadioSet
|
||||
name="sort"
|
||||
className="d-flex flex-column list-group list-group-flush"
|
||||
value={currentSorting}
|
||||
onChange={handleSortFilterChange}
|
||||
>
|
||||
<ActionItem
|
||||
id="sort-activity"
|
||||
label={intl.formatMessage(messages.lastActivityAt)}
|
||||
value={ThreadOrdering.BY_LAST_ACTIVITY}
|
||||
selected={currentSorting}
|
||||
/>
|
||||
<ActionItem
|
||||
id="sort-comments"
|
||||
label={intl.formatMessage(messages.commentCount)}
|
||||
value={ThreadOrdering.BY_COMMENT_COUNT}
|
||||
selected={currentSorting}
|
||||
/>
|
||||
<ActionItem
|
||||
id="sort-votes"
|
||||
label={intl.formatMessage(messages.voteCount)}
|
||||
value={ThreadOrdering.BY_VOTE_COUNT}
|
||||
selected={currentSorting}
|
||||
/>
|
||||
</Form.RadioSet>
|
||||
</div>
|
||||
{renderCohortFilter}
|
||||
</Form>
|
||||
</Collapsible.Body>
|
||||
</Collapsible.Advanced>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(PostFilterBar);
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Form } from '@edx/paragon';
|
||||
|
||||
import { ThreadOrdering } from '../../../data/constants';
|
||||
import { selectThreadSorting } from '../data/selectors';
|
||||
import ActionItem from './ActionItem';
|
||||
import messages from './messages';
|
||||
import withFilterHandleChange from './withFilterHandleChange';
|
||||
|
||||
const PostSortFilters = ({ handleSortFilterChange }) => {
|
||||
const intl = useIntl();
|
||||
const currentSorting = useSelector(selectThreadSorting());
|
||||
|
||||
return (
|
||||
<Form.RadioSet
|
||||
name="sort"
|
||||
className="d-flex flex-column list-group list-group-flush"
|
||||
value={currentSorting}
|
||||
onChange={handleSortFilterChange}
|
||||
>
|
||||
<ActionItem
|
||||
id="sort-activity"
|
||||
label={intl.formatMessage(messages.lastActivityAt)}
|
||||
value={ThreadOrdering.BY_LAST_ACTIVITY}
|
||||
selected={currentSorting}
|
||||
/>
|
||||
<ActionItem
|
||||
id="sort-comments"
|
||||
label={intl.formatMessage(messages.commentCount)}
|
||||
value={ThreadOrdering.BY_COMMENT_COUNT}
|
||||
selected={currentSorting}
|
||||
/>
|
||||
<ActionItem
|
||||
id="sort-votes"
|
||||
label={intl.formatMessage(messages.voteCount)}
|
||||
value={ThreadOrdering.BY_VOTE_COUNT}
|
||||
selected={currentSorting}
|
||||
/>
|
||||
</Form.RadioSet>
|
||||
);
|
||||
};
|
||||
|
||||
PostSortFilters.propTypes = {
|
||||
handleSortFilterChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default React.memo(withFilterHandleChange(PostSortFilters));
|
||||
@@ -1,79 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Form } from '@edx/paragon';
|
||||
|
||||
import { PostsStatusFilter } from '../../../data/constants';
|
||||
import { useCurrentPage } from '../../data/hooks';
|
||||
import { selectUserHasModerationPrivileges, selectUserIsGroupTa } from '../../data/selectors';
|
||||
import { selectThreadFilters } from '../data/selectors';
|
||||
import ActionItem from './ActionItem';
|
||||
import messages from './messages';
|
||||
import withFilterHandleChange from './withFilterHandleChange';
|
||||
|
||||
const PostStatusFilters = ({ handleSortFilterChange }) => {
|
||||
const intl = useIntl();
|
||||
const page = useCurrentPage();
|
||||
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
|
||||
const userIsGroupTa = useSelector(selectUserIsGroupTa);
|
||||
const { status } = useSelector(selectThreadFilters());
|
||||
|
||||
return (
|
||||
<Form.RadioSet
|
||||
name="status"
|
||||
className="d-flex flex-column list-group list-group-flush"
|
||||
value={status}
|
||||
onChange={handleSortFilterChange}
|
||||
>
|
||||
<ActionItem
|
||||
id="status-any"
|
||||
label={intl.formatMessage(messages.filterAnyStatus)}
|
||||
value={PostsStatusFilter.ALL}
|
||||
selected={status}
|
||||
/>
|
||||
<ActionItem
|
||||
id="status-unread"
|
||||
label={intl.formatMessage(messages.filterUnread)}
|
||||
value={PostsStatusFilter.UNREAD}
|
||||
selected={status}
|
||||
/>
|
||||
{page !== 'my-posts' && (
|
||||
<ActionItem
|
||||
id="status-following"
|
||||
label={intl.formatMessage(messages.filterFollowing)}
|
||||
value={PostsStatusFilter.FOLLOWING}
|
||||
selected={status}
|
||||
/>
|
||||
)}
|
||||
{(userHasModerationPrivileges || userIsGroupTa) && (
|
||||
<ActionItem
|
||||
id="status-reported"
|
||||
label={intl.formatMessage(messages.filterReported)}
|
||||
value={PostsStatusFilter.REPORTED}
|
||||
selected={status}
|
||||
/>
|
||||
)}
|
||||
<ActionItem
|
||||
id="status-unanswered"
|
||||
label={intl.formatMessage(messages.filterUnanswered)}
|
||||
value={PostsStatusFilter.UNANSWERED}
|
||||
selected={status}
|
||||
/>
|
||||
<ActionItem
|
||||
id="status-unresponded"
|
||||
label={intl.formatMessage(messages.filterUnresponded)}
|
||||
value={PostsStatusFilter.UNRESPONDED}
|
||||
selected={status}
|
||||
/>
|
||||
</Form.RadioSet>
|
||||
);
|
||||
};
|
||||
|
||||
PostStatusFilters.propTypes = {
|
||||
handleSortFilterChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default React.memo(withFilterHandleChange(PostStatusFilters));
|
||||
@@ -1,52 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Form } from '@edx/paragon';
|
||||
|
||||
import { ThreadType } from '../../../data/constants';
|
||||
import { selectThreadFilters } from '../data/selectors';
|
||||
import ActionItem from './ActionItem';
|
||||
import messages from './messages';
|
||||
import withFilterHandleChange from './withFilterHandleChange';
|
||||
|
||||
const PostTypeFilters = ({ handleSortFilterChange }) => {
|
||||
const intl = useIntl();
|
||||
const currentFilters = useSelector(selectThreadFilters());
|
||||
|
||||
return (
|
||||
<Form.RadioSet
|
||||
name="type"
|
||||
className="d-flex flex-column list-group list-group-flush"
|
||||
value={currentFilters.postType}
|
||||
onChange={handleSortFilterChange}
|
||||
>
|
||||
<ActionItem
|
||||
id="type-all"
|
||||
label={intl.formatMessage(messages.allPosts)}
|
||||
value={ThreadType.ALL}
|
||||
selected={currentFilters.postType}
|
||||
/>
|
||||
<ActionItem
|
||||
id="type-discussions"
|
||||
label={intl.formatMessage(messages.filterDiscussions)}
|
||||
value={ThreadType.DISCUSSION}
|
||||
selected={currentFilters.postType}
|
||||
/>
|
||||
<ActionItem
|
||||
id="type-questions"
|
||||
label={intl.formatMessage(messages.filterQuestions)}
|
||||
value={ThreadType.QUESTION}
|
||||
selected={currentFilters.postType}
|
||||
/>
|
||||
</Form.RadioSet>
|
||||
);
|
||||
};
|
||||
|
||||
PostTypeFilters.propTypes = {
|
||||
handleSortFilterChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default React.memo(withFilterHandleChange(PostTypeFilters));
|
||||
@@ -1,84 +0,0 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import { toString } from 'lodash';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
|
||||
import { PostsStatusFilter, ThreadType } from '../../../data/constants';
|
||||
import { selectCourseCohorts } from '../../cohorts/data/selectors';
|
||||
import {
|
||||
setCohortFilter, setPostsTypeFilter, setSortedBy, setStatusFilter,
|
||||
} from '../data';
|
||||
import { selectThreadFilters, selectThreadSorting } from '../data/selectors';
|
||||
|
||||
const withFilterHandleChange = WrappedComponent => (
|
||||
function FilterHandleChange(props) {
|
||||
const dispatch = useDispatch();
|
||||
const currentSorting = useSelector(selectThreadSorting());
|
||||
const currentFilters = useSelector(selectThreadFilters());
|
||||
const cohorts = useSelector(selectCourseCohorts);
|
||||
|
||||
const selectedCohort = useMemo(() => (
|
||||
cohorts?.find(cohort => (
|
||||
toString(cohort.id) === currentFilters.cohort
|
||||
))
|
||||
), [cohorts, currentFilters.cohort]);
|
||||
|
||||
const handleSortFilterChange = useCallback((event) => {
|
||||
const currentType = currentFilters.postType;
|
||||
const currentStatus = currentFilters.status;
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
} = event.currentTarget;
|
||||
const filterContentEventProperties = {
|
||||
statusFilter: currentStatus,
|
||||
threadTypeFilter: currentType,
|
||||
sortFilter: currentSorting,
|
||||
cohortFilter: selectedCohort,
|
||||
triggeredBy: name,
|
||||
};
|
||||
|
||||
if (name === 'type') {
|
||||
dispatch(setPostsTypeFilter(value));
|
||||
if (
|
||||
value === ThreadType.DISCUSSION && currentStatus === PostsStatusFilter.UNANSWERED
|
||||
) {
|
||||
// You can't filter discussions by unanswered
|
||||
dispatch(setStatusFilter(PostsStatusFilter.ALL));
|
||||
}
|
||||
filterContentEventProperties.threadTypeFilter = value;
|
||||
}
|
||||
|
||||
if (name === 'status') {
|
||||
dispatch(setStatusFilter(value));
|
||||
if (value === PostsStatusFilter.UNANSWERED && currentType !== ThreadType.QUESTION) {
|
||||
// You can't filter discussions by unanswered so switch type to questions
|
||||
dispatch(setPostsTypeFilter(ThreadType.QUESTION));
|
||||
}
|
||||
if (value === PostsStatusFilter.UNRESPONDED && currentType !== ThreadType.DISCUSSION) {
|
||||
// You can't filter questions by not responded so switch type to discussion
|
||||
dispatch(setPostsTypeFilter(ThreadType.DISCUSSION));
|
||||
}
|
||||
filterContentEventProperties.statusFilter = value;
|
||||
}
|
||||
|
||||
if (name === 'sort') {
|
||||
dispatch(setSortedBy(value));
|
||||
filterContentEventProperties.sortFilter = value;
|
||||
}
|
||||
|
||||
if (name === 'cohort') {
|
||||
dispatch(setCohortFilter(value));
|
||||
filterContentEventProperties.cohortFilter = value;
|
||||
}
|
||||
|
||||
sendTrackEvent('edx.forum.filter.content', filterContentEventProperties);
|
||||
}, [currentFilters, currentSorting, selectedCohort]);
|
||||
|
||||
return <WrappedComponent {...props} handleSortFilterChange={handleSortFilterChange} />;
|
||||
}
|
||||
);
|
||||
|
||||
export default withFilterHandleChange;
|
||||
@@ -4,21 +4,22 @@ import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { toString } from 'lodash';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, useToggle } from '@edx/paragon';
|
||||
|
||||
import HTMLLoader from '../../../components/HTMLLoader';
|
||||
import { ContentActions } from '../../../data/constants';
|
||||
import { ContentActions, getFullUrl } from '../../../data/constants';
|
||||
import { selectorForUnitSubsection, selectTopicContext } from '../../../data/selectors';
|
||||
import { AlertBanner, Confirmation } from '../../common';
|
||||
import { DiscussionContext } from '../../common/context';
|
||||
import HoverCard from '../../common/HoverCard';
|
||||
import { ContentTypes } from '../../data/constants';
|
||||
import { selectModerationSettings, selectUserHasModerationPrivileges } from '../../data/selectors';
|
||||
import { selectUserHasModerationPrivileges } from '../../data/selectors';
|
||||
import { selectTopic } from '../../topics/data/selectors';
|
||||
import { truncatePath } from '../../utils';
|
||||
import { selectThread } from '../data/selectors';
|
||||
import { removeThread, updateExistingThread } from '../data/thunks';
|
||||
import ClosePostReasonModal from './ClosePostReasonModal';
|
||||
@@ -35,13 +36,12 @@ const Post = ({ handleAddResponseButton }) => {
|
||||
} = useSelector(selectThread(postId));
|
||||
const intl = useIntl();
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const courseId = useSelector((state) => state.config.id);
|
||||
const { courseId } = useContext(DiscussionContext);
|
||||
const topic = useSelector(selectTopic(topicId));
|
||||
const getTopicSubsection = useSelector(selectorForUnitSubsection);
|
||||
const topicContext = useSelector(selectTopicContext(topicId));
|
||||
const { reasonCodesEnabled } = useSelector(selectModerationSettings);
|
||||
const [isDeleting, showDeleteConfirmation, hideDeleteConfirmation] = useToggle(false);
|
||||
const [isReporting, showReportConfirmation, hideReportConfirmation] = useToggle(false);
|
||||
const [isClosing, showClosePostModal, hideClosePostModal] = useToggle(false);
|
||||
@@ -49,9 +49,11 @@ const Post = ({ handleAddResponseButton }) => {
|
||||
const displayPostFooter = following || voteCount || closed || (groupId && userHasModerationPrivileges);
|
||||
|
||||
const handleDeleteConfirmation = useCallback(async () => {
|
||||
const basePath = truncatePath(location.pathname);
|
||||
|
||||
await dispatch(removeThread(postId));
|
||||
history.push({
|
||||
pathname: '.',
|
||||
navigate({
|
||||
pathname: basePath,
|
||||
search: enableInContextSidebar && '?inContextSidebar',
|
||||
});
|
||||
hideDeleteConfirmation();
|
||||
@@ -62,7 +64,7 @@ const Post = ({ handleAddResponseButton }) => {
|
||||
hideReportConfirmation();
|
||||
}, [abuseFlagged, postId, hideReportConfirmation]);
|
||||
|
||||
const handlePostContentEdit = useCallback(() => history.push({
|
||||
const handlePostContentEdit = useCallback(() => navigate({
|
||||
...location,
|
||||
pathname: `${location.pathname}/edit`,
|
||||
}), [location.pathname]);
|
||||
@@ -70,16 +72,13 @@ const Post = ({ handleAddResponseButton }) => {
|
||||
const handlePostClose = useCallback(() => {
|
||||
if (closed) {
|
||||
dispatch(updateExistingThread(postId, { closed: false }));
|
||||
} else if (reasonCodesEnabled) {
|
||||
showClosePostModal();
|
||||
} else {
|
||||
dispatch(updateExistingThread(postId, { closed: true }));
|
||||
showClosePostModal();
|
||||
}
|
||||
}, [closed, postId, reasonCodesEnabled, showClosePostModal]);
|
||||
}, [closed, postId, showClosePostModal]);
|
||||
|
||||
const handlePostCopyLink = useCallback(() => {
|
||||
const postURL = new URL(`${getConfig().PUBLIC_PATH}${courseId}/posts/${postId}`, window.location.origin);
|
||||
navigator.clipboard.writeText(postURL.href);
|
||||
navigator.clipboard.writeText(getFullUrl(`${courseId}/posts/${postId}`));
|
||||
}, [window.location.origin, postId, courseId]);
|
||||
|
||||
const handlePostPin = useCallback(() => dispatch(
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useContext, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Badge, Icon } from '@edx/paragon';
|
||||
@@ -12,9 +12,7 @@ import { CheckCircle } from '@edx/paragon/icons';
|
||||
import { PushPin } from '../../../components/icons';
|
||||
import { AvatarOutlineAndLabelColors, Routes, ThreadType } from '../../../data/constants';
|
||||
import AuthorLabel from '../../common/AuthorLabel';
|
||||
import {
|
||||
useCategory, useCourseId, useCurrentPage, useEnableInContextSidebar, useLearnerUsername, usePostId,
|
||||
} from '../../data/hooks';
|
||||
import { DiscussionContext } from '../../common/context';
|
||||
import { discussionsPath, isPostPreviewAvailable } from '../../utils';
|
||||
import { selectThread } from '../data/selectors';
|
||||
import messages from './messages';
|
||||
@@ -27,27 +25,27 @@ const PostLink = ({
|
||||
showDivider,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const courseId = useCourseId();
|
||||
const selectedPostId = usePostId();
|
||||
const page = useCurrentPage();
|
||||
const enableInContextSidebar = useEnableInContextSidebar();
|
||||
const category = useCategory();
|
||||
const learnerUsername = useLearnerUsername();
|
||||
|
||||
const { search } = useLocation();
|
||||
const {
|
||||
courseId,
|
||||
postId: selectedPostId,
|
||||
page,
|
||||
enableInContextSidebar,
|
||||
category,
|
||||
learnerUsername,
|
||||
} = useContext(DiscussionContext);
|
||||
const {
|
||||
topicId, hasEndorsed, type, author, authorLabel, abuseFlagged, abuseFlaggedCount, read, commentCount,
|
||||
unreadCommentCount, id, pinned, previewBody, title, voted, voteCount, following, groupId, groupName, createdAt,
|
||||
} = useSelector(selectThread(postId));
|
||||
|
||||
const linkUrl = discussionsPath(Routes.COMMENTS.PAGES[page], {
|
||||
const { pathname } = discussionsPath(Routes.COMMENTS.PAGES[page], {
|
||||
0: enableInContextSidebar ? 'in-context' : undefined,
|
||||
courseId,
|
||||
topicId,
|
||||
postId,
|
||||
category,
|
||||
learnerUsername,
|
||||
});
|
||||
|
||||
})();
|
||||
const showAnsweredBadge = hasEndorsed && type === ThreadType.QUESTION;
|
||||
const authorLabelColor = AvatarOutlineAndLabelColors[authorLabel];
|
||||
const canSeeReportedBadge = abuseFlagged || abuseFlaggedCount;
|
||||
@@ -66,7 +64,7 @@ const PostLink = ({
|
||||
'border-bottom border-light-400': showDivider,
|
||||
})
|
||||
}
|
||||
to={linkUrl}
|
||||
to={`${pathname}${enableInContextSidebar ? search : ''}`}
|
||||
aria-current={checkIsSelected ? 'page' : undefined}
|
||||
role="option"
|
||||
tabIndex={(checkIsSelected || idx === 0) ? 0 : -1}
|
||||
|
||||
@@ -34,7 +34,6 @@ const mockThread = async (id, abuseFlagged) => {
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/settings`).reply(200, {});
|
||||
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`).reply(200, {
|
||||
learners_tab_enabled: true,
|
||||
has_moderation_privileges: true,
|
||||
});
|
||||
axiosMock.onGet(`${threadsApiUrl}${id}/`).reply(200, Factory.build('thread', {
|
||||
@@ -49,7 +48,7 @@ function renderComponent(id) {
|
||||
return render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<DiscussionContext.Provider value={{ courseId }}>
|
||||
<DiscussionContext.Provider value={{ courseId, page: 'posts' }}>
|
||||
<PostLink
|
||||
key={id}
|
||||
postId={id}
|
||||
|
||||
@@ -3,7 +3,7 @@ import React, {
|
||||
} from 'react';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useParams } from 'react-router';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import SearchInfo from '../../components/SearchInfo';
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
|
||||
@@ -3,7 +3,9 @@ import {
|
||||
} from '@testing-library/react';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { MemoryRouter, Route } from 'react-router';
|
||||
import {
|
||||
MemoryRouter, Route, Routes, useLocation,
|
||||
} from 'react-router-dom';
|
||||
import { Factory } from 'rosie';
|
||||
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
@@ -28,24 +30,21 @@ let axiosMock;
|
||||
let lastLocation;
|
||||
let container;
|
||||
|
||||
const LocationComponent = () => {
|
||||
lastLocation = useLocation();
|
||||
return null;
|
||||
};
|
||||
|
||||
function renderComponent() {
|
||||
const wrapper = render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<AppProvider store={store} wrapWithRouter={false}>
|
||||
<DiscussionContext.Provider value={{ courseId }}>
|
||||
<MemoryRouter initialEntries={[`/${courseId}/topics/`]}>
|
||||
<Route path="/:courseId/topics/">
|
||||
<TopicsView />
|
||||
</Route>
|
||||
<Route path="/:courseId/category/:category">
|
||||
<TopicsView />
|
||||
</Route>
|
||||
<Route
|
||||
render={({ location }) => {
|
||||
lastLocation = location;
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
<Routes>
|
||||
<Route path="/:courseId/topics/*" element={<><TopicsView /><LocationComponent /></>} />
|
||||
<Route path="/:courseId/category/:category" element={<><TopicsView /><LocationComponent /></>} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</DiscussionContext.Provider>
|
||||
</AppProvider>
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useContext, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
@@ -20,10 +20,15 @@ const TopicGroupBase = ({
|
||||
topicsIds,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const { courseId } = useContext(DiscussionContext);
|
||||
const { search } = useLocation();
|
||||
const { courseId, enableInContextSidebar } = useContext(DiscussionContext);
|
||||
const filter = useSelector(selectTopicFilter);
|
||||
const topics = useSelector(selectTopicsById(topicsIds));
|
||||
const hasTopics = topics.length > 0;
|
||||
const { pathname } = discussionsPath(Routes.TOPICS.CATEGORY, {
|
||||
courseId,
|
||||
category: groupId,
|
||||
})();
|
||||
|
||||
const matchesFilter = useMemo(() => (
|
||||
filter ? groupTitle?.toLowerCase().includes(filter) : true
|
||||
@@ -69,10 +74,7 @@ const TopicGroupBase = ({
|
||||
{linkToGroup && groupId ? (
|
||||
<Link
|
||||
className="text-decoration-none text-primary-500"
|
||||
to={discussionsPath(Routes.TOPICS.CATEGORY, {
|
||||
courseId,
|
||||
category: groupId,
|
||||
})}
|
||||
to={`${pathname}${enableInContextSidebar ? search : ''}`}
|
||||
>
|
||||
{groupTitle}
|
||||
</Link>
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
/* eslint-disable no-unused-vars, react/forbid-prop-types */
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useParams } from 'react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link, useLocation, useParams } from 'react-router-dom';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Icon, OverlayTrigger, Tooltip } from '@edx/paragon';
|
||||
import { HelpOutline, PostOutline, Report } from '@edx/paragon/icons';
|
||||
|
||||
import { Routes } from '../../../../data/constants';
|
||||
import { DiscussionContext } from '../../../common/context';
|
||||
import { selectUserHasModerationPrivileges, selectUserIsGroupTa } from '../../../data/selectors';
|
||||
import { discussionsPath } from '../../../utils';
|
||||
import { selectTopic } from '../../data/selectors';
|
||||
@@ -20,6 +20,8 @@ import messages from '../../messages';
|
||||
|
||||
const Topic = ({ topicId, showDivider, index }) => {
|
||||
const intl = useIntl();
|
||||
const { search } = useLocation();
|
||||
const { enableInContextSidebar } = useContext(DiscussionContext);
|
||||
const { courseId } = useParams();
|
||||
const topic = useSelector(selectTopic(topicId));
|
||||
const {
|
||||
@@ -28,7 +30,7 @@ const Topic = ({ topicId, showDivider, index }) => {
|
||||
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
|
||||
const userIsGroupTa = useSelector(selectUserIsGroupTa);
|
||||
const canSeeReportedStats = (activeFlags || inactiveFlags) && (userHasModerationPrivileges || userIsGroupTa);
|
||||
const topicUrl = discussionsPath(Routes.TOPICS.TOPIC, { courseId, topicId });
|
||||
const { pathname } = discussionsPath(Routes.TOPICS.TOPIC, { courseId, topicId })();
|
||||
|
||||
const isSelected = useCallback((selectedId) => (
|
||||
window.location.pathname.includes(selectedId)
|
||||
@@ -42,7 +44,7 @@ const Topic = ({ topicId, showDivider, index }) => {
|
||||
})
|
||||
}
|
||||
data-topic-id={id}
|
||||
to={topicUrl}
|
||||
to={`${pathname}${enableInContextSidebar ? search : ''}`}
|
||||
onClick={() => isSelected(id)}
|
||||
aria-current={isSelected(id) ? 'page' : undefined}
|
||||
role="option"
|
||||
|
||||
@@ -1,23 +1,31 @@
|
||||
import React, { memo } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { ProductTour } from '@edx/paragon';
|
||||
|
||||
import withConditionalInContextRendering from '../common/withConditionalInContextRendering';
|
||||
import { useTourConfiguration } from '../data/hooks';
|
||||
import { fetchDiscussionTours } from './data/thunks';
|
||||
|
||||
const DiscussionsProductTour = () => {
|
||||
const dispatch = useDispatch();
|
||||
const config = useTourConfiguration();
|
||||
console.log('DiscussionsProductTour');
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchDiscussionTours());
|
||||
}, []);
|
||||
|
||||
return (
|
||||
!isEmpty(config) && (
|
||||
<ProductTour
|
||||
tours={config}
|
||||
/>
|
||||
)
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
<>
|
||||
{!isEmpty(config) && (
|
||||
<ProductTour
|
||||
tours={config}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withConditionalInContextRendering(DiscussionsProductTour, false));
|
||||
export default DiscussionsProductTour;
|
||||
|
||||
@@ -3,7 +3,9 @@ import { useCallback, useContext, useMemo } from 'react';
|
||||
import { getIn } from 'formik';
|
||||
import { uniqBy } from 'lodash';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { generatePath, useRouteMatch } from 'react-router';
|
||||
import {
|
||||
generatePath, matchPath, useLocation,
|
||||
} from 'react-router-dom';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import {
|
||||
@@ -11,7 +13,9 @@ import {
|
||||
} from '@edx/paragon/icons';
|
||||
|
||||
import { InsertLink } from '../components/icons';
|
||||
import { ContentActions, Routes, ThreadType } from '../data/constants';
|
||||
import {
|
||||
ContentActions, Routes, ThreadType,
|
||||
} from '../data/constants';
|
||||
import { ContentSelectors } from './data/constants';
|
||||
import { PostCommentsContext } from './post-comments/postCommentsContext';
|
||||
import messages from './messages';
|
||||
@@ -42,8 +46,9 @@ export function isFormikFieldInvalid(field, {
|
||||
* @returns {string}
|
||||
*/
|
||||
export function useCommentsPagePath() {
|
||||
const { params } = useRouteMatch(Routes.COMMENTS.PAGE);
|
||||
return Routes.COMMENTS.PAGES[params.page];
|
||||
const location = useLocation();
|
||||
const { params: { page } } = matchPath({ path: Routes.COMMENTS.PAGE }, location.pathname);
|
||||
return Routes.COMMENTS.PAGES[page];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -284,3 +289,7 @@ export function handleKeyDown(event) {
|
||||
export function isLastElementOfList(list, element) {
|
||||
return list[list.length - 1] === element;
|
||||
}
|
||||
|
||||
export function truncatePath(path) {
|
||||
return path.substring(0, path.lastIndexOf('/'));
|
||||
}
|
||||
|
||||
@@ -1,211 +1,211 @@
|
||||
{
|
||||
"discussions.actions.button.alt": "Actions menu",
|
||||
"discussions.actions.copylink": "Copy link",
|
||||
"discussions.actions.edit": "Edit",
|
||||
"discussions.actions.pin": "Pin",
|
||||
"discussions.actions.unpin": "Unpin",
|
||||
"discussions.actions.delete": "Delete",
|
||||
"discussions.confirmation.button.confirm": "Confirm",
|
||||
"discussions.actions.close": "Close",
|
||||
"discussions.actions.reopen": "Reopen",
|
||||
"discussions.actions.report": "Report",
|
||||
"discussions.actions.unreport": "Unreport",
|
||||
"discussions.actions.endorse": "Endorse",
|
||||
"discussions.actions.unendorse": "Unendorse",
|
||||
"discussions.actions.markAnswered": "Mark as answered",
|
||||
"discussions.actions.unMarkAnswered": "Unmark as answered",
|
||||
"discussions.modal.confirmation.button.cancel": "Cancel",
|
||||
"discussions.empty.allTopics": "All discussion activity for these topics will show up here.",
|
||||
"discussions.empty.allPosts": "All discussion activity for your course will show up here.",
|
||||
"discussions.empty.myPosts": "Posts you've interacted with will show up here.",
|
||||
"discussions.empty.topic": "All discussion activity for this topic will show up here.",
|
||||
"discussions.empty.title": "Nothing here yet",
|
||||
"discussions.empty.noPostSelected": "No post selected",
|
||||
"discussions.empty.noTopicSelected": "No topic selected",
|
||||
"discussions.sidebar.noResultsFound": "No results found",
|
||||
"discussions.sidebar.differentKeywords": "Try searching different keywords",
|
||||
"discussions.sidebar.removeKeywords": "Try searching different keywords or removing some filters",
|
||||
"discussions.sidebar.removeKeywordsOnly": "Try searching different keywords",
|
||||
"discussions.sidebar.removeFilters": "Try removing some filters",
|
||||
"discussions.empty.iconAlt": "Empty",
|
||||
"discussions.authors.label.staff": "Staff",
|
||||
"discussions.authors.label.ta": "TA",
|
||||
"discussions.learner.loadMostPosts": "Load more posts",
|
||||
"discussions.post.anonymous.author": "anonymous",
|
||||
"discussion.blackoutBanner.information": "Posting in discussions is disabled by the course team",
|
||||
"discussions.editor.image.warning.message": "Images having width or height greater than 999px will not be visible when the post, response or comment is viewed using in-line course discussions",
|
||||
"discussions.editor.image.warning.title": "Warning!",
|
||||
"discussions.editor.image.warning.dismiss": "Ok",
|
||||
"navigation.course.tabs.label": "Course Material",
|
||||
"discussions.topics.backAlt": "Back to topics list",
|
||||
"discussions.actions.button.alt": "एक्शन मेन्यू",
|
||||
"discussions.actions.copylink": "कॉपी लिंक",
|
||||
"discussions.actions.edit": "संपादित करें",
|
||||
"discussions.actions.pin": "पिन",
|
||||
"discussions.actions.unpin": "अनपिन",
|
||||
"discussions.actions.delete": "नष्ट करें",
|
||||
"discussions.confirmation.button.confirm": "पुष्टि करें",
|
||||
"discussions.actions.close": "बंद करे",
|
||||
"discussions.actions.reopen": "फिर से खोलें",
|
||||
"discussions.actions.report": "रिपोर्ट",
|
||||
"discussions.actions.unreport": "अनरिपोर्ट",
|
||||
"discussions.actions.endorse": "समर्थन",
|
||||
"discussions.actions.unendorse": "समर्थन न करें",
|
||||
"discussions.actions.markAnswered": "उत्तर के रूप में चिह्नित करें",
|
||||
"discussions.actions.unMarkAnswered": "उत्तर के रूप में अचिह्नित करें",
|
||||
"discussions.modal.confirmation.button.cancel": "रद्द करना",
|
||||
"discussions.empty.allTopics": "इन विषयों के लिए सभी चर्चा गतिविधियाँ यहाँ दिखाई देंगी।",
|
||||
"discussions.empty.allPosts": "आपके पाठ्यक्रम की सभी चर्चा गतिविधियाँ यहाँ दिखाई देंगी।",
|
||||
"discussions.empty.myPosts": "जिन पोस्टों के साथ आपने इंटरैक्ट किया है वे यहां दिखाई देंगी।",
|
||||
"discussions.empty.topic": "इस विषय की सभी चर्चा गतिविधियाँ यहाँ दिखाई देंगी।",
|
||||
"discussions.empty.title": "अभी तक यहां कुछ भी नहीं है",
|
||||
"discussions.empty.noPostSelected": "कोई पोस्ट चयनित नहीं",
|
||||
"discussions.empty.noTopicSelected": "कोई विषय चयनित नहीं",
|
||||
"discussions.sidebar.noResultsFound": "कोई परिणाम नहीं मिला",
|
||||
"discussions.sidebar.differentKeywords": "अलग-अलग कीवर्ड खोजने का प्रयास करें",
|
||||
"discussions.sidebar.removeKeywords": "अलग-अलग कीवर्ड खोजने या कुछ फ़िल्टर हटाने का प्रयास करें",
|
||||
"discussions.sidebar.removeKeywordsOnly": "अलग-अलग कीवर्ड खोजने का प्रयास करें",
|
||||
"discussions.sidebar.removeFilters": "कुछ फ़िल्टर हटाने का प्रयास करें",
|
||||
"discussions.empty.iconAlt": "खाली",
|
||||
"discussions.authors.label.staff": "कर्मचारी",
|
||||
"discussions.authors.label.ta": "टीए",
|
||||
"discussions.learner.loadMostPosts": "और पोस्ट लोड करें",
|
||||
"discussions.post.anonymous.author": "गुमनाम",
|
||||
"discussion.blackoutBanner.information": "चर्चाओं में पोस्ट करना पाठ्यक्रम टीम द्वारा अक्षम कर दिया गया है",
|
||||
"discussions.editor.image.warning.message": "जब पोस्ट, प्रतिक्रिया या टिप्पणी को इन-लाइन पाठ्यक्रम चर्चाओं का उपयोग करके देखा जाता है तो 999px से अधिक चौड़ाई या ऊंचाई वाली छवियां दिखाई नहीं देंगी",
|
||||
"discussions.editor.image.warning.title": "चेतावनी!",
|
||||
"discussions.editor.image.warning.dismiss": "ठीक",
|
||||
"navigation.course.tabs.label": "पाठ्यक्रम सामग्री",
|
||||
"discussions.topics.backAlt": "विषय सूची पर वापस जाएं",
|
||||
"discussions.topics.discussions": "{count, plural, =0 {Discussion} one {# Discussion} other {# Discussions} }",
|
||||
"discussions.topics.questions": "{count, plural, =0 {Question} one {# Question} other {# Questions} }",
|
||||
"discussions.topics.reported": "{reported} reported",
|
||||
"discussions.topics.previouslyReported": "{previouslyReported} previously reported",
|
||||
"discussions.topics.find.label": "Search topics",
|
||||
"discussions.topics.unnamed.section.label": "Unnamed Section",
|
||||
"discussions.topics.unnamed.subsection.label": "Unnamed Subsection",
|
||||
"discussions.subtopics.unnamed.topic.label": "Unnamed Topic",
|
||||
"discussions.topics.title": "No topic exists",
|
||||
"discussions.topics.createTopic": "Please contact you admin to create a topic",
|
||||
"discussions.topics.nothing": "Nothing here yet",
|
||||
"discussions.topics.archived.label": "Archived",
|
||||
"discussions.learner.reported": "{reported} reported",
|
||||
"discussions.learner.previouslyReported": "{previouslyReported} previously reported",
|
||||
"discussions.learner.lastLogin": "Last active {lastActiveTime}",
|
||||
"discussions.learner.loadMostLearners": "Load more",
|
||||
"discussions.learner.back": "Back",
|
||||
"discussions.learner.activityForLearner": "Activity for {username}",
|
||||
"discussions.learner.mostActivity": "Most activity",
|
||||
"discussions.learner.reportedActivity": "Reported activity",
|
||||
"discussions.learner.recentActivity": "Recent activity",
|
||||
"discussions.learner.sortFilterStatus": "All learners sorted by {sort, select, flagged {reported activity} activity {most activity} other {{sort}} }",
|
||||
"discussion.learner.allActivity": "All activity",
|
||||
"discussion.learner.posts": "Posts",
|
||||
"discussions.comments.comment.addComment": "Add comment",
|
||||
"discussions.comments.comment.addResponse": "Add a response",
|
||||
"discussions.comments.comment.abuseFlaggedMessage": "Content reported for staff to review",
|
||||
"discussions.actions.back.alt": "Back to list",
|
||||
"discussions.topics.reported": "{reported} रिपोर्ट किया गया",
|
||||
"discussions.topics.previouslyReported": "{previouslyReported} ने पहले रिपोर्ट किया था",
|
||||
"discussions.topics.find.label": "विषय खोजें",
|
||||
"discussions.topics.unnamed.section.label": "अनाम अनुभाग",
|
||||
"discussions.topics.unnamed.subsection.label": "अनाम उपखंड",
|
||||
"discussions.subtopics.unnamed.topic.label": "अनाम विषय",
|
||||
"discussions.topics.title": "कोई विषय मौजूद नहीं है",
|
||||
"discussions.topics.createTopic": "विषय बनाने के लिए कृपया अपने व्यवस्थापक से संपर्क करें",
|
||||
"discussions.topics.nothing": "अभी तक यहां कुछ भी नहीं है",
|
||||
"discussions.topics.archived.label": "संग्रहीत",
|
||||
"discussions.learner.reported": "{reported} रिपोर्ट किया गया",
|
||||
"discussions.learner.previouslyReported": "{previouslyReported} ने पहले रिपोर्ट किया था",
|
||||
"discussions.learner.lastLogin": "अंतिम सक्रिय {lastActiveTime}",
|
||||
"discussions.learner.loadMostLearners": "और लोड करें",
|
||||
"discussions.learner.back": "वापस",
|
||||
"discussions.learner.activityForLearner": "{username} के लिए गतिविधि",
|
||||
"discussions.learner.mostActivity": "सर्वाधिक गतिविधि",
|
||||
"discussions.learner.reportedActivity": "रिपोर्ट की गई गतिविधि",
|
||||
"discussions.learner.recentActivity": "हाल की गतिविधि",
|
||||
"discussions.learner.sortFilterStatus": "सभी शिक्षार्थियों को {sort, select, flagged {reported activity} activity {most activity} other {{sort}} }",
|
||||
"discussion.learner.allActivity": "सारी गतिविधि",
|
||||
"discussion.learner.posts": "पोस्ट",
|
||||
"discussions.comments.comment.addComment": "टिप्पणी जोड़ें",
|
||||
"discussions.comments.comment.addResponse": "एक प्रतिक्रिया जोड़ें",
|
||||
"discussions.comments.comment.abuseFlaggedMessage": "कर्मचारियों की समीक्षा के लिए सामग्री रिपोर्ट की गई",
|
||||
"discussions.actions.back.alt": "सूची पर वापस जाएं",
|
||||
"discussions.comments.comment.responseCount": "{num, plural, =0 {No responses} one {Showing # response} other {Showing # responses} }",
|
||||
"discussions.comments.comment.endorsedResponseCount": "{num, plural, =0 {No endorsed responses} one {Showing # endorsed response} other {Showing # endorsed responses} }",
|
||||
"discussions.comments.comment.loadMoreComments": "Load more comments",
|
||||
"discussions.comments.comment.loadMoreResponses": "Load more responses",
|
||||
"discussions.comments.comment.visibility": "This post is visible to {group, select, null {Everyone} other {{group}} }.",
|
||||
"discussions.comments.comment.loadMoreComments": "और टिप्पणियों को लोड करें",
|
||||
"discussions.comments.comment.loadMoreResponses": "अधिक प्रतिक्रियाएं लोड करें",
|
||||
"discussions.comments.comment.visibility": "यह पोस्ट {group, select, null {Everyone} other {{group}} } को दृश्यमान है।",
|
||||
"discussions.comments.comment.postedTime": "{postType, select, discussion {Discussion} question {Question} other {{postType}} } posted {relativeTime} by",
|
||||
"discussions.comments.comment.commentTime": "Posted {relativeTime}",
|
||||
"discussions.comments.comment.answer": "Answer",
|
||||
"discussions.comments.comment.answeredlabel": "Marked as answered by",
|
||||
"discussions.comments.comment.endorsed": "Endorsed",
|
||||
"discussions.comments.comment.endorsedlabel": "Endorsed by",
|
||||
"discussions.actions.label": "Actions menu",
|
||||
"discussions.editor.submit": "Submit",
|
||||
"discussions.editor.submitting": "Submitting",
|
||||
"discussions.editor.cancel": "Cancel",
|
||||
"discussions.editor.error.empty": "Post content cannot be empty.",
|
||||
"discussions.editor.delete.response.title": "Delete response",
|
||||
"discussions.editor.delete.response.description": "Are you sure you want to permanently delete this response?",
|
||||
"discussions.editor.delete.comment.title": "Delete comment",
|
||||
"discussions.editor.delete.comment.description": "Are you sure you want to permanently delete this comment?",
|
||||
"discussions.delete.confirmation.button.delete": "Delete",
|
||||
"discussions.editor.response.response.title": "Report inappropriate content?",
|
||||
"discussions.editor.response.description": "The discussion moderation team will review this content and take appropriate action.",
|
||||
"discussions.editor.report.comment.title": "Report inappropriate content?",
|
||||
"discussions.editor.report.comment.description": "The discussion moderation team will review this content and take appropriate action.",
|
||||
"discussions.editor.comments.editReasonCode": "Reason for editing",
|
||||
"discussions.editor.posts.editReasonCode.error": "Select reason for editing",
|
||||
"discussions.comment.comments.editedBy": "Edited by",
|
||||
"discussions.comments.comment.commentTime": "{relativeTime} पोस्ट किया गया",
|
||||
"discussions.comments.comment.answer": "उत्तर",
|
||||
"discussions.comments.comment.answeredlabel": "उत्तर देने के रूप में चिह्नित किया गया",
|
||||
"discussions.comments.comment.endorsed": "समर्थन किया",
|
||||
"discussions.comments.comment.endorsedlabel": "द्वारा समर्थन",
|
||||
"discussions.actions.label": "एक्शन मेन्यू",
|
||||
"discussions.editor.submit": "प्रस्तुत",
|
||||
"discussions.editor.submitting": "भेजने से",
|
||||
"discussions.editor.cancel": "रद्द करना",
|
||||
"discussions.editor.error.empty": "पोस्ट सामग्री खाली नहीं हो सकती।",
|
||||
"discussions.editor.delete.response.title": "प्रतिक्रिया हटाएँ",
|
||||
"discussions.editor.delete.response.description": "क्या आप वाकई इस प्रतिक्रिया को स्थायी रूप से हटाना चाहते हैं?",
|
||||
"discussions.editor.delete.comment.title": "टिप्पणी हटाएँ",
|
||||
"discussions.editor.delete.comment.description": "क्या आप वाकई इस टिप्पणी को स्थायी रूप से हटाना चाहते हैं?",
|
||||
"discussions.delete.confirmation.button.delete": "नष्ट करें",
|
||||
"discussions.editor.response.response.title": "अनुचित सामग्री की रिपोर्ट करें?",
|
||||
"discussions.editor.response.description": "चर्चा मॉडरेशन टीम इस सामग्री की समीक्षा करेगी और उचित कार्रवाई करेगी।",
|
||||
"discussions.editor.report.comment.title": "अनुचित सामग्री की रिपोर्ट करें?",
|
||||
"discussions.editor.report.comment.description": "चर्चा मॉडरेशन टीम इस सामग्री की समीक्षा करेगी और उचित कार्रवाई करेगी।",
|
||||
"discussions.editor.comments.editReasonCode": "संपादन का कारण",
|
||||
"discussions.editor.posts.editReasonCode.error": "संपादन का कारण चुनें",
|
||||
"discussions.comment.comments.editedBy": "द्वारा संपादित",
|
||||
"discussions.comment.comments.fullStop": "•",
|
||||
"discussions.comment.comments.reason": "Reason",
|
||||
"discussions.post.closedBy": "Post closed by",
|
||||
"discussion.comment.time": "{time} ago",
|
||||
"discussion.thread.notFound": "Thread not found",
|
||||
"discussions.comment.comments.reason": "कारण",
|
||||
"discussions.post.closedBy": "पोस्ट बंद कर दी गई",
|
||||
"discussion.comment.time": "{time} पहले",
|
||||
"discussion.thread.notFound": "थ्रेड नहीं मिला",
|
||||
"discussions.comment.sortFilterStatus": "{sort, select, false {Oldest first} true {Newest first} other {{sort}} }",
|
||||
"discussions.topics.sort.message": "Sorted by {sortBy}",
|
||||
"discussions.topics.sort.lastActivity": "Recent activity",
|
||||
"discussions.topics.sort.commentCount": "Most activity",
|
||||
"discussions.topics.sort.courseStructure": "Course Structure",
|
||||
"discussions.topics.unnamed.label": "Unnamed category",
|
||||
"discussions.subtopics.unnamed.label": "Unnamed subcategory",
|
||||
"tour.action.advance": "Next",
|
||||
"tour.action.dismiss": "Dismiss",
|
||||
"tour.action.end": "Okay",
|
||||
"tour.body.notRespondedFilter": "Now you can filter discussions to find posts with no response.",
|
||||
"tour.title.notRespondedFilter": "New filtering option!",
|
||||
"tour.body.responseSortTour": "Responses and comments are now sorted by newest first. Please use this option to change the sort order",
|
||||
"tour.title.responseSortTour": "Sort Responses!",
|
||||
"learn.course.tabs.navigation.overflow.menu": "More...",
|
||||
"discussions.navigation.breadcrumbMenu.allTopics": "Topics",
|
||||
"discussions.navigation.breadcrumbMenu.showAll": "Show all",
|
||||
"discussions.navigation.navigationBar.allPosts": "All posts",
|
||||
"discussions.navigation.navigationBar.allTopics": "Topics",
|
||||
"discussions.navigation.navigationBar.myPosts": "My posts",
|
||||
"discussions.navigation.navigationBar.learners": "Learners",
|
||||
"discussions.post.author.anonymous": "anonymous",
|
||||
"discussions.post.addResponse": "Add response",
|
||||
"discussions.post.lastResponse": "Last response {time}",
|
||||
"discussions.post.postedOn": "Posted {time} by {author} {authorLabel}",
|
||||
"discussions.post.contentReported": "Reported",
|
||||
"discussions.post.following": "Following",
|
||||
"discussions.post.follow": "Follow",
|
||||
"discussions.post.followed": "Followed",
|
||||
"discussions.post.notFollowed": "Not Followed",
|
||||
"discussions.post.answered": "Answered",
|
||||
"discussions.post.unFollow": "Unfollow",
|
||||
"discussions.post.like": "Like",
|
||||
"discussions.post.removeLike": "Unlike",
|
||||
"discussions.post.liked": "liked",
|
||||
"discussions.post.likes": "likes",
|
||||
"discussions.post.viewActivity": "View activity",
|
||||
"discussions.post.activity": "Activity",
|
||||
"discussions.post.closed": "Post closed for responses and comments",
|
||||
"discussions.post.relatedTo": "Related to",
|
||||
"discussions.editor.delete.post.title": "Delete post",
|
||||
"discussions.editor.delete.post.description": "Are you sure you want to permanently delete this post?",
|
||||
"discussions.post.delete.confirmation.button.delete": "Delete",
|
||||
"discussions.editor.report.post.title": "Report inappropriate content?",
|
||||
"discussions.editor.report.post.description": "The discussion moderation team will review this content and take appropriate action.",
|
||||
"discussions.post.closePostModal.title": "Close post",
|
||||
"discussions.post.closePostModal.text": "Enter a reason for closing this post. This will only be displayed to other moderators.",
|
||||
"discussions.post.closePostModal.reasonCodeInput": "Reason",
|
||||
"discussions.post.closePostModal.cancel": "Cancel",
|
||||
"discussions.post.closePostModal.confirm": "Close post",
|
||||
"discussions.post.label.new": "{count} New",
|
||||
"discussions.post.editedBy": "Edited by",
|
||||
"discussions.post.editReason": "Reason",
|
||||
"discussions.post.postWithoutPreview": "No preview available",
|
||||
"discussions.post.follow.description": "you are following this post",
|
||||
"discussions.post.unfollow.description": "you are not following this post",
|
||||
"discussions.app.title": "Discussions",
|
||||
"discussions.posts.actionBar.searchAllPosts": "Search all posts",
|
||||
"discussions.topics.sort.message": "{sortBy} द्वारा क्रमबद्ध",
|
||||
"discussions.topics.sort.lastActivity": "हाल की गतिविधि",
|
||||
"discussions.topics.sort.commentCount": "सर्वाधिक गतिविधि",
|
||||
"discussions.topics.sort.courseStructure": "पाठ्यक्रम संरचना",
|
||||
"discussions.topics.unnamed.label": "अनाम श्रेणी",
|
||||
"discussions.subtopics.unnamed.label": "अनाम उपश्रेणी",
|
||||
"tour.action.advance": "अगला",
|
||||
"tour.action.dismiss": "ख़ारिज करें",
|
||||
"tour.action.end": "ठीक है",
|
||||
"tour.body.notRespondedFilter": "अब आप बिना प्रतिक्रिया वाली पोस्ट ढूंढने के लिए चर्चाओं को फ़िल्टर कर सकते हैं।",
|
||||
"tour.title.notRespondedFilter": "नया फ़िल्टरिंग विकल्प!",
|
||||
"tour.body.responseSortTour": "प्रतिक्रियाएँ और टिप्पणियाँ अब सबसे पहले नवीनतम के अनुसार क्रमबद्ध की जाती हैं। कृपया क्रम बदलने के लिए इस विकल्प का उपयोग करें",
|
||||
"tour.title.responseSortTour": "प्रतिक्रियाएँ क्रमबद्ध करें!",
|
||||
"learn.course.tabs.navigation.overflow.menu": "ज़्यादा...",
|
||||
"discussions.navigation.breadcrumbMenu.allTopics": "विषय",
|
||||
"discussions.navigation.breadcrumbMenu.showAll": "सभी दिखाएं",
|
||||
"discussions.navigation.navigationBar.allPosts": "सभी पोस्ट",
|
||||
"discussions.navigation.navigationBar.allTopics": "विषय",
|
||||
"discussions.navigation.navigationBar.myPosts": "मेरी पोस्ट",
|
||||
"discussions.navigation.navigationBar.learners": "छात्र",
|
||||
"discussions.post.author.anonymous": "गुमनाम",
|
||||
"discussions.post.addResponse": "जवाब जोड़ें",
|
||||
"discussions.post.lastResponse": "अंतिम प्रतिक्रिया {time}",
|
||||
"discussions.post.postedOn": "{time} {author} {authorLabel} द्वारा पोस्ट",
|
||||
"discussions.post.contentReported": "सूचित किया जा चूका है",
|
||||
"discussions.post.following": "फ़ॉलोइंग",
|
||||
"discussions.post.follow": "फ़ॉलो",
|
||||
"discussions.post.followed": "अनुसरित",
|
||||
"discussions.post.notFollowed": "नहीं अनुसरित",
|
||||
"discussions.post.answered": "उत्तरित",
|
||||
"discussions.post.unFollow": "करें",
|
||||
"discussions.post.like": "जैसे",
|
||||
"discussions.post.removeLike": "अलग",
|
||||
"discussions.post.liked": "पसंद की",
|
||||
"discussions.post.likes": "पसंद",
|
||||
"discussions.post.viewActivity": "गतिविधि देखें",
|
||||
"discussions.post.activity": "पुनः प्रयास करें",
|
||||
"discussions.post.closed": "प्रतिक्रियाओं और टिप्पणियों के लिए पोस्ट बंद है",
|
||||
"discussions.post.relatedTo": "संबंधित",
|
||||
"discussions.editor.delete.post.title": "पोस्ट हटाएं",
|
||||
"discussions.editor.delete.post.description": "क्या आप वाकई इस पोस्ट को स्थायी रूप से हटाना चाहते हैं?",
|
||||
"discussions.post.delete.confirmation.button.delete": "नष्ट करें",
|
||||
"discussions.editor.report.post.title": "अनुचित सामग्री की रिपोर्ट करें?",
|
||||
"discussions.editor.report.post.description": "चर्चा मॉडरेशन टीम इस सामग्री की समीक्षा करेगी और उचित कार्रवाई करेगी।",
|
||||
"discussions.post.closePostModal.title": "पोस्ट बंद करें",
|
||||
"discussions.post.closePostModal.text": "इस पोस्ट को बंद करने का कारण दर्ज करें। यह केवल अन्य मॉडरेटर्स को प्रदर्शित होगा।",
|
||||
"discussions.post.closePostModal.reasonCodeInput": "कारण",
|
||||
"discussions.post.closePostModal.cancel": "रद्द करना",
|
||||
"discussions.post.closePostModal.confirm": "पोस्ट बंद करें",
|
||||
"discussions.post.label.new": "{count} नया",
|
||||
"discussions.post.editedBy": "द्वारा संपादित",
|
||||
"discussions.post.editReason": "कारण",
|
||||
"discussions.post.postWithoutPreview": "कोई पूर्वावलोकन उपलब्ध नहीं है",
|
||||
"discussions.post.follow.description": "आप इस पोस्ट का पालन कर रहे हैं",
|
||||
"discussions.post.unfollow.description": "आप इस पोस्ट का पालन नहीं कर रहे हैं",
|
||||
"discussions.app.title": "चर्चाएँ",
|
||||
"discussions.posts.actionBar.searchAllPosts": "सभी पोस्ट खोजें",
|
||||
"discussions.posts.actionBar.search": "{page, select, topics {Search topics} posts {Search all posts} learners {Search learners} myPosts {Search all posts} other {{page}} }",
|
||||
"discussions.actionBar.searchInfo": "Showing {count} results for \"{text}\"",
|
||||
"discussions.actionBar.searchRewriteInfo": "No results found for \"{searchString}\". Showing {count} results for \"{textSearchRewrite}\".",
|
||||
"discussions.actionBar.searchInfoSearching": "Searching...",
|
||||
"discussions.actionBar.clearSearch": "Clear results",
|
||||
"discussion.posts.actionBar.add": "Add a post",
|
||||
"discussion.posts.actionBar.close": "Close",
|
||||
"discussions.post.editor.type": "Post type",
|
||||
"discussions.post.editor.addPostHeading": "Add a post",
|
||||
"discussions.post.editor.editPostHeading": "Edit post",
|
||||
"discussions.post.editor.typeDescription": "Questions raise issues that need answers. Discussions share ideas and start conversations.",
|
||||
"discussions.post.editor.required": "Required",
|
||||
"discussions.post.editor.questionType": "Question",
|
||||
"discussions.post.editor.questionDescription": "Raise issues that need answers",
|
||||
"discussions.post.editor.discussionType": "Discussion",
|
||||
"discussions.post.editor.discussionDescription": "Share ideas and start conversations",
|
||||
"discussions.post.editor.topicArea": "Topic area",
|
||||
"discussions.post.editor.topicAreaDescription": "Add your post to a relevant topic to help others find it.",
|
||||
"discussions.post.editor.cohortVisibility": "Cohort visibility",
|
||||
"discussions.post.editor.cohortVisibilityAllLearners": "All learners",
|
||||
"discussions.post.editor.title": "Post title",
|
||||
"discussions.post.editor.titleDescription": "Add a clear and descriptive title to encourage participation.",
|
||||
"discussions.post.editor.title.error": "Post title cannot be empty.",
|
||||
"discussions.post.editor.content.error": "Post content cannot be empty.",
|
||||
"discussions.post.editor.questionText": "Your question or idea (required)",
|
||||
"discussions.post.editor.preview": "Preview",
|
||||
"discussions.post.editor.followPost": "Follow this post",
|
||||
"discussions.post.editor.anonymousPost": "Post anonymously",
|
||||
"discussions.post.editor.anonymousToPeersPost": "Post anonymously to peers",
|
||||
"discussions.editor.posts.editReasonCode": "Reason for editing",
|
||||
"discussions.editor.posts.showPreview.button": "Show preview",
|
||||
"discussions.topic.noName.label": "Unnamed category",
|
||||
"discussions.subtopic.noName.label": "Unnamed subcategory",
|
||||
"discussions.posts.filter.showALl": "Show all",
|
||||
"discussions.posts.filter.discussions": "Discussions",
|
||||
"discussions.posts.filter.questions": "Questions",
|
||||
"discussions.posts.filter.message": "Status: {filterBy}",
|
||||
"discussions.posts.status.filter.anyStatus": "Any status",
|
||||
"discussions.posts.status.filter.unread": "Unread",
|
||||
"discussions.posts.status.filter.following": "Following",
|
||||
"discussions.posts.status.filter.reported": "Reported",
|
||||
"discussions.posts.status.filter.unanswered": "Unanswered",
|
||||
"discussions.posts.status.filter.unresponded": "Not responded",
|
||||
"discussions.posts.filter.myPosts": "My posts",
|
||||
"discussions.posts.filter.myDiscussions": "My discussions",
|
||||
"discussions.posts.filter.myQuestions": "My questions",
|
||||
"discussions.posts.sort.message": "Sorted by {sortBy}",
|
||||
"discussions.posts.sort.lastActivity": "Recent activity",
|
||||
"discussions.posts.sort.commentCount": "Most activity",
|
||||
"discussions.posts.sort.voteCount": "Most likes",
|
||||
"discussions.actionBar.searchInfo": "\"{text}\" के लिए {count} परिणाम दिखा रहा है",
|
||||
"discussions.actionBar.searchRewriteInfo": "\"{searchString}\" के लिए कोई परिणाम नहीं मिले। \"{textSearchRewrite}\" के लिए {count} परिणाम दिखा रहे हैं।",
|
||||
"discussions.actionBar.searchInfoSearching": "खोज रहा है...",
|
||||
"discussions.actionBar.clearSearch": "स्पष्ट परिणाम",
|
||||
"discussion.posts.actionBar.add": "एक पोस्ट जोड़ें",
|
||||
"discussion.posts.actionBar.close": "बंद करे",
|
||||
"discussions.post.editor.type": "पद प्रकार",
|
||||
"discussions.post.editor.addPostHeading": "एक पोस्ट जोड़ें",
|
||||
"discussions.post.editor.editPostHeading": "संपादित पोस्ट",
|
||||
"discussions.post.editor.typeDescription": "प्रश्न ऐसे मुद्दे उठाते हैं जिनके उत्तर की आवश्यकता होती है। चर्चाएँ विचार साझा करती हैं और बातचीत शुरू करती हैं।",
|
||||
"discussions.post.editor.required": "आवश्यक",
|
||||
"discussions.post.editor.questionType": "सवाल",
|
||||
"discussions.post.editor.questionDescription": "जवाब दें",
|
||||
"discussions.post.editor.discussionType": "चर्चा",
|
||||
"discussions.post.editor.discussionDescription": "विचारों को साझा करें और बातचीत शुरू करें",
|
||||
"discussions.post.editor.topicArea": "विषय क्षेत्र",
|
||||
"discussions.post.editor.topicAreaDescription": "दूसरों को इसे ढूंढने में मदद करने के लिए अपनी पोस्ट को किसी प्रासंगिक विषय पर जोड़ें।",
|
||||
"discussions.post.editor.cohortVisibility": "समूह दृश्यता",
|
||||
"discussions.post.editor.cohortVisibilityAllLearners": "सभी शिक्षार्थी",
|
||||
"discussions.post.editor.title": "पोस्ट शीर्षक",
|
||||
"discussions.post.editor.titleDescription": "भागीदारी को प्रोत्साहित करने के लिए एक स्पष्ट और वर्णनात्मक शीर्षक जोड़ें।",
|
||||
"discussions.post.editor.title.error": "पोस्ट शीर्षक खाली नहीं हो सकता है।",
|
||||
"discussions.post.editor.content.error": "पोस्ट सामग्री खाली नहीं हो सकती।",
|
||||
"discussions.post.editor.questionText": "आपका प्रश्न या विचार (आवश्यक)",
|
||||
"discussions.post.editor.preview": "पूर्वावलोकन",
|
||||
"discussions.post.editor.followPost": "इस पोस्ट को फ़ॉलो करें",
|
||||
"discussions.post.editor.anonymousPost": "गुमनाम रूप से पोस्ट करें",
|
||||
"discussions.post.editor.anonymousToPeersPost": "साथियों को गुमनाम रूप से पोस्ट करें",
|
||||
"discussions.editor.posts.editReasonCode": "संपादन का कारण",
|
||||
"discussions.editor.posts.showPreview.button": "पूर्वावलोकन दिखाएं",
|
||||
"discussions.topic.noName.label": "अनाम श्रेणी",
|
||||
"discussions.subtopic.noName.label": "अनाम उपश्रेणी",
|
||||
"discussions.posts.filter.showALl": "सभी दिखाएं",
|
||||
"discussions.posts.filter.discussions": "चर्चाएँ",
|
||||
"discussions.posts.filter.questions": "प्रश्न",
|
||||
"discussions.posts.filter.message": "स्थिति: {filterBy}",
|
||||
"discussions.posts.status.filter.anyStatus": "कोई स्थिति",
|
||||
"discussions.posts.status.filter.unread": "अपठित",
|
||||
"discussions.posts.status.filter.following": "फ़ॉलोइंग",
|
||||
"discussions.posts.status.filter.reported": "सूचित किया जा चूका है",
|
||||
"discussions.posts.status.filter.unanswered": "अनुत्तरित",
|
||||
"discussions.posts.status.filter.unresponded": "जवाब नहीं दिया",
|
||||
"discussions.posts.filter.myPosts": "मेरे पोस्ट",
|
||||
"discussions.posts.filter.myDiscussions": "मेरी चर्चाएँ",
|
||||
"discussions.posts.filter.myQuestions": "मेरे सवाल",
|
||||
"discussions.posts.sort.message": "{sortBy} द्वारा क्रमबद्ध",
|
||||
"discussions.posts.sort.lastActivity": "हाल की गतिविधि",
|
||||
"discussions.posts.sort.commentCount": "सर्वाधिक गतिविधि",
|
||||
"discussions.posts.sort.voteCount": "सबसे अधिक पसंद",
|
||||
"discussions.posts.sort-filter.sortFilterStatus": "{own, select, false {All} true {Own} other {{own}} } {status, select, statusAll {} statusUnread {unread} statusFollowing {followed} statusReported {reported} statusUnanswered {unanswered} statusUnresponded {unresponded} other {{status}} } {type, select, discussion {discussions} question {questions} all {posts} other {{type}} } {cohortType, select, all {} group {in {cohort}} other {{cohortType}} } sorted by {sort, select, lastActivityAt {recent activity} commentCount {most activity} voteCount {most likes} other {{sort}} }"
|
||||
}
|
||||
@@ -1,29 +1,29 @@
|
||||
{
|
||||
"discussions.actions.button.alt": "Actions menu",
|
||||
"discussions.actions.copylink": "Copy link",
|
||||
"discussions.actions.edit": "Edit",
|
||||
"discussions.actions.pin": "Pin",
|
||||
"discussions.actions.unpin": "Unpin",
|
||||
"discussions.actions.delete": "Delete",
|
||||
"discussions.confirmation.button.confirm": "Confirm",
|
||||
"discussions.actions.close": "Close",
|
||||
"discussions.actions.reopen": "Reopen",
|
||||
"discussions.actions.report": "Report",
|
||||
"discussions.actions.unreport": "Unreport",
|
||||
"discussions.actions.endorse": "Endorse",
|
||||
"discussions.actions.unendorse": "Unendorse",
|
||||
"discussions.actions.markAnswered": "Mark as answered",
|
||||
"discussions.actions.unMarkAnswered": "Unmark as answered",
|
||||
"discussions.modal.confirmation.button.cancel": "Cancel",
|
||||
"discussions.empty.allTopics": "All discussion activity for these topics will show up here.",
|
||||
"discussions.empty.allPosts": "All discussion activity for your course will show up here.",
|
||||
"discussions.empty.myPosts": "Posts you've interacted with will show up here.",
|
||||
"discussions.empty.topic": "All discussion activity for this topic will show up here.",
|
||||
"discussions.empty.title": "Nothing here yet",
|
||||
"discussions.empty.noPostSelected": "No post selected",
|
||||
"discussions.empty.noTopicSelected": "No topic selected",
|
||||
"discussions.sidebar.noResultsFound": "No results found",
|
||||
"discussions.sidebar.differentKeywords": "Try searching different keywords",
|
||||
"discussions.actions.button.alt": "操作菜单",
|
||||
"discussions.actions.copylink": "复制链接",
|
||||
"discussions.actions.edit": "编辑",
|
||||
"discussions.actions.pin": "处理",
|
||||
"discussions.actions.unpin": "不做处理",
|
||||
"discussions.actions.delete": "删除",
|
||||
"discussions.confirmation.button.confirm": "确认",
|
||||
"discussions.actions.close": "关闭",
|
||||
"discussions.actions.reopen": "重开",
|
||||
"discussions.actions.report": "报告",
|
||||
"discussions.actions.unreport": "取消报告",
|
||||
"discussions.actions.endorse": "支持",
|
||||
"discussions.actions.unendorse": "取消支持",
|
||||
"discussions.actions.markAnswered": "标记为已回答",
|
||||
"discussions.actions.unMarkAnswered": "取消标记为已回答",
|
||||
"discussions.modal.confirmation.button.cancel": "取消",
|
||||
"discussions.empty.allTopics": "这些主题的所有讨论活动都将显示在这里。",
|
||||
"discussions.empty.allPosts": "您课程的所有讨论活动都将显示在这里。",
|
||||
"discussions.empty.myPosts": "您互动过的帖子会显示在这里。",
|
||||
"discussions.empty.topic": "该主题的所有讨论活动都将显示在这里。",
|
||||
"discussions.empty.title": "什么都没有",
|
||||
"discussions.empty.noPostSelected": "未选择帖子",
|
||||
"discussions.empty.noTopicSelected": "未选择主题",
|
||||
"discussions.sidebar.noResultsFound": "未找到结果",
|
||||
"discussions.sidebar.differentKeywords": "尝试搜索不同的关键字",
|
||||
"discussions.sidebar.removeKeywords": "Try searching different keywords or removing some filters",
|
||||
"discussions.sidebar.removeKeywordsOnly": "Try searching different keywords",
|
||||
"discussions.sidebar.removeFilters": "Try removing some filters",
|
||||
|
||||
@@ -140,11 +140,6 @@ $fa-font-path: "~font-awesome/fonts";
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.mx-3px {
|
||||
margin-left: 3px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.mt-14px {
|
||||
margin-top: 14px;
|
||||
}
|
||||
@@ -340,6 +335,10 @@ header {
|
||||
.nav-item:not(:last-child){
|
||||
.nav-link {
|
||||
border-right: 0;
|
||||
|
||||
@media screen and (max-width: 567px) {
|
||||
border-right: solid 1px #e9e6e4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -488,6 +487,11 @@ header {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.comment-line {
|
||||
width: calc(100% - 180px);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.post-preview,
|
||||
.discussion-comments {
|
||||
blockquote {
|
||||
@@ -529,3 +533,8 @@ header {
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.author-name {
|
||||
line-height: 1;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user