Compare commits

..

3 Commits

Author SHA1 Message Date
ilee2u
3f6e59c6f3 fix: actually correction for gitignore 2024-09-19 12:45:29 -04:00
ilee2u
7452b9d4f6 fix: fixing gitignore env 2024-09-19 12:44:38 -04:00
ilee2u
0ae18e626c feat: setting to toggle Persona plugin on and off. 2024-09-19 12:42:05 -04:00
39 changed files with 445 additions and 632 deletions

3
.env
View File

@@ -1,5 +1,4 @@
ACCESS_TOKEN_COOKIE_NAME=''
ACCOUNT_PROFILE_URL=''
BASE_URL=''
CREDENTIALS_BASE_URL=''
CSRF_TOKEN_API_PATH=''
@@ -32,4 +31,4 @@ APP_ID=
MFE_CONFIG_API_URL=
PASSWORD_RESET_SUPPORT_LINK=''
LEARNER_FEEDBACK_URL=''
SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT='https://help.edx.org/edxlearner/s/article/How-do-I-link-or-unlink-my-edX-account-to-a-social-media-account'
SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT='https://support.edx.org/hc/en-us/articles/207206067'

View File

@@ -1,5 +1,4 @@
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
ACCOUNT_PROFILE_URL='http://localhost:1995'
BASE_URL='localhost:1997'
CREDENTIALS_BASE_URL='http://localhost:18150'
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
@@ -28,9 +27,9 @@ ENABLE_COPPA_COMPLIANCE=''
ENABLE_ACCOUNT_DELETION=''
ENABLE_DOB_UPDATE=''
MARKETING_EMAILS_OPT_IN=''
SHOW_EMAIL_CHANNEL='true'
SHOW_EMAIL_CHANNEL=''
APP_ID=
MFE_CONFIG_API_URL=
PASSWORD_RESET_SUPPORT_LINK='mailto:support@example.com'
LEARNER_FEEDBACK_URL=''
SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT='https://help.edx.org/edxlearner/s/article/How-do-I-link-or-unlink-my-edX-account-to-a-social-media-account'
SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT='https://support.edx.org/hc/en-us/articles/207206067'

View File

@@ -30,4 +30,4 @@ MARKETING_EMAILS_OPT_IN=''
APP_ID=
MFE_CONFIG_API_URL=
LEARNER_FEEDBACK_URL=''
SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT='https://help.edx.org/edxlearner/s/article/How-do-I-link-or-unlink-my-edX-account-to-a-social-media-account'
SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT='https://support.edx.org/hc/en-us/articles/207206067'

View File

@@ -13,11 +13,12 @@ jobs:
- i18n_extract
- lint
- test
node: [18, 20]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
node-version: ${{ matrix.node }}
- run: make requirements
- run: make test NPM_TESTS=build
- run: make test NPM_TESTS=${{ matrix.npm-test }}

4
.gitignore vendored
View File

@@ -17,4 +17,6 @@ temp/babel-plugin-react-intl
/temp
/.vscode
/module.config.js
src/i18n/messages/
src/i18n/messages/
env.config.js

274
package-lock.json generated
View File

@@ -10,8 +10,8 @@
"license": "AGPL-3.0",
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-component-header": "^5.6.0",
"@edx/frontend-platform": "8.1.2",
"@edx/frontend-component-header": "5.3.4",
"@edx/frontend-platform": "8.1.1",
"@edx/openedx-atlas": "^0.6.0",
"@fortawesome/fontawesome-svg-core": "^6.6.0",
"@fortawesome/free-brands-svg-icons": "^6.6.0",
@@ -20,10 +20,10 @@
"@fortawesome/react-fontawesome": "0.2.2",
"@openedx/frontend-plugin-framework": "^1.2.2",
"@openedx/frontend-slot-footer": "^1.0.2",
"@openedx/paragon": "22.9.0",
"@tensorflow-models/blazeface": "0.1.0",
"@tensorflow/tfjs-converter": "4.22.0",
"@tensorflow/tfjs-core": "4.22.0",
"@openedx/paragon": "22.0.0",
"@tensorflow-models/blazeface": "0.0.7",
"@tensorflow/tfjs-converter": "3.21.0",
"@tensorflow/tfjs-core": "3.21.0",
"bowser": "2.11.0",
"classnames": "2.5.1",
"core-js": "3.38.1",
@@ -61,17 +61,17 @@
"redux-thunk": "2.4.2",
"regenerator-runtime": "0.14.1",
"reselect": "^5.1.1",
"universal-cookie": "7.2.2"
"universal-cookie": "4.0.4"
},
"devDependencies": {
"@edx/browserslist-config": "1.2.0",
"@edx/reactifex": "2.2.0",
"@openedx/frontend-build": "14.1.5",
"@testing-library/jest-dom": "6.6.3",
"@edx/reactifex": "1.1.0",
"@openedx/frontend-build": "14.1.2",
"@testing-library/jest-dom": "6.4.8",
"@testing-library/react": "12.1.5",
"react-test-renderer": "17.0.2",
"reactifex": "1.1.1",
"redux-mock-store": "1.5.5"
"redux-mock-store": "1.5.4"
}
},
"node_modules/@adobe/css-tools": {
@@ -2010,9 +2010,7 @@
}
},
"node_modules/@edx/frontend-component-header": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/@edx/frontend-component-header/-/frontend-component-header-5.7.1.tgz",
"integrity": "sha512-3a7JcoqW1jUGbOoEoi8+COlxKLAd+DigAPiUNwcuefmGKYN05pF/vRc6yt0W03rHLYkq7A23CFyoqhZC44GTZg==",
"version": "5.3.4",
"license": "AGPL-3.0",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "6.6.0",
@@ -2020,10 +2018,8 @@
"@fortawesome/free-regular-svg-icons": "6.6.0",
"@fortawesome/free-solid-svg-icons": "6.6.0",
"@fortawesome/react-fontawesome": "^0.2.0",
"@openedx/frontend-plugin-framework": "^1.3.0",
"axios-mock-adapter": "1.22.0",
"babel-polyfill": "6.26.0",
"classnames": "^2.5.1",
"jest-environment-jsdom": "^29.7.0",
"react-responsive": "8.2.0",
"react-transition-group": "4.4.5"
@@ -2033,14 +2029,11 @@
"@openedx/paragon": ">= 21.5.7 < 23.0.0",
"prop-types": "^15.5.10",
"react": "^16.9.0 || ^17.0.0",
"react-dom": "^16.9.0 || ^17.0.0",
"react-router-dom": "^6.14.2"
"react-dom": "^16.9.0 || ^17.0.0"
}
},
"node_modules/@edx/frontend-platform": {
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-8.1.2.tgz",
"integrity": "sha512-xkSpzoxSp3bC4rrOHkHb/tj5bZQjZrOe5t3/kNzJVqqvziFGRU0fTAhh+Pv0aHboDAS8mAEhqZrJFpdeAFe1Tg==",
"version": "8.1.1",
"license": "AGPL-3.0",
"dependencies": {
"@cospired/i18n-iso-languages": "4.2.0",
@@ -2060,7 +2053,7 @@
"lodash.merge": "4.6.2",
"lodash.snakecase": "4.1.1",
"pubsub-js": "1.9.4",
"react-intl": "6.7.0",
"react-intl": "6.6.8",
"universal-cookie": "4.0.4"
},
"bin": {
@@ -2080,8 +2073,6 @@
},
"node_modules/@edx/frontend-platform/node_modules/axios": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz",
"integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.4",
@@ -2089,31 +2080,10 @@
"proxy-from-env": "^1.1.0"
}
},
"node_modules/@edx/frontend-platform/node_modules/cookie": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
"integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/@edx/frontend-platform/node_modules/form-urlencoded": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/form-urlencoded/-/form-urlencoded-4.1.4.tgz",
"integrity": "sha512-R7Vytos0gMYuPQTMwnNzvK9PBItNV+Qkm/pvghEZI3j2kMrzZmJlczAgHFmt12VV+IRYQXgTlSGP1PKAsMCIUA==",
"license": "MIT"
},
"node_modules/@edx/frontend-platform/node_modules/universal-cookie": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-4.0.4.tgz",
"integrity": "sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw==",
"license": "MIT",
"dependencies": {
"@types/cookie": "^0.3.3",
"cookie": "^0.4.0"
}
},
"node_modules/@edx/new-relic-source-map-webpack-plugin": {
"version": "2.1.0",
"license": "AGPL-3.0",
@@ -2131,9 +2101,7 @@
}
},
"node_modules/@edx/reactifex": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@edx/reactifex/-/reactifex-2.2.0.tgz",
"integrity": "sha512-vyGDtx3BwCr6Gjbm4y6gJ8Bzc2TOSNBlBa2hMerz59HoXaot14MihxxiDU+JDNybGLLcKDBiK511bOi/77i1lw==",
"version": "1.1.0",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2146,8 +2114,6 @@
},
"node_modules/@edx/reactifex/node_modules/axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2313,8 +2279,6 @@
},
"node_modules/@formatjs/fast-memoize": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.0.tgz",
"integrity": "sha512-hnk/nY8FyrL5YxwP9e4r9dqeM6cAbo8PeU9UjyXojZMNvVad2Z06FAVHyR3Ecw6fza+0GH7vdJgiKIVXTMbSBA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.4.0"
@@ -2368,9 +2332,7 @@
}
},
"node_modules/@formatjs/intl": {
"version": "2.10.5",
"resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-2.10.5.tgz",
"integrity": "sha512-f9qPNNgLrh2KvoFvHGIfcPTmNGbyy7lyyV4/P6JioDqtTE7Akdmgt+ZzVndr+yMLZnssUShyTMXxM/6aV9eVuQ==",
"version": "2.10.4",
"license": "MIT",
"dependencies": {
"@formatjs/ecma402-abstract": "2.0.0",
@@ -2392,8 +2354,6 @@
},
"node_modules/@formatjs/intl-displaynames": {
"version": "6.6.8",
"resolved": "https://registry.npmjs.org/@formatjs/intl-displaynames/-/intl-displaynames-6.6.8.tgz",
"integrity": "sha512-Lgx6n5KxN16B3Pb05z3NLEBQkGoXnGjkTBNCZI+Cn17YjHJ3fhCeEJJUqRlIZmJdmaXQhjcQVDp6WIiNeRYT5g==",
"license": "MIT",
"dependencies": {
"@formatjs/ecma402-abstract": "2.0.0",
@@ -2403,8 +2363,6 @@
},
"node_modules/@formatjs/intl-displaynames/node_modules/@formatjs/ecma402-abstract": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.0.0.tgz",
"integrity": "sha512-rRqXOqdFmk7RYvj4khklyqzcfQl9vEL/usogncBHRZfZBDOwMGuSRNFl02fu5KGHXdbinju+YXyuR+Nk8xlr/g==",
"license": "MIT",
"dependencies": {
"@formatjs/intl-localematcher": "0.5.4",
@@ -2413,8 +2371,6 @@
},
"node_modules/@formatjs/intl-displaynames/node_modules/@formatjs/intl-localematcher": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz",
"integrity": "sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==",
"license": "MIT",
"dependencies": {
"tslib": "^2.4.0"
@@ -2422,8 +2378,6 @@
},
"node_modules/@formatjs/intl-listformat": {
"version": "7.5.7",
"resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-7.5.7.tgz",
"integrity": "sha512-MG2TSChQJQT9f7Rlv+eXwUFiG24mKSzmF144PLb8m8OixyXqn4+YWU+5wZracZGCgVTVmx8viCf7IH3QXoiB2g==",
"license": "MIT",
"dependencies": {
"@formatjs/ecma402-abstract": "2.0.0",
@@ -2433,8 +2387,6 @@
},
"node_modules/@formatjs/intl-listformat/node_modules/@formatjs/ecma402-abstract": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.0.0.tgz",
"integrity": "sha512-rRqXOqdFmk7RYvj4khklyqzcfQl9vEL/usogncBHRZfZBDOwMGuSRNFl02fu5KGHXdbinju+YXyuR+Nk8xlr/g==",
"license": "MIT",
"dependencies": {
"@formatjs/intl-localematcher": "0.5.4",
@@ -2443,8 +2395,6 @@
},
"node_modules/@formatjs/intl-listformat/node_modules/@formatjs/intl-localematcher": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz",
"integrity": "sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==",
"license": "MIT",
"dependencies": {
"tslib": "^2.4.0"
@@ -2477,8 +2427,6 @@
},
"node_modules/@formatjs/intl/node_modules/@formatjs/ecma402-abstract": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.0.0.tgz",
"integrity": "sha512-rRqXOqdFmk7RYvj4khklyqzcfQl9vEL/usogncBHRZfZBDOwMGuSRNFl02fu5KGHXdbinju+YXyuR+Nk8xlr/g==",
"license": "MIT",
"dependencies": {
"@formatjs/intl-localematcher": "0.5.4",
@@ -2487,8 +2435,6 @@
},
"node_modules/@formatjs/intl/node_modules/@formatjs/intl-localematcher": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz",
"integrity": "sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==",
"license": "MIT",
"dependencies": {
"tslib": "^2.4.0"
@@ -3140,9 +3086,9 @@
}
},
"node_modules/@openedx/frontend-build": {
"version": "14.1.5",
"resolved": "https://registry.npmjs.org/@openedx/frontend-build/-/frontend-build-14.1.5.tgz",
"integrity": "sha512-QEdl55jNitdQL7RDAuX/EgfxsyBeEZfW3fc9Df4Py5KY6NKjRE7wNLeBMxYCFagEgXwaR1Btiw5NxzByAdlnfg==",
"version": "14.1.2",
"resolved": "https://registry.npmjs.org/@openedx/frontend-build/-/frontend-build-14.1.2.tgz",
"integrity": "sha512-pLTcY/BLjCFn2qsBmUY0on2qB4PNShlg8scTYjrsz5fCASuSAJk000jOF5ys6bg6i8krSOIJlcZXPgrq0ywsUg==",
"license": "AGPL-3.0",
"dependencies": {
"@babel/cli": "7.24.8",
@@ -3163,7 +3109,7 @@
"@types/jest": "29.5.12",
"@typescript-eslint/eslint-plugin": "^5.58.0",
"@typescript-eslint/parser": "^5.58.0",
"autoprefixer": "10.4.20",
"autoprefixer": "10.4.19",
"babel-jest": "29.6.1",
"babel-loader": "9.1.3",
"babel-plugin-formatjs": "^10.4.0",
@@ -3192,7 +3138,7 @@
"jest-environment-jsdom": "29.6.1",
"mini-css-extract-plugin": "1.6.2",
"parse5": "7.1.2",
"postcss": "8.4.47",
"postcss": "8.4.39",
"postcss-custom-media": "10.0.8",
"postcss-loader": "7.3.4",
"postcss-rtlcss": "5.1.2",
@@ -3334,9 +3280,7 @@
}
},
"node_modules/@openedx/frontend-plugin-framework": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@openedx/frontend-plugin-framework/-/frontend-plugin-framework-1.4.0.tgz",
"integrity": "sha512-cwwjvGjYnzQdyhw2PuMYfaZoZGclXTzYfR3NjZ8bSLkj0f7lRB64nMy6WUmBjTB6hjaD4vxMs0zahrU4IM7EWQ==",
"version": "1.2.2",
"license": "AGPL-3.0",
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
@@ -3360,16 +3304,15 @@
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.37.1.tgz",
"integrity": "sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw==",
"hasInstallScript": true,
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/@openedx/frontend-slot-footer": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@openedx/frontend-slot-footer/-/frontend-slot-footer-1.0.6.tgz",
"integrity": "sha512-6wdbxYeSzpUOaZrnglkAI0PJ/CB8pMP3xbYXQNQ+VR4U8pkH8EFwEQjwak4OVmodS8//eW9xgYv79YB4H7mGEQ==",
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@openedx/frontend-slot-footer/-/frontend-slot-footer-1.0.5.tgz",
"integrity": "sha512-hlKj2p7zgAHfPTbmW4rAMUCCj35uCbm9VfRkZYPEIX2bdhkPjc4OIGTE5CKGkyWG5+zL+aap1ywwcKEjEgOMOQ==",
"license": "AGPL-3.0",
"dependencies": {
"@openedx/frontend-plugin-framework": "^1.1.2"
@@ -3380,9 +3323,7 @@
}
},
"node_modules/@openedx/paragon": {
"version": "22.9.0",
"resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-22.9.0.tgz",
"integrity": "sha512-r5xD+z64U3phkgT4ooUQaxE/4Rv0D91tpS3kA+mLfOT1vMD8jXIjDZp+/k4BEw4yqWQ8Eyb//ar8xiwL/ugojQ==",
"version": "22.0.0",
"license": "Apache-2.0",
"workspaces": [
"example",
@@ -3433,8 +3374,6 @@
},
"node_modules/@openedx/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==",
"license": "MIT",
"dependencies": {
"prop-types": "^15.8.1"
@@ -3446,8 +3385,6 @@
},
"node_modules/@openedx/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==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
@@ -3455,9 +3392,6 @@
},
"node_modules/@openedx/paragon/node_modules/glob": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
"integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
@@ -3475,8 +3409,6 @@
},
"node_modules/@openedx/paragon/node_modules/minimatch": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
@@ -3588,9 +3520,7 @@
"license": "MIT"
},
"node_modules/@remix-run/router": {
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.20.0.tgz",
"integrity": "sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==",
"version": "1.19.0",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
@@ -3861,31 +3791,29 @@
}
},
"node_modules/@tensorflow-models/blazeface": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@tensorflow-models/blazeface/-/blazeface-0.1.0.tgz",
"integrity": "sha512-Qc5Wii8/OE5beC7XfehkhF9SEFLaPbVKnxxalV0T9JXsUynXqvLommc9Eko7b8zXKy4SJ1BtVlcX2cmCzQrn9A==",
"version": "0.0.7",
"license": "Apache-2.0",
"peerDependencies": {
"@tensorflow/tfjs-converter": "^4.10.0",
"@tensorflow/tfjs-core": "^4.10.0"
"@tensorflow/tfjs-converter": "^3.1.0",
"@tensorflow/tfjs-core": "^3.1.0"
}
},
"node_modules/@tensorflow/tfjs-converter": {
"version": "4.22.0",
"resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-4.22.0.tgz",
"integrity": "sha512-PT43MGlnzIo+YfbsjM79Lxk9lOq6uUwZuCc8rrp0hfpLjF6Jv8jS84u2jFb+WpUeuF4K33ZDNx8CjiYrGQ2trQ==",
"version": "3.21.0",
"license": "Apache-2.0",
"peerDependencies": {
"@tensorflow/tfjs-core": "4.22.0"
"@tensorflow/tfjs-core": "3.21.0"
}
},
"node_modules/@tensorflow/tfjs-core": {
"version": "4.22.0",
"resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-4.22.0.tgz",
"integrity": "sha512-LEkOyzbknKFoWUwfkr59vSB68DMJ4cjwwHgicXN0DUi3a0Vh1Er3JQqCI1Hl86GGZQvY8ezVrtDIvqR1ZFW55A==",
"version": "3.21.0",
"license": "Apache-2.0",
"dependencies": {
"@types/long": "^4.0.1",
"@types/offscreencanvas": "~2019.7.0",
"@types/offscreencanvas": "~2019.3.0",
"@types/seedrandom": "^2.4.28",
"@webgpu/types": "0.1.38",
"@types/webgl-ext": "0.0.30",
"@webgpu/types": "0.1.16",
"long": "4.0.0",
"node-fetch": "~2.6.1",
"seedrandom": "^3.0.5"
@@ -3896,8 +3824,7 @@
},
"node_modules/@tensorflow/tfjs-core/node_modules/long": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
"license": "Apache-2.0"
},
"node_modules/@testing-library/dom": {
"version": "8.20.1",
@@ -3931,13 +3858,12 @@
"license": "MIT"
},
"node_modules/@testing-library/jest-dom": {
"version": "6.6.3",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz",
"integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==",
"version": "6.4.8",
"dev": true,
"license": "MIT",
"dependencies": {
"@adobe/css-tools": "^4.4.0",
"@babel/runtime": "^7.9.2",
"aria-query": "^5.0.0",
"chalk": "^3.0.0",
"css.escape": "^1.5.1",
@@ -3953,8 +3879,6 @@
},
"node_modules/@testing-library/jest-dom/node_modules/chalk": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
"integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4232,8 +4156,7 @@
},
"node_modules/@types/long": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
"integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA=="
"license": "MIT"
},
"node_modules/@types/mime": {
"version": "1.3.5",
@@ -4258,9 +4181,8 @@
}
},
"node_modules/@types/offscreencanvas": {
"version": "2019.7.3",
"resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz",
"integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A=="
"version": "2019.3.0",
"license": "MIT"
},
"node_modules/@types/parse-json": {
"version": "4.0.2",
@@ -4326,8 +4248,7 @@
},
"node_modules/@types/seedrandom": {
"version": "2.4.34",
"resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-2.4.34.tgz",
"integrity": "sha512-ytDiArvrn/3Xk6/vtylys5tlY6eo7Ane0hvcx++TKo6RxQXuVfW0AF/oeWqAj9dN29SyhtawuXstgmPlwNcv/A=="
"license": "MIT"
},
"node_modules/@types/semver": {
"version": "7.5.8",
@@ -4376,6 +4297,10 @@
"version": "3.0.3",
"license": "MIT"
},
"node_modules/@types/webgl-ext": {
"version": "0.0.30",
"license": "MIT"
},
"node_modules/@types/ws": {
"version": "8.5.12",
"license": "MIT",
@@ -4723,9 +4648,8 @@
}
},
"node_modules/@webgpu/types": {
"version": "0.1.38",
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.38.tgz",
"integrity": "sha512-7LrhVKz2PRh+DD7+S+PVaFd5HxaWQvoMqBbsV9fNJO1pjUs1P8bM2vQVNfk+3URTqbuTI7gkXi0rfsN0IadoBA=="
"version": "0.1.16",
"license": "BSD-3-Clause"
},
"node_modules/@webpack-cli/configtest": {
"version": "2.1.1",
@@ -5160,9 +5084,7 @@
}
},
"node_modules/autoprefixer": {
"version": "10.4.20",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
"integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==",
"version": "10.4.19",
"funding": [
{
"type": "opencollective",
@@ -5179,11 +5101,11 @@
],
"license": "MIT",
"dependencies": {
"browserslist": "^4.23.3",
"caniuse-lite": "^1.0.30001646",
"browserslist": "^4.23.0",
"caniuse-lite": "^1.0.30001599",
"fraction.js": "^4.3.7",
"normalize-range": "^0.1.2",
"picocolors": "^1.0.1",
"picocolors": "^1.0.0",
"postcss-value-parser": "^4.2.0"
},
"bin": {
@@ -9489,8 +9411,6 @@
},
"node_modules/intl-messageformat": {
"version": "10.5.14",
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.5.14.tgz",
"integrity": "sha512-IjC6sI0X7YRjjyVH9aUgdftcmZK7WXdHeil4KwbjDnRWjnVitKpAx3rr6t6di1joFp5188VqKcobOPA6mCLG/w==",
"license": "BSD-3-Clause",
"dependencies": {
"@formatjs/ecma402-abstract": "2.0.0",
@@ -9501,8 +9421,6 @@
},
"node_modules/intl-messageformat/node_modules/@formatjs/ecma402-abstract": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.0.0.tgz",
"integrity": "sha512-rRqXOqdFmk7RYvj4khklyqzcfQl9vEL/usogncBHRZfZBDOwMGuSRNFl02fu5KGHXdbinju+YXyuR+Nk8xlr/g==",
"license": "MIT",
"dependencies": {
"@formatjs/intl-localematcher": "0.5.4",
@@ -9511,8 +9429,6 @@
},
"node_modules/intl-messageformat/node_modules/@formatjs/intl-localematcher": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz",
"integrity": "sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==",
"license": "MIT",
"dependencies": {
"tslib": "^2.4.0"
@@ -11671,8 +11587,7 @@
},
"node_modules/node-fetch": {
"version": "2.6.13",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.13.tgz",
"integrity": "sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
@@ -11690,18 +11605,15 @@
},
"node_modules/node-fetch/node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
"license": "MIT"
},
"node_modules/node-fetch/node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
"license": "BSD-2-Clause"
},
"node_modules/node-fetch/node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
@@ -12148,9 +12060,7 @@
}
},
"node_modules/picocolors": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
"version": "1.0.1",
"license": "ISC"
},
"node_modules/picomatch": {
@@ -12355,9 +12265,7 @@
}
},
"node_modules/postcss": {
"version": "8.4.47",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
"version": "8.4.39",
"funding": [
{
"type": "opencollective",
@@ -12375,8 +12283,8 @@
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.1.0",
"source-map-js": "^1.2.1"
"picocolors": "^1.0.1",
"source-map-js": "^1.2.0"
},
"engines": {
"node": "^10 || ^12 || >=14"
@@ -13464,14 +13372,12 @@
}
},
"node_modules/react-intl": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/react-intl/-/react-intl-6.7.0.tgz",
"integrity": "sha512-f5QhjuKb+WEqiAbL5hDqUs2+sSRkF0vxkTbJ4A8ompt55XTyOHcrDlCXGq4o73ywFFrpgz+78C9IXegSLlya2A==",
"version": "6.6.8",
"license": "BSD-3-Clause",
"dependencies": {
"@formatjs/ecma402-abstract": "2.0.0",
"@formatjs/icu-messageformat-parser": "2.7.8",
"@formatjs/intl": "2.10.5",
"@formatjs/intl": "2.10.4",
"@formatjs/intl-displaynames": "6.6.8",
"@formatjs/intl-listformat": "7.5.7",
"@types/hoist-non-react-statics": "^3.3.1",
@@ -13645,12 +13551,10 @@
}
},
"node_modules/react-router": {
"version": "6.27.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.27.0.tgz",
"integrity": "sha512-YA+HGZXz4jaAkVoYBE98VQl+nVzI+cVI2Oj/06F5ZM+0u3TgedN9Y9kmMRo2mnkSK2nCpNQn0DVob4HCsY/WLw==",
"version": "6.26.0",
"license": "MIT",
"dependencies": {
"@remix-run/router": "1.20.0"
"@remix-run/router": "1.19.0"
},
"engines": {
"node": ">=14.0.0"
@@ -13660,13 +13564,11 @@
}
},
"node_modules/react-router-dom": {
"version": "6.27.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.27.0.tgz",
"integrity": "sha512-+bvtFWMC0DgAFrfKXKG9Fc+BcXWRUO1aJIihbB79xaeq0v5UzfvnM5houGUm1Y461WVRcgAQ+Clh5rdb1eCx4g==",
"version": "6.26.0",
"license": "MIT",
"dependencies": {
"@remix-run/router": "1.20.0",
"react-router": "6.27.0"
"@remix-run/router": "1.19.0",
"react-router": "6.26.0"
},
"engines": {
"node": ">=14.0.0"
@@ -13863,16 +13765,11 @@
}
},
"node_modules/redux-mock-store": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/redux-mock-store/-/redux-mock-store-1.5.5.tgz",
"integrity": "sha512-YxX+ofKUTQkZE4HbhYG4kKGr7oCTJfB0GLy7bSeqx86GLpGirrbUWstMnqXkqHNaQpcnbMGbof2dYs5KsPE6Zg==",
"version": "1.5.4",
"dev": true,
"license": "MIT",
"dependencies": {
"lodash.isplainobject": "^4.0.6"
},
"peerDependencies": {
"redux": "*"
}
},
"node_modules/redux-saga": {
@@ -14328,8 +14225,7 @@
},
"node_modules/seedrandom": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz",
"integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg=="
"license": "MIT"
},
"node_modules/select-hose": {
"version": "2.0.0",
@@ -14704,9 +14600,7 @@
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"version": "1.2.0",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@@ -15781,25 +15675,15 @@
}
},
"node_modules/universal-cookie": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-7.2.2.tgz",
"integrity": "sha512-fMiOcS3TmzP2x5QV26pIH3mvhexLIT0HmPa3V7Q7knRfT9HG6kTwq02HZGLPw0sAOXrAmotElGRvTLCMbJsvxQ==",
"version": "4.0.4",
"license": "MIT",
"dependencies": {
"@types/cookie": "^0.6.0",
"cookie": "^0.7.2"
"@types/cookie": "^0.3.3",
"cookie": "^0.4.0"
}
},
"node_modules/universal-cookie/node_modules/@types/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
"license": "MIT"
},
"node_modules/universal-cookie/node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"version": "0.4.2",
"license": "MIT",
"engines": {
"node": ">= 0.6"

View File

@@ -29,8 +29,8 @@
],
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-component-header": "^5.6.0",
"@edx/frontend-platform": "8.1.2",
"@edx/frontend-component-header": "5.3.4",
"@edx/frontend-platform": "8.1.1",
"@edx/openedx-atlas": "^0.6.0",
"@fortawesome/fontawesome-svg-core": "^6.6.0",
"@fortawesome/free-brands-svg-icons": "^6.6.0",
@@ -39,10 +39,10 @@
"@fortawesome/react-fontawesome": "0.2.2",
"@openedx/frontend-plugin-framework": "^1.2.2",
"@openedx/frontend-slot-footer": "^1.0.2",
"@openedx/paragon": "22.9.0",
"@tensorflow-models/blazeface": "0.1.0",
"@tensorflow/tfjs-converter": "4.22.0",
"@tensorflow/tfjs-core": "4.22.0",
"@openedx/paragon": "22.0.0",
"@tensorflow-models/blazeface": "0.0.7",
"@tensorflow/tfjs-converter": "3.21.0",
"@tensorflow/tfjs-core": "3.21.0",
"bowser": "2.11.0",
"classnames": "2.5.1",
"core-js": "3.38.1",
@@ -80,16 +80,16 @@
"redux-thunk": "2.4.2",
"regenerator-runtime": "0.14.1",
"reselect": "^5.1.1",
"universal-cookie": "7.2.2"
"universal-cookie": "4.0.4"
},
"devDependencies": {
"@edx/browserslist-config": "1.2.0",
"@edx/reactifex": "2.2.0",
"@openedx/frontend-build": "14.1.5",
"@testing-library/jest-dom": "6.6.3",
"@edx/reactifex": "1.1.0",
"@openedx/frontend-build": "14.1.2",
"@testing-library/jest-dom": "6.4.8",
"@testing-library/react": "12.1.5",
"react-test-renderer": "17.0.2",
"reactifex": "1.1.1",
"redux-mock-store": "1.5.5"
"redux-mock-store": "1.5.4"
}
}

View File

@@ -14,7 +14,7 @@ import {
getLanguageList,
} from '@edx/frontend-platform/i18n';
import {
Hyperlink, Icon, Alert,
Button, Hyperlink, Icon, Alert,
} from '@openedx/paragon';
import { CheckCircle, Error, WarningFilled } from '@openedx/paragon/icons';
@@ -50,7 +50,6 @@ import {
} from './data/constants';
import { fetchSiteLanguages } from './site-language';
import { fetchCourseList } from '../notification-preferences/data/thunks';
import NotificationSettings from '../notification-preferences/NotificationSettings';
import { withLocation, withNavigate } from './hoc';
class AccountSettingsPage extends React.Component {
@@ -121,15 +120,7 @@ class AccountSettingsPage extends React.Component {
countryOptions: [{
value: '',
label: this.props.intl.formatMessage(messages['account.settings.field.country.options.empty']),
}].concat(
this.removeDisabledCountries(
getCountryList(locale).map(({ code, name }) => ({
value: code,
label: name,
disabled: this.isDisabledCountry(code),
})),
),
),
}].concat(getCountryList(locale).map(({ code, name }) => ({ value: code, label: name }))),
stateOptions: [{
value: '',
label: this.props.intl.formatMessage(messages['account.settings.field.state.options.empty']),
@@ -156,28 +147,11 @@ class AccountSettingsPage extends React.Component {
})),
}));
removeDisabledCountries = (countryList) => {
const { disabledCountries, committedValues } = this.props;
if (!disabledCountries.length) {
return countryList;
}
return countryList.filter(({ value, disabled }) => {
const isUserCountry = value === committedValues.country;
return !disabled || isUserCountry;
});
};
handleEditableFieldChange = (name, value) => {
this.props.updateDraft(name, value);
};
handleSubmit = (formId, values) => {
if (formId === 'country' && this.isDisabledCountry(values)) {
return;
}
const { formValues } = this.props;
let extendedProfileObject = {};
@@ -219,11 +193,6 @@ class AccountSettingsPage extends React.Component {
}
};
isDisabledCountry = (country) => {
const { disabledCountries } = this.props;
return disabledCountries.includes(country);
};
isEditable(fieldName) {
return !this.props.staticFields.includes(fieldName);
}
@@ -342,9 +311,19 @@ class AccountSettingsPage extends React.Component {
header={this.props.intl.formatMessage(messages['account.settings.field.name.verified.failure.message.header'])}
body={
(
<div className="d-flex flex-row">
{this.props.intl.formatMessage(messages['account.settings.field.name.verified.failure.message'])}
</div>
<>
<div className="d-flex flex-row">
{this.props.intl.formatMessage(messages['account.settings.field.name.verified.failure.message'])}
</div>
<div className="d-flex flex-row-reverse mt-3">
<Button
variant="primary"
href="https://support.edx.org/hc/en-us/articles/360004381594-Why-was-my-ID-verification-denied"
>
{this.props.intl.formatMessage(messages['account.settings.field.name.verified.failure.message.help.link'])}
</Button>{' '}
</div>
</>
)
}
/>
@@ -497,8 +476,7 @@ class AccountSettingsPage extends React.Component {
} = this.getLocalizedOptions(this.context.locale, this.props.formValues.country);
// Show State field only if the country is US (could include Canada later)
const { country } = this.props.formValues;
const showState = country === COUNTRY_WITH_STATES && !this.isDisabledCountry(country);
const showState = this.props.formValues.country === COUNTRY_WITH_STATES;
const { verifiedName } = this.props;
const hasWorkExperience = !!this.props.formValues?.extended_profile?.find(field => field.field_name === 'work_experience');
@@ -728,7 +706,7 @@ class AccountSettingsPage extends React.Component {
{...editableFieldProps}
/>
</div>
<div className="account-section pt-3" id="social-media">
<div className="account-section pt-3 mb-5" id="social-media">
<h2 className="section-heading h4 mb-3">
{this.props.intl.formatMessage(messages['account.settings.section.social.media'])}
</h2>
@@ -764,10 +742,8 @@ class AccountSettingsPage extends React.Component {
{...editableFieldProps}
/>
</div>
<div className="border border-light-700 my-6" />
<NotificationSettings />
<div className="border border-light-700 my-6" />
<div className="account-section mb-5" id="site-preferences" ref={this.navLinkRefs['#site-preferences']}>
<div className="account-section pt-3 mb-5" id="site-preferences" ref={this.navLinkRefs['#site-preferences']}>
<h2 className="section-heading h4 mb-3">
{this.props.intl.formatMessage(messages['account.settings.section.site.preferences'])}
</h2>
@@ -904,7 +880,6 @@ AccountSettingsPage.propTypes = {
name: PropTypes.string,
useVerifiedNameForCerts: PropTypes.bool,
verified_name: PropTypes.string,
country: PropTypes.string,
}),
drafts: PropTypes.shape({}),
formErrors: PropTypes.shape({
@@ -963,7 +938,6 @@ AccountSettingsPage.propTypes = {
),
navigate: PropTypes.func.isRequired,
location: PropTypes.string.isRequired,
disabledCountries: PropTypes.arrayOf(PropTypes.string),
};
AccountSettingsPage.defaultProps = {
@@ -973,7 +947,6 @@ AccountSettingsPage.defaultProps = {
committedValues: {
useVerifiedNameForCerts: false,
verified_name: null,
country: '',
},
drafts: {},
formErrors: {},
@@ -990,7 +963,6 @@ AccountSettingsPage.defaultProps = {
verifiedName: null,
mostRecentVerifiedName: {},
verifiedNameHistory: [],
disabledCountries: [],
};
export default withLocation(withNavigate(connect(accountSettingsPageSelector, {

View File

@@ -107,7 +107,6 @@ const EditableSelectField = (props) => {
<option
value={subOption.value}
key={`${subOption.value}-${subOption.label}`}
disabled={subOption?.disabled}
>
{subOption.label}
</option>
@@ -116,7 +115,7 @@ const EditableSelectField = (props) => {
);
}
return (
<option value={option.value} key={`${option.value}-${option.label}`} disabled={option?.disabled}>
<option value={option.value} key={`${option.value}-${option.label}`}>
{option.label}
</option>
);

View File

@@ -1,16 +1,21 @@
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { breakpoints, useWindowSize } from '@openedx/paragon';
import { breakpoints, useWindowSize, Icon } from '@openedx/paragon';
import { OpenInNew } from '@openedx/paragon/icons';
import classNames from 'classnames';
import React from 'react';
import { useSelector } from 'react-redux';
import { NavHashLink } from 'react-router-hash-link';
import Scrollspy from 'react-scrollspy';
import { Link } from 'react-router-dom';
import messages from './AccountSettingsPage.messages';
import { selectShowPreferences } from '../notification-preferences/data/selectors';
const JumpNav = ({
intl,
}) => {
const stickToTop = useWindowSize().width > breakpoints.small.minWidth;
const showPreferences = useSelector(selectShowPreferences());
return (
<div className={classNames('jump-nav px-2.25', { 'jump-nav-sm position-sticky pt-3': stickToTop })}>
@@ -60,6 +65,21 @@ const JumpNav = ({
</li>
)}
</Scrollspy>
{showPreferences && (
<>
<hr />
<Scrollspy
className="list-unstyled"
>
<li>
<Link to="/notifications" target="_blank" rel="noopener noreferrer">
<span>{intl.formatMessage(messages['notification.preferences.notifications.label'])}</span>
<Icon className="d-inline-block align-bottom ml-1" src={OpenInNew} />
</Link>
</li>
</Scrollspy>
</>
)}
</div>
);
};

View File

@@ -39,7 +39,6 @@ export const defaultState = {
verifiedName: null,
mostRecentVerifiedName: {},
verifiedNameHistory: {},
disabledCountries: ['RU'],
};
const reducer = (state = defaultState, action = {}) => {

View File

@@ -206,11 +206,6 @@ const activeAccountSelector = createSelector(
accountSettings => accountSettings.values.is_active,
);
const disabledCountriesSelector = createSelector(
accountSettingsSelector,
accountSettings => accountSettings.disabledCountries,
);
export const siteLanguageSelector = createSelector(
previousSiteLanguageSelector,
draftsSelector,
@@ -242,7 +237,6 @@ export const accountSettingsPageSelector = createSelector(
mostRecentApprovedVerifiedNameValueSelector,
mostRecentVerifiedNameSelector,
sortedVerifiedNameHistorySelector,
disabledCountriesSelector,
(
accountSettings,
siteLanguageOptions,
@@ -260,7 +254,6 @@ export const accountSettingsPageSelector = createSelector(
verifiedName,
mostRecentVerifiedName,
verifiedNameHistory,
disabledCountries,
) => ({
siteLanguageOptions,
siteLanguage,
@@ -281,7 +274,6 @@ export const accountSettingsPageSelector = createSelector(
verifiedName,
mostRecentVerifiedName,
verifiedNameHistory,
disabledCountries,
}),
);

View File

@@ -99,7 +99,7 @@ export class DeleteAccount extends React.Component {
)}
</p>
<p>
<Hyperlink destination="https://help.edx.org/edxlearner/s/topic/0TOQq0000001UdZOAU/account-basics">
<Hyperlink destination="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings">
{intl.formatMessage(messages['account.settings.delete.account.text.change.instead'])}
</Hyperlink>
</p>
@@ -116,7 +116,7 @@ export class DeleteAccount extends React.Component {
{isVerifiedAccount ? null : (
<BeforeProceedingBanner
instructionMessageId={optInInstructionMessageId}
supportArticleUrl="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-confirm-my-email"
supportArticleUrl="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-confirm-my-email-"
/>
)}

View File

@@ -11,7 +11,7 @@ const PrintingInstructions = (props) => {
// TODO: What would a generic version of this link look like? Should
// CERTIFICATE_SHARING_HELP_URL really be a configuration variable? In the meantime,
// We've removed the link from the default message.
destination="https://help.edx.org/edxlearner/s/topic/0TOQq0000001UVVOA2/certificates"
destination="https://support.edx.org/hc/en-us/sections/115004173027-Receive-and-Share-edX-Certificates"
>
{props.intl.formatMessage(messages['account.settings.delete.account.text.3.link'])}
</Hyperlink>

View File

@@ -41,7 +41,7 @@ exports[`ConfirmationModal should match empty password confirmation modal snapsh
/>
<div
aria-label="Are you sure?"
className="pgn__modal pgn__modal-md pgn__modal-default pgn__modal-visible-overflow pgn__alert-modal"
className="pgn__modal pgn__modal-md pgn__modal-default pgn__alert-modal"
role="dialog"
>
<div
@@ -242,7 +242,7 @@ exports[`ConfirmationModal should match open confirmation modal snapshot 1`] = `
/>
<div
aria-label="Are you sure?"
className="pgn__modal pgn__modal-md pgn__modal-default pgn__modal-visible-overflow pgn__alert-modal"
className="pgn__modal pgn__modal-md pgn__modal-default pgn__alert-modal"
role="dialog"
>
<div

View File

@@ -27,7 +27,7 @@ exports[`DeleteAccount should match default section snapshot 1`] = `
<p>
<a
className="pgn__hyperlink default-link standalone-link"
href="https://help.edx.org/edxlearner/s/topic/0TOQq0000001UdZOAU/account-basics"
href="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings"
onClick={[Function]}
target="_self"
>
@@ -74,7 +74,7 @@ exports[`DeleteAccount should match unverified account section snapshot 1`] = `
<p>
<a
className="pgn__hyperlink default-link standalone-link"
href="https://help.edx.org/edxlearner/s/topic/0TOQq0000001UdZOAU/account-basics"
href="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings"
onClick={[Function]}
target="_self"
>
@@ -117,7 +117,7 @@ exports[`DeleteAccount should match unverified account section snapshot 1`] = `
Before proceeding, please
<a
className="pgn__hyperlink default-link standalone-link"
href="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-confirm-my-email"
href="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-confirm-my-email-"
onClick={[Function]}
target="_self"
>
@@ -156,7 +156,7 @@ exports[`DeleteAccount should match unverified account section snapshot 2`] = `
<p>
<a
className="pgn__hyperlink default-link standalone-link"
href="https://help.edx.org/edxlearner/s/topic/0TOQq0000001UdZOAU/account-basics"
href="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings"
onClick={[Function]}
target="_self"
>
@@ -199,7 +199,7 @@ exports[`DeleteAccount should match unverified account section snapshot 2`] = `
Before proceeding, please
<a
className="pgn__hyperlink default-link standalone-link"
href="https://help.edx.org/edxlearner/s/article/How-do-I-link-or-unlink-my-edX-account-to-a-social-media-account"
href="https://support.edx.org/hc/en-us/articles/207206067"
onClick={[Function]}
target="_self"
>

View File

@@ -23,15 +23,10 @@ export async function patchPreferences(username, params) {
export async function postSetLang(code) {
const formData = new FormData();
const requestConfig = {
headers: {
Accept: 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
};
const url = `${getConfig().LMS_BASE_URL}/i18n/setlang/`;
formData.append('language', code);
await getAuthenticatedHttpClient()
.post(url, formData, requestConfig);
.post(`${getConfig().LMS_BASE_URL}/i18n/setlang/`, formData, {
headers: { 'X-Requested-With': 'XMLHttpRequest' },
});
}

View File

@@ -71,16 +71,6 @@ describe('AccountSettingsPage', () => {
afterEach(() => jest.clearAllMocks());
beforeAll(() => {
global.lightningjs = {
require: jest.fn().mockImplementation((module, url) => ({ moduleName: module, url })),
};
});
afterAll(() => {
delete global.lightningjs;
});
it('renders AccountSettingsPage correctly with editing enabled', async () => {
const { getByText, rerender, getByLabelText } = render(reduxWrapper(<IntlAccountSettingsPage {...props} />));

View File

@@ -84,7 +84,7 @@ const mockData = {
profileDataManager: null,
},
notificationPreferences: {
showPreferences: true,
showPreferences: false,
courses: {
status: 'success',
courses: [],
@@ -98,7 +98,7 @@ const mockData = {
preferences: {
status: 'idle',
updatePreferenceStatus: 'idle',
selectedCourse: 'account',
selectedCourse: null,
preferences: [],
apps: [],
nonEditable: {},

View File

@@ -16,9 +16,8 @@ export async function getThirdPartyAuthProviders() {
}
export async function postDisconnectAuth(url) {
const requestConfig = { headers: { Accept: 'application/json' } };
const { data } = await getAuthenticatedHttpClient()
.post(url, {}, requestConfig)
.post(url)
.catch(handleRequestError);
return data;
}

View File

@@ -1,16 +0,0 @@
import PropTypes from 'prop-types';
import classNames from 'classnames';
const Divider = ({ className, ...props }) => (
<div className={classNames('divider', className)} {...props} />
);
Divider.propTypes = {
className: PropTypes.string,
};
Divider.defaultProps = {
className: undefined,
};
export default Divider;

View File

@@ -1,2 +0,0 @@
// eslint-disable-next-line import/prefer-default-export
export { default as Divider } from './Divider';

View File

@@ -20,6 +20,8 @@ import messages from './i18n';
import './index.scss';
import Head from './head/Head';
import NotificationCourses from './notification-preferences/NotificationCourses';
import NotificationPreferences from './notification-preferences/NotificationPreferences';
subscribe(APP_READY, () => {
ReactDOM.render(
@@ -36,6 +38,8 @@ subscribe(APP_READY, () => {
</div>
)}
>
<Route path="/notifications/:courseId" element={<NotificationPreferences />} />
<Route path="/notifications" element={<NotificationCourses />} />
<Route
path="/id-verification/*"
element={<IdVerificationPageSlot />}

View File

@@ -118,7 +118,7 @@ $fa-font-path: "~font-awesome/fonts";
}
.dropdown-item:active,
.dropdown-item:focus,
.dropdown-item:focus,
.btn-tertiary:not(:disabled):not(.disabled).active {
background-color: $light-300 !important;
}
@@ -131,20 +131,6 @@ $fa-font-path: "~font-awesome/fonts";
.h-4\.5 {
height: 36px;
}
.course-dropdown{
#course-dropdown-btn {
width: 100%;
font-size: 14px !important;
padding-top: 10px !important;
padding-bottom: 10px !important;
border: 1px solid $light-500 !important;
}
.dropdown-item {
font-size: 14px !important;
}
}
}
.usabilla_live_button_container {

View File

@@ -0,0 +1,85 @@
import React, { useCallback, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { ArrowForwardIos } from '@openedx/paragon/icons';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
Button, Container, Icon, Spinner,
} from '@openedx/paragon';
import messages from './messages';
import { useFeedbackWrapper } from '../hooks';
import { fetchCourseList } from './data/thunks';
import { NotFoundPage } from '../account-settings';
import { IDLE_STATUS, LOADING_STATUS, SUCCESS_STATUS } from '../constants';
import { selectCourseList, selectCourseListStatus, selectPagination } from './data/selectors';
const NotificationCourses = ({ intl }) => {
useFeedbackWrapper();
const dispatch = useDispatch();
const coursesList = useSelector(selectCourseList());
const courseListStatus = useSelector(selectCourseListStatus());
const { hasMore, currentPage } = useSelector(selectPagination());
const loadMore = useCallback((page = 1, pageSize = 10) => {
dispatch(fetchCourseList(page, pageSize));
}, [dispatch]);
useEffect(() => {
if (courseListStatus === IDLE_STATUS) { loadMore(); }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (courseListStatus === SUCCESS_STATUS && coursesList.length === 0) {
return <NotFoundPage />;
}
return (
<Container size="md">
<h2 className="notification-heading mt-6 mb-5.5">
{intl.formatMessage(messages.notificationHeading)}
</h2>
<div data-testid="courses-list">
{coursesList.map(course => (
<Link
key={course.id}
to={`/notifications/${course.id}`}
className="text-decoration-none"
>
<div className="mb-4 d-flex text-gray-700">
<span className="ml-0 mr-auto">
{course.name}
</span>
<span className="ml-auto mr-0">
<Icon src={ArrowForwardIos} />
</span>
</div>
</Link>
))}
</div>
{courseListStatus === LOADING_STATUS ? (
<div className="d-flex">
<Spinner
variant="primary"
animation="border"
className="mx-auto my-auto"
size="lg"
data-testid="loading-spinner"
/>
</div>
) : hasMore && (
<Button variant="primary" className="w-100 bg-primary-500" onClick={() => loadMore(currentPage + 1)}>
{intl.formatMessage(messages.loadMoreCourses)}
</Button>
)}
</Container>
);
};
NotificationCourses.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(NotificationCourses);

View File

@@ -0,0 +1,97 @@
/* eslint-disable no-import-assign */
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import { BrowserRouter as Router } from 'react-router-dom';
import * as auth from '@edx/frontend-platform/auth';
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { defaultState } from './data/reducers';
import NotificationCourses from './NotificationCourses';
import { LOADING_STATUS, SUCCESS_STATUS } from '../constants';
const mockStore = configureStore();
jest.mock('@edx/frontend-platform/auth');
const courseList = [
{ id: 'course-id-1', name: 'Course Name 1' },
{ id: 'course-id-2', name: 'Course Name 2' },
{ id: 'course-id-3', name: 'Course Name 3' },
];
const setupStore = (override = {}) => {
const storeState = defaultState;
storeState.courses = {
...storeState.courses,
...override,
};
const store = mockStore({
notificationPreferences: storeState,
});
return store;
};
const renderComponent = (store = {}) => (
render(
<Router>
<IntlProvider locale="en">
<Provider store={store}>
<NotificationCourses />
</Provider>
</IntlProvider>
</Router>,
)
);
describe('Notification Courses', () => {
let store;
beforeEach(() => {
store = setupStore({
courses: courseList,
status: SUCCESS_STATUS,
pagination: {
count: 3,
currentPage: 1,
hasMore: false,
totalPages: 1,
},
});
auth.getAuthenticatedHttpClient = jest.fn(() => ({
patch: async () => ({
data: { status: 200 },
catch: () => {},
}),
}));
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3 }));
window.lightningjs = null;
});
afterEach(() => jest.clearAllMocks());
it('tests if all courses are available', async () => {
await renderComponent(store);
expect(screen.queryByTestId('courses-list').children).toHaveLength(3);
});
it('show spinner if api call is in progress', async () => {
store = setupStore({ status: LOADING_STATUS });
await renderComponent(store);
expect(screen.queryByTestId('loading-spinner')).toBeInTheDocument();
});
it('show not found page if course list is empty', async () => {
store = setupStore({ status: SUCCESS_STATUS, courses: [] });
await renderComponent(store);
expect(screen.queryByTestId('not-found-page')).toBeInTheDocument();
});
it('show load more courses button when hasMore True', async () => {
store = setupStore({ status: SUCCESS_STATUS, pagination: { ...store.pagination, hasMore: true, totalPages: 2 } });
await renderComponent(store);
expect(screen.queryByText('Load more courses')).toBeInTheDocument();
});
});

View File

@@ -1,73 +0,0 @@
import React, { useCallback, useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Dropdown } from '@openedx/paragon';
import { IDLE_STATUS, SUCCESS_STATUS } from '../constants';
import { selectCourseList, selectCourseListStatus, selectSelectedCourseId } from './data/selectors';
import { fetchCourseList, setSelectedCourse } from './data/thunks';
import messages from './messages';
const NotificationCoursesDropdown = () => {
const intl = useIntl();
const dispatch = useDispatch();
const coursesList = useSelector(selectCourseList());
const courseListStatus = useSelector(selectCourseListStatus());
const selectedCourseId = useSelector(selectSelectedCourseId());
const selectedCourse = useMemo(
() => coursesList.find((course) => course.id === selectedCourseId),
[coursesList, selectedCourseId],
);
const handleCourseSelection = useCallback((courseId) => {
dispatch(setSelectedCourse(courseId));
}, [dispatch]);
const fetchCourses = useCallback((page = 1, pageSize = 99999) => {
dispatch(fetchCourseList(page, pageSize));
}, [dispatch]);
useEffect(() => {
if (courseListStatus === IDLE_STATUS) {
fetchCourses();
}
}, [courseListStatus, fetchCourses]);
return (
courseListStatus === SUCCESS_STATUS && (
<div className="mb-5">
<h5 className="text-primary-500 mb-3">{intl.formatMessage(messages.notificationDropdownlabel)}</h5>
<Dropdown className="course-dropdown">
<Dropdown.Toggle
variant="outline-primary"
id="course-dropdown-btn"
className="w-100 justify-content-between small"
>
{selectedCourse?.name}
</Dropdown.Toggle>
<Dropdown.Menu className="w-100">
{coursesList.map((course) => (
<Dropdown.Item
className="w-100"
key={course.id}
active={course.id === selectedCourse?.id}
eventKey={course.id}
onSelect={handleCourseSelection}
>
{course.name}
</Dropdown.Item>
))}
</Dropdown.Menu>
</Dropdown>
<span className="x-small text-gray-500">
{selectedCourse?.name === 'Account'
? intl.formatMessage(messages.notificationDropdownApplies)
: intl.formatMessage(messages.notificationCourseDropdownApplies)}
</span>
</div>
)
);
};
export default NotificationCoursesDropdown;

View File

@@ -11,22 +11,27 @@ import { useIsOnMobile } from '../hooks';
import NotificationTypes from './NotificationTypes';
import { notificationChannels, shouldHideAppPreferences } from './data/utils';
import NotificationPreferenceColumn from './NotificationPreferenceColumn';
import { selectPreferenceAppToggleValue, selectAppPreferences } from './data/selectors';
import { selectPreferenceAppToggleValue, selectSelectedCourseId, selectAppPreferences } from './data/selectors';
const NotificationPreferenceApp = ({ appId }) => {
const intl = useIntl();
const courseId = useSelector(selectSelectedCourseId());
const appToggle = useSelector(selectPreferenceAppToggleValue(appId));
const appPreferences = useSelector(selectAppPreferences(appId));
const mobileView = useIsOnMobile();
const NOTIFICATION_CHANNELS = notificationChannels();
const hideAppPreferences = shouldHideAppPreferences(appPreferences, appId) || false;
if (!courseId) {
return null;
}
return (
!hideAppPreferences && (
<Collapsible.Advanced
open={appToggle}
data-testid={`${appId}-app`}
className={classNames({ 'mb-4.5': !mobileView && appToggle })}
className={classNames({ 'mb-5': !mobileView && appToggle })}
>
<Collapsible.Trigger>
<div className="d-flex align-items-center">

View File

@@ -28,9 +28,7 @@ const NotificationPreferenceColumn = ({ appId, channel, appPreference }) => {
const onToggle = useCallback((event, notificationType) => {
const { name: notificationChannel } = event.target;
const appNotificationPreference = appPreferences.find(x => x.id === notificationType);
const value = notificationChannel === 'email_cadence' && courseId ? event.target.innerText : event.target.checked;
const emailCadence = notificationChannel === 'email_cadence' ? event.target.innerText : appNotificationPreference.emailCadence;
const value = notificationChannel === 'email_cadence' ? event.target.innerText : event.target.checked;
dispatch(updatePreferenceToggle(
courseId,
@@ -38,10 +36,9 @@ const NotificationPreferenceColumn = ({ appId, channel, appPreference }) => {
notificationType,
notificationChannel,
value,
emailCadence,
));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appId, appPreferences]);
}, [appId]);
const renderPreference = (preference) => (
(preference?.coreNotificationTypes?.length > 0 || preference.id !== 'core') && (

View File

@@ -1,26 +1,35 @@
import React, { useEffect, useMemo } from 'react';
import { Link, useParams } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import classNames from 'classnames';
import { ArrowBack } from '@openedx/paragon/icons';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Spinner, NavItem } from '@openedx/paragon';
import {
Container, Hyperlink, Icon, Spinner, NavItem,
} from '@openedx/paragon';
import { useIsOnMobile } from '../hooks';
import messages from './messages';
import { NotFoundPage } from '../account-settings';
import NotificationPreferenceApp from './NotificationPreferenceApp';
import { fetchCourseNotificationPreferences } from './data/thunks';
import { LOADING_STATUS } from '../constants';
import { fetchCourseList, fetchCourseNotificationPreferences } from './data/thunks';
import {
selectCourseListStatus, selectNotificationPreferencesStatus, selectPreferenceAppsId, selectSelectedCourseId,
FAILURE_STATUS, IDLE_STATUS, LOADING_STATUS, SUCCESS_STATUS,
} from '../constants';
import {
selectCourse, selectCourseList, selectCourseListStatus, selectNotificationPreferencesStatus, selectPreferenceAppsId,
} from './data/selectors';
import { notificationChannels } from './data/utils';
const NotificationPreferences = () => {
const { courseId } = useParams();
const dispatch = useDispatch();
const intl = useIntl();
const courseStatus = useSelector(selectCourseListStatus());
const courseId = useSelector(selectSelectedCourseId());
const coursesList = useSelector(selectCourseList());
const course = useSelector(selectCourse(courseId));
const notificationStatus = useSelector(selectNotificationPreferencesStatus());
const preferenceAppsIds = useSelector(selectPreferenceAppsId());
const mobileView = useIsOnMobile();
@@ -34,16 +43,46 @@ const NotificationPreferences = () => {
), [preferenceAppsIds]);
useEffect(() => {
if ([IDLE_STATUS, FAILURE_STATUS].includes(courseStatus)) {
dispatch(fetchCourseList());
}
dispatch(fetchCourseNotificationPreferences(courseId));
}, [courseId, dispatch]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [courseId]);
if (preferenceAppsIds.length === 0) {
return null;
if (
(courseStatus === SUCCESS_STATUS && coursesList.length === 0)
|| (notificationStatus === FAILURE_STATUS && coursesList.length !== 0)
) {
return <NotFoundPage />;
}
return (
<div className="h-100">
{!mobileView && !isLoading && (
<Container size="sm" className="notification-preferences">
<h2 className="notification-heading mt-6 mb-4.5">
{intl.formatMessage(messages.notificationHeading)}
</h2>
<div className="mb-6 text-gray-700 font-size-14 margin-bottom-32">
{intl.formatMessage(messages.notificationPreferenceGuideBody)}
<Hyperlink
destination="https://edx.readthedocs.io/projects/open-edx-learner-guide/en/latest/sfd_notifications/managing_notifications.html"
target="_blank"
rel="noopener noreferrer"
className="text-decoration-underline ml-1"
>
{intl.formatMessage(messages.notificationPreferenceGuideLink)}
</Hyperlink>
</div>
<div className="h-100">
<div className="d-flex mb-5">
<Link to="/notifications">
<Icon className="text-primary-500" src={ArrowBack} />
</Link>
<span className="notification-course-title ml-auto mr-auto text-primary-500">
{course?.name}
</span>
</div>
{!mobileView && !isLoading && (
<div className="d-flex flex-row justify-content-between float-right">
<div className="d-flex">
{Object.values(NOTIFICATION_CHANNELS).map((channel) => (
@@ -64,20 +103,21 @@ const NotificationPreferences = () => {
))}
</div>
</div>
)}
{preferencesList}
{isLoading && (
<div className="d-flex">
<Spinner
variant="primary"
animation="border"
className="mx-auto my-auto"
size="lg"
data-testid="loading-spinner"
/>
</div>
)}
</div>
)}
{preferencesList}
{isLoading && (
<div className="d-flex">
<Spinner
variant="primary"
animation="border"
className="mx-auto my-auto"
size="lg"
data-testid="loading-spinner"
/>
</div>
)}
</div>
</Container>
);
};

View File

@@ -9,7 +9,7 @@ import { fireEvent, render, screen } from '@testing-library/react';
import { defaultState } from './data/reducers';
import NotificationPreferences from './NotificationPreferences';
import { LOADING_STATUS, SUCCESS_STATUS } from '../constants';
import { FAILURE_STATUS, LOADING_STATUS, SUCCESS_STATUS } from '../constants';
const courseId = 'selected-course-id';
@@ -77,7 +77,6 @@ const setupStore = (override = {}) => {
storeState.courses = {
status: SUCCESS_STATUS,
courses: [
{ id: '', name: 'Account' },
{ id: 'selected-course-id', name: 'Selected Course' },
],
};
@@ -147,15 +146,9 @@ describe('Notification Preferences', () => {
expect(mockDispatch).toHaveBeenCalled();
});
it('update account preference on click', async () => {
store = setupStore({
...defaultPreferences,
status: SUCCESS_STATUS,
selectedCourse: '',
});
it('show not found page if invalid course id is entered in url', async () => {
store = setupStore({ status: FAILURE_STATUS, selectedCourse: 'invalid-course-id' });
await render(notificationPreferences(store));
const element = screen.getByTestId('core-web');
await fireEvent.click(element);
expect(mockDispatch).toHaveBeenCalled();
expect(screen.queryByTestId('not-found-page')).toBeInTheDocument();
});
});

View File

@@ -1,52 +0,0 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Container, Hyperlink } from '@openedx/paragon';
import { selectSelectedCourseId, selectShowPreferences } from './data/selectors';
import messages from './messages';
import NotificationCoursesDropdown from './NotificationCoursesDropdown';
import NotificationPreferences from './NotificationPreferences';
import { useFeedbackWrapper } from '../hooks';
const NotificationSettings = () => {
useFeedbackWrapper();
const intl = useIntl();
const showPreferences = useSelector(selectShowPreferences());
const courseId = useSelector(selectSelectedCourseId());
return (
showPreferences && (
<Container className="notification-preferences px-0">
<h2 className="notification-heading mb-3">
{intl.formatMessage(messages.notificationHeading)}
</h2>
<div className="text-gray-700 font-size-14 mb-3">
{intl.formatMessage(messages.accountNotificationDescription)}
</div>
<div className="text-gray-700 font-size-14 mb-3">
{intl.formatMessage(messages.notificationCadenceDescription, {
dailyTime: '12:00',
weeklyTime: '00:00',
})}
</div>
<div className="mb-5 text-gray-700 font-size-14">
{intl.formatMessage(messages.notificationPreferenceGuideBody)}
<Hyperlink
destination="https://edx.readthedocs.io/projects/open-edx-learner-guide/en/latest/sfd_notifications/managing_notifications.html"
target="_blank"
rel="noopener noreferrer"
className="text-decoration-underline ml-1"
>
{intl.formatMessage(messages.notificationPreferenceGuideLink)}
</Hyperlink>
</div>
<NotificationCoursesDropdown />
<NotificationPreferences courseId={courseId} />
</Container>
)
);
};
export default NotificationSettings;

View File

@@ -10,10 +10,8 @@ export const Actions = {
UPDATE_APP_PREFERENCE: 'updateAppValue',
};
export const fetchNotificationPreferenceSuccess = (courseId, payload, isAccountPreference) => dispatch => (
dispatch({
type: Actions.FETCHED_PREFERENCES, courseId, payload, isAccountPreference,
})
export const fetchNotificationPreferenceSuccess = (courseId, payload) => dispatch => (
dispatch({ type: Actions.FETCHED_PREFERENCES, courseId, payload })
);
export const fetchNotificationPreferenceFetching = () => dispatch => (

View File

@@ -1,6 +1,7 @@
const EMAIL_CADENCE = {
DAILY: 'Daily',
WEEKLY: 'Weekly',
NEVER: 'Never',
};
export default EMAIL_CADENCE;

View File

@@ -5,19 +5,18 @@ import {
SUCCESS_STATUS,
FAILURE_STATUS,
} from '../../constants';
import { normalizeAccountPreferences } from './thunks';
export const defaultState = {
showPreferences: false,
courses: {
status: IDLE_STATUS,
courses: [{ id: '', name: 'Account' }],
courses: [],
pagination: {},
},
preferences: {
status: IDLE_STATUS,
updatePreferenceStatus: IDLE_STATUS,
selectedCourse: '',
selectedCourse: null,
preferences: [],
apps: [],
nonEditable: {},
@@ -67,22 +66,15 @@ const notificationPreferencesReducer = (state = defaultState, action = {}) => {
},
};
case Actions.FETCHED_PREFERENCES:
{
const { preferences } = state;
if (action.isAccountPreference) {
normalizeAccountPreferences(preferences, action.payload);
}
return {
...state,
preferences: {
...preferences,
...state.preferences,
status: SUCCESS_STATUS,
updatePreferenceStatus: SUCCESS_STATUS,
...action.payload,
},
};
}
case Actions.FAILED_PREFERENCES:
return {
...state,

View File

@@ -36,7 +36,9 @@ describe('notification-preferences reducer', () => {
hasMore: false,
totalPages: 1,
},
courseList: [],
courseList: [
{ id: selectedCourseId, name: 'Selected Course' },
],
};
const result = reducer(
state,
@@ -44,7 +46,7 @@ describe('notification-preferences reducer', () => {
);
expect(result.courses).toEqual({
status: SUCCESS_STATUS,
courses: [{ id: '', name: 'Account' }],
courses: data.courseList,
pagination: data.pagination,
});
});
@@ -59,10 +61,7 @@ describe('notification-preferences reducer', () => {
);
expect(result.courses).toEqual({
status,
courses: [{
id: '',
name: 'Account',
}],
courses: [],
pagination: {},
});
});
@@ -83,7 +82,7 @@ describe('notification-preferences reducer', () => {
expect(result.preferences).toEqual({
status: SUCCESS_STATUS,
updatePreferenceStatus: SUCCESS_STATUS,
selectedCourse: '',
selectedCourse: null,
...preferenceData,
});
});
@@ -98,7 +97,7 @@ describe('notification-preferences reducer', () => {
);
expect(result.preferences).toEqual({
status,
selectedCourse: '',
selectedCourse: null,
preferences: [],
apps: [],
nonEditable: {},

View File

@@ -32,22 +32,3 @@ export const patchPreferenceToggle = async (
const { data } = await getAuthenticatedHttpClient().patch(url, patchData);
return data;
};
export const postPreferenceToggle = async (
notificationApp,
notificationType,
notificationChannel,
value,
emailCadence,
) => {
const patchData = snakeCaseObject({
notificationApp,
notificationType: snakeCase(notificationType),
notificationChannel,
value,
emailCadence,
});
const url = `${getConfig().LMS_BASE_URL}/api/notifications/preferences/update-all/`;
const { data } = await getAuthenticatedHttpClient().post(url, patchData);
return data;
};

View File

@@ -1,5 +1,4 @@
import { camelCaseObject } from '@edx/frontend-platform';
import camelCase from 'lodash.camelcase';
import EMAIL_CADENCE from './constants';
import {
fetchCourseListSuccess,
@@ -15,7 +14,6 @@ import {
getCourseList,
getCourseNotificationPreferences,
patchPreferenceToggle,
postPreferenceToggle,
} from './service';
const normalizeCourses = (responseData) => {
@@ -38,29 +36,8 @@ const normalizeCourses = (responseData) => {
};
};
export const normalizeAccountPreferences = (originalData, updateInfo) => {
const {
app, notificationType, channel, updatedValue,
} = updateInfo.data;
const preferenceToUpdate = originalData.preferences.find(
(preference) => preference.appId === app && preference.id === camelCase(notificationType),
);
if (preferenceToUpdate) {
preferenceToUpdate[camelCase(channel)] = updatedValue;
}
return originalData;
};
const normalizePreferences = (responseData, courseId) => {
let preferences;
if (courseId) {
preferences = responseData.notificationPreferenceConfig;
} else {
preferences = responseData.data;
}
const normalizePreferences = (responseData) => {
const preferences = responseData.notificationPreferenceConfig;
const appKeys = Object.keys(preferences);
const apps = appKeys.map((appId) => ({
@@ -115,7 +92,7 @@ export const fetchCourseNotificationPreferences = (courseId) => (
dispatch(updateSelectedCourse(courseId));
dispatch(fetchNotificationPreferenceFetching());
const data = await getCourseNotificationPreferences(courseId);
const normalizedData = normalizePreferences(camelCaseObject(data), courseId);
const normalizedData = normalizePreferences(camelCaseObject(data));
dispatch(fetchNotificationPreferenceSuccess(courseId, normalizedData));
} catch (errors) {
dispatch(fetchNotificationPreferenceFailed());
@@ -123,19 +100,12 @@ export const fetchCourseNotificationPreferences = (courseId) => (
}
);
export const setSelectedCourse = courseId => (
async (dispatch) => {
dispatch(updateSelectedCourse(courseId));
}
);
export const updatePreferenceToggle = (
courseId,
notificationApp,
notificationType,
notificationChannel,
value,
emailCadence,
) => (
async (dispatch) => {
try {
@@ -145,27 +115,15 @@ export const updatePreferenceToggle = (
notificationChannel,
!value,
));
let data = null;
if (courseId) {
data = await patchPreferenceToggle(
courseId,
notificationApp,
notificationType,
notificationChannel,
value,
);
const normalizedData = normalizePreferences(camelCaseObject(data), courseId);
dispatch(fetchNotificationPreferenceSuccess(courseId, normalizedData));
} else {
data = await postPreferenceToggle(
notificationApp,
notificationType,
notificationChannel,
value,
emailCadence,
);
dispatch(fetchNotificationPreferenceSuccess(courseId, camelCaseObject(data), true));
}
const data = await patchPreferenceToggle(
courseId,
notificationApp,
notificationType,
notificationChannel,
value,
);
const normalizedData = normalizePreferences(camelCaseObject(data));
dispatch(fetchNotificationPreferenceSuccess(courseId, normalizedData));
} catch (errors) {
dispatch(updatePreferenceValue(
notificationApp,

View File

@@ -28,7 +28,6 @@ const messages = defineMessages({
contentReported {Reported content}
courseUpdates {Course updates}
oraStaffNotification {ORA new submissions}
oraGradeAssigned {Essay assignment grade received}
other {{text}}
}`,
description: 'Display text for Notification Types',
@@ -90,36 +89,6 @@ const messages = defineMessages({
defaultMessage: 'Notifications for certain activities are enabled by default,',
description: 'Body of the notification preferences for learner guide',
},
accountNotificationDescription: {
id: 'account.notification.description',
defaultMessage: 'Account-level settings apply to all courses. Notifications for individual courses can be changed within each course and will override account-level settings.',
description: 'Account notification description',
},
notificationCadenceDescription: {
id: 'notification.cadence.description',
defaultMessage: 'Daily notifications are delivered at {dailyTime}. Weekly notifications are delivered at {weeklyTime}.',
description: 'Notification cadence description',
},
notificationDefaultInfo: {
id: 'notification.default.info',
defaultMessage: 'Notifications for certain activities are enabled by default, as detailed here',
description: 'Default notification info',
},
notificationDropdownlabel: {
id: 'notification.dropdown.label',
defaultMessage: 'Select notifications for',
description: 'Dropdown label',
},
notificationDropdownApplies: {
id: 'notification.dropdown.applies',
defaultMessage: 'Applies to all courses',
description: 'Dropdown applies to all courses',
},
notificationCourseDropdownApplies: {
id: 'notification.dropdown.course.applies',
defaultMessage: 'Overrides account-wide settings',
description: 'Dropdown applies to specific course',
},
});
export default messages;