Compare commits

...

35 Commits

Author SHA1 Message Date
sundasnoreen12
42342c2b18 fix: fixed issue to update email cadence for account notification type 2024-11-22 13:26:04 +05:00
sundasnoreen12
5a5de29dea test: added update account preference test case 2024-11-22 01:51:35 +05:00
sundasnoreen12
5381719477 fix: fixed test cases and label 2024-11-20 23:52:19 +05:00
sundasnoreen12
235dd9c462 feat: added api for account notification type 2024-11-20 23:36:14 +05:00
sundasnoreen12
301797aca3 fix: fixed test cases 2024-11-19 16:15:51 +05:00
Awais Ansari
27c686fbe3 feat: added notification preferences settings at account level 2024-11-14 13:40:29 +05:00
ayesha waris
45bcfddab6 chore: rebase with master (#1158)
* fix: fixed support urls (#1155)

* fix(deps): update dependency @edx/frontend-component-header to v5.7.1 (#1156)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update dependency @openedx/frontend-slot-footer to v1.0.6 (#1157)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix: fixed certificates url

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-13 14:57:12 +05:00
Awais Ansari
45b0734e83 Merge pull request #1154 from openedx/aansari/rebase-with-master
chore: rebase with master
2024-11-06 12:40:26 +05:00
Awais Ansari
8db7c54119 Merge branch 'master' of github.com:openedx/frontend-app-account into aansari/rebase-with-master 2024-11-05 17:48:34 +05:00
renovate[bot]
7ec1226965 fix(deps): update dependency @openedx/frontend-plugin-framework to v1.4.0 2024-11-04 06:24:25 +00:00
renovate[bot]
5b00275371 fix(deps): update dependency @edx/frontend-component-header to v5.7.0 2024-11-04 06:24:10 +00:00
renovate[bot]
30c1158775 fix(deps): update dependency universal-cookie to v7.2.2 2024-11-04 05:09:20 +00:00
renovate[bot]
c196b60b39 chore(deps): update dependency @testing-library/jest-dom to v6.6.3 2024-11-04 05:09:08 +00:00
Bilal Qamar
3022a267b1 test: Remove support for Node 18 (#1112) 2024-10-31 15:14:19 -04:00
renovate[bot]
19aacdfaf9 chore(deps): update dependency redux-mock-store to v1.5.5 2024-10-28 04:23:38 +00:00
Muhammad Adeel Tajamul
f04fddd8c0 Merge pull request #1146 from openedx/inf-1620-rebase
Rebase 2u-main with master
2024-10-25 06:19:42 +05:00
renovate[bot]
530cca7c59 chore(deps): update dependency @edx/reactifex to v2 (#1148)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-24 17:11:49 +05:00
Bilal Qamar
e66d77cdd1 chore: update TensorFlow packages to v4.22.0 (#1147) 2024-10-24 16:30:42 +05:00
renovate[bot]
30333edfa2 fix(deps): update dependency universal-cookie to v7 (#1138)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-24 16:27:36 +05:00
muhammadadeeltajamul
84de80eb7f chore: rebase 2u-main with master 2024-10-24 07:43:34 +05:00
Muhammad Adeel Tajamul
538d85b5dd feat: removed never from cadence options (#1142) 2024-10-23 13:28:58 +05:00
Brian Smith
8066ae58f1 feat(deps): update header to 5.6.0 (#1144) 2024-10-22 19:18:39 -04:00
Adolfo R. Brandes
8ce7362fd8 Merge pull request #1139 from eduNEXT/bav/fix-incorrect-post-accept
fix: send json accept headers for POST requests
2024-10-22 12:40:46 -03:00
Alison Langston
7618004054 Merge pull request #1143 from openedx/master
Merge latest changes to 2u-main
2024-10-22 09:18:34 -04:00
Bilal Qamar
9c6f247f0a test: updated failing snapshots 2024-10-22 11:13:43 +00:00
renovate[bot]
085bc19313 fix(deps): update dependency @openedx/paragon to v22.9.0 2024-10-22 11:13:43 +00:00
Alison Langston
ee7c769c2f Merge pull request #1141 from openedx/alangsto/update_idv_failure_message
feat: remove learn more about IDV button
2024-10-21 15:10:15 -04:00
Alie Langston
a37be2dac1 feat: remove learn more about IDV button 2024-10-21 14:58:15 -04:00
renovate[bot]
d0fdc2ac5d chore(deps): update dependency @testing-library/jest-dom to v6.6.2 2024-10-21 07:10:59 +00:00
Bryann Valderrama
8a4eecf6c2 fix: add missing trailing comma 2024-10-16 10:33:37 -05:00
Maria Grimaldi
e2e4003b44 fix: send json accept headers for POST requests 2024-10-15 16:44:42 -05:00
renovate[bot]
434f114220 fix(deps): update react-router monorepo to v6.27.0 2024-10-14 05:00:08 +00:00
Awais Ansari
3b2cf2717e Merge pull request #1135 from openedx/aansari/rebase-with-master
chore: rebase 2u-main with master
2024-10-10 17:03:57 +05:00
Awais Ansari
dbb374860d chore: rebase 2u-main with master 2024-10-10 15:34:25 +05:00
Awais Ansari
aeff548b82 feat: added country disabling feature (#1116)
* feat: added country disabling feature

* refactor: removed isDisabledCountry additional call
2024-10-01 22:11:10 +05:00
38 changed files with 558 additions and 430 deletions

2
.env
View File

@@ -32,4 +32,4 @@ APP_ID=
MFE_CONFIG_API_URL=
PASSWORD_RESET_SUPPORT_LINK=''
LEARNER_FEEDBACK_URL=''
SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT='https://support.edx.org/hc/en-us/articles/207206067'
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'

View File

@@ -28,9 +28,9 @@ ENABLE_COPPA_COMPLIANCE=''
ENABLE_ACCOUNT_DELETION=''
ENABLE_DOB_UPDATE=''
MARKETING_EMAILS_OPT_IN=''
SHOW_EMAIL_CHANNEL=''
SHOW_EMAIL_CHANNEL='true'
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://support.edx.org/hc/en-us/articles/207206067'
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'

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://support.edx.org/hc/en-us/articles/207206067'
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'

View File

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

196
package-lock.json generated
View File

@@ -10,7 +10,7 @@
"license": "AGPL-3.0",
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-component-header": "5.5.0",
"@edx/frontend-component-header": "^5.6.0",
"@edx/frontend-platform": "8.1.2",
"@edx/openedx-atlas": "^0.6.0",
"@fortawesome/fontawesome-svg-core": "^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.0.0",
"@tensorflow-models/blazeface": "0.0.7",
"@tensorflow/tfjs-converter": "3.21.0",
"@tensorflow/tfjs-core": "3.21.0",
"@openedx/paragon": "22.9.0",
"@tensorflow-models/blazeface": "0.1.0",
"@tensorflow/tfjs-converter": "4.22.0",
"@tensorflow/tfjs-core": "4.22.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": "4.0.4"
"universal-cookie": "7.2.2"
},
"devDependencies": {
"@edx/browserslist-config": "1.2.0",
"@edx/reactifex": "1.1.0",
"@edx/reactifex": "2.2.0",
"@openedx/frontend-build": "14.1.5",
"@testing-library/jest-dom": "6.5.0",
"@testing-library/jest-dom": "6.6.3",
"@testing-library/react": "12.1.5",
"react-test-renderer": "17.0.2",
"reactifex": "1.1.1",
"redux-mock-store": "1.5.4"
"redux-mock-store": "1.5.5"
}
},
"node_modules/@adobe/css-tools": {
@@ -2010,9 +2010,9 @@
}
},
"node_modules/@edx/frontend-component-header": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-component-header/-/frontend-component-header-5.5.0.tgz",
"integrity": "sha512-nvpuTIx1o27LUGJ17HpOOm+PX0gg/YumWUMsrl1gSjbrTksyDLOxS1Y4thqlrWzD+dAgk5YKMowZM2BLiRDguQ==",
"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==",
"license": "AGPL-3.0",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "6.6.0",
@@ -2033,7 +2033,8 @@
"@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-dom": "^16.9.0 || ^17.0.0",
"react-router-dom": "^6.14.2"
}
},
"node_modules/@edx/frontend-platform": {
@@ -2088,12 +2089,31 @@
"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",
@@ -2111,7 +2131,9 @@
}
},
"node_modules/@edx/reactifex": {
"version": "1.1.0",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@edx/reactifex/-/reactifex-2.2.0.tgz",
"integrity": "sha512-vyGDtx3BwCr6Gjbm4y6gJ8Bzc2TOSNBlBa2hMerz59HoXaot14MihxxiDU+JDNybGLLcKDBiK511bOi/77i1lw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2124,6 +2146,8 @@
},
"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": {
@@ -3310,9 +3334,9 @@
}
},
"node_modules/@openedx/frontend-plugin-framework": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@openedx/frontend-plugin-framework/-/frontend-plugin-framework-1.3.0.tgz",
"integrity": "sha512-qLtX/4HIuWXiIhBdtBuL6mPVbV2un0rsFYx3I5+3tIUf7+T7WRq81a6JHU5QGyAmZy9dfiv7QwbqwiEQOVXVuQ==",
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@openedx/frontend-plugin-framework/-/frontend-plugin-framework-1.4.0.tgz",
"integrity": "sha512-cwwjvGjYnzQdyhw2PuMYfaZoZGclXTzYfR3NjZ8bSLkj0f7lRB64nMy6WUmBjTB6hjaD4vxMs0zahrU4IM7EWQ==",
"license": "AGPL-3.0",
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
@@ -3343,9 +3367,9 @@
}
},
"node_modules/@openedx/frontend-slot-footer": {
"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==",
"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==",
"license": "AGPL-3.0",
"dependencies": {
"@openedx/frontend-plugin-framework": "^1.1.2"
@@ -3356,7 +3380,9 @@
}
},
"node_modules/@openedx/paragon": {
"version": "22.0.0",
"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==",
"license": "Apache-2.0",
"workspaces": [
"example",
@@ -3407,6 +3433,8 @@
},
"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"
@@ -3418,6 +3446,8 @@
},
"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"
@@ -3425,6 +3455,9 @@
},
"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",
@@ -3442,6 +3475,8 @@
},
"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"
@@ -3553,9 +3588,9 @@
"license": "MIT"
},
"node_modules/@remix-run/router": {
"version": "1.19.2",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.2.tgz",
"integrity": "sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA==",
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.20.0.tgz",
"integrity": "sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
@@ -3826,29 +3861,31 @@
}
},
"node_modules/@tensorflow-models/blazeface": {
"version": "0.0.7",
"license": "Apache-2.0",
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@tensorflow-models/blazeface/-/blazeface-0.1.0.tgz",
"integrity": "sha512-Qc5Wii8/OE5beC7XfehkhF9SEFLaPbVKnxxalV0T9JXsUynXqvLommc9Eko7b8zXKy4SJ1BtVlcX2cmCzQrn9A==",
"peerDependencies": {
"@tensorflow/tfjs-converter": "^3.1.0",
"@tensorflow/tfjs-core": "^3.1.0"
"@tensorflow/tfjs-converter": "^4.10.0",
"@tensorflow/tfjs-core": "^4.10.0"
}
},
"node_modules/@tensorflow/tfjs-converter": {
"version": "3.21.0",
"license": "Apache-2.0",
"version": "4.22.0",
"resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-4.22.0.tgz",
"integrity": "sha512-PT43MGlnzIo+YfbsjM79Lxk9lOq6uUwZuCc8rrp0hfpLjF6Jv8jS84u2jFb+WpUeuF4K33ZDNx8CjiYrGQ2trQ==",
"peerDependencies": {
"@tensorflow/tfjs-core": "3.21.0"
"@tensorflow/tfjs-core": "4.22.0"
}
},
"node_modules/@tensorflow/tfjs-core": {
"version": "3.21.0",
"license": "Apache-2.0",
"version": "4.22.0",
"resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-4.22.0.tgz",
"integrity": "sha512-LEkOyzbknKFoWUwfkr59vSB68DMJ4cjwwHgicXN0DUi3a0Vh1Er3JQqCI1Hl86GGZQvY8ezVrtDIvqR1ZFW55A==",
"dependencies": {
"@types/long": "^4.0.1",
"@types/offscreencanvas": "~2019.3.0",
"@types/offscreencanvas": "~2019.7.0",
"@types/seedrandom": "^2.4.28",
"@types/webgl-ext": "0.0.30",
"@webgpu/types": "0.1.16",
"@webgpu/types": "0.1.38",
"long": "4.0.0",
"node-fetch": "~2.6.1",
"seedrandom": "^3.0.5"
@@ -3859,7 +3896,8 @@
},
"node_modules/@tensorflow/tfjs-core/node_modules/long": {
"version": "4.0.0",
"license": "Apache-2.0"
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
},
"node_modules/@testing-library/dom": {
"version": "8.20.1",
@@ -3893,9 +3931,9 @@
"license": "MIT"
},
"node_modules/@testing-library/jest-dom": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.5.0.tgz",
"integrity": "sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==",
"version": "6.6.3",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz",
"integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4194,7 +4232,8 @@
},
"node_modules/@types/long": {
"version": "4.0.2",
"license": "MIT"
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
"integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA=="
},
"node_modules/@types/mime": {
"version": "1.3.5",
@@ -4219,8 +4258,9 @@
}
},
"node_modules/@types/offscreencanvas": {
"version": "2019.3.0",
"license": "MIT"
"version": "2019.7.3",
"resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz",
"integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A=="
},
"node_modules/@types/parse-json": {
"version": "4.0.2",
@@ -4286,7 +4326,8 @@
},
"node_modules/@types/seedrandom": {
"version": "2.4.34",
"license": "MIT"
"resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-2.4.34.tgz",
"integrity": "sha512-ytDiArvrn/3Xk6/vtylys5tlY6eo7Ane0hvcx++TKo6RxQXuVfW0AF/oeWqAj9dN29SyhtawuXstgmPlwNcv/A=="
},
"node_modules/@types/semver": {
"version": "7.5.8",
@@ -4335,10 +4376,6 @@
"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",
@@ -4686,8 +4723,9 @@
}
},
"node_modules/@webgpu/types": {
"version": "0.1.16",
"license": "BSD-3-Clause"
"version": "0.1.38",
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.38.tgz",
"integrity": "sha512-7LrhVKz2PRh+DD7+S+PVaFd5HxaWQvoMqBbsV9fNJO1pjUs1P8bM2vQVNfk+3URTqbuTI7gkXi0rfsN0IadoBA=="
},
"node_modules/@webpack-cli/configtest": {
"version": "2.1.1",
@@ -11633,7 +11671,8 @@
},
"node_modules/node-fetch": {
"version": "2.6.13",
"license": "MIT",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.13.tgz",
"integrity": "sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
@@ -11651,15 +11690,18 @@
},
"node_modules/node-fetch/node_modules/tr46": {
"version": "0.0.3",
"license": "MIT"
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/node-fetch/node_modules/webidl-conversions": {
"version": "3.0.1",
"license": "BSD-2-Clause"
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/node-fetch/node_modules/whatwg-url": {
"version": "5.0.0",
"license": "MIT",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
@@ -13603,12 +13645,12 @@
}
},
"node_modules/react-router": {
"version": "6.26.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.2.tgz",
"integrity": "sha512-tvN1iuT03kHgOFnLPfLJ8V95eijteveqdOSk+srqfePtQvqCExB8eHOYnlilbOcyJyKnYkr1vJvf7YqotAJu1A==",
"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==",
"license": "MIT",
"dependencies": {
"@remix-run/router": "1.19.2"
"@remix-run/router": "1.20.0"
},
"engines": {
"node": ">=14.0.0"
@@ -13618,13 +13660,13 @@
}
},
"node_modules/react-router-dom": {
"version": "6.26.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.2.tgz",
"integrity": "sha512-z7YkaEW0Dy35T3/QKPYB1LjMK2R1fxnHO8kWpUMTBdfVzZrWOiY9a7CtN8HqdWtDUWd5FY6Dl8HFsqVwH4uOtQ==",
"version": "6.27.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.27.0.tgz",
"integrity": "sha512-+bvtFWMC0DgAFrfKXKG9Fc+BcXWRUO1aJIihbB79xaeq0v5UzfvnM5houGUm1Y461WVRcgAQ+Clh5rdb1eCx4g==",
"license": "MIT",
"dependencies": {
"@remix-run/router": "1.19.2",
"react-router": "6.26.2"
"@remix-run/router": "1.20.0",
"react-router": "6.27.0"
},
"engines": {
"node": ">=14.0.0"
@@ -13821,11 +13863,16 @@
}
},
"node_modules/redux-mock-store": {
"version": "1.5.4",
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/redux-mock-store/-/redux-mock-store-1.5.5.tgz",
"integrity": "sha512-YxX+ofKUTQkZE4HbhYG4kKGr7oCTJfB0GLy7bSeqx86GLpGirrbUWstMnqXkqHNaQpcnbMGbof2dYs5KsPE6Zg==",
"dev": true,
"license": "MIT",
"dependencies": {
"lodash.isplainobject": "^4.0.6"
},
"peerDependencies": {
"redux": "*"
}
},
"node_modules/redux-saga": {
@@ -14281,7 +14328,8 @@
},
"node_modules/seedrandom": {
"version": "3.0.5",
"license": "MIT"
"resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz",
"integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg=="
},
"node_modules/select-hose": {
"version": "2.0.0",
@@ -15733,15 +15781,25 @@
}
},
"node_modules/universal-cookie": {
"version": "4.0.4",
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-7.2.2.tgz",
"integrity": "sha512-fMiOcS3TmzP2x5QV26pIH3mvhexLIT0HmPa3V7Q7knRfT9HG6kTwq02HZGLPw0sAOXrAmotElGRvTLCMbJsvxQ==",
"license": "MIT",
"dependencies": {
"@types/cookie": "^0.3.3",
"cookie": "^0.4.0"
"@types/cookie": "^0.6.0",
"cookie": "^0.7.2"
}
},
"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.4.2",
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"

View File

@@ -29,7 +29,7 @@
],
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-component-header": "5.5.0",
"@edx/frontend-component-header": "^5.6.0",
"@edx/frontend-platform": "8.1.2",
"@edx/openedx-atlas": "^0.6.0",
"@fortawesome/fontawesome-svg-core": "^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.0.0",
"@tensorflow-models/blazeface": "0.0.7",
"@tensorflow/tfjs-converter": "3.21.0",
"@tensorflow/tfjs-core": "3.21.0",
"@openedx/paragon": "22.9.0",
"@tensorflow-models/blazeface": "0.1.0",
"@tensorflow/tfjs-converter": "4.22.0",
"@tensorflow/tfjs-core": "4.22.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": "4.0.4"
"universal-cookie": "7.2.2"
},
"devDependencies": {
"@edx/browserslist-config": "1.2.0",
"@edx/reactifex": "1.1.0",
"@edx/reactifex": "2.2.0",
"@openedx/frontend-build": "14.1.5",
"@testing-library/jest-dom": "6.5.0",
"@testing-library/jest-dom": "6.6.3",
"@testing-library/react": "12.1.5",
"react-test-renderer": "17.0.2",
"reactifex": "1.1.1",
"redux-mock-store": "1.5.4"
"redux-mock-store": "1.5.5"
}
}

View File

@@ -14,7 +14,7 @@ import {
getLanguageList,
} from '@edx/frontend-platform/i18n';
import {
Button, Hyperlink, Icon, Alert,
Hyperlink, Icon, Alert,
} from '@openedx/paragon';
import { CheckCircle, Error, WarningFilled } from '@openedx/paragon/icons';
@@ -50,6 +50,7 @@ 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 {
@@ -120,7 +121,15 @@ class AccountSettingsPage extends React.Component {
countryOptions: [{
value: '',
label: this.props.intl.formatMessage(messages['account.settings.field.country.options.empty']),
}].concat(getCountryList(locale).map(({ code, name }) => ({ value: code, label: name }))),
}].concat(
this.removeDisabledCountries(
getCountryList(locale).map(({ code, name }) => ({
value: code,
label: name,
disabled: this.isDisabledCountry(code),
})),
),
),
stateOptions: [{
value: '',
label: this.props.intl.formatMessage(messages['account.settings.field.state.options.empty']),
@@ -147,11 +156,28 @@ 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 = {};
@@ -193,6 +219,11 @@ class AccountSettingsPage extends React.Component {
}
};
isDisabledCountry = (country) => {
const { disabledCountries } = this.props;
return disabledCountries.includes(country);
};
isEditable(fieldName) {
return !this.props.staticFields.includes(fieldName);
}
@@ -311,19 +342,9 @@ 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-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>
</>
<div className="d-flex flex-row">
{this.props.intl.formatMessage(messages['account.settings.field.name.verified.failure.message'])}
</div>
)
}
/>
@@ -476,7 +497,8 @@ 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 showState = this.props.formValues.country === COUNTRY_WITH_STATES;
const { country } = this.props.formValues;
const showState = country === COUNTRY_WITH_STATES && !this.isDisabledCountry(country);
const { verifiedName } = this.props;
const hasWorkExperience = !!this.props.formValues?.extended_profile?.find(field => field.field_name === 'work_experience');
@@ -706,7 +728,7 @@ class AccountSettingsPage extends React.Component {
{...editableFieldProps}
/>
</div>
<div className="account-section pt-3 mb-5" id="social-media">
<div className="account-section pt-3" id="social-media">
<h2 className="section-heading h4 mb-3">
{this.props.intl.formatMessage(messages['account.settings.section.social.media'])}
</h2>
@@ -742,8 +764,10 @@ class AccountSettingsPage extends React.Component {
{...editableFieldProps}
/>
</div>
<div className="account-section pt-3 mb-5" id="site-preferences" ref={this.navLinkRefs['#site-preferences']}>
<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']}>
<h2 className="section-heading h4 mb-3">
{this.props.intl.formatMessage(messages['account.settings.section.site.preferences'])}
</h2>
@@ -880,6 +904,7 @@ AccountSettingsPage.propTypes = {
name: PropTypes.string,
useVerifiedNameForCerts: PropTypes.bool,
verified_name: PropTypes.string,
country: PropTypes.string,
}),
drafts: PropTypes.shape({}),
formErrors: PropTypes.shape({
@@ -938,6 +963,7 @@ AccountSettingsPage.propTypes = {
),
navigate: PropTypes.func.isRequired,
location: PropTypes.string.isRequired,
disabledCountries: PropTypes.arrayOf(PropTypes.string),
};
AccountSettingsPage.defaultProps = {
@@ -947,6 +973,7 @@ AccountSettingsPage.defaultProps = {
committedValues: {
useVerifiedNameForCerts: false,
verified_name: null,
country: '',
},
drafts: {},
formErrors: {},
@@ -963,6 +990,7 @@ AccountSettingsPage.defaultProps = {
verifiedName: null,
mostRecentVerifiedName: {},
verifiedNameHistory: [],
disabledCountries: [],
};
export default withLocation(withNavigate(connect(accountSettingsPageSelector, {

View File

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

View File

@@ -1,21 +1,16 @@
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { breakpoints, useWindowSize, Icon } from '@openedx/paragon';
import { OpenInNew } from '@openedx/paragon/icons';
import { breakpoints, useWindowSize } from '@openedx/paragon';
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 })}>
@@ -65,21 +60,6 @@ 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,6 +39,7 @@ export const defaultState = {
verifiedName: null,
mostRecentVerifiedName: {},
verifiedNameHistory: {},
disabledCountries: ['RU'],
};
const reducer = (state = defaultState, action = {}) => {

View File

@@ -206,6 +206,11 @@ const activeAccountSelector = createSelector(
accountSettings => accountSettings.values.is_active,
);
const disabledCountriesSelector = createSelector(
accountSettingsSelector,
accountSettings => accountSettings.disabledCountries,
);
export const siteLanguageSelector = createSelector(
previousSiteLanguageSelector,
draftsSelector,
@@ -237,6 +242,7 @@ export const accountSettingsPageSelector = createSelector(
mostRecentApprovedVerifiedNameValueSelector,
mostRecentVerifiedNameSelector,
sortedVerifiedNameHistorySelector,
disabledCountriesSelector,
(
accountSettings,
siteLanguageOptions,
@@ -254,6 +260,7 @@ export const accountSettingsPageSelector = createSelector(
verifiedName,
mostRecentVerifiedName,
verifiedNameHistory,
disabledCountries,
) => ({
siteLanguageOptions,
siteLanguage,
@@ -274,6 +281,7 @@ 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://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings">
<Hyperlink destination="https://help.edx.org/edxlearner/s/topic/0TOQq0000001UdZOAU/account-basics">
{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://support.edx.org/hc/en-us/sections/115004173027-Receive-and-Share-edX-Certificates"
destination="https://help.edx.org/edxlearner/s/topic/0TOQq0000001UVVOA2/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__alert-modal"
className="pgn__modal pgn__modal-md pgn__modal-default pgn__modal-visible-overflow 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__alert-modal"
className="pgn__modal pgn__modal-md pgn__modal-default pgn__modal-visible-overflow 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://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings"
href="https://help.edx.org/edxlearner/s/topic/0TOQq0000001UdZOAU/account-basics"
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://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings"
href="https://help.edx.org/edxlearner/s/topic/0TOQq0000001UdZOAU/account-basics"
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://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings"
href="https://help.edx.org/edxlearner/s/topic/0TOQq0000001UdZOAU/account-basics"
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://support.edx.org/hc/en-us/articles/207206067"
href="https://help.edx.org/edxlearner/s/article/How-do-I-link-or-unlink-my-edX-account-to-a-social-media-account"
onClick={[Function]}
target="_self"
>

View File

@@ -23,10 +23,15 @@ 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(`${getConfig().LMS_BASE_URL}/i18n/setlang/`, formData, {
headers: { 'X-Requested-With': 'XMLHttpRequest' },
});
.post(url, formData, requestConfig);
}

View File

@@ -71,6 +71,16 @@ 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: false,
showPreferences: true,
courses: {
status: 'success',
courses: [],
@@ -98,7 +98,7 @@ const mockData = {
preferences: {
status: 'idle',
updatePreferenceStatus: 'idle',
selectedCourse: null,
selectedCourse: 'account',
preferences: [],
apps: [],
nonEditable: {},

View File

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

16
src/divider/Divider.jsx Normal file
View File

@@ -0,0 +1,16 @@
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;

2
src/divider/index.jsx Normal file
View File

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

View File

@@ -20,8 +20,6 @@ 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(
@@ -38,8 +36,6 @@ 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,6 +131,20 @@ $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

@@ -1,85 +0,0 @@
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

@@ -1,97 +0,0 @@
/* 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

@@ -0,0 +1,73 @@
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,27 +11,22 @@ import { useIsOnMobile } from '../hooks';
import NotificationTypes from './NotificationTypes';
import { notificationChannels, shouldHideAppPreferences } from './data/utils';
import NotificationPreferenceColumn from './NotificationPreferenceColumn';
import { selectPreferenceAppToggleValue, selectSelectedCourseId, selectAppPreferences } from './data/selectors';
import { selectPreferenceAppToggleValue, 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-5': !mobileView && appToggle })}
className={classNames({ 'mb-4.5': !mobileView && appToggle })}
>
<Collapsible.Trigger>
<div className="d-flex align-items-center">

View File

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

View File

@@ -1,35 +1,26 @@
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 {
Container, Hyperlink, Icon, Spinner, NavItem,
} from '@openedx/paragon';
import { Spinner, NavItem } from '@openedx/paragon';
import { useIsOnMobile } from '../hooks';
import messages from './messages';
import { NotFoundPage } from '../account-settings';
import NotificationPreferenceApp from './NotificationPreferenceApp';
import { fetchCourseList, fetchCourseNotificationPreferences } from './data/thunks';
import { fetchCourseNotificationPreferences } from './data/thunks';
import { LOADING_STATUS } from '../constants';
import {
FAILURE_STATUS, IDLE_STATUS, LOADING_STATUS, SUCCESS_STATUS,
} from '../constants';
import {
selectCourse, selectCourseList, selectCourseListStatus, selectNotificationPreferencesStatus, selectPreferenceAppsId,
selectCourseListStatus, selectNotificationPreferencesStatus, selectPreferenceAppsId, selectSelectedCourseId,
} from './data/selectors';
import { notificationChannels } from './data/utils';
const NotificationPreferences = () => {
const { courseId } = useParams();
const dispatch = useDispatch();
const intl = useIntl();
const courseStatus = useSelector(selectCourseListStatus());
const coursesList = useSelector(selectCourseList());
const course = useSelector(selectCourse(courseId));
const courseId = useSelector(selectSelectedCourseId());
const notificationStatus = useSelector(selectNotificationPreferencesStatus());
const preferenceAppsIds = useSelector(selectPreferenceAppsId());
const mobileView = useIsOnMobile();
@@ -43,46 +34,16 @@ const NotificationPreferences = () => {
), [preferenceAppsIds]);
useEffect(() => {
if ([IDLE_STATUS, FAILURE_STATUS].includes(courseStatus)) {
dispatch(fetchCourseList());
}
dispatch(fetchCourseNotificationPreferences(courseId));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [courseId]);
}, [courseId, dispatch]);
if (
(courseStatus === SUCCESS_STATUS && coursesList.length === 0)
|| (notificationStatus === FAILURE_STATUS && coursesList.length !== 0)
) {
return <NotFoundPage />;
if (preferenceAppsIds.length === 0) {
return null;
}
return (
<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="h-100">
{!mobileView && !isLoading && (
<div className="d-flex flex-row justify-content-between float-right">
<div className="d-flex">
{Object.values(NOTIFICATION_CHANNELS).map((channel) => (
@@ -103,21 +64,20 @@ 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>
</Container>
)}
{preferencesList}
{isLoading && (
<div className="d-flex">
<Spinner
variant="primary"
animation="border"
className="mx-auto my-auto"
size="lg"
data-testid="loading-spinner"
/>
</div>
)}
</div>
);
};

View File

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

View File

@@ -0,0 +1,52 @@
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,8 +10,10 @@ export const Actions = {
UPDATE_APP_PREFERENCE: 'updateAppValue',
};
export const fetchNotificationPreferenceSuccess = (courseId, payload) => dispatch => (
dispatch({ type: Actions.FETCHED_PREFERENCES, courseId, payload })
export const fetchNotificationPreferenceSuccess = (courseId, payload, isAccountPreference) => dispatch => (
dispatch({
type: Actions.FETCHED_PREFERENCES, courseId, payload, isAccountPreference,
})
);
export const fetchNotificationPreferenceFetching = () => dispatch => (

View File

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

View File

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

View File

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

View File

@@ -32,3 +32,22 @@ 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,4 +1,5 @@
import { camelCaseObject } from '@edx/frontend-platform';
import camelCase from 'lodash.camelcase';
import EMAIL_CADENCE from './constants';
import {
fetchCourseListSuccess,
@@ -14,6 +15,7 @@ import {
getCourseList,
getCourseNotificationPreferences,
patchPreferenceToggle,
postPreferenceToggle,
} from './service';
const normalizeCourses = (responseData) => {
@@ -36,8 +38,29 @@ const normalizeCourses = (responseData) => {
};
};
const normalizePreferences = (responseData) => {
const preferences = responseData.notificationPreferenceConfig;
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 appKeys = Object.keys(preferences);
const apps = appKeys.map((appId) => ({
@@ -92,7 +115,7 @@ export const fetchCourseNotificationPreferences = (courseId) => (
dispatch(updateSelectedCourse(courseId));
dispatch(fetchNotificationPreferenceFetching());
const data = await getCourseNotificationPreferences(courseId);
const normalizedData = normalizePreferences(camelCaseObject(data));
const normalizedData = normalizePreferences(camelCaseObject(data), courseId);
dispatch(fetchNotificationPreferenceSuccess(courseId, normalizedData));
} catch (errors) {
dispatch(fetchNotificationPreferenceFailed());
@@ -100,12 +123,19 @@ 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 {
@@ -115,15 +145,27 @@ export const updatePreferenceToggle = (
notificationChannel,
!value,
));
const data = await patchPreferenceToggle(
courseId,
notificationApp,
notificationType,
notificationChannel,
value,
);
const normalizedData = normalizePreferences(camelCaseObject(data));
dispatch(fetchNotificationPreferenceSuccess(courseId, normalizedData));
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));
}
} catch (errors) {
dispatch(updatePreferenceValue(
notificationApp,

View File

@@ -90,6 +90,36 @@ 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;