Compare commits
198 Commits
open-relea
...
release/te
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db7c7babd7 | ||
|
|
0e1574dba7 | ||
|
|
0d45ae6599 | ||
|
|
78246cf26b | ||
|
|
ca193563ec | ||
|
|
d0efd35e66 | ||
|
|
375b704eef | ||
|
|
ae121358db | ||
|
|
92b7c58af7 | ||
|
|
a4097fe6fc | ||
|
|
397f688300 | ||
|
|
8bd4b1b9a8 | ||
|
|
54d029c181 | ||
|
|
e6a4636147 | ||
|
|
efb4162926 | ||
|
|
6061232e10 | ||
|
|
ba6b8c8f9b | ||
|
|
9c16ba0075 | ||
|
|
02b987909b | ||
|
|
1bcc54bb05 | ||
|
|
5a5b0b905b | ||
|
|
44ed49c7d2 | ||
|
|
386baa3840 | ||
|
|
f1a56ad6bc | ||
|
|
465bb9f7a0 | ||
|
|
f9b7525d44 | ||
|
|
c7e82295c2 | ||
|
|
e02cf28b54 | ||
|
|
18c51e8e73 | ||
|
|
88b444e796 | ||
|
|
71635b33b6 | ||
|
|
515890d5ef | ||
|
|
c2960e1232 | ||
|
|
17e12f7f87 | ||
|
|
ffc39868e9 | ||
|
|
9ba01af816 | ||
|
|
fc222cc76c | ||
|
|
99a39568de | ||
|
|
9ce3cbfddd | ||
|
|
55a72b3f6e | ||
|
|
597004c82e | ||
|
|
3458b6f410 | ||
|
|
b70b4a796f | ||
|
|
d76945d2f2 | ||
|
|
7ee34f7d9a | ||
|
|
96c619cab8 | ||
|
|
9eda5e588c | ||
|
|
3aff0e03e0 | ||
|
|
852358f243 | ||
|
|
5ffd17db4c | ||
|
|
6f3c9616d6 | ||
|
|
5f4812ed47 | ||
|
|
d694b5c428 | ||
|
|
6b73130e9b | ||
|
|
d608f3947e | ||
|
|
db83838d5e | ||
|
|
004cdf35f1 | ||
|
|
6cb015e49d | ||
|
|
c5ae2c40d7 | ||
|
|
46edd0cb70 | ||
|
|
2c6b5c34a9 | ||
|
|
b000d2e048 | ||
|
|
437fba16fe | ||
|
|
c4038b4085 | ||
|
|
496ff21015 | ||
|
|
23e16ac6e0 | ||
|
|
d8c5762ee2 | ||
|
|
4d2d520234 | ||
|
|
b094722772 | ||
|
|
a9f061f10c | ||
|
|
4eb5d4effc | ||
|
|
925bcce81c | ||
|
|
f44e03c7c8 | ||
|
|
999988dd88 | ||
|
|
7ec1226965 | ||
|
|
5b00275371 | ||
|
|
30c1158775 | ||
|
|
c196b60b39 | ||
|
|
3022a267b1 | ||
|
|
19aacdfaf9 | ||
|
|
530cca7c59 | ||
|
|
e66d77cdd1 | ||
|
|
30333edfa2 | ||
|
|
538d85b5dd | ||
|
|
8066ae58f1 | ||
|
|
8ce7362fd8 | ||
|
|
9c6f247f0a | ||
|
|
085bc19313 | ||
|
|
ee7c769c2f | ||
|
|
a37be2dac1 | ||
|
|
d0fdc2ac5d | ||
|
|
8a4eecf6c2 | ||
|
|
e2e4003b44 | ||
|
|
434f114220 | ||
|
|
c31e1648bf | ||
|
|
379d2df2fc | ||
|
|
5933ae3034 | ||
|
|
d1269197ac | ||
|
|
12438e79f5 | ||
|
|
57317a4bb6 | ||
|
|
3bbc068a73 | ||
|
|
4179aa798a | ||
|
|
a1877bb499 | ||
|
|
cd44f15634 | ||
|
|
83242b1042 | ||
|
|
116c30663b | ||
|
|
0ed92cc7e8 | ||
|
|
3ec887089d | ||
|
|
b46165ede0 | ||
|
|
752d760030 | ||
|
|
3e2c4dc760 | ||
|
|
4136a63e92 | ||
|
|
ca8c9ef4fe | ||
|
|
14f1cfd0e1 | ||
|
|
f81ec9544a | ||
|
|
11d7d4beb4 | ||
|
|
70fc2f9642 | ||
|
|
f1d3c88f76 | ||
|
|
c5c05c86f7 | ||
|
|
b845b34b02 | ||
|
|
27c666cc27 | ||
|
|
dcdd024809 | ||
|
|
dab4b1dc92 | ||
|
|
c40bcd49ea | ||
|
|
8e3de876d0 | ||
|
|
aff083335c | ||
|
|
18ea60b470 | ||
|
|
9260b4834c | ||
|
|
d7a5b222ee | ||
|
|
17ce32b80c | ||
|
|
d4693a2c2e | ||
|
|
a446d5d3fc | ||
|
|
8f4139ad87 | ||
|
|
87e591cd71 | ||
|
|
90c02279e3 | ||
|
|
600a3de7b6 | ||
|
|
4390ca5435 | ||
|
|
5903ad2ae8 | ||
|
|
cc50ad9744 | ||
|
|
4e69301a4e | ||
|
|
3a877eb55c | ||
|
|
673639caee | ||
|
|
474f7c018d | ||
|
|
46398e7edc | ||
|
|
dfb06268ab | ||
|
|
b0301d5028 | ||
|
|
89d848e408 | ||
|
|
56ba73e455 | ||
|
|
c3d30227a3 | ||
|
|
f5bedd7708 | ||
|
|
218c796500 | ||
|
|
604845f4a3 | ||
|
|
751cdb1076 | ||
|
|
0ebc4eb3cd | ||
|
|
8a954c19e2 | ||
|
|
18b999b9a8 | ||
|
|
b155d04814 | ||
|
|
6f715e598f | ||
|
|
ab8d6e7913 | ||
|
|
d2bb164fab | ||
|
|
5aacaf44a3 | ||
|
|
4502dbe413 | ||
|
|
f5e2fa7448 | ||
|
|
5954b3122f | ||
|
|
2bf71cce10 | ||
|
|
8fd0320c88 | ||
|
|
da15a70942 | ||
|
|
52f4c51362 | ||
|
|
a0c2fb2686 | ||
|
|
223f792e2b | ||
|
|
175d9cf5cb | ||
|
|
94ef29938f | ||
|
|
4e38c9e9a6 | ||
|
|
81a1ec7e1e | ||
|
|
90da11782f | ||
|
|
82c9e07f0f | ||
|
|
16fb3a1bb4 | ||
|
|
e623f03c4b | ||
|
|
b2173afb9c | ||
|
|
c787a77f9e | ||
|
|
075587c1f0 | ||
|
|
a0b0b1f8d4 | ||
|
|
16f20cad66 | ||
|
|
40c67995a4 | ||
|
|
0fc68f7d93 | ||
|
|
90d3668128 | ||
|
|
1440c8239c | ||
|
|
bb29f9624e | ||
|
|
746b0fed4b | ||
|
|
02d14b95a7 | ||
|
|
484fc95ea0 | ||
|
|
bdb851eb8b | ||
|
|
b984296550 | ||
|
|
39fca96523 | ||
|
|
d1e817d4ba | ||
|
|
b35042ca97 | ||
|
|
88e9eb3fdf | ||
|
|
4409be4cc6 |
9
.env
9
.env
@@ -1,11 +1,10 @@
|
||||
ACCESS_TOKEN_COOKIE_NAME=''
|
||||
ACCOUNT_PROFILE_URL=''
|
||||
BASE_URL=''
|
||||
CREDENTIALS_BASE_URL=''
|
||||
CSRF_TOKEN_API_PATH=''
|
||||
DEMOGRAPHICS_BASE_URL=''
|
||||
DISCOVERY_API_BASE_URL=''
|
||||
ECOMMERCE_BASE_URL=''
|
||||
ENABLE_DEMOGRAPHICS_COLLECTION=''
|
||||
FAVICON_URL=''
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME=''
|
||||
LMS_BASE_URL=''
|
||||
@@ -13,9 +12,10 @@ LOGIN_URL=''
|
||||
LOGO_TRADEMARK_URL=''
|
||||
LOGO_URL=''
|
||||
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=''
|
||||
@@ -32,4 +32,5 @@ 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'
|
||||
COUNTRIES_WITH_DELETE_ACCOUNT_DISABLED='[]'
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||
ACCOUNT_PROFILE_URL='http://localhost:1995'
|
||||
BASE_URL='localhost:1997'
|
||||
CREDENTIALS_BASE_URL='http://localhost:18150'
|
||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
DEMOGRAPHICS_BASE_URL='http://localhost:18360'
|
||||
DISCOVERY_API_BASE_URL=''
|
||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||
ENABLE_DEMOGRAPHICS_COLLECTION=''
|
||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
|
||||
LMS_BASE_URL='http://localhost:18000'
|
||||
@@ -29,8 +28,10 @@ ENABLE_COPPA_COMPLIANCE=''
|
||||
ENABLE_ACCOUNT_DELETION=''
|
||||
ENABLE_DOB_UPDATE=''
|
||||
MARKETING_EMAILS_OPT_IN=''
|
||||
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'
|
||||
COUNTRIES_WITH_DELETE_ACCOUNT_DISABLED='[]'
|
||||
|
||||
@@ -2,10 +2,8 @@ ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||
BASE_URL='localhost:1997'
|
||||
CREDENTIALS_BASE_URL='http://localhost:18150'
|
||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
DEMOGRAPHICS_BASE_URL='http://localhost:18360'
|
||||
DISCOVERY_API_BASE_URL=''
|
||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||
ENABLE_DEMOGRAPHICS_COLLECTION=''
|
||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
|
||||
LMS_BASE_URL='http://localhost:18000'
|
||||
@@ -26,9 +24,11 @@ SUPPORT_URL='http://localhost:18000/support'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
ENABLE_COPPA_COMPLIANCE=''
|
||||
ENABLE_ACCOUNT_DELETION=''
|
||||
SHOW_EMAIL_CHANNEL=''
|
||||
ENABLE_DOB_UPDATE=''
|
||||
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'
|
||||
COUNTRIES_WITH_DELETE_ACCOUNT_DISABLED='[]'
|
||||
|
||||
@@ -3,3 +3,4 @@ dist/
|
||||
node_modules/
|
||||
__mocks__/
|
||||
__snapshots__/
|
||||
src/i18n/messages/
|
||||
|
||||
24
.github/pull_request_template.md
vendored
Normal file
24
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
### Description
|
||||
|
||||
Include a description of your changes here, along with a link to any relevant Jira tickets and/or GitHub issues.
|
||||
|
||||
#### How Has This Been Tested?
|
||||
|
||||
Please describe in detail how you tested your changes.
|
||||
|
||||
#### Screenshots/sandbox (optional):
|
||||
Include a link to the sandbox for design changes or screenshot for before and after. **Remove this section if it's not applicable.**
|
||||
|
||||
|Before|After|
|
||||
|-------|-----|
|
||||
| | |
|
||||
|
||||
#### Merge Checklist
|
||||
|
||||
* [ ] If your update includes visual changes, have they been reviewed by a designer? Send them a link to the Sandbox, if applicable.
|
||||
* [ ] Is there adequate test coverage for your changes?
|
||||
|
||||
#### Post-merge Checklist
|
||||
|
||||
* [ ] Deploy the changes to prod after verifying on stage or ask **@openedx/edx-infinity** to do it.
|
||||
* [ ] 🎉 🙌 Celebrate! Thanks for your contribution.
|
||||
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
@@ -15,15 +15,15 @@ jobs:
|
||||
- test
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Nodejs Env
|
||||
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VER }}
|
||||
node-version-file: '.nvmrc'
|
||||
- run: make requirements
|
||||
- run: make test NPM_TESTS=build
|
||||
- run: make test NPM_TESTS=${{ matrix.npm-test }}
|
||||
- name: upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
- name: Coverage
|
||||
if: matrix.npm-test == 'test'
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
fail_ci_if_error: false
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
|
||||
3
.github/workflows/lockfileversion-check.yml
vendored
3
.github/workflows/lockfileversion-check.yml
vendored
@@ -10,5 +10,4 @@ on:
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
uses: openedx/.github/.github/workflows/lockfile-check.yml@master
|
||||
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
|
||||
|
||||
18
Makefile
18
Makefile
@@ -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
|
||||
|
||||
95
README.rst
95
README.rst
@@ -16,9 +16,6 @@ What is the domain of this MFE?
|
||||
In this MFE: Private user settings UIs. Public facing profile is in a `separate MFE (Profile) <https://github.com/openedx/frontend-app-profile>`_
|
||||
|
||||
- Account settings page
|
||||
|
||||
- Demographics collection
|
||||
|
||||
- IDV (Identity Verification)
|
||||
|
||||
***************
|
||||
@@ -28,45 +25,24 @@ 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
|
||||
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development
|
||||
|
||||
Installation
|
||||
============
|
||||
Plugins
|
||||
=======
|
||||
This MFE can be customized using `Frontend Plugin Framework <https://github.com/openedx/frontend-plugin-framework>`_.
|
||||
|
||||
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
|
||||
The parts of this MFE that can be customized in that manner are documented `here </src/plugin-slots>`_.
|
||||
|
||||
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:
|
||||
|
||||
@@ -93,31 +69,15 @@ Example: ``'false'`` | ``''`` (empty strings are true)
|
||||
Enable the account deletion option, defaults to true.
|
||||
To disable account deletion set ``ENABLE_ACCOUNT_DELETION`` to ``'false'`` (string), otherwise it will default to true.
|
||||
|
||||
edX-specific Environment Variables
|
||||
==================================
|
||||
|
||||
Furthermore, there are several edX-specific environment variables that enable integrations with closed-source services private to the edX organization, and are unsupported in Open edX. Enabling these environment variables will result in undefined behavior in Open edX installations:
|
||||
|
||||
``ENABLE_DEMOGRAPHICS_COLLECTION``
|
||||
|
||||
Example: ``true`` | ``''`` (empty strings are falsy)
|
||||
|
||||
Enables support for a section of the account settings page where a user can enter demographics information. Integrates with a private demographics service and is only used by edx.org.
|
||||
|
||||
``DEMOGRAPHICS_BASE_URL``
|
||||
|
||||
Example: ``https://demographics.example.com``
|
||||
|
||||
Required only if ``ENABLE_DEMOGRAPHICS_COLLECTION`` is true. The fully-qualified URL to the private demographics service in the target environment.
|
||||
|
||||
Example build syntax with a single environment variable:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
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
|
||||
===================
|
||||
@@ -144,6 +104,34 @@ Cloning and Startup
|
||||
|
||||
``npm start``
|
||||
|
||||
Local module development
|
||||
=========================
|
||||
|
||||
To develop locally on modules that are installed into this app, you'll need to create a ``module.config.js``
|
||||
file (which is git-ignored) that defines where to find your local modules, for instance:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
module.exports = {
|
||||
/*
|
||||
Modules you want to use from local source code. Adding a module here means that when this app
|
||||
runs its build, it'll resolve the source from peer directories of this app.
|
||||
|
||||
moduleName: the name you use to import code from the module.
|
||||
dir: The relative path to the module's source code.
|
||||
dist: The sub-directory of the source code where it puts its build artifact. Often "dist", though you
|
||||
may want to use "src" if the module installs React as a peer/dev dependency.
|
||||
*/
|
||||
localModules: [
|
||||
{ moduleName: '@openedx/paragon/scss', dir: '../paragon', dist: 'scss' },
|
||||
{ moduleName: '@openedx/paragon', dir: '../paragon', dist: 'dist' },
|
||||
{ moduleName: '@openedx/frontend-enterprise', dir: '../frontend-enterprise', dist: 'src' },
|
||||
{ moduleName: '@openedx/frontend-platform', dir: '../frontend-platform', dist: 'dist' },
|
||||
],
|
||||
};
|
||||
|
||||
See https://github.com/openedx/frontend-build#local-module-configuration-for-webpack for more details.
|
||||
|
||||
Known Issues
|
||||
===========
|
||||
|
||||
@@ -154,7 +142,6 @@ Development Roadmap
|
||||
|
||||
We don't have anything planned for the core of the MFE (the account settings page) - this MFE is currently in maintenance mode.
|
||||
There may be a replacement for IDV coming down the pipe, so that may be DEPRed.
|
||||
In the future, it's possible that demographics could be modeled as a plugin rather than being hard-coded into this MFE.
|
||||
|
||||
License
|
||||
=======
|
||||
@@ -210,6 +197,10 @@ All community members are expected to follow the `Open edX Code of Conduct`_.
|
||||
|
||||
.. _Open edX Code of Conduct: https://openedx.org/code-of-conduct/
|
||||
|
||||
People
|
||||
======
|
||||
The assigned maintainers for this component and other project details may be found in Backstage or from inspecting catalog-info.yaml.
|
||||
|
||||
Reporting Security Issues
|
||||
=========================
|
||||
|
||||
|
||||
19
catalog-info.yaml
Normal file
19
catalog-info.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
# This file records information about this repo. Its use is described in OEP-55:
|
||||
# https://open-edx-proposals.readthedocs.io/en/latest/processes/oep-0055-proc-project-maintainers.html
|
||||
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: 'frontend-app-account'
|
||||
description: "Open edX micro-frontend application for managing user account information."
|
||||
links:
|
||||
- url: "https://github.com/openedx/frontend-app-account"
|
||||
title: "Frontend app account"
|
||||
icon: "Web"
|
||||
annotations:
|
||||
openedx.org/arch-interest-groups: ""
|
||||
openedx.org/release: "master"
|
||||
spec:
|
||||
owner: group:2u-infinity
|
||||
type: 'website'
|
||||
lifecycle: 'production'
|
||||
@@ -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}
|
||||
9679
package-lock.json
generated
9679
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
62
package.json
62
package.json
@@ -12,6 +12,7 @@
|
||||
"build": "fedx-scripts webpack",
|
||||
"i18n_extract": "fedx-scripts formatjs extract",
|
||||
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
||||
"start": "fedx-scripts webpack-dev-server --progress",
|
||||
"test": "TZ=UTC fedx-scripts jest --coverage --passWithNoTests"
|
||||
@@ -28,24 +29,25 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||
"@edx/frontend-component-footer": "13.1.0",
|
||||
"@edx/frontend-component-header": "5.3.0",
|
||||
"@edx/frontend-platform": "8.0.1",
|
||||
"@edx/frontend-component-footer": "^14.6.0",
|
||||
"@edx/frontend-component-header": "^6.2.0",
|
||||
"@edx/frontend-platform": "^8.3.3",
|
||||
"@edx/openedx-atlas": "^0.6.0",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||
"@fortawesome/react-fontawesome": "0.2.0",
|
||||
"@openedx/paragon": "22.0.0",
|
||||
"@tensorflow-models/blazeface": "0.0.7",
|
||||
"@tensorflow/tfjs-converter": "3.21.0",
|
||||
"@tensorflow/tfjs-core": "3.21.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.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.37.0",
|
||||
"core-js": "3.41.0",
|
||||
"font-awesome": "4.7.0",
|
||||
"form-urlencoded": "6.1.4",
|
||||
"form-urlencoded": "6.1.5",
|
||||
"formdata-polyfill": "4.0.10",
|
||||
"jslib-html5-camera-photo": "3.3.4",
|
||||
"lodash.camelcase": "4.3.0",
|
||||
@@ -58,16 +60,16 @@
|
||||
"lodash.pick": "4.4.0",
|
||||
"lodash.pickby": "4.6.0",
|
||||
"lodash.snakecase": "4.1.1",
|
||||
"long": "5.2.3",
|
||||
"memoize-one": "5.2.1",
|
||||
"long": "5.3.2",
|
||||
"memoize-one": "^6.0.0",
|
||||
"prop-types": "15.8.1",
|
||||
"qs": "6.12.1",
|
||||
"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.23.0",
|
||||
"react-router-dom": "6.23.0",
|
||||
"react-router": "^6.25.1",
|
||||
"react-router-dom": "^6.25.1",
|
||||
"react-router-hash-link": "2.4.3",
|
||||
"react-scrollspy": "3.4.3",
|
||||
"react-transition-group": "4.4.5",
|
||||
@@ -77,17 +79,17 @@
|
||||
"redux-saga": "1.3.0",
|
||||
"redux-thunk": "2.4.2",
|
||||
"regenerator-runtime": "0.14.1",
|
||||
"reselect": "4.1.8",
|
||||
"universal-cookie": "4.0.4"
|
||||
"reselect": "^5.1.1",
|
||||
"universal-cookie": "7.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/browserslist-config": "1.2.0",
|
||||
"@edx/reactifex": "1.1.0",
|
||||
"@openedx/frontend-build": "14.0.3",
|
||||
"@testing-library/jest-dom": "6.4.5",
|
||||
"@testing-library/react": "12.1.5",
|
||||
"react-test-renderer": "17.0.2",
|
||||
"@edx/browserslist-config": "1.5.0",
|
||||
"@edx/reactifex": "2.2.0",
|
||||
"@openedx/frontend-build": "^14.3.3",
|
||||
"@testing-library/jest-dom": "6.6.3",
|
||||
"@testing-library/react": "14.3.1",
|
||||
"react-test-renderer": "^18.3.1",
|
||||
"reactifex": "1.1.1",
|
||||
"redux-mock-store": "1.5.4"
|
||||
"redux-mock-store": "1.5.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -47,11 +47,13 @@ import {
|
||||
COPPA_COMPLIANCE_YEAR,
|
||||
WORK_EXPERIENCE_OPTIONS,
|
||||
getStatesList,
|
||||
FIELD_LABELS,
|
||||
} from './data/constants';
|
||||
import { fetchSiteLanguages } from './site-language';
|
||||
import DemographicsSection from './demographics/DemographicsSection';
|
||||
import { fetchCourseList } from '../notification-preferences/data/thunks';
|
||||
import NotificationSettings from '../notification-preferences/NotificationSettings';
|
||||
import { withLocation, withNavigate } from './hoc';
|
||||
import AdditionalProfileFieldsSlot from '../plugin-slots/AdditionalProfileFieldsSlot';
|
||||
|
||||
class AccountSettingsPage extends React.Component {
|
||||
constructor(props, context) {
|
||||
@@ -65,8 +67,8 @@ class AccountSettingsPage extends React.Component {
|
||||
this.navLinkRefs = {
|
||||
'#basic-information': React.createRef(),
|
||||
'#profile-information': React.createRef(),
|
||||
'#demographics-information': React.createRef(),
|
||||
'#social-media': React.createRef(),
|
||||
'#notifications': React.createRef(),
|
||||
'#site-preferences': React.createRef(),
|
||||
'#linked-accounts': React.createRef(),
|
||||
'#delete-account': React.createRef(),
|
||||
@@ -122,7 +124,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']),
|
||||
@@ -149,11 +159,30 @@ class AccountSettingsPage extends React.Component {
|
||||
})),
|
||||
}));
|
||||
|
||||
canDeleteAccount = () => {
|
||||
const { committedValues } = this.props;
|
||||
return !getConfig().COUNTRIES_WITH_DELETE_ACCOUNT_DISABLED.includes(committedValues.country);
|
||||
};
|
||||
|
||||
removeDisabledCountries = (countryList) => {
|
||||
const { countriesCodesList, committedValues } = this.props;
|
||||
const committedCountry = committedValues?.country;
|
||||
|
||||
if (!countriesCodesList.length) {
|
||||
return countryList;
|
||||
}
|
||||
return countryList.filter(({ value }) => value === committedCountry || countriesCodesList.find(x => x === value));
|
||||
};
|
||||
|
||||
handleEditableFieldChange = (name, value) => {
|
||||
this.props.updateDraft(name, value);
|
||||
};
|
||||
|
||||
handleSubmit = (formId, values) => {
|
||||
if (formId === FIELD_LABELS.COUNTRY && this.isDisabledCountry(values)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { formValues } = this.props;
|
||||
let extendedProfileObject = {};
|
||||
|
||||
@@ -195,6 +224,12 @@ class AccountSettingsPage extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
isDisabledCountry = (country) => {
|
||||
const { countriesCodesList } = this.props;
|
||||
|
||||
return countriesCodesList.length > 0 && !countriesCodesList.find(x => x === country);
|
||||
};
|
||||
|
||||
isEditable(fieldName) {
|
||||
return !this.props.staticFields.includes(fieldName);
|
||||
}
|
||||
@@ -313,19 +348,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>
|
||||
)
|
||||
}
|
||||
/>
|
||||
@@ -460,16 +485,6 @@ class AccountSettingsPage extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderDemographicsSection() {
|
||||
// check the result of an LMS API call to determine if we should render the DemographicsSection component
|
||||
if (this.props.formValues.shouldDisplayDemographicsSection) {
|
||||
return (
|
||||
<DemographicsSection forwardRef={this.navLinkRefs['#demographics-information']} />
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
const editableFieldProps = {
|
||||
onChange: this.handleEditableFieldChange,
|
||||
@@ -488,7 +503,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');
|
||||
@@ -717,9 +733,10 @@ class AccountSettingsPage extends React.Component {
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.language.proficiencies.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
|
||||
<AdditionalProfileFieldsSlot />
|
||||
</div>
|
||||
{getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && this.renderDemographicsSection()}
|
||||
<div className="account-section pt-3 mb-5" 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>
|
||||
@@ -755,8 +772,11 @@ 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" />
|
||||
<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'])}
|
||||
</h2>
|
||||
@@ -798,16 +818,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>
|
||||
)}
|
||||
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -844,9 +863,7 @@ class AccountSettingsPage extends React.Component {
|
||||
<div>
|
||||
<div className="row">
|
||||
<div className="col-md-2">
|
||||
<JumpNav
|
||||
displayDemographicsLink={this.props.formValues.shouldDisplayDemographicsSection}
|
||||
/>
|
||||
<JumpNav />
|
||||
</div>
|
||||
<div className="col-md-10">
|
||||
{loading ? this.renderLoading() : null}
|
||||
@@ -874,12 +891,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,
|
||||
@@ -888,7 +908,6 @@ AccountSettingsPage.propTypes = {
|
||||
social_link_twitter: PropTypes.string,
|
||||
time_zone: PropTypes.string,
|
||||
state: PropTypes.string,
|
||||
shouldDisplayDemographicsSection: PropTypes.bool,
|
||||
useVerifiedNameForCerts: PropTypes.bool.isRequired,
|
||||
verified_name: PropTypes.string,
|
||||
}).isRequired,
|
||||
@@ -896,6 +915,7 @@ AccountSettingsPage.propTypes = {
|
||||
name: PropTypes.string,
|
||||
useVerifiedNameForCerts: PropTypes.bool,
|
||||
verified_name: PropTypes.string,
|
||||
country: PropTypes.string,
|
||||
}),
|
||||
drafts: PropTypes.shape({}),
|
||||
formErrors: PropTypes.shape({
|
||||
@@ -932,9 +952,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,
|
||||
@@ -954,6 +977,12 @@ AccountSettingsPage.propTypes = {
|
||||
),
|
||||
navigate: PropTypes.func.isRequired,
|
||||
location: PropTypes.string.isRequired,
|
||||
countriesCodesList: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
value: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
}),
|
||||
),
|
||||
};
|
||||
|
||||
AccountSettingsPage.defaultProps = {
|
||||
@@ -963,6 +992,7 @@ AccountSettingsPage.defaultProps = {
|
||||
committedValues: {
|
||||
useVerifiedNameForCerts: false,
|
||||
verified_name: null,
|
||||
country: '',
|
||||
},
|
||||
drafts: {},
|
||||
formErrors: {},
|
||||
@@ -975,10 +1005,11 @@ AccountSettingsPage.defaultProps = {
|
||||
tpaProviders: [],
|
||||
isActive: true,
|
||||
secondary_email_enabled: false,
|
||||
nameChangeModal: {},
|
||||
nameChangeModal: {} || false,
|
||||
verifiedName: null,
|
||||
mostRecentVerifiedName: {},
|
||||
verifiedNameHistory: [],
|
||||
countriesCodesList: [],
|
||||
};
|
||||
|
||||
export default withLocation(withNavigate(connect(accountSettingsPageSelector, {
|
||||
|
||||
@@ -46,11 +46,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Profile Information',
|
||||
description: 'The profile information section heading.',
|
||||
},
|
||||
'account.settings.section.demographics.information': {
|
||||
id: 'account.settings.section.demographics.information',
|
||||
defaultMessage: 'Optional Information',
|
||||
description: 'The optional information section heading.',
|
||||
},
|
||||
'account.settings.section.site.preferences': {
|
||||
id: 'account.settings.section.site.preferences',
|
||||
defaultMessage: 'Site Preferences',
|
||||
@@ -146,11 +141,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Once your proctored exam passes review, this name will appear on your certificate and public-facing records. Verified Name cannot be changed at this time.',
|
||||
description: 'Help text for the account settings verified name field when a verified name has been submitted through proctoring and will appear on certificates.',
|
||||
},
|
||||
'account.settings.field.name.verified.verification.alert': {
|
||||
id: 'account.settings.field.name.verified.verification.help',
|
||||
defaultMessage: 'Enter your name as it appears on your unexpired student, work, or government-issued identification card.',
|
||||
description: 'Form label instructing the user to enter the name on their ID.',
|
||||
},
|
||||
'account.settings.field.full.name.help.text.submitted': {
|
||||
id: 'account.settings.field.full.name.help.text.submitted',
|
||||
defaultMessage: 'Verification has been submitted. This usually takes 48 hours or less. Full name cannot be changed at this time.',
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -1,23 +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 PropTypes from 'prop-types';
|
||||
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,
|
||||
displayDemographicsLink,
|
||||
}) => {
|
||||
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 })}>
|
||||
@@ -25,14 +18,15 @@ const JumpNav = ({
|
||||
items={[
|
||||
'basic-information',
|
||||
'profile-information',
|
||||
'demographics-information',
|
||||
'social-media',
|
||||
'notifications',
|
||||
'site-preferences',
|
||||
'linked-accounts',
|
||||
'delete-account',
|
||||
]}
|
||||
className="list-unstyled"
|
||||
currentClassName="font-weight-bold"
|
||||
offset={-64}
|
||||
>
|
||||
<li>
|
||||
<NavHashLink to="#basic-information">
|
||||
@@ -44,19 +38,16 @@ const JumpNav = ({
|
||||
{intl.formatMessage(messages['account.settings.section.profile.information'])}
|
||||
</NavHashLink>
|
||||
</li>
|
||||
{getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && displayDemographicsLink
|
||||
&& (
|
||||
<li>
|
||||
<NavHashLink to="#demographics-information">
|
||||
{intl.formatMessage(messages['account.settings.section.demographics.information'])}
|
||||
</NavHashLink>
|
||||
</li>
|
||||
)}
|
||||
<li>
|
||||
<NavHashLink to="#social-media">
|
||||
{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'])}
|
||||
@@ -76,28 +67,12 @@ 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>
|
||||
);
|
||||
};
|
||||
|
||||
JumpNav.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
displayDemographicsLink: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(JumpNav);
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -39,6 +39,7 @@ export const defaultState = {
|
||||
verifiedName: null,
|
||||
mostRecentVerifiedName: {},
|
||||
verifiedNameHistory: {},
|
||||
countriesCodesList: [],
|
||||
};
|
||||
|
||||
const reducer = (state = defaultState, action = {}) => {
|
||||
@@ -64,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 {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -88,6 +88,11 @@ const previousSiteLanguageSelector = createSelector(
|
||||
accountSettings => accountSettings.previousSiteLanguage,
|
||||
);
|
||||
|
||||
const countriesSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
accountSettings => accountSettings.countriesCodesList,
|
||||
);
|
||||
|
||||
const editableFieldErrorSelector = createSelector(
|
||||
editableFieldNameSelector,
|
||||
accountSettingsSelector,
|
||||
@@ -237,6 +242,7 @@ export const accountSettingsPageSelector = createSelector(
|
||||
mostRecentApprovedVerifiedNameValueSelector,
|
||||
mostRecentVerifiedNameSelector,
|
||||
sortedVerifiedNameHistorySelector,
|
||||
countriesSelector,
|
||||
(
|
||||
accountSettings,
|
||||
siteLanguageOptions,
|
||||
@@ -254,6 +260,7 @@ export const accountSettingsPageSelector = createSelector(
|
||||
verifiedName,
|
||||
mostRecentVerifiedName,
|
||||
verifiedNameHistory,
|
||||
countriesCodesList,
|
||||
) => ({
|
||||
siteLanguageOptions,
|
||||
siteLanguage,
|
||||
@@ -274,6 +281,7 @@ export const accountSettingsPageSelector = createSelector(
|
||||
verifiedName,
|
||||
mostRecentVerifiedName,
|
||||
verifiedNameHistory,
|
||||
countriesCodesList,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -298,21 +306,6 @@ export const certPreferenceSelector = createSelector(
|
||||
}),
|
||||
);
|
||||
|
||||
export const demographicsSectionSelector = createSelector(
|
||||
formValuesSelector,
|
||||
draftsSelector,
|
||||
errorSelector,
|
||||
(
|
||||
formValues,
|
||||
drafts,
|
||||
errors,
|
||||
) => ({
|
||||
formValues,
|
||||
drafts,
|
||||
formErrors: errors,
|
||||
}),
|
||||
);
|
||||
|
||||
export const nameChangeSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
formValuesSelector,
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
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 pickBy from 'lodash.pickby';
|
||||
import omit from 'lodash.omit';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
|
||||
import { handleRequestError, unpackFieldErrors } from './utils';
|
||||
import { getThirdPartyAuthProviders } from '../third-party-auth';
|
||||
import { postVerifiedNameConfig } from '../certificate-preference/data/service';
|
||||
import { getDemographics, getDemographicsOptions, patchDemographics } from '../demographics/data/service';
|
||||
import { DEMOGRAPHICS_FIELDS } from '../demographics/data/utils';
|
||||
import { FIELD_LABELS } from './constants';
|
||||
|
||||
const SOCIAL_PLATFORMS = [
|
||||
{ id: 'twitter', key: 'social_link_twitter' },
|
||||
@@ -154,28 +153,6 @@ export async function getProfileDataManager(username, userRoles) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A function to determine if the Demographics questions should be displayed to the user. For the
|
||||
* MVP release of Demographics we are limiting the Demographics question visibility only to
|
||||
* MicroBachelors learners.
|
||||
*/
|
||||
export async function shouldDisplayDemographicsQuestions() {
|
||||
const requestUrl = `${getConfig().LMS_BASE_URL}/api/demographics/v1/demographics/status/`;
|
||||
let data = {};
|
||||
|
||||
try {
|
||||
({ data } = await getAuthenticatedHttpClient().get(requestUrl));
|
||||
if (data.display) {
|
||||
return data.display;
|
||||
}
|
||||
} catch (error) {
|
||||
// if there was an error then we just hide the section
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function getVerifiedName() {
|
||||
let data;
|
||||
const client = getAuthenticatedHttpClient();
|
||||
@@ -211,29 +188,43 @@ 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, ThirdPartyAuth, and Demographics
|
||||
* A single function to GET everything considered a setting. Currently encapsulates Account, Preferences, and
|
||||
* ThirdPartyAuth.
|
||||
*/
|
||||
export async function getSettings(username, userRoles, userId) {
|
||||
export async function getSettings(username, userRoles) {
|
||||
const [
|
||||
account,
|
||||
preferences,
|
||||
thirdPartyAuthProviders,
|
||||
profileDataManager,
|
||||
timeZones,
|
||||
shouldDisplayDemographicsQuestionsResponse,
|
||||
demographics,
|
||||
demographicsOptions,
|
||||
countries,
|
||||
] = await Promise.all([
|
||||
getAccount(username),
|
||||
getPreferences(username),
|
||||
getThirdPartyAuthProviders(),
|
||||
getProfileDataManager(username, userRoles),
|
||||
getTimeZones(),
|
||||
getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && shouldDisplayDemographicsQuestions(),
|
||||
getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && getDemographics(userId),
|
||||
getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && getDemographicsOptions(),
|
||||
getCountryList(),
|
||||
]);
|
||||
|
||||
return {
|
||||
@@ -242,9 +233,7 @@ export async function getSettings(username, userRoles, userId) {
|
||||
thirdPartyAuthProviders,
|
||||
profileDataManager,
|
||||
timeZones,
|
||||
shouldDisplayDemographicsSection: shouldDisplayDemographicsQuestionsResponse,
|
||||
...demographics,
|
||||
demographicsOptions,
|
||||
countries,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -252,22 +241,18 @@ export async function getSettings(username, userRoles, userId) {
|
||||
* A single function to PATCH everything considered a setting.
|
||||
* Currently encapsulates Account, Preferences, ThirdPartyAuth
|
||||
*/
|
||||
export async function patchSettings(username, commitValues, userId) {
|
||||
export async function patchSettings(username, commitValues) {
|
||||
// Note: time_zone exists in the return value from user/v1/accounts
|
||||
// but it is always null and won't update. It also exists in
|
||||
// user/v1/preferences where it does update. This is the one we use.
|
||||
const preferenceKeys = ['time_zone'];
|
||||
const demographicsKeys = DEMOGRAPHICS_FIELDS;
|
||||
const certificateKeys = ['useVerifiedNameForCerts'];
|
||||
const isDemographicsKey = (value, key) => key.includes('demographics');
|
||||
const accountCommitValues = omit(
|
||||
commitValues,
|
||||
preferenceKeys,
|
||||
demographicsKeys,
|
||||
certificateKeys,
|
||||
);
|
||||
const preferenceCommitValues = pick(commitValues, preferenceKeys);
|
||||
const demographicsCommitValues = pickBy(commitValues, isDemographicsKey);
|
||||
const certCommitValues = pick(commitValues, certificateKeys);
|
||||
const patchRequests = [];
|
||||
|
||||
@@ -277,9 +262,6 @@ export async function patchSettings(username, commitValues, userId) {
|
||||
if (!isEmpty(preferenceCommitValues)) {
|
||||
patchRequests.push(patchPreferences(username, preferenceCommitValues));
|
||||
}
|
||||
if (!isEmpty(demographicsCommitValues)) {
|
||||
patchRequests.push(patchDemographics(userId, demographicsCommitValues));
|
||||
}
|
||||
if (!isEmpty(certCommitValues)) {
|
||||
patchRequests.push(postVerifiedNameConfig(username, certCommitValues));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings">
|
||||
{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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,17 +7,17 @@ exports[`BeforeProceedingBanner should match the snapshot if SUPPORT_URL_TO_UNLI
|
||||
<div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-exclamation-triangle fa-w-18 mr-2"
|
||||
data-icon="exclamation-triangle"
|
||||
className="svg-inline--fa fa-triangle-exclamation mr-2"
|
||||
data-icon="triangle-exclamation"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 576 512"
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"
|
||||
d="M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480L40 480c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24l0 112c0 13.3 10.7 24 24 24s24-10.7 24-24l0-112c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
@@ -36,17 +36,17 @@ exports[`BeforeProceedingBanner should match the snapshot when SUPPORT_URL_TO_UN
|
||||
<div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-exclamation-triangle fa-w-18 mr-2"
|
||||
data-icon="exclamation-triangle"
|
||||
className="svg-inline--fa fa-triangle-exclamation mr-2"
|
||||
data-icon="triangle-exclamation"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 576 512"
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"
|
||||
d="M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480L40 480c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24l0 112c0 13.3 10.7 24 24 24s24-10.7 24-24l0-112c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
@@ -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
|
||||
|
||||
@@ -69,8 +69,8 @@ exports[`ConfirmationModal should match empty password confirmation modal snapsh
|
||||
<div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-exclamation-circle fa-w-16 mr-2"
|
||||
data-icon="exclamation-circle"
|
||||
className="svg-inline--fa fa-circle-exclamation mr-2"
|
||||
data-icon="circle-exclamation"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
@@ -79,7 +79,7 @@ exports[`ConfirmationModal should match empty password confirmation modal snapsh
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M504 256c0 136.997-111.043 248-248 248S8 392.997 8 256C8 119.083 119.043 8 256 8s248 111.083 248 248zm-248 50c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"
|
||||
d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zm0-384c13.3 0 24 10.7 24 24l0 112c0 13.3-10.7 24-24 24s-24-10.7-24-24l0-112c0-13.3 10.7-24 24-24zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
@@ -102,17 +102,17 @@ exports[`ConfirmationModal should match empty password confirmation modal snapsh
|
||||
<div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-exclamation-triangle fa-w-18 mr-2"
|
||||
data-icon="exclamation-triangle"
|
||||
className="svg-inline--fa fa-triangle-exclamation mr-2"
|
||||
data-icon="triangle-exclamation"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 576 512"
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"
|
||||
d="M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480L40 480c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24l0 112c0 13.3 10.7 24 24 24s24-10.7 24-24l0-112c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
@@ -270,17 +270,17 @@ exports[`ConfirmationModal should match open confirmation modal snapshot 1`] = `
|
||||
<div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-exclamation-triangle fa-w-18 mr-2"
|
||||
data-icon="exclamation-triangle"
|
||||
className="svg-inline--fa fa-triangle-exclamation mr-2"
|
||||
data-icon="triangle-exclamation"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 576 512"
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"
|
||||
d="M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480L40 480c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24l0 112c0 13.3 10.7 24 24 24s24-10.7 24-24l0-112c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
|
||||
@@ -27,8 +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"
|
||||
onClick={[Function]}
|
||||
href="https://help.edx.org/edxlearner/s/topic/0TOQq0000001UdZOAU/account-basics"
|
||||
target="_self"
|
||||
>
|
||||
Want to change your email, name, or password instead?
|
||||
@@ -74,8 +73,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"
|
||||
onClick={[Function]}
|
||||
href="https://help.edx.org/edxlearner/s/topic/0TOQq0000001UdZOAU/account-basics"
|
||||
target="_self"
|
||||
>
|
||||
Want to change your email, name, or password instead?
|
||||
@@ -97,17 +95,17 @@ exports[`DeleteAccount should match unverified account section snapshot 1`] = `
|
||||
<div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-exclamation-triangle fa-w-18 mr-2"
|
||||
data-icon="exclamation-triangle"
|
||||
className="svg-inline--fa fa-triangle-exclamation mr-2"
|
||||
data-icon="triangle-exclamation"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 576 512"
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"
|
||||
d="M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480L40 480c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24l0 112c0 13.3 10.7 24 24 24s24-10.7 24-24l0-112c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
@@ -117,8 +115,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-"
|
||||
onClick={[Function]}
|
||||
href="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-confirm-my-email"
|
||||
target="_self"
|
||||
>
|
||||
activate your account
|
||||
@@ -156,8 +153,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"
|
||||
onClick={[Function]}
|
||||
href="https://help.edx.org/edxlearner/s/topic/0TOQq0000001UdZOAU/account-basics"
|
||||
target="_self"
|
||||
>
|
||||
Want to change your email, name, or password instead?
|
||||
@@ -179,17 +175,17 @@ exports[`DeleteAccount should match unverified account section snapshot 2`] = `
|
||||
<div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-exclamation-triangle fa-w-18 mr-2"
|
||||
data-icon="exclamation-triangle"
|
||||
className="svg-inline--fa fa-triangle-exclamation mr-2"
|
||||
data-icon="triangle-exclamation"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 576 512"
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"
|
||||
d="M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480L40 480c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24l0 112c0 13.3 10.7 24 24 24s24-10.7 24-24l0-112c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
@@ -199,8 +195,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"
|
||||
onClick={[Function]}
|
||||
href="https://help.edx.org/edxlearner/s/article/How-do-I-link-or-unlink-my-edX-account-to-a-social-media-account"
|
||||
target="_self"
|
||||
>
|
||||
unlink all social media accounts
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Form } from '@openedx/paragon';
|
||||
import { DECLINED } from '../data/constants';
|
||||
|
||||
const Checkboxes = (props) => {
|
||||
const {
|
||||
id,
|
||||
options,
|
||||
values,
|
||||
onChange,
|
||||
} = props;
|
||||
|
||||
const [selected, setSelected] = useState(values);
|
||||
useEffect(() => {
|
||||
onChange(id, selected);
|
||||
}, [id, onChange, selected]);
|
||||
|
||||
const handleToggle = (value, option) => {
|
||||
// If the user checked 'declined', uncheck all other options
|
||||
if (value && option === DECLINED) {
|
||||
setSelected([DECLINED]);
|
||||
return;
|
||||
}
|
||||
|
||||
// If option checked, make sure this option is in `selected` (and remove 'declined')
|
||||
if (value && !selected.includes(option)) {
|
||||
const newSelected = selected.filter(i => i !== DECLINED).concat(option);
|
||||
setSelected(newSelected);
|
||||
}
|
||||
|
||||
// If unchecked, make sure this option is NOT in `selected`
|
||||
if (!value) {
|
||||
setSelected(selected.filter(i => i !== option));
|
||||
}
|
||||
};
|
||||
|
||||
const renderCheckboxes = () => options.map((option, index) => {
|
||||
const isFirst = index === 0;
|
||||
const isChecked = selected.includes(option.value);
|
||||
return (
|
||||
<div key={option.value} className="checkboxOption">
|
||||
<Form.Checkbox
|
||||
type="checkbox"
|
||||
id={option.value}
|
||||
name={option.value}
|
||||
value={option.value}
|
||||
checked={isChecked}
|
||||
autoFocus={isFirst}
|
||||
onChange={(event) => handleToggle(event.target.checked, option.value)}
|
||||
>
|
||||
{option.label}
|
||||
</Form.Checkbox>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div role="group">
|
||||
{renderCheckboxes()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Checkboxes.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
options: PropTypes.arrayOf(PropTypes.shape({
|
||||
value: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
})),
|
||||
values: PropTypes.arrayOf(PropTypes.string),
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
Checkboxes.defaultProps = {
|
||||
options: [],
|
||||
values: [],
|
||||
};
|
||||
|
||||
export default Checkboxes;
|
||||
@@ -1,357 +0,0 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import {
|
||||
FormattedMessage,
|
||||
injectIntl,
|
||||
intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { Hyperlink, Form } from '@openedx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import get from 'lodash.get';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import memoize from 'memoize-one';
|
||||
import { demographicsSectionSelector } from '../data/selectors';
|
||||
import EditableSelectField from '../EditableSelectField';
|
||||
import Checkboxes from './Checkboxes';
|
||||
import Alert from '../Alert';
|
||||
import { saveMultipleSettings, updateDraft } from '../data/actions';
|
||||
import {
|
||||
OTHER,
|
||||
SELF_DESCRIBE,
|
||||
} from '../data/constants';
|
||||
import messages from './DemographicsSection.messages';
|
||||
|
||||
class DemographicsSection extends React.Component {
|
||||
// We check the `demographicsOptions` prop to see if it is empty before we attempt to extract and
|
||||
// format the available options for each question from the API response.
|
||||
getApiOptions = memoize((demographicsOptions) => (this.hasRetrievedDemographicsOptions() && {
|
||||
demographicsGenderOptions: this.addDefaultOption('account.settings.field.demographics.gender.options.empty')
|
||||
.concat(demographicsOptions.actions.POST.gender.choices.map(key => ({
|
||||
value: key.value,
|
||||
label: key.display_name,
|
||||
}))),
|
||||
/* Ethnicity options don't need the blank/default option */
|
||||
demographicsEthnicityOptions: demographicsOptions.actions.POST.user_ethnicity.child.children.ethnicity.choices.map(
|
||||
key => ({
|
||||
value: key.value,
|
||||
label: key.display_name,
|
||||
}),
|
||||
),
|
||||
demographicsIncomeOptions: this.addDefaultOption('account.settings.field.demographics.income.options.empty')
|
||||
.concat(demographicsOptions.actions.POST.income.choices.map(key => ({
|
||||
value: key.value,
|
||||
label: key.display_name,
|
||||
}))),
|
||||
demographicsMilitaryHistoryOptions: this.addDefaultOption('account.settings.field.demographics.military_history.options.empty')
|
||||
.concat(demographicsOptions.actions.POST.military_history.choices.map(key => ({
|
||||
value: key.value,
|
||||
label: key.display_name,
|
||||
}))),
|
||||
demographicsEducationLevelOptions: this.addDefaultOption('account.settings.field.demographics.education_level.options.empty')
|
||||
.concat(demographicsOptions.actions.POST.learner_education_level.choices.map(key => ({
|
||||
value: key.value,
|
||||
label: key.display_name,
|
||||
}))),
|
||||
demographicsWorkStatusOptions: this.addDefaultOption('account.settings.field.demographics.work_status.options.empty')
|
||||
.concat(demographicsOptions.actions.POST.work_status.choices.map(key => ({
|
||||
value: key.value,
|
||||
label: key.display_name,
|
||||
}))),
|
||||
demographicsWorkSectorOptions: this.addDefaultOption('account.settings.field.demographics.work_sector.options.empty')
|
||||
.concat(demographicsOptions.actions.POST.current_work_sector.choices.map(key => ({
|
||||
value: key.value,
|
||||
label: key.display_name,
|
||||
}))),
|
||||
}));
|
||||
|
||||
ethnicityFieldDisplay = (demographicsEthnicityOptions) => {
|
||||
let ethnicities = [];
|
||||
if (get(this, 'props.formValues.demographics_user_ethnicity')) {
|
||||
ethnicities = this.props.formValues.demographics_user_ethnicity;
|
||||
}
|
||||
return ethnicities.map((e) => {
|
||||
const matchingOption = demographicsEthnicityOptions.filter(option => option.value === e)[0];
|
||||
return matchingOption && matchingOption.label;
|
||||
}).join(', ');
|
||||
};
|
||||
|
||||
handleEditableFieldChange = (name, value) => {
|
||||
this.props.updateDraft(name, value);
|
||||
};
|
||||
|
||||
handleSubmit = (formId) => {
|
||||
// We have some custom fields in this section. Instead of relying on the
|
||||
// submitted values, submit the values stored in 'drafts'.
|
||||
const { drafts } = this.props;
|
||||
const settingsArray = Object.entries(drafts).map(([field, value]) => ({
|
||||
formId: field,
|
||||
commitValues: value,
|
||||
}));
|
||||
|
||||
this.props.saveMultipleSettings(settingsArray, formId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility method that adds the specified message as a default option to the list of available
|
||||
* choices.
|
||||
*
|
||||
* @param {*} messageId id of message matching desired default label text
|
||||
*/
|
||||
addDefaultOption(messageId) {
|
||||
return [{
|
||||
value: '',
|
||||
label: this.props.intl.formatMessage(messages[messageId]),
|
||||
}];
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method that helps determine if we were able to retrieve the available options for
|
||||
* the Demographics questions. Returns true if the `demographicsOptions` prop is _not_ empty,
|
||||
* otherwise false. This prop being empty is indicative of a failure communicating with the
|
||||
* Demographics IDA's API.
|
||||
*/
|
||||
hasRetrievedDemographicsOptions() {
|
||||
return !isEmpty(this.props.formValues.demographicsOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* If an error is encountered when trying to communicate with the Demographics IDA then we will
|
||||
* display an Alert letting the user know that their info will not be displayed and temporarily
|
||||
* cannot be updated.
|
||||
*/
|
||||
renderDemographicsServiceIssueWarning() {
|
||||
if (!isEmpty(this.props.formErrors.demographicsError)
|
||||
|| !this.hasRetrievedDemographicsOptions()) {
|
||||
return (
|
||||
<div
|
||||
tabIndex="-1"
|
||||
ref={this.alertRef}
|
||||
>
|
||||
<Alert className="alert alert-danger" role="alert">
|
||||
<FormattedMessage
|
||||
id="account.settings.message.demographics.service.issue"
|
||||
defaultMessage="An error occurred attempting to retrieve or save your account information. Please try again later."
|
||||
description="alert message informing the user that the there is a problem retrieving or updating information from the Demographics microservice"
|
||||
/>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const editableFieldProps = {
|
||||
onChange: this.handleEditableFieldChange,
|
||||
onSubmit: this.handleSubmit,
|
||||
};
|
||||
|
||||
const {
|
||||
demographicsGenderOptions,
|
||||
demographicsEthnicityOptions,
|
||||
demographicsIncomeOptions,
|
||||
demographicsMilitaryHistoryOptions,
|
||||
demographicsEducationLevelOptions,
|
||||
demographicsWorkStatusOptions,
|
||||
demographicsWorkSectorOptions,
|
||||
} = this.getApiOptions(this.props.formValues.demographicsOptions);
|
||||
|
||||
const showSelfDescribe = this.props.formValues.demographics_gender === SELF_DESCRIBE;
|
||||
const showWorkStatusDescribe = this.props.formValues.demographics_work_status === OTHER;
|
||||
|
||||
return (
|
||||
<div className="account-section pt-3 mb-5" id="demographics-information" ref={this.props.forwardRef}>
|
||||
<h2 className="section-heading h4 mb-3">
|
||||
{this.props.intl.formatMessage(messages['account.settings.section.demographics.information'])}
|
||||
</h2>
|
||||
<p>
|
||||
<Hyperlink
|
||||
destination={`${getConfig().MARKETING_SITE_BASE_URL}/demographics`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{this.props.intl.formatMessage(
|
||||
messages['account.settings.section.demographics.why'],
|
||||
{
|
||||
siteName: getConfig().SITE_NAME,
|
||||
},
|
||||
)}
|
||||
</Hyperlink>
|
||||
</p>
|
||||
{this.renderDemographicsServiceIssueWarning()}
|
||||
{/*
|
||||
If the demographicsOptions props are empty then there is no need to display the fields as
|
||||
the user will not have any choices available to select, nor will they be able to update
|
||||
their answers.
|
||||
*/}
|
||||
{this.hasRetrievedDemographicsOptions() && (
|
||||
<div id="demographics-fields">
|
||||
<EditableSelectField
|
||||
name="demographics_gender"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_gender}
|
||||
userSuppliedValue={showSelfDescribe ? this.props.formValues.demographics_gender_description : null}
|
||||
options={demographicsGenderOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.gender'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.gender.empty'])}
|
||||
{...editableFieldProps}
|
||||
>
|
||||
{showSelfDescribe && (
|
||||
<Form.Control
|
||||
name="demographics_gender_description"
|
||||
id="field-demographics_gender_description"
|
||||
type="text"
|
||||
placeholder={this.props.intl.formatMessage(messages['account.settings.field.demographics.gender_description.empty'])}
|
||||
value={this.props.formValues.demographics_gender_description}
|
||||
onChange={(e) => this.handleEditableFieldChange('demographics_gender_description', e.target.value)}
|
||||
aria-label={this.props.intl.formatMessage(messages['account.settings.field.demographics.gender_description'])}
|
||||
className="mt-1"
|
||||
/>
|
||||
)}
|
||||
</EditableSelectField>
|
||||
<EditableSelectField
|
||||
name="demographics_user_ethnicity"
|
||||
type="select"
|
||||
hidden
|
||||
value={this.ethnicityFieldDisplay(demographicsEthnicityOptions)}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.ethnicity'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.ethnicity.empty'])}
|
||||
{...editableFieldProps}
|
||||
>
|
||||
<Checkboxes
|
||||
id="demographics_user_ethnicity"
|
||||
options={demographicsEthnicityOptions}
|
||||
values={this.props.formValues.demographics_user_ethnicity}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
</EditableSelectField>
|
||||
<EditableSelectField
|
||||
name="demographics_income"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_income}
|
||||
options={demographicsIncomeOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.income'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.income.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableSelectField
|
||||
name="demographics_military_history"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_military_history}
|
||||
options={demographicsMilitaryHistoryOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.military_history'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.military_history.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableSelectField
|
||||
name="demographics_learner_education_level"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_learner_education_level}
|
||||
options={demographicsEducationLevelOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.learner_education_level'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.learner_education_level.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableSelectField
|
||||
name="demographics_parent_education_level"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_parent_education_level}
|
||||
options={demographicsEducationLevelOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.parent_education_level'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.parent_education_level.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableSelectField
|
||||
name="demographics_work_status"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_work_status}
|
||||
userSuppliedValue={showWorkStatusDescribe
|
||||
? this.props.formValues.demographics_work_status_description
|
||||
: null}
|
||||
options={demographicsWorkStatusOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.work_status'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.work_status.empty'])}
|
||||
{...editableFieldProps}
|
||||
>
|
||||
{showWorkStatusDescribe && (
|
||||
<Form.Control
|
||||
name="demographics_work_status_description"
|
||||
id="field-demographics_work_status_description"
|
||||
type="text"
|
||||
placeholder={this.props.intl.formatMessage(messages['account.settings.field.demographics.work_status_description.empty'])}
|
||||
value={this.props.formValues.demographics_work_status_description}
|
||||
onChange={(e) => this.handleEditableFieldChange('demographics_work_status_description', e.target.value)}
|
||||
aria-label={this.props.intl.formatMessage(messages['account.settings.field.demographics.work_status_description'])}
|
||||
className="mt-1"
|
||||
/>
|
||||
)}
|
||||
</EditableSelectField>
|
||||
<EditableSelectField
|
||||
name="demographics_current_work_sector"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_current_work_sector}
|
||||
options={demographicsWorkSectorOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.current_work_sector'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.current_work_sector.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableSelectField
|
||||
name="demographics_future_work_sector"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_future_work_sector}
|
||||
options={demographicsWorkSectorOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.future_work_sector'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.future_work_sector.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DemographicsSection.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
formValues: PropTypes.shape({
|
||||
demographics_gender: PropTypes.string,
|
||||
demographics_user_ethnicity: PropTypes.shape([]),
|
||||
demographics_income: PropTypes.string,
|
||||
demographics_military_history: PropTypes.string,
|
||||
demographics_learner_education_level: PropTypes.string,
|
||||
demographics_parent_education_level: PropTypes.string,
|
||||
demographics_work_status: PropTypes.string,
|
||||
demographics_current_work_sector: PropTypes.string,
|
||||
demographics_future_work_sector: PropTypes.string,
|
||||
demographics_work_status_description: PropTypes.string,
|
||||
demographics_gender_description: PropTypes.string,
|
||||
demographicsOptions: PropTypes.shape({}),
|
||||
}).isRequired,
|
||||
drafts: PropTypes.shape({
|
||||
demographics_gender: PropTypes.string,
|
||||
demographics_user_ethnicity: PropTypes.shape([]),
|
||||
demographics_income: PropTypes.string,
|
||||
demographics_military_history: PropTypes.string,
|
||||
demographics_learner_education_level: PropTypes.string,
|
||||
demographics_parent_education_level: PropTypes.string,
|
||||
demographics_work_status: PropTypes.string,
|
||||
demographics_current_work_sector: PropTypes.string,
|
||||
demographics_future_work_sector: PropTypes.string,
|
||||
demographics_work_status_description: PropTypes.string,
|
||||
demographics_gender_description: PropTypes.string,
|
||||
demographicsOptions: PropTypes.shape({}),
|
||||
}).isRequired,
|
||||
formErrors: PropTypes.shape({
|
||||
demographicsError: PropTypes.string,
|
||||
}).isRequired,
|
||||
forwardRef: PropTypes.func.isRequired,
|
||||
updateDraft: PropTypes.func.isRequired,
|
||||
saveMultipleSettings: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default connect(demographicsSectionSelector, {
|
||||
saveMultipleSettings,
|
||||
updateDraft,
|
||||
})(injectIntl(DemographicsSection));
|
||||
@@ -1,170 +0,0 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
/* Demographics section heading */
|
||||
'account.settings.section.demographics.information': {
|
||||
id: 'account.settings.section.demographics.information',
|
||||
defaultMessage: 'Optional Information',
|
||||
description: 'The optional information section heading.',
|
||||
},
|
||||
/* Gender identity */
|
||||
'account.settings.field.demographics.gender': {
|
||||
id: 'account.settings.field.demographics.gender',
|
||||
defaultMessage: 'Gender identity',
|
||||
description: 'Label for account settings gender identity field.',
|
||||
},
|
||||
'account.settings.field.demographics.gender.empty': {
|
||||
id: 'account.settings.field.demographics.gender.empty',
|
||||
defaultMessage: 'Add gender identity',
|
||||
description: 'Placeholder for empty account settings gender identity field.',
|
||||
},
|
||||
'account.settings.field.demographics.gender.options.empty': {
|
||||
id: 'account.settings.field.demographics.gender.options.empty',
|
||||
defaultMessage: 'Select a gender identity',
|
||||
description: 'Placeholder for the gender identity options dropdown.',
|
||||
},
|
||||
'account.settings.field.demographics.gender_description': {
|
||||
id: 'account.settings.field.demographics.gender_description',
|
||||
defaultMessage: 'Gender identity description',
|
||||
description: 'Label for account settings gender identity description field.',
|
||||
},
|
||||
'account.settings.field.demographics.gender_description.empty': {
|
||||
id: 'account.settings.field.demographics.gender_description.empty',
|
||||
defaultMessage: 'Enter description',
|
||||
description: 'Placeholder for empty account settings gender identity field.',
|
||||
},
|
||||
/* Ethnicity */
|
||||
'account.settings.field.demographics.ethnicity': {
|
||||
id: 'account.settings.field.demographics.ethnicity',
|
||||
defaultMessage: 'Race/Ethnicity identity',
|
||||
description: 'Label for account settings ethnic background field.',
|
||||
},
|
||||
'account.settings.field.demographics.ethnicity.empty': {
|
||||
id: 'account.settings.field.demographics.ethnicity.empty',
|
||||
defaultMessage: 'Add race/ethnicity identity',
|
||||
description: 'Placeholder for empty account settings ethnic background field.',
|
||||
},
|
||||
'account.settings.field.demographics.ethnicity.options.empty': {
|
||||
id: 'account.settings.field.demographics.ethnicity.options.empty',
|
||||
defaultMessage: 'Select all that apply', // TODO: Is this the desired text?
|
||||
description: 'Placeholder for the ethnic background options field.',
|
||||
},
|
||||
/* Income */
|
||||
'account.settings.field.demographics.income': {
|
||||
id: 'account.settings.field.demographics.income',
|
||||
defaultMessage: 'Family income',
|
||||
description: 'Label for account settings household income field.',
|
||||
},
|
||||
'account.settings.field.demographics.income.empty': {
|
||||
id: 'account.settings.field.demographics.income.empty',
|
||||
defaultMessage: 'Add family income',
|
||||
description: 'Placeholder for empty account settings household income field.',
|
||||
},
|
||||
'account.settings.field.demographics.income.options.empty': {
|
||||
id: 'account.settings.field.demographics.income.options.empty',
|
||||
defaultMessage: 'Select a family income range',
|
||||
description: 'Placeholder for the household income dropdown.',
|
||||
},
|
||||
/* Military history */
|
||||
'account.settings.field.demographics.military_history': {
|
||||
id: 'account.settings.field.demographics.military_history',
|
||||
defaultMessage: 'U.S. Military status',
|
||||
description: 'Label for account settings military history field.',
|
||||
},
|
||||
'account.settings.field.demographics.military_history.empty': {
|
||||
id: 'account.settings.field.demographics.military_history.empty',
|
||||
defaultMessage: 'Add military status',
|
||||
description: 'Placeholder for empty account settings military history field.',
|
||||
},
|
||||
'account.settings.field.demographics.military_history.options.empty': {
|
||||
id: 'account.settings.field.demographics.military_history.options.empty',
|
||||
defaultMessage: 'Select military status',
|
||||
description: 'Placeholder for the military history dropdown.',
|
||||
},
|
||||
/* Learner and family education level */
|
||||
'account.settings.field.demographics.learner_education_level': {
|
||||
id: 'account.settings.field.demographics.learner_education_level',
|
||||
defaultMessage: 'Your education level',
|
||||
description: 'Label for account settings learner education level field.',
|
||||
},
|
||||
'account.settings.field.demographics.learner_education_level.empty': {
|
||||
id: 'account.settings.field.demographics.learner_education_level.empty',
|
||||
defaultMessage: 'Add education level',
|
||||
description: 'Placeholder for empty account settings learner education level field.',
|
||||
},
|
||||
'account.settings.field.demographics.parent_education_level': {
|
||||
id: 'account.settings.field.demographics.parent_education_level',
|
||||
defaultMessage: 'Parents/Guardians education level',
|
||||
description: 'Label for account settings parent education level field.',
|
||||
},
|
||||
'account.settings.field.demographics.parent_education_level.empty': {
|
||||
id: 'account.settings.field.demographics.parent_education_level.empty',
|
||||
defaultMessage: 'Add education level',
|
||||
description: 'Placeholder for empty account settings parent education level field.',
|
||||
},
|
||||
'account.settings.field.demographics.education_level.options.empty': {
|
||||
id: 'account.settings.field.demographics.education_level.options.empty',
|
||||
defaultMessage: 'Select education level',
|
||||
description: 'Placeholder for the education level options dropdown.',
|
||||
},
|
||||
/* Work status */
|
||||
'account.settings.field.demographics.work_status': {
|
||||
id: 'account.settings.field.demographics.work_status',
|
||||
defaultMessage: 'Employment status',
|
||||
description: 'Label for account settings work status field.',
|
||||
},
|
||||
'account.settings.field.demographics.work_status.empty': {
|
||||
id: 'account.settings.field.demographics.work_status.empty',
|
||||
defaultMessage: 'Add employment status',
|
||||
description: 'Placeholder for empty account settings work status field.',
|
||||
},
|
||||
'account.settings.field.demographics.work_status.options.empty': {
|
||||
id: 'account.settings.field.demographics.work_status.options.empty',
|
||||
defaultMessage: 'Select employment status',
|
||||
description: 'Placeholder for the work status options dropdown.',
|
||||
},
|
||||
'account.settings.field.demographics.work_status_description': {
|
||||
id: 'account.settings.field.demographics.work_status_description',
|
||||
defaultMessage: 'Employment status description',
|
||||
description: 'Label for account settings work status description field.',
|
||||
},
|
||||
'account.settings.field.demographics.work_status_description.empty': {
|
||||
id: 'account.settings.field.demographics.work_status_description.empty',
|
||||
defaultMessage: 'Enter description',
|
||||
description: 'Placeholder for empty account settings work status description field.',
|
||||
},
|
||||
/* Work sector */
|
||||
'account.settings.field.demographics.current_work_sector': {
|
||||
id: 'account.settings.field.demographics.current_work_sector',
|
||||
defaultMessage: 'Current work industry',
|
||||
description: 'Label for account settings current work sector field.',
|
||||
},
|
||||
'account.settings.field.demographics.current_work_sector.empty': {
|
||||
id: 'account.settings.field.demographics.current_work_sector.empty',
|
||||
defaultMessage: 'Add work industry',
|
||||
description: 'Placeholder for empty account settings current work sector field.',
|
||||
},
|
||||
'account.settings.field.demographics.future_work_sector': {
|
||||
id: 'account.settings.field.demographics.future_work_sector',
|
||||
defaultMessage: 'Future work industry',
|
||||
description: 'Label for account settings future work sector field.',
|
||||
},
|
||||
'account.settings.field.demographics.future_work_sector.empty': {
|
||||
id: 'account.settings.field.demographics.future_work_sector.empty',
|
||||
defaultMessage: 'Add work industry',
|
||||
description: 'Placeholder for empty account settings future work sector field.',
|
||||
},
|
||||
'account.settings.field.demographics.work_sector.options.empty': {
|
||||
id: 'account.settings.field.demographics.work_sector.options.empty',
|
||||
defaultMessage: 'Select work industry',
|
||||
description: 'Placeholder for the work sector options dropdown.',
|
||||
},
|
||||
/* Legal copy link text */
|
||||
'account.settings.section.demographics.why': {
|
||||
id: 'account.settings.section.demographics.why',
|
||||
defaultMessage: 'Why does {siteName} collect this information?',
|
||||
description: 'Link text for a link to external legal text',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,140 +0,0 @@
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import get from 'lodash.get';
|
||||
import { convertData, TO, FROM } from './utils';
|
||||
|
||||
/**
|
||||
* Utility method that attempts to extract errors from the response of a PATCH request in order to
|
||||
* display a warning or otherwise meaningful message to the user.
|
||||
*
|
||||
* @param {Error} error
|
||||
*/
|
||||
export function createDemographicsError(error) {
|
||||
const apiError = Object.create(error);
|
||||
// If the error received has the `httpResponseData` field in it, then we should have reason to
|
||||
// believe the Demographics service is alive and responding. Extract errors from fields where
|
||||
// appropriate so we can display them to the user.
|
||||
if (get(error, 'customAttributes.httpErrorResponseData')) {
|
||||
apiError.fieldErrors = JSON.parse(error.customAttributes.httpErrorResponseData);
|
||||
if (get(apiError, 'fieldErrors.gender_description')) {
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
apiError.fieldErrors.demographics_gender = apiError.fieldErrors.gender_description[0];
|
||||
delete apiError.fieldErrors.gender_description;
|
||||
} else if (get(apiError, 'fieldErrors.work_status_description')) {
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
apiError.fieldErrors.demographics_work_status = apiError.fieldErrors.work_status_description[0];
|
||||
delete apiError.fieldErrors.work_status_description;
|
||||
}
|
||||
// Otherwise, when the service is down, the error response will not contain a
|
||||
// `httpErrorResponseData` field. Add a generic 'demographicsError' field to the fieldErrors that
|
||||
// will trigger showing an Alert to the user to them them know the update was unsuccessful.
|
||||
} else {
|
||||
apiError.fieldErrors = {
|
||||
demographicsError: error.customAttributes.httpErrorType,
|
||||
};
|
||||
}
|
||||
|
||||
return apiError;
|
||||
}
|
||||
|
||||
/**
|
||||
* post all of the data related to demographics.
|
||||
* @param {Number} userId users are identified in the api by LMS id
|
||||
* @param {Object} commitValues { demographics }
|
||||
*/
|
||||
export async function postDemographics(userId) {
|
||||
const requestConfig = { headers: { 'Content-Type': 'application/json' } };
|
||||
const requestUrl = `${getConfig().DEMOGRAPHICS_BASE_URL}/demographics/api/v1/demographics/`;
|
||||
const commitValues = { user: userId };
|
||||
let data = {};
|
||||
|
||||
({ data } = await getAuthenticatedHttpClient()
|
||||
.post(requestUrl, commitValues, requestConfig)
|
||||
.catch((error) => {
|
||||
const apiError = createDemographicsError(error);
|
||||
throw apiError;
|
||||
}));
|
||||
|
||||
return convertData(data, FROM);
|
||||
}
|
||||
|
||||
/**
|
||||
* get all data related to the demographics.
|
||||
* @param {Number} userId users are identified in the api by LMS id
|
||||
*/
|
||||
export async function getDemographics(userId) {
|
||||
const requestUrl = `${getConfig().DEMOGRAPHICS_BASE_URL}/demographics/api/v1/demographics/${userId}/`;
|
||||
let data = {};
|
||||
|
||||
try {
|
||||
({ data } = await getAuthenticatedHttpClient()
|
||||
.get(requestUrl));
|
||||
|
||||
data = convertData(data, FROM);
|
||||
} catch (error) {
|
||||
const apiError = Object.create(error);
|
||||
// if the API called resulted in this user receiving a 404 then follow up with a POST call to
|
||||
// try and create the demographics entity on the backend
|
||||
if (apiError.customAttributes.httpErrorStatus) {
|
||||
if (apiError.customAttributes.httpErrorStatus === 404) {
|
||||
data = await postDemographics(userId);
|
||||
}
|
||||
} else {
|
||||
data = {
|
||||
user: userId,
|
||||
demographics_gender: '',
|
||||
demographics_gender_description: '',
|
||||
demographics_income: '',
|
||||
demographics_learner_education_level: '',
|
||||
demographics_parent_education_level: '',
|
||||
demographics_military_history: '',
|
||||
demographics_work_status: '',
|
||||
demographics_work_status_description: '',
|
||||
demographics_current_work_sector: '',
|
||||
demographics_future_work_sector: '',
|
||||
demographics_user_ethnicity: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* patch all of the data related to demographics.
|
||||
* @param {Number} userId users are identified in the api by LMS id
|
||||
* @param {Object} commitValues { demographics }
|
||||
*/
|
||||
export async function patchDemographics(userId, commitValues) {
|
||||
const requestUrl = `${getConfig().DEMOGRAPHICS_BASE_URL}/demographics/api/v1/demographics/${userId}/`;
|
||||
const convertedCommitValues = convertData(commitValues, TO);
|
||||
let data = {};
|
||||
|
||||
({ data } = await getAuthenticatedHttpClient()
|
||||
.patch(requestUrl, convertedCommitValues)
|
||||
.catch((error) => {
|
||||
const apiError = createDemographicsError(error);
|
||||
throw apiError;
|
||||
}));
|
||||
|
||||
return convertData(data, FROM);
|
||||
}
|
||||
|
||||
/**
|
||||
* retrieve the options for each field from the Demographics API
|
||||
*/
|
||||
export async function getDemographicsOptions() {
|
||||
const requestUrl = `${getConfig().DEMOGRAPHICS_BASE_URL}/demographics/api/v1/demographics/`;
|
||||
let data = {};
|
||||
|
||||
try {
|
||||
({ data } = await getAuthenticatedHttpClient().options(requestUrl));
|
||||
} catch (error) {
|
||||
// We are catching and suppressing errors here on purpose. If an error occurs during the
|
||||
// getDemographicsOptions call we will pass back an empty `data` object. Downstream we make
|
||||
// the assumption that if the demographicsOptions object is empty that there was an issue or
|
||||
// error communicating with the service/API.
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
export const TO = 'to';
|
||||
export const FROM = 'from';
|
||||
export const DEMOGRAPHICS_FIELDS = [
|
||||
'demographics_gender',
|
||||
'demographics_gender_description',
|
||||
'demographics_income',
|
||||
'demographics_learner_education_level',
|
||||
'demographics_parent_education_level',
|
||||
'demographics_military_history',
|
||||
'demographics_work_status',
|
||||
'demographics_work_status_description',
|
||||
'demographics_current_work_sector',
|
||||
'demographics_future_work_sector',
|
||||
'demographics_user_ethnicity',
|
||||
];
|
||||
|
||||
// Frontend wants (example):
|
||||
// demographics_user_ethnicity: ["asian", "white", "other"]
|
||||
//
|
||||
// Demographics wants (example):
|
||||
// user_ethnicity: [
|
||||
// { ethnicity: "asian" },
|
||||
// { ethnicity: "white" },
|
||||
// { ethnicity: "other" }
|
||||
// ]
|
||||
function convertEthnicity(ethnicityData, direction) {
|
||||
if (direction === FROM) {
|
||||
return ethnicityData.map(e => e.ethnicity);
|
||||
}
|
||||
|
||||
if (direction === TO) {
|
||||
return ethnicityData.map(e => ({ ethnicity: e }));
|
||||
}
|
||||
|
||||
return ethnicityData;
|
||||
}
|
||||
|
||||
// Handles conversion of data to/from Demographics IDA to/from format needed for
|
||||
// frontend
|
||||
// * handles ethnicity field
|
||||
// * adds/removes 'demographics' to/from key
|
||||
// * replace `null` with empty string or empty string with null
|
||||
export function convertData(dataObject, direction) {
|
||||
const converted = {};
|
||||
|
||||
Object.entries(dataObject).forEach(([key, value]) => {
|
||||
let newValue = value;
|
||||
|
||||
if (key.includes('ethnicity')) {
|
||||
newValue = convertEthnicity(value, direction);
|
||||
}
|
||||
|
||||
if (direction === TO) {
|
||||
converted[key.replace('demographics_', '')] = newValue || null;
|
||||
}
|
||||
|
||||
if (direction === FROM) {
|
||||
converted[`demographics_${key}`] = newValue || '';
|
||||
}
|
||||
});
|
||||
|
||||
return converted;
|
||||
}
|
||||
@@ -1,584 +0,0 @@
|
||||
/* eslint-disable no-import-assign */
|
||||
import * as auth from '@edx/frontend-platform/auth';
|
||||
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { Provider } from 'react-redux';
|
||||
import React from 'react';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import renderer from 'react-test-renderer';
|
||||
import DemographicsSection from '../DemographicsSection';
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
|
||||
const IntlDemographicsSection = injectIntl(DemographicsSection);
|
||||
|
||||
jest.mock('../../data/selectors', () => jest.fn().mockImplementation(() => ({ demographicsSectionSelector: () => ({}) })));
|
||||
|
||||
const mockStore = configureStore();
|
||||
|
||||
describe('DemographicsSection', () => {
|
||||
let props = {};
|
||||
let store = {};
|
||||
|
||||
const reduxWrapper = children => (
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>{children}</Provider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore();
|
||||
props = {
|
||||
updateDraft: jest.fn(),
|
||||
formValues: {
|
||||
demographics_gender: 'declined',
|
||||
demographics_gender_description: '',
|
||||
demographics_user_ethnicity: [],
|
||||
demographics_income: 'declined',
|
||||
demographics_military_history: 'declined',
|
||||
demographics_learner_education_level: 'declined',
|
||||
demographics_parent_education_level: 'declined',
|
||||
demographics_work_status: 'declined',
|
||||
demographics_work_status_description: '',
|
||||
demographics_current_work_sector: 'declined',
|
||||
demographics_future_work_sector: 'declined',
|
||||
demographics_user: 1,
|
||||
demographicsOptions: {
|
||||
actions: {
|
||||
POST: {
|
||||
gender: {
|
||||
choices: [
|
||||
{
|
||||
value: 'woman',
|
||||
display_name: 'Woman',
|
||||
},
|
||||
{
|
||||
value: 'man',
|
||||
display_name: 'Man',
|
||||
},
|
||||
{
|
||||
value: 'non-binary',
|
||||
display_name: 'Non-binary',
|
||||
},
|
||||
{
|
||||
value: 'self-describe',
|
||||
display_name: 'Prefer to self describe',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
income: {
|
||||
choices: [
|
||||
{
|
||||
value: 'less-than-10k',
|
||||
display_name: 'Less than US $10,000',
|
||||
},
|
||||
{
|
||||
value: '10k-25k',
|
||||
display_name: 'US $10,000 - $25,000',
|
||||
},
|
||||
{
|
||||
value: '25k-50k',
|
||||
display_name: 'US $25,000 - $50,000',
|
||||
},
|
||||
{
|
||||
value: '50k-75k',
|
||||
display_name: 'US $50,000 - $75,000',
|
||||
},
|
||||
{
|
||||
value: '75k-100k',
|
||||
display_name: 'US $75,000 - $100,000',
|
||||
},
|
||||
{
|
||||
value: 'over-100k',
|
||||
display_name: 'Over US $100,000',
|
||||
},
|
||||
{
|
||||
value: 'unsure',
|
||||
display_name: "I don't know",
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
learner_education_level: {
|
||||
choices: [
|
||||
{
|
||||
value: 'no-high-school',
|
||||
display_name: 'No High School',
|
||||
},
|
||||
{
|
||||
value: 'some-high-school',
|
||||
display_name: 'Some High School',
|
||||
},
|
||||
{
|
||||
value: 'high-school-ged-equivalent',
|
||||
display_name: 'High School diploma, GED, or equivalent',
|
||||
},
|
||||
{
|
||||
value: 'some-college',
|
||||
display_name: 'Some college, but no degree',
|
||||
},
|
||||
{
|
||||
value: 'associates',
|
||||
display_name: 'Associates degree',
|
||||
},
|
||||
{
|
||||
value: 'bachelors',
|
||||
display_name: 'Bachelors degree',
|
||||
},
|
||||
{
|
||||
value: 'masters',
|
||||
display_name: 'Masters degree',
|
||||
},
|
||||
{
|
||||
value: 'professional',
|
||||
display_name: 'Professional degree',
|
||||
},
|
||||
{
|
||||
value: 'doctorate',
|
||||
display_name: 'Doctorate degree',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
parent_education_level: {
|
||||
choices: [
|
||||
{
|
||||
value: 'no-high-school',
|
||||
display_name: 'No High School',
|
||||
},
|
||||
{
|
||||
value: 'some-high-school',
|
||||
display_name: 'Some High School',
|
||||
},
|
||||
{
|
||||
value: 'high-school-ged-equivalent',
|
||||
display_name: 'High School diploma, GED, or equivalent',
|
||||
},
|
||||
{
|
||||
value: 'some-college',
|
||||
display_name: 'Some college, but no degree',
|
||||
},
|
||||
{
|
||||
value: 'associates',
|
||||
display_name: 'Associates degree',
|
||||
},
|
||||
{
|
||||
value: 'bachelors',
|
||||
display_name: 'Bachelors degree',
|
||||
},
|
||||
{
|
||||
value: 'masters',
|
||||
display_name: 'Masters degree',
|
||||
},
|
||||
{
|
||||
value: 'professional',
|
||||
display_name: 'Professional degree',
|
||||
},
|
||||
{
|
||||
value: 'doctorate',
|
||||
display_name: 'Doctorate degree',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
military_history: {
|
||||
choices: [
|
||||
{
|
||||
value: 'never-served',
|
||||
display_name: 'Never served in the military',
|
||||
},
|
||||
{
|
||||
value: 'training',
|
||||
display_name: 'Only on active duty for training',
|
||||
},
|
||||
{
|
||||
value: 'active',
|
||||
display_name: 'Now on active duty',
|
||||
},
|
||||
{
|
||||
value: 'previously-active',
|
||||
display_name: 'On active duty in the past, but not now',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
work_status: {
|
||||
choices: [
|
||||
{
|
||||
value: 'full-time',
|
||||
display_name: 'Employed, working full-time',
|
||||
},
|
||||
{
|
||||
value: 'part-time',
|
||||
display_name: 'Employed, working part-time',
|
||||
},
|
||||
{
|
||||
value: 'self-employed',
|
||||
display_name: 'Self-Employed',
|
||||
},
|
||||
{
|
||||
value: 'not-employed-looking',
|
||||
display_name: 'Not employed, looking for work',
|
||||
},
|
||||
{
|
||||
value: 'not-employed-not-looking',
|
||||
display_name: 'Not employed, not looking for work',
|
||||
},
|
||||
{
|
||||
value: 'unable',
|
||||
display_name: 'Unable to work',
|
||||
},
|
||||
{
|
||||
value: 'retired',
|
||||
display_name: 'Retired',
|
||||
},
|
||||
{
|
||||
value: 'other',
|
||||
display_name: 'Other',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
current_work_sector: {
|
||||
choices: [
|
||||
{
|
||||
value: 'accommodation-food',
|
||||
display_name: 'Accommodation and Food Services',
|
||||
},
|
||||
{
|
||||
value: 'administrative-support-waste-remediation',
|
||||
display_name: 'Administrative and Support and Waste Management and Remediation Services',
|
||||
},
|
||||
{
|
||||
value: 'agriculture-forestry-fishing-hunting',
|
||||
display_name: 'Agriculture, Forestry, Fishing and Hunting',
|
||||
},
|
||||
{
|
||||
value: 'arts-entertainment-recreation',
|
||||
display_name: 'Arts, Entertainment, and Recreation',
|
||||
},
|
||||
{
|
||||
value: 'construction',
|
||||
display_name: 'Construction',
|
||||
},
|
||||
{
|
||||
value: 'educational',
|
||||
display_name: 'Education Services',
|
||||
},
|
||||
{
|
||||
value: 'finance-insurance',
|
||||
display_name: 'Finance and Insurance',
|
||||
},
|
||||
{
|
||||
value: 'healthcare-social',
|
||||
display_name: 'Health Care and Social Assistance',
|
||||
},
|
||||
{
|
||||
value: 'information',
|
||||
display_name: 'Information',
|
||||
},
|
||||
{
|
||||
value: 'management',
|
||||
display_name: 'Management of Companies and Enterprises',
|
||||
},
|
||||
{
|
||||
value: 'manufacturing',
|
||||
display_name: 'Manufacturing',
|
||||
},
|
||||
{
|
||||
value: 'mining-quarry-oil-gas',
|
||||
display_name: 'Mining, Quarrying, and Oil and Gas Extraction',
|
||||
},
|
||||
{
|
||||
value: 'professional-scientific-technical',
|
||||
display_name: 'Professional, Scientific, and Technical Services',
|
||||
},
|
||||
{
|
||||
value: 'public-admin',
|
||||
display_name: 'Public Administration',
|
||||
},
|
||||
{
|
||||
value: 'real-estate',
|
||||
display_name: 'Real Estate and Rental and Leasing',
|
||||
},
|
||||
{
|
||||
value: 'retail',
|
||||
display_name: 'Retail Trade',
|
||||
},
|
||||
{
|
||||
value: 'transport-warehousing',
|
||||
display_name: 'Transportation and Warehousing',
|
||||
},
|
||||
{
|
||||
value: 'utilities',
|
||||
display_name: 'Utilities',
|
||||
},
|
||||
{
|
||||
value: 'trade',
|
||||
display_name: 'Wholesale Trade',
|
||||
},
|
||||
{
|
||||
value: 'other',
|
||||
display_name: 'Other',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
future_work_sector: {
|
||||
choices: [
|
||||
{
|
||||
value: 'accommodation-food',
|
||||
display_name: 'Accommodation and Food Services',
|
||||
},
|
||||
{
|
||||
value: 'administrative-support-waste-remediation',
|
||||
display_name: 'Administrative and Support and Waste Management and Remediation Services',
|
||||
},
|
||||
{
|
||||
value: 'agriculture-forestry-fishing-hunting',
|
||||
display_name: 'Agriculture, Forestry, Fishing and Hunting',
|
||||
},
|
||||
{
|
||||
value: 'arts-entertainment-recreation',
|
||||
display_name: 'Arts, Entertainment, and Recreation',
|
||||
},
|
||||
{
|
||||
value: 'construction',
|
||||
display_name: 'Construction',
|
||||
},
|
||||
{
|
||||
value: 'educational',
|
||||
display_name: 'Education Services',
|
||||
},
|
||||
{
|
||||
value: 'finance-insurance',
|
||||
display_name: 'Finance and Insurance',
|
||||
},
|
||||
{
|
||||
value: 'healthcare-social',
|
||||
display_name: 'Health Care and Social Assistance',
|
||||
},
|
||||
{
|
||||
value: 'information',
|
||||
display_name: 'Information',
|
||||
},
|
||||
{
|
||||
value: 'management',
|
||||
display_name: 'Management of Companies and Enterprises',
|
||||
},
|
||||
{
|
||||
value: 'manufacturing',
|
||||
display_name: 'Manufacturing',
|
||||
},
|
||||
{
|
||||
value: 'mining-quarry-oil-gas',
|
||||
display_name: 'Mining, Quarrying, and Oil and Gas Extraction',
|
||||
},
|
||||
{
|
||||
value: 'professional-scientific-technical',
|
||||
display_name: 'Professional, Scientific, and Technical Services',
|
||||
},
|
||||
{
|
||||
value: 'public-admin',
|
||||
display_name: 'Public Administration',
|
||||
},
|
||||
{
|
||||
value: 'real-estate',
|
||||
display_name: 'Real Estate and Rental and Leasing',
|
||||
},
|
||||
{
|
||||
value: 'retail',
|
||||
display_name: 'Retail Trade',
|
||||
},
|
||||
{
|
||||
value: 'transport-warehousing',
|
||||
display_name: 'Transportation and Warehousing',
|
||||
},
|
||||
{
|
||||
value: 'utilities',
|
||||
display_name: 'Utilities',
|
||||
},
|
||||
{
|
||||
value: 'trade',
|
||||
display_name: 'Wholesale Trade',
|
||||
},
|
||||
{
|
||||
value: 'other',
|
||||
display_name: 'Other',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
user_ethnicity: {
|
||||
child: {
|
||||
children: {
|
||||
ethnicity: {
|
||||
choices: [
|
||||
{
|
||||
value: 'american-indian-or-alaska-native',
|
||||
display_name: 'American Indian or Alaska Native',
|
||||
},
|
||||
{
|
||||
value: 'asian',
|
||||
display_name: 'Asian',
|
||||
},
|
||||
{
|
||||
value: 'black-or-african-american',
|
||||
display_name: 'Black or African American',
|
||||
},
|
||||
{
|
||||
value: 'hispanic-latin-spanish',
|
||||
display_name: 'Hispanic, Latin, or Spanish origin',
|
||||
},
|
||||
{
|
||||
value: 'middle-eastern-or-north-african',
|
||||
display_name: 'Middle Eastern or North African',
|
||||
},
|
||||
{
|
||||
value: 'native-hawaiian-or-pacific-islander',
|
||||
display_name: 'Native Hawaiian or Other Pacific Islander',
|
||||
},
|
||||
{
|
||||
value: 'white',
|
||||
display_name: 'White',
|
||||
},
|
||||
{
|
||||
value: 'other',
|
||||
display_name: 'Some other race, ethnicity, or origin',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
formErrors: {},
|
||||
intl: {},
|
||||
forwardRef: () => {},
|
||||
drafts: {},
|
||||
};
|
||||
auth.getAuthenticatedHttpClient = jest.fn(() => ({
|
||||
patch: async () => ({
|
||||
data: { status: 200 },
|
||||
catch: () => {},
|
||||
}),
|
||||
}));
|
||||
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 1 }));
|
||||
});
|
||||
|
||||
it('should render', () => {
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render an Alert if an error occurs', () => {
|
||||
props = {
|
||||
...props,
|
||||
formErrors: {
|
||||
demographicsError: 'api-error',
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should set user input correctly when user provides gender self-description', () => {
|
||||
props = {
|
||||
...props,
|
||||
formValues: {
|
||||
...props.formValues,
|
||||
demographics_gender: 'self-describe',
|
||||
demographics_gender_description: 'test',
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should set user input correctly when user provides answers to work_status question', () => {
|
||||
props = {
|
||||
...props,
|
||||
formValues: {
|
||||
...props.formValues,
|
||||
demographics_work_status: 'other',
|
||||
demographics_work_status_description: 'test',
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render ethnicity text correctly', () => {
|
||||
props = {
|
||||
...props,
|
||||
formValues: {
|
||||
...props.formValues,
|
||||
demographics_user_ethnicity: ['asian'],
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render ethnicity correctly when multiple options are selected', () => {
|
||||
props = {
|
||||
...props,
|
||||
formValues: {
|
||||
...props.formValues,
|
||||
demographics_user_ethnicity: ['hispanic-latin-spanish', 'white'],
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render an Alert when demographicsOptions props are empty', () => {
|
||||
props = {
|
||||
...props,
|
||||
formValues: {
|
||||
demographicsOptions: null,
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,3953 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DemographicsSection should render 1`] = `
|
||||
<div
|
||||
className="account-section pt-3 mb-5"
|
||||
id="demographics-information"
|
||||
>
|
||||
<h2
|
||||
className="section-heading h4 mb-3"
|
||||
>
|
||||
Optional Information
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does localhost collect this information?
|
||||
<span
|
||||
className="pgn__hyperlink__external-icon"
|
||||
title="Opens in a new tab"
|
||||
>
|
||||
<span
|
||||
className="pgn__icon"
|
||||
data-testid="hyperlink-icon"
|
||||
style={
|
||||
{
|
||||
"height": "1em",
|
||||
"width": "1em",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
className="sr-only"
|
||||
>
|
||||
in a new tab
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
id="demographics-fields"
|
||||
>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Gender identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Race/Ethnicity identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
<button
|
||||
className="p-0 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Add race/ethnicity identity
|
||||
</button>
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Family income
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
U.S. Military status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Your education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Parents/Guardians education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Employment status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Current work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Future work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
|
||||
<div
|
||||
className="account-section pt-3 mb-5"
|
||||
id="demographics-information"
|
||||
>
|
||||
<h2
|
||||
className="section-heading h4 mb-3"
|
||||
>
|
||||
Optional Information
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does localhost collect this information?
|
||||
<span
|
||||
className="pgn__hyperlink__external-icon"
|
||||
title="Opens in a new tab"
|
||||
>
|
||||
<span
|
||||
className="pgn__icon"
|
||||
data-testid="hyperlink-icon"
|
||||
style={
|
||||
{
|
||||
"height": "1em",
|
||||
"width": "1em",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
className="sr-only"
|
||||
>
|
||||
in a new tab
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
tabIndex="-1"
|
||||
>
|
||||
<div
|
||||
className="alert d-flex align-items-start alert alert-danger"
|
||||
>
|
||||
<div />
|
||||
<div>
|
||||
An error occurred attempting to retrieve or save your account information. Please try again later.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="demographics-fields"
|
||||
>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Gender identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Race/Ethnicity identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
<button
|
||||
className="p-0 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Add race/ethnicity identity
|
||||
</button>
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Family income
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
U.S. Military status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Your education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Parents/Guardians education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Employment status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Current work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Future work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DemographicsSection should render an Alert when demographicsOptions props are empty 1`] = `
|
||||
<div
|
||||
className="account-section pt-3 mb-5"
|
||||
id="demographics-information"
|
||||
>
|
||||
<h2
|
||||
className="section-heading h4 mb-3"
|
||||
>
|
||||
Optional Information
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does localhost collect this information?
|
||||
<span
|
||||
className="pgn__hyperlink__external-icon"
|
||||
title="Opens in a new tab"
|
||||
>
|
||||
<span
|
||||
className="pgn__icon"
|
||||
data-testid="hyperlink-icon"
|
||||
style={
|
||||
{
|
||||
"height": "1em",
|
||||
"width": "1em",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
className="sr-only"
|
||||
>
|
||||
in a new tab
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
tabIndex="-1"
|
||||
>
|
||||
<div
|
||||
className="alert d-flex align-items-start alert alert-danger"
|
||||
>
|
||||
<div />
|
||||
<div>
|
||||
An error occurred attempting to retrieve or save your account information. Please try again later.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DemographicsSection should render ethnicity correctly when multiple options are selected 1`] = `
|
||||
<div
|
||||
className="account-section pt-3 mb-5"
|
||||
id="demographics-information"
|
||||
>
|
||||
<h2
|
||||
className="section-heading h4 mb-3"
|
||||
>
|
||||
Optional Information
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does localhost collect this information?
|
||||
<span
|
||||
className="pgn__hyperlink__external-icon"
|
||||
title="Opens in a new tab"
|
||||
>
|
||||
<span
|
||||
className="pgn__icon"
|
||||
data-testid="hyperlink-icon"
|
||||
style={
|
||||
{
|
||||
"height": "1em",
|
||||
"width": "1em",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
className="sr-only"
|
||||
>
|
||||
in a new tab
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
id="demographics-fields"
|
||||
>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Gender identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Race/Ethnicity identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Hispanic, Latin, or Spanish origin, White
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Family income
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
U.S. Military status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Your education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Parents/Guardians education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Employment status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Current work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Future work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DemographicsSection should render ethnicity text correctly 1`] = `
|
||||
<div
|
||||
className="account-section pt-3 mb-5"
|
||||
id="demographics-information"
|
||||
>
|
||||
<h2
|
||||
className="section-heading h4 mb-3"
|
||||
>
|
||||
Optional Information
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does localhost collect this information?
|
||||
<span
|
||||
className="pgn__hyperlink__external-icon"
|
||||
title="Opens in a new tab"
|
||||
>
|
||||
<span
|
||||
className="pgn__icon"
|
||||
data-testid="hyperlink-icon"
|
||||
style={
|
||||
{
|
||||
"height": "1em",
|
||||
"width": "1em",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
className="sr-only"
|
||||
>
|
||||
in a new tab
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
id="demographics-fields"
|
||||
>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Gender identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Race/Ethnicity identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Asian
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Family income
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
U.S. Military status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Your education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Parents/Guardians education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Employment status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Current work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Future work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DemographicsSection should set user input correctly when user provides answers to work_status question 1`] = `
|
||||
<div
|
||||
className="account-section pt-3 mb-5"
|
||||
id="demographics-information"
|
||||
>
|
||||
<h2
|
||||
className="section-heading h4 mb-3"
|
||||
>
|
||||
Optional Information
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does localhost collect this information?
|
||||
<span
|
||||
className="pgn__hyperlink__external-icon"
|
||||
title="Opens in a new tab"
|
||||
>
|
||||
<span
|
||||
className="pgn__icon"
|
||||
data-testid="hyperlink-icon"
|
||||
style={
|
||||
{
|
||||
"height": "1em",
|
||||
"width": "1em",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
className="sr-only"
|
||||
>
|
||||
in a new tab
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
id="demographics-fields"
|
||||
>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Gender identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Race/Ethnicity identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
<button
|
||||
className="p-0 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Add race/ethnicity identity
|
||||
</button>
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Family income
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
U.S. Military status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Your education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Parents/Guardians education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Employment status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Other: test
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Current work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Future work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DemographicsSection should set user input correctly when user provides gender self-description 1`] = `
|
||||
<div
|
||||
className="account-section pt-3 mb-5"
|
||||
id="demographics-information"
|
||||
>
|
||||
<h2
|
||||
className="section-heading h4 mb-3"
|
||||
>
|
||||
Optional Information
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does localhost collect this information?
|
||||
<span
|
||||
className="pgn__hyperlink__external-icon"
|
||||
title="Opens in a new tab"
|
||||
>
|
||||
<span
|
||||
className="pgn__icon"
|
||||
data-testid="hyperlink-icon"
|
||||
style={
|
||||
{
|
||||
"height": "1em",
|
||||
"width": "1em",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
className="sr-only"
|
||||
>
|
||||
in a new tab
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
id="demographics-fields"
|
||||
>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Gender identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer to self describe: test
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Race/Ethnicity identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
<button
|
||||
className="p-0 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Add race/ethnicity identity
|
||||
</button>
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Family income
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
U.S. Military status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Your education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Parents/Guardians education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Employment status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Current work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Future work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -23,7 +23,7 @@ const messages = defineMessages({
|
||||
},
|
||||
'account.settings.name.change.id.name.label': {
|
||||
id: 'account.settings.name.change.id.name.label',
|
||||
defaultMessage: 'Enter your name as it appears on your unexpired student, work, or government-issued identification card.',
|
||||
defaultMessage: 'Enter your name as it appears on your identification card.',
|
||||
description: 'Form label instructing the user to enter the name on their ID.',
|
||||
},
|
||||
'account.settings.name.change.id.name.placeholder': {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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} />));
|
||||
|
||||
|
||||
@@ -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';
|
||||
@@ -11,7 +11,6 @@ const IntlJumpNav = injectIntl(JumpNav);
|
||||
|
||||
describe('JumpNav', () => {
|
||||
mergeConfig({
|
||||
ENABLE_DEMOGRAPHICS_COLLECTION: false,
|
||||
ENABLE_ACCOUNT_DELETION: true,
|
||||
});
|
||||
|
||||
@@ -30,7 +29,6 @@ describe('JumpNav', () => {
|
||||
|
||||
props = {
|
||||
intl: {},
|
||||
displayDemographicsLink: false,
|
||||
};
|
||||
store = configureStore({
|
||||
notificationPreferences: {
|
||||
@@ -39,43 +37,39 @@ 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_DEMOGRAPHICS_COLLECTION: true,
|
||||
ENABLE_ACCOUNT_DELETION: true,
|
||||
});
|
||||
|
||||
props = {
|
||||
...props,
|
||||
displayDemographicsLink: true,
|
||||
};
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,8 +35,8 @@ exports[`EditableSelectField renders EditableSelectField correctly with editing
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
className="svg-inline--fa fa-pencil mr-1"
|
||||
data-icon="pencil"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
@@ -45,7 +45,7 @@ exports[`EditableSelectField renders EditableSelectField correctly with editing
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
d="M410.3 231l11.3-11.3-33.9-33.9-62.1-62.1L291.7 89.8l-11.3 11.3-22.6 22.6L58.6 322.9c-10.4 10.4-18 23.3-22.2 37.4L1 480.7c-2.5 8.4-.2 17.5 6.1 23.7s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L387.7 253.7 410.3 231zM160 399.4l-9.1 22.7c-4 3.1-8.5 5.4-13.3 6.9L59.4 452l23-78.1c1.4-4.9 3.8-9.4 6.9-13.3l22.7-9.1 0 32c0 8.8 7.2 16 16 16l32 0zM362.7 18.7L348.3 33.2 325.7 55.8 314.3 67.1l33.9 33.9 62.1 62.1 33.9 33.9 11.3-11.3 22.6-22.6 14.5-14.5c25-25 25-65.5 0-90.5L453.3 18.7c-25-25-65.5-25-90.5 0zm-47.4 168l-144 144c-6.2 6.2-16.4 6.2-22.6 0s-6.2-16.4 0-22.6l144-144c6.2-6.2 16.4-6.2 22.6 0s6.2 16.4 0 22.6z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
@@ -172,9 +172,7 @@ exports[`EditableSelectField renders EditableSelectField correctly with editing
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<div>
|
||||
|
||||
</div>
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
@@ -243,8 +241,8 @@ exports[`EditableSelectField renders EditableSelectField with an error 1`] = `
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
className="svg-inline--fa fa-pencil mr-1"
|
||||
data-icon="pencil"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
@@ -253,7 +251,7 @@ exports[`EditableSelectField renders EditableSelectField with an error 1`] = `
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
d="M410.3 231l11.3-11.3-33.9-33.9-62.1-62.1L291.7 89.8l-11.3 11.3-22.6 22.6L58.6 322.9c-10.4 10.4-18 23.3-22.2 37.4L1 480.7c-2.5 8.4-.2 17.5 6.1 23.7s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L387.7 253.7 410.3 231zM160 399.4l-9.1 22.7c-4 3.1-8.5 5.4-13.3 6.9L59.4 452l23-78.1c1.4-4.9 3.8-9.4 6.9-13.3l22.7-9.1 0 32c0 8.8 7.2 16 16 16l32 0zM362.7 18.7L348.3 33.2 325.7 55.8 314.3 67.1l33.9 33.9 62.1 62.1 33.9 33.9 11.3-11.3 22.6-22.6 14.5-14.5c25-25 25-65.5 0-90.5L453.3 18.7c-25-25-65.5-25-90.5 0zm-47.4 168l-144 144c-6.2 6.2-16.4 6.2-22.6 0s-6.2-16.4 0-22.6l144-144c6.2-6.2 16.4-6.2 22.6 0s6.2 16.4 0 22.6z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
@@ -312,8 +310,8 @@ exports[`EditableSelectField renders selectOptions when option does not have a g
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
className="svg-inline--fa fa-pencil mr-1"
|
||||
data-icon="pencil"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
@@ -322,7 +320,7 @@ exports[`EditableSelectField renders selectOptions when option does not have a g
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
d="M410.3 231l11.3-11.3-33.9-33.9-62.1-62.1L291.7 89.8l-11.3 11.3-22.6 22.6L58.6 322.9c-10.4 10.4-18 23.3-22.2 37.4L1 480.7c-2.5 8.4-.2 17.5 6.1 23.7s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L387.7 253.7 410.3 231zM160 399.4l-9.1 22.7c-4 3.1-8.5 5.4-13.3 6.9L59.4 452l23-78.1c1.4-4.9 3.8-9.4 6.9-13.3l22.7-9.1 0 32c0 8.8 7.2 16 16 16l32 0zM362.7 18.7L348.3 33.2 325.7 55.8 314.3 67.1l33.9 33.9 62.1 62.1 33.9 33.9 11.3-11.3 22.6-22.6 14.5-14.5c25-25 25-65.5 0-90.5L453.3 18.7c-25-25-65.5-25-90.5 0zm-47.4 168l-144 144c-6.2 6.2-16.4 6.2-22.6 0s-6.2-16.4 0-22.6l144-144c6.2-6.2 16.4-6.2 22.6 0s6.2 16.4 0 22.6z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
@@ -381,8 +379,8 @@ exports[`EditableSelectField renders selectOptions when option has a group 1`] =
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
className="svg-inline--fa fa-pencil mr-1"
|
||||
data-icon="pencil"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
@@ -391,7 +389,7 @@ exports[`EditableSelectField renders selectOptions when option has a group 1`] =
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
d="M410.3 231l11.3-11.3-33.9-33.9-62.1-62.1L291.7 89.8l-11.3 11.3-22.6 22.6L58.6 322.9c-10.4 10.4-18 23.3-22.2 37.4L1 480.7c-2.5 8.4-.2 17.5 6.1 23.7s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L387.7 253.7 410.3 231zM160 399.4l-9.1 22.7c-4 3.1-8.5 5.4-13.3 6.9L59.4 452l23-78.1c1.4-4.9 3.8-9.4 6.9-13.3l22.7-9.1 0 32c0 8.8 7.2 16 16 16l32 0zM362.7 18.7L348.3 33.2 325.7 55.8 314.3 67.1l33.9 33.9 62.1 62.1 33.9 33.9 11.3-11.3 22.6-22.6 14.5-14.5c25-25 25-65.5 0-90.5L453.3 18.7c-25-25-65.5-25-90.5 0zm-47.4 168l-144 144c-6.2 6.2-16.4 6.2-22.6 0s-6.2-16.4 0-22.6l144-144c6.2-6.2 16.4-6.2 22.6 0s6.2 16.4 0 22.6z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
@@ -450,8 +448,8 @@ exports[`EditableSelectField renders selectOptions with multiple groups 1`] = `
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
className="svg-inline--fa fa-pencil mr-1"
|
||||
data-icon="pencil"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
@@ -460,7 +458,7 @@ exports[`EditableSelectField renders selectOptions with multiple groups 1`] = `
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
d="M410.3 231l11.3-11.3-33.9-33.9-62.1-62.1L291.7 89.8l-11.3 11.3-22.6 22.6L58.6 322.9c-10.4 10.4-18 23.3-22.2 37.4L1 480.7c-2.5 8.4-.2 17.5 6.1 23.7s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L387.7 253.7 410.3 231zM160 399.4l-9.1 22.7c-4 3.1-8.5 5.4-13.3 6.9L59.4 452l23-78.1c1.4-4.9 3.8-9.4 6.9-13.3l22.7-9.1 0 32c0 8.8 7.2 16 16 16l32 0zM362.7 18.7L348.3 33.2 325.7 55.8 314.3 67.1l33.9 33.9 62.1 62.1 33.9 33.9 11.3-11.3 22.6-22.6 14.5-14.5c25-25 25-65.5 0-90.5L453.3 18.7c-25-25-65.5-25-90.5 0zm-47.4 168l-144 144c-6.2 6.2-16.4 6.2-22.6 0s-6.2-16.4 0-22.6l144-144c6.2-6.2 16.4-6.2 22.6 0s6.2 16.4 0 22.6z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
|
||||
@@ -1,197 +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="/#demographics-information"
|
||||
isActive={[Function]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Optional 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>
|
||||
`;
|
||||
@@ -19,10 +19,7 @@ const mockData = {
|
||||
},
|
||||
],
|
||||
gender: null,
|
||||
|
||||
'pref-lang': 'en',
|
||||
shouldDisplayDemographicsSection: false,
|
||||
demographicsOptions: false,
|
||||
},
|
||||
errors: {},
|
||||
confirmationValues: {},
|
||||
@@ -87,7 +84,7 @@ const mockData = {
|
||||
profileDataManager: null,
|
||||
},
|
||||
notificationPreferences: {
|
||||
showPreferences: false,
|
||||
showPreferences: true,
|
||||
courses: {
|
||||
status: 'success',
|
||||
courses: [],
|
||||
@@ -101,7 +98,7 @@ const mockData = {
|
||||
preferences: {
|
||||
status: 'idle',
|
||||
updatePreferenceStatus: 'idle',
|
||||
selectedCourse: null,
|
||||
selectedCourse: 'account',
|
||||
preferences: [],
|
||||
apps: [],
|
||||
nonEditable: {},
|
||||
|
||||
@@ -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
16
src/divider/Divider.jsx
Normal 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
2
src/divider/index.jsx
Normal file
@@ -0,0 +1,2 @@
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export { default as Divider } from './Divider';
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
import { breakpoints, useWindowSize } from '@openedx/paragon';
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -6,53 +6,55 @@ 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 Footer from '@edx/frontend-component-footer';
|
||||
import { FooterSlot } from '@edx/frontend-component-footer';
|
||||
|
||||
import configureStore from './data/configureStore';
|
||||
import AccountSettingsPage, { NotFoundPage } from './account-settings';
|
||||
import IdVerificationPage from './id-verification';
|
||||
import IdVerificationPageSlot from './plugin-slots/IdVerificationPageSlot';
|
||||
import messages from './i18n';
|
||||
|
||||
import './index.scss';
|
||||
import Head from './head/Head';
|
||||
import NotificationCourses from './notification-preferences/NotificationCourses';
|
||||
import NotificationPreferences from './notification-preferences/NotificationPreferences';
|
||||
|
||||
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>
|
||||
<Footer />
|
||||
</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="/notifications/:courseId" element={<NotificationPreferences />} />
|
||||
<Route path="/notifications" element={<NotificationCourses />} />
|
||||
<Route path="/id-verification/*" element={<IdVerificationPage />} />
|
||||
<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({
|
||||
@@ -63,10 +65,10 @@ initialize({
|
||||
config: () => {
|
||||
mergeConfig({
|
||||
SUPPORT_URL: process.env.SUPPORT_URL,
|
||||
ENABLE_DEMOGRAPHICS_COLLECTION: (process.env.ENABLE_DEMOGRAPHICS_COLLECTION || false),
|
||||
DEMOGRAPHICS_BASE_URL: process.env.DEMOGRAPHICS_BASE_URL,
|
||||
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,
|
||||
|
||||
@@ -58,10 +58,6 @@ $fa-font-path: "~font-awesome/fonts";
|
||||
padding-left: 0.625rem;
|
||||
}
|
||||
|
||||
.margin-left-2 {
|
||||
margin-left: 2px !important;
|
||||
}
|
||||
|
||||
.notification-sub-heading {
|
||||
font-size: 14px;
|
||||
line-height: 28px;
|
||||
@@ -113,6 +109,21 @@ $fa-font-path: "~font-awesome/fonts";
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.channel-column:last-child {
|
||||
border-right: 0px !important;
|
||||
}
|
||||
|
||||
.email-channel {
|
||||
width: 250px !important;
|
||||
}
|
||||
|
||||
.dropdown-item:active,
|
||||
.dropdown-item:focus,
|
||||
.btn-tertiary:not(:disabled):not(.disabled).active {
|
||||
background-color: $light-300 !important;
|
||||
}
|
||||
|
||||
|
||||
.line-height-36 {
|
||||
line-height: 36px;
|
||||
}
|
||||
@@ -120,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 {
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Dropdown, ModalPopup, Button, useToggle,
|
||||
} from '@openedx/paragon';
|
||||
import { ExpandLess, ExpandMore } from '@openedx/paragon/icons';
|
||||
import {
|
||||
Button, Dropdown, ModalPopup, useToggle,
|
||||
} 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';
|
||||
|
||||
const EmailCadences = ({
|
||||
email, onToggle, emailCadence, notificationType,
|
||||
@@ -14,6 +20,7 @@ const EmailCadences = ({
|
||||
const intl = useIntl();
|
||||
const [isOpen, open, close] = useToggle(false);
|
||||
const [target, setTarget] = useState(null);
|
||||
const updatePreferencesStatus = useSelector(selectUpdatePreferencesStatus());
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -21,10 +28,10 @@ const EmailCadences = ({
|
||||
ref={setTarget}
|
||||
variant="outline-primary"
|
||||
onClick={open}
|
||||
disabled={!email}
|
||||
disabled={!email || updatePreferencesStatus === LOADING_STATUS}
|
||||
size="sm"
|
||||
iconAfter={isOpen ? ExpandLess : ExpandMore}
|
||||
className="border-light-300 text-primary-500 justify-content-between ml-3.5 cadence-button"
|
||||
className="border-light-300 justify-content-between ml-3.5 cadence-button"
|
||||
>
|
||||
{intl.formatMessage(messages.emailCadence, { text: emailCadence })}
|
||||
</Button>
|
||||
@@ -37,14 +44,15 @@ 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}
|
||||
name="email_cadence"
|
||||
className="d-flex justify-content-start py-1.5"
|
||||
as={Button}
|
||||
variant="primary"
|
||||
variant="tertiary"
|
||||
name={EMAIL_CADENCE}
|
||||
className="d-flex justify-content-start py-1.5 font-size-14 cadence-button"
|
||||
size="inline"
|
||||
active={cadence === emailCadence}
|
||||
autoFocus={cadence === emailCadence}
|
||||
onClick={(event) => {
|
||||
onToggle(event, notificationType);
|
||||
@@ -63,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,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Container, Icon, Spinner, Button,
|
||||
} from '@openedx/paragon';
|
||||
import { ArrowForwardIos } from '@openedx/paragon/icons';
|
||||
import { fetchCourseList } from './data/thunks';
|
||||
import { selectCourseListStatus, selectCourseList, selectPagination } from './data/selectors';
|
||||
import {
|
||||
IDLE_STATUS,
|
||||
LOADING_STATUS,
|
||||
SUCCESS_STATUS,
|
||||
} from '../constants';
|
||||
import messages from './messages';
|
||||
import { NotFoundPage } from '../account-settings';
|
||||
import { useFeedbackWrapper } from '../hooks';
|
||||
|
||||
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);
|
||||
@@ -1,95 +0,0 @@
|
||||
/* eslint-disable no-import-assign */
|
||||
import { Provider } from 'react-redux';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import * as auth from '@edx/frontend-platform/auth';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import NotificationCourses from './NotificationCourses';
|
||||
import { defaultState } from './data/reducers';
|
||||
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();
|
||||
});
|
||||
});
|
||||
73
src/notification-preferences/NotificationCoursesDropdown.jsx
Normal file
73
src/notification-preferences/NotificationCoursesDropdown.jsx
Normal 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;
|
||||
@@ -1,64 +1,42 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Collapsible } from '@openedx/paragon';
|
||||
import classNames from 'classnames';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { Collapsible } from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import messages from './messages';
|
||||
import ToggleSwitch from './ToggleSwitch';
|
||||
import {
|
||||
selectPreferenceAppToggleValue,
|
||||
selectSelectedCourseId,
|
||||
selectUpdatePreferencesStatus,
|
||||
} from './data/selectors';
|
||||
import NotificationPreferenceColumn from './NotificationPreferenceColumn';
|
||||
import { updateAppPreferenceToggle } from './data/thunks';
|
||||
import { LOADING_STATUS } from '../constants';
|
||||
import { NOTIFICATION_CHANNELS } from './data/constants';
|
||||
import NotificationTypes from './NotificationTypes';
|
||||
import { useIsOnMobile } from '../hooks';
|
||||
import NotificationTypes from './NotificationTypes';
|
||||
import { notificationChannels, shouldHideAppPreferences } from './data/utils';
|
||||
import NotificationPreferenceColumn from './NotificationPreferenceColumn';
|
||||
import { selectPreferenceAppToggleValue, selectAppPreferences } from './data/selectors';
|
||||
|
||||
const NotificationPreferenceApp = ({ appId }) => {
|
||||
const dispatch = useDispatch();
|
||||
const intl = useIntl();
|
||||
const courseId = useSelector(selectSelectedCourseId());
|
||||
const appToggle = useSelector(selectPreferenceAppToggleValue(appId));
|
||||
const updatePreferencesStatus = useSelector(selectUpdatePreferencesStatus());
|
||||
const appPreferences = useSelector(selectAppPreferences(appId));
|
||||
const mobileView = useIsOnMobile();
|
||||
|
||||
const onChangeAppSettings = useCallback((event) => {
|
||||
dispatch(updateAppPreferenceToggle(courseId, appId, event.target.checked));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [appId]);
|
||||
|
||||
if (!courseId) {
|
||||
return null;
|
||||
}
|
||||
const NOTIFICATION_CHANNELS = notificationChannels();
|
||||
const hideAppPreferences = shouldHideAppPreferences(appPreferences, appId) || false;
|
||||
|
||||
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">
|
||||
<span className="mr-auto preference-app font-weight-bold">
|
||||
<span className={classNames('mr-auto preference-app font-weight-bold', { 'mb-2': !mobileView })}>
|
||||
{intl.formatMessage(messages.notificationAppTitle, { key: appId })}
|
||||
</span>
|
||||
<span className="d-flex" id={`${appId}-app-toggle`}>
|
||||
<ToggleSwitch
|
||||
name={appId}
|
||||
value={appToggle}
|
||||
onChange={onChangeAppSettings}
|
||||
disabled={updatePreferencesStatus === LOADING_STATUS}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
{!mobileView && <hr className="border-light-400 my-4" />}
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Body>
|
||||
<div className="d-flex flex-row justify-content-between">
|
||||
<div className="d-flex flex-row justify-content-between w-100">
|
||||
<NotificationTypes appId={appId} />
|
||||
{!mobileView && (
|
||||
<div className="d-flex">
|
||||
@@ -71,6 +49,7 @@ const NotificationPreferenceApp = ({ appId }) => {
|
||||
{mobileView && <hr className="border-light-400 my-4.5" />}
|
||||
</Collapsible.Body>
|
||||
</Collapsible.Advanced>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,45 +1,62 @@
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { NavItem } from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './messages';
|
||||
import ToggleSwitch from './ToggleSwitch';
|
||||
import {
|
||||
selectNonEditablePreferences,
|
||||
selectSelectedCourseId,
|
||||
selectUpdatePreferencesStatus,
|
||||
selectPreferencesOfApp,
|
||||
} from './data/selectors';
|
||||
import { updatePreferenceToggle, updateChannelPreferenceToggle } from './data/thunks';
|
||||
import { LOADING_STATUS } from '../constants';
|
||||
import EmailCadences from './EmailCadences';
|
||||
import { useIsOnMobile } from '../hooks';
|
||||
import ToggleSwitch from './ToggleSwitch';
|
||||
import EmailCadences from './EmailCadences';
|
||||
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();
|
||||
const intl = useIntl();
|
||||
const courseId = useSelector(selectSelectedCourseId());
|
||||
const appPreferences = useSelector(selectPreferencesOfApp(appId));
|
||||
const nonEditable = useSelector(selectNonEditablePreferences(appId));
|
||||
const appPreferences = useSelector(selectAppPreferences(appId));
|
||||
const updatePreferencesStatus = useSelector(selectUpdatePreferencesStatus());
|
||||
const mobileView = useIsOnMobile();
|
||||
const NOTIFICATION_CHANNELS = Object.values(notificationChannels());
|
||||
const hideAppPreferences = shouldHideAppPreferences(appPreferences, appId) || false;
|
||||
|
||||
const onChannelToggle = useCallback((event) => {
|
||||
const { id: notificationChannel } = event.target;
|
||||
const isPreferenceNonEditable = (preference) => nonEditable?.[preference.id]?.includes(notificationChannel);
|
||||
const getValue = useCallback((notificationChannel, innerText, checked) => {
|
||||
if (notificationChannel === EMAIL_CADENCE && courseId) {
|
||||
return innerText;
|
||||
}
|
||||
return checked;
|
||||
}, [courseId]);
|
||||
|
||||
const hasActivePreferences = appPreferences.some(
|
||||
(preference) => preference[notificationChannel] && !isPreferenceNonEditable(preference),
|
||||
);
|
||||
|
||||
dispatch(updateChannelPreferenceToggle(courseId, appId, notificationChannel, !hasActivePreferences));
|
||||
}, [appId, appPreferences, courseId, dispatch, nonEditable]);
|
||||
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 value = notificationChannel === 'email_cadence' ? event.target.innerText : event.target.checked;
|
||||
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,
|
||||
@@ -47,18 +64,18 @@ const NotificationPreferenceColumn = ({ appId, channel, appPreference }) => {
|
||||
notificationType,
|
||||
notificationChannel,
|
||||
value,
|
||||
emailCadence !== MIXED ? emailCadence : undefined,
|
||||
));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [appId]);
|
||||
}, [appPreferences, getValue, getEmailCadence, dispatch, courseId, appId]);
|
||||
|
||||
const renderPreference = (preference) => (
|
||||
(preference?.coreNotificationTypes?.length > 0 || preference.id !== 'core') && (
|
||||
<div
|
||||
key={`${preference.id}-${channel}`}
|
||||
id={`${preference.id}-${channel}`}
|
||||
className={classNames(
|
||||
'd-flex align-items-center justify-content-center mb-2 h-4.5 column-padding',
|
||||
{
|
||||
'pr-0': channel === 'email',
|
||||
'pl-0': channel === 'web' && mobileView,
|
||||
},
|
||||
)}
|
||||
@@ -67,11 +84,11 @@ const NotificationPreferenceColumn = ({ appId, channel, appPreference }) => {
|
||||
name={channel}
|
||||
value={preference[channel]}
|
||||
onChange={(event) => onToggle(event, preference.id)}
|
||||
disabled={nonEditable?.[preference.id]?.includes(channel) || updatePreferencesStatus === LOADING_STATUS}
|
||||
disabled={updatePreferencesStatus === LOADING_STATUS}
|
||||
id={`${preference.id}-${channel}`}
|
||||
className="my-1"
|
||||
/>
|
||||
{channel === 'email' && (
|
||||
{channel === EMAIL && (
|
||||
<EmailCadences
|
||||
email={preference.email}
|
||||
onToggle={onToggle}
|
||||
@@ -80,22 +97,24 @@ const NotificationPreferenceColumn = ({ appId, channel, appPreference }) => {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={classNames('d-flex flex-column', { 'border-right': channel !== 'email' })}>
|
||||
<div className={classNames('d-flex flex-column border-right channel-column')}>
|
||||
{!hideAppPreferences && mobileView && (
|
||||
<NavItem
|
||||
id={channel}
|
||||
key={channel}
|
||||
role="button"
|
||||
onClick={onChannelToggle}
|
||||
className={classNames('mb-3 header-label column-padding', {
|
||||
'pr-0': channel === 'email',
|
||||
'pl-0': channel === 'web' && mobileView,
|
||||
'pr-0': channel === NOTIFICATION_CHANNELS[NOTIFICATION_CHANNELS.length - 1],
|
||||
'pl-0': channel === 'web',
|
||||
})}
|
||||
>
|
||||
{intl.formatMessage(messages.notificationChannel, { text: channel })}
|
||||
</NavItem>
|
||||
)}
|
||||
{appPreference
|
||||
? renderPreference(appPreference)
|
||||
: appPreferences.map((preference) => (renderPreference(preference)))}
|
||||
|
||||
@@ -1,38 +1,30 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Container, Icon, Spinner, Hyperlink,
|
||||
} from '@openedx/paragon';
|
||||
import { ArrowBack } from '@openedx/paragon/icons';
|
||||
import {
|
||||
selectCourseListStatus,
|
||||
selectCourse,
|
||||
selectPreferenceAppsId,
|
||||
selectNotificationPreferencesStatus,
|
||||
selectCourseList,
|
||||
} from './data/selectors';
|
||||
import { fetchCourseList, fetchCourseNotificationPreferences } from './data/thunks';
|
||||
import { Spinner, NavItem } from '@openedx/paragon';
|
||||
|
||||
import { useIsOnMobile } from '../hooks';
|
||||
import messages from './messages';
|
||||
import NotificationPreferenceApp from './NotificationPreferenceApp';
|
||||
import { fetchCourseNotificationPreferences } from './data/thunks';
|
||||
import { LOADING_STATUS } from '../constants';
|
||||
import {
|
||||
FAILURE_STATUS,
|
||||
IDLE_STATUS,
|
||||
LOADING_STATUS,
|
||||
SUCCESS_STATUS,
|
||||
} from '../constants';
|
||||
import { NotFoundPage } from '../account-settings';
|
||||
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();
|
||||
const NOTIFICATION_CHANNELS = notificationChannels();
|
||||
const isLoading = notificationStatus === LOADING_STATUS || courseStatus === LOADING_STATUS;
|
||||
|
||||
const preferencesList = useMemo(() => (
|
||||
@@ -42,59 +34,50 @@ 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>
|
||||
{preferencesList}
|
||||
{isLoading && (
|
||||
<div className="h-100">
|
||||
{!mobileView && !isLoading && (
|
||||
<div className="d-flex flex-row justify-content-between float-right">
|
||||
<div className="d-flex">
|
||||
<Spinner
|
||||
variant="primary"
|
||||
animation="border"
|
||||
className="mx-auto my-auto"
|
||||
size="lg"
|
||||
data-testid="loading-spinner"
|
||||
/>
|
||||
{Object.values(NOTIFICATION_CHANNELS).map((channel) => (
|
||||
<div className={classNames('d-flex flex-column channel-column')}>
|
||||
<NavItem
|
||||
id={channel}
|
||||
key={channel}
|
||||
className={classNames('header-label column-padding', {
|
||||
'pr-0': channel === NOTIFICATION_CHANNELS[NOTIFICATION_CHANNELS.length - 1],
|
||||
'mr-2': channel === 'web',
|
||||
'email-channel ': channel === 'email',
|
||||
|
||||
})}
|
||||
>
|
||||
{intl.formatMessage(messages.notificationChannel, { text: channel })}
|
||||
</NavItem>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Container>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
/* eslint-disable no-import-assign */
|
||||
import { Provider } from 'react-redux';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import {
|
||||
fireEvent, render, screen, waitFor, act, within,
|
||||
} from '@testing-library/react';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
|
||||
import * as auth from '@edx/frontend-platform/auth';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import NotificationPreferences from './NotificationPreferences';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
|
||||
import { defaultState } from './data/reducers';
|
||||
import { FAILURE_STATUS, LOADING_STATUS, SUCCESS_STATUS } from '../constants';
|
||||
import NotificationPreferences from './NotificationPreferences';
|
||||
import { LOADING_STATUS, SUCCESS_STATUS } from '../constants';
|
||||
|
||||
const courseId = 'selected-course-id';
|
||||
|
||||
@@ -36,6 +36,7 @@ const defaultPreferences = {
|
||||
web: true,
|
||||
push: true,
|
||||
email: true,
|
||||
coreNotificationTypes: ['new_comment'],
|
||||
},
|
||||
{
|
||||
id: 'newComment',
|
||||
@@ -43,6 +44,7 @@ const defaultPreferences = {
|
||||
web: false,
|
||||
push: false,
|
||||
email: false,
|
||||
coreNotificationTypes: [],
|
||||
},
|
||||
{
|
||||
id: 'newAssignment',
|
||||
@@ -50,6 +52,7 @@ const defaultPreferences = {
|
||||
web: false,
|
||||
push: false,
|
||||
email: false,
|
||||
coreNotificationTypes: [],
|
||||
},
|
||||
{
|
||||
id: 'newGrade',
|
||||
@@ -57,6 +60,7 @@ const defaultPreferences = {
|
||||
web: false,
|
||||
push: false,
|
||||
email: false,
|
||||
coreNotificationTypes: [],
|
||||
},
|
||||
],
|
||||
nonEditable: {
|
||||
@@ -68,19 +72,12 @@ const defaultPreferences = {
|
||||
},
|
||||
};
|
||||
|
||||
const updateChannelPreferences = (toggleVal = false) => ({
|
||||
preferences: [
|
||||
{ id: 'core', appId: 'discussion', web: true },
|
||||
{ id: 'newComment', appId: 'discussion', web: toggleVal },
|
||||
{ id: 'newAssignment', appId: 'coursework', web: toggleVal },
|
||||
],
|
||||
});
|
||||
|
||||
const setupStore = (override = {}) => {
|
||||
const storeState = defaultState;
|
||||
storeState.courses = {
|
||||
status: SUCCESS_STATUS,
|
||||
courses: [
|
||||
{ id: '', name: 'Account' },
|
||||
{ id: 'selected-course-id', name: 'Selected Course' },
|
||||
],
|
||||
};
|
||||
@@ -142,13 +139,6 @@ describe('Notification Preferences', () => {
|
||||
expect(screen.queryAllByTestId('notification-preference')).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('update group on click', async () => {
|
||||
const wrapper = await render(notificationPreferences(store));
|
||||
const element = wrapper.container.querySelector('#discussion-app-toggle');
|
||||
await fireEvent.click(element);
|
||||
expect(mockDispatch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('update preference on click', async () => {
|
||||
const wrapper = await render(notificationPreferences(store));
|
||||
const element = wrapper.container.querySelector('#core-web');
|
||||
@@ -157,45 +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();
|
||||
});
|
||||
|
||||
it('updates all preferences in the column on web channel click', async () => {
|
||||
store = setupStore(updateChannelPreferences(true));
|
||||
const wrapper = render(notificationPreferences(store));
|
||||
|
||||
const getChannelSwitch = (id) => screen.queryByTestId(`${id}-web`);
|
||||
const notificationTypes = ['newComment', 'newAssignment'];
|
||||
|
||||
const verifyState = (toggleState) => {
|
||||
notificationTypes.forEach((notificationType) => {
|
||||
if (toggleState) {
|
||||
expect(getChannelSwitch(notificationType)).toBeChecked();
|
||||
} else {
|
||||
expect(getChannelSwitch(notificationType)).not.toBeChecked();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
verifyState(true);
|
||||
expect(getChannelSwitch('core')).toBeChecked();
|
||||
|
||||
const discussionApp = screen.queryByTestId('discussion-app');
|
||||
const webChannel = within(discussionApp).queryByText('Web');
|
||||
|
||||
await act(async () => {
|
||||
await fireEvent.click(webChannel);
|
||||
});
|
||||
|
||||
store = setupStore(updateChannelPreferences(false));
|
||||
wrapper.rerender(notificationPreferences(store));
|
||||
|
||||
await waitFor(() => {
|
||||
verifyState(false);
|
||||
expect(getChannelSwitch('core')).toBeChecked();
|
||||
});
|
||||
const element = screen.getByTestId('core-web');
|
||||
await fireEvent.click(element);
|
||||
expect(mockDispatch).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
53
src/notification-preferences/NotificationSettings.jsx
Normal file
53
src/notification-preferences/NotificationSettings.jsx
Normal file
@@ -0,0 +1,53 @@
|
||||
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: '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/index.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-decoration-underline ml-1"
|
||||
>
|
||||
{intl.formatMessage(messages.notificationPreferenceGuideLink)}
|
||||
</Hyperlink>
|
||||
</div>
|
||||
<NotificationCoursesDropdown />
|
||||
<NotificationPreferences courseId={courseId} />
|
||||
<div className="border border-light-700 my-6" />
|
||||
</Container>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationSettings;
|
||||
@@ -1,26 +1,29 @@
|
||||
import React from 'react';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Icon, OverlayTrigger, Tooltip,
|
||||
} from '@openedx/paragon';
|
||||
|
||||
import { InfoOutline } from '@openedx/paragon/icons';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Icon, OverlayTrigger, Tooltip } from '@openedx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
import { selectPreferencesOfApp } from './data/selectors';
|
||||
import { useIsOnMobile } from '../hooks';
|
||||
import { notificationChannels } from './data/utils';
|
||||
|
||||
import { selectAppPreferences } from './data/selectors';
|
||||
import NotificationPreferenceColumn from './NotificationPreferenceColumn';
|
||||
import { NOTIFICATION_CHANNELS } from './data/constants';
|
||||
|
||||
const NotificationTypes = ({ appId }) => {
|
||||
const intl = useIntl();
|
||||
const preferences = useSelector(selectPreferencesOfApp(appId));
|
||||
const preferences = useSelector(selectAppPreferences(appId));
|
||||
const mobileView = useIsOnMobile();
|
||||
const NOTIFICATION_CHANNELS = notificationChannels();
|
||||
|
||||
return (
|
||||
<div className="d-flex flex-column mr-auto px-0">
|
||||
{!mobileView && <span className="mb-3 header-label">{intl.formatMessage(messages.typeLabel)}</span>}
|
||||
{preferences.map(preference => (
|
||||
(preference?.coreNotificationTypes?.length > 0 || preference.id !== 'core') && (
|
||||
<>
|
||||
<div
|
||||
key={preference.id}
|
||||
@@ -53,6 +56,8 @@ const NotificationTypes = ({ appId }) => {
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Form } from '@openedx/paragon';
|
||||
import React from 'react';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Form } from '@openedx/paragon';
|
||||
|
||||
const ToggleSwitch = ({
|
||||
name,
|
||||
value,
|
||||
|
||||
@@ -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 => (
|
||||
@@ -47,12 +49,3 @@ export const updatePreferenceValue = (appId, preferenceName, notificationChannel
|
||||
value,
|
||||
})
|
||||
);
|
||||
|
||||
export const updateAppToggle = (courseId, appId, value) => dispatch => (
|
||||
dispatch({
|
||||
type: Actions.UPDATE_APP_PREFERENCE,
|
||||
courseId,
|
||||
appId,
|
||||
value,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
export const NOTIFICATION_CHANNELS = {
|
||||
WEB: 'web',
|
||||
EMAIL: 'email',
|
||||
};
|
||||
|
||||
export const EMAIL_CADENCE = {
|
||||
export const EMAIL_CADENCE_PREFERENCES = {
|
||||
DAILY: 'Daily',
|
||||
WEEKLY: 'Weekly',
|
||||
IMMEDIATELY: 'Immediately',
|
||||
NEVER: 'Never',
|
||||
};
|
||||
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',
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {},
|
||||
|
||||
@@ -28,7 +28,7 @@ export const selectPreferenceAppsId = () => state => (
|
||||
state.notificationPreferences.preferences.apps.map(app => app.id)
|
||||
);
|
||||
|
||||
export const selectPreferencesOfApp = appId => state => (
|
||||
export const selectAppPreferences = appId => state => (
|
||||
selectPreferences()(state).filter(preference => (
|
||||
preference.appId === appId
|
||||
))
|
||||
@@ -54,10 +54,6 @@ export const selectPreferenceNonEditableChannels = (appId, name) => state => (
|
||||
state?.notificationPreferences.preferences.nonEditable[appId]?.[name] || []
|
||||
);
|
||||
|
||||
export const selectNonEditablePreferences = appId => state => (
|
||||
state?.notificationPreferences.preferences.nonEditable[appId] || []
|
||||
);
|
||||
|
||||
export const selectSelectedCourseId = () => state => (
|
||||
state.notificationPreferences.preferences.selectedCourse
|
||||
);
|
||||
|
||||
@@ -15,16 +15,6 @@ export const getCourseList = async (page, pageSize) => {
|
||||
return data;
|
||||
};
|
||||
|
||||
export const patchAppPreferenceToggle = async (courseId, appId, value) => {
|
||||
const patchData = snakeCaseObject({
|
||||
notificationApp: appId,
|
||||
value,
|
||||
});
|
||||
const url = `${getConfig().LMS_BASE_URL}/api/notifications/configurations/${courseId}`;
|
||||
const { data } = await getAuthenticatedHttpClient().patch(url, patchData);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const patchPreferenceToggle = async (
|
||||
courseId,
|
||||
notificationApp,
|
||||
@@ -43,9 +33,21 @@ export const patchPreferenceToggle = async (
|
||||
return data;
|
||||
};
|
||||
|
||||
export const patchChannelPreferenceToggle = async (courseId, notificationApp, notificationChannel, value) => {
|
||||
const patchData = snakeCaseObject({ notificationApp, notificationChannel, value });
|
||||
const url = `${getConfig().LMS_BASE_URL}/api/notifications/channel/configurations/${courseId}`;
|
||||
const { data } = await getAuthenticatedHttpClient().patch(url, patchData);
|
||||
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;
|
||||
};
|
||||
|
||||
150
src/notification-preferences/data/thunk.test.js
Normal file
150
src/notification-preferences/data/thunk.test.js
Normal 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());
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { camelCaseObject } from '@edx/frontend-platform';
|
||||
import { EMAIL_CADENCE } from './constants';
|
||||
import camelCase from 'lodash.camelcase';
|
||||
import { EMAIL, EMAIL_CADENCE, EMAIL_CADENCE_PREFERENCES } from './constants';
|
||||
import {
|
||||
fetchCourseListSuccess,
|
||||
fetchCourseListFetching,
|
||||
@@ -7,16 +8,14 @@ import {
|
||||
fetchNotificationPreferenceFailed,
|
||||
fetchNotificationPreferenceFetching,
|
||||
fetchNotificationPreferenceSuccess,
|
||||
updateAppToggle,
|
||||
updatePreferenceValue,
|
||||
updateSelectedCourse,
|
||||
} from './actions';
|
||||
import {
|
||||
getCourseList,
|
||||
getCourseNotificationPreferences,
|
||||
patchAppPreferenceToggle,
|
||||
patchChannelPreferenceToggle,
|
||||
patchPreferenceToggle,
|
||||
postPreferenceToggle,
|
||||
} from './service';
|
||||
|
||||
const normalizeCourses = (responseData) => {
|
||||
@@ -39,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) => ({
|
||||
@@ -59,7 +79,9 @@ const normalizePreferences = (responseData) => {
|
||||
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 || [],
|
||||
}
|
||||
));
|
||||
nonEditable[appId] = preferences[appId].nonEditable;
|
||||
@@ -94,7 +116,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());
|
||||
@@ -102,17 +124,9 @@ export const fetchCourseNotificationPreferences = (courseId) => (
|
||||
}
|
||||
);
|
||||
|
||||
export const updateAppPreferenceToggle = (courseId, appId, value) => (
|
||||
export const setSelectedCourse = courseId => (
|
||||
async (dispatch) => {
|
||||
try {
|
||||
dispatch(updateAppToggle(courseId, appId, value));
|
||||
const data = await patchAppPreferenceToggle(courseId, appId, value);
|
||||
const normalizedData = normalizePreferences(camelCaseObject(data));
|
||||
dispatch(fetchNotificationPreferenceSuccess(courseId, normalizedData));
|
||||
} catch (errors) {
|
||||
dispatch(updateAppToggle(courseId, appId, !value));
|
||||
dispatch(fetchNotificationPreferenceFailed());
|
||||
}
|
||||
dispatch(updateSelectedCourse(courseId));
|
||||
}
|
||||
);
|
||||
|
||||
@@ -122,43 +136,70 @@ export const updatePreferenceToggle = (
|
||||
notificationType,
|
||||
notificationChannel,
|
||||
value,
|
||||
emailCadence,
|
||||
) => (
|
||||
async (dispatch) => {
|
||||
try {
|
||||
// Initially update the UI to give immediate feedback
|
||||
dispatch(updatePreferenceValue(
|
||||
notificationApp,
|
||||
notificationType,
|
||||
notificationChannel,
|
||||
!value,
|
||||
));
|
||||
const data = await patchPreferenceToggle(
|
||||
courseId,
|
||||
notificationApp,
|
||||
notificationType,
|
||||
notificationChannel,
|
||||
value,
|
||||
);
|
||||
const normalizedData = normalizePreferences(camelCaseObject(data));
|
||||
dispatch(fetchNotificationPreferenceSuccess(courseId, normalizedData));
|
||||
} catch (errors) {
|
||||
dispatch(updatePreferenceValue(
|
||||
notificationApp,
|
||||
notificationType,
|
||||
notificationChannel,
|
||||
!value,
|
||||
));
|
||||
dispatch(fetchNotificationPreferenceFailed());
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const updateChannelPreferenceToggle = (courseId, notificationApp, notificationChannel, value) => (
|
||||
async (dispatch) => {
|
||||
try {
|
||||
const data = await patchChannelPreferenceToggle(courseId, notificationApp, notificationChannel, value);
|
||||
const normalizedData = normalizePreferences(camelCaseObject(data));
|
||||
dispatch(fetchNotificationPreferenceSuccess(courseId, normalizedData));
|
||||
// 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,
|
||||
channel,
|
||||
channel === EMAIL_CADENCE ? undefined : toggleValue,
|
||||
cadence,
|
||||
);
|
||||
};
|
||||
|
||||
// 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,
|
||||
);
|
||||
|
||||
handleSuccessResponse(emailCadenceData, !courseId);
|
||||
}
|
||||
} catch (errors) {
|
||||
dispatch(updatePreferenceValue(
|
||||
notificationApp,
|
||||
notificationType,
|
||||
notificationChannel,
|
||||
!value,
|
||||
));
|
||||
dispatch(fetchNotificationPreferenceFailed());
|
||||
}
|
||||
}
|
||||
|
||||
15
src/notification-preferences/data/utils.js
Normal file
15
src/notification-preferences/data/utils.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
export const notificationChannels = () => ({ WEB: 'web', ...(getConfig().SHOW_EMAIL_CHANNEL === 'true' && { EMAIL: 'email' }) });
|
||||
|
||||
export const shouldHideAppPreferences = (preferences, appId) => {
|
||||
const appPreferences = preferences.filter(pref => pref.appId === appId);
|
||||
|
||||
if (appPreferences.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const firstPreference = appPreferences[0];
|
||||
|
||||
return firstPreference?.id === 'core' && (!firstPreference.coreNotificationTypes?.length);
|
||||
};
|
||||
@@ -22,12 +22,13 @@ const messages = defineMessages({
|
||||
id: 'notification.preference.title',
|
||||
defaultMessage: `{
|
||||
text, select,
|
||||
core {Core notifications}
|
||||
core {Activity notifications}
|
||||
newDiscussionPost {New discussion posts}
|
||||
newQuestionPost {New question posts}
|
||||
contentReported {Reported content}
|
||||
courseUpdate {Course updates}
|
||||
oraStaffNotification {ORA new submissions}
|
||||
courseUpdates {Course updates}
|
||||
oraStaffNotifications {New ORA submission for staff grading}
|
||||
oraGradeAssigned {Essay assignment grade received}
|
||||
other {{text}}
|
||||
}`,
|
||||
description: 'Display text for Notification Types',
|
||||
@@ -89,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;
|
||||
|
||||
87
src/plugin-slots/AdditionalProfileFieldsSlot/README.md
Normal file
87
src/plugin-slots/AdditionalProfileFieldsSlot/README.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# Additional Profile Fields
|
||||
|
||||
### Slot ID: `org.openedx.frontend.account.additional_profile_fields.v1`
|
||||
|
||||
## Description
|
||||
|
||||
This slot is used to replace/modify/hide the additional profile fields in the account page.
|
||||
|
||||
## Example
|
||||
The following `env.config.jsx` will extend the default fields with a additional custom fields through a simple example component.
|
||||
|
||||

|
||||
|
||||
### Using the Example Component
|
||||
Create a file named `env.config.jsx` at the MFE root with this:
|
||||
|
||||
```jsx
|
||||
import { PLUGIN_OPERATIONS, DIRECT_PLUGIN } from '@openedx/frontend-plugin-framework';
|
||||
import Example from './src/plugin-slots/AdditionalProfileFieldsSlot/example';
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
'org.openedx.frontend.account.additional_profile_fields.v1': {
|
||||
plugins: [
|
||||
{
|
||||
op: PLUGIN_OPERATIONS.Insert,
|
||||
widget: {
|
||||
id: 'additional_account_fields',
|
||||
type: DIRECT_PLUGIN,
|
||||
RenderWidget: Example,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
## Plugin Props
|
||||
|
||||
When implementing a plugin for this slot, the following props are available:
|
||||
|
||||
### `updateUserProfile`
|
||||
- **Type**: Function
|
||||
- **Description**: A function for updating the user's profile with new field values. This handles the API call to persist changes to the backend.
|
||||
- **Usage**: Pass an object containing the field updates to be saved to the user's profile preserving the required structure. The function automatically handles the persistence and UI updates.
|
||||
|
||||
#### Example
|
||||
``` javascript
|
||||
updateUserProfile({ extendedProfile: [{ fieldName: 'favorite_color', fieldValue: value }] });
|
||||
```
|
||||
|
||||
### `profileFieldValues`
|
||||
- **Type**: Array of Objects
|
||||
- **Description**: Contains the current values of all additional profile fields as an array of objects. Each object has a `fieldName` property (string) and a `fieldValue` property (which can be string, boolean, number, or other data types depending on the field type).
|
||||
- **Usage**: Access specific field values by finding the object with the matching `fieldName` and reading its `fieldValue` property. Use array methods like `find()` to locate specific fields.
|
||||
|
||||
#### Example
|
||||
```json
|
||||
[
|
||||
{
|
||||
"fieldName": "favorite_color",
|
||||
"fieldValue": "red"
|
||||
},
|
||||
{
|
||||
"fieldName": "data_authorization",
|
||||
"fieldValue": true
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
### `profileFieldErrors`
|
||||
- **Type**: Object
|
||||
- **Description**: Contains validation errors for profile fields. Each key corresponds to a field name, and the value is the error message.
|
||||
- **Usage**: Check for field-specific errors to display validation feedback to users.
|
||||
|
||||
### `formComponents`
|
||||
- **Type**: Object
|
||||
- **Description**: Provides access to reusable form components that are consistent with the rest of the account page styling and behavior. These components follow the platform's design system and include proper validation and accessibility features.
|
||||
- **Usage**: Use these components in your custom fields implementation to maintain UI consistency. Available components include `SwitchContent` for managing different UI states.
|
||||
|
||||
### `refreshUserProfile`
|
||||
- **Type**: Function
|
||||
- **Description**: A function that triggers a refresh of the user's profile data. This can be used after updating profile fields to ensure the UI reflects the latest data from the server.
|
||||
- **Usage**: Call this function when you need to manually reload the user profile information. Note that `updateUserProfile` typically handles data refresh automatically.
|
||||
104
src/plugin-slots/AdditionalProfileFieldsSlot/example/index.jsx
Normal file
104
src/plugin-slots/AdditionalProfileFieldsSlot/example/index.jsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Form, Button } from '@openedx/paragon';
|
||||
|
||||
/**
|
||||
* Straightforward example of how you could use the pluginProps provided by
|
||||
* the AdditionalProfileFieldsSlot to create a custom profile field.
|
||||
*
|
||||
* Here you can set a 'favorite_color' field with radio buttons and
|
||||
* save it to the user's profile, especifically to their `meta` in
|
||||
* the user's model. For more information, see the documentation:
|
||||
*
|
||||
* https://github.com/openedx/edx-platform/blob/master/openedx/core/djangoapps/user_api/README.rst#persisting-optional-user-metadata
|
||||
*/
|
||||
const Example = ({
|
||||
updateUserProfile, profileFieldValues, profileFieldErrors, formComponents: { SwitchContent } = {},
|
||||
}) => {
|
||||
const [formMode, setFormMode] = useState('default');
|
||||
|
||||
// Get current favorite color from profileFieldValues
|
||||
const currentColorField = profileFieldValues?.find(field => field.fieldName === 'favorite_color');
|
||||
const currentColor = currentColorField ? currentColorField.fieldValue : '';
|
||||
|
||||
const [value, setValue] = useState(currentColor);
|
||||
const handleChange = e => setValue(e.target.value);
|
||||
|
||||
// Get any validation errors for the favorite_color field
|
||||
const colorFieldError = profileFieldErrors?.favorite_color;
|
||||
|
||||
const handleSubmit = () => {
|
||||
try {
|
||||
updateUserProfile({ extendedProfile: [{ fieldName: 'favorite_color', fieldValue: value }] });
|
||||
setFormMode('default');
|
||||
} catch (error) {
|
||||
setFormMode('edit');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border .border-accent-500 p-3">
|
||||
<h3 className="h3">Example Additional Profile Fields Slot</h3>
|
||||
|
||||
<SwitchContent
|
||||
expression={formMode}
|
||||
cases={{
|
||||
default: (
|
||||
<>
|
||||
<h3 className="text-muted">
|
||||
{value ? `Selected value: ${value}` : 'No color selected'}
|
||||
</h3>
|
||||
<Button onClick={() => setFormMode('edit')}>Edit</Button>
|
||||
</>
|
||||
),
|
||||
edit: (
|
||||
<>
|
||||
<Form.Group>
|
||||
<Form.Label>Which Color?</Form.Label>
|
||||
<Form.RadioSet
|
||||
name="colors"
|
||||
onChange={handleChange}
|
||||
value={value}
|
||||
isInvalid={!!colorFieldError}
|
||||
>
|
||||
<Form.Radio value="red">Red</Form.Radio>
|
||||
<Form.Radio value="green">Green</Form.Radio>
|
||||
<Form.Radio value="blue">Blue</Form.Radio>
|
||||
</Form.RadioSet>
|
||||
{colorFieldError && (
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{colorFieldError}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.Group>
|
||||
|
||||
<Button onClick={handleSubmit} disabled={!value}>
|
||||
Save
|
||||
</Button>
|
||||
</>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Example.propTypes = {
|
||||
updateUserProfile: PropTypes.func.isRequired,
|
||||
profileFieldValues: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
fieldName: PropTypes.string.isRequired,
|
||||
fieldValue: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.bool,
|
||||
PropTypes.number,
|
||||
]).isRequired,
|
||||
}),
|
||||
),
|
||||
profileFieldErrors: PropTypes.objectOf(PropTypes.string),
|
||||
formComponents: PropTypes.shape({
|
||||
SwitchContent: PropTypes.elementType.isRequired,
|
||||
}),
|
||||
};
|
||||
|
||||
export default Example;
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
32
src/plugin-slots/AdditionalProfileFieldsSlot/index.jsx
Normal file
32
src/plugin-slots/AdditionalProfileFieldsSlot/index.jsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { camelCaseObject, snakeCaseObject } from '@edx/frontend-platform';
|
||||
|
||||
import { fetchSettings, saveSettings } from '../../account-settings/data/actions';
|
||||
|
||||
import SwitchContent from '../../account-settings/SwitchContent';
|
||||
|
||||
const AdditionalProfileFieldsSlot = () => {
|
||||
const dispatch = useDispatch();
|
||||
const extendedProfileValues = useSelector((state) => state.accountSettings.values.extended_profile);
|
||||
const errors = useSelector((state) => state.accountSettings.errors);
|
||||
|
||||
const pluginProps = {
|
||||
refreshUserProfile: (username) => dispatch(fetchSettings(username)),
|
||||
updateUserProfile: (params) => dispatch(saveSettings(null, null, snakeCaseObject(params))),
|
||||
profileFieldValues: camelCaseObject(extendedProfileValues),
|
||||
profileFieldErrors: errors,
|
||||
formComponents: {
|
||||
SwitchContent,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<PluginSlot
|
||||
id="org.openedx.frontend.account.additional_profile_fields.v1"
|
||||
pluginProps={pluginProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdditionalProfileFieldsSlot;
|
||||
53
src/plugin-slots/FooterSlot/README.md
Normal file
53
src/plugin-slots/FooterSlot/README.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# 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/).
|
||||
|
||||
## Example
|
||||
|
||||
The following `env.config.jsx` will replace the default footer.
|
||||
|
||||

|
||||
|
||||
with a simple custom footer
|
||||
|
||||

|
||||
|
||||
```jsx
|
||||
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
'org.openedx.frontend.layout.footer.v1': {
|
||||
plugins: [
|
||||
{
|
||||
// Hide the default footer
|
||||
op: PLUGIN_OPERATIONS.Hide,
|
||||
widgetId: 'default_contents',
|
||||
},
|
||||
{
|
||||
// Insert a custom footer
|
||||
op: PLUGIN_OPERATIONS.Insert,
|
||||
widget: {
|
||||
id: 'custom_footer',
|
||||
type: DIRECT_PLUGIN,
|
||||
RenderWidget: () => (
|
||||
<h1 style={{textAlign: 'center'}}>🦶</h1>
|
||||
),
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default config;
|
||||
```
|
||||
BIN
src/plugin-slots/FooterSlot/images/custom_footer.png
Normal file
BIN
src/plugin-slots/FooterSlot/images/custom_footer.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
BIN
src/plugin-slots/FooterSlot/images/default_footer.png
Normal file
BIN
src/plugin-slots/FooterSlot/images/default_footer.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.2 KiB |
48
src/plugin-slots/IdVerificationPageSlot/README.md
Normal file
48
src/plugin-slots/IdVerificationPageSlot/README.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# ID Verification Page Slot
|
||||
|
||||
### Slot ID: `org.openedx.frontend.account.id_verification_page.v1`
|
||||
|
||||
### Slot ID Aliases
|
||||
* `id_verification_page_plugin`
|
||||
|
||||
## Description
|
||||
|
||||
This slot is used to replace/modify the IDV Page.
|
||||
|
||||
The implementation of the `IdVerificationPageSlot` component lives in `src/plugin-slots/IdVerificationPageSlot/index.jsx`.
|
||||
|
||||
## Example
|
||||
|
||||
The following `env.config.jsx` will replace the default IDV Page.
|
||||
|
||||

|
||||
|
||||
```jsx
|
||||
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
'org.openedx.frontend.account.id_verification_page.v1': {
|
||||
plugins: [
|
||||
{
|
||||
// Insert a custom IDV Page
|
||||
op: PLUGIN_OPERATIONS.Insert,
|
||||
widget: {
|
||||
id: 'custom_id_verification_page',
|
||||
type: DIRECT_PLUGIN,
|
||||
RenderWidget: () => (
|
||||
<div>
|
||||
<p>This is the new IDV page</p>
|
||||
<a href="/">Go Home</a>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
```
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 8.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 220 KiB |
13
src/plugin-slots/IdVerificationPageSlot/index.jsx
Normal file
13
src/plugin-slots/IdVerificationPageSlot/index.jsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
import IdVerificationPage from '../../id-verification';
|
||||
|
||||
const IdVerificationPageSlot = () => (
|
||||
<PluginSlot
|
||||
id="org.openedx.frontend.account.id_verification_page.v1"
|
||||
idAliases={['id_verification_page_plugin']}
|
||||
>
|
||||
<IdVerificationPage />
|
||||
</PluginSlot>
|
||||
);
|
||||
|
||||
export default IdVerificationPageSlot;
|
||||
5
src/plugin-slots/README.md
Normal file
5
src/plugin-slots/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# `frontend-app-account` Plugin Slots
|
||||
|
||||
* [`org.openedx.frontend.layout.footer.v1`](./FooterSlot/)
|
||||
* [`org.openedx.frontend.account.id_verification_page.v1`](./IdVerificationPageSlot/)
|
||||
* [`org.openedx.frontend.account.additional_profile_fields.v1`](./AdditionalProfileFieldsSlot/)
|
||||
@@ -1,3 +1,11 @@
|
||||
import 'core-js/stable';
|
||||
import 'regenerator-runtime/runtime';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import MockedPluginSlot from './tests/MockedPluginSlot';
|
||||
|
||||
jest.mock('@openedx/frontend-plugin-framework', () => ({
|
||||
...jest.requireActual('@openedx/frontend-plugin-framework'),
|
||||
Plugin: () => 'Plugin',
|
||||
PluginSlot: MockedPluginSlot,
|
||||
}));
|
||||
|
||||
26
src/tests/MockedPluginSlot.jsx
Normal file
26
src/tests/MockedPluginSlot.jsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const MockedPluginSlot = ({ children, id }) => (
|
||||
<div data-testid={id}>
|
||||
PluginSlot_{id}
|
||||
{ children && <div>{children}</div> }
|
||||
</div>
|
||||
);
|
||||
|
||||
MockedPluginSlot.displayName = 'PluginSlot';
|
||||
|
||||
MockedPluginSlot.propTypes = {
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
]),
|
||||
id: PropTypes.string,
|
||||
};
|
||||
|
||||
MockedPluginSlot.defaultProps = {
|
||||
children: undefined,
|
||||
id: undefined,
|
||||
};
|
||||
|
||||
export default MockedPluginSlot;
|
||||
47
src/tests/MockedPluginSlot.test.jsx
Normal file
47
src/tests/MockedPluginSlot.test.jsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import MockedPluginSlot from './MockedPluginSlot';
|
||||
|
||||
describe('MockedPluginSlot', () => {
|
||||
it('renders mock plugin with "PluginSlot" text', () => {
|
||||
render(<MockedPluginSlot id="test_plugin" />);
|
||||
|
||||
const component = screen.getByText('PluginSlot_test_plugin');
|
||||
expect(component).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders as the slot children directly if there is content within', () => {
|
||||
render(
|
||||
<div role="article">
|
||||
<MockedPluginSlot id="test_plugin">
|
||||
<q role="note">How much wood could a woodchuck chuck if a woodchuck could chuck wood?</q>
|
||||
</MockedPluginSlot>
|
||||
</div>,
|
||||
);
|
||||
|
||||
const component = screen.getByRole('article');
|
||||
expect(component).toBeInTheDocument();
|
||||
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');
|
||||
});
|
||||
|
||||
it('renders mock plugin with a data-testid ', () => {
|
||||
render(
|
||||
<MockedPluginSlot id="guybrush">
|
||||
<q role="note">I am selling these fine leather jackets.</q>
|
||||
</MockedPluginSlot>,
|
||||
);
|
||||
|
||||
const component = screen.getByTestId('guybrush');
|
||||
expect(component).toBeInTheDocument();
|
||||
|
||||
const quote = component.querySelector('[role=note]');
|
||||
expect(quote).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,9 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
|
||||
import { useAsyncCall } from '../hooks';
|
||||
import { LOADING_STATUS, SUCCESS_STATUS, FAILURE_STATUS } from '../constants';
|
||||
import { FAILURE_STATUS, LOADING_STATUS, SUCCESS_STATUS } from '../constants';
|
||||
|
||||
const TestUseAsyncCallHookComponent = ({ asyncFunc }) => {
|
||||
const { status, data } = useAsyncCall(asyncFunc);
|
||||
|
||||
Reference in New Issue
Block a user