Compare commits

...

21 Commits

Author SHA1 Message Date
ayeshoali
ed3addc021 chore: removed enable_learners_tab_in_discussions_mfe flag dependency 2024-01-05 16:24:06 +05:00
sundasnoreen12
b467298d9a fix: fixed UI issues of discussion for incontext sidebar (#633)
* fix: fixed UI issues of discussion for incontext sidebar

* refactor: added paragon class

* refactor: improved actionBar UI

---------

Co-authored-by: Awais Ansari <awais.ansari63@gmail.com>
2024-01-02 13:35:47 +05:00
renovate[bot]
b5d036a54d fix(deps): update dependency regenerator-runtime to v0.14.1 2023-12-18 15:05:01 +00:00
renovate[bot]
bc997108ef chore(deps): update dependency @edx/frontend-build to v13.0.14 (#628)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-18 18:33:51 +05:00
vladislavkeblysh
6ae5130c14 feat: fixed page styles (#577) 2023-12-18 16:26:38 +05:00
Jenkins
67d79cb3aa chore(i18n): update translations 2023-12-17 15:22:26 -05:00
renovate[bot]
f31a0e71f3 fix(deps): update dependency formik to v2.4.5 (#619)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-14 15:15:42 +05:00
renovate[bot]
9761787c89 fix(deps): update dependency @reduxjs/toolkit to v1.9.7 (#616)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-14 12:30:12 +05:00
renovate[bot]
e5a21f4a75 chore(deps): update dependency @edx/frontend-build to v13.0.12 (#624)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-12 14:16:54 +05:00
renovate[bot]
1d89e9556a fix(deps): update dependency @edx/frontend-component-footer to v12.6.1 (#625)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-12 14:11:01 +05:00
Syed Ali Abbas Zaidi
b35632df64 feat: upgrade react router to v6 (#542)
* feat: upgrade react router to v6

* fix: routing issues

* fix: category route should redirect to all posts

* fix: path error on routes
2023-12-07 18:10:48 +05:00
Kshitij Sobti
b36c0266fd fix: null error at useRouteMatch when running on tutor (#613)
tutor sets the PUBLIC_PATH to '/discussions' which causes frontend-platform to
treat all URLs for matching etc to be relative to this path. Since many places
include '/discussions' in the match it causes those matches to break.

This change makes the default PUBLIC_PATH in .env.development to match the one
set by tutor and removes it from the base path of the router letting frontend
platform handle the prefix.

This also allows for deployments to customise this path to be something other
than 'discussions'.
2023-12-06 17:20:28 +05:00
renovate[bot]
0d5df18ab2 fix(deps): update dependency redux to v4.2.1 (#621)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-05 12:21:46 +05:00
renovate[bot]
c61435546d fix(deps): update dependency regenerator-runtime to v0.14.0 (#622)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-05 12:03:29 +05:00
renovate[bot]
df4a3c2a73 chore(deps): update dependency rosie to v2.1.1 (#605)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-04 16:15:31 +05:00
renovate[bot]
ac635edcb8 chore(deps): update dependency @edx/frontend-build to v13.0.8 (#608)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-04 13:07:44 +05:00
renovate[bot]
c4f7115732 fix(deps): update dependency @edx/frontend-component-footer to v12.6.0 (#609)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-04 13:01:08 +05:00
renovate[bot]
5cc8ba43fe fix(deps): update dependency @edx/frontend-component-header to v4.10.1 (#610)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-04 12:51:30 +05:00
renovate[bot]
68505821bb fix(deps): update dependency @edx/paragon to v20.46.3 (#611)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-04 11:51:58 +05:00
Ahtisham Shahid
c6d953fe7b feat: removed enable_moderation_reason_codes flag (#615)
* chore: removed deprecated flag

fix: resolved linter error

fix: changed workflow

fix: changed workflow

fix: changed workflow

fix: changed workflow

fix: changed workflow

fix: changed workflow

fix: changed workflow

fix: changed workflow

fix: changed workflow

fix: changed workflow

fix: changed workflow

fix: changed workflow

* test: fixed postEditor test case

---------

Co-authored-by: Awais Ansari <awais.ansari63@gmail.com>
2023-12-03 22:49:08 +05:00
Jenkins
a479f5ae5b chore(i18n): update translations 2023-11-19 15:22:18 -05:00
59 changed files with 1449 additions and 1189 deletions

1143
package-lock.json generated
View File

@@ -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",

View File

@@ -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"
}
}

View File

@@ -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';

View File

@@ -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',
};
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;

View File

@@ -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

View File

@@ -90,7 +90,6 @@ describe.each([
store = initializeStore({
config: {
hasModerationPrivileges: true,
reasonCodesEnabled: true,
},
});
const content = buildTestContent(type, props);

View File

@@ -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,
})}

View File

@@ -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, {});

View File

@@ -84,7 +84,6 @@ describe.each([
store = initializeStore({
config: {
hasModerationPrivileges: true,
reasonCodesEnabled: true,
},
});
const content = buildTestContent(type, props);

View File

@@ -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>

View File

@@ -4,7 +4,9 @@ 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';
@@ -29,8 +31,6 @@ import {
selectIsCourseAdmin,
selectIsCourseStaff,
selectIsPostingEnabled,
selectLearnersTabEnabled,
selectModerationSettings,
selectPostThreadCount,
selectUserHasModerationPrivileges,
selectUserIsGroupTa,
@@ -53,16 +53,18 @@ 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;
}
@@ -85,7 +87,7 @@ export function useCourseDiscussionData(courseId) {
export function useRedirectToThread(courseId, enableInContextSidebar) {
const dispatch = useDispatch();
const history = useHistory();
const navigate = useNavigate();
const location = useLocation();
const redirectToThread = useSelector(
@@ -102,7 +104,7 @@ export function useRedirectToThread(courseId, enableInContextSidebar) {
postId: redirectToThread.threadId,
topicId: redirectToThread.topicId,
})(location);
history.push(newLocation);
navigate({ ...newLocation });
}
}, [redirectToThread]);
}
@@ -159,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.

View File

@@ -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;

View File

@@ -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,

View File

@@ -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>

View File

@@ -6,17 +6,15 @@ 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 { RequestStatus, Routes as ROUTES } from '../../data/constants';
import { DiscussionContext } from '../common/context';
import {
useContainerSize, useIsOnDesktop, useIsOnXLDesktop, useShowLearnersTab,
} from '../data/hooks';
import { useContainerSize, useIsOnDesktop, useIsOnXLDesktop } from '../data/hooks';
import { selectConfigLoadingStatus, selectEnableInContext } from '../data/selectors';
const TopicPostsView = lazy(() => import('../in-context-topics/TopicPostsView'));
@@ -27,13 +25,11 @@ const PostsView = lazy(() => import('../posts/PostsView'));
const LegacyTopicsView = lazy(() => import('../topics/TopicsView'));
const DiscussionSidebar = ({ displaySidebar, postActionBarRef }) => {
const location = useLocation();
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();
@@ -62,47 +58,58 @@ const DiscussionSidebar = ({ displaySidebar, postActionBarRef }) => {
data-testid="sidebar"
>
<Suspense fallback={(<Spinner />)}>
<Switch>
<Routes>
{enableInContext && !enableInContextSidebar && (
<Route
path={Routes.TOPICS.ALL}
component={InContextTopicsView}
exact
/>
<Route
path={ROUTES.TOPICS.ALL}
element={<InContextTopicsView />}
/>
)}
{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} />
[
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 && (
<Redirect
from={Routes.DISCUSSIONS.PATH}
to={{
...location,
pathname: Routes.POSTS.ALL_POSTS,
}}
/>
<Route path={`${ROUTES.DISCUSSIONS.PATH}/*`} element={<Navigate to="posts" />} />
)}
</Switch>
</Routes>
</Suspense>
</div>
);

View File

@@ -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>

View File

@@ -4,17 +4,17 @@ import React, { lazy, Suspense, useRef } from 'react';
import classNames from 'classnames';
import { useSelector } from 'react-redux';
import {
Route, Switch, useLocation, useRouteMatch,
} from 'react-router';
matchPath, Route, Routes, useLocation, useMatch,
} from 'react-router-dom';
import { LearningHeader as Header } from '@edx/frontend-component-header';
import { Spinner } from '../../components';
import { selectCourseTabs } from '../../components/NavigationBar/data/selectors';
import { ALL_ROUTES, DiscussionProvider, Routes } from '../../data/constants';
import { ALL_ROUTES, DiscussionProvider, Routes as ROUTES } from '../../data/constants';
import { DiscussionContext } from '../common/context';
import {
useCourseDiscussionData, useIsOnDesktop, useRedirectToThread, useShowLearnersTab, useSidebarVisible,
useCourseDiscussionData, useIsOnDesktop, useRedirectToThread, useSidebarVisible,
} from '../data/hooks';
import { selectDiscussionProvider, selectEnableInContext } from '../data/selectors';
import { EmptyLearners, EmptyPosts, EmptyTopics } from '../empty-posts';
@@ -40,9 +40,10 @@ const DiscussionsHome = () => {
const provider = useSelector(selectDiscussionProvider);
const enableInContext = useSelector(selectEnableInContext);
const { courseNumber, courseTitle, org } = useSelector(selectCourseTabs);
const { params: { page } } = useRouteMatch(`${Routes.COMMENTS.PAGE}?`);
const { params } = useRouteMatch(ALL_ROUTES);
const isRedirectToLearners = useShowLearnersTab();
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);
@@ -84,7 +85,7 @@ const DiscussionsHome = () => {
>
<div
className={classNames('d-flex flex-row justify-content-between navbar fixed-top', {
'pl-4 pr-3 py-0': enableInContextSidebar,
'pl-4 pr-2 py-0': enableInContextSidebar,
})}
>
{!enableInContextSidebar && (
@@ -95,12 +96,24 @@ const DiscussionsHome = () => {
<DiscussionsRestrictionBanner />
</div>
{provider === DiscussionProvider.LEGACY && (
<Suspense fallback={(<Spinner />)}>
<Route
path={[Routes.POSTS.PATH, Routes.TOPICS.CATEGORY]}
component={LegacyBreadcrumbMenu}
/>
</Suspense>
<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 />)}>
@@ -112,21 +125,29 @@ const DiscussionsHome = () => {
</Suspense>
)}
{!displayContentArea && (
<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>
<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 && (

View File

@@ -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`);

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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) {

View File

@@ -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}

View File

@@ -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"

View File

@@ -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">

View File

@@ -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>

View File

@@ -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">

View File

@@ -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();

View File

@@ -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 () => {

View File

@@ -18,7 +18,7 @@ const LearnerCard = ({ learner }) => {
0: enableInContextSidebar ? 'in-context' : undefined,
learnerUsername: learner.username,
courseId,
});
})();
return (
<Link

View File

@@ -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>

View File

@@ -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', '#');

View File

@@ -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>,

View File

@@ -1,21 +1,20 @@
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 { DiscussionContext } from '../../common/context';
import { useShowLearnersTab } from '../../data/hooks';
import { discussionsPath } from '../../utils';
import messages from './messages';
const NavigationBar = () => {
const intl = useIntl();
const { courseId } = useContext(DiscussionContext);
const showLearnersTab = useShowLearnersTab();
const location = useLocation();
const isTopicsNavActive = Boolean(matchPath({ path: `${Routes.TOPICS.CATEGORY}/*` }, location.pathname));
const navLinks = useMemo(() => ([
{
@@ -28,19 +27,14 @@ 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]);
]), []);
return (
<Nav variant="pills" className="py-2 nav-button-group">
@@ -49,8 +43,8 @@ const NavigationBar = () => {
<Nav.Link
key={link.route}
as={NavLink}
to={discussionsPath(link.route, { courseId })}
isActive={link.isActive}
to={discussionsPath(link.route, { courseId })()}
className={isTopicsNavActive && link.route === Routes.TOPICS.ALL && 'active'}
>
{intl.formatMessage(link.labelMessage)}
</Nav.Link>

View File

@@ -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)}
/>
)

View File

@@ -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');

View File

@@ -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"
>

View File

@@ -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
);

View File

@@ -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>

View File

@@ -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>

View File

@@ -26,6 +26,7 @@ Factory.define('thread')
'type',
'voted',
'pinned',
'copy_link',
],
author: 'test_user',
author_label: 'Staff',

View File

@@ -67,15 +67,14 @@ const PostActionsBar = () => {
)}
{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>
</>

View File

@@ -1,3 +1,4 @@
.small-font {
font-size: .875rem !important;
}

View File

@@ -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]);

View File

@@ -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',

View File

@@ -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(

View File

@@ -3,7 +3,7 @@ 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';
@@ -25,6 +25,7 @@ const PostLink = ({
showDivider,
}) => {
const intl = useIntl();
const { search } = useLocation();
const {
courseId,
postId: selectedPostId,
@@ -37,14 +38,14 @@ const PostLink = ({
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;
@@ -63,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}

View File

@@ -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}

View File

@@ -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';

View File

@@ -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>

View File

@@ -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>

View File

@@ -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"

View File

@@ -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('/'));
}

View File

@@ -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}} }"
}

View File

@@ -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",

View File

@@ -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;
}