Compare commits
1 Commits
open-relea
...
inf-554-2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
034638f093 |
5
.env
5
.env
@@ -20,6 +20,5 @@ SEGMENT_KEY=''
|
||||
SITE_NAME=''
|
||||
USER_INFO_COOKIE_NAME=''
|
||||
SUPPORT_URL=''
|
||||
TA_FEEDBACK_FORM= ''
|
||||
STAFF_FEEDBACK_FORM= ''
|
||||
DISPLAY_FEEDBACK_BANNER='false'
|
||||
TA_FEEDBACK_FORM: ''
|
||||
STAFF_FEEDBACK_FORM: ''
|
||||
@@ -21,6 +21,5 @@ SEGMENT_KEY=''
|
||||
SITE_NAME=localhost
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
SUPPORT_URL='https://support.edx.org'
|
||||
TA_FEEDBACK_FORM='https://learner-form.test'
|
||||
STAFF_FEEDBACK_FORM='https://staff-form.test'
|
||||
DISPLAY_FEEDBACK_BANNER='false'
|
||||
TA_FEEDBACK_FORM: 'https://learner-form.test'
|
||||
STAFF_FEEDBACK_FORM: 'https://staff-form.test'
|
||||
@@ -19,6 +19,5 @@ SEGMENT_KEY=''
|
||||
SITE_NAME='localhost'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
SUPPORT_URL='https://support.edx.org'
|
||||
TA_FEEDBACK_FORM='https://learner-form.test'
|
||||
STAFF_FEEDBACK_FORM='https://staff-form.test'
|
||||
DISPLAY_FEEDBACK_BANNER='false'
|
||||
TA_FEEDBACK_FORM: 'https://learner-form.test'
|
||||
STAFF_FEEDBACK_FORM: 'https://staff-form.test'
|
||||
2
Makefile
2
Makefile
@@ -59,7 +59,7 @@ push_translations:
|
||||
|
||||
# Pulls translations from Transifex.
|
||||
pull_translations:
|
||||
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
|
||||
tx pull -f --mode reviewed --languages=$(transifex_langs)
|
||||
|
||||
# This target is used by Travis.
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
|
||||
@@ -8,4 +8,5 @@ openedx-release:
|
||||
# The openedx-release key is described in OEP-10:
|
||||
# https://open-edx-proposals.readthedocs.io/en/latest/oep-0010-proc-openedx-releases.html
|
||||
# The FAQ might also be helpful: https://openedx.atlassian.net/wiki/spaces/COMM/pages/1331268879/Open+edX+Release+FAQ
|
||||
maybe: true # Delete this "maybe" line when you have decided about Open edX inclusion.
|
||||
ref: master
|
||||
|
||||
1262
package-lock.json
generated
1262
package-lock.json
generated
@@ -10,10 +10,10 @@
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
||||
"@edx/frontend-component-footer": "12.0.0",
|
||||
"@edx/frontend-component-header": "4.0.0",
|
||||
"@edx/frontend-platform": "4.2.0",
|
||||
"@edx/paragon": "20.15.0",
|
||||
"@edx/frontend-component-footer": "11.2.0",
|
||||
"@edx/frontend-component-header": "3.2.0",
|
||||
"@edx/frontend-platform": "2.6.1",
|
||||
"@edx/paragon": "19.10.1",
|
||||
"@reduxjs/toolkit": "1.8.0",
|
||||
"@tinymce/tinymce-react": "3.13.1",
|
||||
"babel-polyfill": "6.26.0",
|
||||
@@ -3395,42 +3395,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-footer": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-12.0.0.tgz",
|
||||
"integrity": "sha512-m8Rx6ZPWzIN5XLrz6Ft3aTuFo0rty0jECd79CBYWdm0D9KD1WxoYEG+fElluyOQp/t42T5jLImHTSWjFURx5kw==",
|
||||
"version": "11.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-11.2.0.tgz",
|
||||
"integrity": "sha512-prN6SeoWenbNq6jCqlpmYx57OhNbVGw1wjQFVH+aA5JMjjLUf6PmjPG87fuD9udQyFteb2lvbCyongPEZOozrg==",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "6.4.0",
|
||||
"@fortawesome/free-brands-svg-icons": "6.4.0",
|
||||
"@fortawesome/free-regular-svg-icons": "6.4.0",
|
||||
"@fortawesome/free-solid-svg-icons": "6.4.0",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||
"@fortawesome/react-fontawesome": "0.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@edx/frontend-platform": "^4.0.0",
|
||||
"@edx/frontend-platform": "^2.3.0",
|
||||
"prop-types": "^15.5.10",
|
||||
"react": "^16.9.0 || ^17.0.0",
|
||||
"react-dom": "^16.9.0 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/fontawesome-common-types": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.0.tgz",
|
||||
"integrity": "sha512-HNii132xfomg5QVZw0HwXXpN22s7VBHQBv9CeOu9tfJnhsWQNd2lmTNi8CSrnw5B+5YOmzu1UoPAyxaXsJ6RgQ==",
|
||||
"hasInstallScript": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/fontawesome-svg-core": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.0.tgz",
|
||||
"integrity": "sha512-Bertv8xOiVELz5raB2FlXDPKt+m94MQ3JgDfsVbrqNpLU9+UE2E18GKjLKw+d3XbeYPqg1pzyQKGsrzbw+pPaw==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
"react": "^16.9.0",
|
||||
"react-dom": "^16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/react-fontawesome": {
|
||||
@@ -3446,110 +3425,25 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-header": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-component-header/-/frontend-component-header-4.0.0.tgz",
|
||||
"integrity": "sha512-r/L3p2ZSI1DitjxVKAor18GmgJllafYslrdpzGI0vcX/gTemH13jf2Xr9iQqrT921DP2nzZ5GOwGJNptTSjiaA==",
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-component-header/-/frontend-component-header-3.2.0.tgz",
|
||||
"integrity": "sha512-ep9yOwe9CG03lSf/QBsBOZO3S/Ksrttgi4MkjpUe/6Sm/KdAnMCOhx2Gbj9jBcFrHqCkpeef26zGjjhpHGDwkQ==",
|
||||
"dependencies": {
|
||||
"@edx/paragon": "20.30.1",
|
||||
"@fortawesome/fontawesome-svg-core": "6.3.0",
|
||||
"@fortawesome/free-brands-svg-icons": "6.3.0",
|
||||
"@fortawesome/free-regular-svg-icons": "6.3.0",
|
||||
"@fortawesome/free-solid-svg-icons": "6.3.0",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"babel-polyfill": "6.26.0",
|
||||
"react-responsive": "8.2.0",
|
||||
"react-transition-group": "4.4.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@edx/frontend-platform": "^4.0.0",
|
||||
"@edx/frontend-platform": "^2.0.0",
|
||||
"@edx/paragon": ">= 7.0.0 < 21.0.0",
|
||||
"prop-types": "^15.5.10",
|
||||
"react": "^16.9.0 || ^17.0.0",
|
||||
"react-dom": "^16.9.0 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-header/node_modules/@edx/paragon": {
|
||||
"version": "20.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.30.1.tgz",
|
||||
"integrity": "sha512-v3Ek8deZWqVKi3IWP08Mj4egrvbmbqQEyRA6+qazHZdgHJA4qOP1SST42UKd9XxPeRbLWUgaJWd0iBAOAna/gw==",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
||||
"@fortawesome/react-fontawesome": "^0.1.18",
|
||||
"@popperjs/core": "^2.11.4",
|
||||
"bootstrap": "^4.6.2",
|
||||
"classnames": "^2.3.1",
|
||||
"email-prop-type": "^3.0.0",
|
||||
"file-selector": "^0.6.0",
|
||||
"font-awesome": "^4.7.0",
|
||||
"glob": "^8.0.3",
|
||||
"lodash.uniqby": "^4.7.0",
|
||||
"mailto-link": "^2.0.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-bootstrap": "^1.6.5",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dropzone": "^14.2.1",
|
||||
"react-focus-on": "^3.5.4",
|
||||
"react-loading-skeleton": "^3.1.0",
|
||||
"react-popper": "^2.2.5",
|
||||
"react-proptype-conditional-require": "^1.0.4",
|
||||
"react-responsive": "^8.2.0",
|
||||
"react-table": "^7.7.0",
|
||||
"react-transition-group": "^4.4.2",
|
||||
"tabbable": "^5.3.3",
|
||||
"uncontrollable": "^7.2.1",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.6 || ^17.0.0",
|
||||
"react-dom": "^16.8.6 || ^17.0.0",
|
||||
"react-intl": "^5.25.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-header/node_modules/@edx/paragon/node_modules/@fortawesome/react-fontawesome": {
|
||||
"version": "0.1.19",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.19.tgz",
|
||||
"integrity": "sha512-Hyb+lB8T18cvLNX0S3llz7PcSOAJMLwiVKBuuzwM/nI5uoBw+gQjnf9il0fR1C3DKOI5Kc79pkJ4/xB0Uw9aFQ==",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "~1 || ~6",
|
||||
"react": ">=16.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/free-brands-svg-icons": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.3.0.tgz",
|
||||
"integrity": "sha512-xI0c+a8xnKItAXCN8rZgCNCJQiVAd2Y7p9e2ND6zN3J3ekneu96qrePieJ7yA7073C1JxxoM3vH1RU7rYsaj8w==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/free-regular-svg-icons": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.3.0.tgz",
|
||||
"integrity": "sha512-cZnwiVHZ51SVzWHOaNCIA+u9wevZjCuAGSvSYpNlm6A4H4Vhwh8481Bf/5rwheIC3fFKlgXxLKaw8Xeroz8Ntg==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/free-solid-svg-icons": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.3.0.tgz",
|
||||
"integrity": "sha512-x5tMwzF2lTH8pyv8yeZRodItP2IVlzzmBuD1M7BjawWgg9XAvktqJJ91Qjgoaf8qJpHQ8FEU9VxRfOkLhh86QA==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
"react": "^16.9.0",
|
||||
"react-dom": "^16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/react-fontawesome": {
|
||||
@@ -3564,63 +3458,18 @@
|
||||
"react": ">=16.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-header/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-header/node_modules/glob": {
|
||||
"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",
|
||||
"inherits": "2",
|
||||
"minimatch": "^5.0.1",
|
||||
"once": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-header/node_modules/minimatch": {
|
||||
"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"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-header/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==",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-platform": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-4.2.0.tgz",
|
||||
"integrity": "sha512-iDoFeccENQKBjqUgdjl5KSwBrjNEj8YW6Ual+6twcHHJUBg3yRoBEphwHIoRREcMgQjhdKVAdWj8eleh4JsEKA==",
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-2.6.1.tgz",
|
||||
"integrity": "sha512-5ZcHBvwmJRYPPKEqv+H4DbsfW9atr5V2+uY/zX58F/Tqh0T5X8MmgppVjplWFbmon5QV4aNw5C1ViQnirNqUiA==",
|
||||
"dependencies": {
|
||||
"@cospired/i18n-iso-languages": "2.2.0",
|
||||
"@formatjs/intl-pluralrules": "4.3.3",
|
||||
"@formatjs/intl-relativetimeformat": "10.0.1",
|
||||
"axios": "0.27.2",
|
||||
"axios-cache-interceptor": "0.10.7",
|
||||
"axios": "0.26.1",
|
||||
"axios-cache-adapter": "2.7.3",
|
||||
"form-urlencoded": "4.1.4",
|
||||
"glob": "7.2.3",
|
||||
"glob": "7.2.0",
|
||||
"history": "4.10.1",
|
||||
"i18n-iso-countries": "4.3.1",
|
||||
"jwt-decode": "3.1.2",
|
||||
@@ -3635,58 +3484,24 @@
|
||||
"universal-cookie": "4.0.4"
|
||||
},
|
||||
"bin": {
|
||||
"intl-imports.js": "i18n/scripts/intl-imports.js",
|
||||
"transifex-utils.js": "i18n/scripts/transifex-utils.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@edx/paragon": ">= 10.0.0 < 21.0.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^16.9.0 || ^17.0.0",
|
||||
"react-dom": "^16.9.0 || ^17.0.0",
|
||||
"react": "^16.9.0",
|
||||
"react-dom": "^16.9.0",
|
||||
"react-redux": "^7.1.1",
|
||||
"react-router-dom": "^5.0.1",
|
||||
"redux": "^4.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-platform/node_modules/axios": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
|
||||
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
|
||||
"version": "0.26.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
|
||||
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.9",
|
||||
"form-data": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-platform/node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-platform/node_modules/glob": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.1.1",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
"follow-redirects": "^1.14.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/new-relic-source-map-webpack-plugin": {
|
||||
@@ -3699,75 +3514,36 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/paragon": {
|
||||
"version": "20.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.15.0.tgz",
|
||||
"integrity": "sha512-Sq5/je3Ub3UpXrrneaCShz+kB6wuMNmeF1Y/XfVt6X2i+ZCQGlQrBa3KtcwGwDOHIQ65eC6dqJpL9LISHg+xtg==",
|
||||
"version": "19.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-19.10.1.tgz",
|
||||
"integrity": "sha512-kVIx/yJWYeaTIb+j2NmB/iz1XGR4WY8aYEQbt1FtGFWcKzeiThtiqr4TeRxQeA8UrooyQl5HyzYISq7xC5AH2w==",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
||||
"@fortawesome/react-fontawesome": "^0.1.18",
|
||||
"@popperjs/core": "^2.11.4",
|
||||
"bootstrap": "^4.6.2",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||
"@fortawesome/react-fontawesome": "^0.1.16",
|
||||
"@popperjs/core": "^2.11.2",
|
||||
"airbnb-prop-types": "^2.12.0",
|
||||
"bootstrap": "4.6.0",
|
||||
"classnames": "^2.3.1",
|
||||
"email-prop-type": "^3.0.0",
|
||||
"file-selector": "^0.6.0",
|
||||
"font-awesome": "^4.7.0",
|
||||
"glob": "^8.0.3",
|
||||
"lodash.uniqby": "^4.7.0",
|
||||
"mailto-link": "^2.0.0",
|
||||
"mailto-link": "^1.0.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-bootstrap": "^1.6.5",
|
||||
"react-dropzone": "^14.2.1",
|
||||
"react-bootstrap": "^1.6.4",
|
||||
"react-focus-on": "^3.5.4",
|
||||
"react-loading-skeleton": "^3.1.0",
|
||||
"react-popper": "^2.2.5",
|
||||
"react-proptype-conditional-require": "^1.0.4",
|
||||
"react-responsive": "^8.2.0",
|
||||
"react-table": "^7.7.0",
|
||||
"react-transition-group": "^4.4.2",
|
||||
"tabbable": "^5.3.3",
|
||||
"uncontrollable": "^7.2.1"
|
||||
"tabbable": "^4.0.0",
|
||||
"uncontrollable": "7.2.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.6 || ^17.0.0",
|
||||
"react-dom": "^16.8.6 || ^17.0.0",
|
||||
"react-intl": "^5.25.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/paragon/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^5.0.1",
|
||||
"once": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/reactifex": {
|
||||
@@ -4056,89 +3832,62 @@
|
||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-common-types": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.3.0.tgz",
|
||||
"integrity": "sha512-4BC1NMoacEBzSXRwKjZ/X/gmnbp/HU5Qqat7E8xqorUtBFZS+bwfGH5/wqOC2K6GV0rgEobp3OjGRMa5fK9pFg==",
|
||||
"version": "0.2.36",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz",
|
||||
"integrity": "sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg==",
|
||||
"hasInstallScript": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-svg-core": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.3.0.tgz",
|
||||
"integrity": "sha512-uz9YifyKlixV6AcKlOX8WNdtF7l6nakGyLYxYaCa823bEBqyj/U2ssqtctO38itNEwXb8/lMzjdoJ+aaJuOdrw==",
|
||||
"version": "1.2.36",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.36.tgz",
|
||||
"integrity": "sha512-YUcsLQKYb6DmaJjIHdDWpBIGCcyE/W+p/LMGvjQem55Mm2XWVAP5kWTMKWLv9lwpCVjpLxPyOMOyUocP1GxrtA==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.3.0"
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.36"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-brands-svg-icons": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.4.0.tgz",
|
||||
"integrity": "sha512-qvxTCo0FQ5k2N+VCXb/PZQ+QMhqRVM4OORiO6MXdG6bKolIojGU/srQ1ptvKk0JTbRgaJOfL2qMqGvBEZG7Z6g==",
|
||||
"version": "5.15.4",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-5.15.4.tgz",
|
||||
"integrity": "sha512-f1witbwycL9cTENJegcmcZRYyawAFbm8+c6IirLmwbbpqz46wyjbQYLuxOc7weXFXfB7QR8/Vd2u5R3q6JYD9g==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.4.0"
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.36"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-brands-svg-icons/node_modules/@fortawesome/fontawesome-common-types": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.0.tgz",
|
||||
"integrity": "sha512-HNii132xfomg5QVZw0HwXXpN22s7VBHQBv9CeOu9tfJnhsWQNd2lmTNi8CSrnw5B+5YOmzu1UoPAyxaXsJ6RgQ==",
|
||||
"hasInstallScript": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-regular-svg-icons": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.4.0.tgz",
|
||||
"integrity": "sha512-ZfycI7D0KWPZtf7wtMFnQxs8qjBXArRzczABuMQqecA/nXohquJ5J/RCR77PmY5qGWkxAZDxpnUFVXKwtY/jPw==",
|
||||
"version": "5.15.4",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.15.4.tgz",
|
||||
"integrity": "sha512-9VNNnU3CXHy9XednJ3wzQp6SwNwT3XaM26oS4Rp391GsxVYA+0oDR2J194YCIWf7jNRCYKjUCOduxdceLrx+xw==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.4.0"
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.36"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-regular-svg-icons/node_modules/@fortawesome/fontawesome-common-types": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.0.tgz",
|
||||
"integrity": "sha512-HNii132xfomg5QVZw0HwXXpN22s7VBHQBv9CeOu9tfJnhsWQNd2lmTNi8CSrnw5B+5YOmzu1UoPAyxaXsJ6RgQ==",
|
||||
"hasInstallScript": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-solid-svg-icons": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.0.tgz",
|
||||
"integrity": "sha512-kutPeRGWm8V5dltFP1zGjQOEAzaLZj4StdQhWVZnfGFCvAPVvHh8qk5bRrU4KXnRRRNni5tKQI9PBAdI6MP8nQ==",
|
||||
"version": "5.15.4",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.4.tgz",
|
||||
"integrity": "sha512-JLmQfz6tdtwxoihXLg6lT78BorrFyCf59SAwBM6qV/0zXyVeDygJVb3fk+j5Qat+Yvcxp1buLTY5iDh1ZSAQ8w==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.4.0"
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.36"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-solid-svg-icons/node_modules/@fortawesome/fontawesome-common-types": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.0.tgz",
|
||||
"integrity": "sha512-HNii132xfomg5QVZw0HwXXpN22s7VBHQBv9CeOu9tfJnhsWQNd2lmTNi8CSrnw5B+5YOmzu1UoPAyxaXsJ6RgQ==",
|
||||
"hasInstallScript": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/react-fontawesome": {
|
||||
"version": "0.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.18.tgz",
|
||||
@@ -7325,6 +7074,28 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/airbnb-prop-types": {
|
||||
"version": "2.16.0",
|
||||
"resolved": "https://registry.npmjs.org/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz",
|
||||
"integrity": "sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg==",
|
||||
"dependencies": {
|
||||
"array.prototype.find": "^2.1.1",
|
||||
"function.prototype.name": "^1.1.2",
|
||||
"is-regex": "^1.1.0",
|
||||
"object-is": "^1.1.2",
|
||||
"object.assign": "^4.1.0",
|
||||
"object.entries": "^1.1.2",
|
||||
"prop-types": "^15.7.2",
|
||||
"prop-types-exact": "^1.2.0",
|
||||
"react-is": "^16.13.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^0.14 || ^15.0.0 || ^16.0.0-alpha"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
@@ -7612,6 +7383,20 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/array.prototype.find": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/array.prototype.find/-/array.prototype.find-2.2.0.tgz",
|
||||
"integrity": "sha512-sn40qmUiLYAcRb/1HsIQjTTZ1kCy8II8VtZJpMn2Aoen9twULhbWXisfh3HimGqMlHGUul0/TfKCnXg42LuPpQ==",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.2",
|
||||
"define-properties": "^1.1.3",
|
||||
"es-abstract": "^1.19.4",
|
||||
"es-shim-unscopables": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/array.prototype.flat": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz",
|
||||
@@ -7677,7 +7462,8 @@
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/at-least-node": {
|
||||
"version": "1.0.0",
|
||||
@@ -7700,14 +7486,6 @@
|
||||
"node": ">= 4.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/attr-accept": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz",
|
||||
"integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/autoprefixer": {
|
||||
"version": "10.2.6",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.2.6.tgz",
|
||||
@@ -7739,22 +7517,20 @@
|
||||
"version": "0.21.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
|
||||
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios-cache-interceptor": {
|
||||
"version": "0.10.7",
|
||||
"resolved": "https://registry.npmjs.org/axios-cache-interceptor/-/axios-cache-interceptor-0.10.7.tgz",
|
||||
"integrity": "sha512-UjpxChG5DpF6Kf1IPGMLOzRDNL8ZNS6TOn1jTaVvCE7cWFU904jJwi0T1s+IbijpnLEjK2iq5uLIuR8Sj+RsFQ==",
|
||||
"node_modules/axios-cache-adapter": {
|
||||
"version": "2.7.3",
|
||||
"resolved": "https://registry.npmjs.org/axios-cache-adapter/-/axios-cache-adapter-2.7.3.tgz",
|
||||
"integrity": "sha512-A+ZKJ9lhpjthOEp4Z3QR/a9xC4du1ALaAsejgRGrH9ef6kSDxdFrhRpulqsh9khsEnwXxGfgpUuDp1YXMNMEiQ==",
|
||||
"dependencies": {
|
||||
"cache-parser": "^1.2.4",
|
||||
"fast-defer": "^1.1.7",
|
||||
"object-code": "^1.2.4"
|
||||
"cache-control-esm": "1.0.0",
|
||||
"md5": "^2.2.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/ArthurFiorette/axios-cache-interceptor?sponsor=1"
|
||||
"peerDependencies": {
|
||||
"axios": "~0.21.1"
|
||||
}
|
||||
},
|
||||
"node_modules/axios-mock-adapter": {
|
||||
@@ -8728,19 +8504,13 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/bootstrap": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz",
|
||||
"integrity": "sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/twbs"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/bootstrap"
|
||||
}
|
||||
],
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.0.tgz",
|
||||
"integrity": "sha512-Io55IuQY3kydzHtbGvQya3H+KorS/M9rSNyfCGCg9WZ4pyT/lCxIlpJgG1GXW/PswzC84Tr2fBYi+7+jFVQQBw==",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/bootstrap"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"jquery": "1.9.1 - 3",
|
||||
"popper.js": "^1.16.1"
|
||||
@@ -8910,10 +8680,10 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cache-parser": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/cache-parser/-/cache-parser-1.2.4.tgz",
|
||||
"integrity": "sha512-O0KwuHuJnbHUrghHi2kGp0SxnWSIBXTYt7M8WVhW0kbPRUNUKoE/Of6e1rRD6AAxmfxFunKnt90yEK09D+sc5g=="
|
||||
"node_modules/cache-control-esm": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cache-control-esm/-/cache-control-esm-1.0.0.tgz",
|
||||
"integrity": "sha512-Fa3UV4+eIk4EOih8FTV6EEsVKO0W5XWtNs6FC3InTfVz+EjurjPfDXY5wZDo/lxjDxg5RjNcurLyxEJBcEUx9g=="
|
||||
},
|
||||
"node_modules/cacheable-request": {
|
||||
"version": "2.1.4",
|
||||
@@ -9008,7 +8778,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1",
|
||||
"get-intrinsic": "^1.0.2"
|
||||
@@ -9146,6 +8915,14 @@
|
||||
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/charenc": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
|
||||
"integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/check-types": {
|
||||
"version": "8.0.3",
|
||||
"resolved": "https://registry.npmjs.org/check-types/-/check-types-8.0.3.tgz",
|
||||
@@ -9588,6 +9365,7 @@
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
@@ -9886,6 +9664,14 @@
|
||||
"semver": "bin/semver"
|
||||
}
|
||||
},
|
||||
"node_modules/crypt": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
|
||||
"integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/css": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz",
|
||||
@@ -10247,6 +10033,7 @@
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
|
||||
"integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
@@ -10634,7 +10421,6 @@
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz",
|
||||
"integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-property-descriptors": "^1.0.0",
|
||||
"object-keys": "^1.1.1"
|
||||
@@ -10681,6 +10467,7 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
@@ -11234,7 +11021,6 @@
|
||||
"version": "1.20.1",
|
||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.1.tgz",
|
||||
"integrity": "sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.2",
|
||||
"es-to-primitive": "^1.2.1",
|
||||
@@ -11294,7 +11080,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz",
|
||||
"integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has": "^1.0.3"
|
||||
}
|
||||
@@ -11303,7 +11088,6 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
|
||||
"integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-callable": "^1.1.4",
|
||||
"is-date-object": "^1.0.1",
|
||||
@@ -12523,11 +12307,6 @@
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
||||
},
|
||||
"node_modules/fast-defer": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/fast-defer/-/fast-defer-1.1.7.tgz",
|
||||
"integrity": "sha512-tJ01ulDWT2WhqxMKS20nXX6wyX2iInBYpbN3GO7yjKwXMY4qvkdBRxak9IFwBLlFDESox+SwSvqMCZDfe1tqeg=="
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.2.11",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz",
|
||||
@@ -12686,22 +12465,6 @@
|
||||
"webpack": "^4.0.0 || ^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/file-selector": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz",
|
||||
"integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/file-selector/node_modules/tslib": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
||||
},
|
||||
"node_modules/file-type": {
|
||||
"version": "12.4.2",
|
||||
"resolved": "https://registry.npmjs.org/file-type/-/file-type-12.4.2.tgz",
|
||||
@@ -12757,14 +12520,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/filter-obj": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz",
|
||||
"integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
|
||||
@@ -13298,14 +13053,12 @@
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
|
||||
"dev": true
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
|
||||
},
|
||||
"node_modules/function.prototype.name": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz",
|
||||
"integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.2",
|
||||
"define-properties": "^1.1.3",
|
||||
@@ -13329,7 +13082,6 @@
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
|
||||
"integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
@@ -13356,7 +13108,6 @@
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz",
|
||||
"integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
@@ -13412,7 +13163,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
|
||||
"integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.2",
|
||||
"get-intrinsic": "^1.1.1"
|
||||
@@ -13596,7 +13346,6 @@
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
|
||||
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
@@ -13768,7 +13517,6 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1"
|
||||
},
|
||||
@@ -13780,7 +13528,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
|
||||
"integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
@@ -13798,7 +13545,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz",
|
||||
"integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"get-intrinsic": "^1.1.1"
|
||||
},
|
||||
@@ -13820,7 +13566,6 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
||||
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
@@ -13845,7 +13590,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
|
||||
"integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.2"
|
||||
},
|
||||
@@ -15010,7 +14754,6 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz",
|
||||
"integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"get-intrinsic": "^1.1.0",
|
||||
"has": "^1.0.3",
|
||||
@@ -15146,7 +14889,6 @@
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
|
||||
"integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-bigints": "^1.0.1"
|
||||
},
|
||||
@@ -15182,7 +14924,6 @@
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
|
||||
"integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.2",
|
||||
"has-tostringtag": "^1.0.0"
|
||||
@@ -15221,7 +14962,6 @@
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz",
|
||||
"integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
@@ -15295,7 +15035,6 @@
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
|
||||
"integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-tostringtag": "^1.0.0"
|
||||
},
|
||||
@@ -15463,7 +15202,6 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
|
||||
"integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
@@ -15484,7 +15222,6 @@
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz",
|
||||
"integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-tostringtag": "^1.0.0"
|
||||
},
|
||||
@@ -15580,7 +15317,6 @@
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
|
||||
"integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.2",
|
||||
"has-tostringtag": "^1.0.0"
|
||||
@@ -15621,7 +15357,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz",
|
||||
"integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.2"
|
||||
},
|
||||
@@ -15642,7 +15377,6 @@
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
|
||||
"integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-tostringtag": "^1.0.0"
|
||||
},
|
||||
@@ -15673,7 +15407,6 @@
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
|
||||
"integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.2"
|
||||
},
|
||||
@@ -15706,7 +15439,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
|
||||
"integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.2"
|
||||
},
|
||||
@@ -19810,17 +19542,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mailto-link": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mailto-link/-/mailto-link-2.0.0.tgz",
|
||||
"integrity": "sha512-b5FErkZ4t6mpH1IFZSw7Mm2IQHXQ2R0/5Q4xd7Rv8dVkWvE54mFG/UW7HjfFazXFjXTNsM+dSX2tTeIDrV9K9A==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mailto-link/-/mailto-link-1.0.0.tgz",
|
||||
"integrity": "sha512-DVRvtkoeXLHAbH+S+9m3ILIdnvQsSc9IvJwfEclQVD8e8FhzwA5Mtw4Q0XXYr/sAziw/HsMc/gpGAI+5w6ohIw==",
|
||||
"dependencies": {
|
||||
"assert-ok": "~1.0.0",
|
||||
"cast-array": "~1.0.1",
|
||||
"cast-array": "~1.0.0",
|
||||
"object-filter": "~1.0.2",
|
||||
"query-string": "~7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
"query-string": "~2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
@@ -19883,6 +19612,21 @@
|
||||
"css-mediaquery": "^0.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/md5": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz",
|
||||
"integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==",
|
||||
"dependencies": {
|
||||
"charenc": "0.0.2",
|
||||
"crypt": "0.0.2",
|
||||
"is-buffer": "~1.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/md5/node_modules/is-buffer": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
|
||||
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
|
||||
},
|
||||
"node_modules/mdn-data": {
|
||||
"version": "2.0.14",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
|
||||
@@ -20460,11 +20204,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-code": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/object-code/-/object-code-1.2.4.tgz",
|
||||
"integrity": "sha512-uGq4ETUuWe+GA586NXEriiaozNuff+YNFXlpD8cVrM1GoiuTZpCABP+bZCWDrvQDoCiSTyiWAFHD/HF/iwhb2w=="
|
||||
},
|
||||
"node_modules/object-copy": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz",
|
||||
@@ -20565,7 +20304,6 @@
|
||||
"version": "1.12.2",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz",
|
||||
"integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
@@ -20574,7 +20312,6 @@
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz",
|
||||
"integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.2",
|
||||
"define-properties": "^1.1.3"
|
||||
@@ -20590,7 +20327,6 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
||||
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
@@ -20611,7 +20347,6 @@
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
|
||||
"integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.0",
|
||||
"define-properties": "^1.1.3",
|
||||
@@ -20629,7 +20364,6 @@
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz",
|
||||
"integrity": "sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.2",
|
||||
"define-properties": "^1.1.3",
|
||||
@@ -22219,6 +21953,16 @@
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types-exact": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/prop-types-exact/-/prop-types-exact-1.2.0.tgz",
|
||||
"integrity": "sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA==",
|
||||
"dependencies": {
|
||||
"has": "^1.0.3",
|
||||
"object.assign": "^4.1.0",
|
||||
"reflect.ownkeys": "^0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types-extra": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz",
|
||||
@@ -22308,28 +22052,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/query-string": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/query-string/-/query-string-7.0.1.tgz",
|
||||
"integrity": "sha512-uIw3iRvHnk9to1blJCG3BTc+Ro56CBowJXKmNNAm3RulvPBzWLRqKSiiDk+IplJhsydwtuNMHi8UGQFcCLVfkA==",
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/query-string/-/query-string-2.4.2.tgz",
|
||||
"integrity": "sha512-Y+OMYUuY7HxznI6WBN822fi/FMvnCTiuqd6KNcidPColOmMWPoV1RGYyyzObve1T/dD1i0ZgCCbO8ytu0ZUrkA==",
|
||||
"dependencies": {
|
||||
"decode-uri-component": "^0.2.0",
|
||||
"filter-obj": "^1.1.0",
|
||||
"split-on-first": "^1.0.0",
|
||||
"strict-uri-encode": "^2.0.0"
|
||||
"strict-uri-encode": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/query-string/node_modules/strict-uri-encode": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
|
||||
"integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
@@ -22455,15 +22185,6 @@
|
||||
"react": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-colorful": {
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz",
|
||||
"integrity": "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dev-utils": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.0.tgz",
|
||||
@@ -22722,22 +22443,6 @@
|
||||
"react": "^16.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dropzone": {
|
||||
"version": "14.2.2",
|
||||
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.2.tgz",
|
||||
"integrity": "sha512-5oyGN/B5rNhop2ggUnxztXBQ6q6zii+OMEftPzsxAR2hhpVWz0nAV+3Ktxo2h5bZzdcCKrpd8bfWAVsveIBM+w==",
|
||||
"dependencies": {
|
||||
"attr-accept": "^2.2.2",
|
||||
"file-selector": "^0.6.0",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.8 || 18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-error-overlay": {
|
||||
"version": "6.0.11",
|
||||
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz",
|
||||
@@ -22852,14 +22557,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
|
||||
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
|
||||
},
|
||||
"node_modules/react-loading-skeleton": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-loading-skeleton/-/react-loading-skeleton-3.1.0.tgz",
|
||||
"integrity": "sha512-j1U1CWWs68nBPOg7tkQqnlFcAMFF6oEK6MgqAo15f8A5p7mjH6xyKn2gHbkcimpwfO0VQXqxAswnSYVr8lWzjw==",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-mathjax-preview": {
|
||||
"version": "2.2.6",
|
||||
"resolved": "https://registry.npmjs.org/react-mathjax-preview/-/react-mathjax-preview-2.2.6.tgz",
|
||||
@@ -23330,6 +23027,11 @@
|
||||
"redux": "^4"
|
||||
}
|
||||
},
|
||||
"node_modules/reflect.ownkeys": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz",
|
||||
"integrity": "sha512-qOLsBKHCpSOFKK1NUOCGC5VyeufB6lEsFe92AL2bhIJsacZS1qdoOZSbPk3MYKuT2cFlRDnulKXuuElIrMjGUg=="
|
||||
},
|
||||
"node_modules/regenerate": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
|
||||
@@ -23385,7 +23087,6 @@
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz",
|
||||
"integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.2",
|
||||
"define-properties": "^1.1.3",
|
||||
@@ -24412,7 +24113,6 @@
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.0",
|
||||
"get-intrinsic": "^1.0.2",
|
||||
@@ -24906,14 +24606,6 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/split-on-first": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
|
||||
"integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/split-string": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
|
||||
@@ -25098,8 +24790,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
|
||||
"integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -25193,7 +24883,6 @@
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz",
|
||||
"integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.2",
|
||||
"define-properties": "^1.1.4",
|
||||
@@ -25207,7 +24896,6 @@
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz",
|
||||
"integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.2",
|
||||
"define-properties": "^1.1.4",
|
||||
@@ -25507,9 +25195,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/tabbable": {
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.3.3.tgz",
|
||||
"integrity": "sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA=="
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-4.0.0.tgz",
|
||||
"integrity": "sha512-H1XoH1URcBOa/rZZWxLxHCtOdVUEev+9vo5YdYhC9tCY4wnybX+VQrCYuy9ubkg69fCBxCONJOSLGfw0DWMffQ=="
|
||||
},
|
||||
"node_modules/table": {
|
||||
"version": "5.4.6",
|
||||
@@ -26220,7 +25908,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
|
||||
"integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.2",
|
||||
"has-bigints": "^1.0.2",
|
||||
@@ -27418,7 +27105,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
|
||||
"integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-bigint": "^1.0.1",
|
||||
"is-boolean-object": "^1.1.0",
|
||||
@@ -30201,30 +29887,17 @@
|
||||
}
|
||||
},
|
||||
"@edx/frontend-component-footer": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-12.0.0.tgz",
|
||||
"integrity": "sha512-m8Rx6ZPWzIN5XLrz6Ft3aTuFo0rty0jECd79CBYWdm0D9KD1WxoYEG+fElluyOQp/t42T5jLImHTSWjFURx5kw==",
|
||||
"version": "11.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-11.2.0.tgz",
|
||||
"integrity": "sha512-prN6SeoWenbNq6jCqlpmYx57OhNbVGw1wjQFVH+aA5JMjjLUf6PmjPG87fuD9udQyFteb2lvbCyongPEZOozrg==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-svg-core": "6.4.0",
|
||||
"@fortawesome/free-brands-svg-icons": "6.4.0",
|
||||
"@fortawesome/free-regular-svg-icons": "6.4.0",
|
||||
"@fortawesome/free-solid-svg-icons": "6.4.0",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||
"@fortawesome/react-fontawesome": "0.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.0.tgz",
|
||||
"integrity": "sha512-HNii132xfomg5QVZw0HwXXpN22s7VBHQBv9CeOu9tfJnhsWQNd2lmTNi8CSrnw5B+5YOmzu1UoPAyxaXsJ6RgQ=="
|
||||
},
|
||||
"@fortawesome/fontawesome-svg-core": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.0.tgz",
|
||||
"integrity": "sha512-Bertv8xOiVELz5raB2FlXDPKt+m94MQ3JgDfsVbrqNpLU9+UE2E18GKjLKw+d3XbeYPqg1pzyQKGsrzbw+pPaw==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-common-types": "6.4.0"
|
||||
}
|
||||
},
|
||||
"@fortawesome/react-fontawesome": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.0.tgz",
|
||||
@@ -30236,87 +29909,20 @@
|
||||
}
|
||||
},
|
||||
"@edx/frontend-component-header": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-component-header/-/frontend-component-header-4.0.0.tgz",
|
||||
"integrity": "sha512-r/L3p2ZSI1DitjxVKAor18GmgJllafYslrdpzGI0vcX/gTemH13jf2Xr9iQqrT921DP2nzZ5GOwGJNptTSjiaA==",
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-component-header/-/frontend-component-header-3.2.0.tgz",
|
||||
"integrity": "sha512-ep9yOwe9CG03lSf/QBsBOZO3S/Ksrttgi4MkjpUe/6Sm/KdAnMCOhx2Gbj9jBcFrHqCkpeef26zGjjhpHGDwkQ==",
|
||||
"requires": {
|
||||
"@edx/paragon": "20.30.1",
|
||||
"@fortawesome/fontawesome-svg-core": "6.3.0",
|
||||
"@fortawesome/free-brands-svg-icons": "6.3.0",
|
||||
"@fortawesome/free-regular-svg-icons": "6.3.0",
|
||||
"@fortawesome/free-solid-svg-icons": "6.3.0",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"babel-polyfill": "6.26.0",
|
||||
"react-responsive": "8.2.0",
|
||||
"react-transition-group": "4.4.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/paragon": {
|
||||
"version": "20.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.30.1.tgz",
|
||||
"integrity": "sha512-v3Ek8deZWqVKi3IWP08Mj4egrvbmbqQEyRA6+qazHZdgHJA4qOP1SST42UKd9XxPeRbLWUgaJWd0iBAOAna/gw==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
||||
"@fortawesome/react-fontawesome": "^0.1.18",
|
||||
"@popperjs/core": "^2.11.4",
|
||||
"bootstrap": "^4.6.2",
|
||||
"classnames": "^2.3.1",
|
||||
"email-prop-type": "^3.0.0",
|
||||
"file-selector": "^0.6.0",
|
||||
"font-awesome": "^4.7.0",
|
||||
"glob": "^8.0.3",
|
||||
"lodash.uniqby": "^4.7.0",
|
||||
"mailto-link": "^2.0.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-bootstrap": "^1.6.5",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dropzone": "^14.2.1",
|
||||
"react-focus-on": "^3.5.4",
|
||||
"react-loading-skeleton": "^3.1.0",
|
||||
"react-popper": "^2.2.5",
|
||||
"react-proptype-conditional-require": "^1.0.4",
|
||||
"react-responsive": "^8.2.0",
|
||||
"react-table": "^7.7.0",
|
||||
"react-transition-group": "^4.4.2",
|
||||
"tabbable": "^5.3.3",
|
||||
"uncontrollable": "^7.2.1",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/react-fontawesome": {
|
||||
"version": "0.1.19",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.19.tgz",
|
||||
"integrity": "sha512-Hyb+lB8T18cvLNX0S3llz7PcSOAJMLwiVKBuuzwM/nI5uoBw+gQjnf9il0fR1C3DKOI5Kc79pkJ4/xB0Uw9aFQ==",
|
||||
"requires": {
|
||||
"prop-types": "^15.8.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@fortawesome/free-brands-svg-icons": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.3.0.tgz",
|
||||
"integrity": "sha512-xI0c+a8xnKItAXCN8rZgCNCJQiVAd2Y7p9e2ND6zN3J3ekneu96qrePieJ7yA7073C1JxxoM3vH1RU7rYsaj8w==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-common-types": "6.3.0"
|
||||
}
|
||||
},
|
||||
"@fortawesome/free-regular-svg-icons": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.3.0.tgz",
|
||||
"integrity": "sha512-cZnwiVHZ51SVzWHOaNCIA+u9wevZjCuAGSvSYpNlm6A4H4Vhwh8481Bf/5rwheIC3fFKlgXxLKaw8Xeroz8Ntg==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-common-types": "6.3.0"
|
||||
}
|
||||
},
|
||||
"@fortawesome/free-solid-svg-icons": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.3.0.tgz",
|
||||
"integrity": "sha512-x5tMwzF2lTH8pyv8yeZRodItP2IVlzzmBuD1M7BjawWgg9XAvktqJJ91Qjgoaf8qJpHQ8FEU9VxRfOkLhh86QA==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-common-types": "6.3.0"
|
||||
}
|
||||
},
|
||||
"@fortawesome/react-fontawesome": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.0.tgz",
|
||||
@@ -30324,54 +29930,21 @@
|
||||
"requires": {
|
||||
"prop-types": "^15.8.1"
|
||||
}
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"glob": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
|
||||
"integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^5.0.1",
|
||||
"once": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
||||
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
||||
"requires": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"uuid": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
|
||||
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@edx/frontend-platform": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-4.2.0.tgz",
|
||||
"integrity": "sha512-iDoFeccENQKBjqUgdjl5KSwBrjNEj8YW6Ual+6twcHHJUBg3yRoBEphwHIoRREcMgQjhdKVAdWj8eleh4JsEKA==",
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-2.6.1.tgz",
|
||||
"integrity": "sha512-5ZcHBvwmJRYPPKEqv+H4DbsfW9atr5V2+uY/zX58F/Tqh0T5X8MmgppVjplWFbmon5QV4aNw5C1ViQnirNqUiA==",
|
||||
"requires": {
|
||||
"@cospired/i18n-iso-languages": "2.2.0",
|
||||
"@formatjs/intl-pluralrules": "4.3.3",
|
||||
"@formatjs/intl-relativetimeformat": "10.0.1",
|
||||
"axios": "0.27.2",
|
||||
"axios-cache-interceptor": "0.10.7",
|
||||
"axios": "0.26.1",
|
||||
"axios-cache-adapter": "2.7.3",
|
||||
"form-urlencoded": "4.1.4",
|
||||
"glob": "7.2.3",
|
||||
"glob": "7.2.0",
|
||||
"history": "4.10.1",
|
||||
"i18n-iso-countries": "4.3.1",
|
||||
"jwt-decode": "3.1.2",
|
||||
@@ -30387,35 +29960,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
|
||||
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
|
||||
"version": "0.26.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
|
||||
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.14.9",
|
||||
"form-data": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"glob": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.1.1",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
"follow-redirects": "^1.14.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30430,63 +29979,31 @@
|
||||
}
|
||||
},
|
||||
"@edx/paragon": {
|
||||
"version": "20.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.15.0.tgz",
|
||||
"integrity": "sha512-Sq5/je3Ub3UpXrrneaCShz+kB6wuMNmeF1Y/XfVt6X2i+ZCQGlQrBa3KtcwGwDOHIQ65eC6dqJpL9LISHg+xtg==",
|
||||
"version": "19.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-19.10.1.tgz",
|
||||
"integrity": "sha512-kVIx/yJWYeaTIb+j2NmB/iz1XGR4WY8aYEQbt1FtGFWcKzeiThtiqr4TeRxQeA8UrooyQl5HyzYISq7xC5AH2w==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
||||
"@fortawesome/react-fontawesome": "^0.1.18",
|
||||
"@popperjs/core": "^2.11.4",
|
||||
"bootstrap": "^4.6.2",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||
"@fortawesome/react-fontawesome": "^0.1.16",
|
||||
"@popperjs/core": "^2.11.2",
|
||||
"airbnb-prop-types": "^2.12.0",
|
||||
"bootstrap": "4.6.0",
|
||||
"classnames": "^2.3.1",
|
||||
"email-prop-type": "^3.0.0",
|
||||
"file-selector": "^0.6.0",
|
||||
"font-awesome": "^4.7.0",
|
||||
"glob": "^8.0.3",
|
||||
"lodash.uniqby": "^4.7.0",
|
||||
"mailto-link": "^2.0.0",
|
||||
"mailto-link": "^1.0.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-bootstrap": "^1.6.5",
|
||||
"react-dropzone": "^14.2.1",
|
||||
"react-bootstrap": "^1.6.4",
|
||||
"react-focus-on": "^3.5.4",
|
||||
"react-loading-skeleton": "^3.1.0",
|
||||
"react-popper": "^2.2.5",
|
||||
"react-proptype-conditional-require": "^1.0.4",
|
||||
"react-responsive": "^8.2.0",
|
||||
"react-table": "^7.7.0",
|
||||
"react-transition-group": "^4.4.2",
|
||||
"tabbable": "^5.3.3",
|
||||
"uncontrollable": "^7.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"glob": {
|
||||
"version": "8.0.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz",
|
||||
"integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==",
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^5.0.1",
|
||||
"once": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz",
|
||||
"integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==",
|
||||
"requires": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
}
|
||||
}
|
||||
"tabbable": "^4.0.0",
|
||||
"uncontrollable": "7.2.1"
|
||||
}
|
||||
},
|
||||
"@edx/reactifex": {
|
||||
@@ -30782,61 +30299,40 @@
|
||||
}
|
||||
},
|
||||
"@fortawesome/fontawesome-common-types": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.3.0.tgz",
|
||||
"integrity": "sha512-4BC1NMoacEBzSXRwKjZ/X/gmnbp/HU5Qqat7E8xqorUtBFZS+bwfGH5/wqOC2K6GV0rgEobp3OjGRMa5fK9pFg=="
|
||||
"version": "0.2.36",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz",
|
||||
"integrity": "sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg=="
|
||||
},
|
||||
"@fortawesome/fontawesome-svg-core": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.3.0.tgz",
|
||||
"integrity": "sha512-uz9YifyKlixV6AcKlOX8WNdtF7l6nakGyLYxYaCa823bEBqyj/U2ssqtctO38itNEwXb8/lMzjdoJ+aaJuOdrw==",
|
||||
"version": "1.2.36",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.36.tgz",
|
||||
"integrity": "sha512-YUcsLQKYb6DmaJjIHdDWpBIGCcyE/W+p/LMGvjQem55Mm2XWVAP5kWTMKWLv9lwpCVjpLxPyOMOyUocP1GxrtA==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-common-types": "6.3.0"
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.36"
|
||||
}
|
||||
},
|
||||
"@fortawesome/free-brands-svg-icons": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.4.0.tgz",
|
||||
"integrity": "sha512-qvxTCo0FQ5k2N+VCXb/PZQ+QMhqRVM4OORiO6MXdG6bKolIojGU/srQ1ptvKk0JTbRgaJOfL2qMqGvBEZG7Z6g==",
|
||||
"version": "5.15.4",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-5.15.4.tgz",
|
||||
"integrity": "sha512-f1witbwycL9cTENJegcmcZRYyawAFbm8+c6IirLmwbbpqz46wyjbQYLuxOc7weXFXfB7QR8/Vd2u5R3q6JYD9g==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-common-types": "6.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.0.tgz",
|
||||
"integrity": "sha512-HNii132xfomg5QVZw0HwXXpN22s7VBHQBv9CeOu9tfJnhsWQNd2lmTNi8CSrnw5B+5YOmzu1UoPAyxaXsJ6RgQ=="
|
||||
}
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.36"
|
||||
}
|
||||
},
|
||||
"@fortawesome/free-regular-svg-icons": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.4.0.tgz",
|
||||
"integrity": "sha512-ZfycI7D0KWPZtf7wtMFnQxs8qjBXArRzczABuMQqecA/nXohquJ5J/RCR77PmY5qGWkxAZDxpnUFVXKwtY/jPw==",
|
||||
"version": "5.15.4",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.15.4.tgz",
|
||||
"integrity": "sha512-9VNNnU3CXHy9XednJ3wzQp6SwNwT3XaM26oS4Rp391GsxVYA+0oDR2J194YCIWf7jNRCYKjUCOduxdceLrx+xw==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-common-types": "6.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.0.tgz",
|
||||
"integrity": "sha512-HNii132xfomg5QVZw0HwXXpN22s7VBHQBv9CeOu9tfJnhsWQNd2lmTNi8CSrnw5B+5YOmzu1UoPAyxaXsJ6RgQ=="
|
||||
}
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.36"
|
||||
}
|
||||
},
|
||||
"@fortawesome/free-solid-svg-icons": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.0.tgz",
|
||||
"integrity": "sha512-kutPeRGWm8V5dltFP1zGjQOEAzaLZj4StdQhWVZnfGFCvAPVvHh8qk5bRrU4KXnRRRNni5tKQI9PBAdI6MP8nQ==",
|
||||
"version": "5.15.4",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.4.tgz",
|
||||
"integrity": "sha512-JLmQfz6tdtwxoihXLg6lT78BorrFyCf59SAwBM6qV/0zXyVeDygJVb3fk+j5Qat+Yvcxp1buLTY5iDh1ZSAQ8w==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-common-types": "6.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.0.tgz",
|
||||
"integrity": "sha512-HNii132xfomg5QVZw0HwXXpN22s7VBHQBv9CeOu9tfJnhsWQNd2lmTNi8CSrnw5B+5YOmzu1UoPAyxaXsJ6RgQ=="
|
||||
}
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.36"
|
||||
}
|
||||
},
|
||||
"@fortawesome/react-fontawesome": {
|
||||
@@ -33331,6 +32827,22 @@
|
||||
"indent-string": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"airbnb-prop-types": {
|
||||
"version": "2.16.0",
|
||||
"resolved": "https://registry.npmjs.org/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz",
|
||||
"integrity": "sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg==",
|
||||
"requires": {
|
||||
"array.prototype.find": "^2.1.1",
|
||||
"function.prototype.name": "^1.1.2",
|
||||
"is-regex": "^1.1.0",
|
||||
"object-is": "^1.1.2",
|
||||
"object.assign": "^4.1.0",
|
||||
"object.entries": "^1.1.2",
|
||||
"prop-types": "^15.7.2",
|
||||
"prop-types-exact": "^1.2.0",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
@@ -33530,6 +33042,17 @@
|
||||
"integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==",
|
||||
"dev": true
|
||||
},
|
||||
"array.prototype.find": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/array.prototype.find/-/array.prototype.find-2.2.0.tgz",
|
||||
"integrity": "sha512-sn40qmUiLYAcRb/1HsIQjTTZ1kCy8II8VtZJpMn2Aoen9twULhbWXisfh3HimGqMlHGUul0/TfKCnXg42LuPpQ==",
|
||||
"requires": {
|
||||
"call-bind": "^1.0.2",
|
||||
"define-properties": "^1.1.3",
|
||||
"es-abstract": "^1.19.4",
|
||||
"es-shim-unscopables": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"array.prototype.flat": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz",
|
||||
@@ -33583,7 +33106,8 @@
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"dev": true
|
||||
},
|
||||
"at-least-node": {
|
||||
"version": "1.0.0",
|
||||
@@ -33597,11 +33121,6 @@
|
||||
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
|
||||
"dev": true
|
||||
},
|
||||
"attr-accept": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz",
|
||||
"integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg=="
|
||||
},
|
||||
"autoprefixer": {
|
||||
"version": "10.2.6",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.2.6.tgz",
|
||||
@@ -33620,19 +33139,17 @@
|
||||
"version": "0.21.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
|
||||
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"follow-redirects": "^1.14.0"
|
||||
}
|
||||
},
|
||||
"axios-cache-interceptor": {
|
||||
"version": "0.10.7",
|
||||
"resolved": "https://registry.npmjs.org/axios-cache-interceptor/-/axios-cache-interceptor-0.10.7.tgz",
|
||||
"integrity": "sha512-UjpxChG5DpF6Kf1IPGMLOzRDNL8ZNS6TOn1jTaVvCE7cWFU904jJwi0T1s+IbijpnLEjK2iq5uLIuR8Sj+RsFQ==",
|
||||
"axios-cache-adapter": {
|
||||
"version": "2.7.3",
|
||||
"resolved": "https://registry.npmjs.org/axios-cache-adapter/-/axios-cache-adapter-2.7.3.tgz",
|
||||
"integrity": "sha512-A+ZKJ9lhpjthOEp4Z3QR/a9xC4du1ALaAsejgRGrH9ef6kSDxdFrhRpulqsh9khsEnwXxGfgpUuDp1YXMNMEiQ==",
|
||||
"requires": {
|
||||
"cache-parser": "^1.2.4",
|
||||
"fast-defer": "^1.1.7",
|
||||
"object-code": "^1.2.4"
|
||||
"cache-control-esm": "1.0.0",
|
||||
"md5": "^2.2.1"
|
||||
}
|
||||
},
|
||||
"axios-mock-adapter": {
|
||||
@@ -34425,9 +33942,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"bootstrap": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz",
|
||||
"integrity": "sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==",
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.0.tgz",
|
||||
"integrity": "sha512-Io55IuQY3kydzHtbGvQya3H+KorS/M9rSNyfCGCg9WZ4pyT/lCxIlpJgG1GXW/PswzC84Tr2fBYi+7+jFVQQBw==",
|
||||
"requires": {}
|
||||
},
|
||||
"brace-expansion": {
|
||||
@@ -34552,10 +34069,10 @@
|
||||
"unset-value": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"cache-parser": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/cache-parser/-/cache-parser-1.2.4.tgz",
|
||||
"integrity": "sha512-O0KwuHuJnbHUrghHi2kGp0SxnWSIBXTYt7M8WVhW0kbPRUNUKoE/Of6e1rRD6AAxmfxFunKnt90yEK09D+sc5g=="
|
||||
"cache-control-esm": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cache-control-esm/-/cache-control-esm-1.0.0.tgz",
|
||||
"integrity": "sha512-Fa3UV4+eIk4EOih8FTV6EEsVKO0W5XWtNs6FC3InTfVz+EjurjPfDXY5wZDo/lxjDxg5RjNcurLyxEJBcEUx9g=="
|
||||
},
|
||||
"cacheable-request": {
|
||||
"version": "2.1.4",
|
||||
@@ -34634,7 +34151,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1",
|
||||
"get-intrinsic": "^1.0.2"
|
||||
@@ -34740,6 +34256,11 @@
|
||||
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
|
||||
"dev": true
|
||||
},
|
||||
"charenc": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
|
||||
"integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA=="
|
||||
},
|
||||
"check-types": {
|
||||
"version": "8.0.3",
|
||||
"resolved": "https://registry.npmjs.org/check-types/-/check-types-8.0.3.tgz",
|
||||
@@ -35098,6 +34619,7 @@
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
}
|
||||
@@ -35331,6 +34853,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"crypt": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
|
||||
"integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow=="
|
||||
},
|
||||
"css": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz",
|
||||
@@ -35600,7 +35127,8 @@
|
||||
"decode-uri-component": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
|
||||
"integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og=="
|
||||
"integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==",
|
||||
"dev": true
|
||||
},
|
||||
"decompress": {
|
||||
"version": "4.2.1",
|
||||
@@ -35900,7 +35428,6 @@
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz",
|
||||
"integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-property-descriptors": "^1.0.0",
|
||||
"object-keys": "^1.1.1"
|
||||
@@ -35934,7 +35461,8 @@
|
||||
"delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"dev": true
|
||||
},
|
||||
"depd": {
|
||||
"version": "2.0.0",
|
||||
@@ -36381,7 +35909,6 @@
|
||||
"version": "1.20.1",
|
||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.1.tgz",
|
||||
"integrity": "sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind": "^1.0.2",
|
||||
"es-to-primitive": "^1.2.1",
|
||||
@@ -36429,7 +35956,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz",
|
||||
"integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has": "^1.0.3"
|
||||
}
|
||||
@@ -36438,7 +35964,6 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
|
||||
"integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-callable": "^1.1.4",
|
||||
"is-date-object": "^1.0.1",
|
||||
@@ -37400,11 +36925,6 @@
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
||||
},
|
||||
"fast-defer": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/fast-defer/-/fast-defer-1.1.7.tgz",
|
||||
"integrity": "sha512-tJ01ulDWT2WhqxMKS20nXX6wyX2iInBYpbN3GO7yjKwXMY4qvkdBRxak9IFwBLlFDESox+SwSvqMCZDfe1tqeg=="
|
||||
},
|
||||
"fast-glob": {
|
||||
"version": "3.2.11",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz",
|
||||
@@ -37533,21 +37053,6 @@
|
||||
"schema-utils": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"file-selector": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz",
|
||||
"integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==",
|
||||
"requires": {
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"file-type": {
|
||||
"version": "12.4.2",
|
||||
"resolved": "https://registry.npmjs.org/file-type/-/file-type-12.4.2.tgz",
|
||||
@@ -37588,11 +37093,6 @@
|
||||
"to-regex-range": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"filter-obj": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz",
|
||||
"integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ=="
|
||||
},
|
||||
"finalhandler": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
|
||||
@@ -37979,14 +37479,12 @@
|
||||
"function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
|
||||
"dev": true
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
|
||||
},
|
||||
"function.prototype.name": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz",
|
||||
"integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind": "^1.0.2",
|
||||
"define-properties": "^1.1.3",
|
||||
@@ -38003,8 +37501,7 @@
|
||||
"functions-have-names": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
|
||||
"integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="
|
||||
},
|
||||
"gensync": {
|
||||
"version": "1.0.0-beta.2",
|
||||
@@ -38022,7 +37519,6 @@
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz",
|
||||
"integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
@@ -38063,7 +37559,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
|
||||
"integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind": "^1.0.2",
|
||||
"get-intrinsic": "^1.1.1"
|
||||
@@ -38188,7 +37683,6 @@
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
|
||||
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
@@ -38328,7 +37822,6 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1"
|
||||
}
|
||||
@@ -38336,8 +37829,7 @@
|
||||
"has-bigints": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
|
||||
"integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ=="
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "3.0.0",
|
||||
@@ -38349,7 +37841,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz",
|
||||
"integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"get-intrinsic": "^1.1.1"
|
||||
}
|
||||
@@ -38364,8 +37855,7 @@
|
||||
"has-symbols": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
||||
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
|
||||
"dev": true
|
||||
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="
|
||||
},
|
||||
"has-to-string-tag-x": {
|
||||
"version": "1.4.1",
|
||||
@@ -38381,7 +37871,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
|
||||
"integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-symbols": "^1.0.2"
|
||||
}
|
||||
@@ -39247,7 +38736,6 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz",
|
||||
"integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"get-intrinsic": "^1.1.0",
|
||||
"has": "^1.0.3",
|
||||
@@ -39365,7 +38853,6 @@
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
|
||||
"integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-bigints": "^1.0.1"
|
||||
}
|
||||
@@ -39389,7 +38876,6 @@
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
|
||||
"integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind": "^1.0.2",
|
||||
"has-tostringtag": "^1.0.0"
|
||||
@@ -39404,8 +38890,7 @@
|
||||
"is-callable": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz",
|
||||
"integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==",
|
||||
"dev": true
|
||||
"integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w=="
|
||||
},
|
||||
"is-ci": {
|
||||
"version": "2.0.0",
|
||||
@@ -39465,7 +38950,6 @@
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
|
||||
"integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-tostringtag": "^1.0.0"
|
||||
}
|
||||
@@ -39585,8 +39069,7 @@
|
||||
"is-negative-zero": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
|
||||
"integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA=="
|
||||
},
|
||||
"is-number": {
|
||||
"version": "7.0.0",
|
||||
@@ -39598,7 +39081,6 @@
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz",
|
||||
"integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-tostringtag": "^1.0.0"
|
||||
}
|
||||
@@ -39667,7 +39149,6 @@
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
|
||||
"integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind": "^1.0.2",
|
||||
"has-tostringtag": "^1.0.0"
|
||||
@@ -39696,7 +39177,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz",
|
||||
"integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind": "^1.0.2"
|
||||
}
|
||||
@@ -39711,7 +39191,6 @@
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
|
||||
"integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-tostringtag": "^1.0.0"
|
||||
}
|
||||
@@ -39730,7 +39209,6 @@
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
|
||||
"integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-symbols": "^1.0.2"
|
||||
}
|
||||
@@ -39754,7 +39232,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
|
||||
"integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind": "^1.0.2"
|
||||
}
|
||||
@@ -42918,14 +42395,14 @@
|
||||
"dev": true
|
||||
},
|
||||
"mailto-link": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mailto-link/-/mailto-link-2.0.0.tgz",
|
||||
"integrity": "sha512-b5FErkZ4t6mpH1IFZSw7Mm2IQHXQ2R0/5Q4xd7Rv8dVkWvE54mFG/UW7HjfFazXFjXTNsM+dSX2tTeIDrV9K9A==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mailto-link/-/mailto-link-1.0.0.tgz",
|
||||
"integrity": "sha512-DVRvtkoeXLHAbH+S+9m3ILIdnvQsSc9IvJwfEclQVD8e8FhzwA5Mtw4Q0XXYr/sAziw/HsMc/gpGAI+5w6ohIw==",
|
||||
"requires": {
|
||||
"assert-ok": "~1.0.0",
|
||||
"cast-array": "~1.0.1",
|
||||
"cast-array": "~1.0.0",
|
||||
"object-filter": "~1.0.2",
|
||||
"query-string": "~7.0.0"
|
||||
"query-string": "~2.4.1"
|
||||
}
|
||||
},
|
||||
"make-dir": {
|
||||
@@ -42978,6 +42455,23 @@
|
||||
"css-mediaquery": "^0.1.2"
|
||||
}
|
||||
},
|
||||
"md5": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz",
|
||||
"integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==",
|
||||
"requires": {
|
||||
"charenc": "0.0.2",
|
||||
"crypt": "0.0.2",
|
||||
"is-buffer": "~1.1.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"is-buffer": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
|
||||
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"mdn-data": {
|
||||
"version": "2.0.14",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
|
||||
@@ -43425,11 +42919,6 @@
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="
|
||||
},
|
||||
"object-code": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/object-code/-/object-code-1.2.4.tgz",
|
||||
"integrity": "sha512-uGq4ETUuWe+GA586NXEriiaozNuff+YNFXlpD8cVrM1GoiuTZpCABP+bZCWDrvQDoCiSTyiWAFHD/HF/iwhb2w=="
|
||||
},
|
||||
"object-copy": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz",
|
||||
@@ -43512,14 +43001,12 @@
|
||||
"object-inspect": {
|
||||
"version": "1.12.2",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz",
|
||||
"integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ=="
|
||||
},
|
||||
"object-is": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz",
|
||||
"integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind": "^1.0.2",
|
||||
"define-properties": "^1.1.3"
|
||||
@@ -43528,8 +43015,7 @@
|
||||
"object-keys": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
||||
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
|
||||
},
|
||||
"object-visit": {
|
||||
"version": "1.0.1",
|
||||
@@ -43544,7 +43030,6 @@
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
|
||||
"integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind": "^1.0.0",
|
||||
"define-properties": "^1.1.3",
|
||||
@@ -43556,7 +43041,6 @@
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz",
|
||||
"integrity": "sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind": "^1.0.2",
|
||||
"define-properties": "^1.1.3",
|
||||
@@ -44668,6 +44152,16 @@
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"prop-types-exact": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/prop-types-exact/-/prop-types-exact-1.2.0.tgz",
|
||||
"integrity": "sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA==",
|
||||
"requires": {
|
||||
"has": "^1.0.3",
|
||||
"object.assign": "^4.1.0",
|
||||
"reflect.ownkeys": "^0.2.0"
|
||||
}
|
||||
},
|
||||
"prop-types-extra": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz",
|
||||
@@ -44742,21 +44236,11 @@
|
||||
}
|
||||
},
|
||||
"query-string": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/query-string/-/query-string-7.0.1.tgz",
|
||||
"integrity": "sha512-uIw3iRvHnk9to1blJCG3BTc+Ro56CBowJXKmNNAm3RulvPBzWLRqKSiiDk+IplJhsydwtuNMHi8UGQFcCLVfkA==",
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/query-string/-/query-string-2.4.2.tgz",
|
||||
"integrity": "sha512-Y+OMYUuY7HxznI6WBN822fi/FMvnCTiuqd6KNcidPColOmMWPoV1RGYyyzObve1T/dD1i0ZgCCbO8ytu0ZUrkA==",
|
||||
"requires": {
|
||||
"decode-uri-component": "^0.2.0",
|
||||
"filter-obj": "^1.1.0",
|
||||
"split-on-first": "^1.0.0",
|
||||
"strict-uri-encode": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"strict-uri-encode": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
|
||||
"integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ=="
|
||||
}
|
||||
"strict-uri-encode": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"queue-microtask": {
|
||||
@@ -44842,12 +44326,6 @@
|
||||
"@babel/runtime": "^7.12.13"
|
||||
}
|
||||
},
|
||||
"react-colorful": {
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz",
|
||||
"integrity": "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==",
|
||||
"requires": {}
|
||||
},
|
||||
"react-dev-utils": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.0.tgz",
|
||||
@@ -45036,16 +44514,6 @@
|
||||
"scheduler": "^0.19.1"
|
||||
}
|
||||
},
|
||||
"react-dropzone": {
|
||||
"version": "14.2.2",
|
||||
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.2.tgz",
|
||||
"integrity": "sha512-5oyGN/B5rNhop2ggUnxztXBQ6q6zii+OMEftPzsxAR2hhpVWz0nAV+3Ktxo2h5bZzdcCKrpd8bfWAVsveIBM+w==",
|
||||
"requires": {
|
||||
"attr-accept": "^2.2.2",
|
||||
"file-selector": "^0.6.0",
|
||||
"prop-types": "^15.8.1"
|
||||
}
|
||||
},
|
||||
"react-error-overlay": {
|
||||
"version": "6.0.11",
|
||||
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz",
|
||||
@@ -45134,12 +44602,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
|
||||
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
|
||||
},
|
||||
"react-loading-skeleton": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-loading-skeleton/-/react-loading-skeleton-3.1.0.tgz",
|
||||
"integrity": "sha512-j1U1CWWs68nBPOg7tkQqnlFcAMFF6oEK6MgqAo15f8A5p7mjH6xyKn2gHbkcimpwfO0VQXqxAswnSYVr8lWzjw==",
|
||||
"requires": {}
|
||||
},
|
||||
"react-mathjax-preview": {
|
||||
"version": "2.2.6",
|
||||
"resolved": "https://registry.npmjs.org/react-mathjax-preview/-/react-mathjax-preview-2.2.6.tgz",
|
||||
@@ -45495,6 +44957,11 @@
|
||||
"integrity": "sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q==",
|
||||
"requires": {}
|
||||
},
|
||||
"reflect.ownkeys": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz",
|
||||
"integrity": "sha512-qOLsBKHCpSOFKK1NUOCGC5VyeufB6lEsFe92AL2bhIJsacZS1qdoOZSbPk3MYKuT2cFlRDnulKXuuElIrMjGUg=="
|
||||
},
|
||||
"regenerate": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
|
||||
@@ -45544,7 +45011,6 @@
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz",
|
||||
"integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind": "^1.0.2",
|
||||
"define-properties": "^1.1.3",
|
||||
@@ -46351,7 +45817,6 @@
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind": "^1.0.0",
|
||||
"get-intrinsic": "^1.0.2",
|
||||
@@ -46778,11 +46243,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"split-on-first": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
|
||||
"integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw=="
|
||||
},
|
||||
"split-string": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
|
||||
@@ -46935,9 +46395,7 @@
|
||||
"strict-uri-encode": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
|
||||
"integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ=="
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
@@ -47017,7 +46475,6 @@
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz",
|
||||
"integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind": "^1.0.2",
|
||||
"define-properties": "^1.1.4",
|
||||
@@ -47028,7 +46485,6 @@
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz",
|
||||
"integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind": "^1.0.2",
|
||||
"define-properties": "^1.1.4",
|
||||
@@ -47256,9 +46712,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"tabbable": {
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.3.3.tgz",
|
||||
"integrity": "sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA=="
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-4.0.0.tgz",
|
||||
"integrity": "sha512-H1XoH1URcBOa/rZZWxLxHCtOdVUEev+9vo5YdYhC9tCY4wnybX+VQrCYuy9ubkg69fCBxCONJOSLGfw0DWMffQ=="
|
||||
},
|
||||
"table": {
|
||||
"version": "5.4.6",
|
||||
@@ -47821,7 +47277,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
|
||||
"integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind": "^1.0.2",
|
||||
"has-bigints": "^1.0.2",
|
||||
@@ -48702,7 +48157,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
|
||||
"integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-bigint": "^1.0.1",
|
||||
"is-boolean-object": "^1.1.0",
|
||||
|
||||
@@ -34,10 +34,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
||||
"@edx/frontend-component-footer": "12.0.0",
|
||||
"@edx/frontend-component-header": "4.0.0",
|
||||
"@edx/frontend-platform": "4.2.0",
|
||||
"@edx/paragon": "20.15.0",
|
||||
"@edx/frontend-component-footer": "11.2.0",
|
||||
"@edx/frontend-component-header": "3.2.0",
|
||||
"@edx/frontend-platform": "2.6.1",
|
||||
"@edx/paragon": "19.10.1",
|
||||
"@reduxjs/toolkit": "1.8.0",
|
||||
"@tinymce/tinymce-react": "3.13.1",
|
||||
"babel-polyfill": "6.26.0",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
type="image/x-icon"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root" class="small"></div>
|
||||
<body class="vh-100 vw-100 h-100 m-0">
|
||||
<div id="root" class="vh-100 vw-100 small"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,201 +0,0 @@
|
||||
/* eslint-disable react/forbid-prop-types */
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { capitalize, toString } from 'lodash';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Collapsible, Form, Icon, Spinner,
|
||||
} from '@edx/paragon';
|
||||
import { Tune } from '@edx/paragon/icons';
|
||||
|
||||
import {
|
||||
PostsStatusFilter, RequestStatus,
|
||||
ThreadOrdering, ThreadType,
|
||||
} from '../data/constants';
|
||||
import { selectCourseCohorts } from '../discussions/cohorts/data/selectors';
|
||||
import messages from '../discussions/posts/post-filter-bar/messages';
|
||||
import { ActionItem } from '../discussions/posts/post-filter-bar/PostFilterBar';
|
||||
|
||||
function FilterBar({
|
||||
intl,
|
||||
filters,
|
||||
selectedFilters,
|
||||
onFilterChange,
|
||||
showCohortsFilter,
|
||||
}) {
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
const cohorts = useSelector(selectCourseCohorts);
|
||||
const { status } = useSelector(state => state.cohorts);
|
||||
const selectedCohort = useMemo(() => cohorts.find(cohort => (
|
||||
toString(cohort.id) === selectedFilters.cohort)),
|
||||
[selectedFilters.cohort]);
|
||||
|
||||
const allFilters = [
|
||||
{
|
||||
id: 'type-all',
|
||||
label: intl.formatMessage(messages.allPosts),
|
||||
value: ThreadType.ALL,
|
||||
},
|
||||
{
|
||||
id: 'type-discussions',
|
||||
label: intl.formatMessage(messages.filterDiscussions),
|
||||
value: ThreadType.DISCUSSION,
|
||||
},
|
||||
{
|
||||
id: 'type-questions',
|
||||
label: intl.formatMessage(messages.filterQuestions),
|
||||
value: ThreadType.QUESTION,
|
||||
},
|
||||
{
|
||||
id: 'status-any',
|
||||
label: intl.formatMessage(messages.filterAnyStatus),
|
||||
value: PostsStatusFilter.ALL,
|
||||
},
|
||||
{
|
||||
id: 'status-unread',
|
||||
label: intl.formatMessage(messages.filterUnread),
|
||||
value: PostsStatusFilter.UNREAD,
|
||||
},
|
||||
{
|
||||
id: 'status-reported',
|
||||
label: intl.formatMessage(messages.filterReported),
|
||||
value: PostsStatusFilter.REPORTED,
|
||||
},
|
||||
{
|
||||
id: 'status-unanswered',
|
||||
label: intl.formatMessage(messages.filterUnanswered),
|
||||
value: PostsStatusFilter.UNANSWERED,
|
||||
},
|
||||
{
|
||||
id: 'status-unresponded',
|
||||
label: intl.formatMessage(messages.filterUnresponded),
|
||||
value: PostsStatusFilter.UNRESPONDED,
|
||||
},
|
||||
{
|
||||
id: 'sort-activity',
|
||||
label: intl.formatMessage(messages.lastActivityAt),
|
||||
value: ThreadOrdering.BY_LAST_ACTIVITY,
|
||||
},
|
||||
{
|
||||
id: 'sort-comments',
|
||||
label: intl.formatMessage(messages.commentCount),
|
||||
value: ThreadOrdering.BY_COMMENT_COUNT,
|
||||
},
|
||||
{
|
||||
id: 'sort-votes',
|
||||
label: intl.formatMessage(messages.voteCount),
|
||||
value: ThreadOrdering.BY_VOTE_COUNT,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Collapsible.Advanced
|
||||
open={isOpen}
|
||||
onToggle={() => setOpen(!isOpen)}
|
||||
className="filter-bar collapsible-card-lg border-0"
|
||||
>
|
||||
<Collapsible.Trigger className="collapsible-trigger border-0">
|
||||
<span className="text-primary-700 pr-4">
|
||||
{intl.formatMessage(messages.sortFilterStatus, {
|
||||
own: false,
|
||||
type: selectedFilters.postType,
|
||||
sort: selectedFilters.orderBy,
|
||||
status: selectedFilters.status,
|
||||
cohortType: selectedCohort?.name ? 'group' : 'all',
|
||||
cohort: capitalize(selectedCohort?.name),
|
||||
})}
|
||||
</span>
|
||||
<Collapsible.Visible whenClosed>
|
||||
<Icon src={Tune} />
|
||||
</Collapsible.Visible>
|
||||
<Collapsible.Visible whenOpen>
|
||||
<Icon src={Tune} />
|
||||
</Collapsible.Visible>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Body className="collapsible-body px-4 pb-3 pt-0">
|
||||
<Form>
|
||||
<div className="d-flex flex-row py-2 justify-content-between">
|
||||
{filters.map((value) => (
|
||||
<Form.RadioSet
|
||||
name={value.name}
|
||||
className="d-flex flex-column list-group list-group-flush"
|
||||
value={selectedFilters[value.name]}
|
||||
onChange={onFilterChange}
|
||||
>
|
||||
{
|
||||
value.filters.map(filterName => {
|
||||
const element = allFilters.find(obj => obj.id === filterName);
|
||||
if (element) {
|
||||
return (
|
||||
<ActionItem
|
||||
id={element.id}
|
||||
label={element.label}
|
||||
value={element.value}
|
||||
selected={selectedFilters[value.name]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return false;
|
||||
})
|
||||
}
|
||||
|
||||
</Form.RadioSet>
|
||||
))}
|
||||
</div>
|
||||
{showCohortsFilter && (
|
||||
<>
|
||||
<div className="border-bottom my-2" />
|
||||
{status === RequestStatus.IN_PROGRESS ? (
|
||||
<div className="d-flex justify-content-center p-4">
|
||||
<Spinner animation="border" variant="primary" size="lg" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="d-flex flex-row pt-2">
|
||||
<Form.RadioSet
|
||||
name="cohort"
|
||||
className="d-flex flex-column list-group list-group-flush w-100"
|
||||
value={selectedFilters.cohort}
|
||||
onChange={onFilterChange}
|
||||
>
|
||||
<ActionItem
|
||||
id="all-groups"
|
||||
label="All groups"
|
||||
value=""
|
||||
selected={selectedFilters.cohort}
|
||||
/>
|
||||
{cohorts.map(cohort => (
|
||||
<ActionItem
|
||||
key={toString(cohort.id)}
|
||||
id={toString(cohort.id)}
|
||||
label={capitalize(cohort.name)}
|
||||
value={toString(cohort.id)}
|
||||
selected={selectedFilters.cohort}
|
||||
/>
|
||||
))}
|
||||
</Form.RadioSet>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</Collapsible.Body>
|
||||
</Collapsible.Advanced>
|
||||
);
|
||||
}
|
||||
|
||||
FilterBar.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
filters: PropTypes.array.isRequired,
|
||||
selectedFilters: PropTypes.object.isRequired,
|
||||
onFilterChange: PropTypes.func.isRequired,
|
||||
showCohortsFilter: PropTypes.bool,
|
||||
};
|
||||
|
||||
FilterBar.defaultProps = {
|
||||
showCohortsFilter: false,
|
||||
};
|
||||
|
||||
export default injectIntl(FilterBar);
|
||||
@@ -11,15 +11,12 @@ const baseConfig = {
|
||||
['\\\\(', '\\\\)'],
|
||||
['\\(', '\\)'],
|
||||
['[mathjaxinline]', '[/mathjaxinline]'],
|
||||
['\\begin{math}', '\\end{math}'],
|
||||
],
|
||||
displayMath: [
|
||||
['[mathjax]', '[/mathjax]'],
|
||||
['$$', '$$'],
|
||||
['\\\\[', '\\\\]'],
|
||||
['\\[', '\\]'],
|
||||
['\\begin{displaymath}', '\\end{displaymath}'],
|
||||
['\\begin{equation}', '\\end{equation}'],
|
||||
],
|
||||
},
|
||||
|
||||
@@ -30,9 +27,6 @@ function HTMLLoader({ htmlNode, componentId, cssClassName }) {
|
||||
const isLatex = htmlNode.match(/(\${1,2})((?:\\.|.)*)\1/)
|
||||
|| htmlNode.match(/(\[mathjax](.+?)\[\/mathjax])+/)
|
||||
|| htmlNode.match(/(\[mathjaxinline](.+?)\[\/mathjaxinline])+/)
|
||||
|| htmlNode.match(/(\\begin\{math}(.+?)\\end\{math})+/)
|
||||
|| htmlNode.match(/(\\begin\{displaymath}(.+?)\\end\{displaymath})+/)
|
||||
|| htmlNode.match(/(\\begin\{equation}(.+?)\\end\{equation})+/)
|
||||
|| htmlNode.match(/(\\\[(.+?)\\\])+/)
|
||||
|| htmlNode.match(/(\\\((.+?)\\\))+/);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { camelCaseObject } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
import { getApiBaseUrl } from '../../../data/constants';
|
||||
import { API_BASE_URL } from '../../../data/constants';
|
||||
|
||||
function normalizeCourseHomeCourseMetadata(metadata, rootSlug) {
|
||||
const data = camelCaseObject(metadata);
|
||||
@@ -21,7 +21,7 @@ function normalizeCourseHomeCourseMetadata(metadata, rootSlug) {
|
||||
}
|
||||
|
||||
export async function getCourseHomeCourseMetadata(courseId, rootSlug) {
|
||||
const url = `${getApiBaseUrl()}/api/course_home/course_metadata/${courseId}`;
|
||||
const url = `${API_BASE_URL}/api/course_home/course_metadata/${courseId}`;
|
||||
// don't know the context of adding timezone in url. hence omitting it
|
||||
// url = appendBrowserTimezoneToUrl(url);
|
||||
const { data } = await getAuthenticatedHttpClient().get(url);
|
||||
|
||||
@@ -17,8 +17,8 @@ function PostPreviewPane({
|
||||
<>
|
||||
{showPreviewPane && (
|
||||
<div
|
||||
className={`w-100 p-2 bg-light-200 rounded box-shadow-down-1 post-preview ${isPost ? 'mt-2 mb-5' : 'my-3'}`}
|
||||
style={{ minHeight: '200px', wordBreak: 'break-word' }}
|
||||
className={`p-2 bg-light-200 rounded box-shadow-down-1 post-preview ${isPost ? 'mt-2 mb-5' : 'my-3'}`}
|
||||
style={{ maxHeight: '200px', overflow: 'scroll' }}
|
||||
>
|
||||
<IconButton
|
||||
onClick={() => setShowPreviewPane(false)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
|
||||
import camelCase from 'lodash/camelCase';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
@@ -14,7 +14,6 @@ import postsMessages from '../discussions/posts/post-actions-bar/messages';
|
||||
import { setFilter as setTopicFilter } from '../discussions/topics/data/slices';
|
||||
|
||||
function Search({ intl }) {
|
||||
const [previousSearchValue, setPreviousSearchValue] = useState('');
|
||||
const dispatch = useDispatch();
|
||||
const { page } = useContext(DiscussionContext);
|
||||
const postSearch = useSelector(({ threads }) => threads.filters.search);
|
||||
@@ -36,7 +35,6 @@ function Search({ intl }) {
|
||||
dispatch(setSearchQuery(''));
|
||||
dispatch(setTopicFilter(''));
|
||||
dispatch(setUsernameSearch(''));
|
||||
setPreviousSearchValue('');
|
||||
};
|
||||
|
||||
const onChange = (query) => {
|
||||
@@ -44,7 +42,7 @@ function Search({ intl }) {
|
||||
};
|
||||
|
||||
const onSubmit = (query) => {
|
||||
if (query === '' || query === previousSearchValue) {
|
||||
if (query === '') {
|
||||
return;
|
||||
}
|
||||
if (isPostSearch) {
|
||||
@@ -54,7 +52,6 @@ function Search({ intl }) {
|
||||
} else if (page === 'learners') {
|
||||
dispatch(setUsernameSearch(query));
|
||||
}
|
||||
setPreviousSearchValue(query);
|
||||
};
|
||||
|
||||
useEffect(() => onClear(), [page]);
|
||||
|
||||
@@ -14,23 +14,18 @@ function SearchInfo({
|
||||
text,
|
||||
loadingStatus,
|
||||
onClear,
|
||||
textSearchRewrite,
|
||||
}) {
|
||||
return (
|
||||
<div className="d-flex flex-row border-bottom border-light-400">
|
||||
<Icon src={Search} className="justify-content-start ml-3.5 mr-2 mb-2 mt-2.5" />
|
||||
<Button variant="" size="inline" className="text-justify p-2">
|
||||
{loadingStatus === RequestStatus.SUCCESSFUL && (
|
||||
textSearchRewrite ? intl.formatMessage(messages.searchRewriteInfo, {
|
||||
searchString: text,
|
||||
count,
|
||||
textSearchRewrite,
|
||||
})
|
||||
: intl.formatMessage(messages.searchInfo, { count, text })
|
||||
)}
|
||||
{loadingStatus !== RequestStatus.SUCCESSFUL && intl.formatMessage(messages.searchInfoSearching)}
|
||||
<Button variant="" size="inline">
|
||||
{
|
||||
loadingStatus === RequestStatus.SUCCESSFUL
|
||||
? intl.formatMessage(messages.searchInfo, { count, text })
|
||||
: intl.formatMessage(messages.searchInfoSearching)
|
||||
}
|
||||
</Button>
|
||||
<Button variant="link" size="inline" className="ml-auto mr-3" onClick={onClear} style={{ minWidth: '26%' }}>
|
||||
<Button variant="link" size="inline" className="ml-auto mr-4" onClick={onClear}>
|
||||
{intl.formatMessage(messages.clearSearch)}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -42,13 +37,11 @@ SearchInfo.propTypes = {
|
||||
count: PropTypes.number.isRequired,
|
||||
text: PropTypes.string.isRequired,
|
||||
loadingStatus: PropTypes.string.isRequired,
|
||||
textSearchRewrite: PropTypes.string,
|
||||
onClear: PropTypes.func,
|
||||
};
|
||||
|
||||
SearchInfo.defaultProps = {
|
||||
onClear: () => {},
|
||||
textSearchRewrite: null,
|
||||
};
|
||||
|
||||
export default injectIntl(SearchInfo);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { Editor } from '@tinymce/tinymce-react';
|
||||
import { useParams } from 'react-router';
|
||||
@@ -6,11 +6,7 @@ import { useParams } from 'react-router';
|
||||
// eslint-disable-next-line no-unused-vars,import/no-extraneous-dependencies
|
||||
import tinymce from 'tinymce/tinymce';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { ActionRow, AlertModal, Button } from '@edx/paragon';
|
||||
|
||||
import { MAX_UPLOAD_FILE_SIZE } from '../data/constants';
|
||||
import messages from '../discussions/messages';
|
||||
import { uploadFile } from '../discussions/posts/data/api';
|
||||
|
||||
import 'tinymce/plugins/code';
|
||||
@@ -22,7 +18,6 @@ import 'tinymce/icons/default';
|
||||
import 'tinymce/skins/ui/oxide/skin.css';
|
||||
// importing the plugin js.
|
||||
import 'tinymce/plugins/autolink';
|
||||
import 'tinymce/plugins/autoresize';
|
||||
import 'tinymce/plugins/autosave';
|
||||
import 'tinymce/plugins/codesample';
|
||||
import 'tinymce/plugins/image';
|
||||
@@ -62,8 +57,7 @@ export default function TinyMCEEditor(props) {
|
||||
// loading process and is instead loaded as a string via content_style
|
||||
|
||||
const { courseId, postId } = useParams();
|
||||
const [showImageWarning, setShowImageWarning] = useState(false);
|
||||
const intl = useIntl();
|
||||
|
||||
const uploadHandler = async (blobInfo, success, failure) => {
|
||||
try {
|
||||
const blob = blobInfo.blob();
|
||||
@@ -74,11 +68,6 @@ export default function TinyMCEEditor(props) {
|
||||
}
|
||||
const filename = blobInfo.filename();
|
||||
const { location } = await uploadFile(blob, filename, courseId, postId || 'root');
|
||||
const img = new Image();
|
||||
img.onload = function () {
|
||||
if (img.height > 999 || img.width > 999) { setShowImageWarning(true); }
|
||||
};
|
||||
img.src = location;
|
||||
success(location);
|
||||
} catch (e) {
|
||||
failure(e.toString(), { remove: true });
|
||||
@@ -94,54 +83,34 @@ export default function TinyMCEEditor(props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Editor
|
||||
init={{
|
||||
skin: false,
|
||||
menubar: false,
|
||||
branding: false,
|
||||
contextmenu: false,
|
||||
browser_spellcheck: true,
|
||||
a11y_advanced_options: true,
|
||||
autosave_interval: '1s',
|
||||
autosave_restore_when_empty: false,
|
||||
plugins: 'autoresize autosave codesample link lists image imagetools code emoticons charmap',
|
||||
toolbar: 'undo redo'
|
||||
+ ' | formatselect | bold italic underline'
|
||||
+ ' | link blockquote openedx_code image'
|
||||
+ ' | bullist numlist outdent indent'
|
||||
+ ' | removeformat'
|
||||
+ ' | openedx_html'
|
||||
+ ' | emoticons'
|
||||
+ ' | charmap',
|
||||
content_css: false,
|
||||
content_style: contentStyle,
|
||||
body_class: 'm-2 text-editor',
|
||||
default_link_target: '_blank',
|
||||
target_list: false,
|
||||
images_upload_handler: uploadHandler,
|
||||
setup,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
<AlertModal
|
||||
title={intl.formatMessage(messages.imageWarningModalTitle)}
|
||||
isOpen={showImageWarning}
|
||||
onClose={() => setShowImageWarning(false)}
|
||||
isBlocking
|
||||
footerNode={(
|
||||
<ActionRow>
|
||||
<Button variant="danger" onClick={() => setShowImageWarning(false)}>
|
||||
{intl.formatMessage(messages.imageWarningDismissButton)}
|
||||
</Button>
|
||||
</ActionRow>
|
||||
)}
|
||||
>
|
||||
<p>
|
||||
{intl.formatMessage(messages.imageWarningMessage)}
|
||||
</p>
|
||||
</AlertModal>
|
||||
</>
|
||||
|
||||
<Editor
|
||||
init={{
|
||||
skin: false,
|
||||
menubar: false,
|
||||
branding: false,
|
||||
contextmenu: false,
|
||||
browser_spellcheck: true,
|
||||
a11y_advanced_options: true,
|
||||
autosave_interval: '1s',
|
||||
autosave_restore_when_empty: false,
|
||||
plugins: 'autosave codesample link lists image imagetools code emoticons charmap',
|
||||
toolbar: 'undo redo'
|
||||
+ ' | formatselect | bold italic underline'
|
||||
+ ' | link blockquote openedx_code image'
|
||||
+ ' | bullist numlist outdent indent'
|
||||
+ ' | removeformat'
|
||||
+ ' | openedx_html'
|
||||
+ ' | emoticons'
|
||||
+ ' | charmap',
|
||||
content_css: false,
|
||||
content_style: contentStyle,
|
||||
body_class: 'm-2 text-editor',
|
||||
default_link_target: '_blank',
|
||||
target_list: false,
|
||||
images_upload_handler: uploadHandler,
|
||||
setup,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
24
src/components/icons/ReportGmailerrorred.jsx
Normal file
24
src/components/icons/ReportGmailerrorred.jsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function ReportGmailerrorred() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
fill="none"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<g clipPath="url(#clip0_6935_1296)">
|
||||
<path d="M13.1083 2.5H6.89167L2.5 6.89167V13.1083L6.89167 17.5H13.1083L17.5 13.1083V6.89167L13.1083 2.5ZM15.8333 12.4167L12.4167 15.8333H7.58333L4.16667 12.4167V7.58333L7.58333 4.16667H12.4167L15.8333 7.58333V12.4167Z" fill="#00262B" />
|
||||
<path d="M9.99996 14.1667C10.4602 14.1667 10.8333 13.7936 10.8333 13.3333C10.8333 12.8731 10.4602 12.5 9.99996 12.5C9.53972 12.5 9.16663 12.8731 9.16663 13.3333C9.16663 13.7936 9.53972 14.1667 9.99996 14.1667Z" fill="#00262B" />
|
||||
<path d="M9.16663 5.83331H10.8333V11.6666H9.16663V5.83331Z" fill="#00262B" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_6935_1296">
|
||||
<rect width="20" height="20" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ export { default as PushPin } from './PushPin';
|
||||
export { default as Question } from './Question';
|
||||
export { default as QuestionAnswer } from './QuestionAnswer';
|
||||
export { default as QuestionAnswerOutline } from './QuestionAnswerOutline';
|
||||
export { default as ReportGmailerrorred } from './ReportGmailerrorred';
|
||||
export { default as StarFilled } from './StarFilled';
|
||||
export { default as StarOutline } from './StarOutline';
|
||||
export { default as ThumbUpFilled } from './ThumbUpFilled';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
import { getApiBaseUrl } from './constants';
|
||||
import { API_BASE_URL } from './constants';
|
||||
|
||||
export const getBlocksAPIURL = () => `${getApiBaseUrl()}/api/courses/v1/blocks/`;
|
||||
export const blocksAPIURL = `${API_BASE_URL}/api/courses/v1/blocks/`;
|
||||
export async function getCourseBlocks(courseId, username) {
|
||||
const params = {
|
||||
course_id: courseId,
|
||||
@@ -14,6 +14,6 @@ export async function getCourseBlocks(courseId, username) {
|
||||
student_view_data: 'discussion',
|
||||
};
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(getBlocksAPIURL(), { params });
|
||||
.get(blocksAPIURL, { params });
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
export const getApiBaseUrl = () => getConfig().LMS_BASE_URL;
|
||||
export const API_BASE_URL = getConfig().LMS_BASE_URL;
|
||||
|
||||
/**
|
||||
* Enum for thread types.
|
||||
@@ -90,6 +90,16 @@ export const ThreadOrdering = {
|
||||
BY_VOTE_COUNT: 'voteCount',
|
||||
};
|
||||
|
||||
/**
|
||||
* Enum for thread view status filtering.
|
||||
* @readonly
|
||||
* @enum {string}
|
||||
*/
|
||||
export const ThreadViewStatus = {
|
||||
UNREAD: 'unread',
|
||||
UNANSWERED: 'unanswered',
|
||||
};
|
||||
|
||||
/**
|
||||
* Enum for filtering posts by status.
|
||||
* @readonly
|
||||
@@ -101,7 +111,6 @@ export const PostsStatusFilter = {
|
||||
FOLLOWING: 'statusFollowing',
|
||||
REPORTED: 'statusReported',
|
||||
UNANSWERED: 'statusUnanswered',
|
||||
UNRESPONDED: 'statusUnresponded',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -124,7 +133,6 @@ export const TopicOrdering = {
|
||||
export const LearnersOrdering = {
|
||||
BY_FLAG: 'flagged',
|
||||
BY_LAST_ACTIVITY: 'activity',
|
||||
BY_RECENCY: 'recency',
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,11 +7,10 @@ import { initializeMockApp } from '@edx/frontend-platform/testing';
|
||||
import { initializeStore } from '../store';
|
||||
import { executeThunk } from '../test-utils';
|
||||
import { getBlocksAPIResponse } from './__factories__';
|
||||
import { getBlocksAPIURL } from './api';
|
||||
import { blocksAPIURL } from './api';
|
||||
import { RequestStatus } from './constants';
|
||||
import { fetchCourseBlocks } from './thunks';
|
||||
|
||||
const blocksAPIURL = getBlocksAPIURL();
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
|
||||
let axiosMock;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
|
||||
import { selectDiscussionProvider, selectGroupAtSubsection } from '../discussions/data/selectors';
|
||||
import { selectDiscussionProvider } from '../discussions/data/selectors';
|
||||
import { DiscussionProvider } from './constants';
|
||||
|
||||
export const selectTopicContext = (topicId) => (state) => state.blocks.topics[topicId];
|
||||
@@ -14,18 +14,6 @@ export const selectorForUnitSubsection = createSelector(
|
||||
blocks => key => blocks[blocks[key]?.parent],
|
||||
);
|
||||
|
||||
// If subsection grouping is enabled, and the current selection is a unit, then get the current subsection.
|
||||
export const selectCurrentCategoryGrouping = createSelector(
|
||||
selectDiscussionProvider,
|
||||
selectGroupAtSubsection,
|
||||
selectBlocks,
|
||||
(provider, groupAtSubsection, blocks) => blockId => (
|
||||
(provider !== 'openedx' || !groupAtSubsection || blocks[blockId]?.type !== 'vertical')
|
||||
? blockId
|
||||
: blocks[blockId].parent
|
||||
),
|
||||
);
|
||||
|
||||
export const selectChapters = (state) => state.blocks.chapters;
|
||||
export const selectTopicsUnderCategory = createSelector(
|
||||
selectDiscussionProvider,
|
||||
|
||||
@@ -6,7 +6,9 @@ ensureConfig([
|
||||
'LMS_BASE_URL',
|
||||
], 'Comments API service');
|
||||
|
||||
export const getCohortsApiUrl = courseId => `${getConfig().LMS_BASE_URL}/api/cohorts/v1/courses/${courseId}/cohorts/`;
|
||||
const apiBaseUrl = getConfig().LMS_BASE_URL;
|
||||
|
||||
export const getCohortsApiUrl = courseId => `${apiBaseUrl}/api/cohorts/v1/courses/${courseId}/cohorts/`;
|
||||
|
||||
export async function getCourseCohorts(courseId) {
|
||||
const params = snakeCaseObject({ courseId });
|
||||
|
||||
@@ -1,28 +1,18 @@
|
||||
import React, { useContext, useEffect, useMemo } from 'react';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useParams } from 'react-router';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Button, Icon, IconButton, Spinner,
|
||||
} from '@edx/paragon';
|
||||
import { ArrowBack } from '@edx/paragon/icons';
|
||||
import { Button, Spinner } from '@edx/paragon';
|
||||
|
||||
import {
|
||||
EndorsementStatus, PostsPages, ThreadType,
|
||||
} from '../../data/constants';
|
||||
import { EndorsementStatus, ThreadType } from '../../data/constants';
|
||||
import { useDispatchWithState } from '../../data/hooks';
|
||||
import { DiscussionContext } from '../common/context';
|
||||
import { useIsOnDesktop } from '../data/hooks';
|
||||
import { EmptyPage } from '../empty-posts';
|
||||
import { Post } from '../posts';
|
||||
import { selectThread } from '../posts/data/selectors';
|
||||
import { fetchThread, markThreadAsRead } from '../posts/data/thunks';
|
||||
import { discussionsPath, filterPosts } from '../utils';
|
||||
import { filterPosts } from '../utils';
|
||||
import { selectThreadComments, selectThreadCurrentPage, selectThreadHasMorePages } from './data/selectors';
|
||||
import { fetchThreadComments } from './data/thunks';
|
||||
import { Comment, ResponseEditor } from './comment';
|
||||
@@ -79,31 +69,25 @@ function DiscussionCommentsView({
|
||||
} = usePostComments(postId, endorsed);
|
||||
const sortedComments = useMemo(() => [...filterPosts(comments, 'endorsed'),
|
||||
...filterPosts(comments, 'unendorsed')], [comments]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{((hasMorePages && isLoading) || !isLoading)
|
||||
&& (
|
||||
<div className="mx-4 text-primary-700" role="heading" aria-level="2" style={{ lineHeight: '28px' }}>
|
||||
{endorsed === EndorsementStatus.ENDORSED
|
||||
? intl.formatMessage(messages.endorsedResponseCount, { num: sortedComments.length })
|
||||
: intl.formatMessage(messages.responseCount, { num: sortedComments.length })}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mx-4" role="list">
|
||||
{sortedComments.map(comment => (
|
||||
<Comment comment={comment} key={comment.id} postType={postType} isClosedPost={isClosed} />
|
||||
))}
|
||||
{!!sortedComments.length && !isClosed
|
||||
&& <ResponseEditor postId={postId} addWrappingDiv />}
|
||||
{hasMorePages && !isLoading && (
|
||||
<Button
|
||||
onClick={handleLoadMoreResponses}
|
||||
variant="link"
|
||||
block="true"
|
||||
className="card p-4 mb-4 font-weight-500 font-size-14"
|
||||
style={{
|
||||
lineHeight: '20px',
|
||||
}}
|
||||
className="card p-4"
|
||||
data-testid="load-more-comments"
|
||||
>
|
||||
{intl.formatMessage(messages.loadMoreResponses)}
|
||||
@@ -115,8 +99,6 @@ function DiscussionCommentsView({
|
||||
<Spinner animation="border" variant="primary" />
|
||||
</div>
|
||||
)}
|
||||
{!!sortedComments.length && !isClosed
|
||||
&& <ResponseEditor postId={postId} addWrappingDiv />}
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -134,74 +116,23 @@ DiscussionCommentsView.propTypes = {
|
||||
};
|
||||
|
||||
function CommentsView({ intl }) {
|
||||
const [isLoading, submitDispatch] = useDispatchWithState();
|
||||
const { postId } = useParams();
|
||||
const thread = usePost(postId);
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const isOnDesktop = useIsOnDesktop();
|
||||
const {
|
||||
courseId, learnerUsername, category, topicId, page, inContext,
|
||||
} = useContext(DiscussionContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (!thread) { submitDispatch(fetchThread(postId, courseId, true)); }
|
||||
}, [postId]);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
if (!thread) {
|
||||
if (!isLoading) {
|
||||
return (
|
||||
<EmptyPage title={intl.formatMessage(messages.noThreadFound)} />
|
||||
);
|
||||
}
|
||||
dispatch(fetchThread(postId, true));
|
||||
return (
|
||||
<Spinner animation="border" variant="primary" data-testid="loading-indicator" />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isOnDesktop && (
|
||||
inContext ? (
|
||||
<>
|
||||
<div className="px-4 py-1.5 bg-white">
|
||||
<Button
|
||||
variant="plain"
|
||||
className="px-0 font-weight-light text-primary-500"
|
||||
iconBefore={ArrowBack}
|
||||
onClick={() => history.push(discussionsPath(PostsPages[page], {
|
||||
courseId, learnerUsername, category, topicId,
|
||||
})(location))}
|
||||
size="sm"
|
||||
>
|
||||
{intl.formatMessage(messages.backAlt)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="border-bottom border-light-400" />
|
||||
</>
|
||||
) : (
|
||||
<IconButton
|
||||
src={ArrowBack}
|
||||
iconAs={Icon}
|
||||
style={{ padding: '18px' }}
|
||||
size="inline"
|
||||
className="ml-4 mt-4"
|
||||
onClick={() => history.push(discussionsPath(PostsPages[page], {
|
||||
courseId, learnerUsername, category, topicId,
|
||||
})(location))}
|
||||
alt={intl.formatMessage(messages.backAlt)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div className={classNames('discussion-comments d-flex flex-column card', {
|
||||
'm-4 p-4.5': !inContext,
|
||||
'p-4 rounded-0 border-0 mb-4': inContext,
|
||||
})}
|
||||
>
|
||||
<div className="discussion-comments d-flex flex-column m-4 p-4.5 card">
|
||||
<Post post={thread} />
|
||||
{!thread.closed && <ResponseEditor postId={postId} /> }
|
||||
</div>
|
||||
{thread.type === ThreadType.DISCUSSION && (
|
||||
{thread.type === ThreadType.DISCUSSION
|
||||
&& (
|
||||
<DiscussionCommentsView
|
||||
postId={postId}
|
||||
intl={intl}
|
||||
@@ -209,7 +140,7 @@ function CommentsView({ intl }) {
|
||||
endorsed={EndorsementStatus.DISCUSSION}
|
||||
isClosed={thread.closed}
|
||||
/>
|
||||
)}
|
||||
)}
|
||||
{thread.type === ThreadType.QUESTION && (
|
||||
<>
|
||||
<DiscussionCommentsView
|
||||
|
||||
@@ -13,19 +13,16 @@ import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { initializeStore } from '../../store';
|
||||
import { executeThunk } from '../../test-utils';
|
||||
import { DiscussionContext } from '../common/context';
|
||||
import { getCourseConfigApiUrl } from '../data/api';
|
||||
import { courseConfigApiUrl } from '../data/api';
|
||||
import { fetchCourseConfig } from '../data/thunks';
|
||||
import DiscussionContent from '../discussions-home/DiscussionContent';
|
||||
import { getThreadsApiUrl } from '../posts/data/api';
|
||||
import { threadsApiUrl } from '../posts/data/api';
|
||||
import { fetchThreads } from '../posts/data/thunks';
|
||||
import { getCommentsApiUrl } from './data/api';
|
||||
import { commentsApiUrl } from './data/api';
|
||||
|
||||
import '../posts/data/__factories__';
|
||||
import './data/__factories__';
|
||||
|
||||
const courseConfigApiUrl = getCourseConfigApiUrl();
|
||||
const commentsApiUrl = getCommentsApiUrl();
|
||||
const threadsApiUrl = getThreadsApiUrl();
|
||||
const discussionPostId = 'thread-1';
|
||||
const questionPostId = 'thread-2';
|
||||
const closedPostId = 'thread-2';
|
||||
@@ -105,7 +102,7 @@ function renderComponent(postId) {
|
||||
}
|
||||
|
||||
describe('CommentsView', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
@@ -150,7 +147,7 @@ describe('CommentsView', () => {
|
||||
)];
|
||||
});
|
||||
|
||||
executeThunk(fetchThreads(courseId), store.dispatch, store.getState);
|
||||
await executeThunk(fetchThreads(courseId), store.dispatch, store.getState);
|
||||
mockAxiosReturnPagedComments();
|
||||
mockAxiosReturnPagedCommentsResponses();
|
||||
});
|
||||
@@ -452,9 +449,9 @@ describe('CommentsView', () => {
|
||||
describe('for discussion thread', () => {
|
||||
const findLoadMoreCommentsButton = () => screen.findByTestId('load-more-comments');
|
||||
|
||||
it('shown post not found when post id does not belong to course', async () => {
|
||||
it('shown spinner when post isn\'t loaded', async () => {
|
||||
renderComponent('unloaded-id');
|
||||
expect(await screen.findByText('Thread not found', { exact: true }))
|
||||
expect(await screen.findByTestId('loading-indicator'))
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import classNames from 'classnames';
|
||||
@@ -10,7 +10,6 @@ import { Button, useToggle } from '@edx/paragon';
|
||||
import HTMLLoader from '../../../components/HTMLLoader';
|
||||
import { ContentActions } from '../../../data/constants';
|
||||
import { AlertBanner, DeleteConfirmation, EndorsedAlertBanner } from '../../common';
|
||||
import { DiscussionContext } from '../../common/context';
|
||||
import { selectBlackoutDate } from '../../data/selectors';
|
||||
import { fetchThread } from '../../posts/data/thunks';
|
||||
import { inBlackoutDateRange } from '../../utils';
|
||||
@@ -40,9 +39,7 @@ function Comment({
|
||||
const hasMorePages = useSelector(selectCommentHasMorePages(comment.id));
|
||||
const currentPage = useSelector(selectCommentCurrentPage(comment.id));
|
||||
const blackoutDateRange = useSelector(selectBlackoutDate);
|
||||
const {
|
||||
courseId,
|
||||
} = useContext(DiscussionContext);
|
||||
|
||||
useEffect(() => {
|
||||
// If the comment has a parent comment, it won't have any children, so don't fetch them.
|
||||
if (hasChildren && !currentPage && showFullThread) {
|
||||
@@ -54,7 +51,7 @@ function Comment({
|
||||
[ContentActions.EDIT_CONTENT]: () => setEditing(true),
|
||||
[ContentActions.ENDORSE]: async () => {
|
||||
await dispatch(editComment(comment.id, { endorsed: !comment.endorsed }, ContentActions.ENDORSE));
|
||||
await dispatch(fetchThread(comment.threadId, courseId));
|
||||
await dispatch(fetchThread(comment.threadId));
|
||||
},
|
||||
[ContentActions.DELETE]: showDeleteConfirmation,
|
||||
[ContentActions.REPORT]: () => dispatch(editComment(comment.id, { flagged: !comment.abuseFlagged })),
|
||||
@@ -83,7 +80,7 @@ function Comment({
|
||||
<CommentHeader comment={comment} actionHandlers={actionHandlers} postType={postType} />
|
||||
{isEditing
|
||||
? (
|
||||
<CommentEditor comment={comment} onCloseEditor={() => setEditing(false)} formClasses="pt-3" />
|
||||
<CommentEditor comment={comment} onCloseEditor={() => setEditing(false)} />
|
||||
)
|
||||
: <HTMLLoader cssClassName="comment-body pt-4 text-primary-500" componentId="comment" htmlNode={comment.renderedBody} />}
|
||||
<CommentIcons
|
||||
@@ -115,7 +112,7 @@ function Comment({
|
||||
lineHeight: '20px',
|
||||
}}
|
||||
>
|
||||
{intl.formatMessage(messages.loadMoreComments)}
|
||||
{intl.formatMessage(messages.loadMoreResponses)}
|
||||
</Button>
|
||||
)}
|
||||
{!isNested && showFullThread && (
|
||||
@@ -133,11 +130,8 @@ function Comment({
|
||||
{(!isClosedPost && !inBlackoutDateRange(blackoutDateRange))
|
||||
&& (
|
||||
<Button
|
||||
className="d-flex flex-grow mt-3 py-2 font-size-14"
|
||||
className="d-flex flex-grow mt-4.5"
|
||||
variant="outline-primary"
|
||||
style={{
|
||||
lineHeight: '20px',
|
||||
}}
|
||||
onClick={() => setReplying(true)}
|
||||
>
|
||||
{intl.formatMessage(messages.addComment)}
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
selectModerationSettings,
|
||||
selectUserHasModerationPrivileges,
|
||||
selectUserIsGroupTa,
|
||||
selectUserIsStaff,
|
||||
} from '../../data/selectors';
|
||||
import { formikCompatibleHandler, isFormikFieldInvalid } from '../../utils';
|
||||
import { addComment, editComment } from '../data/thunks';
|
||||
@@ -28,19 +27,16 @@ function CommentEditor({
|
||||
comment,
|
||||
onCloseEditor,
|
||||
edit,
|
||||
formClasses,
|
||||
}) {
|
||||
const editorRef = useRef(null);
|
||||
const { authenticatedUser } = useContext(AppContext);
|
||||
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
|
||||
const userIsGroupTa = useSelector(selectUserIsGroupTa);
|
||||
const userIsStaff = useSelector(selectUserIsStaff);
|
||||
const { reasonCodesEnabled, editReasons } = useSelector(selectModerationSettings);
|
||||
const [submitting, dispatch] = useDispatchWithState();
|
||||
|
||||
const canDisplayEditReason = (reasonCodesEnabled && edit
|
||||
&& (userHasModerationPrivileges || userIsGroupTa || userIsStaff)
|
||||
&& comment?.author !== authenticatedUser.username
|
||||
const canDisplayEditReason = (reasonCodesEnabled && (userHasModerationPrivileges || userIsGroupTa)
|
||||
&& edit && comment.author !== authenticatedUser.username
|
||||
);
|
||||
|
||||
const editReasonCodeValidation = canDisplayEditReason && {
|
||||
@@ -55,9 +51,7 @@ function CommentEditor({
|
||||
|
||||
const initialValues = {
|
||||
comment: comment.rawBody,
|
||||
editReasonCode: comment?.lastEdit?.reasonCode || (
|
||||
userIsStaff && canDisplayEditReason ? 'violates-guidelines' : undefined
|
||||
),
|
||||
editReasonCode: comment?.lastEdit?.reasonCode || '',
|
||||
};
|
||||
|
||||
const handleCloseEditor = (resetForm) => {
|
||||
@@ -100,7 +94,7 @@ function CommentEditor({
|
||||
handleChange,
|
||||
resetForm,
|
||||
}) => (
|
||||
<Form onSubmit={handleSubmit} className={formClasses}>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
{canDisplayEditReason && (
|
||||
<Form.Group
|
||||
isInvalid={isFormikFieldInvalid('editReasonCode', {
|
||||
@@ -110,7 +104,7 @@ function CommentEditor({
|
||||
>
|
||||
<Form.Control
|
||||
name="editReasonCode"
|
||||
className="mt-2 mr-0"
|
||||
className="mt-2"
|
||||
as="select"
|
||||
value={values.editReasonCode}
|
||||
onChange={handleChange}
|
||||
@@ -187,12 +181,10 @@ CommentEditor.propTypes = {
|
||||
onCloseEditor: PropTypes.func.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
edit: PropTypes.bool,
|
||||
formClasses: PropTypes.string,
|
||||
};
|
||||
|
||||
CommentEditor.defaultProps = {
|
||||
edit: true,
|
||||
formClasses: '',
|
||||
};
|
||||
|
||||
export default injectIntl(CommentEditor);
|
||||
|
||||
@@ -5,17 +5,14 @@ import classNames from 'classnames';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
import {
|
||||
Avatar, Icon,
|
||||
} from '@edx/paragon';
|
||||
import { Avatar, Icon } from '@edx/paragon';
|
||||
import { CheckCircle, Verified } from '@edx/paragon/icons';
|
||||
|
||||
import { AvatarOutlineAndLabelColors, EndorsementStatus, ThreadType } from '../../../data/constants';
|
||||
import { AvatarOutlineAndLabelColors, ThreadType } from '../../../data/constants';
|
||||
import { AuthorLabel } from '../../common';
|
||||
import ActionsDropdown from '../../common/ActionsDropdown';
|
||||
import { useAlertBannerVisible } from '../../data/hooks';
|
||||
import { selectAuthorAvatars } from '../../posts/data/selectors';
|
||||
import { useActions } from '../../utils';
|
||||
import { commentShape } from './proptypes';
|
||||
|
||||
function CommentHeader({
|
||||
@@ -27,20 +24,6 @@ function CommentHeader({
|
||||
const colorClass = AvatarOutlineAndLabelColors[comment.authorLabel];
|
||||
const hasAnyAlert = useAlertBannerVisible(comment);
|
||||
|
||||
const actions = useActions({
|
||||
...comment,
|
||||
postType,
|
||||
});
|
||||
const actionIcons = actions.find(({ action }) => action === EndorsementStatus.ENDORSED);
|
||||
|
||||
const handleIcons = (action) => {
|
||||
const actionFunction = actionHandlers[action];
|
||||
if (actionFunction) {
|
||||
actionFunction();
|
||||
} else {
|
||||
logError(`Unknown or unimplemented action ${action}`);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className={classNames('d-flex flex-row justify-content-between', {
|
||||
'mt-2': hasAnyAlert,
|
||||
@@ -56,31 +39,14 @@ function CommentHeader({
|
||||
height: '32px',
|
||||
}}
|
||||
/>
|
||||
<AuthorLabel
|
||||
author={comment.author}
|
||||
authorLabel={comment.authorLabel}
|
||||
labelColor={colorClass && `text-${colorClass}`}
|
||||
linkToProfile
|
||||
/>
|
||||
<AuthorLabel author={comment.author} authorLabel={comment.authorLabel} labelColor={colorClass && `text-${colorClass}`} linkToProfile />
|
||||
</div>
|
||||
<div className="d-flex align-items-center">
|
||||
|
||||
{actionIcons && (
|
||||
<span className="btn-icon btn-icon-sm mr-1 align-items-center">
|
||||
<Icon
|
||||
data-testid="check-icon"
|
||||
onClick={
|
||||
() => {
|
||||
handleIcons(actionIcons.action);
|
||||
}
|
||||
}
|
||||
src={actionIcons.icon}
|
||||
className={['endorse', 'unendorse'].includes(actionIcons.id) ? 'text-dark-500' : 'text-success-500'}
|
||||
size="sm"
|
||||
/>
|
||||
{comment.endorsed && (postType === 'question'
|
||||
? <Icon src={CheckCircle} className="text-success" data-testid="check-icon" />
|
||||
: <Icon src={Verified} className="text-dark-500" data-testid="verified-icon" />)}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<ActionsDropdown
|
||||
commentOrPost={{
|
||||
...comment,
|
||||
|
||||
@@ -30,7 +30,7 @@ const mockComment = {
|
||||
author: 'abc123',
|
||||
authorLabel: 'ABC 123',
|
||||
endorsed: true,
|
||||
editableFields: ['endorsed'],
|
||||
editableFields: [],
|
||||
};
|
||||
|
||||
describe('Comment Header', () => {
|
||||
@@ -48,7 +48,7 @@ describe('Comment Header', () => {
|
||||
|
||||
it('should render verified icon for endorsed discussion posts', () => {
|
||||
renderComponent(mockComment, 'discussion', {});
|
||||
expect(screen.queryAllByTestId('check-icon')).toHaveLength(1);
|
||||
expect(screen.queryAllByTestId('verified-icon')).toHaveLength(1);
|
||||
});
|
||||
it('should render check icon for endorsed question posts', () => {
|
||||
renderComponent(mockComment, 'question', {});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import classNames from 'classnames';
|
||||
@@ -7,7 +7,6 @@ import { useSelector } from 'react-redux';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@edx/paragon';
|
||||
|
||||
import { DiscussionContext } from '../../common/context';
|
||||
import { selectBlackoutDate } from '../../data/selectors';
|
||||
import { inBlackoutDateRange } from '../../utils';
|
||||
import messages from '../messages';
|
||||
@@ -18,7 +17,6 @@ function ResponseEditor({
|
||||
intl,
|
||||
addWrappingDiv,
|
||||
}) {
|
||||
const { inContext } = useContext(DiscussionContext);
|
||||
const [addingResponse, setAddingResponse] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -39,14 +37,7 @@ function ResponseEditor({
|
||||
)
|
||||
: !inBlackoutDateRange(blackoutDateRange) && (
|
||||
<div className={classNames({ 'mb-4': addWrappingDiv }, 'actions d-flex')}>
|
||||
<Button
|
||||
variant="primary"
|
||||
className={classNames('px-2.5 py-2 font-size-14', { 'w-100': inContext })}
|
||||
onClick={() => setAddingResponse(true)}
|
||||
style={{
|
||||
lineHeight: '20px',
|
||||
}}
|
||||
>
|
||||
<Button variant="primary" className="px-2.5 py-2" onClick={() => setAddingResponse(true)}>
|
||||
{intl.formatMessage(messages.addResponse)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,9 @@ ensureConfig([
|
||||
'LMS_BASE_URL',
|
||||
], 'Comments API service');
|
||||
|
||||
export const getCommentsApiUrl = () => `${getConfig().LMS_BASE_URL}/api/discussion/v1/comments/`;
|
||||
const apiBaseUrl = getConfig().LMS_BASE_URL;
|
||||
|
||||
export const commentsApiUrl = `${apiBaseUrl}/api/discussion/v1/comments/`;
|
||||
|
||||
/**
|
||||
* Returns all the comments for the specified thread.
|
||||
@@ -34,7 +36,7 @@ export async function getThreadComments(
|
||||
});
|
||||
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(getCommentsApiUrl(), { params });
|
||||
.get(commentsApiUrl, { params });
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -51,7 +53,7 @@ export async function getCommentResponses(
|
||||
pageSize,
|
||||
} = {},
|
||||
) {
|
||||
const url = `${getCommentsApiUrl()}${commentId}/`;
|
||||
const url = `${commentsApiUrl}${commentId}/`;
|
||||
const params = snakeCaseObject({
|
||||
page,
|
||||
pageSize,
|
||||
@@ -71,7 +73,7 @@ export async function getCommentResponses(
|
||||
*/
|
||||
export async function postComment(comment, threadId, parentId = null) {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.post(getCommentsApiUrl(), snakeCaseObject({ threadId, raw_body: comment, parentId }));
|
||||
.post(commentsApiUrl, snakeCaseObject({ threadId, raw_body: comment, parentId }));
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -92,7 +94,7 @@ export async function updateComment(commentId, {
|
||||
endorsed,
|
||||
editReasonCode,
|
||||
}) {
|
||||
const url = `${getCommentsApiUrl()}${commentId}/`;
|
||||
const url = `${commentsApiUrl}${commentId}/`;
|
||||
const postData = snakeCaseObject({
|
||||
raw_body: comment,
|
||||
voted,
|
||||
@@ -111,7 +113,7 @@ export async function updateComment(commentId, {
|
||||
* @param {string} commentId ID of comment to delete
|
||||
*/
|
||||
export async function deleteComment(commentId) {
|
||||
const url = `${getCommentsApiUrl()}${commentId}/`;
|
||||
const url = `${commentsApiUrl}${commentId}/`;
|
||||
await getAuthenticatedHttpClient()
|
||||
.delete(url);
|
||||
}
|
||||
|
||||
@@ -7,14 +7,13 @@ import { initializeMockApp } from '@edx/frontend-platform/testing';
|
||||
import { EndorsementStatus } from '../../../data/constants';
|
||||
import { initializeStore } from '../../../store';
|
||||
import { executeThunk } from '../../../test-utils';
|
||||
import { getCommentsApiUrl } from './api';
|
||||
import { commentsApiUrl } from './api';
|
||||
import {
|
||||
addComment, editComment, fetchCommentResponses, fetchThreadComments, removeComment,
|
||||
} from './thunks';
|
||||
|
||||
import './__factories__';
|
||||
|
||||
const commentsApiUrl = getCommentsApiUrl();
|
||||
let axiosMock;
|
||||
let store;
|
||||
|
||||
|
||||
@@ -16,11 +16,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Content reported for staff to review',
|
||||
description: 'Alert banner over comment that has been reported for abuse',
|
||||
},
|
||||
backAlt: {
|
||||
id: 'discussions.actions.back.alt',
|
||||
defaultMessage: 'Back to list',
|
||||
description: 'Back to Posts list button text',
|
||||
},
|
||||
responseCount: {
|
||||
id: 'discussions.comments.comment.responseCount',
|
||||
defaultMessage: `{num, plural,
|
||||
@@ -182,11 +177,6 @@ const messages = defineMessages({
|
||||
defaultMessage: '{time} ago',
|
||||
description: 'Time text for endorse banner',
|
||||
},
|
||||
noThreadFound: {
|
||||
id: 'discussion.thread.notFound',
|
||||
defaultMessage: 'Thread not found',
|
||||
description: 'message to show on screen if the request thread is not found in course',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
@@ -6,7 +6,7 @@ import { useSelector } from 'react-redux';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
import {
|
||||
Button, Dropdown, Icon, IconButton, ModalPopup, useToggle,
|
||||
Button, Dropdown, Icon, IconButton, ModalPopup,
|
||||
} from '@edx/paragon';
|
||||
import { MoreHoriz } from '@edx/paragon/icons';
|
||||
|
||||
@@ -16,7 +16,6 @@ import { selectBlackoutDate } from '../data/selectors';
|
||||
import messages from '../messages';
|
||||
import { postShape } from '../posts/post/proptypes';
|
||||
import { inBlackoutDateRange, useActions } from '../utils';
|
||||
import { DiscussionContext } from './context';
|
||||
|
||||
function ActionsDropdown({
|
||||
intl,
|
||||
@@ -24,10 +23,9 @@ function ActionsDropdown({
|
||||
disabled,
|
||||
actionHandlers,
|
||||
}) {
|
||||
const [isOpen, open, close] = useToggle(false);
|
||||
const [target, setTarget] = useState(null);
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
const dropdownIconRef = React.useRef(null);
|
||||
const actions = useActions(commentOrPost);
|
||||
const { inContext } = useContext(DiscussionContext);
|
||||
const handleActions = (action) => {
|
||||
const actionFunction = actionHandlers[action];
|
||||
if (actionFunction) {
|
||||
@@ -44,19 +42,19 @@ function ActionsDropdown({
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
onClick={open}
|
||||
onClick={() => setOpen(!isOpen)}
|
||||
alt={intl.formatMessage(messages.actionsAlt)}
|
||||
src={MoreHoriz}
|
||||
iconAs={Icon}
|
||||
disabled={disabled}
|
||||
size="sm"
|
||||
ref={setTarget}
|
||||
ref={dropdownIconRef}
|
||||
/>
|
||||
<ModalPopup
|
||||
onClose={close}
|
||||
positionRef={target}
|
||||
onClose={() => setOpen(false)}
|
||||
positionRef={dropdownIconRef}
|
||||
isOpen={isOpen}
|
||||
placement={inContext ? 'left' : 'auto-start'}
|
||||
placement="auto-start"
|
||||
>
|
||||
<div
|
||||
className="bg-white p-1 shadow d-flex flex-column"
|
||||
@@ -72,7 +70,7 @@ function ActionsDropdown({
|
||||
variant="tertiary"
|
||||
size="inline"
|
||||
onClick={() => {
|
||||
close();
|
||||
setOpen(false);
|
||||
handleActions(action.action);
|
||||
}}
|
||||
className="d-flex justify-content-start py-1.5 mr-4"
|
||||
|
||||
@@ -23,11 +23,12 @@ function AlertBanner({
|
||||
const { reasonCodesEnabled } = useSelector(selectModerationSettings);
|
||||
const userIsContentAuthor = getAuthenticatedUser().username === content.author;
|
||||
const canSeeLastEditOrClosedAlert = (userHasModerationPrivileges || userIsContentAuthor || userIsGroupTa);
|
||||
const canSeeReportedBanner = content?.abuseFlagged;
|
||||
const isReportedByCurrentUser = getAuthenticatedUser().username === content?.abuseFlaggedBy;
|
||||
const canSeeReportedBanner = (userHasModerationPrivileges || userIsGroupTa || isReportedByCurrentUser);
|
||||
|
||||
return (
|
||||
<>
|
||||
{canSeeReportedBanner && (
|
||||
{content.abuseFlagged && canSeeReportedBanner && (
|
||||
<Alert icon={Error} variant="danger" className="px-3 mb-2 py-10px shadow-none flex-fill">
|
||||
{intl.formatMessage(messages.abuseFlaggedMessage)}
|
||||
</Alert>
|
||||
|
||||
@@ -20,7 +20,6 @@ function AuthorLabel({
|
||||
authorLabel,
|
||||
linkToProfile,
|
||||
labelColor,
|
||||
alert,
|
||||
}) {
|
||||
const location = useLocation();
|
||||
const { courseId } = useContext(DiscussionContext);
|
||||
@@ -41,14 +40,14 @@ function AuthorLabel({
|
||||
const className = classNames('d-flex align-items-center', labelColor);
|
||||
|
||||
const showUserNameAsLink = useShowLearnersTab()
|
||||
&& linkToProfile && author && author !== intl.formatMessage(messages.anonymous);
|
||||
&& linkToProfile && author && author !== messages.anonymous;
|
||||
|
||||
const labelContents = (
|
||||
<div className={className}>
|
||||
<span
|
||||
className={classNames('mr-1 font-size-14 font-style-normal font-family-inter font-weight-500', {
|
||||
'text-primary-500': !authorLabelMessage && !isRetiredUser,
|
||||
'text-gray-700': isRetiredUser,
|
||||
'text-primary-500': !authorLabelMessage && !isRetiredUser && !alert,
|
||||
})}
|
||||
role="heading"
|
||||
aria-level="2"
|
||||
@@ -66,9 +65,8 @@ function AuthorLabel({
|
||||
)}
|
||||
{authorLabelMessage && (
|
||||
<span
|
||||
className={classNames('mr-1 font-size-14 font-style-normal font-family-inter font-weight-500', {
|
||||
'text-primary-500': !authorLabelMessage && !isRetiredUser && !alert,
|
||||
'text-gray-700': isRetiredUser,
|
||||
className={classNames('mr-3 font-size-14 font-style-normal font-family-inter font-weight-500', {
|
||||
'text-primary-500': !authorLabelMessage,
|
||||
})}
|
||||
style={{ marginLeft: '2px' }}
|
||||
>
|
||||
@@ -99,14 +97,12 @@ AuthorLabel.propTypes = {
|
||||
authorLabel: PropTypes.string,
|
||||
linkToProfile: PropTypes.bool,
|
||||
labelColor: PropTypes.string,
|
||||
alert: PropTypes.bool,
|
||||
};
|
||||
|
||||
AuthorLabel.defaultProps = {
|
||||
linkToProfile: false,
|
||||
authorLabel: null,
|
||||
labelColor: '',
|
||||
alert: false,
|
||||
};
|
||||
|
||||
export default injectIntl(AuthorLabel);
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import { initializeStore } from '../../store';
|
||||
import { executeThunk } from '../../test-utils';
|
||||
import { getCourseConfigApiUrl } from '../data/api';
|
||||
import { fetchCourseConfig } from '../data/thunks';
|
||||
import AuthorLabel from './AuthorLabel';
|
||||
import { DiscussionContext } from './context';
|
||||
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const courseConfigApiUrl = getCourseConfigApiUrl();
|
||||
let store;
|
||||
let axiosMock;
|
||||
let container;
|
||||
|
||||
function renderComponent(author, authorLabel, linkToProfile, labelColor) {
|
||||
const wrapper = render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<DiscussionContext.Provider value={{ courseId }}>
|
||||
<AuthorLabel
|
||||
author={author}
|
||||
authorLabel={authorLabel}
|
||||
linkToProfile={linkToProfile}
|
||||
labelColor={labelColor}
|
||||
/>
|
||||
</DiscussionContext.Provider>
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
container = wrapper.container;
|
||||
return container;
|
||||
}
|
||||
|
||||
describe('Author label', () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
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, {});
|
||||
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
|
||||
});
|
||||
|
||||
describe.each([
|
||||
['anonymous', null, false, ''],
|
||||
['ta_user', 'Community TA', true, 'text-TA-color'],
|
||||
['retired__user', null, false, ''],
|
||||
['staff_user', 'Staff', true, 'text-staff-color'],
|
||||
['learner_user', null, false, ''],
|
||||
])('for %s', (
|
||||
author, authorLabel, linkToProfile, labelColor,
|
||||
) => {
|
||||
it('it has author name text',
|
||||
async () => {
|
||||
renderComponent(author, authorLabel, linkToProfile, labelColor);
|
||||
const authorElement = container.querySelector('[role=heading]');
|
||||
const authorName = author.startsWith('retired__user') ? '[Deactivated]' : author;
|
||||
|
||||
expect(authorElement).toHaveTextContent(authorName);
|
||||
});
|
||||
|
||||
it(`it is "${!linkToProfile && 'not'}" clickable when linkToProfile is ${!!linkToProfile}`,
|
||||
async () => {
|
||||
renderComponent(author, authorLabel, linkToProfile, labelColor);
|
||||
|
||||
if (linkToProfile) {
|
||||
expect(screen.queryByTestId('learner-posts-link')).toBeInTheDocument();
|
||||
} else {
|
||||
expect(screen.queryByTestId('learner-posts-link')).not.toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
|
||||
it(`it has "${!linkToProfile && 'not'}" label text and label color when linkToProfile is ${!!linkToProfile}`,
|
||||
async () => {
|
||||
renderComponent(author, authorLabel, linkToProfile, labelColor);
|
||||
const authorElement = container.querySelector('[role=heading]');
|
||||
const labelElement = authorElement.parentNode.lastChild;
|
||||
const label = ['TA', 'Staff'].includes(labelElement.textContent) && labelElement.textContent;
|
||||
|
||||
if (linkToProfile) {
|
||||
expect(authorElement.parentNode).toHaveClass(labelColor);
|
||||
expect(authorElement.parentNode.lastChild).toHaveTextContent(label);
|
||||
} else {
|
||||
expect(authorElement.parentNode.lastChild).not.toHaveTextContent(label, { exact: true });
|
||||
expect(authorElement.parentNode).not.toHaveClass(labelColor, { exact: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -15,7 +15,7 @@ function DeleteConfirmation({
|
||||
onDelete,
|
||||
}) {
|
||||
return (
|
||||
<ModalDialog title={title} isOpen={isOpen} hasCloseButton={false} onClose={onClose} zIndex={5000}>
|
||||
<ModalDialog title={title} isOpen={isOpen} hasCloseButton={false} onClose={onClose}>
|
||||
<ModalDialog.Header>
|
||||
<ModalDialog.Title>
|
||||
{title}
|
||||
|
||||
@@ -39,19 +39,14 @@ function EndorsedAlertBanner({
|
||||
)}
|
||||
</strong>
|
||||
<span className="d-flex align-items-center mr-1 flex-wrap">
|
||||
<span className="mr-1">
|
||||
<span className="mr-2">
|
||||
{intl.formatMessage(
|
||||
isQuestion
|
||||
? messages.answeredLabel
|
||||
: messages.endorsedLabel,
|
||||
)}
|
||||
</span>
|
||||
<AuthorLabel
|
||||
author={content.endorsedBy}
|
||||
authorLabel={content.endorsedByLabel}
|
||||
linkToProfile
|
||||
alert={content.endorsed}
|
||||
/>
|
||||
<AuthorLabel author={content.endorsedBy} authorLabel={content.endorsedByLabel} linkToProfile />
|
||||
{intl.formatMessage(messages.time, { time: timeago.format(content.endorsedAt, 'time-locale') })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -7,14 +7,16 @@ ensureConfig([
|
||||
'LMS_BASE_URL',
|
||||
], 'Posts API service');
|
||||
|
||||
export const getCourseConfigApiUrl = () => `${getConfig().LMS_BASE_URL}/api/discussion/v1/courses/`;
|
||||
const apiBaseUrl = getConfig().LMS_BASE_URL;
|
||||
|
||||
export const courseConfigApiUrl = `${apiBaseUrl}/api/discussion/v1/courses/`;
|
||||
|
||||
/**
|
||||
* Get discussions course config
|
||||
* @param {string} courseId
|
||||
*/
|
||||
export async function getDiscussionsConfig(courseId) {
|
||||
const url = `${getCourseConfigApiUrl()}${courseId}/`;
|
||||
const url = `${courseConfigApiUrl}${courseId}/`;
|
||||
const { data } = await getAuthenticatedHttpClient().get(url);
|
||||
return data;
|
||||
}
|
||||
@@ -24,7 +26,7 @@ export async function getDiscussionsConfig(courseId) {
|
||||
* @param {string} courseId
|
||||
*/
|
||||
export async function getDiscussionsSettings(courseId) {
|
||||
const url = `${getCourseConfigApiUrl()}${courseId}/settings`;
|
||||
const url = `${courseConfigApiUrl}${courseId}/settings`;
|
||||
const { data } = await getAuthenticatedHttpClient().get(url);
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -11,9 +11,7 @@ import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { breakpoints, useWindowSize } from '@edx/paragon';
|
||||
|
||||
import { Routes } from '../../data/constants';
|
||||
import { selectTopicsUnderCategory } from '../../data/selectors';
|
||||
import { fetchCourseBlocks } from '../../data/thunks';
|
||||
import { DiscussionContext } from '../common/context';
|
||||
import { clearRedirect } from '../posts/data';
|
||||
import { selectTopics } from '../topics/data/selectors';
|
||||
import { fetchCourseTopics } from '../topics/data/thunks';
|
||||
@@ -23,7 +21,7 @@ import {
|
||||
selectModerationSettings,
|
||||
selectPostThreadCount,
|
||||
selectUserHasModerationPrivileges,
|
||||
selectUserIsGroupTa,
|
||||
selectUserIsGroupTa, selectUserIsStaff, selectUserRoles,
|
||||
} from './selectors';
|
||||
import { fetchCourseConfig } from './thunks';
|
||||
|
||||
@@ -72,7 +70,7 @@ export function useCourseDiscussionData(courseId) {
|
||||
}, [courseId]);
|
||||
}
|
||||
|
||||
export function useRedirectToThread(courseId, inContext) {
|
||||
export function useRedirectToThread(courseId) {
|
||||
const dispatch = useDispatch();
|
||||
const redirectToThread = useSelector(
|
||||
(state) => state.threads.redirectToThread,
|
||||
@@ -85,10 +83,9 @@ export function useRedirectToThread(courseId, inContext) {
|
||||
// stored in redirectToThread
|
||||
if (redirectToThread) {
|
||||
dispatch(clearRedirect());
|
||||
const newLocation = discussionsPath(Routes.COMMENTS.PAGES[inContext ? 'topics' : 'my-posts'], {
|
||||
const newLocation = discussionsPath(Routes.COMMENTS.PAGES['my-posts'], {
|
||||
courseId,
|
||||
postId: redirectToThread.threadId,
|
||||
topicId: redirectToThread.topicId,
|
||||
})(location);
|
||||
history.push(newLocation);
|
||||
}
|
||||
@@ -96,8 +93,7 @@ export function useRedirectToThread(courseId, inContext) {
|
||||
}
|
||||
|
||||
export function useIsOnDesktop() {
|
||||
const windowSize = useWindowSize();
|
||||
return windowSize.width >= breakpoints.large.minWidth;
|
||||
return window.outerWidth >= breakpoints.large.minWidth;
|
||||
}
|
||||
|
||||
export function useIsOnXLDesktop() {
|
||||
@@ -144,7 +140,8 @@ export const useAlertBannerVisible = (content) => {
|
||||
const { reasonCodesEnabled } = useSelector(selectModerationSettings);
|
||||
const userIsContentAuthor = getAuthenticatedUser().username === content.author;
|
||||
const canSeeLastEditOrClosedAlert = (userHasModerationPrivileges || userIsContentAuthor || userIsGroupTa);
|
||||
const canSeeReportedBanner = content.abuseFlagged;
|
||||
const isReportedByCurrentUser = getAuthenticatedUser().username === content?.abuseFlaggedBy;
|
||||
const canSeeReportedBanner = (userHasModerationPrivileges || userIsGroupTa || isReportedByCurrentUser);
|
||||
|
||||
return (
|
||||
(reasonCodesEnabled && canSeeLastEditOrClosedAlert && (content.lastEdit?.reason || content.closed))
|
||||
@@ -152,25 +149,12 @@ export const useAlertBannerVisible = (content) => {
|
||||
);
|
||||
};
|
||||
|
||||
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.
|
||||
* If the URL has the current block ID it cannot get the topicID from that. This hook
|
||||
* gets the topic ID from the URL if available, or from the current category otherwise.
|
||||
* It only returns an ID if a single ID is available, if navigating a subsection it
|
||||
* returns null.
|
||||
* @returns {null|string} A topic ID if a single one available in the current context.
|
||||
*/
|
||||
export const useCurrentDiscussionTopic = () => {
|
||||
const { topicId, category } = useContext(DiscussionContext);
|
||||
const topics = useSelector(selectTopicsUnderCategory)(category);
|
||||
if (topicId) {
|
||||
return topicId;
|
||||
}
|
||||
if (topics?.length === 1) {
|
||||
return topics[0];
|
||||
}
|
||||
return null;
|
||||
export const useShowLearnersTab = () => {
|
||||
const learnersTabEnabled = useSelector(selectLearnersTabEnabled);
|
||||
const userRoles = useSelector(selectUserRoles);
|
||||
const isAdmin = useSelector(selectUserIsStaff);
|
||||
const IsGroupTA = useSelector(selectUserIsGroupTa);
|
||||
const privileged = useSelector(selectUserHasModerationPrivileges);
|
||||
const allowedUsers = isAdmin || IsGroupTA || privileged || (userRoles.includes('Student') && userRoles.length > 1);
|
||||
return learnersTabEnabled && allowedUsers;
|
||||
};
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import { initializeStore } from '../../store';
|
||||
import { DiscussionContext } from '../common/context';
|
||||
import { useCurrentDiscussionTopic } from './hooks';
|
||||
|
||||
let store;
|
||||
initializeMockApp();
|
||||
describe('Hooks', () => {
|
||||
describe('useCurrentDiscussionTopic', () => {
|
||||
function ComponentWithHook() {
|
||||
const topic = useCurrentDiscussionTopic();
|
||||
return (
|
||||
<div>
|
||||
{String(topic)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderComponent({ topicId, category }) {
|
||||
return render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<DiscussionContext.Provider
|
||||
value={{
|
||||
topicId,
|
||||
category,
|
||||
}}
|
||||
>
|
||||
<ComponentWithHook />
|
||||
</DiscussionContext.Provider>
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
store = initializeStore({
|
||||
blocks: {
|
||||
blocks: {
|
||||
'some-unit-key': { topics: ['some-topic-0'], parent: 'some-sequence-key' },
|
||||
'some-sequence-key': { topics: ['some-topic-0'] },
|
||||
'another-sequence-key': { topics: ['some-topic-1', 'some-topic-2'] },
|
||||
'empty-key': { topics: [] },
|
||||
},
|
||||
},
|
||||
config: { provider: 'openedx' },
|
||||
});
|
||||
});
|
||||
|
||||
test('when topicId is in context', () => {
|
||||
const { queryByText } = renderComponent({ topicId: 'some-topic' });
|
||||
expect(queryByText('some-topic')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('when the category is a unit', () => {
|
||||
const { queryByText } = renderComponent({ category: 'some-unit-key' });
|
||||
expect(queryByText('some-topic-0')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('when the category is a sequence with one unit', () => {
|
||||
const { queryByText } = renderComponent({ category: 'some-sequence-key' });
|
||||
expect(queryByText('some-topic-0')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('when the category is a sequence with multiple units', () => {
|
||||
const { queryByText } = renderComponent({ category: 'another-sequence-key' });
|
||||
expect(queryByText('null')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('when the category is invalid', () => {
|
||||
const { queryByText } = renderComponent({ category: 'invalid-key' });
|
||||
expect(queryByText('null')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('when the category has no topics', () => {
|
||||
const { queryByText } = renderComponent({ category: 'empty-key' });
|
||||
expect(queryByText('null')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -22,8 +22,6 @@ export const selectDivisionSettings = state => state.config.settings;
|
||||
|
||||
export const selectBlackoutDate = state => state.config.blackouts;
|
||||
|
||||
export const selectGroupAtSubsection = state => state.config.groupAtSubsection;
|
||||
|
||||
export const selectModerationSettings = state => ({
|
||||
postCloseReasons: state.config.postCloseReasons,
|
||||
editReasons: state.config.editReasons,
|
||||
|
||||
@@ -11,7 +11,6 @@ const configSlice = createSlice({
|
||||
allowAnonymous: false,
|
||||
allowAnonymousToPeers: false,
|
||||
userRoles: [],
|
||||
groupAtSubsection: false,
|
||||
hasModerationPrivileges: false,
|
||||
isGroupTa: false,
|
||||
isUserAdmin: false,
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { PageBanner } from '@edx/paragon';
|
||||
|
||||
import { selectBlackoutDate } from '../data/selectors';
|
||||
import messages from '../messages';
|
||||
import { inBlackoutDateRange } from '../utils';
|
||||
|
||||
function BlackoutInformationBanner({
|
||||
intl,
|
||||
}) {
|
||||
const isDiscussionsBlackout = inBlackoutDateRange(useSelector(selectBlackoutDate));
|
||||
const [showBanner, setShowBanner] = useState(true);
|
||||
|
||||
return (
|
||||
<PageBanner
|
||||
variant="accentB"
|
||||
show={isDiscussionsBlackout && showBanner}
|
||||
dismissible
|
||||
onDismiss={() => setShowBanner(false)}
|
||||
>
|
||||
<div className="font-weight-500">
|
||||
{intl.formatMessage(messages.blackoutDiscussionInformation)}
|
||||
</div>
|
||||
</PageBanner>
|
||||
);
|
||||
}
|
||||
|
||||
BlackoutInformationBanner.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(BlackoutInformationBanner);
|
||||
@@ -1,76 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import { initializeStore } from '../../store';
|
||||
import { DiscussionContext } from '../common/context';
|
||||
import { fetchConfigSuccess } from '../data/slices';
|
||||
import messages from '../messages';
|
||||
import BlackoutInformationBanner from './BlackoutInformationBanner';
|
||||
|
||||
let store;
|
||||
let container;
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
let activeStartDate = new Date();
|
||||
activeStartDate.setDate(activeStartDate.getDate() - 2);
|
||||
let activeEndDate = new Date();
|
||||
activeEndDate.setDate(activeEndDate.getDate() + 2);
|
||||
activeStartDate = activeStartDate.toISOString();
|
||||
activeEndDate = activeEndDate.toISOString();
|
||||
|
||||
const getConfigData = (blackouts = []) => ({
|
||||
id: 'course-v1:edX+DemoX+Demo_Course',
|
||||
userRoles: ['Admin', 'Student'],
|
||||
hasModerationPrivileges: false,
|
||||
isGroupTa: false,
|
||||
isUserAdmin: false,
|
||||
blackouts,
|
||||
});
|
||||
|
||||
function renderComponent() {
|
||||
const wrapper = render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<DiscussionContext.Provider value={{ courseId }}>
|
||||
<BlackoutInformationBanner />
|
||||
</DiscussionContext.Provider>
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
container = wrapper.container;
|
||||
return container;
|
||||
}
|
||||
|
||||
describe('Blackout Information Banner', () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: false,
|
||||
roles: ['Student'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ blackouts: [], visibility: false },
|
||||
{ blackouts: ['2021-12-31T10:15', '2021-12-31T10:20'], visibility: false },
|
||||
{ blackouts: [{ start: activeStartDate, end: activeEndDate }], visibility: true },
|
||||
{ blackouts: [{ start: activeEndDate, end: activeEndDate }], visibility: false },
|
||||
])('Test Blackout Banner is visible on app load if blackout date is active', async ({ blackouts, visibility }) => {
|
||||
store = initializeStore();
|
||||
await store.dispatch(fetchConfigSuccess(getConfigData(blackouts)));
|
||||
renderComponent();
|
||||
if (visibility) {
|
||||
const element = await screen.findByRole('alert');
|
||||
expect(element).toBeInTheDocument();
|
||||
expect(element).toHaveTextContent(messages.blackoutDiscussionInformation.defaultMessage);
|
||||
} else {
|
||||
const element = await screen.queryByRole('alert');
|
||||
expect(element).not.toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,20 +1,46 @@
|
||||
import React from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Route, Switch } from 'react-router';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Icon, IconButton } from '@edx/paragon';
|
||||
import { ArrowBack } from '@edx/paragon/icons';
|
||||
|
||||
import { Routes } from '../../data/constants';
|
||||
import { PostsPages, Routes } from '../../data/constants';
|
||||
import { CommentsView } from '../comments';
|
||||
import { DiscussionContext } from '../common/context';
|
||||
import { useIsOnDesktop } from '../data/hooks';
|
||||
import messages from '../messages';
|
||||
import { PostEditor } from '../posts';
|
||||
import { discussionsPath } from '../utils';
|
||||
|
||||
function DiscussionContent() {
|
||||
function DiscussionContent({ intl }) {
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
const postEditorVisible = useSelector((state) => state.threads.postEditorVisible);
|
||||
const isOnDesktop = useIsOnDesktop();
|
||||
const {
|
||||
courseId, learnerUsername, category, topicId, page,
|
||||
} = useContext(DiscussionContext);
|
||||
|
||||
return (
|
||||
<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">
|
||||
{!isOnDesktop && (
|
||||
<IconButton
|
||||
src={ArrowBack}
|
||||
iconAs={Icon}
|
||||
style={{ padding: '18px' }}
|
||||
size="inline"
|
||||
className="ml-4 mt-4"
|
||||
onClick={() => history.push(discussionsPath(PostsPages[page], {
|
||||
courseId, learnerUsername, category, topicId,
|
||||
})(location))}
|
||||
alt={intl.formatMessage(messages.backAlt)}
|
||||
/>
|
||||
)}
|
||||
{postEditorVisible ? (
|
||||
<Route path={Routes.POSTS.NEW_POST}>
|
||||
<PostEditor />
|
||||
@@ -34,4 +60,8 @@ function DiscussionContent() {
|
||||
);
|
||||
}
|
||||
|
||||
DiscussionContent.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(DiscussionContent);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useContext, useEffect, useRef } from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import classNames from 'classnames';
|
||||
@@ -7,10 +7,7 @@ import {
|
||||
Redirect, Route, Switch, useLocation,
|
||||
} from 'react-router';
|
||||
|
||||
import { useWindowSize } from '@edx/paragon';
|
||||
|
||||
import { RequestStatus, Routes } from '../../data/constants';
|
||||
import { DiscussionContext } from '../common/context';
|
||||
import {
|
||||
useContainerSize, useIsOnDesktop, useIsOnXLDesktop, useShowLearnersTab,
|
||||
} from '../data/hooks';
|
||||
@@ -27,29 +24,26 @@ export default function DiscussionSidebar({ displaySidebar, postActionBarRef })
|
||||
const redirectToLearnersTab = useShowLearnersTab();
|
||||
const sidebarRef = useRef(null);
|
||||
const postActionBarHeight = useContainerSize(postActionBarRef);
|
||||
const { height: windowHeight } = useWindowSize();
|
||||
const { inContext } = useContext(DiscussionContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (sidebarRef && postActionBarHeight && !inContext) {
|
||||
if (sidebarRef && postActionBarHeight) {
|
||||
if (isOnDesktop) {
|
||||
sidebarRef.current.style.maxHeight = `${windowHeight - postActionBarHeight}px`;
|
||||
sidebarRef.current.style.maxHeight = `${document.body.offsetHeight - postActionBarHeight}px`;
|
||||
}
|
||||
sidebarRef.current.style.minHeight = `${windowHeight - postActionBarHeight}px`;
|
||||
sidebarRef.current.style.minHeight = `${document.body.offsetHeight - postActionBarHeight}px`;
|
||||
sidebarRef.current.style.top = `${postActionBarHeight}px`;
|
||||
}
|
||||
}, [sidebarRef, postActionBarHeight, inContext]);
|
||||
}, [sidebarRef, postActionBarHeight]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={sidebarRef}
|
||||
className={classNames('flex-column position-sticky', {
|
||||
className={classNames('flex-column min-content-height position-sticky', {
|
||||
'd-none': !displaySidebar,
|
||||
'd-flex overflow-auto': displaySidebar,
|
||||
'w-100': !isOnDesktop,
|
||||
'sidebar-desktop-width': isOnDesktop && !isOnXLDesktop,
|
||||
'w-25 sidebar-XL-width': isOnXLDesktop,
|
||||
'min-content-height': !inContext,
|
||||
})}
|
||||
data-testid="sidebar"
|
||||
>
|
||||
@@ -60,11 +54,12 @@ export default function DiscussionSidebar({ displaySidebar, postActionBarRef })
|
||||
/>
|
||||
<Route path={Routes.TOPICS.PATH} component={TopicsView} />
|
||||
{redirectToLearnersTab && (
|
||||
<Route path={Routes.LEARNERS.POSTS} component={LearnerPostsView} />
|
||||
<Route path={Routes.LEARNERS.POSTS} component={LearnerPostsView} />
|
||||
)}
|
||||
{redirectToLearnersTab && (
|
||||
<Route path={Routes.LEARNERS.PATH} component={LearnersView} />
|
||||
<Route path={Routes.LEARNERS.PATH} component={LearnersView} />
|
||||
)}
|
||||
|
||||
{configStatus === RequestStatus.SUCCESSFUL && (
|
||||
<Redirect
|
||||
from={Routes.DISCUSSIONS.PATH}
|
||||
|
||||
@@ -13,7 +13,7 @@ import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { initializeStore } from '../../store';
|
||||
import { DiscussionContext } from '../common/context';
|
||||
import { fetchConfigSuccess } from '../data/slices';
|
||||
import { getThreadsApiUrl } from '../posts/data/api';
|
||||
import { threadsApiUrl } from '../posts/data/api';
|
||||
import DiscussionSidebar from './DiscussionSidebar';
|
||||
|
||||
import '../posts/data/__factories__';
|
||||
@@ -21,7 +21,6 @@ import '../posts/data/__factories__';
|
||||
let store;
|
||||
let container;
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const threadsApiUrl = getThreadsApiUrl();
|
||||
let axiosMock;
|
||||
|
||||
function renderComponent(displaySidebar = true, location = `/${courseId}/`) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useSelector } from 'react-redux';
|
||||
import {
|
||||
Route, Switch, useLocation, useRouteMatch,
|
||||
@@ -8,7 +7,6 @@ import {
|
||||
|
||||
import Footer from '@edx/frontend-component-footer';
|
||||
import { LearningHeader as Header } from '@edx/frontend-component-header';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import { PostActionsBar } from '../../components';
|
||||
import { CourseTabsNavigation } from '../../components/NavigationBar';
|
||||
@@ -22,7 +20,6 @@ import { EmptyLearners, EmptyPosts, EmptyTopics } from '../empty-posts';
|
||||
import messages from '../messages';
|
||||
import { BreadcrumbMenu, LegacyBreadcrumbMenu, NavigationBar } from '../navigation';
|
||||
import { postMessageToParent } from '../utils';
|
||||
import BlackoutInformationBanner from './BlackoutInformationBanner';
|
||||
import DiscussionContent from './DiscussionContent';
|
||||
import DiscussionSidebar from './DiscussionSidebar';
|
||||
import InformationBanner from './InformationsBanner';
|
||||
@@ -40,7 +37,6 @@ export default function DiscussionsHome() {
|
||||
const { params: { path } } = useRouteMatch(`${Routes.DISCUSSIONS.PATH}/:path*`);
|
||||
const { params } = useRouteMatch(ALL_ROUTES);
|
||||
const isRedirectToLearners = useShowLearnersTab();
|
||||
const isFeedbackBannerVisible = getConfig().DISPLAY_FEEDBACK_BANNER === 'true';
|
||||
|
||||
const {
|
||||
courseId,
|
||||
@@ -50,13 +46,16 @@ export default function DiscussionsHome() {
|
||||
learnerUsername,
|
||||
} = params;
|
||||
const inContext = new URLSearchParams(location.search).get('inContext') !== null;
|
||||
const inIframe = new URLSearchParams(location.search).get('inIframe')?.toLowerCase() === 'true';
|
||||
// Display the content area if we are currently viewing/editing a post or creating one.
|
||||
const displayContentArea = postId || postEditorVisible || (learnerUsername && postId);
|
||||
let displaySidebar = useSidebarVisible();
|
||||
|
||||
const isOnDesktop = useIsOnDesktop();
|
||||
|
||||
const { courseNumber, courseTitle, org } = useSelector((state) => state.courseTabs);
|
||||
const { courseNumber, courseTitle, org } = useSelector(
|
||||
(state) => state.courseTabs,
|
||||
);
|
||||
if (displayContentArea) {
|
||||
// If the window is larger than a particular size, show the sidebar for navigating between posts/topics.
|
||||
// However, for smaller screens or embeds, only show the sidebar if the content area isn't displayed.
|
||||
@@ -65,7 +64,7 @@ export default function DiscussionsHome() {
|
||||
|
||||
const provider = useSelector(selectDiscussionProvider);
|
||||
useCourseDiscussionData(courseId);
|
||||
useRedirectToThread(courseId, inContext);
|
||||
useRedirectToThread(courseId);
|
||||
useEffect(() => {
|
||||
if (path && path !== 'undefined') {
|
||||
postMessageToParent('discussions.navigate', { path });
|
||||
@@ -83,50 +82,45 @@ export default function DiscussionsHome() {
|
||||
learnerUsername,
|
||||
}}
|
||||
>
|
||||
{!inContext && <Header courseOrg={org} courseNumber={courseNumber} courseTitle={courseTitle} />}
|
||||
{!inIframe && <Header courseOrg={org} courseNumber={courseNumber} courseTitle={courseTitle} />}
|
||||
<main className="container-fluid d-flex flex-column p-0 w-100" id="main" tabIndex="-1">
|
||||
{!inContext && <CourseTabsNavigation activeTab="discussion" courseId={courseId} />}
|
||||
<div
|
||||
className={classNames('header-action-bar', { 'shadow-none border-light-300 border-bottom': inContext })}
|
||||
ref={postActionBarRef}
|
||||
>
|
||||
{!inIframe
|
||||
&& <CourseTabsNavigation activeTab="discussion" courseId={courseId} />}
|
||||
<div className="header-action-bar" ref={postActionBarRef}>
|
||||
<div
|
||||
className={classNames('d-flex flex-row justify-content-between navbar fixed-top', {
|
||||
'pl-4 pr-2.5 py-1.5': inContext,
|
||||
})}
|
||||
className="d-flex flex-row justify-content-between navbar fixed-top"
|
||||
>
|
||||
{!inContext && <Route path={Routes.DISCUSSIONS.PATH} component={NavigationBar} />}
|
||||
{!inContext && (
|
||||
<Route path={Routes.DISCUSSIONS.PATH} component={NavigationBar} />
|
||||
)}
|
||||
<PostActionsBar inContext={inContext} />
|
||||
</div>
|
||||
{isFeedbackBannerVisible && <InformationBanner />}
|
||||
<BlackoutInformationBanner />
|
||||
<InformationBanner />
|
||||
</div>
|
||||
{!inContext && (
|
||||
<Route
|
||||
path={[Routes.POSTS.PATH, Routes.TOPICS.CATEGORY]}
|
||||
component={provider === DiscussionProvider.LEGACY ? LegacyBreadcrumbMenu : BreadcrumbMenu}
|
||||
/>
|
||||
)}
|
||||
<Route
|
||||
path={[Routes.POSTS.PATH, Routes.TOPICS.CATEGORY]}
|
||||
component={provider === DiscussionProvider.LEGACY ? LegacyBreadcrumbMenu : BreadcrumbMenu}
|
||||
/>
|
||||
<div className="d-flex flex-row">
|
||||
<DiscussionSidebar displaySidebar={displaySidebar} postActionBarRef={postActionBarRef} />
|
||||
{displayContentArea && <DiscussionContent />}
|
||||
{!displayContentArea && (
|
||||
<Switch>
|
||||
<Route path={Routes.TOPICS.PATH} component={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>
|
||||
<Switch>
|
||||
<Route path={Routes.TOPICS.PATH} component={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>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
{!inContext && <Footer />}
|
||||
{!inIframe && <Footer />}
|
||||
</DiscussionContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -88,10 +88,35 @@ describe('DiscussionsHome', () => {
|
||||
window.parent = parent;
|
||||
});
|
||||
|
||||
test('header, course navigation bar and footer are visible', async () => {
|
||||
renderComponent();
|
||||
expect(screen.queryByRole('banner')).toBeInTheDocument();
|
||||
expect(document.getElementById('courseTabsNavigation')).toBeInTheDocument();
|
||||
expect(screen.queryByRole('contentinfo')).toBeInTheDocument();
|
||||
});
|
||||
describe.each([
|
||||
{
|
||||
queryParam: 'inIframe=True',
|
||||
iframeView: true,
|
||||
},
|
||||
{
|
||||
queryParam: 'inIframe=False',
|
||||
iframeView: false,
|
||||
},
|
||||
{
|
||||
queryParam: '',
|
||||
iframeView: false,
|
||||
},
|
||||
])(
|
||||
'Header/Footer visibility',
|
||||
({
|
||||
queryParam,
|
||||
iframeView,
|
||||
}) => {
|
||||
test(`inIframe query param ${queryParam}`, async () => {
|
||||
renderComponent(`/${courseId}/topics?${queryParam}`);
|
||||
if (iframeView) {
|
||||
expect(screen.queryByRole('banner')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('contentinfo')).not.toBeInTheDocument();
|
||||
} else {
|
||||
expect(screen.queryByRole('banner')).toBeInTheDocument();
|
||||
expect(screen.queryByRole('contentinfo')).toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -27,7 +27,7 @@ function InformationBanner({
|
||||
dismissible
|
||||
onDismiss={() => setShowBanner(false)}
|
||||
>
|
||||
<div className="font-weight-500">
|
||||
<div style={{ fontWeight: '500' }}>
|
||||
{intl.formatMessage(messages.bannerMessage)}
|
||||
{!hideLearnMoreButton
|
||||
&& (
|
||||
|
||||
@@ -12,7 +12,7 @@ import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { initializeStore } from '../../store';
|
||||
import { executeThunk } from '../../test-utils';
|
||||
import messages from '../messages';
|
||||
import { getThreadsApiUrl } from '../posts/data/api';
|
||||
import { threadsApiUrl } from '../posts/data/api';
|
||||
import { fetchThreads } from '../posts/data/thunks';
|
||||
import EmptyPosts from './EmptyPosts';
|
||||
|
||||
@@ -20,7 +20,6 @@ import '../posts/data/__factories__';
|
||||
|
||||
let store;
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const threadsApiUrl = getThreadsApiUrl();
|
||||
|
||||
function renderComponent(location = `/${courseId}/`) {
|
||||
return render(
|
||||
|
||||
@@ -9,7 +9,7 @@ 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 { API_BASE_URL } from '../../data/constants';
|
||||
import { initializeStore } from '../../store';
|
||||
import { executeThunk } from '../../test-utils';
|
||||
import messages from '../messages';
|
||||
@@ -20,7 +20,7 @@ import '../topics/data/__factories__';
|
||||
|
||||
let store;
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const topicsApiUrl = `${getApiBaseUrl()}/api/discussion/v1/course_topics/${courseId}`;
|
||||
const topicsApiUrl = `${API_BASE_URL}/api/discussion/v1/course_topics/${courseId}`;
|
||||
|
||||
function renderComponent(location = `/${courseId}/topics/`) {
|
||||
return render(
|
||||
|
||||
@@ -12,23 +12,17 @@ import {
|
||||
} from '@edx/paragon';
|
||||
import { ArrowBack } from '@edx/paragon/icons';
|
||||
|
||||
import {
|
||||
RequestStatus,
|
||||
Routes,
|
||||
} from '../../data/constants';
|
||||
import { RequestStatus, Routes } from '../../data/constants';
|
||||
import { DiscussionContext } from '../common/context';
|
||||
import { selectUserHasModerationPrivileges, selectUserIsStaff } from '../data/selectors';
|
||||
import {
|
||||
selectAllThreads,
|
||||
selectThreadNextPage,
|
||||
threadsLoadingStatus,
|
||||
} from '../posts/data/selectors';
|
||||
import { clearPostsPages } from '../posts/data/slices';
|
||||
import NoResults from '../posts/NoResults';
|
||||
import { PostLink } from '../posts/post';
|
||||
import { discussionsPath, filterPosts } from '../utils';
|
||||
import { fetchUserPosts } from './data/thunks';
|
||||
import LearnerPostFilterBar from './learner-post-filter-bar/LearnerPostFilterBar';
|
||||
import messages from './messages';
|
||||
|
||||
function LearnerPostsView({ intl }) {
|
||||
@@ -38,28 +32,18 @@ function LearnerPostsView({ intl }) {
|
||||
|
||||
const posts = useSelector(selectAllThreads);
|
||||
const loadingStatus = useSelector(threadsLoadingStatus());
|
||||
const postFilter = useSelector(state => state.learners.postFilter);
|
||||
const { courseId, learnerUsername: username } = useContext(DiscussionContext);
|
||||
const nextPage = useSelector(selectThreadNextPage());
|
||||
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
|
||||
const userIsStaff = useSelector(selectUserIsStaff);
|
||||
|
||||
const loadMorePosts = (pageNum = undefined) => {
|
||||
const params = {
|
||||
author: username,
|
||||
page: pageNum,
|
||||
filters: postFilter,
|
||||
orderBy: postFilter.orderBy,
|
||||
countFlagged: (userHasModerationPrivileges || userIsStaff) || undefined,
|
||||
};
|
||||
|
||||
dispatch(fetchUserPosts(courseId, params));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(clearPostsPages());
|
||||
loadMorePosts();
|
||||
}, [courseId, postFilter, username]);
|
||||
dispatch(fetchUserPosts(courseId, username));
|
||||
}, [courseId, username]);
|
||||
|
||||
const loadMorePosts = () => (
|
||||
dispatch(fetchUserPosts(courseId, username, {
|
||||
page: nextPage,
|
||||
}))
|
||||
);
|
||||
|
||||
const checkIsSelected = (id) => window.location.pathname.includes(id);
|
||||
const pinnedPosts = useMemo(() => filterPosts(posts, 'pinned'), [posts]);
|
||||
@@ -94,8 +78,6 @@ function LearnerPostsView({ intl }) {
|
||||
<div style={{ padding: '18px' }} />
|
||||
</div>
|
||||
<div className="bg-light-400 border border-light-300" />
|
||||
<LearnerPostFilterBar />
|
||||
<div className="border-bottom border-light-400" />
|
||||
<div className="list-group list-group-flush">
|
||||
{postInstances(pinnedPosts)}
|
||||
{postInstances(unpinnedPosts)}
|
||||
@@ -106,7 +88,7 @@ function LearnerPostsView({ intl }) {
|
||||
</div>
|
||||
) : (
|
||||
nextPage && loadingStatus === RequestStatus.SUCCESSFUL && (
|
||||
<Button onClick={() => loadMorePosts(nextPage)} variant="primary" size="md">
|
||||
<Button onClick={() => loadMorePosts()} variant="primary" size="md">
|
||||
{intl.formatMessage(messages.loadMore)}
|
||||
</Button>
|
||||
)
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
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 { Factory } from 'rosie';
|
||||
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import { initializeStore } from '../../store';
|
||||
import { executeThunk } from '../../test-utils';
|
||||
import { DiscussionContext } from '../common/context';
|
||||
import { getCourseConfigApiUrl } from '../data/api';
|
||||
import { fetchCourseConfig } from '../data/thunks';
|
||||
import { getCoursesApiUrl } from './data/api';
|
||||
import LearnerPostsView from './LearnerPostsView';
|
||||
|
||||
import './data/__factories__';
|
||||
|
||||
let store;
|
||||
let axiosMock;
|
||||
const coursesApiUrl = getCoursesApiUrl();
|
||||
const courseConfigApiUrl = getCourseConfigApiUrl();
|
||||
const courseId = 'course-v1:edX+TestX+Test_Course';
|
||||
const username = 'abc123';
|
||||
|
||||
function renderComponent(path = `/${courseId}/learners/${username}/posts`) {
|
||||
return render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<DiscussionContext.Provider
|
||||
value={{
|
||||
learnerUsername: username,
|
||||
courseId,
|
||||
}}
|
||||
>
|
||||
<MemoryRouter initialEntries={[path]}>
|
||||
<Route path={path}>
|
||||
<LearnerPostsView />
|
||||
</Route>
|
||||
</MemoryRouter>
|
||||
</DiscussionContext.Provider>
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe('LearnerPostsView', () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username,
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore();
|
||||
Factory.resetAll();
|
||||
const learnerPosts = Factory.build('learnerPosts', {}, {
|
||||
abuseFlaggedCount: 1,
|
||||
});
|
||||
const apiUrl = `${coursesApiUrl}${courseId}/learner/`;
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock.onGet(apiUrl, { username, count_flagged: true })
|
||||
.reply(() => [200, learnerPosts]);
|
||||
});
|
||||
|
||||
describe('Basic', () => {
|
||||
test('Reported icon is visible to moderator for post with reported comment', async () => {
|
||||
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`).reply(200, {
|
||||
has_moderation_privileges: true,
|
||||
});
|
||||
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/settings`).reply(200, {});
|
||||
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
|
||||
await act(async () => {
|
||||
renderComponent();
|
||||
});
|
||||
expect(screen.queryAllByTestId('reported-post')[0]).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Reported icon is not visible to learner for post with reported comment', async () => {
|
||||
await renderComponent();
|
||||
expect(screen.queryByTestId('reported-post')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -13,9 +13,9 @@ import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import { initializeStore } from '../../store';
|
||||
import { executeThunk } from '../../test-utils';
|
||||
import { getCourseConfigApiUrl } from '../data/api';
|
||||
import { courseConfigApiUrl } from '../data/api';
|
||||
import { fetchCourseConfig } from '../data/thunks';
|
||||
import { getCoursesApiUrl, getUserProfileApiUrl } from './data/api';
|
||||
import { coursesApiUrl, userProfileApiUrl } from './data/api';
|
||||
import { fetchLearners } from './data/thunks';
|
||||
import LearnersView from './LearnersView';
|
||||
|
||||
@@ -23,9 +23,6 @@ import './data/__factories__';
|
||||
|
||||
let store;
|
||||
let axiosMock;
|
||||
const coursesApiUrl = getCoursesApiUrl();
|
||||
const courseConfigApiUrl = getCourseConfigApiUrl();
|
||||
const userProfileApiUrl = getUserProfileApiUrl();
|
||||
const courseId = 'course-v1:edX+TestX+Test_Course';
|
||||
|
||||
function renderComponent() {
|
||||
|
||||
@@ -86,63 +86,3 @@ Factory.define('learnersProfile')
|
||||
}));
|
||||
return profiles;
|
||||
});
|
||||
|
||||
Factory.define('learnerPosts')
|
||||
.option('abuseFlaggedCount', null, null)
|
||||
.option('courseId', null, 'course-v1:edX+TestX+Test_Course')
|
||||
.attr(
|
||||
'results',
|
||||
['abuseFlaggedCount', 'courseId'],
|
||||
(abuseFlaggedCount, courseId) => {
|
||||
const threads = [];
|
||||
for (let i = 0; i < 2; i++) {
|
||||
threads.push({
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
editable_fields: [
|
||||
'abuse_flagged',
|
||||
'following',
|
||||
'group_id',
|
||||
'raw_body',
|
||||
'closed',
|
||||
'read',
|
||||
'title',
|
||||
'topic_id',
|
||||
'type',
|
||||
'voted',
|
||||
'pinned',
|
||||
],
|
||||
id: `post_id${i}`,
|
||||
author: 'test_user',
|
||||
author_label: 'Staff',
|
||||
abuse_flagged: false,
|
||||
can_delete: true,
|
||||
voted: false,
|
||||
vote_count: 1,
|
||||
title: `Title ${i}`,
|
||||
raw_body: `<p>body ${i}</p>`,
|
||||
preview_body: `<p>body ${i}</p>`,
|
||||
course_id: courseId,
|
||||
group_id: null,
|
||||
group_name: null,
|
||||
abuse_flagged_count: abuseFlaggedCount,
|
||||
following: false,
|
||||
comment_count: 8,
|
||||
unread_comment_count: 0,
|
||||
endorsed_comment_list_url: null,
|
||||
non_endorsed_comment_list_url: null,
|
||||
read: false,
|
||||
has_endorsed: false,
|
||||
pinned: false,
|
||||
topic_id: 'topic',
|
||||
});
|
||||
}
|
||||
return threads;
|
||||
},
|
||||
)
|
||||
.attr('pagination', [], () => ({
|
||||
next: null,
|
||||
prev: null,
|
||||
count: 2,
|
||||
num_pages: 1,
|
||||
}));
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import snakeCase from 'lodash/snakeCase';
|
||||
|
||||
import { ensureConfig, getConfig, snakeCaseObject } from '@edx/frontend-platform';
|
||||
import { ensureConfig, getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
ensureConfig([
|
||||
'LMS_BASE_URL',
|
||||
], 'Posts API service');
|
||||
|
||||
export const getCoursesApiUrl = () => `${getConfig().LMS_BASE_URL}/api/discussion/v1/courses/`;
|
||||
export const getUserProfileApiUrl = () => `${getConfig().LMS_BASE_URL}/api/user/v1/accounts`;
|
||||
const apiBaseUrl = getConfig().LMS_BASE_URL;
|
||||
|
||||
export const coursesApiUrl = `${apiBaseUrl}/api/discussion/v1/courses/`;
|
||||
export const userProfileApiUrl = `${apiBaseUrl}/api/user/v1/accounts`;
|
||||
|
||||
/**
|
||||
* Fetches all the learners in the given course.
|
||||
@@ -18,7 +18,7 @@ export const getUserProfileApiUrl = () => `${getConfig().LMS_BASE_URL}/api/user/
|
||||
* @returns {Promise<{}>}
|
||||
*/
|
||||
export async function getLearners(courseId, params) {
|
||||
const url = `${getCoursesApiUrl()}${courseId}/activity_stats/`;
|
||||
const url = `${coursesApiUrl}${courseId}/activity_stats/`;
|
||||
const { data } = await getAuthenticatedHttpClient().get(url, { params });
|
||||
return data;
|
||||
}
|
||||
@@ -28,7 +28,7 @@ export async function getLearners(courseId, params) {
|
||||
* @param {string} usernames
|
||||
*/
|
||||
export async function getUserProfiles(usernames) {
|
||||
const url = `${getUserProfileApiUrl()}?username=${usernames.join()}`;
|
||||
const url = `${userProfileApiUrl}?username=${usernames.join()}`;
|
||||
const { data } = await getAuthenticatedHttpClient().get(url);
|
||||
return data;
|
||||
}
|
||||
@@ -37,50 +37,18 @@ export async function getUserProfiles(usernames) {
|
||||
* Get the posts by a specific user in a course's discussions
|
||||
*
|
||||
* @param {string} courseId Course ID of the course
|
||||
* @param {string} author
|
||||
* @param {string} username Username of the user
|
||||
* @param {number} page
|
||||
* @param {number} pageSize
|
||||
* @param {string} textSearch A search string to match.
|
||||
* @param {ThreadOrdering} orderBy The results wil be sorted on this basis.
|
||||
* @param {boolean} following If true, only threads followed by the current user will be returned.
|
||||
* @param {boolean} flagged If true, only threads that have been reported will be returned.
|
||||
* @param {string} threadType Can be 'discussion' or 'question'.
|
||||
* @param {ThreadViewStatus} view Set to "unread" on "unanswered" to filter to only those statuses.
|
||||
* @param {boolean} countFlagged If true, abuseFlaggedCount will be available.
|
||||
* @param {number} cohort
|
||||
* @returns API Response object in the format
|
||||
* {
|
||||
* results: [array of posts],
|
||||
* pagination: {count, num_pages, next, previous}
|
||||
* }
|
||||
*/
|
||||
export async function getUserPosts(courseId, {
|
||||
page,
|
||||
pageSize,
|
||||
textSearch,
|
||||
orderBy,
|
||||
status,
|
||||
author,
|
||||
threadType,
|
||||
countFlagged,
|
||||
cohort,
|
||||
} = {}) {
|
||||
const learnerPostsApiUrl = `${getCoursesApiUrl()}${courseId}/learner/`;
|
||||
|
||||
const params = snakeCaseObject({
|
||||
page,
|
||||
pageSize,
|
||||
textSearch,
|
||||
threadType,
|
||||
orderBy: orderBy && snakeCase(orderBy),
|
||||
status,
|
||||
requestedFields: 'profile_image',
|
||||
username: author,
|
||||
countFlagged,
|
||||
groupId: cohort,
|
||||
});
|
||||
export async function getUserPosts(courseId, username, { page }) {
|
||||
const learnerPostsApiUrl = `${coursesApiUrl}${courseId}/learner/`;
|
||||
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(learnerPostsApiUrl, { params });
|
||||
.get(learnerPostsApiUrl, { params: { username, page } });
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -3,10 +3,7 @@ import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
import {
|
||||
LearnersOrdering,
|
||||
PostsStatusFilter,
|
||||
RequestStatus,
|
||||
ThreadOrdering,
|
||||
ThreadType,
|
||||
} from '../../../data/constants';
|
||||
|
||||
const learnersSlice = createSlice({
|
||||
@@ -19,12 +16,6 @@ const learnersSlice = createSlice({
|
||||
totalPages: null,
|
||||
totalLearners: null,
|
||||
sortedBy: LearnersOrdering.BY_LAST_ACTIVITY,
|
||||
postFilter: {
|
||||
postType: ThreadType.ALL,
|
||||
status: PostsStatusFilter.ALL,
|
||||
orderBy: ThreadOrdering.BY_LAST_ACTIVITY,
|
||||
cohort: '',
|
||||
},
|
||||
usernameSearch: null,
|
||||
},
|
||||
reducers: {
|
||||
@@ -56,10 +47,6 @@ const learnersSlice = createSlice({
|
||||
state.usernameSearch = payload;
|
||||
state.pages = [];
|
||||
},
|
||||
setPostFilter: (state, { payload }) => {
|
||||
state.pages = [];
|
||||
state.postFilter = payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -70,7 +57,6 @@ export const {
|
||||
fetchLearnersDenied,
|
||||
setSortedBy,
|
||||
setUsernameSearch,
|
||||
setPostFilter,
|
||||
} = learnersSlice.actions;
|
||||
|
||||
export const learnersReducer = learnersSlice.reducer;
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
import { camelCaseObject, snakeCaseObject } from '@edx/frontend-platform';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
|
||||
import {
|
||||
PostsStatusFilter, ThreadType,
|
||||
} from '../../../data/constants';
|
||||
import {
|
||||
fetchLearnerThreadsRequest,
|
||||
fetchThreadsDenied,
|
||||
@@ -70,48 +67,15 @@ export function fetchLearners(courseId, {
|
||||
* @param page
|
||||
* @returns a promise that will update the state with the learner's posts
|
||||
*/
|
||||
export function fetchUserPosts(courseId, {
|
||||
orderBy,
|
||||
filters = {},
|
||||
page = 1,
|
||||
author = null,
|
||||
countFlagged,
|
||||
} = {}) {
|
||||
const options = {
|
||||
orderBy,
|
||||
page,
|
||||
author,
|
||||
countFlagged,
|
||||
};
|
||||
if (filters.status === PostsStatusFilter.UNREAD) {
|
||||
options.status = 'unread';
|
||||
}
|
||||
if (filters.status === PostsStatusFilter.UNANSWERED) {
|
||||
options.status = 'unanswered';
|
||||
}
|
||||
if (filters.status === PostsStatusFilter.REPORTED) {
|
||||
options.status = 'flagged';
|
||||
}
|
||||
if (filters.status === PostsStatusFilter.UNRESPONDED) {
|
||||
options.status = 'unresponded';
|
||||
}
|
||||
if (filters.postType !== ThreadType.ALL) {
|
||||
options.threadType = filters.postType;
|
||||
}
|
||||
if (filters.search) {
|
||||
options.textSearch = filters.search;
|
||||
}
|
||||
if (filters.cohort) {
|
||||
options.cohort = filters.cohort;
|
||||
}
|
||||
export function fetchUserPosts(courseId, username, { page = 1 } = {}) {
|
||||
return async (dispatch) => {
|
||||
try {
|
||||
dispatch(fetchLearnerThreadsRequest({ courseId, author }));
|
||||
dispatch(fetchLearnerThreadsRequest({ courseId, author: username }));
|
||||
|
||||
const data = await getUserPosts(courseId, options);
|
||||
const data = await getUserPosts(courseId, username, { page });
|
||||
const normalisedData = normaliseThreads(camelCaseObject(data));
|
||||
|
||||
dispatch(fetchThreadsSuccess({ ...normalisedData, page, author }));
|
||||
dispatch(fetchThreadsSuccess({ ...normalisedData, page, author: username }));
|
||||
} catch (error) {
|
||||
if (getHttpErrorStatus(error) === 403) {
|
||||
dispatch(fetchThreadsDenied());
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { isEmpty } from 'lodash';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import FilterBar from '../../../components/FilterBar';
|
||||
import { selectCourseCohorts } from '../../cohorts/data/selectors';
|
||||
import { fetchCourseCohorts } from '../../cohorts/data/thunks';
|
||||
import { selectUserHasModerationPrivileges, selectUserIsGroupTa } from '../../data/selectors';
|
||||
import { setPostFilter } from '../data/slices';
|
||||
|
||||
function LearnerPostFilterBar() {
|
||||
const dispatch = useDispatch();
|
||||
const { courseId } = useParams();
|
||||
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
|
||||
const userIsGroupTa = useSelector(selectUserIsGroupTa);
|
||||
const cohorts = useSelector(selectCourseCohorts);
|
||||
const postFilter = useSelector(state => state.learners.postFilter);
|
||||
|
||||
const filtersToShow = [
|
||||
{
|
||||
name: 'postType',
|
||||
filters: ['type-all', 'type-discussions', 'type-questions'],
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
filters: ['status-any', 'status-unread', 'status-unanswered', 'status-unresponded'],
|
||||
},
|
||||
{
|
||||
name: 'orderBy',
|
||||
filters: ['sort-activity', 'sort-comments', 'sort-votes'],
|
||||
},
|
||||
];
|
||||
|
||||
if (userHasModerationPrivileges || userIsGroupTa) {
|
||||
filtersToShow[1].filters.splice(2, 0, 'status-reported');
|
||||
}
|
||||
|
||||
const handleFilterChange = (event) => {
|
||||
const { name, value } = event.currentTarget;
|
||||
if (name === 'postType') {
|
||||
if (postFilter.postType !== value) {
|
||||
dispatch(setPostFilter({
|
||||
...postFilter,
|
||||
postType: value,
|
||||
}));
|
||||
}
|
||||
} else if (name === 'status') {
|
||||
if (postFilter.status !== value) {
|
||||
dispatch(setPostFilter({
|
||||
...postFilter,
|
||||
status: value,
|
||||
}));
|
||||
}
|
||||
} else if (name === 'orderBy') {
|
||||
if (postFilter.orderBy !== value) {
|
||||
dispatch(setPostFilter({
|
||||
...postFilter,
|
||||
orderBy: value,
|
||||
}));
|
||||
}
|
||||
} else if (name === 'cohort') {
|
||||
if (postFilter.cohort !== value) {
|
||||
dispatch(setPostFilter({
|
||||
...postFilter,
|
||||
cohort: value,
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (userHasModerationPrivileges && isEmpty(cohorts)) {
|
||||
dispatch(fetchCourseCohorts(courseId));
|
||||
}
|
||||
}, [courseId, userHasModerationPrivileges]);
|
||||
|
||||
return (
|
||||
<FilterBar
|
||||
filters={filtersToShow}
|
||||
selectedFilters={postFilter}
|
||||
onFilterChange={handleFilterChange}
|
||||
showCohortsFilter={userHasModerationPrivileges || userIsGroupTa}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default LearnerPostFilterBar;
|
||||
@@ -1,108 +0,0 @@
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
render,
|
||||
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';
|
||||
|
||||
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>,
|
||||
);
|
||||
}
|
||||
|
||||
describe('LearnerPostFilterBar', () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username,
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
const initialData = {
|
||||
config: {
|
||||
hasModerationPrivileges: true,
|
||||
isGroupTa: false,
|
||||
},
|
||||
cohorts: {
|
||||
status: 'successful',
|
||||
cohorts: [
|
||||
{
|
||||
name: 'Default Group',
|
||||
id: 1,
|
||||
userCount: 1,
|
||||
groupId: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
store = initializeStore(initialData);
|
||||
});
|
||||
|
||||
test('checks if all filters are visible', async () => {
|
||||
const { queryAllByRole } = await renderComponent();
|
||||
await act(async () => {
|
||||
fireEvent.click(queryAllByRole('button')[0]);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(queryAllByRole('radiogroup')).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
|
||||
test('checks if default values are selected', async () => {
|
||||
const { queryAllByRole } = await renderComponent();
|
||||
await act(async () => {
|
||||
fireEvent.click(queryAllByRole('button')[0]);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
queryAllByRole('radiogroup')[0].querySelector('input[value="all"]'),
|
||||
).toBeChecked();
|
||||
expect(
|
||||
queryAllByRole('radiogroup')[1].querySelector('input[value="statusAll"]'),
|
||||
).toBeChecked();
|
||||
expect(
|
||||
queryAllByRole('radiogroup')[2].querySelector('input[value="lastActivityAt"]'),
|
||||
).toBeChecked();
|
||||
expect(
|
||||
queryAllByRole('radiogroup')[3].querySelector('input[value=""]'),
|
||||
).toBeChecked();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -103,12 +103,6 @@ function LearnerFilterBar({
|
||||
selected={currentSorting}
|
||||
/>
|
||||
)}
|
||||
<ActionItem
|
||||
id="sort-recency"
|
||||
label={intl.formatMessage(messages.recentActivity)}
|
||||
value={LearnersOrdering.BY_RECENCY}
|
||||
selected={currentSorting}
|
||||
/>
|
||||
</Form.RadioSet>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
@@ -4,9 +4,9 @@ import { useSelector } from 'react-redux';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Icon, OverlayTrigger, Tooltip } from '@edx/paragon';
|
||||
import { Edit, Report, ReportGmailerrorred } from '@edx/paragon/icons';
|
||||
import { Edit, Report } from '@edx/paragon/icons';
|
||||
|
||||
import { QuestionAnswerOutline } from '../../../components/icons';
|
||||
import { QuestionAnswerOutline, ReportGmailerrorred } from '../../../components/icons';
|
||||
import { selectUserHasModerationPrivileges, selectUserIsGroupTa } from '../../data/selectors';
|
||||
import messages from '../messages';
|
||||
import { learnerShape } from './proptypes';
|
||||
|
||||
@@ -38,14 +38,9 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Reported activity',
|
||||
description: 'Text for learners sorting by reported activity',
|
||||
},
|
||||
recentActivity: {
|
||||
id: 'discussions.learner.recentActivity',
|
||||
defaultMessage: 'Recent activity',
|
||||
description: 'Text for learners sorting by recent activity',
|
||||
},
|
||||
sortFilterStatus: {
|
||||
id: 'discussions.learner.sortFilterStatus',
|
||||
defaultMessage: `All learners sorted by {sort, select,
|
||||
defaultMessage: `All learners by {sort, select,
|
||||
flagged {reported activity}
|
||||
activity {most activity}
|
||||
other {{sort}}
|
||||
|
||||
@@ -6,6 +6,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Actions menu',
|
||||
description: 'Alt-text for dropdown button for actions related to a post or comment',
|
||||
},
|
||||
backAlt: {
|
||||
id: 'discussions.actions.back.alt',
|
||||
defaultMessage: 'Back',
|
||||
description: 'Text on button for back to posts list',
|
||||
},
|
||||
copyLink: {
|
||||
id: 'discussions.actions.copylink',
|
||||
defaultMessage: 'Copy link',
|
||||
@@ -63,7 +68,7 @@ const messages = defineMessages({
|
||||
},
|
||||
markAnsweredAction: {
|
||||
id: 'discussions.actions.markAnswered',
|
||||
defaultMessage: 'Mark as answered',
|
||||
defaultMessage: 'Mark as Answered',
|
||||
description: 'Action to mark a comment as answering a post',
|
||||
},
|
||||
unmarkAnsweredAction: {
|
||||
@@ -171,7 +176,7 @@ const messages = defineMessages({
|
||||
bannerMessage: {
|
||||
id: 'discussion.banner.welcomeMessage',
|
||||
defaultMessage: '🎉 Welcome to the new and improved discussions experience!',
|
||||
description: 'Information banner welcome text',
|
||||
description: 'Author name displayed when a post is anonymous',
|
||||
},
|
||||
learnMoreBannerLink: {
|
||||
id: 'discussion.banner.learnMore',
|
||||
@@ -183,26 +188,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Share feedback',
|
||||
description: 'Share feedback button to open feedback forms',
|
||||
},
|
||||
blackoutDiscussionInformation: {
|
||||
id: 'discussion.blackoutBanner.information',
|
||||
defaultMessage: 'Blackout dates are currently active. Posting in discussions is unavailable at this time.',
|
||||
description: 'Informative text when discussions blackout is active',
|
||||
},
|
||||
imageWarningMessage: {
|
||||
id: 'discussions.editor.image.warning.message',
|
||||
defaultMessage: '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',
|
||||
description: 'Modal message to tell image dimensions compatibility issue with legacy',
|
||||
},
|
||||
imageWarningModalTitle: {
|
||||
id: 'discussions.editor.image.warning.title',
|
||||
defaultMessage: 'Warning!',
|
||||
description: 'Modal message title',
|
||||
},
|
||||
imageWarningDismissButton: {
|
||||
id: 'discussions.editor.image.warning.dismiss',
|
||||
defaultMessage: 'Ok',
|
||||
description: 'Modal dismiss button text',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -44,7 +44,7 @@ function BreadcrumbMenu() {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="breadcrumb-menu d-flex flex-row bg-light-200 box-shadow-down-1 px-2.5 py-1">
|
||||
<div className="breadcrumb-menu d-flex flex-row mt-2 mx-3">
|
||||
<BreadcrumbDropdown
|
||||
currentItem={currentChapter}
|
||||
showAllPath={discussionsPath(Routes.TOPICS.ALL, { courseId })}
|
||||
|
||||
@@ -13,8 +13,8 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import { getBlocksAPIResponse } from '../../../data/__factories__';
|
||||
import { getBlocksAPIURL } from '../../../data/api';
|
||||
import { getApiBaseUrl, Routes } from '../../../data/constants';
|
||||
import { blocksAPIURL } from '../../../data/api';
|
||||
import { API_BASE_URL, Routes } from '../../../data/constants';
|
||||
import { fetchCourseBlocks } from '../../../data/thunks';
|
||||
import { initializeStore } from '../../../store';
|
||||
import { executeThunk } from '../../../test-utils';
|
||||
@@ -25,7 +25,7 @@ import { BreadcrumbMenu } from '../index';
|
||||
import '../../topics/data/__factories__';
|
||||
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const topicsApiUrl = `${getApiBaseUrl()}/api/discussion/v2/course_topics/${courseId}`;
|
||||
const topicsApiUrl = `${API_BASE_URL}/api/discussion/v2/course_topics/${courseId}`;
|
||||
let store;
|
||||
let axiosMock;
|
||||
|
||||
@@ -78,7 +78,7 @@ describe('BreadcrumbMenu', () => {
|
||||
Factory.resetAll();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
blocksAPIResponse = getBlocksAPIResponse();
|
||||
axiosMock.onGet(getBlocksAPIURL())
|
||||
axiosMock.onGet(blocksAPIURL)
|
||||
.reply(200, blocksAPIResponse);
|
||||
await executeThunk(fetchCourseBlocks(courseId, 'test-user'), store.dispatch, store.getState);
|
||||
const data = [
|
||||
|
||||
@@ -31,7 +31,7 @@ function LegacyBreadcrumbMenu() {
|
||||
const isNonCoursewareTopic = currentTopic && !currentCategory;
|
||||
|
||||
return (
|
||||
<div className="breadcrumb-menu d-flex flex-row bg-light-200 box-shadow-down-1 px-2.5 py-1">
|
||||
<div className="breadcrumb-menu d-flex flex-row mt-2 mx-3">
|
||||
{isNonCoursewareTopic ? (
|
||||
<BreadcrumbDropdown
|
||||
currentItem={currentTopic}
|
||||
|
||||
@@ -12,7 +12,7 @@ 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 { API_BASE_URL, Routes } from '../../../data/constants';
|
||||
import { initializeStore } from '../../../store';
|
||||
import { executeThunk } from '../../../test-utils';
|
||||
import { fetchCourseTopics } from '../../topics/data/thunks';
|
||||
@@ -21,7 +21,7 @@ import { LegacyBreadcrumbMenu } from '../index';
|
||||
import '../../topics/data/__factories__';
|
||||
|
||||
const courseId = 'course-v1:edX+TestX+Test_Course';
|
||||
const topicsApiUrl = `${getApiBaseUrl()}/api/discussion/v1/course_topics/${courseId}`;
|
||||
const topicsApiUrl = `${API_BASE_URL}/api/discussion/v1/course_topics/${courseId}`;
|
||||
let store;
|
||||
let axiosMock;
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import { Button, Spinner } from '@edx/paragon';
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import { DiscussionContext } from '../common/context';
|
||||
import { selectconfigLoadingStatus, selectUserHasModerationPrivileges, selectUserIsStaff } from '../data/selectors';
|
||||
import { fetchUserPosts } from '../learners/data/thunks';
|
||||
import messages from '../messages';
|
||||
import { filterPosts } from '../utils';
|
||||
import {
|
||||
@@ -38,22 +37,16 @@ function PostsList({ posts, topics, intl }) {
|
||||
const userIsStaff = useSelector(selectUserIsStaff);
|
||||
const configStatus = useSelector(selectconfigLoadingStatus);
|
||||
|
||||
const loadThreads = (topicIds, pageNum = undefined) => {
|
||||
const params = {
|
||||
const loadThreads = (topicIds, pageNum = undefined) => (
|
||||
dispatch(fetchThreads(courseId, {
|
||||
topicIds,
|
||||
orderBy,
|
||||
filters,
|
||||
page: pageNum,
|
||||
author: showOwnPosts ? authenticatedUser.username : null,
|
||||
countFlagged: (userHasModerationPrivileges || userIsStaff) || undefined,
|
||||
topicIds,
|
||||
};
|
||||
|
||||
if (showOwnPosts && filters.search === '') {
|
||||
dispatch(fetchUserPosts(courseId, params));
|
||||
} else {
|
||||
dispatch(fetchThreads(courseId, params));
|
||||
}
|
||||
};
|
||||
countFlagged: userHasModerationPrivileges || userIsStaff,
|
||||
}))
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (topics !== undefined && configStatus === RequestStatus.SUCCESSFUL) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import SearchInfo from '../../components/SearchInfo';
|
||||
import { selectCurrentCategoryGrouping, selectTopicsUnderCategory } from '../../data/selectors';
|
||||
import { selectTopicsUnderCategory } from '../../data/selectors';
|
||||
import { DiscussionContext } from '../common/context';
|
||||
import {
|
||||
selectAllThreads,
|
||||
@@ -29,10 +29,7 @@ TopicPostsList.propTypes = {
|
||||
};
|
||||
|
||||
function CategoryPostsList({ category }) {
|
||||
const { inContext } = useContext(DiscussionContext);
|
||||
const groupedCategory = useSelector(selectCurrentCategoryGrouping)(category);
|
||||
// If grouping at subsection is enabled, only apply it when browsing discussions in context in the learning MFE.
|
||||
const topicIds = useSelector(selectTopicsUnderCategory)(inContext ? groupedCategory : category);
|
||||
const topicIds = useSelector(selectTopicsUnderCategory)(category);
|
||||
const posts = useSelector(selectTopicThreads(topicIds));
|
||||
return <PostsList posts={posts} topics={topicIds} />;
|
||||
}
|
||||
@@ -49,7 +46,6 @@ function PostsView() {
|
||||
const dispatch = useDispatch();
|
||||
const searchString = useSelector(({ threads }) => threads.filters.search);
|
||||
const resultsFound = useSelector(({ threads }) => threads.totalThreads);
|
||||
const textSearchRewrite = useSelector(({ threads }) => threads.textSearchRewrite);
|
||||
const loadingStatus = useSelector(({ threads }) => threads.status);
|
||||
|
||||
let postsListComponent;
|
||||
@@ -78,15 +74,9 @@ function PostsView() {
|
||||
|
||||
return (
|
||||
<div className="discussion-posts d-flex flex-column h-100">
|
||||
{searchString && (
|
||||
<SearchInfo
|
||||
count={resultsFound}
|
||||
text={searchString}
|
||||
loadingStatus={loadingStatus}
|
||||
onClear={() => dispatch(setSearchQuery(''))}
|
||||
textSearchRewrite={textSearchRewrite}
|
||||
/>
|
||||
)}
|
||||
{
|
||||
searchString && <SearchInfo count={resultsFound} text={searchString} loadingStatus={loadingStatus} onClear={() => dispatch(setSearchQuery(''))} />
|
||||
}
|
||||
<PostFilterBar />
|
||||
<div className="border-bottom border-light-400" />
|
||||
<div className="list-group list-group-flush flex-fill" role="list" onKeyDown={e => handleKeyDown(e)}>
|
||||
|
||||
@@ -18,22 +18,18 @@ import { initializeStore } from '../../store';
|
||||
import { getCohortsApiUrl } from '../cohorts/data/api';
|
||||
import { DiscussionContext } from '../common/context';
|
||||
import { fetchConfigSuccess } from '../data/slices';
|
||||
import { getCoursesApiUrl } from '../learners/data/api';
|
||||
import { getThreadsApiUrl } from './data/api';
|
||||
import { threadsApiUrl } from './data/api';
|
||||
import { PostsView } from './index';
|
||||
|
||||
import './data/__factories__';
|
||||
import '../cohorts/data/__factories__';
|
||||
|
||||
const courseId = 'course-v1:edX+TestX+Test_Course';
|
||||
const coursesApiUrl = getCoursesApiUrl();
|
||||
const threadsApiUrl = getThreadsApiUrl();
|
||||
let store;
|
||||
let axiosMock;
|
||||
const username = 'abc123';
|
||||
|
||||
async function renderComponent({
|
||||
postId, topicId, category, myPosts, inContext = false,
|
||||
postId, topicId, category, myPosts,
|
||||
} = { myPosts: false }) {
|
||||
let path = generatePath(Routes.POSTS.ALL_POSTS, { courseId });
|
||||
let page;
|
||||
@@ -60,7 +56,6 @@ async function renderComponent({
|
||||
topicId,
|
||||
category,
|
||||
page,
|
||||
inContext,
|
||||
}}
|
||||
>
|
||||
<Switch>
|
||||
@@ -85,11 +80,17 @@ describe('PostsView', () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore({
|
||||
blocks: { blocks: { 'test-usage-key': { topics: ['some-topic-2', 'some-topic-0'] } } },
|
||||
config: { hasModerationPrivileges: true },
|
||||
});
|
||||
store.dispatch(fetchConfigSuccess({}));
|
||||
Factory.resetAll();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock.onGet(getCohortsApiUrl(courseId)).reply(200, Factory.buildList('cohort', 1));
|
||||
@@ -108,20 +109,8 @@ describe('PostsView', () => {
|
||||
});
|
||||
});
|
||||
|
||||
function setupStore(data = {}) {
|
||||
const storeData = {
|
||||
blocks: { blocks: { 'test-usage-key': { topics: ['some-topic-2', 'some-topic-0'] } } },
|
||||
config: { hasModerationPrivileges: true },
|
||||
...data,
|
||||
};
|
||||
// console.log(storeData);
|
||||
store = initializeStore(storeData);
|
||||
store.dispatch(fetchConfigSuccess({}));
|
||||
}
|
||||
|
||||
describe('Basic', () => {
|
||||
test('displays a list of all posts', async () => {
|
||||
setupStore();
|
||||
await act(async () => {
|
||||
await renderComponent();
|
||||
});
|
||||
@@ -129,62 +118,27 @@ describe('PostsView', () => {
|
||||
});
|
||||
|
||||
test('displays a list of user posts', async () => {
|
||||
setupStore();
|
||||
axiosMock.onGet(`${coursesApiUrl}${courseId}/learner/`, { username, count_flagged: true })
|
||||
.reply(() => {
|
||||
const threadAttrs = { previewBody: 'thread preview body', author: username };
|
||||
return [200, Factory.build('threadsResult', {}, {
|
||||
topicId: undefined,
|
||||
count: threadCount,
|
||||
threadAttrs,
|
||||
pageSize: 6,
|
||||
})];
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await renderComponent({ myPosts: true });
|
||||
});
|
||||
|
||||
expect(screen.getAllByText('abc123')).toHaveLength(threadCount);
|
||||
});
|
||||
|
||||
test('displays a list of posts in a topic', async () => {
|
||||
setupStore();
|
||||
await act(async () => {
|
||||
await renderComponent({ topicId: 'some-topic-1' });
|
||||
});
|
||||
expect(screen.getAllByText(/this is thread-\d+ in topic some-topic-1/i)).toHaveLength(Math.ceil(threadCount / 3));
|
||||
});
|
||||
|
||||
test.each([true, false])(
|
||||
'displays a list of posts in a category with grouping at subsection = %s',
|
||||
async (grouping) => {
|
||||
setupStore({
|
||||
blocks: {
|
||||
blocks: {
|
||||
'test-usage-key': {
|
||||
type: 'vertical',
|
||||
topics: ['some-topic-2', 'some-topic-0'],
|
||||
parent: 'test-seq-key',
|
||||
},
|
||||
'test-seq-key': { type: 'sequential', topics: ['some-topic-0', 'some-topic-1', 'some-topic-2'] },
|
||||
},
|
||||
},
|
||||
config: { groupAtSubsection: grouping, hasModerationPrivileges: true, provider: 'openedx' },
|
||||
});
|
||||
await act(async () => {
|
||||
await renderComponent({ category: 'test-usage-key', inContext: true, p: true });
|
||||
});
|
||||
const topicThreadCount = Math.ceil(threadCount / 3);
|
||||
expect(screen.queryAllByText(/this is thread-\d+ in topic some-topic-2/i))
|
||||
.toHaveLength(topicThreadCount);
|
||||
expect(screen.queryAllByText(/this is thread-\d+ in topic some-topic-0/i))
|
||||
.toHaveLength(topicThreadCount);
|
||||
// When grouping is enabled, topic 1 will be shown, but not otherwise.
|
||||
expect(screen.queryAllByText(/this is thread-\d+ in topic some-topic-1/i))
|
||||
.toHaveLength(grouping ? topicThreadCount : 0);
|
||||
},
|
||||
);
|
||||
test('displays a list of posts in a category', async () => {
|
||||
await act(async () => {
|
||||
await renderComponent({ category: 'test-usage-key' });
|
||||
});
|
||||
expect(screen.queryAllByText(/this is thread-\d+ in topic some-topic-1}/i)).toHaveLength(0);
|
||||
expect(screen.queryAllByText(/this is thread-\d+ in topic some-topic-2/i)).toHaveLength(Math.ceil(threadCount / 3));
|
||||
expect(screen.queryAllByText(/this is thread-\d+ in topic some-topic-0/i)).toHaveLength(Math.ceil(threadCount / 3));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Filtering', () => {
|
||||
@@ -196,12 +150,11 @@ describe('PostsView', () => {
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
setupStore();
|
||||
await act(async () => {
|
||||
await renderComponent();
|
||||
});
|
||||
dropDownButton = screen.getByRole('button', {
|
||||
name: /all posts sorted by recent activity/i,
|
||||
name: /all posts by recent activity/i,
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.click(dropDownButton);
|
||||
@@ -213,7 +166,7 @@ describe('PostsView', () => {
|
||||
// 5 status filters: any, unread, following, reported, unanswered
|
||||
// 3 sort: activity, comments, likes
|
||||
// 2 cohort: all groups, 1 api mock response cohort
|
||||
expect(screen.queryAllByRole('radio')).toHaveLength(14);
|
||||
expect(screen.queryAllByRole('radio')).toHaveLength(13);
|
||||
});
|
||||
|
||||
test('test that the cohorts filter works', async () => {
|
||||
@@ -222,7 +175,7 @@ describe('PostsView', () => {
|
||||
});
|
||||
|
||||
dropDownButton = screen.getByRole('button', {
|
||||
name: /All posts in Cohort 1 sorted by recent activity/i,
|
||||
name: /All posts in Cohort 1 by recent activity/i,
|
||||
});
|
||||
|
||||
expect(dropDownButton).toBeInTheDocument();
|
||||
|
||||
@@ -8,8 +8,10 @@ ensureConfig([
|
||||
'LMS_BASE_URL',
|
||||
], 'Posts API service');
|
||||
|
||||
export const getThreadsApiUrl = () => `${getConfig().LMS_BASE_URL}/api/discussion/v1/threads/`;
|
||||
export const getCoursesApiUrl = () => `${getConfig().LMS_BASE_URL}/api/discussion/v1/courses/`;
|
||||
const apiBaseUrl = getConfig().LMS_BASE_URL;
|
||||
|
||||
export const threadsApiUrl = `${apiBaseUrl}/api/discussion/v1/threads/`;
|
||||
export const coursesApiUrl = `${apiBaseUrl}/api/discussion/v1/courses/`;
|
||||
|
||||
/**
|
||||
* Fetches all the threads in the given course and topic.
|
||||
@@ -60,7 +62,7 @@ export async function getThreads(
|
||||
countFlagged,
|
||||
groupId: cohort,
|
||||
});
|
||||
const { data } = await getAuthenticatedHttpClient().get(getThreadsApiUrl(), { params });
|
||||
const { data } = await getAuthenticatedHttpClient().get(threadsApiUrl, { params });
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -69,9 +71,9 @@ export async function getThreads(
|
||||
* @param {string} threadId
|
||||
* @returns {Promise<{}>}
|
||||
*/
|
||||
export async function getThread(threadId, courseId) {
|
||||
const params = { requested_fields: 'profile_image', course_id: courseId };
|
||||
const url = `${getThreadsApiUrl()}${threadId}/`;
|
||||
export async function getThread(threadId) {
|
||||
const params = { requested_fields: 'profile_image' };
|
||||
const url = `${threadsApiUrl}${threadId}/`;
|
||||
const { data } = await getAuthenticatedHttpClient().get(url, { params });
|
||||
return data;
|
||||
}
|
||||
@@ -115,7 +117,7 @@ export async function postThread(
|
||||
});
|
||||
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.post(getThreadsApiUrl(), postData);
|
||||
.post(threadsApiUrl, postData);
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -150,7 +152,7 @@ export async function updateThread(threadId, {
|
||||
editReasonCode,
|
||||
closeReasonCode,
|
||||
} = {}) {
|
||||
const url = `${getThreadsApiUrl()}${threadId}/`;
|
||||
const url = `${threadsApiUrl}${threadId}/`;
|
||||
const patchData = snakeCaseObject({
|
||||
topicId,
|
||||
abuse_flagged: flagged,
|
||||
@@ -175,7 +177,7 @@ export async function updateThread(threadId, {
|
||||
* @param {string} threadId
|
||||
*/
|
||||
export async function deleteThread(threadId) {
|
||||
const url = `${getThreadsApiUrl()}${threadId}/`;
|
||||
const url = `${threadsApiUrl}${threadId}/`;
|
||||
await getAuthenticatedHttpClient()
|
||||
.delete(url);
|
||||
}
|
||||
@@ -189,7 +191,7 @@ export async function deleteThread(threadId) {
|
||||
* @returns {Promise<{ location: string }>}
|
||||
*/
|
||||
export async function uploadFile(blob, filename, courseId, threadKey) {
|
||||
const uploadUrl = `${getCoursesApiUrl()}${courseId}/upload`;
|
||||
const uploadUrl = `${coursesApiUrl}${courseId}/upload`;
|
||||
const formData = new FormData();
|
||||
formData.append('thread_key', threadKey);
|
||||
formData.append('uploaded_file', blob, filename);
|
||||
|
||||
@@ -3,10 +3,9 @@ import MockAdapter from 'axios-mock-adapter';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { initializeMockApp } from '@edx/frontend-platform/testing';
|
||||
|
||||
import { getCoursesApiUrl, uploadFile } from './api';
|
||||
import { coursesApiUrl, uploadFile } from './api';
|
||||
|
||||
const courseId = 'course-v1:edX+TestX+Test_Course';
|
||||
const coursesApiUrl = getCoursesApiUrl();
|
||||
|
||||
let axiosMock = null;
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { initializeMockApp } from '@edx/frontend-platform/testing';
|
||||
|
||||
import { initializeStore } from '../../../store';
|
||||
import { executeThunk } from '../../../test-utils';
|
||||
import { getThreadsApiUrl } from './api';
|
||||
import { threadsApiUrl } from './api';
|
||||
import {
|
||||
createNewThread, fetchThread, fetchThreads, removeThread, updateExistingThread,
|
||||
} from './thunks';
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
import './__factories__';
|
||||
|
||||
const courseId = 'course-v1:edX+TestX+Test_Course';
|
||||
const threadsApiUrl = getThreadsApiUrl();
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
|
||||
@@ -40,7 +40,6 @@ const threadsSlice = createSlice({
|
||||
nextPage: null,
|
||||
totalPages: null,
|
||||
totalThreads: null,
|
||||
textSearchRewrite: null,
|
||||
postStatus: RequestStatus.SUCCESSFUL,
|
||||
filters: {
|
||||
status: PostsStatusFilter.ALL,
|
||||
@@ -61,9 +60,6 @@ const threadsSlice = createSlice({
|
||||
}
|
||||
state.status = RequestStatus.IN_PROGRESS;
|
||||
},
|
||||
clearPostsPages: (state) => {
|
||||
state.pages = [];
|
||||
},
|
||||
fetchThreadsRequest: (state) => {
|
||||
state.status = RequestStatus.IN_PROGRESS;
|
||||
},
|
||||
@@ -84,7 +80,6 @@ const threadsSlice = createSlice({
|
||||
state.nextPage = (payload.page < payload.pagination.numPages) ? payload.page + 1 : null;
|
||||
state.totalPages = payload.pagination.numPages;
|
||||
state.totalThreads = payload.pagination.count;
|
||||
state.textSearchRewrite = payload.textSearchRewrite;
|
||||
},
|
||||
fetchThreadsFailed: (state) => {
|
||||
state.status = RequestStatus.FAILED;
|
||||
@@ -245,7 +240,6 @@ export const {
|
||||
showPostEditor,
|
||||
hidePostEditor,
|
||||
clearRedirect,
|
||||
clearPostsPages,
|
||||
} = threadsSlice.actions;
|
||||
|
||||
export const threadsReducer = threadsSlice.reducer;
|
||||
|
||||
@@ -120,9 +120,6 @@ export function fetchThreads(courseId, {
|
||||
if (filters.status === PostsStatusFilter.UNANSWERED) {
|
||||
options.view = 'unanswered';
|
||||
}
|
||||
if (filters.status === PostsStatusFilter.UNRESPONDED) {
|
||||
options.view = 'unresponded';
|
||||
}
|
||||
if (filters.status === PostsStatusFilter.REPORTED) {
|
||||
options.flagged = true;
|
||||
}
|
||||
@@ -141,7 +138,7 @@ export function fetchThreads(courseId, {
|
||||
const data = await getThreads(courseId, options);
|
||||
const normalisedData = normaliseThreads(camelCaseObject(data), topicIds);
|
||||
dispatch(fetchThreadsSuccess({
|
||||
...normalisedData, page, author, textSearchRewrite: data.text_search_rewrite,
|
||||
...normalisedData, page, author,
|
||||
}));
|
||||
} catch (error) {
|
||||
if (getHttpErrorStatus(error) === 403) {
|
||||
@@ -154,18 +151,18 @@ export function fetchThreads(courseId, {
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchThread(threadId, courseId, isDirectLinkPost = false) {
|
||||
export function fetchThread(threadId, isDirectLinkPost = false) {
|
||||
return async (dispatch) => {
|
||||
try {
|
||||
dispatch(fetchThreadRequest({ threadId }));
|
||||
const data = await getThread(threadId, courseId);
|
||||
const data = await getThread(threadId);
|
||||
if (isDirectLinkPost) {
|
||||
dispatch(fetchThreadByDirectLinkSuccess({ ...normaliseThreads(camelCaseObject(data)), page: 1 }));
|
||||
} else {
|
||||
dispatch(fetchThreadSuccess(normaliseThreads(camelCaseObject(data))));
|
||||
}
|
||||
} catch (error) {
|
||||
if (getHttpErrorStatus(error) === 403 || getHttpErrorStatus(error) === 404) {
|
||||
if (getHttpErrorStatus(error) === 403) {
|
||||
dispatch(fetchThreadDenied());
|
||||
} else {
|
||||
dispatch(fetchThreadFailed());
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
@@ -11,8 +10,7 @@ import {
|
||||
import { Close } from '@edx/paragon/icons';
|
||||
|
||||
import Search from '../../../components/Search';
|
||||
import { RequestStatus } from '../../../data/constants';
|
||||
import { selectBlackoutDate, selectconfigLoadingStatus } from '../../data/selectors';
|
||||
import { selectBlackoutDate } from '../../data/selectors';
|
||||
import { inBlackoutDateRange, postMessageToParent } from '../../utils';
|
||||
import { showPostEditor } from '../data';
|
||||
import messages from './messages';
|
||||
@@ -24,37 +22,38 @@ function PostActionsBar({
|
||||
inContext,
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
const loadingStatus = useSelector(selectconfigLoadingStatus);
|
||||
const blackoutDateRange = useSelector(selectBlackoutDate);
|
||||
|
||||
const handleCloseInContext = () => {
|
||||
postMessageToParent('learning.events.sidebar.close');
|
||||
};
|
||||
|
||||
const blackoutDateRange = useSelector(selectBlackoutDate);
|
||||
return (
|
||||
<div className={classNames('d-flex justify-content-end flex-grow-1', { 'py-1': !inContext })}>
|
||||
{!inContext && <Search />}
|
||||
<div className="d-flex justify-content-end py-1 flex-grow-1">
|
||||
{!inContext && (
|
||||
<>
|
||||
<Search />
|
||||
<div className="border-right border-light-400 mx-3" />
|
||||
</>
|
||||
)}
|
||||
{inContext && (
|
||||
<h4 className="d-flex flex-grow-1 font-weight-bold my-0 py-0 align-self-center">
|
||||
{intl.formatMessage(messages.title)}
|
||||
</h4>
|
||||
)}
|
||||
{(!inBlackoutDateRange(blackoutDateRange) && loadingStatus === RequestStatus.SUCCESSFUL) && (
|
||||
<>
|
||||
{!inContext && <div className="border-right border-light-400 mx-3" />}
|
||||
{
|
||||
!inBlackoutDateRange(blackoutDateRange) && (
|
||||
<Button
|
||||
variant={inContext ? 'plain' : 'brand'}
|
||||
className={classNames('my-0', { 'p-0': inContext })}
|
||||
className="my-0"
|
||||
onClick={() => dispatch(showPostEditor())}
|
||||
size={inContext ? 'md' : 'sm'}
|
||||
size="sm"
|
||||
>
|
||||
{intl.formatMessage(messages.addAPost)}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
)
|
||||
}
|
||||
{inContext && (
|
||||
<>
|
||||
<div className="border-right border-light-300 mr-2 ml-3.5 my-2" />
|
||||
<div className="border-right mr-3 ml-4" />
|
||||
<IconButton
|
||||
src={Close}
|
||||
iconAs={Icon}
|
||||
@@ -63,6 +62,7 @@ function PostActionsBar({
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,11 +26,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Showing {count} results for "{text}"',
|
||||
description: 'Message displayed when user performs a search',
|
||||
},
|
||||
searchRewriteInfo: {
|
||||
id: 'discussions.actionBar.searchRewriteInfo',
|
||||
defaultMessage: 'No results found for "{searchString}". Showing {count} results for "{textSearchRewrite}".',
|
||||
description: 'Message displayed when user performs a search and search query is rewritten because matching results are not found',
|
||||
},
|
||||
searchInfoSearching: {
|
||||
id: 'discussions.actionBar.searchInfoSearching',
|
||||
defaultMessage: 'Searching...',
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useHistory, useLocation, useParams } from 'react-router-dom';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import {
|
||||
Button, Card, Form, Spinner, StatefulButton,
|
||||
@@ -23,8 +23,6 @@ import PostPreviewPane from '../../../components/PostPreviewPane';
|
||||
import { useDispatchWithState } from '../../../data/hooks';
|
||||
import { selectCourseCohorts } from '../../cohorts/data/selectors';
|
||||
import { fetchCourseCohorts } from '../../cohorts/data/thunks';
|
||||
import { DiscussionContext } from '../../common/context';
|
||||
import { useCurrentDiscussionTopic } from '../../data/hooks';
|
||||
import {
|
||||
selectAnonymousPostingConfig,
|
||||
selectDivisionSettings,
|
||||
@@ -33,7 +31,6 @@ import {
|
||||
selectUserIsGroupTa,
|
||||
selectUserIsStaff,
|
||||
} from '../../data/selectors';
|
||||
import { EmptyPage } from '../../empty-posts';
|
||||
import { selectCoursewareTopics, selectNonCoursewareIds, selectNonCoursewareTopics } from '../../topics/data/selectors';
|
||||
import {
|
||||
discussionsPath, formikCompatibleHandler, isFormikFieldInvalid, useCommentsPagePath,
|
||||
@@ -80,9 +77,9 @@ DiscussionPostType.propTypes = {
|
||||
};
|
||||
|
||||
function PostEditor({
|
||||
intl,
|
||||
editExisting,
|
||||
}) {
|
||||
const intl = useIntl();
|
||||
const { authenticatedUser } = useContext(AppContext);
|
||||
const dispatch = useDispatch();
|
||||
const editorRef = useRef(null);
|
||||
@@ -92,9 +89,10 @@ function PostEditor({
|
||||
const commentsPagePath = useCommentsPagePath();
|
||||
const {
|
||||
courseId,
|
||||
topicId,
|
||||
postId,
|
||||
} = useParams();
|
||||
const topicId = useCurrentDiscussionTopic();
|
||||
|
||||
const nonCoursewareTopics = useSelector(selectNonCoursewareTopics);
|
||||
const nonCoursewareIds = useSelector(selectNonCoursewareIds);
|
||||
const coursewareTopics = useSelector(selectCoursewareTopics);
|
||||
@@ -106,11 +104,9 @@ function PostEditor({
|
||||
const { allowAnonymous, allowAnonymousToPeers } = useSelector(selectAnonymousPostingConfig);
|
||||
const { reasonCodesEnabled, editReasons } = useSelector(selectModerationSettings);
|
||||
const userIsStaff = useSelector(selectUserIsStaff);
|
||||
const { category, inContext } = useContext(DiscussionContext);
|
||||
|
||||
const canDisplayEditReason = (reasonCodesEnabled && editExisting
|
||||
&& (userHasModerationPrivileges || userIsGroupTa || userIsStaff)
|
||||
&& post?.author !== authenticatedUser.username
|
||||
&& (userHasModerationPrivileges || userIsGroupTa || userIsStaff) && post?.author !== authenticatedUser.username
|
||||
);
|
||||
|
||||
const editReasonCodeValidation = canDisplayEditReason && {
|
||||
@@ -138,9 +134,7 @@ function PostEditor({
|
||||
follow: isEmpty(post?.following) ? true : post?.following,
|
||||
anonymous: allowAnonymous ? false : undefined,
|
||||
anonymousToPeers: allowAnonymousToPeers ? false : undefined,
|
||||
editReasonCode: post?.lastEdit?.reasonCode || (
|
||||
userIsStaff && canDisplayEditReason ? 'violates-guidelines' : undefined
|
||||
),
|
||||
editReasonCode: post?.lastEdit?.reasonCode || '',
|
||||
cohort: post?.cohort || 'default',
|
||||
};
|
||||
|
||||
@@ -152,7 +146,6 @@ function PostEditor({
|
||||
topicId,
|
||||
postId,
|
||||
learnerUsername: post?.author,
|
||||
category,
|
||||
})(location);
|
||||
history.push(newLocation);
|
||||
}
|
||||
@@ -196,25 +189,16 @@ function PostEditor({
|
||||
dispatch(fetchCourseCohorts(courseId));
|
||||
}
|
||||
if (editExisting) {
|
||||
dispatchSubmit(fetchThread(postId, courseId));
|
||||
dispatch(fetchThread(postId));
|
||||
}
|
||||
}, [courseId, editExisting]);
|
||||
|
||||
if (editExisting && !post) {
|
||||
if (submitting) {
|
||||
return (
|
||||
<div className="m-4 card p-4 align-items-center">
|
||||
<Spinner animation="border" variant="primary" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!submitting) {
|
||||
return (
|
||||
<EmptyPage
|
||||
title={intl.formatMessage(messages.noThreadFound)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="m-4 card p-4 align-items-center">
|
||||
<Spinner animation="border" variant="primary" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
@@ -298,20 +282,15 @@ function PostEditor({
|
||||
onBlur={handleBlur}
|
||||
aria-describedby="topicAreaInput"
|
||||
floatingLabel={intl.formatMessage(messages.topicArea)}
|
||||
disabled={inContext}
|
||||
>
|
||||
{nonCoursewareTopics.map(topic => (
|
||||
<option
|
||||
key={topic.id}
|
||||
value={topic.id}
|
||||
>{topic.name || intl.formatMessage(messages.unnamedSubTopics)}
|
||||
</option>
|
||||
<option key={topic.id} value={topic.id}>{topic.name}</option>
|
||||
))}
|
||||
{coursewareTopics.map(categoryObj => (
|
||||
<optgroup label={categoryObj.name || intl.formatMessage(messages.unnamedTopics)} key={categoryObj.id}>
|
||||
{categoryObj.topics.map(subtopic => (
|
||||
{coursewareTopics.map(category => (
|
||||
<optgroup label={category.name} key={category.id}>
|
||||
{category.topics.map(subtopic => (
|
||||
<option key={subtopic.id} value={subtopic.id}>
|
||||
{subtopic.name || intl.formatMessage(messages.unnamedSubTopics)}
|
||||
{subtopic.name}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
@@ -371,7 +350,7 @@ function PostEditor({
|
||||
name="editReasonCode"
|
||||
className="m-0"
|
||||
as="select"
|
||||
value={values.editReasonCode}
|
||||
value={userIsStaff ? 'violates-guidelines' : values.editReasonCode}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
aria-describedby="editReasonCodeInput"
|
||||
@@ -460,6 +439,7 @@ function PostEditor({
|
||||
}
|
||||
|
||||
PostEditor.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
editExisting: PropTypes.bool,
|
||||
};
|
||||
|
||||
@@ -467,4 +447,4 @@ PostEditor.defaultProps = {
|
||||
editExisting: false,
|
||||
};
|
||||
|
||||
export default PostEditor;
|
||||
export default injectIntl(PostEditor);
|
||||
|
||||
@@ -12,13 +12,12 @@ 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 { API_BASE_URL, Routes } from '../../../data/constants';
|
||||
import { initializeStore } from '../../../store';
|
||||
import { executeThunk } from '../../../test-utils';
|
||||
import { getCohortsApiUrl } from '../../cohorts/data/api';
|
||||
import { DiscussionContext } from '../../common/context';
|
||||
import { fetchCourseTopics } from '../../topics/data/thunks';
|
||||
import { getThreadsApiUrl } from '../data/api';
|
||||
import { threadsApiUrl } from '../data/api';
|
||||
import { fetchThread } from '../data/thunks';
|
||||
import { PostEditor } from '../index';
|
||||
|
||||
@@ -28,8 +27,7 @@ import '../../topics/data/__factories__';
|
||||
import '../data/__factories__';
|
||||
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const topicsApiUrl = `${getApiBaseUrl()}/api/discussion/v1/course_topics/${courseId}`;
|
||||
const threadsApiUrl = getThreadsApiUrl();
|
||||
const topicsApiUrl = `${API_BASE_URL}/api/discussion/v1/course_topics/${courseId}`;
|
||||
let store;
|
||||
let axiosMock;
|
||||
|
||||
@@ -38,15 +36,11 @@ async function renderComponent(editExisting = false, location = `/${courseId}/po
|
||||
await render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<DiscussionContext.Provider
|
||||
value={{ courseId, category: null }}
|
||||
>
|
||||
<MemoryRouter initialEntries={[location]}>
|
||||
<Route path={path}>
|
||||
<PostEditor editExisting={editExisting} />
|
||||
</Route>
|
||||
</MemoryRouter>
|
||||
</DiscussionContext.Provider>
|
||||
<MemoryRouter initialEntries={[location]}>
|
||||
<Route path={path}>
|
||||
<PostEditor editExisting={editExisting} />
|
||||
</Route>
|
||||
</MemoryRouter>
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
@@ -116,26 +116,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Show Preview',
|
||||
description: 'show preview button text to allow user to see their post content.',
|
||||
},
|
||||
actionsAlt: {
|
||||
id: 'discussions.actions.label',
|
||||
defaultMessage: 'Actions menu',
|
||||
description: 'Button to see actions for a post or comment',
|
||||
},
|
||||
unnamedTopics: {
|
||||
id: 'discussions.topic.noName.label',
|
||||
defaultMessage: 'Unnamed category',
|
||||
description: 'display string for topics with missing names',
|
||||
},
|
||||
unnamedSubTopics: {
|
||||
id: 'discussions.subtopic.noName.label',
|
||||
defaultMessage: 'Unnamed subcategory',
|
||||
description: 'display string for topics with missing names',
|
||||
},
|
||||
noThreadFound: {
|
||||
id: 'discussion.thread.notFound',
|
||||
defaultMessage: 'Thread not found',
|
||||
description: 'message to show on screen if the request thread is not found in course',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import React, {
|
||||
useContext, useEffect, useMemo, useState,
|
||||
} from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import classNames from 'classnames';
|
||||
@@ -20,7 +18,6 @@ import {
|
||||
} from '../../../data/constants';
|
||||
import { selectCourseCohorts } from '../../cohorts/data/selectors';
|
||||
import { fetchCourseCohorts } from '../../cohorts/data/thunks';
|
||||
import { DiscussionContext } from '../../common/context';
|
||||
import { selectUserHasModerationPrivileges, selectUserIsGroupTa } from '../../data/selectors';
|
||||
import {
|
||||
setCohortFilter, setPostsTypeFilter, setSortedBy, setStatusFilter,
|
||||
@@ -28,7 +25,7 @@ import {
|
||||
import { selectThreadFilters, selectThreadSorting } from '../data/selectors';
|
||||
import messages from './messages';
|
||||
|
||||
export const ActionItem = ({
|
||||
const ActionItem = ({
|
||||
id,
|
||||
label,
|
||||
value,
|
||||
@@ -63,7 +60,6 @@ function PostFilterBar({
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
const { courseId } = useParams();
|
||||
const { page } = useContext(DiscussionContext);
|
||||
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
|
||||
const userIsGroupTa = useSelector(selectUserIsGroupTa);
|
||||
const currentSorting = useSelector(selectThreadSorting());
|
||||
@@ -99,10 +95,6 @@ function PostFilterBar({
|
||||
// You can't filter discussions by unanswered so switch type to questions
|
||||
dispatch(setPostsTypeFilter(ThreadType.QUESTION));
|
||||
}
|
||||
if (value === PostsStatusFilter.UNRESPONDED && currentType !== ThreadType.DISCUSSION) {
|
||||
// You can't filter questions by not responded so switch type to discussion
|
||||
dispatch(setPostsTypeFilter(ThreadType.DISCUSSION));
|
||||
}
|
||||
}
|
||||
if (name === 'sort') {
|
||||
dispatch(setSortedBy(value));
|
||||
@@ -188,14 +180,12 @@ function PostFilterBar({
|
||||
value={PostsStatusFilter.UNREAD}
|
||||
selected={currentFilters.status}
|
||||
/>
|
||||
{page !== 'my-posts' && (
|
||||
<ActionItem
|
||||
id="status-following"
|
||||
label={intl.formatMessage(messages.filterFollowing)}
|
||||
value={PostsStatusFilter.FOLLOWING}
|
||||
selected={currentFilters.status}
|
||||
/>
|
||||
)}
|
||||
<ActionItem
|
||||
id="status-following"
|
||||
label={intl.formatMessage(messages.filterFollowing)}
|
||||
value={PostsStatusFilter.FOLLOWING}
|
||||
selected={currentFilters.status}
|
||||
/>
|
||||
{(userHasModerationPrivileges || userIsGroupTa) && (
|
||||
<ActionItem
|
||||
id="status-reported"
|
||||
@@ -210,12 +200,6 @@ function PostFilterBar({
|
||||
value={PostsStatusFilter.UNANSWERED}
|
||||
selected={currentFilters.status}
|
||||
/>
|
||||
<ActionItem
|
||||
id="status-unresponded"
|
||||
label={intl.formatMessage(messages.filterUnresponded)}
|
||||
value={PostsStatusFilter.UNRESPONDED}
|
||||
selected={currentFilters.status}
|
||||
/>
|
||||
</Form.RadioSet>
|
||||
<Form.RadioSet
|
||||
name="sort"
|
||||
|
||||
@@ -46,11 +46,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Unanswered',
|
||||
description: 'Option in dropdown to filter to unanswered posts',
|
||||
},
|
||||
filterUnresponded: {
|
||||
id: 'discussions.posts.status.filter.unresponded',
|
||||
defaultMessage: 'Not responded',
|
||||
description: 'Option in dropdown to filter to unresponded posts',
|
||||
},
|
||||
myPosts: {
|
||||
id: 'discussions.posts.filter.myPosts',
|
||||
defaultMessage: 'My posts',
|
||||
@@ -98,7 +93,6 @@ const messages = defineMessages({
|
||||
statusFollowing {followed}
|
||||
statusReported {reported}
|
||||
statusUnanswered {unanswered}
|
||||
statusUnresponded {unresponded}
|
||||
other {{status}}
|
||||
} {type, select,
|
||||
discussion {discussions}
|
||||
@@ -109,10 +103,10 @@ const messages = defineMessages({
|
||||
all {}
|
||||
group {in {cohort}}
|
||||
other {{cohortType}}
|
||||
} sorted by {sort, select,
|
||||
} by {sort, select,
|
||||
lastActivityAt {recent activity}
|
||||
commentCount {most activity}
|
||||
voteCount {most likes}
|
||||
voteCount {most votes}
|
||||
other {{sort}}
|
||||
}`,
|
||||
description: 'Status message showing current sorting and filtering status',
|
||||
|
||||
@@ -50,7 +50,8 @@ function ClosePostReasonModal({
|
||||
isOpen={isOpen}
|
||||
onClose={onCancel}
|
||||
hasCloseButton={false}
|
||||
zIndex={5000}
|
||||
isFullscreenOnMobile
|
||||
isFullscreenScroll
|
||||
>
|
||||
<ModalDialog.Header>
|
||||
<ModalDialog.Title>
|
||||
|
||||
@@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, useToggle } from '@edx/paragon';
|
||||
|
||||
@@ -36,9 +35,6 @@ function Post({
|
||||
const { reasonCodesEnabled } = useSelector(selectModerationSettings);
|
||||
const [isDeleting, showDeleteConfirmation, hideDeleteConfirmation] = useToggle(false);
|
||||
const [isClosing, showClosePostModal, hideClosePostModal] = useToggle(false);
|
||||
|
||||
const postURL = new URL(`${getConfig().PUBLIC_PATH}${courseId}/posts/${post.id}`, window.location.origin);
|
||||
|
||||
const actionHandlers = {
|
||||
[ContentActions.EDIT_CONTENT]: () => history.push({
|
||||
...location,
|
||||
@@ -54,7 +50,7 @@ function Post({
|
||||
dispatch(updateExistingThread(post.id, { closed: true }));
|
||||
}
|
||||
},
|
||||
[ContentActions.COPY_LINK]: () => { navigator.clipboard.writeText(postURL.href); },
|
||||
[ContentActions.COPY_LINK]: () => { navigator.clipboard.writeText(`${window.location.origin}/${courseId}/posts/${post.id}`); },
|
||||
[ContentActions.PIN]: () => dispatch(updateExistingThread(post.id, { pinned: !post.pinned })),
|
||||
[ContentActions.REPORT]: () => dispatch(updateExistingThread(post.id, { flagged: !post.abuseFlagged })),
|
||||
};
|
||||
|
||||
@@ -93,14 +93,14 @@ function PostFooter({
|
||||
</span>
|
||||
</OverlayTrigger>
|
||||
<span
|
||||
className="text-gray-700 mx-1.5 font-weight-500"
|
||||
className="text-light-700 mx-1.5 font-weight-500"
|
||||
style={{ fontSize: '16px' }}
|
||||
>
|
||||
·
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<span title={post.createdAt} className="text-gray-700">
|
||||
<span title={post.createdAt} className="text-gray-500">
|
||||
{timeago.format(post.createdAt, 'time-locale')}
|
||||
</span>
|
||||
{!preview && post.closed
|
||||
|
||||
@@ -120,7 +120,7 @@ function PostHeader({
|
||||
</div>
|
||||
{!preview
|
||||
&& (
|
||||
<div className="ml-auto d-flex">
|
||||
<div className="ml-auto d-flex align-items-center">
|
||||
<ActionsDropdown commentOrPost={post} actionHandlers={actionHandlers} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
/* eslint-disable react/no-unknown-property */
|
||||
import React, { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Badge, Icon, Truncate } from '@edx/paragon';
|
||||
import { CheckCircle } from '@edx/paragon/icons';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Badge, Icon } from '@edx/paragon';
|
||||
|
||||
import { PushPin } from '../../../components/icons';
|
||||
import { AvatarOutlineAndLabelColors, Routes, ThreadType } from '../../../data/constants';
|
||||
import AuthorLabel from '../../common/AuthorLabel';
|
||||
import { DiscussionContext } from '../../common/context';
|
||||
import { selectUserHasModerationPrivileges, selectUserIsGroupTa } from '../../data/selectors';
|
||||
import { discussionsPath, isPostPreviewAvailable } from '../../utils';
|
||||
import messages from './messages';
|
||||
import PostFooter from './PostFooter';
|
||||
@@ -22,10 +22,11 @@ import { postShape } from './proptypes';
|
||||
function PostLink({
|
||||
post,
|
||||
isSelected,
|
||||
intl,
|
||||
learnerTab,
|
||||
showDivider,
|
||||
idx,
|
||||
}) {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
page,
|
||||
postId,
|
||||
@@ -41,9 +42,12 @@ function PostLink({
|
||||
category,
|
||||
learnerUsername,
|
||||
});
|
||||
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
|
||||
const userIsGroupTa = useSelector(selectUserIsGroupTa);
|
||||
const showAnsweredBadge = post.hasEndorsed && post.type === ThreadType.QUESTION;
|
||||
const authorLabelColor = AvatarOutlineAndLabelColors[post.authorLabel];
|
||||
const canSeeReportedBadge = post.abuseFlagged || post.abuseFlaggedCount;
|
||||
const postReported = post.abuseFlagged || post.abuseFlaggedCount;
|
||||
const canSeeReportedBadge = postReported && (userHasModerationPrivileges || userIsGroupTa);
|
||||
const read = post.read || (!post.read && post.commentCount !== post.unreadCommentCount);
|
||||
|
||||
return (
|
||||
@@ -56,10 +60,10 @@ function PostLink({
|
||||
}
|
||||
to={linkUrl}
|
||||
onClick={() => isSelected(post.id)}
|
||||
style={{ lineHeight: '22px' }}
|
||||
style={{ lineHeight: '21px' }}
|
||||
aria-current={isSelected(post.id) ? 'page' : undefined}
|
||||
role="option"
|
||||
tabIndex={(isSelected(post.id) || idx === 0) ? 0 : -1}
|
||||
tabindex={(isSelected(post.id) || idx === 0) ? 0 : -1}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
@@ -75,28 +79,20 @@ function PostLink({
|
||||
<div className="d-flex flex-column flex-fill" style={{ minWidth: 0 }}>
|
||||
<div className="d-flex flex-column justify-content-start mw-100 flex-fill">
|
||||
<div className="d-flex align-items-center pb-0 mb-0 flex-fill font-weight-500">
|
||||
<Truncate lines={1} className="mr-1.5" whiteSpace>
|
||||
<span
|
||||
class={
|
||||
classNames('font-weight-500 font-size-14 text-primary-500 font-style-normal font-family-inter align-bottom',
|
||||
{ 'font-weight-bolder': !read })
|
||||
}
|
||||
>
|
||||
{post.title}
|
||||
</span>
|
||||
<span class="align-bottom"> </span>
|
||||
<span
|
||||
class="text-gray-700 font-weight-normal font-size-14 font-style-normal font-family-inter align-bottom"
|
||||
>
|
||||
{isPostPreviewAvailable(post.previewBody)
|
||||
? post.previewBody
|
||||
: intl.formatMessage(messages.postWithoutPreview)}
|
||||
</span>
|
||||
</Truncate>
|
||||
<div
|
||||
className={
|
||||
classNames('text-truncate font-weight-500 font-size-14 text-primary-500 font-style-normal font-family-inter',
|
||||
{ 'font-weight-bolder': !read })
|
||||
}
|
||||
>
|
||||
{post.title}
|
||||
</div>
|
||||
|
||||
{showAnsweredBadge && (
|
||||
<Icon src={CheckCircle} className="text-success font-weight-500 ml-auto badge-padding" data-testid="check-icon">
|
||||
<span className="sr-only">{' '}answered</span>
|
||||
</Icon>
|
||||
<Badge variant="success" className="font-weight-500 ml-auto badge-padding">
|
||||
{intl.formatMessage(messages.answered)}
|
||||
<span className="sr-only">{' '}answered</span>
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{canSeeReportedBadge && (
|
||||
@@ -122,8 +118,19 @@ function PostLink({
|
||||
author={post.author || intl.formatMessage(messages.anonymous)}
|
||||
authorLabel={post.authorLabel}
|
||||
labelColor={authorLabelColor && `text-${authorLabelColor}`}
|
||||
linkToProfile={!learnerTab && post.author}
|
||||
/>
|
||||
<PostFooter post={post} preview intl={intl} showNewCountLabel={read} />
|
||||
<div
|
||||
className="text-truncate text-primary-500 font-weight-normal font-size-14 font-style-normal font-family-inter"
|
||||
style={{ maxHeight: '1.5rem' }}
|
||||
>
|
||||
{isPostPreviewAvailable(post.previewBody)
|
||||
? post.previewBody
|
||||
: intl.formatMessage(messages.postWithoutPreview)}
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
<PostFooter post={post} preview intl={intl} showNewCountLabel={read} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!showDivider && post.pinned && <div className="pt-1 bg-light-500 border-top border-light-700" />}
|
||||
@@ -135,13 +142,16 @@ function PostLink({
|
||||
PostLink.propTypes = {
|
||||
post: postShape.isRequired,
|
||||
isSelected: PropTypes.func.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
learnerTab: PropTypes.bool,
|
||||
showDivider: PropTypes.bool,
|
||||
idx: PropTypes.number,
|
||||
};
|
||||
|
||||
PostLink.defaultProps = {
|
||||
learnerTab: false,
|
||||
showDivider: true,
|
||||
idx: -1,
|
||||
};
|
||||
|
||||
export default PostLink;
|
||||
export default injectIntl(PostLink);
|
||||
|
||||
@@ -11,21 +11,20 @@ import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { initializeStore } from '../../../store';
|
||||
import { executeThunk } from '../../../test-utils';
|
||||
import { DiscussionContext } from '../../common/context';
|
||||
import { getCourseConfigApiUrl } from '../../data/api';
|
||||
import { courseConfigApiUrl } from '../../data/api';
|
||||
import { fetchCourseConfig } from '../../data/thunks';
|
||||
import PostLink from './PostLink';
|
||||
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const courseConfigApiUrl = getCourseConfigApiUrl();
|
||||
let store;
|
||||
let axiosMock;
|
||||
|
||||
function renderComponent(post) {
|
||||
function renderComponent(post, learnerTab = false) {
|
||||
return render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<DiscussionContext.Provider value={{ courseId }}>
|
||||
<PostLink post={post} key={post.id} isSelected={() => true} />
|
||||
<PostLink post={post} key={post.id} isSelected={() => true} learnerTab={learnerTab} />
|
||||
</DiscussionContext.Provider>
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
@@ -104,10 +103,27 @@ describe('Post username', () => {
|
||||
});
|
||||
|
||||
it.each([
|
||||
'anonymous',
|
||||
'test-user',
|
||||
])('is not clickable for %s user', async (user) => {
|
||||
renderComponent({ ...mockPost, author: user });
|
||||
expect(screen.queryByTestId('learner-posts-link')).not.toBeInTheDocument();
|
||||
true,
|
||||
false,
|
||||
])('is a clickable link %s', async (leanerTab) => {
|
||||
renderComponent(mockPost, leanerTab);
|
||||
|
||||
if (leanerTab) {
|
||||
expect(screen.queryByTestId('learner-posts-link')).not.toBeInTheDocument();
|
||||
} else {
|
||||
expect(screen.queryByTestId('learner-posts-link')).toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
true,
|
||||
false,
|
||||
])('is only clickable if user is not anonymous', async (isAnonymous) => {
|
||||
renderComponent({ ...mockPost, author: isAnonymous ? null : 'test-user' });
|
||||
if (isAnonymous) {
|
||||
expect(screen.queryByTestId('learner-posts-link')).not.toBeInTheDocument();
|
||||
} else {
|
||||
expect(screen.queryByTestId('learner-posts-link')).toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,19 +22,15 @@ function CourseWideTopics() {
|
||||
const { category } = useParams();
|
||||
const filter = useSelector(selectTopicFilter);
|
||||
const nonCoursewareTopics = useSelector(selectNonCoursewareTopics);
|
||||
const filteredNonCoursewareTopics = nonCoursewareTopics.filter(item => (filter
|
||||
? item.name.toLowerCase().includes(filter)
|
||||
: true
|
||||
));
|
||||
|
||||
return (nonCoursewareTopics && category === undefined)
|
||||
&& filteredNonCoursewareTopics.map((topic, index) => (
|
||||
<Topic
|
||||
topic={topic}
|
||||
key={topic.id}
|
||||
index={index}
|
||||
showDivider={(filteredNonCoursewareTopics.length - 1) !== index}
|
||||
/>
|
||||
return (nonCoursewareTopics && category === undefined) && nonCoursewareTopics.filter(
|
||||
item => (filter
|
||||
? item.name.toLowerCase()
|
||||
.includes(filter)
|
||||
: true
|
||||
),
|
||||
)
|
||||
.map(topic => (
|
||||
<Topic topic={topic} key={topic.id} />
|
||||
));
|
||||
}
|
||||
|
||||
@@ -80,20 +76,6 @@ function TopicsView() {
|
||||
const { courseId } = useContext(DiscussionContext);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleKeyDown = (event) => {
|
||||
const { key } = event;
|
||||
if (key !== 'ArrowDown' && key !== 'ArrowUp') { return; }
|
||||
const option = event.target;
|
||||
|
||||
let selectedOption;
|
||||
if (key === 'ArrowDown') { selectedOption = option.nextElementSibling; }
|
||||
if (key === 'ArrowUp') { selectedOption = option.previousElementSibling; }
|
||||
|
||||
if (selectedOption) {
|
||||
selectedOption.focus();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Don't load till the provider information is available
|
||||
if (provider) {
|
||||
@@ -107,19 +89,19 @@ function TopicsView() {
|
||||
}, [topicFilter]);
|
||||
|
||||
return (
|
||||
<div className="discussion-topics d-flex flex-column h-100" data-testid="topics-view">
|
||||
{topicFilter && (
|
||||
<SearchInfo
|
||||
text={topicFilter}
|
||||
count={filteredTopicsCount}
|
||||
loadingStatus={loadingStatus}
|
||||
onClear={() => dispatch(setFilter(''))}
|
||||
/>
|
||||
)}
|
||||
<div className="list-group list-group-flush flex-fill" role="list" onKeyDown={e => handleKeyDown(e)}>
|
||||
<CourseWideTopics />
|
||||
{provider === DiscussionProvider.OPEN_EDX && <CoursewareTopics />}
|
||||
{provider === DiscussionProvider.LEGACY && <LegacyCoursewareTopics />}
|
||||
<div className="d-flex flex-column flex-fill">
|
||||
<div
|
||||
className="discussion-topics card"
|
||||
data-testid="topics-view"
|
||||
>
|
||||
{
|
||||
topicFilter && <SearchInfo text={topicFilter} count={filteredTopicsCount} loadingStatus={loadingStatus} onClear={() => dispatch(setFilter(''))} />
|
||||
}
|
||||
<div className="list-group list-group-flush">
|
||||
<CourseWideTopics />
|
||||
{provider === DiscussionProvider.OPEN_EDX && <CoursewareTopics />}
|
||||
{provider === DiscussionProvider.LEGACY && <LegacyCoursewareTopics />}
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
filteredTopicsCount === 0
|
||||
|
||||
@@ -11,8 +11,8 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import { getBlocksAPIResponse } from '../../data/__factories__';
|
||||
import { getBlocksAPIURL } from '../../data/api';
|
||||
import { DiscussionProvider, getApiBaseUrl } from '../../data/constants';
|
||||
import { blocksAPIURL } from '../../data/api';
|
||||
import { API_BASE_URL, DiscussionProvider } from '../../data/constants';
|
||||
import { selectSequences } from '../../data/selectors';
|
||||
import { fetchCourseBlocks } from '../../data/thunks';
|
||||
import { initializeStore } from '../../store';
|
||||
@@ -26,8 +26,8 @@ import './data/__factories__';
|
||||
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
|
||||
const topicsApiUrl = `${getApiBaseUrl()}/api/discussion/v1/course_topics/${courseId}`;
|
||||
const topicsv2ApiUrl = `${getApiBaseUrl()}/api/discussion/v2/course_topics/${courseId}`;
|
||||
const topicsApiUrl = `${API_BASE_URL}/api/discussion/v1/course_topics/${courseId}`;
|
||||
const topicsv2ApiUrl = `${API_BASE_URL}/api/discussion/v2/course_topics/${courseId}`;
|
||||
let store;
|
||||
let axiosMock;
|
||||
let lastLocation;
|
||||
@@ -113,7 +113,7 @@ describe('TopicsView', () => {
|
||||
axiosMock
|
||||
.onGet(topicsv2ApiUrl)
|
||||
.reply(200, data);
|
||||
axiosMock.onGet(getBlocksAPIURL())
|
||||
axiosMock.onGet(blocksAPIURL)
|
||||
.reply(200, getBlocksAPIResponse(true));
|
||||
axiosMock.onAny().networkError();
|
||||
await executeThunk(fetchCourseBlocks(courseId, 'abc123'), store.dispatch, store.getState);
|
||||
@@ -159,7 +159,7 @@ describe('TopicsView', () => {
|
||||
renderComponent();
|
||||
const archivedTopicGroup = screen.queryAllByTestId('topic-group').pop();
|
||||
expect(archivedTopicGroup).toHaveTextContent(/archived/i);
|
||||
const archivedTopicLinks = within(archivedTopicGroup).queryAllByRole('option');
|
||||
const archivedTopicLinks = within(archivedTopicGroup).queryAllByRole('link');
|
||||
expect(archivedTopicLinks).toHaveLength(2);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
import { getApiBaseUrl } from '../../../data/constants';
|
||||
import { API_BASE_URL } from '../../../data/constants';
|
||||
|
||||
export async function getCourseTopics(courseId, topicIds) {
|
||||
const url = `${getApiBaseUrl()}/api/discussion/v1/course_topics/${courseId}`;
|
||||
const url = `${API_BASE_URL}/api/discussion/v1/course_topics/${courseId}`;
|
||||
const params = {};
|
||||
if (topicIds) {
|
||||
params.topic_id = topicIds.join(',');
|
||||
@@ -15,7 +15,7 @@ export async function getCourseTopics(courseId, topicIds) {
|
||||
}
|
||||
|
||||
export async function getCourseTopicsV2(courseId, topicIds) {
|
||||
const url = `${getApiBaseUrl()}/api/discussion/v2/course_topics/${courseId}`;
|
||||
const url = `${API_BASE_URL}/api/discussion/v2/course_topics/${courseId}`;
|
||||
const params = {};
|
||||
if (topicIds) {
|
||||
params.topic_id = topicIds.join(',');
|
||||
|
||||
@@ -34,7 +34,7 @@ export const selectCoursewareTopics = createSelector(
|
||||
: sequences.map(sequence => ({
|
||||
id: sequence.id,
|
||||
name: sequence.displayName,
|
||||
topics: sequence.topics.map(topicId => ({ id: topicId, name: topics[topicId]?.name || 'unnamed' })),
|
||||
topics: sequence.topics.map(topicId => ({ id: topicId, name: topics[topicId].name })),
|
||||
}))
|
||||
),
|
||||
);
|
||||
@@ -45,7 +45,6 @@ export const selectNonCoursewareTopics = state => state.topics.nonCoursewareIds.
|
||||
|
||||
export const selectTopic = topicId => state => state.topics.topics[topicId];
|
||||
|
||||
export const selectTopicsById = topicIds => state => topicIds.map(topicId => state.topics.topics[topicId])
|
||||
.filter(Boolean);
|
||||
export const selectTopicsById = topicIds => state => topicIds.map(topicId => state.topics.topics[topicId]);
|
||||
|
||||
export const topicsLoadingStatus = state => state.topics.status;
|
||||
|
||||
@@ -1,34 +1,6 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
discussions: {
|
||||
id: 'discussions.topics.discussions',
|
||||
defaultMessage: `{count, plural,
|
||||
=0 {Discussion}
|
||||
one {# Discussion}
|
||||
other {# Discussions}
|
||||
}`,
|
||||
description: 'Display tooltip text used to indicate how many posts type are discussion',
|
||||
},
|
||||
questions: {
|
||||
id: 'discussions.topics.questions',
|
||||
defaultMessage: `{count, plural,
|
||||
=0 {Question}
|
||||
one {# Question}
|
||||
other {# Questions}
|
||||
}`,
|
||||
description: 'Display tooltip text used to indicate how many posts type are questions',
|
||||
},
|
||||
reported: {
|
||||
id: 'discussions.topics.reported',
|
||||
defaultMessage: '{reported} reported',
|
||||
description: 'Display tooltip text used to indicate how many posts are reported',
|
||||
},
|
||||
previouslyReported: {
|
||||
id: 'discussions.topics.previouslyReported',
|
||||
defaultMessage: '{previouslyReported} previously reported',
|
||||
description: 'Display tooltip text used to indicate how many posts are previously reported',
|
||||
},
|
||||
sortedBy: {
|
||||
id: 'discussions.topics.sort.message',
|
||||
defaultMessage: 'Sorted by {sortBy}',
|
||||
@@ -59,16 +31,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Archived',
|
||||
description: 'Heading for displaying topics that are archived.',
|
||||
},
|
||||
unnamedTopicCategories: {
|
||||
id: 'discussions.topics.unnamed.label',
|
||||
defaultMessage: 'Unnamed category',
|
||||
description: 'Text to display in place of topic name if topic name is empty',
|
||||
},
|
||||
unnamedTopicSubCategories: {
|
||||
id: 'discussions.subtopics.unnamed.label',
|
||||
defaultMessage: 'Unnamed subcategory',
|
||||
description: 'Text to display in place of topic name if topic name is empty',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -4,13 +4,10 @@ import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { Routes } from '../../../data/constants';
|
||||
import { DiscussionContext } from '../../common/context';
|
||||
import { discussionsPath } from '../../utils';
|
||||
import { selectTopicFilter } from '../data/selectors';
|
||||
import messages from '../messages';
|
||||
import Topic, { topicShape } from './topic/Topic';
|
||||
|
||||
function TopicGroupBase({
|
||||
@@ -18,56 +15,50 @@ function TopicGroupBase({
|
||||
groupTitle,
|
||||
linkToGroup,
|
||||
topics,
|
||||
intl,
|
||||
}) {
|
||||
const { courseId } = useContext(DiscussionContext);
|
||||
const filter = useSelector(selectTopicFilter);
|
||||
const hasTopics = topics.length > 0;
|
||||
const matchesFilter = filter
|
||||
? groupTitle?.toLowerCase().includes(filter)
|
||||
? groupTitle?.toLowerCase()
|
||||
.includes(filter)
|
||||
: true;
|
||||
|
||||
const filteredTopicElements = topics.filter(
|
||||
topic => (filter
|
||||
? (topic.name.toLowerCase().includes(filter) || matchesFilter)
|
||||
: true
|
||||
const topicElements = topics.filter(
|
||||
topic => (
|
||||
filter
|
||||
? (topic.name.toLowerCase()
|
||||
.includes(filter) || matchesFilter)
|
||||
: true
|
||||
),
|
||||
);
|
||||
|
||||
const hasFilteredSubtopics = (filteredTopicElements.length > 0);
|
||||
)
|
||||
.map(topic => (<Topic topic={topic} key={topic.id} />));
|
||||
const hasFilteredSubtopics = (topicElements.length > 0);
|
||||
if (!hasTopics || (!matchesFilter && !hasFilteredSubtopics)) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="discussion-topic-group d-flex flex-column text-primary-500"
|
||||
className="discussion-topic-group d-flex flex-column"
|
||||
data-category-id={groupId}
|
||||
data-testid="topic-group"
|
||||
>
|
||||
<div className="pt-2.5 px-4 font-weight-bold">
|
||||
{linkToGroup && groupId
|
||||
? (
|
||||
<Link
|
||||
className="text-decoration-none text-primary-500"
|
||||
to={discussionsPath(Routes.TOPICS.CATEGORY, {
|
||||
courseId,
|
||||
category: groupId,
|
||||
})}
|
||||
>
|
||||
{groupTitle}
|
||||
</Link>
|
||||
) : (
|
||||
groupTitle || intl.formatMessage(messages.unnamedTopicCategories)
|
||||
)}
|
||||
</div>
|
||||
{filteredTopicElements.map((topic, index) => (
|
||||
<Topic
|
||||
topic={topic}
|
||||
key={topic.id}
|
||||
index={index}
|
||||
showDivider={(filteredTopicElements.length - 1) !== index}
|
||||
/>
|
||||
))}
|
||||
{linkToGroup
|
||||
? (
|
||||
<Link
|
||||
className="list-group-item p-4 text-primary-500"
|
||||
to={discussionsPath(Routes.TOPICS.CATEGORY, {
|
||||
courseId,
|
||||
category: groupId,
|
||||
})}
|
||||
>
|
||||
{groupTitle}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="list-group-item p-4 text-primary-500">
|
||||
{groupTitle}
|
||||
</span>
|
||||
)}
|
||||
{topicElements}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -77,11 +68,10 @@ TopicGroupBase.propTypes = {
|
||||
groupTitle: PropTypes.string.isRequired,
|
||||
topics: PropTypes.arrayOf(topicShape).isRequired,
|
||||
linkToGroup: PropTypes.bool,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
TopicGroupBase.defaultProps = {
|
||||
linkToGroup: true,
|
||||
};
|
||||
|
||||
export default injectIntl(TopicGroupBase);
|
||||
export default TopicGroupBase;
|
||||
|
||||
@@ -2,120 +2,65 @@
|
||||
import React 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 { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Icon, OverlayTrigger, Tooltip } from '@edx/paragon';
|
||||
import { HelpOutline, PostOutline, Report } from '@edx/paragon/icons';
|
||||
import { Icon } from '@edx/paragon';
|
||||
import { Error as ErrorIcon, Help, PostOutline } from '@edx/paragon/icons';
|
||||
|
||||
import { Routes } from '../../../../data/constants';
|
||||
import { selectUserHasModerationPrivileges, selectUserIsGroupTa } from '../../../data/selectors';
|
||||
import { discussionsPath } from '../../../utils';
|
||||
import messages from '../../messages';
|
||||
|
||||
function Topic({
|
||||
topic,
|
||||
showDivider,
|
||||
index,
|
||||
intl,
|
||||
}) {
|
||||
function Topic({ topic }) {
|
||||
const { courseId } = useParams();
|
||||
const topicUrl = discussionsPath(Routes.TOPICS.TOPIC, {
|
||||
courseId,
|
||||
topicId: topic.id,
|
||||
});
|
||||
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
|
||||
const userIsGroupTa = useSelector(selectUserIsGroupTa);
|
||||
const { inactiveFlags, activeFlags } = topic;
|
||||
const canSeeReportedStats = (activeFlags || inactiveFlags) && (userHasModerationPrivileges || userIsGroupTa);
|
||||
const isSelected = (id) => window.location.pathname.includes(id);
|
||||
|
||||
const icons = [
|
||||
{
|
||||
key: 'discussions',
|
||||
icon: PostOutline,
|
||||
count: topic.threadCounts?.discussion || 0,
|
||||
},
|
||||
{
|
||||
key: 'questions',
|
||||
icon: Help,
|
||||
count: topic.threadCounts?.question || 0,
|
||||
},
|
||||
];
|
||||
return (
|
||||
<Link
|
||||
className={
|
||||
classNames('discussion-topic p-0 text-decoration-none text-primary-500', {
|
||||
'border-bottom border-light-400': showDivider,
|
||||
})
|
||||
}
|
||||
className="discussion-topic d-flex flex-column list-group-item px-4 py-3 text-primary-500"
|
||||
data-topic-id={topic.id}
|
||||
to={topicUrl}
|
||||
onClick={() => isSelected(topic.id)}
|
||||
aria-current={isSelected(topic.id) ? 'page' : undefined}
|
||||
role="option"
|
||||
tabIndex={(isSelected(topic.id) || index === 0) ? 0 : -1}
|
||||
>
|
||||
<div className="d-flex flex-row pt-2.5 pb-2 px-4">
|
||||
<div className="d-flex flex-column flex-fill" style={{ minWidth: 0 }}>
|
||||
<div className="d-flex flex-column justify-content-start mw-100 flex-fill">
|
||||
<div className="topic-name text-truncate">
|
||||
{topic.name || intl.formatMessage(messages.unnamedTopicSubCategories)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-flex align-items-center mt-2.5" style={{ marginBottom: '2px' }}>
|
||||
<OverlayTrigger
|
||||
overlay={(
|
||||
<Tooltip>
|
||||
<div className="d-flex flex-column align-items-start">
|
||||
{intl.formatMessage(messages.discussions, {
|
||||
count: topic.threadCounts?.discussion || 0,
|
||||
})}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<div className="d-flex align-items-center mr-3.5">
|
||||
<Icon src={PostOutline} className="icon-size mr-2" />
|
||||
{topic.threadCounts?.discussion || 0}
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
<OverlayTrigger
|
||||
overlay={(
|
||||
<Tooltip>
|
||||
<div className="d-flex flex-column align-items-start">
|
||||
{intl.formatMessage(messages.questions, {
|
||||
count: topic.threadCounts?.question || 0,
|
||||
})}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<div className="d-flex align-items-center mr-3.5">
|
||||
<Icon src={HelpOutline} className="icon-size mr-2" />
|
||||
{topic.threadCounts?.question || 0}
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
{Boolean(canSeeReportedStats) && (
|
||||
<OverlayTrigger
|
||||
overlay={(
|
||||
<Tooltip>
|
||||
<div className="d-flex flex-column align-items-start">
|
||||
{Boolean(activeFlags) && (
|
||||
<span>
|
||||
{intl.formatMessage(messages.reported, { reported: activeFlags })}
|
||||
</span>
|
||||
)}
|
||||
{Boolean(inactiveFlags) && (
|
||||
<span>
|
||||
{intl.formatMessage(messages.previouslyReported, { previouslyReported: inactiveFlags })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<div className="d-flex align-items-center">
|
||||
<Icon src={Report} className="icon-size mr-2 text-danger" />
|
||||
{activeFlags}{Boolean(inactiveFlags) && `/${inactiveFlags}`}
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="topic-name">
|
||||
{topic.name}
|
||||
</div>
|
||||
<div className="d-flex mt-3">
|
||||
{
|
||||
icons.map(({
|
||||
key,
|
||||
icon,
|
||||
count,
|
||||
}) => (
|
||||
<div className="mr-4 d-flex align-items-center" key={key}>
|
||||
<Icon className="mr-2" src={icon} />
|
||||
{/* Reserve some space for larger counts */}
|
||||
<span>
|
||||
{count}
|
||||
</span>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
{topic?.flags && (
|
||||
<div className="d-flex align-items-center">
|
||||
<Icon className="mr-2" src={ErrorIcon} />
|
||||
{topic.flags}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!showDivider && <div className="divider pt-1 bg-light-500 border-top border-light-700" />}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -128,15 +73,7 @@ export const topicShape = PropTypes.shape({
|
||||
flags: PropTypes.number,
|
||||
});
|
||||
Topic.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
topic: topicShape.isRequired,
|
||||
showDivider: PropTypes.bool,
|
||||
index: PropTypes.number,
|
||||
};
|
||||
|
||||
Topic.defaultProps = {
|
||||
showDivider: true,
|
||||
index: -1,
|
||||
};
|
||||
|
||||
export default injectIntl(Topic);
|
||||
export default Topic;
|
||||
|
||||
@@ -5,9 +5,7 @@ import { generatePath, useRouteMatch } from 'react-router';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import {
|
||||
CheckCircle,
|
||||
CheckCircleOutline,
|
||||
Delete, Edit, Pin, QuestionAnswer, Report, Verified, VerifiedOutline,
|
||||
Delete, Edit, Pin, QuestionAnswer, Report, VerifiedBadge,
|
||||
} from '@edx/paragon/icons';
|
||||
|
||||
import { InsertLink } from '../components/icons';
|
||||
@@ -103,7 +101,7 @@ export const ACTIONS_LIST = [
|
||||
{
|
||||
id: 'endorse',
|
||||
action: ContentActions.ENDORSE,
|
||||
icon: VerifiedOutline,
|
||||
icon: VerifiedBadge,
|
||||
label: messages.endorseAction,
|
||||
conditions: {
|
||||
endorsed: false,
|
||||
@@ -113,7 +111,7 @@ export const ACTIONS_LIST = [
|
||||
{
|
||||
id: 'unendorse',
|
||||
action: ContentActions.ENDORSE,
|
||||
icon: Verified,
|
||||
icon: VerifiedBadge,
|
||||
label: messages.unendorseAction,
|
||||
conditions: {
|
||||
endorsed: true,
|
||||
@@ -123,7 +121,7 @@ export const ACTIONS_LIST = [
|
||||
{
|
||||
id: 'answer',
|
||||
action: ContentActions.ENDORSE,
|
||||
icon: CheckCircleOutline,
|
||||
icon: VerifiedBadge,
|
||||
label: messages.markAnsweredAction,
|
||||
conditions: {
|
||||
endorsed: false,
|
||||
@@ -133,7 +131,7 @@ export const ACTIONS_LIST = [
|
||||
{
|
||||
id: 'unanswer',
|
||||
action: ContentActions.ENDORSE,
|
||||
icon: CheckCircle,
|
||||
icon: VerifiedBadge,
|
||||
label: messages.unmarkAnsweredAction,
|
||||
conditions: {
|
||||
endorsed: true,
|
||||
@@ -185,7 +183,6 @@ export function useActions(content) {
|
||||
.every(condition => condition === true)
|
||||
: true
|
||||
);
|
||||
|
||||
return ACTIONS_LIST.filter(
|
||||
({
|
||||
action,
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"discussions.comments.comment.addResponse": "Add a response",
|
||||
"discussions.comments.comment.addComment": "Add a comment",
|
||||
"discussions.comments.comment.abuseFlaggedMessage": "Content reported for staff to review",
|
||||
"discussions.actions.back.alt": "Back to list",
|
||||
"discussions.comments.comment.responseCount": "{num, plural,\n =0 {No responses}\n one {Showing # response}\n other {Showing # responses}\n }",
|
||||
"discussions.comments.comment.endorsedResponseCount": "{num, plural,\n =0 {No endorsed responses}\n one {Showing # endorsed response}\n other {Showing # endorsed responses}\n }",
|
||||
"discussions.comments.comment.loadMoreComments": "Load more comments",
|
||||
@@ -43,11 +42,9 @@
|
||||
"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,\n flagged {reported activity}\n activity {most activity}\n other {{sort}}\n }",
|
||||
"discussion.learner.allActivity": "All activity",
|
||||
"discussion.learner.posts": "Posts",
|
||||
"discussions.learner.sortFilterStatus": "All learners by {sort, select,\n flagged {reported activity}\n activity {most activity}\n other {{sort}}\n }",
|
||||
"discussions.actions.button.alt": "Actions menu",
|
||||
"discussions.actions.back.alt": "Back",
|
||||
"discussions.actions.copylink": "Copy link",
|
||||
"discussions.actions.unpin": "Unpin",
|
||||
"discussions.actions.close": "Close",
|
||||
@@ -56,7 +53,7 @@
|
||||
"discussions.actions.unreport": "Unreport",
|
||||
"discussions.actions.endorse": "Endorse",
|
||||
"discussions.actions.unendorse": "Unendorse",
|
||||
"discussions.actions.markAnswered": "Mark as answered",
|
||||
"discussions.actions.markAnswered": "Mark as Answered",
|
||||
"discussions.actions.unMarkAnswered": "Unmark as answered",
|
||||
"discussions.delete.confirmation.button.cancel": "Cancel",
|
||||
"discussions.delete.confirmation.button.delete": "Delete",
|
||||
@@ -80,10 +77,6 @@
|
||||
"discussion.banner.welcomeMessage": "🎉 Welcome to the new and improved discussions experience!",
|
||||
"discussion.banner.learnMore": "Learn more",
|
||||
"discussion.banner.shareFeedback": "Share feedback",
|
||||
"discussion.blackoutBanner.information": "Blackout dates are currently active. Posting in discussions is unavailable at this time.",
|
||||
"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",
|
||||
"discussions.navigation.breadcrumbMenu.allTopics": "Topics",
|
||||
"discussions.navigation.breadcrumbMenu.showAll": "Show all",
|
||||
"discussions.navigation.navigationBar.allPosts": "All posts",
|
||||
@@ -92,9 +85,8 @@
|
||||
"discussions.navigation.navigationBar.learners": "Learners",
|
||||
"discussions.app.title": "Discussions",
|
||||
"discussions.posts.actionBar.searchAllPosts": "Search all posts",
|
||||
"discussions.posts.actionBar.search": "{page, select,\n topics {Search topics}\n posts {Search all posts}\n learners {Search learners}\n myPosts {Search all posts}\n other {{page}}\n }",
|
||||
"discussions.posts.actionBar.search": "{page, select,\n topics {Find a topic}\n posts {Search all posts}\n learners {Search learner}\n myPosts {Search all posts}\n other {{page}}\n }",
|
||||
"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",
|
||||
@@ -123,8 +115,6 @@
|
||||
"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",
|
||||
@@ -134,7 +124,6 @@
|
||||
"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",
|
||||
@@ -142,7 +131,7 @@
|
||||
"discussions.posts.sort.lastActivity": "Recent activity",
|
||||
"discussions.posts.sort.commentCount": "Most activity",
|
||||
"discussions.posts.sort.voteCount": "Most likes",
|
||||
"discussions.posts.sort-filter.sortFilterStatus": "{own, select,\n false {All}\n true {Own}\n other {{own}}\n } {status, select,\n statusAll {}\n statusUnread {unread}\n statusFollowing {followed}\n statusReported {reported}\n statusUnanswered {unanswered}\n statusUnresponded {unresponded}\n other {{status}}\n } {type, select,\n discussion {discussions}\n question {questions}\n all {posts}\n other {{type}}\n } {cohortType, select,\n all {}\n group {in {cohort}}\n other {{cohortType}}\n } sorted by {sort, select,\n lastActivityAt {recent activity}\n commentCount {most activity}\n voteCount {most likes}\n other {{sort}}\n }",
|
||||
"discussions.posts.sort-filter.sortFilterStatus": "{own, select,\n false {All}\n true {Own}\n other {{own}}\n } {status, select,\n statusAll {}\n statusUnread {unread}\n statusFollowing {followed}\n statusReported {reported}\n statusUnanswered {unanswered}\n other {{status}}\n } {type, select,\n discussion {discussions}\n question {questions}\n all {posts}\n other {{type}}\n } {cohortType, select,\n all {}\n group {in {cohort}}\n other {{cohortType}}\n } by {sort, select,\n lastActivityAt {recent activity}\n commentCount {most activity}\n voteCount {most votes}\n other {{sort}}\n }",
|
||||
"discussions.post.author.anonymous": "anonymous",
|
||||
"discussions.post.lastResponse": "Last response {time}",
|
||||
"discussions.post.postedOn": "Posted {time} by {author} {authorLabel}",
|
||||
@@ -167,16 +156,10 @@
|
||||
"discussions.post.editedBy": "Edited by",
|
||||
"discussions.post.editReason": "Reason",
|
||||
"discussions.post.postWithoutPreview": "No preview available",
|
||||
"discussions.topics.discussions": "{count, plural,\n =0 {Discussion}\n one {# Discussion}\n other {# Discussions}\n }",
|
||||
"discussions.topics.questions": "{count, plural,\n =0 {Question}\n one {# Question}\n other {# Questions}\n }",
|
||||
"discussions.topics.reported": "{reported} reported",
|
||||
"discussions.topics.previouslyReported": "{previouslyReported} previously reported",
|
||||
"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.find.label": "Search topics",
|
||||
"discussions.topics.archived.label": "Archived",
|
||||
"discussions.topics.unnamed.label": "Unnamed category",
|
||||
"discussions.subtopics.unnamed.label": "Unnamed subcategory"
|
||||
"discussions.topics.find.label": "Find a topic",
|
||||
"discussions.topics.archived.label": "Archived"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user