Compare commits

..

79 Commits

Author SHA1 Message Date
Hassan Raza
987484d205 feat: Add notification for discussion app 2025-06-17 14:47:36 +05:00
renovate[bot]
95db89a9dd chore(deps): update dependency @openedx/frontend-build to v14.6.0 (#1244)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-05 05:28:19 +00:00
renovate[bot]
81a878a658 fix(deps): update dependency @edx/frontend-component-footer to v14.7.1 (#1243)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-05 05:28:06 +00:00
edX requirements bot
b502de846a chore: update browserslist DB (#1242)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-05-05 00:37:13 +00:00
renovate[bot]
39f0123820 fix(deps): update dependency @edx/openedx-atlas to ^0.7.0 (#1240)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-28 02:05:08 -04:00
renovate[bot]
7ee70193c0 fix(deps): update dependency @edx/frontend-component-footer to v14.7.0 (#1239)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-28 06:05:00 +00:00
Awais Ansari
0e1574dba7 chore: updated NODE_ENV string in .env (#1238) 2025-04-25 15:21:20 +05:00
Brian Smith
0d45ae6599 feat: import FooterSlot from component package instead of slot package (#1230) 2025-04-24 12:18:00 -04:00
Brian Smith
78246cf26b feat: standardize slot ids (#1237) 2025-04-24 07:28:24 -04:00
renovate[bot]
ca193563ec fix(deps): update dependency @edx/frontend-component-header to v6.4.0 (#1236)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-23 16:13:09 -04:00
Awais Ansari
d0efd35e66 Revert "fix: removed extra api call and strict mode (#1234)" (#1235)
This reverts commit 375b704eef.
2025-04-23 15:42:58 -04:00
sundasnoreen12
375b704eef fix: removed extra api call and strict mode (#1234)
* fix: removed extra api call and strict mode

* fix: added default values
2025-04-23 22:13:06 +05:00
Hassan Raza
ae121358db fix: Update ORA notification display title (#1233) 2025-04-23 16:09:29 +05:00
renovate[bot]
92b7c58af7 fix(deps): update dependency long to v5.3.2 (#1232)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-21 05:46:58 +00:00
renovate[bot]
a4097fe6fc fix(deps): update dependency @openedx/frontend-slot-footer to v1.2.1 (#1231)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-21 05:46:49 +00:00
Awais Ansari
397f688300 fix: scrolling issue for active menu item 2025-04-16 15:29:09 +05:00
Awais Ansari
8bd4b1b9a8 chore: updated package-lock file 2025-04-16 15:29:09 +05:00
Awais Ansari
54d029c181 test: updated test case snapshots 2025-04-16 15:29:09 +05:00
Hassan Raza
e6a4636147 fix: Update course preference title for ora submission (#1222) 2025-04-16 15:29:09 +05:00
sundasnoreen12
efb4162926 test: added test cases 2025-04-16 15:29:09 +05:00
sundasnoreen12
6061232e10 refactor: refactor code 2025-04-16 15:29:09 +05:00
sundasnoreen12
ba6b8c8f9b refactor: refactor code 2025-04-16 15:29:09 +05:00
sundasnoreen12
9c16ba0075 fix: fixed course level preference issue 2025-04-16 15:29:09 +05:00
sundasnoreen12
02b987909b fix: fixed varaible name 2025-04-16 15:29:09 +05:00
sundasnoreen12
1bcc54bb05 fix: added changes for restricted country 2025-04-16 15:29:09 +05:00
sundasnoreen12
5a5b0b905b fix: removed unused selector 2025-04-16 15:29:09 +05:00
sundasnoreen12
44ed49c7d2 refactor: refactored code 2025-04-16 15:29:09 +05:00
sundasnoreen12
386baa3840 fix: changed frequency from never to daily on email preference change 2025-04-16 15:29:09 +05:00
Awais Ansari
f1a56ad6bc fix: updated notifications section url (#1185)
* fix: updated notifiations section url

* fix: updated test cases
2025-04-16 15:29:09 +05:00
Awais Ansari
465bb9f7a0 feat: added notification preferences settings at account level (#1159)
* feat: added notification preferences settings at account level

* fix: fixed test cases

* feat: added api for account notification type

* fix: fixed test cases and label

* test: added update account preference test case

* fix: fixed issue to update email cadence for account notification type

* refactor: updated time

* fix: fixed mixed cadence issue

* fix: fixed border issue when no preferences

* refactor: refactor code

---------

Co-authored-by: sundasnoreen12 <sundasnoreen12@gmail.com>
2025-04-16 15:29:09 +05:00
Muhammad Adeel Tajamul
f9b7525d44 refactor: moved unable to delete into component (#1177) 2025-04-16 15:29:09 +05:00
Muhammad Adeel Tajamul
c7e82295c2 feat: added feature to hide delete button for countries (#1176) 2025-04-16 15:29:09 +05:00
sundasnoreen12
e02cf28b54 fix: rebase with 2u 2025-04-16 15:29:09 +05:00
Awais Ansari
18c51e8e73 fix: translation and console errors (#1166) 2025-04-16 15:29:09 +05:00
ayesha waris
88b444e796 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>
2025-04-16 15:29:09 +05:00
Awais Ansari
71635b33b6 feat: added country disabling feature (#1116)
* feat: added country disabling feature

* refactor: removed isDisabledCountry additional call
2025-04-16 15:29:09 +05:00
Hunia Fatima
515890d5ef feat: upgrade react to v18 (#1220) 2025-04-04 11:29:11 -04:00
renovate[bot]
c2960e1232 chore(deps): update dependency @openedx/frontend-build to v14.4.1 (#1223)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-31 06:44:07 +00:00
Brian Smith
17e12f7f87 chore(deps): update @openedx dependencies to versions that support React 18 (#1218)
* chore(deps): update `@openedx` dependencies to versions that support React 18

* fix(tests): update `JumpNav` tests

* update JumpNav tests to use RTL render/screen instead of `react-test-renderer` snapshots
* update test names to reflect "optional information" not appearing in either scenario
  * the only difference in the snapshot ffc39868e9/src/account-settings/test/__snapshots__/JumpNav.test.jsx.snap is the delete account link

* chore(tests): update snapshots

* chore: fix lint
2025-03-25 12:22:13 -04:00
renovate[bot]
ffc39868e9 fix(deps): update react-router monorepo to v6.30.0 (#1217)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 07:32:44 +00:00
renovate[bot]
9ba01af816 fix(deps): update dependency qs to v6.14.0 (#1216)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 07:32:39 +00:00
Sarina Canelake
fc222cc76c Update README with Tutor instructions; update outdated edx.rtd.io links (#1203)
* docs: remove Devstack mentions from README instructions

* docs: Update edx.rtd.io links to point at new homes

* docs: Apply code review suggestions

Co-authored-by: Brian Smith <112954497+brian-smith-tcril@users.noreply.github.com>

---------

Co-authored-by: Brian Smith <112954497+brian-smith-tcril@users.noreply.github.com>
2025-03-21 15:15:04 +00:00
renovate[bot]
99a39568de fix(deps): update dependency @openedx/frontend-plugin-framework to v1.6.0 (#1210)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-17 06:23:54 +00:00
renovate[bot]
9ce3cbfddd fix(deps): update dependency long to v5.3.1 (#1211)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-17 06:23:47 +00:00
renovate[bot]
55a72b3f6e fix(deps): update dependency core-js to v3.41.0 (#1207)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-10 07:26:24 +00:00
renovate[bot]
597004c82e fix(deps): update dependency @openedx/frontend-slot-footer to v1.1.0 (#1206)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-10 07:25:41 +00:00
renovate[bot]
3458b6f410 chore(deps): update dependency @openedx/frontend-build to v14.3.2 (#1204)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-10 05:52:22 +00:00
renovate[bot]
b70b4a796f fix(deps): update dependency @openedx/frontend-plugin-framework to v1.5.0 (#1200)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-03 06:16:01 +00:00
renovate[bot]
d76945d2f2 fix(deps): update dependency @edx/frontend-platform to v8.2.1 (#1199)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-03 06:15:48 +00:00
renovate[bot]
7ee34f7d9a chore(deps): update dependency @openedx/frontend-build to v14.3.1 (#1196)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 06:00:19 +00:00
renovate[bot]
96c619cab8 fix(deps): update dependency @edx/frontend-component-header to v5.8.3 (#1195)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 06:00:08 +00:00
renovate[bot]
9eda5e588c chore(deps): update dependency @edx/browserslist-config to v1.5.0 (#1191)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-17 05:26:57 +00:00
Feanil Patel
3aff0e03e0 Merge pull request #1190 from openedx/feanil-patch-1
docs: Update catalog-info.yaml
2025-02-14 11:12:57 -05:00
Feanil Patel
852358f243 docs: Update catalog-info.yaml
This team was replace/renamed at some point but the catalog file was not updated.
2025-02-14 11:09:12 -05:00
Feanil Patel
5ffd17db4c Merge pull request #1186 from salman2013/salman/update-catalog-info-file
Update catalog-info-file for release data.
2025-01-24 09:40:22 -05:00
renovate[bot]
6f3c9616d6 fix(deps): update dependency long to v5.2.4 (#1188)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-20 05:37:47 +00:00
renovate[bot]
5f4812ed47 fix(deps): update dependency @edx/frontend-platform to v8.1.5 (#1187)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-20 05:33:41 +00:00
salman2013
d694b5c428 chore: update catalog-info-file for release data and remove openedx.yaml 2025-01-14 15:21:34 +05:00
renovate[bot]
6b73130e9b fix(deps): update font awesome to v6.7.2 (#1183)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-06 07:08:39 +00:00
renovate[bot]
d608f3947e fix(deps): update dependency @edx/frontend-platform to v8.1.4 (#1182)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-06 07:08:29 +00:00
renovate[bot]
db83838d5e fix(deps): update dependency @openedx/paragon to v22.13.0 (#1180)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-30 08:07:33 +00:00
renovate[bot]
004cdf35f1 fix(deps): update dependency core-js to v3.39.0 (#1181)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-30 08:07:20 +00:00
renovate[bot]
6cb015e49d chore(deps): update dependency @edx/browserslist-config to v1.4.0 (#1179)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-23 06:14:39 +00:00
renovate[bot]
c5ae2c40d7 fix(deps): update dependency @openedx/frontend-slot-footer to v1.0.7 (#1178)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-23 06:14:32 +00:00
renovate[bot]
46edd0cb70 fix(deps): update dependency @edx/frontend-platform to v8.1.3 (#1175)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-16 06:16:09 +00:00
renovate[bot]
2c6b5c34a9 fix(deps): update dependency @edx/frontend-component-header to v5.8.2 (#1174)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-16 06:15:57 +00:00
renovate[bot]
b000d2e048 fix(deps): update dependency @edx/frontend-component-header to v5.8.1 (#1173)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 05:35:05 +00:00
renovate[bot]
437fba16fe chore(deps): update dependency @openedx/frontend-build to v14.2.2 (#1172)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 05:34:44 +00:00
renovate[bot]
c4038b4085 fix(deps): update dependency @openedx/paragon to v22.10.0 (#1170)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-02 07:12:14 +00:00
renovate[bot]
496ff21015 fix(deps): update dependency @openedx/frontend-plugin-framework to v1.4.1 (#1169)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-02 07:11:53 +00:00
renovate[bot]
23e16ac6e0 fix(deps): update dependency @edx/frontend-component-header to v5.8.0 (#1163)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-27 09:42:18 +00:00
renovate[bot]
d8c5762ee2 chore(deps): update dependency @openedx/frontend-build to v14.2.0 (#1162)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-27 09:42:02 +00:00
sundasnoreen12
4d2d520234 Merge pull request #1165 from openedx/sundas/INF-1655
fix: fixed url break issue
2024-11-27 14:38:48 +05:00
sundasnoreen12
b094722772 fix: fixed url break issue 2024-11-26 17:01:27 +05:00
renovate[bot]
a9f061f10c fix(deps): update dependency qs to v6.13.1 (#1161)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-18 06:51:21 +00:00
renovate[bot]
4eb5d4effc fix(deps): update dependency @edx/frontend-component-header to v5.7.2 (#1160)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-18 06:49:32 +00:00
renovate[bot]
925bcce81c 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>
2024-11-11 07:12:06 +00:00
renovate[bot]
f44e03c7c8 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>
2024-11-11 07:11:39 +00:00
ayesha waris
999988dd88 fix: fixed support urls (#1155) 2024-11-08 14:12:27 +05:00
45 changed files with 6430 additions and 2019 deletions

3
.env
View File

@@ -15,7 +15,7 @@ LOGO_WHITE_URL=''
SHOW_EMAIL_CHANNEL=''
LOGOUT_URL=''
MARKETING_SITE_BASE_URL=''
NODE_ENV=''
NODE_ENV='production'
ORDER_HISTORY_URL=''
PUBLISHER_BASE_URL=''
REFRESH_ACCESS_TOKEN_ENDPOINT=''
@@ -33,3 +33,4 @@ 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'
COUNTRIES_WITH_DELETE_ACCOUNT_DISABLED='[]'

View File

@@ -34,3 +34,4 @@ 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'
COUNTRIES_WITH_DELETE_ACCOUNT_DISABLED='[]'

View File

@@ -31,3 +31,4 @@ 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'
COUNTRIES_WITH_DELETE_ACCOUNT_DISABLED='[]'

View File

@@ -3,3 +3,4 @@ dist/
node_modules/
__mocks__/
__snapshots__/
src/i18n/messages/

View File

@@ -1,5 +1,3 @@
intl_imports = ./node_modules/.bin/intl-imports.js
transifex_utils = ./node_modules/.bin/transifex-utils.js
i18n = ./src/i18n
@@ -19,6 +17,11 @@ test.npm.%: validate-no-uncommitted-package-lock-changes
npm run $(*)
.PHONY: requirements
precommit:
npm run lint
npm audit
requirements: ## install ci requirements
npm ci
@@ -38,6 +41,17 @@ detect_changed_source_translations:
# Checking for changed translations...
git diff --exit-code $(i18n)
# Pushes translations to Transifex. You must run make extract_translations first.
push_translations:
# Pushing strings to Transifex...
tx push -s
# Fetching hashes from Transifex...
./node_modules/@edx/reactifex/bash_scripts/get_hashed_strings_v3.sh
# Writing out comments to file...
$(transifex_utils) $(transifex_temp) --comments --v3-scripts-path
# Pushing comments to Transifex...
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
pull_translations:
rm -rf src/i18n/messages
mkdir src/i18n/messages

View File

@@ -25,40 +25,13 @@ Getting Started
Prerequisites
=============
The `devstack`_ is currently recommended as a development environment for your
new MFE. If you start it with ``make dev.up.lms`` that should give you
everything you need as a companion to this frontend.
Note that it is also possible to use `Tutor`_ to develop an MFE. You can refer
`Tutor`_ is currently recommended as a development environment for your
new MFE. Please refer
to the `relevant tutor-mfe documentation`_ to get started using it.
.. _Devstack: https://github.com/openedx/devstack
.. _Tutor: https://github.com/overhangio/tutor
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe#mfe-development
Installation
============
This MFE is bundled with `Devstack <https://github.com/openedx/devstack>`_, see the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ section for setup instructions.
1. Install Devstack using the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ instructions.
2. Start up Devstack, if it's not already started.
3. Log in to Devstack (http://localhost:18000/login )
4. Within this project, install requirements and start the development server:
.. code-block::
npm install
npm start # The server will run on port 1997
5. Once the dev server is up, visit http://localhost:1997 to access the MFE
.. image:: ./docs/images/localhost_preview.png
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development
Plugins
=======
@@ -69,7 +42,7 @@ The parts of this MFE that can be customized in that manner are documented `here
Environment Variables/Setup Notes
=================================
This MFE is configured via environment variables supplied at build time. All micro-frontends have a shared set of required environment variables, as documented in the Open edX Developer Guide under `Required Environment Variables <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`__.
This MFE is configured via the ``frontend-platform`` configuration module. For more information on MFE configuration see the `Configuration documentation`_.
The account settings micro-frontend also supports the following additional variable:
@@ -102,8 +75,9 @@ Example build syntax with a single environment variable:
NODE_ENV=development ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload' npm run build
For more information see the document: `Micro-frontend applications in Open
edX <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`__.
For more information see the document: `Configuration documentation`_
.. _Configuration documentation: https://openedx.github.io/frontend-platform/module-Config.html
Cloning and Startup
===================

View File

@@ -12,7 +12,8 @@ metadata:
icon: "Web"
annotations:
openedx.org/arch-interest-groups: ""
openedx.org/release: "master"
spec:
owner: group:edx-infinity
owner: group:2u-infinity
type: 'website'
lifecycle: 'production'

View File

@@ -1,6 +0,0 @@
# This file describes this Open edX repo, as described in OEP-2:
# https://open-edx-proposals.readthedocs.io/en/latest/oep-0002-bp-repo-metadata.html#specification
nick: acct
oeps: {}
openedx-release: {ref: master}

7372
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -29,23 +29,23 @@
],
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-component-header": "^5.6.0",
"@edx/frontend-platform": "8.1.2",
"@edx/openedx-atlas": "^0.6.0",
"@edx/frontend-component-footer": "^14.6.0",
"@edx/frontend-component-header": "^6.2.0",
"@edx/frontend-platform": "^8.3.3",
"@edx/openedx-atlas": "^0.7.0",
"@fortawesome/fontawesome-svg-core": "^6.6.0",
"@fortawesome/free-brands-svg-icons": "^6.6.0",
"@fortawesome/free-regular-svg-icons": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.6.0",
"@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",
"@openedx/frontend-plugin-framework": "^1.7.0",
"@openedx/paragon": "^22.16.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",
"core-js": "3.41.0",
"font-awesome": "4.7.0",
"form-urlencoded": "6.1.5",
"formdata-polyfill": "4.0.10",
@@ -60,12 +60,12 @@
"lodash.pick": "4.4.0",
"lodash.pickby": "4.6.0",
"lodash.snakecase": "4.1.1",
"long": "5.2.3",
"long": "5.3.2",
"memoize-one": "^6.0.0",
"prop-types": "15.8.1",
"qs": "6.13.0",
"react": "17.0.2",
"react-dom": "17.0.2",
"qs": "6.14.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-helmet": "6.1.0",
"react-redux": "7.2.9",
"react-router": "^6.25.1",
@@ -83,12 +83,12 @@
"universal-cookie": "7.2.2"
},
"devDependencies": {
"@edx/browserslist-config": "1.2.0",
"@edx/browserslist-config": "1.5.0",
"@edx/reactifex": "2.2.0",
"@openedx/frontend-build": "14.1.5",
"@openedx/frontend-build": "^14.3.3",
"@testing-library/jest-dom": "6.6.3",
"@testing-library/react": "12.1.5",
"react-test-renderer": "17.0.2",
"@testing-library/react": "14.3.1",
"react-test-renderer": "^18.3.1",
"reactifex": "1.1.1",
"redux-mock-store": "1.5.5"
}

View File

@@ -47,6 +47,7 @@ import {
COPPA_COMPLIANCE_YEAR,
WORK_EXPERIENCE_OPTIONS,
getStatesList,
FIELD_LABELS,
} from './data/constants';
import { fetchSiteLanguages } from './site-language';
import { fetchCourseList } from '../notification-preferences/data/thunks';
@@ -66,6 +67,7 @@ class AccountSettingsPage extends React.Component {
'#basic-information': React.createRef(),
'#profile-information': React.createRef(),
'#social-media': React.createRef(),
'#notifications': React.createRef(),
'#site-preferences': React.createRef(),
'#linked-accounts': React.createRef(),
'#delete-account': React.createRef(),
@@ -156,17 +158,19 @@ class AccountSettingsPage extends React.Component {
})),
}));
removeDisabledCountries = (countryList) => {
const { disabledCountries, committedValues } = this.props;
canDeleteAccount = () => {
const { committedValues } = this.props;
return !getConfig().COUNTRIES_WITH_DELETE_ACCOUNT_DISABLED.includes(committedValues.country);
};
if (!disabledCountries.length) {
removeDisabledCountries = (countryList) => {
const { countriesCodesList, committedValues } = this.props;
const committedCountry = committedValues?.country;
if (!countriesCodesList.length) {
return countryList;
}
return countryList.filter(({ value, disabled }) => {
const isUserCountry = value === committedValues.country;
return !disabled || isUserCountry;
});
return countryList.filter(({ value }) => value === committedCountry || countriesCodesList.find(x => x === value));
};
handleEditableFieldChange = (name, value) => {
@@ -174,7 +178,7 @@ class AccountSettingsPage extends React.Component {
};
handleSubmit = (formId, values) => {
if (formId === 'country' && this.isDisabledCountry(values)) {
if (formId === FIELD_LABELS.COUNTRY && this.isDisabledCountry(values)) {
return;
}
@@ -220,8 +224,9 @@ class AccountSettingsPage extends React.Component {
};
isDisabledCountry = (country) => {
const { disabledCountries } = this.props;
return disabledCountries.includes(country);
const { countriesCodesList } = this.props;
return countriesCodesList.length > 0 && !countriesCodesList.find(x => x === country);
};
isEditable(fieldName) {
@@ -728,7 +733,7 @@ class AccountSettingsPage extends React.Component {
{...editableFieldProps}
/>
</div>
<div className="account-section pt-3" id="social-media">
<div className="account-section pt-3 mb-6" id="social-media">
<h2 className="section-heading h4 mb-3">
{this.props.intl.formatMessage(messages['account.settings.section.social.media'])}
</h2>
@@ -764,9 +769,10 @@ 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="border border-light-700" />
<div className="mt-6" id="notifications" ref={this.navLinkRefs['#notifications']}>
<NotificationSettings />
</div>
<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'])}
@@ -809,16 +815,15 @@ class AccountSettingsPage extends React.Component {
<ThirdPartyAuth />
</div>
{getConfig().ENABLE_ACCOUNT_DELETION
&& (
{getConfig().ENABLE_ACCOUNT_DELETION && (
<div className="account-section pt-3 mb-5" id="delete-account" ref={this.navLinkRefs['#delete-account']}>
<DeleteAccount
isVerifiedAccount={this.props.isActive}
hasLinkedTPA={hasLinkedTPA}
canDeleteAccount={this.canDeleteAccount()}
/>
</div>
)}
)}
</>
);
}
@@ -883,12 +888,15 @@ AccountSettingsPage.propTypes = {
name: PropTypes.string,
email: PropTypes.string,
secondary_email: PropTypes.string,
secondary_email_enabled: PropTypes.bool,
secondary_email_enabled: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
year_of_birth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
country: PropTypes.string,
level_of_education: PropTypes.string,
gender: PropTypes.string,
extended_profile: PropTypes.string,
extended_profile: PropTypes.arrayOf(PropTypes.shape({
field_name: PropTypes.string,
field_value: PropTypes.string,
})),
language_proficiencies: PropTypes.string,
pending_name_change: PropTypes.string,
phone_number: PropTypes.string,
@@ -941,9 +949,12 @@ AccountSettingsPage.propTypes = {
tpaProviders: PropTypes.arrayOf(PropTypes.shape({
connected: PropTypes.bool,
})),
nameChangeModal: PropTypes.shape({
formId: PropTypes.string,
}),
nameChangeModal: PropTypes.oneOfType([
PropTypes.shape({
formId: PropTypes.string,
}),
PropTypes.bool,
]),
verifiedName: PropTypes.shape({
verified_name: PropTypes.string,
status: PropTypes.string,
@@ -963,7 +974,12 @@ AccountSettingsPage.propTypes = {
),
navigate: PropTypes.func.isRequired,
location: PropTypes.string.isRequired,
disabledCountries: PropTypes.arrayOf(PropTypes.string),
countriesCodesList: PropTypes.arrayOf(
PropTypes.shape({
value: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
}),
),
};
AccountSettingsPage.defaultProps = {
@@ -986,11 +1002,11 @@ AccountSettingsPage.defaultProps = {
tpaProviders: [],
isActive: true,
secondary_email_enabled: false,
nameChangeModal: {},
nameChangeModal: {} || false,
verifiedName: null,
mostRecentVerifiedName: {},
verifiedNameHistory: [],
disabledCountries: [],
countriesCodesList: [],
};
export default withLocation(withNavigate(connect(accountSettingsPageSelector, {

View File

@@ -19,12 +19,14 @@ const JumpNav = ({
'basic-information',
'profile-information',
'social-media',
'notifications',
'site-preferences',
'linked-accounts',
'delete-account',
]}
className="list-unstyled"
currentClassName="font-weight-bold"
offset={-64}
>
<li>
<NavHashLink to="#basic-information">
@@ -41,6 +43,11 @@ const JumpNav = ({
{intl.formatMessage(messages['account.settings.section.social.media'])}
</NavHashLink>
</li>
<li>
<NavHashLink to="#notifications">
{intl.formatMessage(messages['notification.preferences.notifications.label'])}
</NavHashLink>
</li>
<li>
<NavHashLink to="#site-preferences">
{intl.formatMessage(messages['account.settings.section.site.preferences'])}

View File

@@ -1,6 +1,5 @@
/* eslint-disable no-import-assign */
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { BrowserRouter as Router } from 'react-router-dom';
import configureStore from 'redux-mock-store';
@@ -13,8 +12,11 @@ import {
import * as auth from '@edx/frontend-platform/auth';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
// Modal creates a portal. Overriding ReactDOM.createPortal allows portals to be tested in jest.
ReactDOM.createPortal = node => node;
// Modal creates a portal. Overriding createPortal allows portals to be tested in jest.
jest.mock('react-dom', () => ({
...jest.requireActual('react-dom'),
createPortal: jest.fn(node => node), // Mock portal behavior
}));
import CertificatePreference from '../CertificatePreference'; // eslint-disable-line import/first

View File

@@ -27,6 +27,7 @@ export const fetchSettingsSuccess = ({
profileDataManager,
timeZones,
verifiedNameHistory,
countriesCodesList,
}) => ({
type: FETCH_SETTINGS.SUCCESS,
payload: {
@@ -35,6 +36,7 @@ export const fetchSettingsSuccess = ({
profileDataManager,
timeZones,
verifiedNameHistory,
countriesCodesList,
},
});

View File

@@ -132,6 +132,10 @@ export function getStatesList(country) {
return country && COUNTRY_STATES_MAP[country.toUpperCase()];
}
export const FIELD_LABELS = {
COUNTRY: 'country',
};
export const DECLINED = 'declined';
export const SELF_DESCRIBE = 'self-describe';
export const OTHER = 'other';

View File

@@ -39,7 +39,7 @@ export const defaultState = {
verifiedName: null,
mostRecentVerifiedName: {},
verifiedNameHistory: {},
disabledCountries: ['RU'],
countriesCodesList: [],
};
const reducer = (state = defaultState, action = {}) => {
@@ -65,6 +65,7 @@ const reducer = (state = defaultState, action = {}) => {
loaded: true,
loadingError: null,
verifiedNameHistory: action.payload.verifiedNameHistory,
countriesCodesList: action.payload.countriesCodesList,
};
case FETCH_SETTINGS.FAILURE:
return {

View File

@@ -53,7 +53,7 @@ export function* handleFetchSettings() {
const { username, userId, roles: userRoles } = getAuthenticatedUser();
const {
thirdPartyAuthProviders, profileDataManager, timeZones, ...values
thirdPartyAuthProviders, profileDataManager, timeZones, countries, ...values
} = yield call(
getSettings,
username,
@@ -71,6 +71,7 @@ export function* handleFetchSettings() {
profileDataManager,
timeZones,
verifiedNameHistory,
countriesCodesList: countries,
}));
} catch (e) {
yield put(fetchSettingsFailure(e.message));

View File

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

View File

@@ -1,5 +1,6 @@
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { logError } from '@edx/frontend-platform/logging';
import pick from 'lodash.pick';
import omit from 'lodash.omit';
import isEmpty from 'lodash.isempty';
@@ -7,6 +8,7 @@ import isEmpty from 'lodash.isempty';
import { handleRequestError, unpackFieldErrors } from './utils';
import { getThirdPartyAuthProviders } from '../third-party-auth';
import { postVerifiedNameConfig } from '../certificate-preference/data/service';
import { FIELD_LABELS } from './constants';
const SOCIAL_PLATFORMS = [
{ id: 'twitter', key: 'social_link_twitter' },
@@ -186,6 +188,24 @@ export async function postVerifiedName(data) {
.catch(error => handleRequestError(error));
}
function extractCountryList(data) {
return data?.fields
.find(({ name }) => name === FIELD_LABELS.COUNTRY)
?.options?.map(({ value }) => (value)) || [];
}
export async function getCountryList() {
const url = `${getConfig().LMS_BASE_URL}/user_api/v1/account/registration/`;
try {
const { data } = await getAuthenticatedHttpClient().get(url);
return extractCountryList(data);
} catch (e) {
logError(e);
return [];
}
}
/**
* A single function to GET everything considered a setting. Currently encapsulates Account, Preferences, and
* ThirdPartyAuth.
@@ -197,12 +217,14 @@ export async function getSettings(username, userRoles) {
thirdPartyAuthProviders,
profileDataManager,
timeZones,
countries,
] = await Promise.all([
getAccount(username),
getPreferences(username),
getThirdPartyAuthProviders(),
getProfileDataManager(username, userRoles),
getTimeZones(),
getCountryList(),
]);
return {
@@ -211,6 +233,7 @@ export async function getSettings(username, userRoles) {
thirdPartyAuthProviders,
profileDataManager,
timeZones,
countries,
};
}

View File

@@ -1,9 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom';
import renderer from 'react-test-renderer';
import { IntlProvider, injectIntl, createIntl } from '@edx/frontend-platform/i18n';
ReactDOM.createPortal = node => node;
jest.mock('react-dom', () => ({
...jest.requireActual('react-dom'),
createPortal: jest.fn(node => node), // Mock portal behavior
}));
import BeforeProceedingBanner from './BeforeProceedingBanner'; // eslint-disable-line import/first

View File

@@ -1,10 +1,12 @@
import React from 'react';
import ReactDOM from 'react-dom';
import renderer from 'react-test-renderer';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
// Modal creates a portal. Overriding ReactDOM.createPortal allows portals to be tested in jest.
ReactDOM.createPortal = node => node;
// Modal creates a portal. Overriding createPortal allows portals to be tested in jest.
jest.mock('react-dom', () => ({
...jest.requireActual('react-dom'),
createPortal: jest.fn(node => node), // Mock portal behavior
}));
import { ConfirmationModal } from './ConfirmationModal'; // eslint-disable-line import/first

View File

@@ -76,67 +76,74 @@ export class DeleteAccount extends React.Component {
<h2 className="section-heading h4 mb-3">
{intl.formatMessage(messages['account.settings.delete.account.header'])}
</h2>
<p>{intl.formatMessage(messages['account.settings.delete.account.subheader'])}</p>
<p>
{intl.formatMessage(
messages['account.settings.delete.account.text.1'],
{ siteName: getConfig().SITE_NAME },
)}
</p>
<p>
{intl.formatMessage(
messages[deleteAccountText2MessageKey],
{ siteName: getConfig().SITE_NAME },
)}
</p>
<p>
<PrintingInstructions />
</p>
<p className="text-danger h6">
{intl.formatMessage(
messages['account.settings.delete.account.text.warning'],
{ siteName: getConfig().SITE_NAME },
)}
</p>
<p>
<Hyperlink destination="https://help.edx.org/edxlearner/s/topic/0TOQq0000001UdZOAU/account-basics">
{intl.formatMessage(messages['account.settings.delete.account.text.change.instead'])}
</Hyperlink>
</p>
<p>
<Button
variant="outline-danger"
onClick={canDelete ? this.props.deleteAccountConfirmation : null}
disabled={!canDelete}
>
{intl.formatMessage(messages['account.settings.delete.account.button'])}
</Button>
</p>
{
this.props.canDeleteAccount ? (
<>
<p>{intl.formatMessage(messages['account.settings.delete.account.subheader'])}</p>
<p>
{intl.formatMessage(
messages['account.settings.delete.account.text.1'],
{ siteName: getConfig().SITE_NAME },
)}
</p>
<p>
{intl.formatMessage(
messages[deleteAccountText2MessageKey],
{ siteName: getConfig().SITE_NAME },
)}
</p>
<p>
<PrintingInstructions />
</p>
<p className="text-danger h6">
{intl.formatMessage(
messages['account.settings.delete.account.text.warning'],
{ siteName: getConfig().SITE_NAME },
)}
</p>
<p>
<Hyperlink destination="https://help.edx.org/edxlearner/s/topic/0TOQq0000001UdZOAU/account-basics">
{intl.formatMessage(messages['account.settings.delete.account.text.change.instead'])}
</Hyperlink>
</p>
<p>
<Button
variant="outline-danger"
onClick={canDelete ? this.props.deleteAccountConfirmation : null}
disabled={!canDelete}
>
{intl.formatMessage(messages['account.settings.delete.account.button'])}
</Button>
</p>
{isVerifiedAccount ? null : (
<BeforeProceedingBanner
instructionMessageId={optInInstructionMessageId}
supportArticleUrl="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-confirm-my-email"
/>
)}
{hasLinkedTPA ? (
<BeforeProceedingBanner
instructionMessageId="account.settings.delete.account.please.unlink"
supportArticleUrl={supportArticleUrl}
/>
) : null}
{isVerifiedAccount ? null : (
<BeforeProceedingBanner
instructionMessageId={optInInstructionMessageId}
supportArticleUrl="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-confirm-my-email"
/>
)}
<ConnectedConfirmationModal
status={status}
errorType={errorType}
onSubmit={this.handleSubmit}
onCancel={this.handleCancel}
onChange={this.handlePasswordChange}
password={this.state.password}
/>
{hasLinkedTPA ? (
<BeforeProceedingBanner
instructionMessageId="account.settings.delete.account.please.unlink"
supportArticleUrl={supportArticleUrl}
/>
) : null}
<ConnectedSuccessModal status={status} onClose={this.handleFinalClose} />
</>
) : (
<p>{intl.formatMessage(messages['account.settings.cannot.delete.account.text'])}</p>
)
}
<ConnectedConfirmationModal
status={status}
errorType={errorType}
onSubmit={this.handleSubmit}
onCancel={this.handleCancel}
onChange={this.handlePasswordChange}
password={this.state.password}
/>
<ConnectedSuccessModal status={status} onClose={this.handleFinalClose} />
</div>
);
}
@@ -152,6 +159,7 @@ DeleteAccount.propTypes = {
errorType: PropTypes.oneOf(['empty-password', 'server']),
hasLinkedTPA: PropTypes.bool,
isVerifiedAccount: PropTypes.bool,
canDeleteAccount: PropTypes.bool,
intl: intlShape.isRequired,
};
@@ -160,6 +168,7 @@ DeleteAccount.defaultProps = {
isVerifiedAccount: true,
status: null,
errorType: null,
canDeleteAccount: true,
};
// Assume we're part of the accountSettings state.

View File

@@ -1,12 +1,14 @@
import React from 'react';
import ReactDOM from 'react-dom';
import renderer from 'react-test-renderer';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
import { waitFor } from '@testing-library/react';
import { SuccessModal } from './SuccessModal';
// Modal creates a portal. Overriding ReactDOM.createPortal allows portals to be tested in jest.
ReactDOM.createPortal = node => node;
import { SuccessModal } from './SuccessModal'; // eslint-disable-line import/first
// Modal creates a portal. Overriding createPortal allows portals to be tested in jest.
jest.mock('react-dom', () => ({
...jest.requireActual('react-dom'),
createPortal: jest.fn(node => node), // Mock portal behavior
}));
const IntlSuccessModal = injectIntl(SuccessModal);
@@ -20,39 +22,40 @@ describe('SuccessModal', () => {
};
});
it('should match default closed success modal snapshot', () => {
let tree = renderer.create((
<IntlProvider locale="en"><IntlSuccessModal {...props} /></IntlProvider>))
.toJSON();
expect(tree).toMatchSnapshot();
tree = renderer.create((
<IntlProvider locale="en"><IntlSuccessModal {...props} status="confirming" /></IntlProvider>))
.toJSON();
expect(tree).toMatchSnapshot();
tree = renderer.create((
<IntlProvider locale="en"><IntlSuccessModal {...props} status="pending" /></IntlProvider>))
.toJSON();
expect(tree).toMatchSnapshot();
tree = renderer.create((
<IntlProvider locale="en"><IntlSuccessModal {...props} status="failed" /></IntlProvider>))
.toJSON();
expect(tree).toMatchSnapshot();
it('should match default closed success modal snapshot', async () => {
await waitFor(() => {
const tree = renderer.create((
<IntlProvider locale="en"><IntlSuccessModal {...props} /></IntlProvider>)).toJSON();
expect(tree).toMatchSnapshot();
});
await waitFor(() => {
const tree = renderer.create((
<IntlProvider locale="en"><IntlSuccessModal {...props} status="confirming" /></IntlProvider>)).toJSON();
expect(tree).toMatchSnapshot();
});
await waitFor(() => {
const tree = renderer.create((
<IntlProvider locale="en"><IntlSuccessModal {...props} status="pending" /></IntlProvider>)).toJSON();
expect(tree).toMatchSnapshot();
});
await waitFor(() => {
const tree = renderer.create((
<IntlProvider locale="en"><IntlSuccessModal {...props} status="failed" /></IntlProvider>)).toJSON();
expect(tree).toMatchSnapshot();
});
});
it('should match open success modal snapshot', () => {
const tree = renderer
.create((
it('should match open success modal snapshot', async () => {
await waitFor(() => {
const tree = renderer.create(
<IntlProvider locale="en">
<IntlSuccessModal
{...props}
status="deleted" // This will cause 'modal-backdrop' and 'show' to appear on the modal as CSS classes.
status="deleted"
/>
</IntlProvider>
))
.toJSON();
expect(tree).toMatchSnapshot();
</IntlProvider>,
).toJSON();
expect(tree).toMatchSnapshot();
});
});
});

View File

@@ -57,7 +57,6 @@ exports[`BeforeProceedingBanner should match the snapshot when SUPPORT_URL_TO_UN
<a
className="pgn__hyperlink default-link standalone-link"
href="http://test-support.edx"
onClick={[Function]}
target="_self"
>
unlink all social media accounts

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

@@ -28,7 +28,6 @@ exports[`DeleteAccount should match default section snapshot 1`] = `
<a
className="pgn__hyperlink default-link standalone-link"
href="https://help.edx.org/edxlearner/s/topic/0TOQq0000001UdZOAU/account-basics"
onClick={[Function]}
target="_self"
>
Want to change your email, name, or password instead?
@@ -75,7 +74,6 @@ exports[`DeleteAccount should match unverified account section snapshot 1`] = `
<a
className="pgn__hyperlink default-link standalone-link"
href="https://help.edx.org/edxlearner/s/topic/0TOQq0000001UdZOAU/account-basics"
onClick={[Function]}
target="_self"
>
Want to change your email, name, or password instead?
@@ -118,7 +116,6 @@ exports[`DeleteAccount should match unverified account section snapshot 1`] = `
<a
className="pgn__hyperlink default-link standalone-link"
href="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-confirm-my-email"
onClick={[Function]}
target="_self"
>
activate your account
@@ -157,7 +154,6 @@ exports[`DeleteAccount should match unverified account section snapshot 2`] = `
<a
className="pgn__hyperlink default-link standalone-link"
href="https://help.edx.org/edxlearner/s/topic/0TOQq0000001UdZOAU/account-basics"
onClick={[Function]}
target="_self"
>
Want to change your email, name, or password instead?
@@ -200,7 +196,6 @@ exports[`DeleteAccount should match unverified account section snapshot 2`] = `
<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"
onClick={[Function]}
target="_self"
>
unlink all social media accounts

View File

@@ -1,6 +1,11 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'account.settings.cannot.delete.account.text': {
id: 'account.settings.cannot.delete.account.text',
defaultMessage: 'Please note that, for legal and regulatory compliance purposes, account deletion is currently unavailable.',
description: 'This text is visible when user is not allowed to delete account',
},
'account.settings.delete.account.header': {
id: 'account.settings.delete.account.header',
defaultMessage: 'Delete My Account',

View File

@@ -1,6 +1,5 @@
/* eslint-disable no-import-assign */
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { BrowserRouter as Router } from 'react-router-dom';
import configureStore from 'redux-mock-store';
@@ -13,8 +12,11 @@ import {
import * as auth from '@edx/frontend-platform/auth';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
// Modal creates a portal. Overriding ReactDOM.createPortal allows portals to be tested in jest.
ReactDOM.createPortal = node => node;
// Modal creates a portal. Overriding createPortal allows portals to be tested in jest.
jest.mock('react-dom', () => ({
...jest.requireActual('react-dom'),
createPortal: jest.fn(node => node), // Mock portal behavior
}));
import NameChange from '../NameChange'; // eslint-disable-line import/first

View File

@@ -1,5 +1,5 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { render, screen } from '@testing-library/react';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
import { initializeMockApp, mergeConfig, setConfig } from '@edx/frontend-platform';
@@ -37,24 +37,23 @@ describe('JumpNav', () => {
});
});
it('should not render Optional Information or delete account link', () => {
it('should not render delete account link', async () => {
setConfig({
ENABLE_ACCOUNT_DELETION: false,
});
const tree = renderer.create((
render(
<IntlProvider locale="en">
<AppProvider store={store}>
<IntlJumpNav {...props} />
</AppProvider>
</IntlProvider>
))
.toJSON();
</IntlProvider>,
);
expect(tree).toMatchSnapshot();
expect(await screen.queryByText('Delete My Account')).toBeNull();
});
it('should render Optional Information and delete account link', () => {
it('should render delete account link', async () => {
setConfig({
ENABLE_ACCOUNT_DELETION: true,
});
@@ -63,15 +62,14 @@ describe('JumpNav', () => {
...props,
};
const tree = renderer.create((
render(
<IntlProvider locale="en">
<AppProvider store={store}>
<IntlJumpNav {...props} />
</AppProvider>
</IntlProvider>
))
.toJSON();
</IntlProvider>,
);
expect(tree).toMatchSnapshot();
expect(await screen.findByText('Delete My Account')).toBeVisible();
});
});

View File

@@ -172,9 +172,7 @@ exports[`EditableSelectField renders EditableSelectField correctly with editing
/>
</svg>
</span>
<div>
</div>
<div />
</div>
</div>
<p>

View File

@@ -1,184 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`JumpNav should not render Optional Information or delete account link 1`] = `
<div
data-testid="redux-provider"
>
<div
data-testid="browser-router"
>
<div
className="jump-nav px-2.25 jump-nav-sm position-sticky pt-3"
>
<ul
className="list-unstyled"
style={{}}
>
<li
className=""
>
<a
aria-current="page"
className="active"
href="/#basic-information"
isActive={[Function]}
onClick={[Function]}
>
Account Information
</a>
</li>
<li
className=""
>
<a
aria-current="page"
className="active"
href="/#profile-information"
isActive={[Function]}
onClick={[Function]}
>
Profile Information
</a>
</li>
<li
className=""
>
<a
aria-current="page"
className="active"
href="/#social-media"
isActive={[Function]}
onClick={[Function]}
>
Social Media Links
</a>
</li>
<li
className=""
>
<a
aria-current="page"
className="active"
href="/#site-preferences"
isActive={[Function]}
onClick={[Function]}
>
Site Preferences
</a>
</li>
<li
className=""
>
<a
aria-current="page"
className="active"
href="/#linked-accounts"
isActive={[Function]}
onClick={[Function]}
>
Linked Accounts
</a>
</li>
</ul>
</div>
</div>
</div>
`;
exports[`JumpNav should render Optional Information and delete account link 1`] = `
<div
data-testid="redux-provider"
>
<div
data-testid="browser-router"
>
<div
className="jump-nav px-2.25 jump-nav-sm position-sticky pt-3"
>
<ul
className="list-unstyled"
style={{}}
>
<li
className=""
>
<a
aria-current="page"
className="active"
href="/#basic-information"
isActive={[Function]}
onClick={[Function]}
>
Account Information
</a>
</li>
<li
className=""
>
<a
aria-current="page"
className="active"
href="/#profile-information"
isActive={[Function]}
onClick={[Function]}
>
Profile Information
</a>
</li>
<li
className=""
>
<a
aria-current="page"
className="active"
href="/#social-media"
isActive={[Function]}
onClick={[Function]}
>
Social Media Links
</a>
</li>
<li
className=""
>
<a
aria-current="page"
className="active"
href="/#site-preferences"
isActive={[Function]}
onClick={[Function]}
>
Site Preferences
</a>
</li>
<li
className=""
>
<a
aria-current="page"
className="active"
href="/#linked-accounts"
isActive={[Function]}
onClick={[Function]}
>
Linked Accounts
</a>
</li>
<li
className=""
>
<a
aria-current="page"
className="active"
href="/#delete-account"
isActive={[Function]}
onClick={[Function]}
>
Delete My Account
</a>
</li>
</ul>
</div>
</div>
</div>
`;

View File

@@ -7,7 +7,7 @@ import {
render, act, screen, fireEvent,
} from '@testing-library/react';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
import IdVerificationPage from '../IdVerificationPage';
import IdVerificationPageSlot from '../../plugin-slots/IdVerificationPageSlot';
import * as selectors from '../data/selectors';
jest.mock('../data/selectors', () => jest.fn().mockImplementation(() => ({ idVerificationSelector: () => ({}) })));
@@ -47,7 +47,7 @@ jest.mock('../panels/SubmittedPanel', () => function SubmittedPanelMock() {
return <></>;
});
const IntlIdVerificationPage = injectIntl(IdVerificationPage);
const IntlIdVerificationPage = injectIntl(IdVerificationPageSlot);
const mockStore = configureStore();
describe('IdVerificationPage', () => {

View File

@@ -6,12 +6,13 @@ import { AppProvider, ErrorPage } from '@edx/frontend-platform/react';
import {
subscribe, initialize, APP_INIT_ERROR, APP_READY, mergeConfig,
} from '@edx/frontend-platform';
import React from 'react';
import ReactDOM from 'react-dom';
import React, { StrictMode } from 'react';
// eslint-disable-next-line import/no-unresolved
import { createRoot } from 'react-dom/client';
import { Route, Routes, Outlet } from 'react-router-dom';
import Header from '@edx/frontend-component-header';
import FooterSlot from '@openedx/frontend-slot-footer';
import { FooterSlot } from '@edx/frontend-component-footer';
import configureStore from './data/configureStore';
import AccountSettingsPage, { NotFoundPage } from './account-settings';
@@ -21,37 +22,39 @@ import messages from './i18n';
import './index.scss';
import Head from './head/Head';
const rootNode = createRoot(document.getElementById('root'));
subscribe(APP_READY, () => {
ReactDOM.render(
<AppProvider store={configureStore()}>
<Head />
<Routes>
<Route element={(
<div className="d-flex flex-column" style={{ minHeight: '100vh' }}>
<Header />
<main className="flex-grow-1" id="main">
<Outlet />
</main>
<FooterSlot />
</div>
rootNode.render(
<StrictMode>
<AppProvider store={configureStore()}>
<Head />
<Routes>
<Route element={(
<div className="d-flex flex-column" style={{ minHeight: '100vh' }}>
<Header />
<main className="flex-grow-1" id="main">
<Outlet />
</main>
<FooterSlot />
</div>
)}
>
<Route
path="/id-verification/*"
element={<IdVerificationPageSlot />}
/>
<Route path="/" element={<AccountSettingsPage />} />
<Route path="/notfound" element={<NotFoundPage />} />
<Route path="*" element={<NotFoundPage />} />
</Route>
</Routes>
</AppProvider>,
document.getElementById('root'),
>
<Route
path="/id-verification/*"
element={<IdVerificationPageSlot />}
/>
<Route path="/" element={<AccountSettingsPage />} />
<Route path="/notfound" element={<NotFoundPage />} />
<Route path="*" element={<NotFoundPage />} />
</Route>
</Routes>
</AppProvider>
</StrictMode>,
);
});
subscribe(APP_INIT_ERROR, (error) => {
ReactDOM.render(<ErrorPage message={error.message} />, document.getElementById('root'));
rootNode.render(<ErrorPage message={error.message} />);
});
initialize({
@@ -65,6 +68,7 @@ initialize({
SHOW_EMAIL_CHANNEL: process.env.SHOW_EMAIL_CHANNEL || 'false',
ENABLE_COPPA_COMPLIANCE: (process.env.ENABLE_COPPA_COMPLIANCE || false),
ENABLE_ACCOUNT_DELETION: (process.env.ENABLE_ACCOUNT_DELETION !== 'false'),
COUNTRIES_WITH_DELETE_ACCOUNT_DISABLED: JSON.parse(process.env.COUNTRIES_WITH_DELETE_ACCOUNT_DISABLED || '[]'),
ENABLE_DOB_UPDATE: (process.env.ENABLE_DOB_UPDATE || false),
MARKETING_EMAILS_OPT_IN: (process.env.MARKETING_EMAILS_OPT_IN || false),
PASSWORD_RESET_SUPPORT_LINK: process.env.PASSWORD_RESET_SUPPORT_LINK,

View File

@@ -10,7 +10,7 @@ import {
} from '@openedx/paragon';
import messages from './messages';
import EMAIL_CADENCE from './data/constants';
import { EMAIL_CADENCE_PREFERENCES, EMAIL_CADENCE } from './data/constants';
import { selectUpdatePreferencesStatus } from './data/selectors';
import { LOADING_STATUS } from '../constants';
@@ -44,12 +44,12 @@ const EmailCadences = ({
className="bg-white shadow d-flex flex-column margin-left-2"
data-testid="email-cadence-dropdown"
>
{Object.values(EMAIL_CADENCE).map((cadence) => (
{Object.values(EMAIL_CADENCE_PREFERENCES).map((cadence) => (
<Dropdown.Item
key={cadence}
as={Button}
variant="tertiary"
name="email_cadence"
name={EMAIL_CADENCE}
className="d-flex justify-content-start py-1.5 font-size-14 cadence-button"
size="inline"
active={cadence === emailCadence}
@@ -71,7 +71,7 @@ const EmailCadences = ({
EmailCadences.propTypes = {
email: PropTypes.bool.isRequired,
onToggle: PropTypes.func.isRequired,
emailCadence: PropTypes.oneOf(Object.values(EMAIL_CADENCE)).isRequired,
emailCadence: PropTypes.oneOf(Object.values(EMAIL_CADENCE_PREFERENCES)).isRequired,
notificationType: PropTypes.string.isRequired,
};

View File

@@ -15,6 +15,9 @@ import { LOADING_STATUS } from '../constants';
import { updatePreferenceToggle } from './data/thunks';
import { selectAppPreferences, selectSelectedCourseId, selectUpdatePreferencesStatus } from './data/selectors';
import { notificationChannels, shouldHideAppPreferences } from './data/utils';
import {
EMAIL, EMAIL_CADENCE, EMAIL_CADENCE_PREFERENCES, MIXED,
} from './data/constants';
const NotificationPreferenceColumn = ({ appId, channel, appPreference }) => {
const dispatch = useDispatch();
@@ -26,11 +29,34 @@ const NotificationPreferenceColumn = ({ appId, channel, appPreference }) => {
const NOTIFICATION_CHANNELS = Object.values(notificationChannels());
const hideAppPreferences = shouldHideAppPreferences(appPreferences, appId) || false;
const getValue = useCallback((notificationChannel, innerText, checked) => {
if (notificationChannel === EMAIL_CADENCE && courseId) {
return innerText;
}
return checked;
}, [courseId]);
const getEmailCadence = useCallback((notificationChannel, checked, innerText, emailCadence) => {
if (notificationChannel === EMAIL_CADENCE) {
return innerText;
}
if (notificationChannel === EMAIL && checked) {
return EMAIL_CADENCE_PREFERENCES.DAILY;
}
return emailCadence;
}, []);
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 { name: notificationChannel, checked, innerText } = event.target;
const appNotificationPreference = appPreferences.find(preference => preference.id === notificationType);
const value = getValue(notificationChannel, innerText, checked);
const emailCadence = getEmailCadence(
notificationChannel,
checked,
innerText,
appNotificationPreference.emailCadence,
);
dispatch(updatePreferenceToggle(
courseId,
@@ -38,10 +64,9 @@ const NotificationPreferenceColumn = ({ appId, channel, appPreference }) => {
notificationType,
notificationChannel,
value,
emailCadence,
emailCadence !== MIXED ? emailCadence : undefined,
));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appId, appPreferences]);
}, [appPreferences, getValue, getEmailCadence, dispatch, courseId, appId]);
const renderPreference = (preference) => (
(preference?.coreNotificationTypes?.length > 0 || preference.id !== 'core') && (
@@ -63,7 +88,7 @@ const NotificationPreferenceColumn = ({ appId, channel, appPreference }) => {
id={`${preference.id}-${channel}`}
className="my-1"
/>
{channel === 'email' && (
{channel === EMAIL && (
<EmailCadences
email={preference.email}
onToggle={onToggle}

View File

@@ -27,14 +27,14 @@ const NotificationSettings = () => {
</div>
<div className="text-gray-700 font-size-14 mb-3">
{intl.formatMessage(messages.notificationCadenceDescription, {
dailyTime: '12:00',
weeklyTime: '00:00',
dailyTime: '22:00 UTC',
weeklyTime: '22:00 UTC Every Sunday',
})}
</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"
destination="https://edx.readthedocs.io/projects/open-edx-learner-guide/en/latest/sfd_notifications/index.html"
target="_blank"
rel="noopener noreferrer"
className="text-decoration-underline ml-1"
@@ -44,6 +44,7 @@ const NotificationSettings = () => {
</div>
<NotificationCoursesDropdown />
<NotificationPreferences courseId={courseId} />
<div className="border border-light-700 my-6" />
</Container>
)
);

View File

@@ -1,6 +1,18 @@
const EMAIL_CADENCE = {
export const EMAIL_CADENCE_PREFERENCES = {
DAILY: 'Daily',
WEEKLY: 'Weekly',
};
export default EMAIL_CADENCE;
export const EMAIL_CADENCE = 'email_cadence';
export const EMAIL = 'email';
export const MIXED = 'Mixed';
export const RequestStatus = /** @type {const} */ ({
IN_PROGRESS: 'in-progress',
SUCCESSFUL: 'successful',
FAILED: 'failed',
DENIED: 'denied',
PENDING: 'pending',
CLEAR: 'clear',
PARTIAL: 'partial',
PARTIAL_FAILURE: 'partial failure',
NOT_FOUND: 'not-found',
});

View File

@@ -0,0 +1,150 @@
import { updatePreferenceToggle } from './thunks';
import {
updatePreferenceValue,
fetchNotificationPreferenceSuccess,
fetchNotificationPreferenceFailed,
} from './actions';
import { patchPreferenceToggle, postPreferenceToggle } from './service';
import { EMAIL } from './constants';
jest.mock('./service', () => ({
patchPreferenceToggle: jest.fn(),
postPreferenceToggle: jest.fn(),
}));
jest.mock('./actions', () => ({
updatePreferenceValue: jest.fn(),
fetchNotificationPreferenceSuccess: jest.fn(),
fetchNotificationPreferenceFailed: jest.fn(),
}));
describe('updatePreferenceToggle', () => {
const dispatch = jest.fn();
const courseId = 'course-v1:edX+DemoX+2023';
const notificationApp = 'app';
const notificationType = 'type';
const notificationChannel = 'channel';
const value = true;
const emailCadence = 'daily';
const mockData = {
updated_value: false,
notification_type: 'ora_grade_assigned',
channel: 'email',
app: 'grading',
successfully_updated_courses: [
{
course_id: 'course-v1:ACCA+ColSid+1T2024',
current_setting: {
web: false,
push: false,
email: false,
email_cadence: 'Weekly',
},
},
{
course_id: 'course-v1:arbisoft_acca+cs1023+2021_T4',
current_setting: {
web: false,
push: false,
email: false,
email_cadence: 'Weekly',
},
},
],
total_updated: 2,
total_courses: 2,
};
beforeEach(() => {
jest.clearAllMocks();
});
it('should update preference for a course-specific notification', async () => {
patchPreferenceToggle.mockResolvedValue({ data: mockData });
await updatePreferenceToggle(
courseId,
notificationApp,
notificationType,
notificationChannel,
value,
emailCadence,
)(dispatch);
expect(dispatch).toHaveBeenCalledWith(updatePreferenceValue(
notificationApp,
notificationType,
notificationChannel,
!value,
));
expect(patchPreferenceToggle).toHaveBeenCalledWith(
courseId,
notificationApp,
notificationType,
notificationChannel,
value,
);
expect(dispatch).toHaveBeenCalledWith(fetchNotificationPreferenceSuccess(courseId, { data: mockData }, false));
});
it('should update preference globally when courseId is not provided', async () => {
postPreferenceToggle.mockResolvedValue({ data: mockData });
await updatePreferenceToggle(
null,
notificationApp,
notificationType,
notificationChannel,
value,
emailCadence,
)(dispatch);
expect(dispatch).toHaveBeenCalledWith(updatePreferenceValue(
notificationApp,
notificationType,
notificationChannel,
!value,
));
expect(postPreferenceToggle).toHaveBeenCalledWith(
notificationApp,
notificationType,
notificationChannel,
value,
emailCadence,
);
expect(dispatch).toHaveBeenCalledWith(fetchNotificationPreferenceSuccess(null, { data: mockData }, true));
});
it('should handle email preferences separately', async () => {
patchPreferenceToggle.mockResolvedValue({ data: mockData });
await updatePreferenceToggle(courseId, notificationApp, notificationType, EMAIL, value, emailCadence)(dispatch);
expect(patchPreferenceToggle).toHaveBeenCalledWith(
courseId,
notificationApp,
notificationType,
EMAIL,
true,
);
expect(dispatch).toHaveBeenCalledWith(fetchNotificationPreferenceSuccess(courseId, { data: mockData }, false));
});
it('should dispatch fetchNotificationPreferenceFailed on error', async () => {
patchPreferenceToggle.mockRejectedValue(new Error('Network Error'));
await updatePreferenceToggle(
courseId,
notificationApp,
notificationType,
notificationChannel,
value,
emailCadence,
)(dispatch);
expect(dispatch).toHaveBeenCalledWith(updatePreferenceValue(
notificationApp,
notificationType,
notificationChannel,
!value,
));
expect(dispatch).toHaveBeenCalledWith(fetchNotificationPreferenceFailed());
});
});

View File

@@ -1,6 +1,6 @@
import { camelCaseObject } from '@edx/frontend-platform';
import camelCase from 'lodash.camelcase';
import EMAIL_CADENCE from './constants';
import { EMAIL, EMAIL_CADENCE, EMAIL_CADENCE_PREFERENCES } from './constants';
import {
fetchCourseListSuccess,
fetchCourseListFetching,
@@ -79,7 +79,8 @@ const normalizePreferences = (responseData, courseId) => {
push: preferences[appId].notificationTypes[preferenceId].push,
email: preferences[appId].notificationTypes[preferenceId].email,
info: preferences[appId].notificationTypes[preferenceId].info || '',
emailCadence: preferences[appId].notificationTypes[preferenceId].emailCadence || EMAIL_CADENCE.DAILY,
emailCadence: preferences[appId].notificationTypes[preferenceId].emailCadence
|| EMAIL_CADENCE_PREFERENCES.DAILY,
coreNotificationTypes: preferences[appId].coreNotificationTypes || [],
}
));
@@ -139,32 +140,58 @@ export const updatePreferenceToggle = (
) => (
async (dispatch) => {
try {
// Initially update the UI to give immediate feedback
dispatch(updatePreferenceValue(
notificationApp,
notificationType,
notificationChannel,
!value,
));
let data = null;
if (courseId) {
data = await patchPreferenceToggle(
courseId,
// Function to handle data normalization and dispatching success
const handleSuccessResponse = (data, isGlobal = false) => {
const processedData = courseId
? normalizePreferences(camelCaseObject(data), courseId)
: camelCaseObject(data);
dispatch(fetchNotificationPreferenceSuccess(courseId, processedData, isGlobal));
return processedData;
};
// Function to toggle preference based on context (course-specific or global)
const togglePreference = async (channel, toggleValue, cadence) => {
if (courseId) {
return patchPreferenceToggle(
courseId,
notificationApp,
notificationType,
channel,
channel === EMAIL_CADENCE ? cadence : toggleValue,
);
}
return postPreferenceToggle(
notificationApp,
notificationType,
notificationChannel,
value,
channel,
channel === EMAIL_CADENCE ? undefined : toggleValue,
cadence,
);
const normalizedData = normalizePreferences(camelCaseObject(data), courseId);
dispatch(fetchNotificationPreferenceSuccess(courseId, normalizedData));
} else {
data = await postPreferenceToggle(
notificationApp,
notificationType,
notificationChannel,
value,
emailCadence,
};
// Execute the main preference toggle
const data = await togglePreference(notificationChannel, value, emailCadence);
handleSuccessResponse(data, !courseId);
// Handle special case for email notifications
if (notificationChannel === EMAIL && value) {
const emailCadenceData = await togglePreference(
EMAIL_CADENCE,
courseId ? undefined : value,
EMAIL_CADENCE_PREFERENCES.DAILY,
);
dispatch(fetchNotificationPreferenceSuccess(courseId, camelCaseObject(data), true));
handleSuccessResponse(emailCadenceData, !courseId);
}
} catch (errors) {
dispatch(updatePreferenceValue(

View File

@@ -27,8 +27,9 @@ const messages = defineMessages({
newQuestionPost {New question posts}
contentReported {Reported content}
courseUpdates {Course updates}
oraStaffNotification {ORA new submissions}
oraStaffNotifications {New ORA submission for staff grading}
oraGradeAssigned {Essay assignment grade received}
newInstructorAllLearnersPost {New posts from instructors}
other {{text}}
}`,
description: 'Display text for Notification Types',

View File

@@ -1,12 +1,15 @@
# Footer Slot
### Slot ID: `footer_slot`
### Slot ID: `org.openedx.frontend.layout.footer.v1`
### Slot ID Aliases
* `footer_slot`
## Description
This slot is used to replace/modify/hide the footer.
The implementation of the `FooterSlot` component lives in [the `frontend-component-footer` repository](https://github.com/openedx/frontend-component-footer/tree/master/src/components/footer-slot).
The implementation of the `FooterSlot` component lives in [the `frontend-component-footer` repository](https://github.com/openedx/frontend-component-footer/).
## Example
@@ -23,7 +26,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
const config = {
pluginSlots: {
footer_slot: {
'org.openedx.frontend.layout.footer.v1': {
plugins: [
{
// Hide the default footer

View File

@@ -1,6 +1,9 @@
# Footer Slot
# ID Verification Page Slot
### Slot ID: `id_verification_page_plugin`
### Slot ID: `org.openedx.frontend.account.id_verification_page.v1`
### Slot ID Aliases
* `id_verification_page_plugin`
## Description
@@ -19,13 +22,13 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
const config = {
pluginSlots: {
id_verification_page_plugin: {
'org.openedx.frontend.account.id_verification_page.v1': {
plugins: [
{
// Insert a custom IDV Page
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'id_verification_page_plugin',
id: 'custom_id_verification_page',
type: DIRECT_PLUGIN,
RenderWidget: () => (
<div>

View File

@@ -2,7 +2,10 @@ import { PluginSlot } from '@openedx/frontend-plugin-framework';
import IdVerificationPage from '../../id-verification';
const IdVerificationPageSlot = () => (
<PluginSlot id="id_verification_page_plugin">
<PluginSlot
id="org.openedx.frontend.account.id_verification_page.v1"
idAliases={['id_verification_page_plugin']}
>
<IdVerificationPage />
</PluginSlot>
);

View File

@@ -1,4 +1,4 @@
# `frontend-app-account` Plugin Slots
* [`footer_slot`](./FooterSlot/)
* [`id_verification_page_plugin`](./IdVerificationPageSlot/)
* [`org.openedx.frontend.layout.footer.v1`](./FooterSlot/)
* [`org.openedx.frontend.account.id_verification_page.v1`](./IdVerificationPageSlot/)

View File

@@ -13,7 +13,7 @@ describe('MockedPluginSlot', () => {
it('renders as the slot children directly if there is content within', () => {
render(
<div role="article">
<MockedPluginSlot>
<MockedPluginSlot id="test_plugin">
<q role="note">How much wood could a woodchuck chuck if a woodchuck could chuck wood?</q>
</MockedPluginSlot>
</div>,
@@ -21,9 +21,13 @@ describe('MockedPluginSlot', () => {
const component = screen.getByRole('article');
expect(component).toBeInTheDocument();
// Direct children
const quote = component.querySelector(':scope > q');
const slot = component.querySelector('[data-testid="test_plugin"]');
expect(slot).toBeInTheDocument();
expect(slot).toHaveTextContent('PluginSlot_test_plugin');
// Check if the quote is a direct child of the MockedPluginSlot
const quote = slot.querySelector('q');
expect(quote).toBeInTheDocument();
expect(quote).toHaveTextContent('How much wood could a woodchuck chuck if a woodchuck could chuck wood?');
expect(quote.getAttribute('role')).toBe('note');
});