Compare commits

..

2 Commits

Author SHA1 Message Date
Sagirov Eugeniy
b23d72dd37 chore: update frontend-platform version to v4.2.0 2023-05-09 18:11:54 -03:00
Eugene Dyudyunov
c792b0ec65 feat: make password reset support URL configurable (#746)
* feat: make password reset support URL configurable

Replace the hardcoded `support.edx.org` value with the one from the env vars.

* fix: linting error
2023-03-02 12:26:32 +05:00
181 changed files with 44430 additions and 13308 deletions

8
.env
View File

@@ -1,10 +1,12 @@
ACCESS_TOKEN_COOKIE_NAME=''
ACCOUNT_PROFILE_URL=''
BASE_URL=''
COACHING_ENABLED=''
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=''
@@ -12,7 +14,6 @@ LOGIN_URL=''
LOGO_TRADEMARK_URL=''
LOGO_URL=''
LOGO_WHITE_URL=''
SHOW_EMAIL_CHANNEL=''
LOGOUT_URL=''
MARKETING_SITE_BASE_URL=''
NODE_ENV=''
@@ -25,11 +26,8 @@ STUDIO_BASE_URL=''
SUPPORT_URL=''
USER_INFO_COOKIE_NAME=''
ENABLE_COPPA_COMPLIANCE=''
ENABLE_ACCOUNT_DELETION=''
ENABLE_DOB_UPDATE=''
MARKETING_EMAILS_OPT_IN=''
APP_ID=
MFE_CONFIG_API_URL=
PASSWORD_RESET_SUPPORT_LINK=''
LEARNER_FEEDBACK_URL=''
SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT='https://help.edx.org/edxlearner/s/article/How-do-I-link-or-unlink-my-edX-account-to-a-social-media-account'

View File

@@ -1,10 +1,12 @@
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
ACCOUNT_PROFILE_URL='http://localhost:1995'
BASE_URL='localhost:1997'
COACHING_ENABLED=''
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'
@@ -25,12 +27,9 @@ STUDIO_BASE_URL=''
SUPPORT_URL='http://localhost:18000/support'
USER_INFO_COOKIE_NAME='edx-user-info'
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://help.edx.org/edxlearner/s/article/How-do-I-link-or-unlink-my-edX-account-to-a-social-media-account'

View File

@@ -1,9 +1,12 @@
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
BASE_URL='localhost:1997'
COACHING_ENABLED=''
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'
@@ -23,11 +26,7 @@ STUDIO_BASE_URL=''
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://help.edx.org/edxlearner/s/article/How-do-I-link-or-unlink-my-edX-account-to-a-social-media-account'

View File

@@ -1,4 +1,4 @@
// eslint-disable-next-line import/no-extraneous-dependencies
const { createConfig } = require('@openedx/frontend-build');
const { createConfig } = require('@edx/frontend-build');
module.exports = createConfig('eslint');

View File

@@ -1,24 +0,0 @@
### 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.

View File

@@ -16,4 +16,4 @@ jobs:
secrets:
GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }}
GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}

View File

@@ -1,20 +0,0 @@
# This workflow runs when a comment is made on the ticket
# If the comment starts with "label: " it tries to apply
# the label indicated in rest of comment.
# If the comment starts with "remove label: ", it tries
# to remove the indicated label.
# Note: Labels are allowed to have spaces and this script does
# not parse spaces (as often a space is legitimate), so the command
# "label: really long lots of words label" will apply the
# label "really long lots of words label"
name: Allows for the adding and removing of labels via comment
on:
issue_comment:
types: [created]
jobs:
add_remove_labels:
uses: openedx/.github/.github/workflows/add-remove-label-on-comment.yml@master

View File

@@ -9,21 +9,22 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node: [16]
npm: [8.5.0]
npm-test:
- i18n_extract
- lint
- test
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version-file: '.nvmrc'
node-version: ${{ matrix.node }}
- run: npm install -g npm@${{ matrix.npm }}
- run: make requirements
- run: make test NPM_TESTS=build
- run: make test NPM_TESTS=${{ matrix.npm-test }}
- name: Coverage
if: matrix.npm-test == 'test'
uses: codecov/codecov-action@v4
- name: upload coverage
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
fail_ci_if_error: false

View File

@@ -10,4 +10,5 @@ on:
jobs:
version-check:
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
uses: openedx/.github/.github/workflows/lockfileversion-check.yml@master

View File

@@ -1,12 +0,0 @@
# This workflow runs when a comment is made on the ticket
# If the comment starts with "assign me" it assigns the author to the
# ticket (case insensitive)
name: Assign comment author to ticket if they say "assign me"
on:
issue_comment:
types: [created]
jobs:
self_assign_by_comment:
uses: openedx/.github/.github/workflows/self-assign-issue.yml@master

View File

@@ -0,0 +1,41 @@
name: Update Browserlist DB
on:
schedule:
- cron: '0 0 * * 1'
workflow_dispatch:
jobs:
update-dep:
runs-on: ubuntu-latest
strategy:
matrix:
node: [16]
npm: [8.5.x]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node }}
- run: npm install -g npm@${{ matrix.npm }}
- run: make requirements
- name: Update dependencies
run: npx browserslist@latest --update-db
- name: setup testeng-ci
run: |
git clone https://github.com/edx/testeng-ci.git
cd $GITHUB_WORKSPACE/testeng-ci
pip install -r requirements/base.txt
- name: create pull request
id: createpullrequest
env:
GITHUB_TOKEN: ${{ secrets.requirements_bot_github_token }}
GITHUB_USER_EMAIL: ${{ secrets.requirements_bot_github_email }}
run: |
cd $GITHUB_WORKSPACE/testeng-ci
python -m jenkins.pull_request_creator --repo-root=$GITHUB_WORKSPACE \
--target-branch="master" --base-branch-name="update-browserslist" \
--commit-message="chore: update browserslist" --pr-title="Update browserslist DB" \
--pr-body="Updated browserlist DB" --delete-old-pull-requests --output-pr-url-for-github-action

View File

@@ -1,12 +0,0 @@
name: Update Browserslist DB
on:
schedule:
- cron: '0 0 * * 1'
workflow_dispatch:
jobs:
update-browserslist:
uses: openedx/.github/.github/workflows/update-browserslist-db.yml@master
secrets:
requirements_bot_github_token: ${{ secrets.requirements_bot_github_token }}

1
.gitignore vendored
View File

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

2
.nvmrc
View File

@@ -1 +1 @@
20
v16

9
.tx/config Normal file
View File

@@ -0,0 +1,9 @@
[main]
host = https://www.transifex.com
[o:open-edx:p:edx-platform:r:frontend-app-account]
file_filter = src/i18n/messages/<lang>.json
source_file = src/i18n/transifex_input.json
source_lang = en
type = KEYVALUEJSON

33
Makefile Normal file → Executable file
View File

@@ -1,12 +1,15 @@
export TRANSIFEX_RESOURCE = frontend-app-account
transifex_resource = frontend-app-account
transifex_langs = "ar,fr,es_419,zh_CN"
intl_imports = ./node_modules/.bin/intl-imports.js
transifex_utils = ./node_modules/.bin/transifex-utils.js
i18n = ./src/i18n
transifex_input = $(i18n)/transifex_input.json
tx_url1 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/translation/en/strings/
tx_url2 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/source/
# This directory must match .babelrc .
transifex_temp = ./temp/babel-plugin-formatjs
transifex_temp = ./temp/babel-plugin-react-intl
NPM_TESTS=build i18n_extract lint test
@@ -38,18 +41,20 @@ detect_changed_source_translations:
# Checking for changed translations...
git diff --exit-code $(i18n)
pull_translations:
rm -rf src/i18n/messages
mkdir src/i18n/messages
cd src/i18n/messages \
&& atlas pull $(ATLAS_OPTIONS) \
translations/frontend-platform/src/i18n/messages:frontend-platform \
translations/paragon/src/i18n/messages:paragon \
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
translations/frontend-app-account/src/i18n/messages:frontend-app-account
# 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
$(intl_imports) frontend-platform paragon frontend-component-header frontend-component-footer frontend-app-account
# Pulls translations from Transifex.
pull_translations:
tx pull -f --mode reviewed --languages=$(transifex_langs)
# This target is used by Travis.
validate-no-uncommitted-package-lock-changes:

View File

@@ -1,13 +1,12 @@
####################
frontend-app-account
####################
|ci-badge| |Codecov| |npm_version| |npm_downloads| |license| |semantic-release|
frontend-app-account
====================
********
Purpose
********
Please tag **@edx/community-engineering** on any PRs or issues. Thanks!
Introduction
------------
This is a micro-frontend application responsible for the display and updating of a user's account information.
@@ -16,30 +15,13 @@ 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)
***************
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
to the `relevant tutor-mfe documentation`_ to get started using it.
.. _Devstack: https://github.com/openedx/devstack
.. _Tutor: https://github.com/overhangio/tutor
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe#mfe-development
Installation
============
------------
This MFE is bundled with `Devstack <https://github.com/openedx/devstack>`_, see the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ section for setup instructions.
@@ -60,14 +42,8 @@ This MFE is bundled with `Devstack <https://github.com/openedx/devstack>`_, see
.. image:: ./docs/images/localhost_preview.png
Plugins
=======
This MFE can be customized using `Frontend Plugin Framework <https://github.com/openedx/frontend-plugin-framework>`_.
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>`__.
@@ -89,12 +65,28 @@ Examples:
The fully-qualified URL to the support page or email to request the support from in the target environment.
``ENABLE_ACCOUNT_DELETION``
edX-specific Environment Variables
**********************************
Example: ``'false'`` | ``''`` (empty strings are true)
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 the account deletion option, defaults to true.
To disable account deletion set ``ENABLE_ACCOUNT_DELETION`` to ``'false'`` (string), otherwise it will default to true.
``COACHING_ENABLED``
Example: ``true`` | ``''`` (empty strings are falsy)
Enables support for a section of the micro-frontend that helps users arrange for coaching sessions. Integrates with a private coaching plugin and is only used by edx.org.
``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:
@@ -105,132 +97,18 @@ Example build syntax with a single environment variable:
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>`__.
Cloning and Startup
===================
.. code-block::
1. Clone your new repo:
``git clone https://github.com/openedx/frontend-app-account.git``
2. Use node v18.x.
The current version of the micro-frontend build scripts support node 18.
Using other major versions of node *may* work, but this is unsupported. For
convenience, this repository includes an .nvmrc file to help in setting the
correct node version via `nvm <https://github.com/nvm-sh/nvm>`_.
3. Install npm dependencies:
``cd frontend-app-account && npm ci``
4. Start the dev server:
``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
===========
------------
None
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
=======
The code in this repository is licensed under the AGPLv3 unless otherwise
noted.
Please see `LICENSE <LICENSE>`_ for details.
Contributing
============
Contributions are very welcome. Please read `How To Contribute`_ for details.
.. _How To Contribute: https://openedx.org/r/how-to-contribute
This project is currently accepting all types of contributions, bug fixes,
security fixes, maintenance work, or new features. However, please make sure
to have a discussion about your new feature idea with the maintainers prior to
beginning development to maximize the chances of your change being accepted.
You can start a conversation by creating a new issue on this repo summarizing
your idea.
Getting Help
===========
If you're having trouble, we have discussion forums at
https://discuss.openedx.org where you can connect with others in the community.
Our real-time conversations are on Slack. You can request a `Slack
invitation`_, then join our `community Slack workspace`_. Because this is a
frontend repository, the best place to discuss it would be in the `#wg-frontend
channel`_.
For anything non-trivial, the best path is to open an issue in this repository
with as many details about the issue you are facing as you can provide.
https://github.com/openedx/frontend-app-account/issues
For more information about these options, see the `Getting Help`_ page.
.. _Slack invitation: https://openedx.org/slack
.. _community Slack workspace: https://openedx.slack.com/
.. _#wg-frontend channel: https://openedx.slack.com/archives/C04BM6YC7A6
.. _Getting Help: https://openedx.org/community/connect
The Open edX Code of Conduct
============================
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
=========================
Please do not report security issues in public. Please email security@openedx.org.
==============================

View File

@@ -1,18 +0,0 @@
# 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: ""
spec:
owner: group:edx-infinity
type: 'website'
lifecycle: 'production'

View File

@@ -1,4 +1,4 @@
const { createConfig } = require('@openedx/frontend-build');
const { createConfig } = require('@edx/frontend-build');
module.exports = createConfig('jest', {
setupFilesAfterEnv: [

42551
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,9 +10,8 @@
},
"scripts": {
"build": "fedx-scripts webpack",
"i18n_extract": "fedx-scripts formatjs extract",
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
"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,28 +27,27 @@
"extends @edx/browserslist-config"
],
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-component-header": "^5.6.0",
"@edx/frontend-platform": "8.1.2",
"@edx/openedx-atlas": "^0.6.0",
"@fortawesome/fontawesome-svg-core": "^6.6.0",
"@fortawesome/free-brands-svg-icons": "^6.6.0",
"@fortawesome/free-regular-svg-icons": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.6.0",
"@fortawesome/react-fontawesome": "0.2.2",
"@openedx/frontend-plugin-framework": "^1.2.2",
"@openedx/frontend-slot-footer": "^1.0.2",
"@openedx/paragon": "22.9.0",
"@tensorflow-models/blazeface": "0.1.0",
"@tensorflow/tfjs-converter": "4.22.0",
"@tensorflow/tfjs-core": "4.22.0",
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
"@edx/frontend-component-footer": "12.0.0",
"@edx/frontend-component-header": "4.0.0",
"@edx/frontend-platform": "4.2.0",
"@edx/paragon": "20.28.5",
"@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.1.16",
"@tensorflow-models/blazeface": "0.0.7",
"@tensorflow/tfjs-converter": "3.18.0",
"@tensorflow/tfjs-core": "3.18.0",
"bowser": "2.11.0",
"classnames": "2.5.1",
"core-js": "3.38.1",
"classnames": "2.3.2",
"core-js": "3.19.3",
"font-awesome": "4.7.0",
"form-urlencoded": "6.1.5",
"form-urlencoded": "4.0.1",
"formdata-polyfill": "4.0.10",
"jslib-html5-camera-photo": "3.3.4",
"history": "4.10.1",
"jslib-html5-camera-photo": "3.1.8",
"lodash.camelcase": "4.3.0",
"lodash.debounce": "4.0.8",
"lodash.findindex": "4.6.0",
@@ -59,37 +57,38 @@
"lodash.omit": "4.5.0",
"lodash.pick": "4.4.0",
"lodash.pickby": "4.6.0",
"lodash.snakecase": "4.1.1",
"long": "5.2.3",
"memoize-one": "^6.0.0",
"prop-types": "15.8.1",
"qs": "6.13.0",
"react": "17.0.2",
"react-dom": "17.0.2",
"long": "5.2.0",
"memoize-one": "5.2.1",
"prop-types": "15.7.2",
"qs": "6.10.3",
"react": "16.14.0",
"react-dom": "16.14.0",
"react-helmet": "6.1.0",
"react-redux": "7.2.9",
"react-router": "^6.25.1",
"react-router-dom": "^6.25.1",
"react-router-hash-link": "2.4.3",
"react-redux": "7.2.8",
"react-router": "5.2.1",
"react-router-dom": "5.3.0",
"react-router-hash-link": "1.2.2",
"react-scrollspy": "3.4.3",
"react-transition-group": "4.4.5",
"redux": "4.2.1",
"redux": "4.1.2",
"redux-devtools-extension": "2.13.9",
"redux-logger": "3.0.6",
"redux-saga": "1.3.0",
"redux-thunk": "2.4.2",
"regenerator-runtime": "0.14.1",
"reselect": "^5.1.1",
"universal-cookie": "7.2.2"
"redux-saga": "1.1.3",
"redux-thunk": "2.3.0",
"regenerator-runtime": "0.13.9",
"reselect": "4.0.0",
"universal-cookie": "4.0.4"
},
"devDependencies": {
"@edx/browserslist-config": "1.2.0",
"@edx/reactifex": "2.2.0",
"@openedx/frontend-build": "14.1.5",
"@testing-library/jest-dom": "6.6.3",
"@edx/browserslist-config": "1.1.1",
"@edx/frontend-build": "12.0.6",
"@edx/reactifex": "1.1.0",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "12.1.5",
"react-test-renderer": "17.0.2",
"enzyme": "3.11.0",
"enzyme-adapter-react-16": "1.15.6",
"react-test-renderer": "16.14.0",
"reactifex": "1.1.1",
"redux-mock-store": "1.5.5"
"redux-mock-store": "1.5.4"
}
}
}

View File

@@ -1,148 +1,17 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en-us">
<head>
<title>Account | <%= process.env.SITE_NAME %></title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="shortcut icon"
href="<%=htmlWebpackPlugin.options.FAVICON_URL%>"
type="image/x-icon"
/>
<% if (process.env.OPTIMIZELY_PROJECT_ID) { %>
<script src="<%= process.env.MARKETING_SITE_BASE_URL %>/optimizelyjs/<%= process.env.OPTIMIZELY_PROJECT_ID %>.js"></script>
<% } %>
<title>Account | <%= process.env.SITE_NAME %></title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon" />
<% if (process.env.OPTIMIZELY_PROJECT_ID) { %>
<script
src="<%= process.env.MARKETING_SITE_BASE_URL %>/optimizelyjs/<%= process.env.OPTIMIZELY_PROJECT_ID %>.js"
></script>
<% } %>
</head>
<body>
<!-- begin usabilla live embed code -->
<script defer type="text/javascript">
window.lightningjs ||
(function (n) {
var e = "lightningjs";
function t(e, t) {
var r, i, a, o, d, c;
return (
t && (t += (/\?/.test(t) ? "&" : "?") + "lv=1"),
n[e] ||
((r = window),
(i = document),
(a = e),
(o = i.location.protocol),
(d = "load"),
(c = 0),
(function () {
n[a] = function () {
var t = arguments,
i = this,
o = ++c,
d = (i && i != r && i.id) || 0;
function s() {
return (s.id = o), n[a].apply(s, arguments);
}
return (
(e.s = e.s || []).push([o, d, t]),
(s.then = function (n, t, r) {
var i = (e.fh[o] = e.fh[o] || []),
a = (e.eh[o] = e.eh[o] || []),
d = (e.ph[o] = e.ph[o] || []);
return (
n && i.push(n), t && a.push(t), r && d.push(r), s
);
}),
s
);
};
var e = (n[a]._ = {});
function s() {
e.P(d), (e.w = 1), n[a]("_load");
}
(e.fh = {}),
(e.eh = {}),
(e.ph = {}),
(e.l = t
? t.replace(/^\/\//, ("https:" == o ? o : "http:") + "//")
: t),
(e.p = { 0: +new Date() }),
(e.P = function (n) {
e.p[n] = new Date() - e.p[0];
}),
e.w && s(),
r.addEventListener
? r.addEventListener(d, s, !1)
: r.attachEvent("onload", s);
var l = function () {
function n() {
return [
"<!DOCTYPE ",
o,
"><",
o,
"><head></head><",
t,
"><",
r,
' src="',
e.l,
'"></',
r,
"></",
t,
"></",
o,
">",
].join("");
}
var t = "body",
r = "script",
o = "html",
d = i[t];
if (!d) return setTimeout(l, 100);
e.P(1);
var c,
s = i.createElement("div"),
h = s.appendChild(i.createElement("div")),
u = i.createElement("iframe");
(s.style.display = "none"),
(d.insertBefore(s, d.firstChild).id = "lightningjs-" + a),
(u.frameBorder = "0"),
(u.id = "lightningjs-frame-" + a),
/MSIE[ ]+6/.test(navigator.userAgent) &&
(u.src = "javascript:false"),
(u.allowTransparency = "true"),
h.appendChild(u);
try {
u.contentWindow.document.open();
} catch (n) {
(e.domain = i.domain),
(c =
"javascript:var d=document.open();d.domain='" +
i.domain +
"';"),
(u.src = c + "void(0);");
}
try {
var p = u.contentWindow.document;
p.write(n()), p.close();
} catch (e) {
u.src =
c +
'd.write("' +
n().replace(/"/g, String.fromCharCode(92) + '"') +
'");d.close();';
}
e.P(2);
};
e.l && l();
})()),
(n[e].lv = "1"),
n[e]
);
}
var r = (window.lightningjs = t(e));
(r.require = t), (r.modules = n);
})({});
</script>
<!-- end usabilla live embed code -->
<div id="root"></div>
</body>
</html>

View File

@@ -22,11 +22,6 @@
"pin"
],
"automerge": true
},
{
"matchPackagePatterns": ["@edx", "@openedx"],
"matchUpdateTypes": ["minor", "patch"],
"automerge": true
}
],
"timezone": "America/New_York"

View File

@@ -1,5 +1,5 @@
import { AppContext } from '@edx/frontend-platform/react';
import { getConfig, getQueryParameters } from '@edx/frontend-platform';
import { getConfig, history, getQueryParameters } from '@edx/frontend-platform';
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
@@ -14,9 +14,9 @@ import {
getLanguageList,
} from '@edx/frontend-platform/i18n';
import {
Hyperlink, Icon, Alert,
} from '@openedx/paragon';
import { CheckCircle, Error, WarningFilled } from '@openedx/paragon/icons';
Button, Hyperlink, Icon, Alert,
} from '@edx/paragon';
import { CheckCircle, Error, WarningFilled } from '@edx/paragon/icons';
import messages from './AccountSettingsPage.messages';
import {
@@ -31,7 +31,6 @@ import PageLoading from './PageLoading';
import JumpNav from './JumpNav';
import DeleteAccount from './delete-account';
import EditableField from './EditableField';
import EditableSelectField from './EditableSelectField';
import ResetPassword from './reset-password';
import NameChange from './name-change';
import ThirdPartyAuth from './third-party-auth';
@@ -45,19 +44,24 @@ import {
GENDER_OPTIONS,
COUNTRY_WITH_STATES,
COPPA_COMPLIANCE_YEAR,
WORK_EXPERIENCE_OPTIONS,
getStatesList,
} from './data/constants';
import { fetchSiteLanguages } from './site-language';
import { fetchCourseList } from '../notification-preferences/data/thunks';
import NotificationSettings from '../notification-preferences/NotificationSettings';
import { withLocation, withNavigate } from './hoc';
import CoachingToggle from './coaching/CoachingToggle';
import DemographicsSection from './demographics/DemographicsSection';
class AccountSettingsPage extends React.Component {
constructor(props, context) {
super(props, context);
// If there is a "duplicate_provider" query parameter, that's the backend's
// way of telling us that the provider account the user tried to link is already linked
// to another user account on the platform. We use this to display a message to that effect,
// and remove the parameter from the URL.
const duplicateTpaProvider = getQueryParameters().duplicate_provider;
if (duplicateTpaProvider !== undefined) {
history.replace(history.location.pathname);
}
this.state = {
duplicateTpaProvider,
};
@@ -65,6 +69,7 @@ class AccountSettingsPage extends React.Component {
this.navLinkRefs = {
'#basic-information': React.createRef(),
'#profile-information': React.createRef(),
'#demographics-information': React.createRef(),
'#social-media': React.createRef(),
'#site-preferences': React.createRef(),
'#linked-accounts': React.createRef(),
@@ -73,9 +78,8 @@ class AccountSettingsPage extends React.Component {
}
componentDidMount() {
this.props.fetchCourseList();
this.props.fetchSettings();
this.props.fetchSiteLanguages(this.props.navigate);
this.props.fetchSiteLanguages();
sendTrackingLogEvent('edx.user.settings.viewed', {
page: 'account',
visibility: null,
@@ -121,15 +125,7 @@ class AccountSettingsPage extends React.Component {
countryOptions: [{
value: '',
label: this.props.intl.formatMessage(messages['account.settings.field.country.options.empty']),
}].concat(
this.removeDisabledCountries(
getCountryList(locale).map(({ code, name }) => ({
value: code,
label: name,
disabled: this.isDisabledCountry(code),
})),
),
),
}].concat(getCountryList(locale).map(({ code, name }) => ({ value: code, label: name }))),
stateOptions: [{
value: '',
label: this.props.intl.formatMessage(messages['account.settings.field.state.options.empty']),
@@ -150,45 +146,14 @@ class AccountSettingsPage extends React.Component {
value: key,
label: this.props.intl.formatMessage(messages[`account.settings.field.gender.options.${key || 'empty'}`]),
})),
workExperienceOptions: WORK_EXPERIENCE_OPTIONS.map(key => ({
value: key,
label: key === '' ? this.props.intl.formatMessage(messages['account.settings.field.work.experience.options.empty']) : key,
})),
}));
removeDisabledCountries = (countryList) => {
const { disabledCountries, committedValues } = this.props;
if (!disabledCountries.length) {
return countryList;
}
return countryList.filter(({ value, disabled }) => {
const isUserCountry = value === committedValues.country;
return !disabled || isUserCountry;
});
};
handleEditableFieldChange = (name, value) => {
this.props.updateDraft(name, value);
};
handleSubmit = (formId, values) => {
if (formId === 'country' && this.isDisabledCountry(values)) {
return;
}
const { formValues } = this.props;
let extendedProfileObject = {};
if ('extended_profile' in formValues && formValues.extended_profile.some((field) => field.field_name === formId)) {
extendedProfileObject = {
extended_profile: formValues.extended_profile.map(field => (field.field_name === formId
? { ...field, field_value: values }
: field)),
};
}
this.props.saveSettings(formId, values, extendedProfileObject);
this.props.saveSettings(formId, values);
};
handleSubmitProfileName = (formId, values) => {
@@ -219,11 +184,6 @@ class AccountSettingsPage extends React.Component {
}
};
isDisabledCountry = (country) => {
const { disabledCountries } = this.props;
return disabledCountries.includes(country);
};
isEditable(fieldName) {
return !this.props.staticFields.includes(fieldName);
}
@@ -239,12 +199,6 @@ class AccountSettingsPage extends React.Component {
return null;
}
// If there is a "duplicate_provider" query parameter, that's the backend's
// way of telling us that the provider account the user tried to link is already linked
// to another user account on the platform. We use this to display a message to that effect,
// and remove the parameter from the URL.
this.props.navigate(this.props.location, { replace: true });
return (
<div>
<Alert variant="danger">
@@ -342,9 +296,19 @@ class AccountSettingsPage extends React.Component {
header={this.props.intl.formatMessage(messages['account.settings.field.name.verified.failure.message.header'])}
body={
(
<div className="d-flex flex-row">
{this.props.intl.formatMessage(messages['account.settings.field.name.verified.failure.message'])}
</div>
<>
<div className="d-flex flex-row">
{this.props.intl.formatMessage(messages['account.settings.field.name.verified.failure.message'])}
</div>
<div className="d-flex flex-row-reverse mt-3">
<Button
variant="primary"
href="https://support.edx.org/hc/en-us/articles/360004381594-Why-was-my-ID-verification-denied"
>
{this.props.intl.formatMessage(messages['account.settings.field.name.verified.failure.message.help.link'])}
</Button>{' '}
</div>
</>
)
}
/>
@@ -479,6 +443,16 @@ 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,
@@ -493,15 +467,12 @@ class AccountSettingsPage extends React.Component {
yearOfBirthOptions,
educationLevelOptions,
genderOptions,
workExperienceOptions,
} = this.getLocalizedOptions(this.context.locale, this.props.formValues.country);
// Show State field only if the country is US (could include Canada later)
const { country } = this.props.formValues;
const showState = country === COUNTRY_WITH_STATES && !this.isDisabledCountry(country);
const { verifiedName } = this.props;
const showState = this.props.formValues.country === COUNTRY_WITH_STATES;
const hasWorkExperience = !!this.props.formValues?.extended_profile?.find(field => field.field_name === 'work_experience');
const { verifiedName } = this.props;
const timeZoneOptions = this.getLocalizedTimeZoneOptions(
this.props.timeZoneOptions,
@@ -638,7 +609,7 @@ class AccountSettingsPage extends React.Component {
<ResetPassword email={this.props.formValues.email} />
{(!getConfig().ENABLE_COPPA_COMPLIANCE)
&& (
<EditableSelectField
<EditableField
name="year_of_birth"
type="select"
label={this.props.intl.formatMessage(messages['account.settings.field.dob'])}
@@ -648,7 +619,7 @@ class AccountSettingsPage extends React.Component {
{...editableFieldProps}
/>
)}
<EditableSelectField
<EditableField
name="country"
type="select"
value={this.props.formValues.country}
@@ -664,7 +635,7 @@ class AccountSettingsPage extends React.Component {
/>
{showState
&& (
<EditableSelectField
<EditableField
name="state"
type="select"
value={this.props.formValues.state}
@@ -686,18 +657,16 @@ class AccountSettingsPage extends React.Component {
{this.props.intl.formatMessage(messages['account.settings.section.profile.information'])}
</h2>
<EditableSelectField
<EditableField
name="level_of_education"
type="select"
value={this.props.formValues.level_of_education}
options={getConfig().ENABLE_COPPA_COMPLIANCE
? educationLevelOptions.filter(option => option.value !== 'el')
: educationLevelOptions}
options={educationLevelOptions}
label={this.props.intl.formatMessage(messages['account.settings.field.education'])}
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.education.empty'])}
{...editableFieldProps}
/>
<EditableSelectField
<EditableField
name="gender"
type="select"
value={this.props.formValues.gender}
@@ -706,19 +675,7 @@ class AccountSettingsPage extends React.Component {
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.gender.empty'])}
{...editableFieldProps}
/>
{hasWorkExperience
&& (
<EditableSelectField
name="work_experience"
type="select"
value={this.props.formValues?.extended_profile?.find(field => field.field_name === 'work_experience')?.field_value}
options={workExperienceOptions}
label={this.props.intl.formatMessage(messages['account.settings.field.work.experience'])}
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.work.experience.empty'])}
{...editableFieldProps}
/>
)}
<EditableSelectField
<EditableField
name="language_proficiencies"
type="select"
value={this.props.formValues.language_proficiencies}
@@ -727,8 +684,18 @@ class AccountSettingsPage extends React.Component {
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.language.proficiencies.empty'])}
{...editableFieldProps}
/>
{getConfig().COACHING_ENABLED
&& this.props.formValues.coaching.eligible_for_coaching
&& (
<CoachingToggle
name="coaching"
phone_number={this.props.formValues.phone_number}
coaching={this.props.formValues.coaching}
/>
)}
</div>
<div className="account-section pt-3" id="social-media">
{getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && this.renderDemographicsSection()}
<div className="account-section pt-3 mb-5" id="social-media">
<h2 className="section-heading h4 mb-3">
{this.props.intl.formatMessage(messages['account.settings.section.social.media'])}
</h2>
@@ -764,16 +731,14 @@ class AccountSettingsPage extends React.Component {
{...editableFieldProps}
/>
</div>
<div className="border border-light-700 my-6" />
<NotificationSettings />
<div className="border border-light-700 my-6" />
<div className="account-section mb-5" id="site-preferences" ref={this.navLinkRefs['#site-preferences']}>
<div className="account-section pt-3 mb-5" id="site-preferences" ref={this.navLinkRefs['#site-preferences']}>
<h2 className="section-heading h4 mb-3">
{this.props.intl.formatMessage(messages['account.settings.section.site.preferences'])}
</h2>
<BetaLanguageBanner />
<EditableSelectField
<EditableField
name="siteLanguage"
type="select"
options={this.props.siteLanguageOptions}
@@ -782,7 +747,7 @@ class AccountSettingsPage extends React.Component {
helpText={this.props.intl.formatMessage(messages['account.settings.field.site.language.help.text'])}
{...editableFieldProps}
/>
<EditableSelectField
<EditableField
name="time_zone"
type="select"
value={this.props.formValues.time_zone}
@@ -809,15 +774,12 @@ class AccountSettingsPage extends React.Component {
<ThirdPartyAuth />
</div>
{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}
/>
</div>
)}
<div className="account-section pt-3 mb-5" id="delete-account" ref={this.navLinkRefs['#delete-account']}>
<DeleteAccount
isVerifiedAccount={this.props.isActive}
hasLinkedTPA={hasLinkedTPA}
/>
</div>
</>
);
@@ -854,10 +816,12 @@ class AccountSettingsPage extends React.Component {
</h1>
<div>
<div className="row">
<div className="col-md-2">
<JumpNav />
<div className="col-md-3">
<JumpNav
displayDemographicsLink={this.props.formValues.shouldDisplayDemographicsSection}
/>
</div>
<div className="col-md-10">
<div className="col-md-9">
{loading ? this.renderLoading() : null}
{loaded ? this.renderContent() : null}
{loadingError ? this.renderError() : null}
@@ -888,7 +852,6 @@ AccountSettingsPage.propTypes = {
country: PropTypes.string,
level_of_education: PropTypes.string,
gender: PropTypes.string,
extended_profile: PropTypes.string,
language_proficiencies: PropTypes.string,
pending_name_change: PropTypes.string,
phone_number: PropTypes.string,
@@ -896,7 +859,13 @@ AccountSettingsPage.propTypes = {
social_link_facebook: PropTypes.string,
social_link_twitter: PropTypes.string,
time_zone: PropTypes.string,
coaching: PropTypes.shape({
coaching_consent: PropTypes.bool.isRequired,
user: PropTypes.number.isRequired,
eligible_for_coaching: PropTypes.bool.isRequired,
}),
state: PropTypes.string,
shouldDisplayDemographicsSection: PropTypes.bool,
useVerifiedNameForCerts: PropTypes.bool.isRequired,
verified_name: PropTypes.string,
}).isRequired,
@@ -904,7 +873,6 @@ AccountSettingsPage.propTypes = {
name: PropTypes.string,
useVerifiedNameForCerts: PropTypes.bool,
verified_name: PropTypes.string,
country: PropTypes.string,
}),
drafts: PropTypes.shape({}),
formErrors: PropTypes.shape({
@@ -937,7 +905,6 @@ AccountSettingsPage.propTypes = {
saveSettings: PropTypes.func.isRequired,
fetchSettings: PropTypes.func.isRequired,
beginNameChange: PropTypes.func.isRequired,
fetchCourseList: PropTypes.func.isRequired,
tpaProviders: PropTypes.arrayOf(PropTypes.shape({
connected: PropTypes.bool,
})),
@@ -961,9 +928,6 @@ AccountSettingsPage.propTypes = {
proctored_exam_attempt_id: PropTypes.number,
}),
),
navigate: PropTypes.func.isRequired,
location: PropTypes.string.isRequired,
disabledCountries: PropTypes.arrayOf(PropTypes.string),
};
AccountSettingsPage.defaultProps = {
@@ -973,7 +937,6 @@ AccountSettingsPage.defaultProps = {
committedValues: {
useVerifiedNameForCerts: false,
verified_name: null,
country: '',
},
drafts: {},
formErrors: {},
@@ -990,15 +953,13 @@ AccountSettingsPage.defaultProps = {
verifiedName: null,
mostRecentVerifiedName: {},
verifiedNameHistory: [],
disabledCountries: [],
};
export default withLocation(withNavigate(connect(accountSettingsPageSelector, {
fetchCourseList,
export default connect(accountSettingsPageSelector, {
fetchSettings,
saveSettings,
saveMultipleSettings,
updateDraft,
fetchSiteLanguages,
beginNameChange,
})(injectIntl(AccountSettingsPage))));
})(injectIntl(AccountSettingsPage));

View File

@@ -46,6 +46,11 @@ 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',
@@ -141,6 +146,11 @@ 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.',
@@ -266,16 +276,6 @@ const messages = defineMessages({
defaultMessage: 'Year',
description: 'Label for account settings year of birth field.',
},
'account.settings.field.dob.month.default': {
id: 'account.settings.field.month.year.default',
defaultMessage: 'Select month',
description: 'Default label for account settings month of birth field.',
},
'account.settings.field.dob.year.default': {
id: 'account.settings.field.dob.year.default',
defaultMessage: 'Select year',
description: 'Default label for account settings year of birth field.',
},
'account.settings.field.dob.form.button': {
id: 'account.settings.field.dob.form.button',
defaultMessage: 'Please confirm your date of birth',
@@ -283,17 +283,17 @@ const messages = defineMessages({
},
'account.settings.field.dob.form.title': {
id: 'account.settings.field.dob.form.title',
defaultMessage: 'Enter your birth month and year',
defaultMessage: 'Confirm your date of birth',
description: 'Title of DOB form',
},
'account.settings.field.dob.form.help.text': {
id: 'account.settings.field.dob.form.help.text',
defaultMessage: 'We ask for birth month and year information to help us comply with our legal obligations.',
defaultMessage: 'We ask for birth date information to help us comply with our legal obligations.',
description: 'Help text for DOB form',
},
'account.settings.field.dob.form.success': {
id: 'account.settings.field.dob.form.success',
defaultMessage: 'Thank you for entering your information.',
defaultMessage: 'Thank you for entering your birth date information.',
description: 'Title of banner when date of birth is successfully entered',
},
'account.settings.field.month_of_birth.options.empty': {
@@ -401,8 +401,8 @@ const messages = defineMessages({
defaultMessage: 'No formal education',
description: 'Selected by the user to describe their education.',
},
'account.settings.field.education.levels.other': {
id: 'account.settings.field.education.levels.other',
'account.settings.field.education.levels.o': {
id: 'account.settings.field.education.levels.o',
defaultMessage: 'Other education',
description: 'Selected by the user if they have a type of education not described by the other choices.',
},
@@ -555,26 +555,6 @@ const messages = defineMessages({
defaultMessage: 'No value set.',
description: 'The placeholder for an empty but uneditable field when there is no administrator',
},
'notification.preferences.notifications.label': {
id: 'notification.preferences.notifications.label',
defaultMessage: 'Notifications',
description: 'Label for Notifications',
},
'account.settings.field.work.experience': {
id: 'account.settings.work.experience',
defaultMessage: 'Work Experience',
description: 'Label for account settings Work experience field.',
},
'account.settings.field.work.experience.empty': {
id: 'account.settings.field.work.experience.empty',
defaultMessage: 'Add work experience',
description: 'Placeholder for empty account settings work experience field.',
},
'account.settings.field.work.experience.options.empty': {
id: 'account.settings.field.work.experience.options.empty',
defaultMessage: 'Select work experience',
description: 'Placeholder for the work experience levels dropdown.',
},
});
export default messages;

View File

@@ -2,16 +2,18 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
const Alert = (props) => (
<div className={classNames('alert d-flex align-items-start', props.className)}>
<div>
{props.icon}
function Alert(props) {
return (
<div className={classNames('alert d-flex align-items-start', props.className)}>
<div>
{props.icon}
</div>
<div>
{props.children}
</div>
</div>
<div>
{props.children}
</div>
</div>
);
);
}
Alert.propTypes = {
className: PropTypes.string,

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { AppContext } from '@edx/frontend-platform/react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { connect } from 'react-redux';
import { Button, Hyperlink } from '@openedx/paragon';
import { Button, Hyperlink } from '@edx/paragon';
import { betaLanguageBannerSelector } from './data/selectors';
import messages from './AccountSettingsPage.messages';
@@ -49,9 +49,6 @@ class BetaLanguageBanner extends React.Component {
render() {
const savedLanguage = this.getSiteLanguageEntry(this.context.locale);
if (!savedLanguage) {
return null;
}
const isSavedLanguageReleased = savedLanguage.released === true;
const noPreviousLanguageSet = this.props.siteLanguage.previousValue === null;
if (isSavedLanguageReleased || noPreviousLanguageSet) {

View File

@@ -2,15 +2,15 @@ import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
Form, StatefulButton, ModalDialog, ActionRow, useToggle, Button,
} from '@openedx/paragon';
import React, { useCallback, useEffect, useState } from 'react';
} from '@edx/paragon';
import React, { useCallback, useEffect } from 'react';
import { connect, useDispatch } from 'react-redux';
import messages from './AccountSettingsPage.messages';
import { YEAR_OF_BIRTH_OPTIONS } from './data/constants';
import { editableFieldSelector } from './data/selectors';
import { saveSettingsReset } from './data/actions';
const DOBModal = (props) => {
function DOBModal(props) {
const {
saveState,
error,
@@ -22,23 +22,13 @@ const DOBModal = (props) => {
// eslint-disable-next-line no-unused-vars
const [isOpen, open, close, toggle] = useToggle(true, {});
const [monthValue, setMonthValue] = useState('');
const [yearValue, setYearValue] = useState('');
const handleChange = (e) => {
e.preventDefault();
if (e.target.name === 'month') {
setMonthValue(e.target.value);
} else if (e.target.name === 'year') {
setYearValue(e.target.value);
}
};
const handleSubmit = (e) => {
e.preventDefault();
const data = monthValue !== '' && yearValue !== '' ? [{ field_name: 'DOB', field_value: `${yearValue}-${monthValue}` }] : [];
const month = e.target.month.value;
const year = e.target.year.value;
const data = month !== '' && year !== '' ? [{ field_name: 'DOB', field_value: `${year}-${month}` }] : [];
onSubmit('extended_profile', data);
};
@@ -68,7 +58,7 @@ const DOBModal = (props) => {
if (saveState === 'complete' && isOpen) {
handleComplete();
}
}, [handleComplete, saveState, isOpen, monthValue, yearValue]);
}, [handleComplete, saveState, isOpen]);
return (
<>
@@ -99,9 +89,7 @@ const DOBModal = (props) => {
<Form.Control
as="select"
name="month"
onChange={handleChange}
>
<option value="">{intl.formatMessage(messages['account.settings.field.dob.month.default'])}</option>
{[...Array(12).keys()].map(month => (
<option key={month + 1} value={month + 1}>{month + 1}</option>
))}
@@ -114,9 +102,7 @@ const DOBModal = (props) => {
<Form.Control
as="select"
name="year"
onChange={handleChange}
>
<option value="">{intl.formatMessage(messages['account.settings.field.dob.year.default'])}</option>
{YEAR_OF_BIRTH_OPTIONS.map(year => (
<option key={year.value} value={year.value}>{year.label}</option>
))}
@@ -132,11 +118,11 @@ const DOBModal = (props) => {
</ModalDialog.CloseButton>
<StatefulButton
type="submit"
state={!(monthValue && yearValue) ? 'unedited' : saveState}
state={saveState}
labels={{
default: intl.formatMessage(messages['account.settings.editable.field.action.save']),
}}
disabledStates={['unedited']}
disabledStates={[]}
/>
</ActionRow>
</ModalDialog.Footer>
@@ -145,7 +131,7 @@ const DOBModal = (props) => {
</ModalDialog>
</>
);
};
}
DOBModal.propTypes = {
saveState: PropTypes.oneOf(['default', 'pending', 'complete', 'error']),

View File

@@ -1,11 +1,12 @@
/* eslint-disable react/jsx-no-useless-fragment */
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import classNames from 'classnames';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
Button, Form, StatefulButton,
} from '@openedx/paragon';
Button, Input, StatefulButton, ValidationFormGroup,
} from '@edx/paragon';
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -19,7 +20,7 @@ import {
import { editableFieldSelector } from './data/selectors';
import CertificatePreference from './certificate-preference/CertificatePreference';
const EditableField = (props) => {
function EditableField(props) {
const {
name,
label,
@@ -27,6 +28,7 @@ const EditableField = (props) => {
type,
value,
userSuppliedValue,
options,
saveState,
error,
confirmationMessageDefinition,
@@ -43,6 +45,10 @@ const EditableField = (props) => {
...others
} = props;
const id = `field-${name}`;
let inputOptions = options;
if (getConfig().ENABLE_COPPA_COMPLIANCE && name === 'level_of_education' && options) {
inputOptions = options.filter(option => option.value !== 'el');
}
const handleSubmit = (e) => {
e.preventDefault();
@@ -74,6 +80,15 @@ const EditableField = (props) => {
}
let finalValue = rawValue;
if (options) {
// Use == instead of === to prevent issues when HTML casts numbers as strings
// eslint-disable-next-line eqeqeq
const selectedOption = options.find(option => option.value == rawValue);
if (selectedOption) {
finalValue = selectedOption.label;
}
}
if (userSuppliedValue) {
finalValue += `: ${userSuppliedValue}`;
}
@@ -97,24 +112,25 @@ const EditableField = (props) => {
editing: (
<>
<form onSubmit={handleSubmit}>
<Form.Group
controlId={id}
isInvalid={error != null}
<ValidationFormGroup
for={id}
invalid={error != null}
invalidMessage={error}
helpText={helpText}
>
<Form.Label size="sm" className="h6 d-block" htmlFor={id}>{label}</Form.Label>
<Form.Control
<label className="h6 d-block" htmlFor={id}>{label}</label>
<Input
data-hj-suppress
name={name}
id={id}
type={type}
value={value}
onChange={handleChange}
options={inputOptions}
{...others}
/>
{!!helpText && <Form.Text>{helpText}</Form.Text>}
{error != null && <Form.Control.Feedback hasIcon={false}>{error}</Form.Control.Feedback>}
{others.children}
</Form.Group>
<>{others.children}</>
</ValidationFormGroup>
<p>
<StatefulButton
type="submit"
@@ -156,14 +172,14 @@ const EditableField = (props) => {
</Button>
) : null}
</div>
<p data-hj-suppress className={classNames('text-truncate', { 'grayed-out': isGrayedOut })}>{renderValue(value)}</p>
<p data-hj-suppress className={isGrayedOut ? 'grayed-out' : null}>{renderValue(value)}</p>
<p className="small text-muted mt-n2">{renderConfirmationMessage() || helpText}</p>
</div>
),
}}
/>
);
};
}
EditableField.propTypes = {
name: PropTypes.string.isRequired,
@@ -172,6 +188,10 @@ EditableField.propTypes = {
type: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
userSuppliedValue: PropTypes.string,
options: PropTypes.arrayOf(PropTypes.shape({
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
})),
saveState: PropTypes.oneOf(['default', 'pending', 'complete', 'error']),
error: PropTypes.string,
confirmationMessageDefinition: PropTypes.shape({
@@ -193,6 +213,7 @@ EditableField.propTypes = {
EditableField.defaultProps = {
value: undefined,
options: undefined,
saveState: undefined,
label: undefined,
emptyLabel: undefined,

View File

@@ -1,252 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
Button, Form, StatefulButton,
} from '@openedx/paragon';
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import SwitchContent from './SwitchContent';
import messages from './AccountSettingsPage.messages';
import {
openForm,
closeForm,
} from './data/actions';
import { editableFieldSelector } from './data/selectors';
import CertificatePreference from './certificate-preference/CertificatePreference';
const EditableSelectField = (props) => {
const {
name,
label,
emptyLabel,
type,
value,
userSuppliedValue,
options,
saveState,
error,
confirmationMessageDefinition,
confirmationValue,
helpText,
onEdit,
onCancel,
onSubmit,
onChange,
isEditing,
isEditable,
isGrayedOut,
intl,
...others
} = props;
const id = `field-${name}`;
const handleSubmit = (e) => {
e.preventDefault();
onSubmit(name, new FormData(e.target).get(name));
};
const handleChange = (e) => {
onChange(name, e.target.value);
};
const handleEdit = () => {
onEdit(name);
};
const handleCancel = () => {
onCancel(name);
};
const renderEmptyLabel = () => {
if (isEditable) {
return <Button variant="link" onClick={handleEdit} className="p-0">{emptyLabel}</Button>;
}
return <span className="text-muted">{emptyLabel}</span>;
};
const renderValue = (rawValue) => {
if (!rawValue) {
return renderEmptyLabel();
}
let finalValue = rawValue;
if (options) {
// Use == instead of === to prevent issues when HTML casts numbers as strings
// eslint-disable-next-line eqeqeq
const selectedOption = options.find(option => option.value == rawValue);
if (selectedOption) {
finalValue = selectedOption.label;
}
}
if (userSuppliedValue) {
finalValue += `: ${userSuppliedValue}`;
}
return finalValue;
};
const renderConfirmationMessage = () => {
if (!confirmationMessageDefinition || !confirmationValue) {
return null;
}
return intl.formatMessage(confirmationMessageDefinition, {
value: confirmationValue,
});
};
const selectOptions = options.map((option) => {
if (option.group) {
// If the option has a 'group' property, it represents an element with sub-options.
return (
<optgroup label={option.label} key={option.label}>
{option.group.map((subOption) => (
<option
value={subOption.value}
key={`${subOption.value}-${subOption.label}`}
disabled={subOption?.disabled}
>
{subOption.label}
</option>
))}
</optgroup>
);
}
return (
<option value={option.value} key={`${option.value}-${option.label}`} disabled={option?.disabled}>
{option.label}
</option>
);
});
return (
<SwitchContent
expression={isEditing ? 'editing' : 'default'}
cases={{
editing: (
<>
<form onSubmit={handleSubmit}>
<Form.Group
controlId={id}
isInvalid={error != null}
>
<Form.Label size="sm" className="h6 d-block" htmlFor={id}>{label}</Form.Label>
<Form.Control
data-hj-suppress
name={name}
id={id}
type={type}
as={type}
value={value}
onChange={handleChange}
{...others}
>
{options.length > 0 && selectOptions}
</Form.Control>
{!!helpText && <Form.Text>{helpText}</Form.Text>}
{error != null && <Form.Control.Feedback>{error}</Form.Control.Feedback>}
{others.children}
</Form.Group>
<p>
<StatefulButton
type="submit"
className="mr-2"
state={saveState}
labels={{
default: intl.formatMessage(messages['account.settings.editable.field.action.save']),
}}
onClick={(e) => {
// Swallow clicks if the state is pending.
// We do this instead of disabling the button to prevent
// it from losing focus (disabled elements cannot have focus).
// Disabling it would causes upstream issues in focus management.
// Swallowing the onSubmit event on the form would be better, but
// we would have to add that logic for every field given our
// current structure of the application.
if (saveState === 'pending') { e.preventDefault(); }
}}
disabledStates={[]}
/>
<Button
variant="outline-primary"
onClick={handleCancel}
>
{intl.formatMessage(messages['account.settings.editable.field.action.cancel'])}
</Button>
</p>
</form>
{['name', 'verified_name'].includes(name) && <CertificatePreference fieldName={name} />}
</>
),
default: (
<div className="form-group">
<div className="d-flex align-items-start">
<h6 aria-level="3">{label}</h6>
{isEditable ? (
<Button variant="link" onClick={handleEdit} className="ml-3">
<FontAwesomeIcon className="mr-1" icon={faPencilAlt} />{intl.formatMessage(messages['account.settings.editable.field.action.edit'])}
</Button>
) : null}
</div>
<p data-hj-suppress className={isGrayedOut ? 'grayed-out' : null}>{renderValue(value)}</p>
<p className="small text-muted mt-n2">{renderConfirmationMessage() || helpText}</p>
</div>
),
}}
/>
);
};
EditableSelectField.propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.node]),
emptyLabel: PropTypes.node,
type: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
userSuppliedValue: PropTypes.string,
options: PropTypes.arrayOf(PropTypes.shape({
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
})),
saveState: PropTypes.oneOf(['default', 'pending', 'complete', 'error']),
error: PropTypes.string,
confirmationMessageDefinition: PropTypes.shape({
id: PropTypes.string.isRequired,
defaultMessage: PropTypes.string.isRequired,
description: PropTypes.string,
}),
confirmationValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
helpText: PropTypes.node,
onEdit: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
isEditing: PropTypes.bool,
isEditable: PropTypes.bool,
isGrayedOut: PropTypes.bool,
intl: intlShape.isRequired,
};
EditableSelectField.defaultProps = {
value: undefined,
options: [],
saveState: undefined,
label: undefined,
emptyLabel: undefined,
error: undefined,
confirmationMessageDefinition: undefined,
confirmationValue: undefined,
helpText: undefined,
isEditing: false,
isEditable: true,
isGrayedOut: false,
userSuppliedValue: undefined,
};
export default connect(editableFieldSelector, {
onEdit: openForm,
onCancel: closeForm,
})(injectIntl(EditableSelectField));

View File

@@ -3,8 +3,8 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import {
Button, StatefulButton, Form,
} from '@openedx/paragon';
Button, StatefulButton, Input, ValidationFormGroup,
} from '@edx/paragon';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faExclamationTriangle, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
@@ -18,7 +18,7 @@ import {
} from './data/actions';
import { editableFieldSelector } from './data/selectors';
const EmailField = (props) => {
function EmailField(props) {
const {
name,
label,
@@ -106,12 +106,14 @@ const EmailField = (props) => {
cases={{
editing: (
<form onSubmit={handleSubmit}>
<Form.Group
controlId={id}
isInvalid={error != null}
<ValidationFormGroup
for={id}
invalid={error != null}
invalidMessage={error}
helpText={helpText}
>
<Form.Label className="h6 d-block" htmlFor={id}>{label}</Form.Label>
<Form.Control
<label className="h6 d-block" htmlFor={id}>{label}</label>
<Input
data-hj-suppress
name={name}
id={id}
@@ -119,9 +121,7 @@ const EmailField = (props) => {
value={value}
onChange={handleChange}
/>
{!!helpText && <Form.Text>{helpText}</Form.Text>}
{error != null && <Form.Control.Feedback hasIcon={false}>{error}</Form.Control.Feedback>}
</Form.Group>
</ValidationFormGroup>
<p>
<StatefulButton
type="submit"
@@ -169,7 +169,7 @@ const EmailField = (props) => {
}}
/>
);
};
}
EmailField.propTypes = {
name: PropTypes.string.isRequired,

View File

@@ -1,23 +1,25 @@
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { breakpoints, useWindowSize } from '@openedx/paragon';
import { breakpoints, useWindowSize } from '@edx/paragon';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import { NavHashLink } from 'react-router-hash-link';
import Scrollspy from 'react-scrollspy';
import messages from './AccountSettingsPage.messages';
const JumpNav = ({
function JumpNav({
intl,
}) => {
displayDemographicsLink,
}) {
const stickToTop = useWindowSize().width > breakpoints.small.minWidth;
return (
<div className={classNames('jump-nav px-2.25', { 'jump-nav-sm position-sticky pt-3': stickToTop })}>
<div className={classNames('jump-nav', { 'jump-nav-sm position-sticky pt-3': stickToTop })}>
<Scrollspy
items={[
'basic-information',
'profile-information',
'demographics-information',
'social-media',
'site-preferences',
'linked-accounts',
@@ -36,6 +38,14 @@ 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'])}
@@ -51,21 +61,19 @@ const JumpNav = ({
{intl.formatMessage(messages['account.settings.section.linked.accounts'])}
</NavHashLink>
</li>
{getConfig().ENABLE_ACCOUNT_DELETION
&& (
<li>
<NavHashLink to="#delete-account">
{intl.formatMessage(messages['account.settings.jump.nav.delete.account'])}
</NavHashLink>
</li>
)}
<li>
<NavHashLink to="#delete-account">
{intl.formatMessage(messages['account.settings.jump.nav.delete.account'])}
</NavHashLink>
</li>
</Scrollspy>
</div>
);
};
}
JumpNav.propTypes = {
intl: intlShape.isRequired,
displayDemographicsLink: PropTypes.bool.isRequired,
};
export default injectIntl(JumpNav);

View File

@@ -1,19 +1,16 @@
import React from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
const NotFoundPage = () => (
<div
className="container-fluid d-flex py-5 justify-content-center align-items-start text-center"
data-testid="not-found-page"
>
<p className="my-0 py-5 text-muted" style={{ maxWidth: '32em' }}>
<FormattedMessage
id="error.notfound.message"
defaultMessage="The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again."
description="Error message when a page does not exist"
/>
</p>
</div>
);
export default NotFoundPage;
export default function NotFoundPage() {
return (
<div className="container-fluid d-flex py-5 justify-content-center align-items-start text-center">
<p className="my-0 py-5 text-muted" style={{ maxWidth: '32em' }}>
<FormattedMessage
id="error.notfound.message"
defaultMessage="The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again."
description="Error message when a page does not exist"
/>
</p>
</div>
);
}

View File

@@ -1,9 +1,9 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Alert } from '@openedx/paragon';
import { Alert } from '@edx/paragon';
const OneTimeDismissibleAlert = (props) => {
export default function OneTimeDismissibleAlert(props) {
const [dismissed, setDismissed] = useState(localStorage.getItem(props.id) !== 'true');
const onClose = () => {
@@ -25,7 +25,7 @@ const OneTimeDismissibleAlert = (props) => {
</p>
</Alert>
);
};
}
OneTimeDismissibleAlert.propTypes = {
id: PropTypes.string.isRequired,
@@ -41,5 +41,3 @@ OneTimeDismissibleAlert.defaultProps = {
header: undefined,
body: undefined,
};
export default OneTimeDismissibleAlert;

View File

@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { TransitionReplace } from '@openedx/paragon';
import { TransitionReplace } from '@edx/paragon';
const onChildExit = (htmlNode) => {
// If the leaving child has focus, take control and redirect it
@@ -22,7 +22,7 @@ const onChildExit = (htmlNode) => {
}
};
const SwitchContent = ({ expression, cases, className }) => {
function SwitchContent({ expression, cases, className }) {
const getContent = (caseKey) => {
if (cases[caseKey]) {
if (typeof cases[caseKey] === 'string') {
@@ -48,7 +48,7 @@ const SwitchContent = ({ expression, cases, className }) => {
{getContent(expression)}
</TransitionReplace>
);
};
}
SwitchContent.propTypes = {
expression: PropTypes.string,

View File

@@ -7,7 +7,7 @@ import {
Form,
ModalDialog,
StatefulButton,
} from '@openedx/paragon';
} from '@edx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
@@ -21,14 +21,14 @@ import { certPreferenceSelector } from '../data/selectors';
import commonMessages from '../AccountSettingsPage.messages';
import messages from './messages';
const CertificatePreference = ({
function CertificatePreference({
intl,
fieldName,
originalFullName,
originalVerifiedName,
saveState,
useVerifiedNameForCerts,
}) => {
}) {
const dispatch = useDispatch();
const [checked, setChecked] = useState(false);
const [modalIsOpen, setModalIsOpen] = useState(false);
@@ -152,7 +152,7 @@ const CertificatePreference = ({
</ModalDialog>
</>
) : null;
};
}
CertificatePreference.propTypes = {
intl: intlShape.isRequired,

View File

@@ -2,13 +2,14 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { BrowserRouter as Router } from 'react-router-dom';
import { Router } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import {
fireEvent,
render,
screen,
} from '@testing-library/react';
import { createMemoryHistory } from 'history';
import * as auth from '@edx/frontend-platform/auth';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
@@ -27,6 +28,8 @@ jest.mock('react-redux', () => ({
jest.mock('@edx/frontend-platform/auth');
jest.mock('../../data/selectors', () => jest.fn().mockImplementation(() => ({ certPreferenceSelector: () => ({}) })));
const history = createMemoryHistory();
const IntlCertificatePreference = injectIntl(CertificatePreference);
const mockStore = configureStore();
@@ -39,7 +42,7 @@ describe('NameChange', () => {
const labelText = 'If checked, this name will appear on your certificates and public-facing records.';
const reduxWrapper = children => (
<Router>
<Router history={history}>
<IntlProvider locale="en">
<Provider store={store}>{children}</Provider>
</IntlProvider>
@@ -151,7 +154,7 @@ describe('NameChange', () => {
const submitButton = screen.getByText('Choose name');
fireEvent.click(submitButton);
expect(mockDispatch).toHaveBeenCalledWith({
payload: { formId, commitValues: false, extendedProfile: {} },
payload: { formId, commitValues: false },
type: 'ACCOUNT_SETTINGS__SAVE_SETTINGS',
});
});

View File

@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NameChange does not render if there is no verified name 1`] = `
{
Object {
"asFragment": [Function],
"baseElement": <body>
<div />

View File

@@ -0,0 +1,271 @@
import React from 'react';
import { getConfig, getQueryParameters } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Hyperlink } from '@edx/paragon';
import { faCheck } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import get from 'lodash.get';
import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth';
import PageLoading from '../PageLoading';
import CoachingConsentForm from './CoachingConsentForm';
import messages from './CoachingConsent.messages';
import LogoSVG from '../../logo.svg';
import { fetchSettings } from '../data/actions';
import { coachingConsentPageSelector } from '../data/selectors';
function Logo({ src, alt, ...attributes }) {
return <img src={src} alt={alt} {...attributes} />;
}
function SuccessMessage(props) {
return (
<div className="col-12 col-lg-6 shadow-lg mx-auto mt-4 p-5">
<FontAwesomeIcon className="text-success" icon={faCheck} size="5x" />
<div className="h3">{props.header}</div>
<div>{props.message}</div>
<Hyperlink destination={props.continueUrl} className="d-block p-2 my-3 text-center text-white bg-primary rounded">
{props.continue}
</Hyperlink>
</div>
);
}
function AutoRedirect(props) {
window.location.href = props.redirectUrl;
// eslint-disable-next-line react/jsx-no-useless-fragment
return <></>;
}
const VIEWS = {
NOT_LOADED: 'NOT_LOADED',
LOADED: 'LOADED',
SUCCESS: 'SUCCESS',
SUCCESS_PENDING: 'SUCCESS_PENDING',
DECLINED: 'DECLINED',
DECLINE_PENDING: 'DECLINE_PENDING',
};
class CoachingConsent extends React.Component {
constructor(props, context) {
super(props, context);
// Used to redirect back to the courseware.
const nextUrl = this.sanitizeForwardingUrl(getQueryParameters().next);
this.state = {
redirectUrl: nextUrl || `${getConfig().LMS_BASE_URL}/dashboard/`,
formErrors: {},
formSubmitted: false,
declineSubmitted: false,
submissionSuccess: false,
};
this.handleSubmit = this.handleSubmit.bind(this);
this.declineCoaching = this.declineCoaching.bind(this);
this.patchUsingCoachingConsentForm = this.patchUsingCoachingConsentForm.bind(this);
}
componentDidMount() {
this.props.fetchSettings();
}
handleSubmit(e) {
e.preventDefault();
const fullName = e.target.fullName.value;
const phoneNumber = e.target.phoneNumber.value;
const body = {
coaching_consent: true,
consent_form_seen: true,
phone_number: phoneNumber,
full_name: fullName,
};
this.setState({
formErrors: {},
formSubmitted: true,
declineSubmitted: false,
}, () => this.patchUsingCoachingConsentForm(body));
}
sanitizeForwardingUrl(url) {
// Redirect to root of MFE if invalid next param is sent
return url && url.startsWith(getConfig().LMS_BASE_URL) ? url : `${getConfig().LMS_BASE_URL}/dashboard/`;
}
async patchUsingCoachingConsentForm(body) {
const { userId } = getAuthenticatedUser();
const requestUrl = `${getConfig().LMS_BASE_URL}/api/coaching/v1/coaching_consent/${userId}/`;
let formErrors = {};
const data = await getAuthenticatedHttpClient()
.patch(requestUrl, body)
.catch((error) => {
if (get(error, 'customAttributes.httpErrorResponseData')) {
formErrors = JSON.parse(error.customAttributes.httpErrorResponseData);
} else {
formErrors = { full_name: 'Something went wrong. Please try again.' };
}
this.setState({
submissionSuccess: false,
formErrors,
formSubmitted: false,
});
});
if (get(data, 'status') === 200) {
this.setState({ submissionSuccess: true });
}
}
declineCoaching(e) {
e.preventDefault();
const body = {
coaching_consent: false,
consent_form_seen: true,
};
this.setState({
formErrors: {},
formSubmitted: false,
declineSubmitted: true,
}, () => this.patchUsingCoachingConsentForm(body));
}
renderView(currentView) {
switch (currentView) {
case VIEWS.NOT_LOADED:
return <PageLoading srMessage="" />;
case VIEWS.LOADED:
return (
<CoachingConsentForm
onSubmit={this.handleSubmit}
declineCoaching={this.declineCoaching}
formErrors={this.state.formErrors}
formValues={this.props.formValues}
redirectUrl={this.state.redirectUrl}
profileDataManager={this.props.profileDataManager}
/>
);
case VIEWS.SUCCESS_PENDING:
return <PageLoading srMessage="Submitting..." />;
case VIEWS.SUCCESS:
return (
<SuccessMessage
continueUrl={this.state.redirectUrl}
header={this.props.intl.formatMessage(messages['account.settings.coaching.consent.success.header'])}
message={this.props.intl.formatMessage(messages['account.settings.coaching.consent.success.message'])}
continue={this.props.intl.formatMessage(messages['account.settings.coaching.consent.success.continue'])}
/>
);
case VIEWS.DECLINE_PENDING:
return <PageLoading srMessage="Redirecting..." />;
case VIEWS.DECLINED:
return <AutoRedirect redirectUrl={this.state.redirectUrl} />;
default:
// eslint-disable-next-line react/jsx-no-useless-fragment
return <></>;
}
}
render() {
const { loaded } = this.props;
const formHasErrors = Object.keys(this.state.formErrors).length > 0;
let currentView = null;
// This amount of logic was making the template very hard to read, so I broke it out into views.
if (!loaded) {
currentView = VIEWS.NOT_LOADED;
} else if (this.state.formSubmitted && !formHasErrors) {
if (this.state.submissionSuccess) {
currentView = VIEWS.SUCCESS;
} else {
currentView = VIEWS.SUCCESS_PENDING;
}
} else if (this.state.declineSubmitted && !formHasErrors) {
if (this.state.submissionSuccess) {
currentView = VIEWS.DECLINED;
} else {
currentView = VIEWS.DECLINE_PENDING;
}
} else {
currentView = VIEWS.LOADED;
}
return (
<main>
<div className="w-100 d-flex justify-content-center align-items-center shadow coaching-header">
<Logo
className="logo"
src={LogoSVG}
alt="Logo"
/>
</div>
{this.renderView(currentView)}
</main>
);
}
}
Logo.defaultProps = {
src: '',
alt: '',
};
Logo.propTypes = {
src: PropTypes.string,
alt: PropTypes.string,
};
SuccessMessage.defaultProps = {
header: '',
message: '',
continueUrl: '',
continue: '',
};
SuccessMessage.propTypes = {
header: PropTypes.string,
message: PropTypes.string,
continueUrl: PropTypes.string,
continue: PropTypes.string,
};
AutoRedirect.defaultProps = {
redirectUrl: '',
};
AutoRedirect.propTypes = {
redirectUrl: PropTypes.string,
};
CoachingConsent.defaultProps = {
loaded: false,
profileDataManager: null,
};
CoachingConsent.propTypes = {
intl: intlShape.isRequired,
loaded: PropTypes.bool,
formValues: PropTypes.shape({
name: PropTypes.string,
phone_number: PropTypes.string,
coaching: PropTypes.shape({
coaching_consent: PropTypes.bool.isRequired,
user: PropTypes.number.isRequired,
eligible_for_coaching: PropTypes.bool.isRequired,
consent_form_seen: PropTypes.bool.isRequired,
}),
}).isRequired,
formErrors: PropTypes.shape({
coaching: PropTypes.shape({}),
}).isRequired,
confirmationValues: PropTypes.shape({
coaching: PropTypes.shape({}),
name: PropTypes.shape({}),
phone_number: PropTypes.shape({}),
}).isRequired,
fetchSettings: PropTypes.func.isRequired,
profileDataManager: PropTypes.string,
};
export default connect(coachingConsentPageSelector, {
fetchSettings,
})(injectIntl(CoachingConsent));

View File

@@ -0,0 +1,66 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'account.settings.coaching.consent.welcome.header': {
id: 'account.settings.coaching.consent.welcome.header',
defaultMessage: 'Lets get started.',
description: 'The welcome header for consent form.',
},
'account.settings.coaching.consent.welcome.subheader': {
id: 'account.settings.coaching.consent.welcome.subheader',
defaultMessage: "We're here for you from start to finish",
description: 'The welcome subheader for consent form.',
},
'account.settings.coaching.consent.description': {
id: 'account.settings.coaching.consent.description',
defaultMessage: "MicroBachelors programs include coaching that focuses on your career, education, and how you'll achieve results through one-on-one communication with an experienced professional. If youre interested, provide the information below and click “Submit,” and our coaching partner will connect with you via email and/or text message to help you move forward. Terms and conditions apply.*",
description: 'Text describing what Coaching is.',
},
'account.settings.coaching.consent.text-messaging.disclaimer': {
id: 'account.settings.coaching.consent.text-messaging.disclaimer',
defaultMessage: '* Coaching services are included at no additional cost to learners with US phone numbers. Coaching includes recurring text messages. Message and data rates may apply. Text STOP to opt-out.',
description: 'Text describing what Coaching is.',
},
'account.settings.coaching.consent.accept-coaching': {
id: 'account.settings.coaching.consent.accept-coaching',
defaultMessage: 'Sign up for coaching',
description: 'Text to confirm coaching enablement',
},
'account.settings.coaching.consent.decline-coaching': {
id: 'account.settings.coaching.consent.decline-coaching',
defaultMessage: 'I prefer not to be contacted with free coaching services',
description: 'Text to decline coaching enablement',
},
'account.settings.coaching.consent.label.name': {
id: 'account.settings.coaching.consent.label.name',
defaultMessage: 'Please confirm your name',
description: 'Label for name input',
},
'account.settings.coaching.consent.label.phone-number': {
id: 'account.settings.coaching.consent.label.phone-number',
defaultMessage: 'Enter your mobile number',
description: 'Label for mobile phone number input',
},
'account.settings.coaching.consent.success.header': {
id: 'account.settings.coaching.consent.success.header',
defaultMessage: 'Success!',
description: 'Heading announcing that submission succeeded',
},
'account.settings.coaching.consent.success.message': {
id: 'account.settings.coaching.consent.success.message',
defaultMessage: "You're signed up for coaching. You can expect a message via email or SMS in the coming days.",
description: 'Text announcing that you have signed up and will receive texts',
},
'account.settings.coaching.consent.success.continue': {
id: 'account.settings.coaching.consent.success.continue',
defaultMessage: 'Start my course',
description: 'Text that the user will be sent back to the courseware',
},
'account.settings.coaching.managed.support': {
id: 'account.settings.coaching.managed.support',
defaultMessage: 'support',
description: 'website support',
},
});
export default messages;

View File

@@ -0,0 +1,135 @@
import React from 'react';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { Input, Button, Hyperlink } from '@edx/paragon';
import PropTypes from 'prop-types';
import Alert from '../Alert';
import messages from './CoachingConsent.messages';
function ErrorMessage(props) {
return <div className="alert-warning mb-2">{props.message}</div>;
}
function ManagedProfileAlert({ profileDataManager }) {
return (
<Alert className="alert alert-primary" role="alert">
<FormattedMessage
id="account.settings.coaching.managed.alert"
defaultMessage="Your name is managed by {managerTitle}. Contact your administrator for help."
description="Alert message informing the user their account data is managed by a third party"
values={{
managerTitle: <b>{profileDataManager}</b>,
}}
/>
</Alert>
);
}
function CoachingForm(props) {
return (
<div className="col-12 col-md-6 col-xl-5 mx-auto mt-4 p-5 shadow-lg">
<h2 className="h2">
{props.intl.formatMessage(messages['account.settings.coaching.consent.welcome.header'])}
</h2>
<p>{props.intl.formatMessage(messages['account.settings.coaching.consent.description'])}</p>
<div>
<form onSubmit={props.onSubmit}>
<div className="py-3">
{!!props.profileDataManager && (
<ManagedProfileAlert profileDataManager={props.profileDataManager} />
)}
<ErrorMessage message={props.formErrors.full_name} />
<label className="h6" htmlFor="fullName">
{props.intl.formatMessage(messages['account.settings.coaching.consent.label.name'])}
</label>
<Input
type="text"
name="full-name"
id="fullName"
disabled={!!props.profileDataManager}
defaultValue={props.formValues.name}
/>
</div>
<div className="py-3">
<ErrorMessage message={props.formErrors.phone_number} />
<label className="h6" htmlFor="phoneNumber">
{props.intl.formatMessage(messages['account.settings.coaching.consent.label.phone-number'])}
</label>
<Input
type="text"
name="phone_number"
id="phoneNumber"
defaultValue={props.formValues.phone_number}
/>
</div>
<div className=" py-3">
<p className="small font-italic">
{props.intl.formatMessage(messages['account.settings.coaching.consent.text-messaging.disclaimer'])}
</p>
</div>
<ErrorMessage message={props.formErrors.coaching} />
<div className="d-flex flex-column align-items-center">
<Button variant="outline-primary" className="w-100" type="submit">
{props.intl.formatMessage(messages['account.settings.coaching.consent.accept-coaching'])}
</Button>
</div>
<div className="mt-3">
<Hyperlink
className="mt-3 text-dark btn-link small"
destination={props.redirectUrl}
onClick={props.declineCoaching}
>
{props.intl.formatMessage(messages['account.settings.coaching.consent.decline-coaching'])}
</Hyperlink>
</div>
</form>
</div>
</div>
);
}
CoachingForm.defaultProps = {
formErrors: {
coaching: '',
name: '',
phone_number: '',
},
};
CoachingForm.propTypes = {
intl: intlShape.isRequired,
onSubmit: PropTypes.func.isRequired,
declineCoaching: PropTypes.func.isRequired,
formValues: PropTypes.shape({
name: PropTypes.string,
phone_number: PropTypes.string,
coaching: PropTypes.shape({
coaching_consent: PropTypes.bool.isRequired,
user: PropTypes.number.isRequired,
eligible_for_coaching: PropTypes.bool.isRequired,
consent_form_seen: PropTypes.bool.isRequired,
}),
}).isRequired,
formErrors: PropTypes.shape({
coaching: PropTypes.string,
full_name: PropTypes.string,
phone_number: PropTypes.string,
}),
redirectUrl: PropTypes.string.isRequired,
profileDataManager: PropTypes.string.isRequired,
};
ErrorMessage.defaultProps = {
message: '',
};
ErrorMessage.propTypes = {
message: PropTypes.string,
};
ManagedProfileAlert.propTypes = {
profileDataManager: PropTypes.string.isRequired,
};
export default injectIntl(CoachingForm);

View File

@@ -0,0 +1,101 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { ValidationFormGroup, Input } from '@edx/paragon';
import messages from './CoachingToggle.messages';
import { editableFieldSelector } from '../data/selectors';
import { saveSettings, updateDraft, saveMultipleSettings } from '../data/actions';
import EditableField from '../EditableField';
function CoachingToggle(props) {
return (
<>
<EditableField
name="phone_number"
type="text"
value={props.phone_number}
label={props.intl.formatMessage(messages['account.settings.field.phone_number'])}
emptyLabel={props.intl.formatMessage(messages['account.settings.field.phone_number.empty'])}
onChange={props.updateDraft}
onSubmit={() => {
const { coaching } = props;
if (coaching.coaching_consent === true) {
return props.saveMultipleSettings([
{
formId: 'coaching',
commitValues: {
...coaching,
phone_number: props.phone_number,
},
},
{
formId: 'phone_number',
commitValues: props.phone_number,
},
], 'phone_number');
}
return props.saveSettings('phone_number', props.phone_number);
}}
/>
<ValidationFormGroup
for="coachingConsent"
helpText={props.intl.formatMessage(messages['account.settings.field.coaching_consent.tooltip'])}
invalid={!!props.error}
invalidMessage={props.intl.formatMessage(messages['account.settings.field.coaching_consent.error'])}
className="custom-control custom-switch"
>
<Input
name={props.name}
className="custom-control-input"
disabled={props.saveState === 'pending'}
type="checkbox"
id="coachingConsent"
checked={props.coaching.coaching_consent}
value={props.coaching.coaching_consent}
onChange={async (e) => {
const { name } = e.target;
// eslint-disable-next-line camelcase
const { user, eligible_for_coaching } = props.coaching;
const value = {
user,
// eslint-disable-next-line camelcase
eligible_for_coaching,
coaching_consent: e.target.checked,
};
props.saveSettings(name, value);
}}
/>
<label className="custom-control-label" htmlFor="coachingConsent">{props.intl.formatMessage(messages['account.settings.field.coaching_consent'])}</label>
</ValidationFormGroup>
</>
);
}
CoachingToggle.defaultProps = {
phone_number: '',
error: '',
saveState: undefined,
};
CoachingToggle.propTypes = {
name: PropTypes.string.isRequired,
error: PropTypes.string,
coaching: PropTypes.shape({
coaching_consent: PropTypes.bool.isRequired,
user: PropTypes.number.isRequired,
eligible_for_coaching: PropTypes.bool.isRequired,
}).isRequired,
saveState: PropTypes.oneOf(['default', 'pending', 'complete', 'error']),
saveSettings: PropTypes.func.isRequired,
saveMultipleSettings: PropTypes.func.isRequired,
updateDraft: PropTypes.func.isRequired,
intl: intlShape.isRequired,
phone_number: PropTypes.string,
};
export default connect(editableFieldSelector, {
saveSettings,
updateDraft,
saveMultipleSettings,
})(injectIntl(CoachingToggle));

View File

@@ -0,0 +1,31 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'account.settings.field.phone_number': {
id: 'account.settings.field.phone_number',
defaultMessage: 'Phone Number',
description: 'The label for a phone numbers setting in the user profile',
},
'account.settings.field.phone_number.empty': {
id: 'account.settings.field.phone_number.empty',
defaultMessage: 'Add a phone number',
description: 'placeholder for a profiles empty phone number field',
},
'account.settings.field.coaching_consent': {
id: 'account.settings.field.coaching_consent',
defaultMessage: 'Coaching consent',
description: 'The label for the coaching consent setting in the user profile',
},
'account.settings.field.coaching_consent.tooltip': {
id: 'account.settings.field.coaching_consent.tooltip',
defaultMessage: 'MicroBachelors programs include text message based coaching that helps you pair educational experiences with your career goals through one-on-one advice. Coaching services are included at no additional cost, and are available to learners with U.S. mobile phone numbers. Standard messaging rates apply. Text STOP at anytime to opt-out of messages.',
description: 'A tooltip explaining what coaching is and who it is for',
},
'account.settings.field.coaching_consent.error': {
id: 'account.settings.field.coaching_consent.error',
defaultMessage: 'A valid US phone number is required to opt into coaching',
description: 'An error message that displays when a user attempts to consent to coaching without first providing a phone number in their profile',
},
});
export default messages;

View File

@@ -0,0 +1,51 @@
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { getConfig } from '@edx/frontend-platform';
import get from 'lodash.get';
/**
* get all settings related to the coaching plugin. Settings used
* by Microbachelors students.
* @param {Number} userId users are identified in the api by LMS id
*/
export async function getCoachingPreferences(userId) {
let data = {};
try {
({ data } = await getAuthenticatedHttpClient()
.get(`${getConfig().LMS_BASE_URL}/api/coaching/v1/users/${userId}/`));
} catch (error) {
// If a user isn't active the API call will fail with a lack of credentials.
data = {
coaching_consent: false,
user: userId,
eligible_for_coaching: false,
consent_form_seen: false,
};
}
return data;
}
/**
* patch all of the settings related to coaching.
* @param {Number} userId users are identified in the api by LMS id
* @param {Object} commitValues { coaching }
*/
export async function patchCoachingPreferences(userId, commitValues) {
const requestUrl = `${getConfig().LMS_BASE_URL}/api/coaching/v1/users/${userId}/`;
const { coaching } = commitValues;
coaching.user = userId;
await getAuthenticatedHttpClient()
.patch(requestUrl, coaching)
.catch((error) => {
const apiError = Object.create(error);
apiError.fieldErrors = JSON.parse(error.customAttributes.httpErrorResponseData);
if (get(apiError, 'fieldErrors.phone_number')) {
// eslint-disable-next-line prefer-destructuring
apiError.fieldErrors.coaching = apiError.fieldErrors.phone_number[0];
delete apiError.fieldErrors.phone_number;
}
throw apiError;
});
return commitValues;
}

View File

@@ -0,0 +1,103 @@
/* eslint-disable no-import-assign */
import React from 'react';
import { Provider } from 'react-redux';
import renderer from 'react-test-renderer';
import { act } from 'react-dom/test-utils';
import configureStore from 'redux-mock-store';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
import * as auth from '@edx/frontend-platform/auth';
import CoachingConsent from '../CoachingConsent';
import * as selectors from '../../data/selectors';
jest.mock('@edx/frontend-platform/auth');
const IntlCoachingConsent = injectIntl(CoachingConsent);
jest.mock('../../data/selectors', () => jest.fn().mockImplementation(() => ({ coachingConsentPageSelector: () => ({}) })));
const mockStore = configureStore();
describe('CoachingConsent', () => {
let props = {};
let store = {};
selectors.mockClear();
const reduxWrapper = children => (
<IntlProvider locale="en">
<Provider store={store}>{children}</Provider>
</IntlProvider>
);
beforeEach(() => {
store = mockStore();
props = {
fetchSettings: jest.fn(),
loaded: true,
saveState: undefined,
formValues: {
name: 'edx edx',
phone_number: '1234567890',
coaching: {
coaching_consent: true,
consent_form_seen: false,
eligible_for_coaching: true,
user: 1,
},
},
formErrors: {},
confirmationValues: {},
profileDataManager: '',
intl: {},
};
auth.getAuthenticatedHttpClient = jest.fn(() => ({
patch: async () => ({
data: { status: 200 },
catch: () => {},
}),
}));
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3 }));
});
it('should render', () => {
const wrapper = renderer.create(reduxWrapper(<IntlCoachingConsent {...props} />)).toJSON();
expect(wrapper).toMatchSnapshot();
});
it('disables name field on enterprise user', () => {
props = {
...props,
profileDataManager: 'test person',
};
const wrapper = renderer.create(reduxWrapper(<IntlCoachingConsent {...props} />)).toJSON();
expect(wrapper).toMatchSnapshot();
});
it('display completed box when successfully submitted', async () => {
const fakeEvent = {
preventDefault: () => {},
target: {
fullName: { value: 'edx edx' },
phoneNumber: { value: '9783028731' },
},
};
const wrapper = renderer.create(
reduxWrapper(<IntlCoachingConsent {...props} />),
{
// bypass the forward-ref. we don't care about focus for this one test
createNodeMock: (element) => {
if (element.type === 'button') {
// mock a focus function
return {
focus: async () => wrapper.root.findByType('form').props.onSubmit(fakeEvent),
};
}
return null;
},
},
);
const form = wrapper.root.findByType('form');
await act(async () => { await form.props.onSubmit(fakeEvent); });
expect(wrapper.toJSON()).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,276 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CoachingConsent disables name field on enterprise user 1`] = `
<main>
<div
className="w-100 d-flex justify-content-center align-items-center shadow coaching-header"
>
<img
alt="Logo"
className="logo"
src="icon/mock/path"
/>
</div>
<div
className="col-12 col-md-6 col-xl-5 mx-auto mt-4 p-5 shadow-lg"
>
<h2
className="h2"
>
Lets get started.
</h2>
<p>
MicroBachelors programs include coaching that focuses on your career, education, and how you'll achieve results through one-on-one communication with an experienced professional. If youre interested, provide the information below and click “Submit,” and our coaching partner will connect with you via email and/or text message to help you move forward. Terms and conditions apply.*
</p>
<div>
<form
onSubmit={[Function]}
>
<div
className="py-3"
>
<div
className="alert d-flex align-items-start alert alert-primary"
>
<div />
<div>
Your name is managed by
<b>
test person
</b>
. Contact your administrator for help.
</div>
</div>
<div
className="alert-warning mb-2"
>
</div>
<label
className="h6"
htmlFor="fullName"
>
Please confirm your name
</label>
<input
className="form-control"
defaultValue="edx edx"
disabled={true}
id="fullName"
name="full-name"
type="text"
/>
</div>
<div
className="py-3"
>
<div
className="alert-warning mb-2"
>
</div>
<label
className="h6"
htmlFor="phoneNumber"
>
Enter your mobile number
</label>
<input
className="form-control"
defaultValue="1234567890"
id="phoneNumber"
name="phone_number"
type="text"
/>
</div>
<div
className=" py-3"
>
<p
className="small font-italic"
>
* Coaching services are included at no additional cost to learners with US phone numbers. Coaching includes recurring text messages. Message and data rates may apply. Text STOP to opt-out.
</p>
</div>
<div
className="alert-warning mb-2"
>
</div>
<div
className="d-flex flex-column align-items-center"
>
<button
className="w-100 btn btn-outline-primary"
disabled={false}
type="submit"
>
Sign up for coaching
</button>
</div>
<div
className="mt-3"
>
<a
className="pgn__hyperlink default-link standalone-link mt-3 text-dark btn-link small"
href="http://localhost:18000/dashboard/"
onClick={[Function]}
target="_self"
>
I prefer not to be contacted with free coaching services
</a>
</div>
</form>
</div>
</div>
</main>
`;
exports[`CoachingConsent display completed box when successfully submitted 1`] = `
<main>
<div
className="w-100 d-flex justify-content-center align-items-center shadow coaching-header"
>
<img
alt="Logo"
className="logo"
src="icon/mock/path"
/>
</div>
<div>
<div
className="d-flex justify-content-center align-items-center flex-column"
style={
Object {
"height": "50vh",
}
}
>
<div
className="spinner-border text-primary"
role="status"
>
<span
className="sr-only"
>
Submitting...
</span>
</div>
</div>
</div>
</main>
`;
exports[`CoachingConsent should render 1`] = `
<main>
<div
className="w-100 d-flex justify-content-center align-items-center shadow coaching-header"
>
<img
alt="Logo"
className="logo"
src="icon/mock/path"
/>
</div>
<div
className="col-12 col-md-6 col-xl-5 mx-auto mt-4 p-5 shadow-lg"
>
<h2
className="h2"
>
Lets get started.
</h2>
<p>
MicroBachelors programs include coaching that focuses on your career, education, and how you'll achieve results through one-on-one communication with an experienced professional. If youre interested, provide the information below and click “Submit,” and our coaching partner will connect with you via email and/or text message to help you move forward. Terms and conditions apply.*
</p>
<div>
<form
onSubmit={[Function]}
>
<div
className="py-3"
>
<div
className="alert-warning mb-2"
>
</div>
<label
className="h6"
htmlFor="fullName"
>
Please confirm your name
</label>
<input
className="form-control"
defaultValue="edx edx"
disabled={false}
id="fullName"
name="full-name"
type="text"
/>
</div>
<div
className="py-3"
>
<div
className="alert-warning mb-2"
>
</div>
<label
className="h6"
htmlFor="phoneNumber"
>
Enter your mobile number
</label>
<input
className="form-control"
defaultValue="1234567890"
id="phoneNumber"
name="phone_number"
type="text"
/>
</div>
<div
className=" py-3"
>
<p
className="small font-italic"
>
* Coaching services are included at no additional cost to learners with US phone numbers. Coaching includes recurring text messages. Message and data rates may apply. Text STOP to opt-out.
</p>
</div>
<div
className="alert-warning mb-2"
>
</div>
<div
className="d-flex flex-column align-items-center"
>
<button
className="w-100 btn btn-outline-primary"
disabled={false}
type="submit"
>
Sign up for coaching
</button>
</div>
<div
className="mt-3"
>
<a
className="pgn__hyperlink default-link standalone-link mt-3 text-dark btn-link small"
href="http://localhost:18000/dashboard/"
onClick={[Function]}
target="_self"
>
I prefer not to be contacted with free coaching services
</a>
</div>
</form>
</div>
</div>
</main>
`;

View File

@@ -77,9 +77,9 @@ export const beginNameChange = (formId) => ({
});
// SAVE SETTINGS ACTIONS
export const saveSettings = (formId, commitValues, extendedProfile = {}) => ({
export const saveSettings = (formId, commitValues) => ({
type: SAVE_SETTINGS.BASE,
payload: { formId, commitValues, extendedProfile },
payload: { formId, commitValues },
});
export const saveSettingsBegin = () => ({

View File

@@ -25,7 +25,7 @@ export const EDUCATION_LEVELS = [
'jhs',
'el',
'none',
'other',
'o',
];
export const GENDER_OPTIONS = [
@@ -34,21 +34,6 @@ export const GENDER_OPTIONS = [
'm',
'o',
];
export const WORK_EXPERIENCE_OPTIONS = [
'',
'0',
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'10+',
];
export const COUNTRY_WITH_STATES = 'US';

View File

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

View File

@@ -83,8 +83,8 @@ export function* handleSaveSettings(action) {
yield put(saveSettingsBegin());
const { username, userId } = getAuthenticatedUser();
const { commitValues, formId, extendedProfile } = action.payload;
const commitData = Object.keys(extendedProfile).length > 0 ? extendedProfile : { [formId]: commitValues };
const { commitValues, formId } = action.payload;
const commitData = { [formId]: commitValues };
let savedValues = null;
if (formId === 'siteLanguage') {
const previousSiteLanguage = getLocale();

View File

@@ -106,6 +106,11 @@ const isEditingSelector = createSelector(
(name, accountSettings) => accountSettings.openFormId === name,
);
const confirmationValuesSelector = createSelector(
accountSettingsSelector,
accountSettings => accountSettings.confirmationValues,
);
const errorSelector = createSelector(
accountSettingsSelector,
accountSettings => accountSettings.errors,
@@ -156,7 +161,7 @@ function chooseFormValue(draft, committed) {
return draft !== undefined ? draft : committed;
}
export const formValuesSelector = createSelector(
const formValuesSelector = createSelector(
valuesSelector,
draftsSelector,
(values, drafts) => {
@@ -164,20 +169,6 @@ export const formValuesSelector = createSelector(
Object.entries(values).forEach(([name, value]) => {
if (typeof value === 'boolean') {
formValues[name] = chooseFormValue(drafts[name], value);
} else if (typeof value === 'object' && name === 'extended_profile' && value !== null) {
const extendedProfile = value.slice();
const draftsKeys = Object.keys(drafts);
if (draftsKeys.length !== 0) {
const draftFieldName = draftsKeys[0];
const index = extendedProfile.findIndex((profile) => profile.field_name === draftFieldName);
if (index !== -1) {
extendedProfile[index] = { field_name: draftFieldName, field_value: drafts[draftFieldName] };
}
}
formValues.extended_profile = [...extendedProfile];
} else {
formValues[name] = chooseFormValue(drafts[name], value) || '';
}
@@ -206,11 +197,6 @@ const activeAccountSelector = createSelector(
accountSettings => accountSettings.values.is_active,
);
const disabledCountriesSelector = createSelector(
accountSettingsSelector,
accountSettings => accountSettings.disabledCountries,
);
export const siteLanguageSelector = createSelector(
previousSiteLanguageSelector,
draftsSelector,
@@ -242,7 +228,6 @@ export const accountSettingsPageSelector = createSelector(
mostRecentApprovedVerifiedNameValueSelector,
mostRecentVerifiedNameSelector,
sortedVerifiedNameHistorySelector,
disabledCountriesSelector,
(
accountSettings,
siteLanguageOptions,
@@ -260,7 +245,6 @@ export const accountSettingsPageSelector = createSelector(
verifiedName,
mostRecentVerifiedName,
verifiedNameHistory,
disabledCountries,
) => ({
siteLanguageOptions,
siteLanguage,
@@ -281,7 +265,6 @@ export const accountSettingsPageSelector = createSelector(
verifiedName,
mostRecentVerifiedName,
verifiedNameHistory,
disabledCountries,
}),
);
@@ -306,6 +289,50 @@ export const certPreferenceSelector = createSelector(
}),
);
export const coachingConsentPageSelector = createSelector(
accountSettingsSelector,
formValuesSelector,
activeAccountSelector,
profileDataManagerSelector,
saveStateSelector,
confirmationValuesSelector,
errorSelector,
(
accountSettings,
formValues,
activeAccount,
profileDataManager,
saveState,
confirmationValues,
errors,
) => ({
loading: accountSettings.loading,
loaded: accountSettings.loaded,
loadingError: accountSettings.loadingError,
isActive: activeAccount,
profileDataManager,
formValues,
saveState,
confirmationValues,
formErrors: errors,
}),
);
export const demographicsSectionSelector = createSelector(
formValuesSelector,
draftsSelector,
errorSelector,
(
formValues,
drafts,
errors,
) => ({
formValues,
drafts,
formErrors: errors,
}),
);
export const nameChangeSelector = createSelector(
accountSettingsSelector,
formValuesSelector,

View File

@@ -1,72 +0,0 @@
import { profileDataManagerSelector, formValuesSelector } from './selectors';
const testValue = 'test VALUE';
describe('profileDataManagerSelector', () => {
it('returns the profileDataManager from the state', () => {
const state = {
accountSettings: {
profileDataManager: { testValue },
},
};
const result = profileDataManagerSelector(state);
expect(result).toEqual(state.accountSettings.profileDataManager);
});
it('should correctly select form values', () => {
const state = {
accountSettings: {
values: {
name: 'John Doe',
age: 25,
},
drafts: {
age: 26,
},
verifiedNameHistory: 'test',
confirmationValues: {},
},
};
const result = formValuesSelector(state);
const expected = {
name: 'John Doe',
age: 26,
verified_name: '',
useVerifiedNameForCerts: false,
};
expect(result).toEqual(expected);
});
it('should correctly select form values with extended_profile', () => {
// Mock data with extended_profile field in both values and drafts
const state = {
accountSettings: {
values: {
extended_profile: [
{ field_name: 'test_field', field_value: '5' },
],
},
drafts: { test_field: '6' },
verifiedNameHistory: 'test',
confirmationValues: {},
},
};
const result = formValuesSelector(state);
const expected = {
verified_name: '',
useVerifiedNameForCerts: false,
extended_profile: [ // Draft value should override the committed value
{ field_name: 'test_field', field_value: '6' }, // Value from the committed values
],
};
expect(result).toEqual(expected);
});
});

View File

@@ -1,12 +1,16 @@
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
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 { getCoachingPreferences, patchCoachingPreferences } from '../coaching/data/service';
import { getDemographics, getDemographicsOptions, patchDemographics } from '../demographics/data/service';
import { DEMOGRAPHICS_FIELDS } from '../demographics/data/utils';
const SOCIAL_PLATFORMS = [
{ id: 'twitter', key: 'social_link_twitter' },
@@ -151,6 +155,28 @@ 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();
@@ -187,22 +213,30 @@ export async function postVerifiedName(data) {
}
/**
* A single function to GET everything considered a setting. Currently encapsulates Account, Preferences, and
* ThirdPartyAuth.
* A single function to GET everything considered a setting.
* Currently encapsulates Account, Preferences, Coaching, ThirdPartyAuth, and Demographics
*/
export async function getSettings(username, userRoles) {
export async function getSettings(username, userRoles, userId) {
const [
account,
preferences,
thirdPartyAuthProviders,
profileDataManager,
timeZones,
coaching,
shouldDisplayDemographicsQuestionsResponse,
demographics,
demographicsOptions,
] = await Promise.all([
getAccount(username),
getPreferences(username),
getThirdPartyAuthProviders(),
getProfileDataManager(username, userRoles),
getTimeZones(),
getConfig().COACHING_ENABLED && getCoachingPreferences(userId),
getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && shouldDisplayDemographicsQuestions(),
getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && getDemographics(userId),
getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && getDemographicsOptions(),
]);
return {
@@ -211,25 +245,36 @@ export async function getSettings(username, userRoles) {
thirdPartyAuthProviders,
profileDataManager,
timeZones,
coaching,
shouldDisplayDemographicsSection: shouldDisplayDemographicsQuestionsResponse,
...demographics,
demographicsOptions,
};
}
/**
* A single function to PATCH everything considered a setting.
* Currently encapsulates Account, Preferences, ThirdPartyAuth
* Currently encapsulates Account, Preferences, coaching and ThirdPartyAuth
*/
export async function patchSettings(username, commitValues) {
export async function patchSettings(username, commitValues, userId) {
// 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 coachingKeys = ['coaching'];
const demographicsKeys = DEMOGRAPHICS_FIELDS;
const certificateKeys = ['useVerifiedNameForCerts'];
const isDemographicsKey = (value, key) => key.includes('demographics');
const accountCommitValues = omit(
commitValues,
preferenceKeys,
coachingKeys,
demographicsKeys,
certificateKeys,
);
const preferenceCommitValues = pick(commitValues, preferenceKeys);
const coachingCommitValues = pick(commitValues, coachingKeys);
const demographicsCommitValues = pickBy(commitValues, isDemographicsKey);
const certCommitValues = pick(commitValues, certificateKeys);
const patchRequests = [];
@@ -239,6 +284,12 @@ export async function patchSettings(username, commitValues) {
if (!isEmpty(preferenceCommitValues)) {
patchRequests.push(patchPreferences(username, preferenceCommitValues));
}
if (!isEmpty(coachingCommitValues)) {
patchRequests.push(patchCoachingPreferences(userId, coachingCommitValues));
}
if (!isEmpty(demographicsCommitValues)) {
patchRequests.push(patchDemographics(userId, demographicsCommitValues));
}
if (!isEmpty(certCommitValues)) {
patchRequests.push(postVerifiedNameConfig(username, certCommitValues));
}

View File

@@ -1,7 +1,8 @@
import { put } from 'redux-saga/effects';
import { logError } from '@edx/frontend-platform/logging';
import { history } from '@edx/frontend-platform';
export default function* handleFailure(error, navigate, failureAction = null, failureRedirectPath = null) {
export default function* handleFailure(error, failureAction = null, failureRedirectPath = null) {
if (error.fieldErrors && failureAction !== null) {
yield put(failureAction({ fieldErrors: error.fieldErrors }));
}
@@ -10,6 +11,6 @@ export default function* handleFailure(error, navigate, failureAction = null, fa
yield put(failureAction(error.message));
}
if (failureRedirectPath !== null) {
navigate(failureRedirectPath);
history.push(failureRedirectPath);
}
}

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Hyperlink } from '@openedx/paragon';
import { Hyperlink } from '@edx/paragon';
// Messages
import { getConfig } from '@edx/frontend-platform';
@@ -12,7 +12,7 @@ import messages from './messages';
// Components
import Alert from '../Alert';
const BeforeProceedingBanner = (props) => {
function BeforeProceedingBanner(props) {
const { instructionMessageId, intl, supportArticleUrl } = props;
return (
@@ -25,19 +25,17 @@ const BeforeProceedingBanner = (props) => {
defaultMessage="Before proceeding, please {actionLink}."
description="Error that appears if you are trying to delete your account, but something about your account needs attention first. The actionLink will be instructions, such as 'unlink your Facebook account'."
values={{
actionLink: supportArticleUrl ? (
actionLink: (
<Hyperlink destination={supportArticleUrl}>
{intl.formatMessage(messages[instructionMessageId])}
</Hyperlink>
) : (
intl.formatMessage(messages[instructionMessageId])
),
siteName: getConfig().SITE_NAME,
}}
/>
</Alert>
);
};
}
BeforeProceedingBanner.propTypes = {
instructionMessageId: PropTypes.string.isRequired,

View File

@@ -1,48 +0,0 @@
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;
import BeforeProceedingBanner from './BeforeProceedingBanner'; // eslint-disable-line import/first
const IntlBeforeProceedingBanner = injectIntl(BeforeProceedingBanner);
describe('BeforeProceedingBanner', () => {
it('should match the snapshot if SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT does not have a support link', () => {
const props = {
instructionMessageId: 'account.settings.delete.account.please.unlink',
intl: createIntl({ locale: 'en' }),
supportArticleUrl: '',
};
const tree = renderer
.create((
<IntlProvider locale="en">
<IntlBeforeProceedingBanner
{...props}
/>
</IntlProvider>
))
.toJSON();
expect(tree).toMatchSnapshot();
});
it('should match the snapshot when SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT has a support link', () => {
const props = {
instructionMessageId: 'account.settings.delete.account.please.unlink',
intl: createIntl({ locale: 'en' }),
supportArticleUrl: 'http://test-support.edx',
};
const tree = renderer
.create((
<IntlProvider locale="en">
<IntlBeforeProceedingBanner
{...props}
/>
</IntlProvider>
))
.toJSON();
expect(tree).toMatchSnapshot();
});
});

View File

@@ -2,12 +2,12 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {
AlertModal,
Button, Input, ValidationFormGroup, ActionRow,
} from '@openedx/paragon';
Button, Input, Modal, ValidationFormGroup,
} from '@edx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { faExclamationCircle, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { getConfig } from '@edx/frontend-platform';
import messages from './messages';
import Alert from '../Alert';
@@ -74,58 +74,59 @@ export class ConfirmationModal extends Component {
: 'account.settings.delete.account.modal.text.2';
return (
<AlertModal
isOpen={open}
<Modal
open={open}
title={intl.formatMessage(messages['account.settings.delete.account.modal.header'])}
onClose={onCancel}
footerNode={(
<ActionRow>
<Button variant="link" onClick={onCancel}>Cancel</Button>
<Button variant="danger" onClick={onSubmit}>Yes, Delete</Button>
</ActionRow>
body={(
<div>
{this.renderError()}
<Alert
className="alert-warning mt-n2"
icon={<FontAwesomeIcon className="mr-2" icon={faExclamationTriangle} />}
>
<h6>
{intl.formatMessage(
messages['account.settings.delete.account.modal.text.1'],
{ siteName: getConfig().SITE_NAME },
)}
</h6>
<p>
{intl.formatMessage(
messages[deleteAccountModalText2MessageKey],
{ siteName: getConfig().SITE_NAME },
)}
</p>
<p>
<PrintingInstructions />
</p>
</Alert>
<ValidationFormGroup
for={passwordFieldId}
invalid={errorType !== null}
invalidMessage={intl.formatMessage(invalidMessage)}
>
<label className="d-block" htmlFor={passwordFieldId}>
{intl.formatMessage(messages['account.settings.delete.account.modal.enter.password'])}
</label>
<Input
name="password"
id={passwordFieldId}
type="password"
value={password}
onChange={onChange}
/>
</ValidationFormGroup>
</div>
)}
>
<div className="p-3">
{this.renderError()}
<Alert
className="alert-warning mt-n2"
icon={<FontAwesomeIcon className="mr-2" icon={faExclamationTriangle} />}
>
<h6>
{intl.formatMessage(
messages['account.settings.delete.account.modal.text.1'],
{ siteName: getConfig().SITE_NAME },
)}
</h6>
<p>
{intl.formatMessage(
messages[deleteAccountModalText2MessageKey],
{ siteName: getConfig().SITE_NAME },
)}
</p>
<p>
<PrintingInstructions />
</p>
</Alert>
<ValidationFormGroup
for={passwordFieldId}
invalid={errorType !== null}
invalidMessage={intl.formatMessage(invalidMessage)}
>
<label className="d-block" htmlFor={passwordFieldId}>
{intl.formatMessage(messages['account.settings.delete.account.modal.enter.password'])}
</label>
<Input
name="password"
id={passwordFieldId}
type="password"
value={password}
onChange={onChange}
/>
</ValidationFormGroup>
</div>
</AlertModal>
buttons={[
<Button variant="danger" onClick={onSubmit}>
{intl.formatMessage(messages['account.settings.delete.account.modal.confirm.delete'])}
</Button>,
]}
closeText={intl.formatMessage(messages['account.settings.delete.account.modal.confirm.cancel'])}
renderHeaderCloseButton={false}
onClose={onCancel}
/>
);
}
}

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button, Hyperlink } from '@openedx/paragon';
import { Button, Hyperlink } from '@edx/paragon';
// Actions
import {
@@ -59,7 +59,6 @@ export class DeleteAccount extends React.Component {
hasLinkedTPA, isVerifiedAccount, status, errorType, intl,
} = this.props;
const canDelete = isVerifiedAccount && !hasLinkedTPA;
const supportArticleUrl = process.env.SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT;
// TODO: We lack a good way of providing custom language for a particular site. This is a hack
// to allow edx.org to fulfill its business requirements.
@@ -99,7 +98,7 @@ export class DeleteAccount extends React.Component {
)}
</p>
<p>
<Hyperlink destination="https://help.edx.org/edxlearner/s/topic/0TOQq0000001UdZOAU/account-basics">
<Hyperlink destination="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings">
{intl.formatMessage(messages['account.settings.delete.account.text.change.instead'])}
</Hyperlink>
</p>
@@ -116,14 +115,14 @@ export class DeleteAccount extends React.Component {
{isVerifiedAccount ? null : (
<BeforeProceedingBanner
instructionMessageId={optInInstructionMessageId}
supportArticleUrl="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-confirm-my-email"
supportArticleUrl="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-confirm-my-email-"
/>
)}
{hasLinkedTPA ? (
<BeforeProceedingBanner
instructionMessageId="account.settings.delete.account.please.unlink"
supportArticleUrl={supportArticleUrl}
supportArticleUrl="https://support.edx.org/hc/en-us/articles/207206067"
/>
) : null}

View File

@@ -4,10 +4,10 @@ import renderer from 'react-test-renderer';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
// Testing the modals separately, they just clutter up the snapshots if included here.
jest.mock('./ConfirmationModal', () => function ConfirmationModalMock() {
jest.mock('./ConfirmationModal', () => function () {
return <></>;
});
jest.mock('./SuccessModal', () => function SuccessModalMock() {
jest.mock('./SuccessModal', () => function () {
return <></>;
});

View File

@@ -1,17 +1,17 @@
import React from 'react';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Hyperlink } from '@openedx/paragon';
import { Hyperlink } from '@edx/paragon';
import { getConfig } from '@edx/frontend-platform';
import messages from './messages';
const PrintingInstructions = (props) => {
function PrintingInstructions(props) {
const actionLink = (
<Hyperlink
// TODO: What would a generic version of this link look like? Should
// CERTIFICATE_SHARING_HELP_URL really be a configuration variable? In the meantime,
// We've removed the link from the default message.
destination="https://help.edx.org/edxlearner/s/topic/0TOQq0000001UVVOA2/certificates"
destination="https://support.edx.org/hc/en-us/sections/115004173027-Receive-and-Share-edX-Certificates"
>
{props.intl.formatMessage(messages['account.settings.delete.account.text.3.link'])}
</Hyperlink>
@@ -38,7 +38,7 @@ const PrintingInstructions = (props) => {
values={{ actionLink }}
/>
);
};
}
PrintingInstructions.propTypes = {
intl: intlShape.isRequired,

View File

@@ -1,33 +1,29 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { ModalLayer, ModalCloseButton } from '@openedx/paragon';
import { Modal } from '@edx/paragon';
import messages from './messages';
export const SuccessModal = (props) => {
export function SuccessModal(props) {
const { status, intl, onClose } = props;
return (
<ModalLayer isOpen={status === 'deleted'} onClose={onClose}>
<div className="mw-sm p-5 bg-white mx-auto my-3">
<h3>
{intl.formatMessage(messages['account.settings.delete.account.modal.after.header'])}
</h3>
<div className="p-3">
<Modal
open={status === 'deleted'}
title={intl.formatMessage(messages['account.settings.delete.account.modal.after.header'])}
body={(
<div>
<p className="h6">
{intl.formatMessage(messages['account.settings.delete.account.modal.after.text'])}
</p>
</div>
<p>
<ModalCloseButton className="float-right" variant="link">Close</ModalCloseButton>
</p>
</div>
</ModalLayer>
)}
closeText={intl.formatMessage(messages['account.settings.delete.account.modal.after.button'])}
renderHeaderCloseButton={false}
onClose={onClose}
/>
);
};
}
SuccessModal.propTypes = {
status: PropTypes.oneOf(['confirming', 'pending', 'deleted', 'failed']),

View File

@@ -1,68 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BeforeProceedingBanner should match the snapshot if SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT does not have a support link 1`] = `
<div
className="alert d-flex align-items-start alert-warning mt-n2"
>
<div>
<svg
aria-hidden="true"
className="svg-inline--fa fa-triangle-exclamation mr-2"
data-icon="triangle-exclamation"
data-prefix="fas"
focusable="false"
role="img"
style={{}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
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={{}}
/>
</svg>
</div>
<div>
Before proceeding, please unlink all social media accounts.
</div>
</div>
`;
exports[`BeforeProceedingBanner should match the snapshot when SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT has a support link 1`] = `
<div
className="alert d-flex align-items-start alert-warning mt-n2"
>
<div>
<svg
aria-hidden="true"
className="svg-inline--fa fa-triangle-exclamation mr-2"
data-icon="triangle-exclamation"
data-prefix="fas"
focusable="false"
role="img"
style={{}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
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={{}}
/>
</svg>
</div>
<div>
Before proceeding, please
<a
className="pgn__hyperlink default-link standalone-link"
href="http://test-support.edx"
onClick={[Function]}
target="_self"
>
unlink all social media accounts
</a>
.
</div>
</div>
`;

View File

@@ -1,120 +1,80 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ConfirmationModal should match default closed confirmation modal snapshot 1`] = `null`;
exports[`ConfirmationModal should match empty password confirmation modal snapshot 1`] = `
[
exports[`ConfirmationModal should match default closed confirmation modal snapshot 1`] = `
Array [
<div
data-focus-guard={true}
style={
{
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={0}
className="fade"
role="presentation"
/>,
<div
className="pgn__modal-layer"
data-focus-lock-disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onScrollCapture={[Function]}
onTouchMoveCapture={[Function]}
onTouchStart={[Function]}
onWheelCapture={[Function]}
className="modal fade"
role="presentation"
>
<div
className="pgn__modal-content-container"
aria-labelledby="id1"
aria-modal={true}
className=""
role="dialog"
>
<div
className="pgn__modal-backdrop"
data-testid="modal-backdrop"
onClick={[MockFunction]}
onKeyDown={[MockFunction]}
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
<div
aria-label="Are you sure?"
className="pgn__modal pgn__modal-md pgn__modal-default pgn__modal-visible-overflow pgn__alert-modal"
role="dialog"
data-focus-lock-disabled="disabled"
onBlur={[Function]}
onFocus={[Function]}
onScrollCapture={[Function]}
onTouchMoveCapture={[Function]}
onWheelCapture={[Function]}
>
<div
className="pgn__modal-header"
className="modal-content"
>
<h2
className="pgn__modal-title"
>
Are you sure?
</h2>
</div>
<div
className="pgn__modal-body pgn__modal-body-scroll-top pgn__modal-body-scroll-bottom"
>
<div />
<div
className="pgn__modal-body-content"
className="modal-header"
>
<div
className="p-3"
<h2
className="modal-title"
id="id1"
>
<div
className="alert d-flex align-items-start alert-danger mt-n2"
>
<div>
<svg
aria-hidden="true"
className="svg-inline--fa fa-circle-exclamation mr-2"
data-icon="circle-exclamation"
data-prefix="fas"
focusable="false"
role="img"
style={{}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
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={{}}
/>
</svg>
</div>
<div>
<h6>
A password is required
</h6>
<p
className="text-danger"
>
Sorry, there was an error trying to process your request. Please try again later.
</p>
</div>
</div>
Are you sure?
</h2>
</div>
<div
className="modal-body"
>
<div>
<div
className="alert d-flex align-items-start alert-warning mt-n2"
>
<div>
<svg
aria-hidden="true"
className="svg-inline--fa fa-triangle-exclamation mr-2"
data-icon="triangle-exclamation"
className="svg-inline--fa fa-exclamation-triangle fa-w-18 mr-2"
data-icon="exclamation-triangle"
data-prefix="fas"
focusable="false"
role="img"
style={{}}
viewBox="0 0 512 512"
style={Object {}}
viewBox="0 0 576 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
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"
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"
fill="currentColor"
style={{}}
style={Object {}}
/>
</svg>
</div>
@@ -132,175 +92,6 @@ exports[`ConfirmationModal should match empty password confirmation modal snapsh
</div>
<div
className="form-group"
data-testid="validation-form-group"
>
<label
className="d-block"
htmlFor="passwordFieldId"
>
If you still wish to continue and delete your account, please enter your account password:
</label>
<input
aria-describedby="passwordFieldId-invalid-feedback"
className="form-control is-invalid"
id="passwordFieldId"
name="password"
onChange={[MockFunction]}
type="password"
value="fluffy bunnies"
/>
<strong
className="invalid-feedback"
id="passwordFieldId-invalid-feedback"
>
A password is required
</strong>
</div>
</div>
</div>
<div />
</div>
<div
className="pgn__modal-footer"
>
<div
className="pgn__action-row"
>
<button
className="btn btn-link"
disabled={false}
onClick={[MockFunction]}
type="button"
>
Cancel
</button>
<button
className="btn btn-danger"
disabled={false}
onClick={[MockFunction]}
type="button"
>
Yes, Delete
</button>
</div>
</div>
</div>
</div>
</div>,
<div
data-focus-guard={true}
style={
{
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={0}
/>,
]
`;
exports[`ConfirmationModal should match open confirmation modal snapshot 1`] = `
[
<div
data-focus-guard={true}
style={
{
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>,
<div
className="pgn__modal-layer"
data-focus-lock-disabled="disabled"
onBlur={[Function]}
onFocus={[Function]}
onScrollCapture={[Function]}
onTouchMoveCapture={[Function]}
onWheelCapture={[Function]}
>
<div
className="pgn__modal-content-container"
>
<div
className="pgn__modal-backdrop"
data-testid="modal-backdrop"
onClick={[MockFunction]}
onKeyDown={[MockFunction]}
/>
<div
aria-label="Are you sure?"
className="pgn__modal pgn__modal-md pgn__modal-default pgn__modal-visible-overflow pgn__alert-modal"
role="dialog"
>
<div
className="pgn__modal-header"
>
<h2
className="pgn__modal-title"
>
Are you sure?
</h2>
</div>
<div
className="pgn__modal-body pgn__modal-body-scroll-top pgn__modal-body-scroll-bottom"
>
<div />
<div
className="pgn__modal-body-content"
>
<div
className="p-3"
>
<div
className="alert d-flex align-items-start alert-warning mt-n2"
>
<div>
<svg
aria-hidden="true"
className="svg-inline--fa fa-triangle-exclamation mr-2"
data-icon="triangle-exclamation"
data-prefix="fas"
focusable="false"
role="img"
style={{}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
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={{}}
/>
</svg>
</div>
<div>
<h6>
You have selected "Delete My Account". Deletion of your account and personal data is permanent and cannot be undone. localhost will not be able to recover your account or the data that is deleted.
</h6>
<p>
If you proceed, you will be unable to use this account to take courses on localhost.
</p>
<p>
You may also lose access to verified certificates and other program credentials. You can make a copy of these for your records before proceeding with deletion.
</p>
</div>
</div>
<div
className="form-group"
data-testid="validation-form-group"
>
<label
className="d-block"
@@ -326,18 +117,13 @@ exports[`ConfirmationModal should match open confirmation modal snapshot 1`] = `
</div>
</div>
</div>
<div />
</div>
<div
className="pgn__modal-footer"
>
<div
className="pgn__action-row"
className="modal-footer"
>
<button
className="btn btn-link"
disabled={false}
onClick={[MockFunction]}
onClick={[Function]}
type="button"
>
Cancel
@@ -353,22 +139,375 @@ exports[`ConfirmationModal should match open confirmation modal snapshot 1`] = `
</div>
</div>
</div>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
</div>
</div>,
]
`;
exports[`ConfirmationModal should match empty password confirmation modal snapshot 1`] = `
Array [
<div
className="modal-backdrop show"
role="presentation"
/>,
<div
className="modal show d-block"
role="presentation"
>
<div
aria-labelledby="id3"
aria-modal={true}
className="modal-dialog"
role="dialog"
>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={0}
/>
<div
data-focus-lock-disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onScrollCapture={[Function]}
onTouchMoveCapture={[Function]}
onTouchStart={[Function]}
onWheelCapture={[Function]}
>
<div
className="modal-content"
>
<div
className="modal-header"
>
<h2
className="modal-title"
id="id3"
>
Are you sure?
</h2>
</div>
<div
className="modal-body"
>
<div>
<div
className="alert d-flex align-items-start alert-danger mt-n2"
>
<div>
<svg
aria-hidden="true"
className="svg-inline--fa fa-exclamation-circle fa-w-16 mr-2"
data-icon="exclamation-circle"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
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"
fill="currentColor"
style={Object {}}
/>
</svg>
</div>
<div>
<h6>
A password is required
</h6>
<p
className="text-danger"
>
Sorry, there was an error trying to process your request. Please try again later.
</p>
</div>
</div>
<div
className="alert d-flex align-items-start alert-warning mt-n2"
>
<div>
<svg
aria-hidden="true"
className="svg-inline--fa fa-exclamation-triangle fa-w-18 mr-2"
data-icon="exclamation-triangle"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 576 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"
fill="currentColor"
style={Object {}}
/>
</svg>
</div>
<div>
<h6>
You have selected "Delete My Account". Deletion of your account and personal data is permanent and cannot be undone. localhost will not be able to recover your account or the data that is deleted.
</h6>
<p>
If you proceed, you will be unable to use this account to take courses on localhost.
</p>
<p>
You may also lose access to verified certificates and other program credentials. You can make a copy of these for your records before proceeding with deletion.
</p>
</div>
</div>
<div
className="form-group"
>
<label
className="d-block"
htmlFor="passwordFieldId"
>
If you still wish to continue and delete your account, please enter your account password:
</label>
<input
aria-describedby="passwordFieldId-invalid-feedback"
className="form-control is-invalid"
id="passwordFieldId"
name="password"
onChange={[MockFunction]}
type="password"
value="fluffy bunnies"
/>
<strong
className="invalid-feedback"
id="passwordFieldId-invalid-feedback"
>
A password is required
</strong>
</div>
</div>
</div>
<div
className="modal-footer"
>
<button
className="btn btn-link"
disabled={false}
onClick={[Function]}
type="button"
>
Cancel
</button>
<button
className="btn btn-danger"
disabled={false}
onClick={[MockFunction]}
type="button"
>
Yes, Delete
</button>
</div>
</div>
</div>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={0}
/>
</div>
</div>,
]
`;
exports[`ConfirmationModal should match open confirmation modal snapshot 1`] = `
Array [
<div
className="modal-backdrop show"
role="presentation"
/>,
<div
className="modal show d-block"
role="presentation"
>
<div
aria-labelledby="id2"
aria-modal={true}
className="modal-dialog"
role="dialog"
>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
<div
data-focus-lock-disabled="disabled"
onBlur={[Function]}
onFocus={[Function]}
onScrollCapture={[Function]}
onTouchMoveCapture={[Function]}
onWheelCapture={[Function]}
>
<div
className="modal-content"
>
<div
className="modal-header"
>
<h2
className="modal-title"
id="id2"
>
Are you sure?
</h2>
</div>
<div
className="modal-body"
>
<div>
<div
className="alert d-flex align-items-start alert-warning mt-n2"
>
<div>
<svg
aria-hidden="true"
className="svg-inline--fa fa-exclamation-triangle fa-w-18 mr-2"
data-icon="exclamation-triangle"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 576 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"
fill="currentColor"
style={Object {}}
/>
</svg>
</div>
<div>
<h6>
You have selected "Delete My Account". Deletion of your account and personal data is permanent and cannot be undone. localhost will not be able to recover your account or the data that is deleted.
</h6>
<p>
If you proceed, you will be unable to use this account to take courses on localhost.
</p>
<p>
You may also lose access to verified certificates and other program credentials. You can make a copy of these for your records before proceeding with deletion.
</p>
</div>
</div>
<div
className="form-group"
>
<label
className="d-block"
htmlFor="passwordFieldId"
>
If you still wish to continue and delete your account, please enter your account password:
</label>
<input
aria-describedby=""
className="form-control"
id="passwordFieldId"
name="password"
onChange={[MockFunction]}
type="password"
value="fluffy bunnies"
/>
<strong
className="invalid-feedback"
id="passwordFieldId-invalid-feedback"
>
Unable to delete account
</strong>
</div>
</div>
</div>
<div
className="modal-footer"
>
<button
className="btn btn-link"
disabled={false}
onClick={[Function]}
type="button"
>
Cancel
</button>
<button
className="btn btn-danger"
disabled={false}
onClick={[MockFunction]}
type="button"
>
Yes, Delete
</button>
</div>
</div>
</div>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
</div>
</div>,
<div
data-focus-guard={true}
style={
{
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>,
]
`;

View File

@@ -27,7 +27,7 @@ exports[`DeleteAccount should match default section snapshot 1`] = `
<p>
<a
className="pgn__hyperlink default-link standalone-link"
href="https://help.edx.org/edxlearner/s/topic/0TOQq0000001UdZOAU/account-basics"
href="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings"
onClick={[Function]}
target="_self"
>
@@ -74,7 +74,7 @@ exports[`DeleteAccount should match unverified account section snapshot 1`] = `
<p>
<a
className="pgn__hyperlink default-link standalone-link"
href="https://help.edx.org/edxlearner/s/topic/0TOQq0000001UdZOAU/account-basics"
href="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings"
onClick={[Function]}
target="_self"
>
@@ -97,19 +97,19 @@ exports[`DeleteAccount should match unverified account section snapshot 1`] = `
<div>
<svg
aria-hidden="true"
className="svg-inline--fa fa-triangle-exclamation mr-2"
data-icon="triangle-exclamation"
className="svg-inline--fa fa-exclamation-triangle fa-w-18 mr-2"
data-icon="exclamation-triangle"
data-prefix="fas"
focusable="false"
role="img"
style={{}}
viewBox="0 0 512 512"
style={Object {}}
viewBox="0 0 576 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
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"
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"
fill="currentColor"
style={{}}
style={Object {}}
/>
</svg>
</div>
@@ -117,7 +117,7 @@ exports[`DeleteAccount should match unverified account section snapshot 1`] = `
Before proceeding, please
<a
className="pgn__hyperlink default-link standalone-link"
href="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-confirm-my-email"
href="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-confirm-my-email-"
onClick={[Function]}
target="_self"
>
@@ -156,7 +156,7 @@ exports[`DeleteAccount should match unverified account section snapshot 2`] = `
<p>
<a
className="pgn__hyperlink default-link standalone-link"
href="https://help.edx.org/edxlearner/s/topic/0TOQq0000001UdZOAU/account-basics"
href="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings"
onClick={[Function]}
target="_self"
>
@@ -179,19 +179,19 @@ exports[`DeleteAccount should match unverified account section snapshot 2`] = `
<div>
<svg
aria-hidden="true"
className="svg-inline--fa fa-triangle-exclamation mr-2"
data-icon="triangle-exclamation"
className="svg-inline--fa fa-exclamation-triangle fa-w-18 mr-2"
data-icon="exclamation-triangle"
data-prefix="fas"
focusable="false"
role="img"
style={{}}
viewBox="0 0 512 512"
style={Object {}}
viewBox="0 0 576 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
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"
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"
fill="currentColor"
style={{}}
style={Object {}}
/>
</svg>
</div>
@@ -199,7 +199,7 @@ exports[`DeleteAccount should match unverified account section snapshot 2`] = `
Before proceeding, please
<a
className="pgn__hyperlink default-link standalone-link"
href="https://help.edx.org/edxlearner/s/article/How-do-I-link-or-unlink-my-edX-account-to-a-social-media-account"
href="https://support.edx.org/hc/en-us/articles/207206067"
onClick={[Function]}
target="_self"
>

View File

@@ -1,90 +1,486 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SuccessModal should match default closed success modal snapshot 1`] = `null`;
exports[`SuccessModal should match default closed success modal snapshot 2`] = `null`;
exports[`SuccessModal should match default closed success modal snapshot 3`] = `null`;
exports[`SuccessModal should match default closed success modal snapshot 4`] = `null`;
exports[`SuccessModal should match open success modal snapshot 1`] = `
[
exports[`SuccessModal should match default closed success modal snapshot 1`] = `
Array [
<div
data-focus-guard={true}
style={
{
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
className="fade"
role="presentation"
/>,
<div
className="pgn__modal-layer"
data-focus-lock-disabled="disabled"
onBlur={[Function]}
onFocus={[Function]}
onScrollCapture={[Function]}
onTouchMoveCapture={[Function]}
onWheelCapture={[Function]}
className="modal fade"
role="presentation"
>
<div
className="pgn__modal-content-container"
aria-labelledby="id1"
aria-modal={true}
className=""
role="dialog"
>
<div
className="pgn__modal-backdrop"
data-testid="modal-backdrop"
onClick={[MockFunction]}
onKeyDown={[MockFunction]}
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
<div
className="mw-sm p-5 bg-white mx-auto my-3"
data-focus-lock-disabled="disabled"
onBlur={[Function]}
onFocus={[Function]}
onScrollCapture={[Function]}
onTouchMoveCapture={[Function]}
onWheelCapture={[Function]}
>
<h3>
We're sorry to see you go! Your account will be deleted shortly.
</h3>
<div
className="p-3"
className="modal-content"
>
<p
className="h6"
<div
className="modal-header"
>
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
</p>
<h2
className="modal-title"
id="id1"
>
We're sorry to see you go! Your account will be deleted shortly.
</h2>
</div>
<div
className="modal-body"
>
<div>
<p
className="h6"
>
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
</p>
</div>
</div>
<div
className="modal-footer"
>
<button
className="btn btn-link"
disabled={false}
onClick={[Function]}
type="button"
>
Close
</button>
</div>
</div>
<p>
<button
className="pgn__modal-close-button float-right btn btn-link"
disabled={false}
onClick={[Function]}
type="button"
>
Close
</button>
</p>
</div>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
</div>
</div>,
]
`;
exports[`SuccessModal should match default closed success modal snapshot 2`] = `
Array [
<div
className="fade"
role="presentation"
/>,
<div
className="modal fade"
role="presentation"
>
<div
aria-labelledby="id2"
aria-modal={true}
className=""
role="dialog"
>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
<div
data-focus-lock-disabled="disabled"
onBlur={[Function]}
onFocus={[Function]}
onScrollCapture={[Function]}
onTouchMoveCapture={[Function]}
onWheelCapture={[Function]}
>
<div
className="modal-content"
>
<div
className="modal-header"
>
<h2
className="modal-title"
id="id2"
>
We're sorry to see you go! Your account will be deleted shortly.
</h2>
</div>
<div
className="modal-body"
>
<div>
<p
className="h6"
>
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
</p>
</div>
</div>
<div
className="modal-footer"
>
<button
className="btn btn-link"
disabled={false}
onClick={[Function]}
type="button"
>
Close
</button>
</div>
</div>
</div>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
</div>
</div>,
]
`;
exports[`SuccessModal should match default closed success modal snapshot 3`] = `
Array [
<div
className="fade"
role="presentation"
/>,
<div
className="modal fade"
role="presentation"
>
<div
aria-labelledby="id3"
aria-modal={true}
className=""
role="dialog"
>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
<div
data-focus-lock-disabled="disabled"
onBlur={[Function]}
onFocus={[Function]}
onScrollCapture={[Function]}
onTouchMoveCapture={[Function]}
onWheelCapture={[Function]}
>
<div
className="modal-content"
>
<div
className="modal-header"
>
<h2
className="modal-title"
id="id3"
>
We're sorry to see you go! Your account will be deleted shortly.
</h2>
</div>
<div
className="modal-body"
>
<div>
<p
className="h6"
>
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
</p>
</div>
</div>
<div
className="modal-footer"
>
<button
className="btn btn-link"
disabled={false}
onClick={[Function]}
type="button"
>
Close
</button>
</div>
</div>
</div>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
</div>
</div>,
]
`;
exports[`SuccessModal should match default closed success modal snapshot 4`] = `
Array [
<div
className="fade"
role="presentation"
/>,
<div
className="modal fade"
role="presentation"
>
<div
aria-labelledby="id4"
aria-modal={true}
className=""
role="dialog"
>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
<div
data-focus-lock-disabled="disabled"
onBlur={[Function]}
onFocus={[Function]}
onScrollCapture={[Function]}
onTouchMoveCapture={[Function]}
onWheelCapture={[Function]}
>
<div
className="modal-content"
>
<div
className="modal-header"
>
<h2
className="modal-title"
id="id4"
>
We're sorry to see you go! Your account will be deleted shortly.
</h2>
</div>
<div
className="modal-body"
>
<div>
<p
className="h6"
>
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
</p>
</div>
</div>
<div
className="modal-footer"
>
<button
className="btn btn-link"
disabled={false}
onClick={[Function]}
type="button"
>
Close
</button>
</div>
</div>
</div>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
</div>
</div>,
]
`;
exports[`SuccessModal should match open success modal snapshot 1`] = `
Array [
<div
className="modal-backdrop show"
role="presentation"
/>,
<div
className="modal show d-block"
role="presentation"
>
<div
aria-labelledby="id5"
aria-modal={true}
className="modal-dialog"
role="dialog"
>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
<div
data-focus-lock-disabled="disabled"
onBlur={[Function]}
onFocus={[Function]}
onScrollCapture={[Function]}
onTouchMoveCapture={[Function]}
onWheelCapture={[Function]}
>
<div
className="modal-content"
>
<div
className="modal-header"
>
<h2
className="modal-title"
id="id5"
>
We're sorry to see you go! Your account will be deleted shortly.
</h2>
</div>
<div
className="modal-body"
>
<div>
<p
className="h6"
>
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
</p>
</div>
</div>
<div
className="modal-footer"
>
<button
className="btn btn-link"
disabled={false}
onClick={[Function]}
type="button"
>
Close
</button>
</div>
</div>
</div>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
</div>
</div>,
<div
data-focus-guard={true}
style={
{
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>,
]
`;

View File

@@ -0,0 +1,79 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { CheckBox } from '@edx/paragon';
import { DECLINED } from '../data/constants';
function 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">
<CheckBox
type="checkbox"
id={option.value}
name={option.value}
value={option.value}
checked={isChecked}
autoFocus={isFirst}
label={option.label}
onChange={(value) => handleToggle(value, option.value)}
/>
</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;

View File

@@ -0,0 +1,357 @@
import { getConfig } from '@edx/frontend-platform';
import {
FormattedMessage,
injectIntl,
intlShape,
} from '@edx/frontend-platform/i18n';
import { Hyperlink, Input } from '@edx/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 EditableField from '../EditableField';
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">
<EditableField
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 && (
<Input
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"
/>
)}
</EditableField>
<EditableField
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}
/>
</EditableField>
<EditableField
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}
/>
<EditableField
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}
/>
<EditableField
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}
/>
<EditableField
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}
/>
<EditableField
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 && (
<Input
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"
/>
)}
</EditableField>
<EditableField
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}
/>
<EditableField
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));

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,584 @@
/* 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();
});
});

View File

@@ -0,0 +1,3946 @@
// 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"
style={
Object {
"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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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"
style={
Object {
"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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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"
style={
Object {
"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"
style={
Object {
"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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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"
style={
Object {
"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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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"
style={
Object {
"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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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"
style={
Object {
"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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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={
Object {
"height": null,
}
}
>
<div
style={
Object {
"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={Object {}}
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={Object {}}
/>
</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>
`;

View File

@@ -1,19 +0,0 @@
import React from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
export const withNavigate = Component => {
const WrappedComponent = props => {
const navigate = useNavigate();
return <Component {...props} navigate={navigate} />;
};
return WrappedComponent;
};
export const withLocation = Component => {
const WrappedComponent = props => {
const location = useLocation();
return <Component {...props} location={location.pathname} />;
};
return WrappedComponent;
};

View File

@@ -1,38 +0,0 @@
import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import { withLocation, withNavigate } from './hoc';
const mockedNavigator = jest.fn();
jest.mock('react-router-dom', () => ({
useNavigate: () => mockedNavigator,
useLocation: () => ({
pathname: '/current-location',
}),
}));
// eslint-disable-next-line react/prop-types
const MockComponent = ({ navigate, location }) => (
// eslint-disable-next-line react/button-has-type, react/prop-types
<button data-testid="btn" onClick={() => navigate('/some-route')}>{location}</button>
);
const WrappedComponent = withNavigate(withLocation(MockComponent));
test('Provide Navigation to Component', () => {
render(
<WrappedComponent />,
);
const btn = screen.getByTestId('btn');
fireEvent.click(btn);
expect(mockedNavigator).toHaveBeenCalledWith('/some-route');
});
test('Provide Location Pathname to Component', () => {
render(
<WrappedComponent />,
);
expect(screen.getByTestId('btn').textContent).toContain('/current-location');
});

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useState } from 'react';
import { connect, useDispatch } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { useHistory } from 'react-router-dom';
import PropTypes from 'prop-types';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
@@ -13,7 +13,7 @@ import {
Form,
ModalDialog,
StatefulButton,
} from '@openedx/paragon';
} from '@edx/paragon';
import { closeForm, saveSettingsReset } from '../data/actions';
import { nameChangeSelector } from '../data/selectors';
@@ -21,15 +21,15 @@ import { nameChangeSelector } from '../data/selectors';
import { requestNameChange, requestNameChangeFailure, requestNameChangeReset } from './data/actions';
import messages from './messages';
const NameChangeModal = ({
function NameChangeModal({
targetFormId,
errors,
formValues,
intl,
saveState,
}) => {
}) {
const dispatch = useDispatch();
const navigate = useNavigate();
const { push } = useHistory();
const { username } = getAuthenticatedUser();
const [verifiedNameInput, setVerifiedNameInput] = useState(formValues.verified_name || '');
const [confirmedWarning, setConfirmedWarning] = useState(false);
@@ -69,9 +69,9 @@ const NameChangeModal = ({
useEffect(() => {
if (saveState === 'complete') {
handleClose();
navigate(`/id-verification?next=${encodeURIComponent('account/settings')}`);
push(`/id-verification?next=${encodeURIComponent('account/settings')}`);
}
}, [handleClose, navigate, saveState]);
}, [handleClose, push, saveState]);
function renderErrors() {
if (Object.keys(errors).length > 0) {
@@ -183,7 +183,7 @@ const NameChangeModal = ({
</ModalDialog>
);
};
}
NameChangeModal.propTypes = {
targetFormId: PropTypes.string.isRequired,

View File

@@ -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 identification card.',
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.name.change.id.name.placeholder': {

View File

@@ -2,13 +2,14 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { BrowserRouter as Router } from 'react-router-dom';
import { Router } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import {
fireEvent,
render,
screen,
} from '@testing-library/react';
import { createMemoryHistory } from 'history';
import * as auth from '@edx/frontend-platform/auth';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
@@ -27,6 +28,8 @@ jest.mock('react-redux', () => ({
jest.mock('@edx/frontend-platform/auth');
jest.mock('../../data/selectors', () => jest.fn().mockImplementation(() => ({ nameChangeSelector: () => ({}) })));
const history = createMemoryHistory();
const IntlNameChange = injectIntl(NameChange);
const mockStore = configureStore();
@@ -36,7 +39,7 @@ describe('NameChange', () => {
let store = {};
const reduxWrapper = children => (
<Router>
<Router history={history}>
<IntlProvider locale="en">
<Provider store={store}>{children}</Provider>
</IntlProvider>
@@ -165,6 +168,6 @@ describe('NameChange', () => {
props.saveState = 'complete';
render(reduxWrapper(<IntlNameChange {...props} />));
expect(window.location.pathname).toEqual('/id-verification');
expect(history.location.pathname).toEqual('/id-verification');
});
});

View File

@@ -2,13 +2,13 @@ import React from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Hyperlink } from '@openedx/paragon';
import { Hyperlink } from '@edx/paragon';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
import Alert from '../Alert';
const ConfirmationAlert = (props) => {
function ConfirmationAlert(props) {
const { email } = props;
const technicalSupportLink = (
@@ -39,7 +39,7 @@ const ConfirmationAlert = (props) => {
/>
</Alert>
);
};
}
ConfirmationAlert.propTypes = {
email: PropTypes.string.isRequired,

View File

@@ -5,17 +5,19 @@ import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
import Alert from '../Alert';
const RequestInProgressAlert = () => (
<Alert
className="alert-warning mt-n2"
icon={<FontAwesomeIcon className="mr-2" icon={faExclamationTriangle} />}
>
<FormattedMessage
id="account.settings.editable.field.password.reset.button.forbidden"
defaultMessage="Your previous request is in progress, please try again in few moments."
description="A message displayed when a previous password reset request is still in progress."
/>
</Alert>
);
function RequestInProgressAlert() {
return (
<Alert
className="alert-warning mt-n2"
icon={<FontAwesomeIcon className="mr-2" icon={faExclamationTriangle} />}
>
<FormattedMessage
id="account.settings.editable.field.password.reset.button.forbidden"
defaultMessage="Your previous request is in progress, please try again in few moments."
description="A message displayed when a previous password reset request is still in progress."
/>
</Alert>
);
}
export default RequestInProgressAlert;

View File

@@ -2,14 +2,14 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { StatefulButton } from '@openedx/paragon';
import { StatefulButton } from '@edx/paragon';
import { resetPassword } from './data/actions';
import messages from './messages';
import ConfirmationAlert from './ConfirmationAlert';
import RequestInProgressAlert from './RequestInProgressAlert';
const ResetPassword = (props) => {
function ResetPassword(props) {
const { email, intl, status } = props;
return (
<div className="form-group">
@@ -47,7 +47,7 @@ const ResetPassword = (props) => {
{status === 'forbidden' ? <RequestInProgressAlert /> : null}
</div>
);
};
}
ResetPassword.propTypes = {
email: PropTypes.string,

View File

@@ -2,9 +2,8 @@ import { AsyncActionType } from '../data/utils';
export const FETCH_SITE_LANGUAGES = new AsyncActionType('SITE_LANGUAGE', 'FETCH_SITE_LANGUAGES');
export const fetchSiteLanguages = handleNavigation => ({
export const fetchSiteLanguages = () => ({
type: FETCH_SITE_LANGUAGES.BASE,
payload: { handleNavigation },
});
export const fetchSiteLanguagesBegin = () => ({

View File

@@ -19,11 +19,6 @@ const siteLanguageList = [
name: 'Español (Latinoamérica)',
released: true,
},
{
code: 'fa-ir',
name: 'فارسی',
released: true,
},
{
code: 'fr',
name: 'Français',
@@ -74,61 +69,6 @@ const siteLanguageList = [
name: '中文 (简体)',
released: true,
},
{
code: 'pt-pt',
name: 'Português',
released: true,
},
{
code: 'it-it',
name: 'Italian',
released: true,
},
{
code: 'de-de',
name: 'German',
released: true,
},
{
code: 'hi',
name: 'Hindi',
released: true,
},
{
code: 'fr-ca',
name: 'French (CA)',
released: true,
},
{
code: 'te',
name: 'తెలుగు',
released: true,
},
{
code: 'da',
name: 'dansk',
released: true,
},
{
code: 'el',
name: 'Ελληνικά',
released: true,
},
{
code: 'es-es',
name: 'Español (España)',
released: true,
},
{
code: 'sw',
name: 'Kiswahili',
released: true,
},
{
code: 'tr-tr',
name: 'Türkçe (Türkiye)',
released: true,
},
];
export default siteLanguageList;

View File

@@ -10,13 +10,13 @@ import {
import { getSiteLanguageList } from './service';
import { handleFailure } from '../data/utils';
function* handleFetchSiteLanguages(action) {
function* handleFetchSiteLanguages() {
try {
yield put(fetchSiteLanguagesBegin());
const siteLanguageList = yield call(getSiteLanguageList);
yield put(fetchSiteLanguagesSuccess(siteLanguageList));
} catch (e) {
yield call(handleFailure, e, action.payload.handleNavigation, fetchSiteLanguagesFailure);
yield call(handleFailure, e, fetchSiteLanguagesFailure);
}
}

View File

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

View File

@@ -1,111 +0,0 @@
import React from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
import { AppContext } from '@edx/frontend-platform/react';
import {
render, screen, fireEvent,
} from '@testing-library/react';
import configureStore from 'redux-mock-store';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
import AccountSettingsPage from '../AccountSettingsPage';
import mockData from './mockData';
const mockDispatch = jest.fn();
jest.mock('@edx/frontend-platform/analytics', () => ({
sendTrackingLogEvent: jest.fn(),
getCountryList: jest.fn(),
}));
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useDispatch: () => mockDispatch,
}));
jest.mock('@edx/frontend-platform/auth');
const IntlAccountSettingsPage = injectIntl(AccountSettingsPage);
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
describe('AccountSettingsPage', () => {
let props = {};
let store = {};
const appContext = { locale: 'en', authenticatedUser: { userId: 3, roles: [] } };
const reduxWrapper = children => (
<AppContext.Provider value={appContext}>
<Router>
<IntlProvider locale="en">
<Provider store={store}>
{children}
</Provider>
</IntlProvider>
</Router>
</AppContext.Provider>
);
beforeEach(() => {
store = mockStore(mockData);
props = {
loaded: true,
siteLanguage: {},
formValues: {
username: 'test_username',
accomplishments_shared: false,
name: 'test_name',
email: 'test_email@test.com',
id: 534,
extended_profile: [
{
field_name: 'work_experience',
field_value: '',
},
],
},
fetchSettings: jest.fn(),
};
});
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} />));
const workExperienceText = getByText('Work Experience');
const workExperienceEditButton = workExperienceText.parentElement.querySelector('button');
expect(workExperienceEditButton).toBeInTheDocument();
store = mockStore({
...mockData,
accountSettings: {
...mockData.accountSettings,
openFormId: 'work_experience',
},
});
rerender(reduxWrapper(<IntlAccountSettingsPage {...props} />));
const submitButton = screen.getByText('Save');
expect(submitButton).toBeInTheDocument();
const workExperienceSelect = getByLabelText('Work Experience');
// Use fireEvent.change to simulate changing the selected value
fireEvent.change(workExperienceSelect, { target: { value: '4' } });
fireEvent.click(submitButton);
});
});

View File

@@ -1,169 +0,0 @@
import React from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import { Provider } from 'react-redux';
import renderer from 'react-test-renderer';
import configureStore from 'redux-mock-store';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
import EditableSelectField from '../EditableSelectField';
const mockDispatch = jest.fn();
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useDispatch: () => mockDispatch,
}));
jest.mock('@edx/frontend-platform/auth');
jest.mock('../data/selectors', () => jest.fn().mockImplementation(() => ({ certPreferenceSelector: () => ({}) })));
const IntlEditableSelectField = injectIntl(EditableSelectField);
const mockStore = configureStore();
describe('EditableSelectField', () => {
let props = {};
let store = {};
const reduxWrapper = children => (
<Router>
<IntlProvider locale="en">
<Provider store={store}>{children}</Provider>
</IntlProvider>
</Router>
);
beforeEach(() => {
store = mockStore();
props = {
name: 'testField',
label: 'Main Label',
emptyLabel: 'Empty Main Label',
type: 'text',
value: 'Test Field',
userSuppliedValue: '',
options: [
{
label: 'Default Option',
value: 'defaultOption',
},
{
label: 'User Options',
group: [
{
label: 'Suboption 1',
value: 'suboption1',
},
],
},
{
label: 'Other Options',
group: [
{
label: 'Suboption 2',
value: 'suboption2',
},
{
label: 'Suboption 3',
value: 'suboption3',
},
],
},
],
saveState: 'default',
error: '',
confirmationMessageDefinition: {
id: 'confirmationMessageId',
defaultMessage: 'Default Confirmation Message',
description: 'Description of the confirmation message',
},
confirmationValue: 'Confirmation Value',
helpText: 'Helpful Text',
isEditing: false,
isEditable: true,
isGrayedOut: false,
};
});
afterEach(() => jest.clearAllMocks());
it('renders EditableSelectField correctly with editing disabled', () => {
const tree = renderer.create(reduxWrapper(<IntlEditableSelectField {...props} />)).toJSON();
expect(tree).toMatchSnapshot();
});
it('renders EditableSelectField correctly with editing enabled', () => {
props = {
...props,
isEditing: true,
};
const tree = renderer.create(reduxWrapper(<IntlEditableSelectField {...props} />)).toJSON();
expect(tree).toMatchSnapshot();
});
it('renders EditableSelectField with an error', () => {
const errorProps = {
...props,
error: 'This is an error message',
};
const tree = renderer.create(reduxWrapper(<IntlEditableSelectField {...errorProps} />)).toJSON();
expect(tree).toMatchSnapshot();
});
it('renders selectOptions when option has a group', () => {
const propsWithGroup = {
...props,
options: [
{
label: 'User Options',
group: [
{
label: 'Suboption 1',
value: 'suboption1',
},
],
},
],
};
const tree = renderer.create(reduxWrapper(<IntlEditableSelectField {...propsWithGroup} />)).toJSON();
expect(tree).toMatchSnapshot();
});
it('renders selectOptions when option does not have a group', () => {
const propsWithoutGroup = {
...props,
options: [
{
label: 'Default Option',
value: 'defaultOption',
},
],
};
const tree = renderer.create(reduxWrapper(<IntlEditableSelectField {...propsWithoutGroup} />)).toJSON();
expect(tree).toMatchSnapshot();
});
it('renders selectOptions with multiple groups', () => {
const propsWithGroups = {
...props,
options: [
{
label: 'Mixed Options',
group: [
{
label: 'Suboption 1',
value: 'suboption1',
},
{
label: 'Suboption 2',
value: 'suboption2',
},
],
},
],
};
const tree = renderer.create(reduxWrapper(<IntlEditableSelectField {...propsWithGroups} />)).toJSON();
expect(tree).toMatchSnapshot();
});
});

View File

@@ -1,74 +1,59 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
import { initializeMockApp, mergeConfig, setConfig } from '@edx/frontend-platform';
import { BrowserRouter as Router } from 'react-router-dom';
import { mergeConfig, setConfig } from '@edx/frontend-platform';
import JumpNav from '../JumpNav';
import configureStore from '../../data/configureStore';
const IntlJumpNav = injectIntl(JumpNav);
describe('JumpNav', () => {
mergeConfig({
ENABLE_ACCOUNT_DELETION: true,
ENABLE_DEMOGRAPHICS_COLLECTION: false,
});
let props = {};
let store;
beforeEach(() => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
props = {
intl: {},
displayDemographicsLink: false,
};
store = configureStore({
notificationPreferences: {
showPreferences: false,
},
});
});
it('should not render Optional Information or delete account link', () => {
setConfig({
ENABLE_ACCOUNT_DELETION: false,
});
it('should not render Optional Information link', () => {
const tree = renderer.create((
<IntlProvider locale="en">
<AppProvider store={store}>
// Had to wrap the following in a router or I will receive an error stating:
// "Invariant failed: You should not use <NavLink> outside a <Router>"
<Router>
<IntlProvider locale="en">
<IntlJumpNav {...props} />
</AppProvider>
</IntlProvider>
</IntlProvider>
</Router>
))
.toJSON();
expect(tree).toMatchSnapshot();
});
it('should render Optional Information and delete account link', () => {
it('should render Optional Information link', () => {
setConfig({
ENABLE_ACCOUNT_DELETION: true,
ENABLE_DEMOGRAPHICS_COLLECTION: true,
});
props = {
...props,
displayDemographicsLink: true,
};
const tree = renderer.create((
<IntlProvider locale="en">
<AppProvider store={store}>
// Same as previous test
<Router>
<IntlProvider locale="en">
<IntlJumpNav {...props} />
</AppProvider>
</IntlProvider>
</IntlProvider>
</Router>
))
.toJSON();

View File

@@ -1,485 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EditableSelectField renders EditableSelectField correctly with editing disabled 1`] = `
<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"
>
Main Label
</h6>
<button
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil mr-1"
data-icon="pencil"
data-prefix="fas"
focusable="false"
role="img"
style={{}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
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={{}}
/>
</svg>
Edit
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Test Field
</p>
<p
className="small text-muted mt-n2"
>
Default Confirmation Message
</p>
</div>
</div>
</div>
`;
exports[`EditableSelectField renders EditableSelectField correctly with editing enabled 1`] = `
<div
className="pgn-transition-replace-group position-relative"
style={
{
"height": null,
}
}
>
<div
style={
{
"padding": ".1px 0",
}
}
>
<form
onSubmit={[Function]}
>
<div
className="pgn__form-group"
>
<label
className="pgn__form-label h6 d-block"
htmlFor="field-testField"
size="sm"
>
Main Label
</label>
<div
className="pgn__form-control-decorator-group"
>
<text
aria-describedby="field-testField-2"
className="has-value form-control is-invalid"
data-hj-suppress={true}
id="field-testField"
name="testField"
onBlur={[Function]}
onChange={[Function]}
type="text"
value="Test Field"
>
<option
value="defaultOption"
>
Default Option
</option>
<optgroup
label="User Options"
>
<option
value="suboption1"
>
Suboption 1
</option>
</optgroup>
<optgroup
label="Other Options"
>
<option
value="suboption2"
>
Suboption 2
</option>
<option
value="suboption3"
>
Suboption 3
</option>
</optgroup>
</text>
</div>
<div
className="pgn__form-text pgn__form-text-default"
>
<div>
Helpful Text
</div>
</div>
<div
className="pgn__form-control-description pgn__form-text pgn__form-text-invalid"
id="field-testField-2"
>
<span
className="pgn__icon"
>
<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 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41Z"
fill="currentColor"
/>
</svg>
</span>
<div>
</div>
</div>
</div>
<p>
<button
aria-disabled={false}
aria-live="assertive"
className="pgn__stateful-btn pgn__stateful-btn-state-default mr-2 btn btn-primary"
disabled={false}
onClick={[Function]}
type="submit"
>
<span
className="d-flex align-items-center justify-content-center"
>
<span>
Save
</span>
</span>
</button>
<button
className="btn btn-outline-primary"
disabled={false}
onClick={[Function]}
type="button"
>
Cancel
</button>
</p>
</form>
</div>
</div>
`;
exports[`EditableSelectField renders EditableSelectField with an error 1`] = `
<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"
>
Main Label
</h6>
<button
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil mr-1"
data-icon="pencil"
data-prefix="fas"
focusable="false"
role="img"
style={{}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
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={{}}
/>
</svg>
Edit
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Test Field
</p>
<p
className="small text-muted mt-n2"
>
Default Confirmation Message
</p>
</div>
</div>
</div>
`;
exports[`EditableSelectField renders selectOptions when option does not have a group 1`] = `
<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"
>
Main Label
</h6>
<button
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil mr-1"
data-icon="pencil"
data-prefix="fas"
focusable="false"
role="img"
style={{}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
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={{}}
/>
</svg>
Edit
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Test Field
</p>
<p
className="small text-muted mt-n2"
>
Default Confirmation Message
</p>
</div>
</div>
</div>
`;
exports[`EditableSelectField renders selectOptions when option has a group 1`] = `
<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"
>
Main Label
</h6>
<button
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil mr-1"
data-icon="pencil"
data-prefix="fas"
focusable="false"
role="img"
style={{}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
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={{}}
/>
</svg>
Edit
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Test Field
</p>
<p
className="small text-muted mt-n2"
>
Default Confirmation Message
</p>
</div>
</div>
</div>
`;
exports[`EditableSelectField renders selectOptions with multiple groups 1`] = `
<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"
>
Main Label
</h6>
<button
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil mr-1"
data-icon="pencil"
data-prefix="fas"
focusable="false"
role="img"
style={{}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
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={{}}
/>
</svg>
Edit
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Test Field
</p>
<p
className="small text-muted mt-n2"
>
Default Confirmation Message
</p>
</div>
</div>
</div>
`;

View File

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

View File

@@ -1,109 +0,0 @@
const mockData = {
accountSettings: {
loading: false,
loaded: true,
loadingError: null,
data: null,
values: {
username: 'test_username',
country: 'AD',
accomplishments_shared: false,
name: 'test_name',
email: 'test_email@test.com',
id: 533,
verified_name: null,
extended_profile: [
{
field_name: 'work_experience',
field_value: '',
},
],
gender: null,
'pref-lang': 'en',
},
errors: {},
confirmationValues: {},
drafts: {},
saveState: null,
timeZones: [
{
time_zone: 'Africa/Abidjan',
description: 'Africa/Abidjan (GMT, UTC+0000)',
},
],
countryTimeZones: [
{
time_zone: 'Europe/Andorra',
description: 'Europe/Andorra (CET, UTC+0100)',
},
],
previousSiteLanguage: null,
deleteAccount: {
status: null,
errorType: null,
},
siteLanguage: {
loading: false,
loaded: true,
loadingError: null,
siteLanguageList: [
{
code: 'en',
name: 'English',
released: true,
},
],
},
resetPassword: {
status: null,
},
nameChange: {
saveState: null,
errors: {},
},
thirdPartyAuth: {
providers: [
{
id: 'oa2-google-oauth2',
name: 'Google',
connected: false,
accepts_logins: true,
connectUrl: 'http://localhost:18000/auth/login/google-oauth2/?auth_entry=account_settings&next=%2Faccount%2Fsettings',
disconnectUrl: 'http://localhost:18000/auth/disconnect/google-oauth2/?',
},
],
disconnectionStatuses: {},
errors: {},
},
verifiedName: null,
mostRecentVerifiedName: {},
verifiedNameHistory: {
use_verified_name_for_certs: false,
results: [],
},
profileDataManager: null,
},
notificationPreferences: {
showPreferences: true,
courses: {
status: 'success',
courses: [],
pagination: {
count: 0,
currentPage: 1,
hasMore: false,
totalPages: 1,
},
},
preferences: {
status: 'idle',
updatePreferenceStatus: 'idle',
selectedCourse: 'account',
preferences: [],
apps: [],
nonEditable: {},
},
},
};
export default mockData;

View File

@@ -2,7 +2,7 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Hyperlink, StatefulButton } from '@openedx/paragon';
import { Hyperlink, StatefulButton } from '@edx/paragon';
import Alert from '../Alert';
import { disconnectAuth } from './data/actions';

View File

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

View File

@@ -4,10 +4,8 @@ import {
reducer as accountSettingsReducer,
storeName as accountSettingsStoreName,
} from '../account-settings';
import notificationPreferencesReducer from '../notification-preferences/data/reducers';
const createRootReducer = () => combineReducers({
[accountSettingsStoreName]: accountSettingsReducer,
notificationPreferences: notificationPreferencesReducer,
});
export default createRootReducer;

View File

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

View File

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

View File

@@ -5,14 +5,16 @@ import { getConfig } from '@edx/frontend-platform';
import messages from './messages';
const Head = ({ intl }) => (
<Helmet>
<title>
{intl.formatMessage(messages['account.page.title'], { siteName: getConfig().SITE_NAME })}
</title>
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
</Helmet>
);
function Head({ intl }) {
return (
<Helmet>
<title>
{intl.formatMessage(messages['account.page.title'], { siteName: getConfig().SITE_NAME })}
</title>
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
</Helmet>
);
}
Head.propTypes = {
intl: intlShape.isRequired,

View File

@@ -1,14 +1,14 @@
import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { Helmet } from 'react-helmet';
import { render } from '@testing-library/react';
import { mount } from 'enzyme';
import { getConfig } from '@edx/frontend-platform';
import Head from './Head';
describe('Head', () => {
const props = {};
it('should match render title tag and fivicon with the site configuration values', () => {
render(<IntlProvider locale="en"><Head {...props} /></IntlProvider>);
mount(<IntlProvider locale="en"><Head {...props} /></IntlProvider>);
const helmet = Helmet.peek();
expect(helmet.title).toEqual(`Account | ${getConfig().SITE_NAME}`);
expect(helmet.linkTags[0].rel).toEqual('shortcut icon');

View File

@@ -1,9 +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';
import {
IDLE_STATUS, LOADING_STATUS, SUCCESS_STATUS, FAILURE_STATUS,
} from './constants';
@@ -57,19 +53,3 @@ export function useRedirect() {
return redirect;
}
export function useFeedbackWrapper() {
useEffect(() => {
try {
// eslint-disable-next-line no-undef
window.usabilla_live = lightningjs?.require('usabilla_live', getConfig().LEARNER_FEEDBACK_URL);
} catch (error) {
logError('Error loading usabilla_live', error);
}
}, []);
}
export function useIsOnMobile() {
const windowSize = useWindowSize();
return windowSize.width <= breakpoints.small.minWidth;
}

View File

@@ -1 +1,32 @@
export default [];
import arMessages from './messages/ar.json';
import caMessages from './messages/ca.json';
// no need to import en messages-- they are in the defaultMessage field
import es419Messages from './messages/es_419.json';
import frMessages from './messages/fr.json';
import zhcnMessages from './messages/zh_CN.json';
import heMessages from './messages/he.json';
import idMessages from './messages/id.json';
import kokrMessages from './messages/ko_kr.json';
import plMessages from './messages/pl.json';
import ptbrMessages from './messages/pt_br.json';
import ruMessages from './messages/ru.json';
import thMessages from './messages/th.json';
import ukMessages from './messages/uk.json';
const messages = {
ar: arMessages,
'es-419': es419Messages,
fr: frMessages,
'zh-cn': zhcnMessages,
ca: caMessages,
he: heMessages,
id: idMessages,
'ko-kr': kokrMessages,
pl: plMessages,
'pt-br': ptbrMessages,
ru: ruMessages,
th: thMessages,
uk: ukMessages,
};
export default messages;

359
src/i18n/messages/ar.json Normal file
View File

@@ -0,0 +1,359 @@
{
"account.settings.message.duplicate.tpa.provider": "حساب {provider} الذي حددته موصول من قبل بحساب آخر على {siteName}.",
"account.settings.message.managed.settings": "إعدادات ملفك الشخصي يديرها {ManagerTitle}. اتصل بمديرك أو ب{support} للحصول على المساعدة.",
"account.settings.message.managed.settings.support": "الدعم",
"account.settings.page.heading": "إعدادات الحساب",
"account.settings.loading.message": "التحميل جارٍ...",
"account.settings.loading.error": "خطأ: {error}",
"account.settings.banner.beta.language": "لقد قمت بضبط لغتك على {beta_language}، وهي حاليا غير مترجمة بالكامل. يمكنك مساعدتنا في إتمام ترجمة هذه اللغة من خلال الانضمام إلى مجتمع Transifex وإضافة ترجمات من الإنجليزية للمتعلمين الذين يتحدثون {beta_language}.",
"account.settings.banner.beta.language.action.switch.back": "العودة إلى {previous_language}",
"account.settings.banner.beta.language.action.help.translate": "ساعد في الترجمة إلى {beta_language}",
"account.settings.section.account.information": "معلومات الحساب",
"account.settings.section.account.information.description": "تتضمن هذه الإعدادات معلومات أساسية عن حسابك.",
"account.settings.section.profile.information": "معلومات الملف الشخصي",
"account.settings.section.demographics.information": "معلومات اختيارية",
"account.settings.section.site.preferences": "تفضيلات الموقع",
"account.settings.section.linked.accounts": "الحسابات الموصولة",
"account.settings.section.linked.accounts.description": "يمكنك وصل حسابات هويتك لتبسيط تسجيل دخولك إلى {siteName}.",
"account.settings.field.username": "اسم المستخدم",
"account.settings.field.username.help.text": "الاسم الذي يعرّفك على {siteName}. لا يمكنك تغيير اسم المستخدم الخاص بك.",
"account.settings.field.full.name": "الاسم الكامل",
"account.settings.field.full.name.empty": "إضافة الاسم",
"account.settings.field.full.name.help.text": "الاسم المستعمل للتحقق من هويتك والذي يظهر في شهاداتك.",
"account.settings.field.full.name.help.text.default": "الاسم الذي يظهر في ملفك الشخصي العامّ.",
"account.settings.field.full.name.help.text.default.certificate": "سيظهر هذا الاسم في شهاداتك و سجلّاتك العامّة.",
"account.settings.field.name.verified": "اسم متحقَّق منه",
"account.settings.field.name.verified.help.text.verified": "تم التحقق من هذا الاسم عن طريق بطاقة تعريف ذات صورة.",
"account.settings.field.name.verified.help.text.verified.proctored": "تم التحقق من هذا الاسم عن طريق المراقبة.",
"account.settings.field.name.verified.help.text.verified.certificate": "تم التحقق من هذا الاسم عن طريق بطاقة هوية ذات صورة، وتم اختياره للظهور في الشهادات والسجلات العامة.",
"account.settings.field.name.verified.help.text.verified.proctored.certificate": "تم التحقق من هذا الاسم من خلال المراقبة، وتم تحديده للظهور في الشهادات والسجلات العامة.",
"account.settings.field.name.verified.help.text.submitted": "تم تسليم طلب التحقق. في العادة يستغرق هذا 48 ساعة أو أقل. لا يمكن تغيير الاسم المتحقَّق منه في هذا الوقت.",
"account.settings.field.name.verified.help.text.submitted.proctored": "تم تسليم امتحانك المراقَب. لا يمكن تغيير الاسم المتحقَّق منه في هذا الوقت. رجاءً أعد التفقد بعد 2-5 أيام.",
"account.settings.field.name.verified.help.text.submitted.certificate": "عند نجاح التحقق من الهوية، سيظهر هذا الاسم على شهاداتك و سجلاتك العامة. لا يمكن تغيير الاسم المتحقَّق منه في هذا الوقت.",
"account.settings.field.name.verified.help.text.submitted.proctored.certificate": "بمجرد اجتياز امتحانك المراقب للمراجعة، سيظهر هذا الاسم في شهادتك و سجلاتك العامة. لا يمكن تغيير الاسم المتحقَّق منه في هذا الوقت.",
"account.settings.field.name.verified.verification.help": "أدخل اسمك كما يظهر في بطاقة تعريف الطالب أو الموظف الخاصة بك، أو بطاقة تعريفك الصادرة عن الحكومة.",
"account.settings.field.full.name.help.text.submitted": "تم تسليم طلب التحقق. يستغرق هذا عادةً 48 ساعة أو أقل. لا يمكن تغيير الاسم الكامل في هذا الوقت.",
"account.settings.field.full.name.help.text.submitted.proctored": "تم تسليم امتحانك المراقب. لا يمكن تغيير الاسم الكامل في هذا الوقت. يرجى التحقق مرة أخرى خلال 2-5 أيام.",
"account.settings.field.full.name.help.text.submitted.certificate": "عند نجاح التحقق من الهوية، سيظهر هذا الاسم على الشهادات والسجلات العامة. لا يمكن تغيير الاسم الكامل في هذا الوقت.",
"account.settings.field.full.name.help.text.submitted.proctored.certificate": "بمجرد اجتياز امتحانك المراقب للمراجعة، سيظهر هذا الاسم في شهاداتك وسجلاتك العامة. لا يمكن تغيير الاسم الكامل في هذا الوقت.",
"account.settings.field.name.verified.success.message": "اكتمل طلب التحقق من هويتك بنجاح. لديك الآن خيار تحديد الاسم الذي تفضل ظهوره على شهاداتك وسجلاتك العامة.",
"account.settings.field.name.verified.success.message.header": "اكتمل طلب تغيير اسمك!",
"account.settings.field.name.verified.failure.message": "لم تنجح آخر محاولاتك للتحقق من هويتك. تم إرجاع إعدادات الحساب ذات الصلة إلى وضعها السابق.",
"account.settings.field.name.verified.failure.message.header": "لم نستطع التحقق من هويتك.",
"account.settings.field.name.verified.failure.message.help.link": "اعرف المزيد عن التحقق من الهوية.",
"account.settings.field.name.verified.submitted.message": "تم تسليم طلب التحقق من هويتك و عادة ما يستغرق إكماله ما بين 24 و 48 ساعة.",
"account.settings.field.name.verified.submitted.message.certificate": "عند الموافقة على طلبك، سيظهر اسمك المحدَّث على جميع الشهادات السجلات العامة ذات الصلة.",
"account.settings.field.name.verified.submitted.message.header": "طلب تغيير اسمك يكاد يكتمل!",
"account.settings.field.email": "البريد الالكتروني (تسجيل الدخول)",
"account.settings.field.email.empty": "إضافة عنوان البريد الإلكتروني",
"account.settings.field.email.confirmation": "لقد أرسلنا رسالة تأكيد إلى {value}. انقر على الرابط في الرسالة لتحديث عنوان بريدك الإلكتروني.",
"account.settings.field.email.help.text": "على هذا العنوان تصلك الرسائل من {siteName} و من فرق المساقات.",
"account.settings.field.secondary.email": "عنوان بريد الاستعادة الإلكتروني.",
"account.settings.field.secondary.email.empty": "إضافة عنوان بريد إلكتروني لاستعادة حسابك",
"account.settings.field.secondary.email.confirmation": "لقد أرسلنا رسالة تأكيد إلى {value}. انقر على الرابط في الرسالة لتحديث عنوان بريد الاستعادة الإلكتروني الخاص بك.",
"account.settings.email.field.confirmation.header": "تعليق عملية التأكيد",
"account.settings.field.dob": "سنة الميلاد",
"account.settings.field.dob.empty": "إضافة سنة الميلاد",
"account.settings.field.year_of_birth.options.empty": "تحديد سنة الميلاد",
"account.settings.field.dob.month": "الشهر",
"account.settings.field.dob.year": "السنة",
"account.settings.field.dob.form.button": "يرجى تأكيد تاريخ ميلادك",
"account.settings.field.dob.form.title": "تأكيد تاريخ ميلادك",
"account.settings.field.dob.form.help.text": "نطلب معلومة تاريخ الميلاد لمساعدتنا على الالتزام بواجباتنا القانونية.",
"account.settings.field.dob.form.success": "شكرا لك على إدخال تاريخ ميلادك.",
"account.settings.field.month_of_birth.options.empty": "تحديد شهر الميلاد",
"account.settingsfield.dob.error.general": "حدث خطأ تقني. رجاءً حاول مجددا.",
"account.settings.field.country": "البلد",
"account.settings.field.country.empty": "إضافة البلد ",
"account.settings.field.country.options.empty": "اختر البلد",
"account.settings.field.state": "المنطقة / الولاية / المحافظة",
"account.settings.field.state.empty": "إضافة المنطقة / الولاية / المحافظة",
"account.settings.field.state.options.empty": "حدد المنطقة",
"account.settings.field.site.language": "لغة الموقع",
"account.settings.field.site.language.help.text": "اللغة المستخدمة في كافة أقسام هذا الموقع. يتوفّر هذا الموقع حاليًا بعدد محدود من اللغات.",
"account.settings.field.education": "المستوى التعليمي",
"account.settings.field.education.empty": "إضافة المستوى التعليمي",
"account.settings.field.education.levels.empty": "اختر مستوى تعليميًّا",
"account.settings.field.education.levels.p": "دكتوراه",
"account.settings.field.education.levels.m": "ماجستير / ماستر أو شهادة مهنيّة",
"account.settings.field.education.levels.b": "بكالوريوس / ليسانس",
"account.settings.field.education.levels.a": "درجة الزمالة / دبلوم الدراسات الجامعية",
"account.settings.field.education.levels.hs": "الثانوية العامة / البكالوريا",
"account.settings.field.education.levels.jhs": "المدرسة الإعدادية / المتوسطة",
"account.settings.field.education.levels.el": "المدرسة الابتدائية / الأساسية",
"account.settings.field.education.levels.none": "دون تعليم رسمي",
"account.settings.field.education.levels.o": "نوع آخر من التعليم",
"account.settings.field.gender": "الجنس",
"account.settings.field.gender.empty": "إضافة الجنس",
"account.settings.field.gender.options.empty": "اختر جنسًا",
"account.settings.field.gender.options.f": "أنثى",
"account.settings.field.gender.options.m": "ذكر",
"account.settings.field.gender.options.o": "آخر",
"account.settings.field.language.proficiencies": "لغة التحدّث",
"account.settings.field.language.proficiencies.empty": "إضافة لغة التحدث",
"account.settings.field.language_proficiencies.options.empty": "اختر لغة",
"account.settings.field.time.zone": "المنطقة الزمنية",
"account.settings.field.time.zone.empty": "ضبط المنطقة الزمنية",
"account.settings.field.time.zone.description": "حدد المنطقة الزمنية لعرض تواريخ المساقات. إذا لم تحدد منطقة زمنية، فسيتم عرض تواريخ المساق، بما في ذلك المواعيد النهائية للواجبات بالتوقيت المحلّي للمتصفح.",
"account.settings.field.time.zone.default": "الافتراضي (المنطقة الزمنية المحلية)",
"account.settings.field.time.zone.all": "جميع المناطق الزمنية",
"account.settings.field.time.zone.country": "المناطق الزمنية للبلدان",
"account.settings.section.social.media": "روابط التواصل الإجتماعي",
"account.settings.section.social.media.description": "اختياريًا، اربط حساباتك الشخصية بأيقونات التواصل الاجتماعي في ملفك على {siteName}.",
"account.settings.field.social.platform.name.linkedin": "لينكد إن",
"account.settings.field.social.platform.name.linkedin.empty": "إضافة سيرة لينكد إن",
"account.settings.jump.nav.delete.account": "حذف حسابي",
"account.settings.field.social.platform.name.twitter": "تويتر",
"account.settings.field.social.platform.name.twitter.empty": "إضافة صفحة تويتر",
"account.settings.field.social.platform.name.facebook": "فيسبوك",
"account.settings.field.social.platform.name.facebook.empty": "إضافة حساب فيسبوك",
"account.settings.editable.field.action.save": "حفظ",
"account.settings.editable.field.action.cancel": "إلغاء",
"account.settings.editable.field.action.edit": "تعديل",
"account.settings.static.field.empty": "لا قيمة محددة. رجاءً اتصل بمديرك في {enterprise} ليقوم بالتعديلات.",
"account.settings.static.field.empty.no.admin": "لا قيمة محددة.",
"account.settings.field.name.certificate.select": "في حال التأشير، سيظهر هذا الاسم في شهاداتك و سجلاتك العامة.",
"account.settings.field.name.modal.certificate.title": "اختر اسمًا مفضلًا للشهادات والسجلات العامة",
"account.settings.field.name.modal.certificate.select": "اختر اسمًا",
"account.settings.field.name.modal.certificate.option.full": "الاسم الكامل",
"account.settings.field.name.modal.certificate.option.verified": "اسم متحقَّق منه",
"account.settings.field.name.modal.certificate.button.choose": "اختيار الاسم",
"account.settings.coaching.consent.welcome.header": "لنبدأ.",
"account.settings.coaching.consent.welcome.subheader": "نحن هنا لأجلك من البداية حتى النهاية",
"account.settings.coaching.consent.description": "تتضمن برامج MicroBachelors مرافقة تركز على مهنتك و تعليمك و كيفية تحقيقك للنتائج، و ذلك من خلال التواصل الفردي مع خبير متمرس. إن كنت مهتمًا، فيرجى تزويدنا بالمعلومات أدناه و النقر على \"إرسال\"، و سيتصل بك شريكنا في المرافقة عبر البريد الإلكتروني و/أو الرسائل النصية لمساعدتك على المضي قدمًا. تنطبق الشروط والأحكام.*",
"account.settings.coaching.consent.text-messaging.disclaimer": "* خدمات المرافقة مشمولة دون تكاليف إضافية للمتعلمين الذين لديهم أرقام هواتف أمريكية. تتضمن المرافقة رسائل نصية دورية. قد تنطبق أسعار على الرسائل والبيانات. أرسل STOP للانسحاب.",
"account.settings.coaching.consent.accept-coaching": "سجّل للاستفادة من خدمات المرافقة",
"account.settings.coaching.consent.decline-coaching": "أفضّل ألا يٌتصَل بي بخصوص خدمات المرافقة المجانية",
"account.settings.coaching.consent.label.name": "رجاءً أكّد اسمك",
"account.settings.coaching.consent.label.phone-number": "أدخل رقم هاتفك الجوّال",
"account.settings.coaching.consent.success.header": "نجحت العملية!",
"account.settings.coaching.consent.success.message": "أنت الآن مشترك في المرافقة. ترقّب رسالة عبر البريد الإلكتروني أو خدمة الرسائل القصيرة في في الأيام المقبلة.",
"account.settings.coaching.consent.success.continue": "البدء في مساقي",
"account.settings.coaching.managed.support": "الدعم",
"account.settings.coaching.managed.alert": "اسمك يديره {ManagerTitle}. اتصل بمديرك للحصول على المساعدة.",
"account.settings.field.phone_number": "رقم الهاتف",
"account.settings.field.phone_number.empty": "إضافة رقم هاتف",
"account.settings.field.coaching_consent": "الموافقة على المرافقة",
"account.settings.field.coaching_consent.tooltip": "تتضمن برامج MicroBachelors مرافقة قائمة على الرسائل النصية، تساعدك على إقران تجاربك التعلّمية مع أهدافك المهنية من خلال النصائح الفردية. خدمات المرافقة مشمولة دون تكاليف إضافية، وهي متوفرة للمتعلمين الذين لديهم أرقام هواتف جوالة أمريكية. تنطبق أسعار المراسلة القياسية. أرسل 'STOP' في أي وقت للانسحاب من الرسائل.",
"account.settings.field.coaching_consent.error": "مطلوب رقم هاتف أمريكي صحيح للتسجيل في المرافقة",
"account.settings.delete.account.before.proceeding": "قبل المتابعة، يرجى {actionLink}.",
"account.settings.delete.account.header": "حذف حسابي",
"account.settings.delete.account.subheader": "نأسف لذهابك!",
"account.settings.delete.account.text.1": "ترجى الملاحظة: إن حذف حسابك وبياناتك الشخصية له أثر دائم و لا يمكن التراجع عنه. لن يكون بمقدور {siteName} استعادة حسابك ولا البيانات التي يتم حذفها.",
"account.settings.delete.account.text.2": "بمجرد حذف حسابك، فإنك لن تستطيع استخدامه لمتابعة المساقات على {siteName}.",
"account.settings.delete.account.text.2.edX": "بمجرد حذف حسابك، فإنك لن تستطيع استخدامه لمتابعة المساقات على تطبيق edX ولا edx.org ولا أي موقع آخر تستضيفه edX. وهذا يشمل الوصول إلى edx.org من نظام صاحب العمل أو الجامعة و الوصول إلى المواقع الخاصة التي تقدمها MIT Open Learning و Wharton Executive Education و Harvard Medical School.",
"account.settings.delete.account.text.3.link": "اتّبع هذه التعليمات لطباعة أو تحميل شهادة",
"account.settings.delete.account.text.warning": "تحذير: حذف الحساب أثره دائم. يرجى قراءة ما ورد أعلاه بعناية قبل المتابعة. هذا إجراء غير رجعي، و لن تتمكن بعده من استخدام نفس البريد الإلكتروني على {siteName}.",
"account.settings.delete.account.text.change.instead": "هل تريد بدلاً من ذلك تغيير بريدك الإلكتروني أو اسمك أو كلمة المرور الخاصة بك؟",
"account.settings.delete.account.button": "حذف حسابي",
"account.settings.delete.account.please.activate": "تفعيل حسابك",
"account.settings.delete.account.please.confirm": "تأكيد حسابك",
"account.settings.delete.account.please.unlink": "فصل جميع حسابات التواصل الاجتماعي",
"account.settings.delete.account.modal.header": "هل أنت متأكد؟",
"account.settings.delete.account.modal.text.1": "لقد اخترت \"حذف حسابي\". إن حذف حسابك وبياناتك الشخصية ذو أثر دائم لا يمكن التراجع عنه. لن يكون بمقدور {siteName} استعادة حسابك و لا البيانات التي حذفت.",
"account.settings.delete.account.modal.text.2": "إن واصلت، فلن تستطيع استخدام هذا الحساب لمتابعة المساقات على {siteName}.",
"account.settings.delete.account.modal.text.2.edX": "إن واصلت، فلن تستطيع استخدام هذا الحساب لمتابعة المساقات على على تطبيق edX و لا edx.org و لا أي موقع آخر تستضيفه edX. وهذا يشمل الوصول إلى edx.org من نظام صاحب العمل أو الجامعة و الوصول إلى المواقع الخاصة التي يقدمها MIT Open Learning و Wharton Executive Education و Harvard Medical School.",
"account.settings.delete.account.modal.enter.password": "إن كنت لا تزال ترغب في المتابعة و حذف حسابك، فيرجى إدخال كلمة المرور:",
"account.settings.delete.account.modal.confirm.delete": "تعم، احذف",
"account.settings.delete.account.modal.confirm.cancel": "لا",
"account.settings.delete.account.error.unable.to.delete": "لم نستطع حذف الحساب",
"account.settings.delete.account.error.no.password": "كلمة المرور مطلوبة",
"account.settings.delete.account.error.invalid.password": "كلمة المرور غير صحيحة",
"account.settings.delete.account.error.unable.to.delete.details": "عذراً، حدث خطأ أثناء محاولة معالجة طلبك. رجاءً أعد المحاولة لاحقًا.",
"account.settings.delete.account.modal.after.header": "نأسف لذهابك! سيُحذف حسابك في ظرف وجيز.",
"account.settings.delete.account.modal.after.text": "حذف الحساب، بما في ذلك من إزالة من القوائم البريدية، إجراء قد يستغرق بضعة أسابيع حتى يكتمل عبر نظامنا. إن كنت تريد قبل ذلك الحين إيقاف تلقي البريد الإلكتروني، فيرجى إلغاء الاشتراك من تذييل أي بريد إلكتروني.",
"account.settings.delete.account.modal.after.button": "إغلاق ",
"account.settings.delete.account.text.3.edX": "قد تفقد كذلك إمكانية الوصول إلى الشهادات الموثقة و كذا مؤهلات البرامج الأخرى مثل شهادات MicroMasters. يمكنك الاحتفاظ بنسخة عنها لديك قبل المواصلة إلى الحذف. {actionLink}.",
"account.settings.delete.account.text.3": "قد تفقد كذلك إمكانية الوصول إلى الشهادات الموثقة و كذا مؤهلات البرامج الأخرى. يمكنك الاحتفاظ بنسخة عنها لديك قبل المواصلة إلى الحذف.",
"account.settings.message.demographics.service.issue": "حدث خطأ أثناء محاولة استخراج أو حفظ معلومات حسابك. رجاءً أعد المحاولة لاحقًا.",
"account.settings.field.demographics.gender": "هوية الجنس",
"account.settings.field.demographics.gender.empty": "إضافة هوية الجنس",
"account.settings.field.demographics.gender.options.empty": "حدد هوية الجنس",
"account.settings.field.demographics.gender_description": "وصف هوية الجنس",
"account.settings.field.demographics.gender_description.empty": "أدخل الوصف",
"account.settings.field.demographics.ethnicity": "هوية العرق/ الأصل",
"account.settings.field.demographics.ethnicity.empty": "إضافة هوية العرق / الأصل",
"account.settings.field.demographics.ethnicity.options.empty": "اختر كل ما ينطبق",
"account.settings.field.demographics.income": "دخل الأسرة",
"account.settings.field.demographics.income.empty": "إضافة دخل الأسرة",
"account.settings.field.demographics.income.options.empty": "حدد نطاقًا لدخل الأسرة",
"account.settings.field.demographics.military_history": "الوضعية إزاء الخدمة العسكرية في الولايات المتحدة",
"account.settings.field.demographics.military_history.empty": "إضافة الوضعية إزاء الخدمة العسكرية",
"account.settings.field.demographics.military_history.options.empty": "اختر الوضعية إزاء الخدمة العسكرية",
"account.settings.field.demographics.learner_education_level": "مستواك التعليمي",
"account.settings.field.demographics.learner_education_level.empty": "إضافة المستوى التعليمي",
"account.settings.field.demographics.parent_education_level": "مستوى الوالدين/الأولياء التعليمي",
"account.settings.field.demographics.parent_education_level.empty": "إضافة المستوى التعليمي",
"account.settings.field.demographics.education_level.options.empty": "حدد المستوى التعليمي",
"account.settings.field.demographics.work_status": "الحالة الوظيفية",
"account.settings.field.demographics.work_status.empty": "إضافة الحالة الوظيفية",
"account.settings.field.demographics.work_status.options.empty": "حدد الحالة الوظيفية",
"account.settings.field.demographics.work_status_description": "وصف الحالة الوظيفية",
"account.settings.field.demographics.work_status_description.empty": "أدخل الوصف",
"account.settings.field.demographics.current_work_sector": "مجال العمل الحالي",
"account.settings.field.demographics.current_work_sector.empty": "إضافة مجال العمل",
"account.settings.field.demographics.future_work_sector": "مجال العمل المستقبلي",
"account.settings.field.demographics.future_work_sector.empty": "إضافة مجال العمل",
"account.settings.field.demographics.work_sector.options.empty": "حدد مجال العمل",
"account.settings.section.demographics.why": "ما هي غاية {siteName} من جمع هذه المعلومات؟",
"account.settings.name.change.title.id": "تغيير الاسم هذا يتطلب التحقق من الهوية",
"account.settings.name.change.title.begin": "قبل أن نبدأ",
"account.settings.name.change.warning.one": "تحذير: يقوم هذا الإجراء بتحديث الاسم الذي يظهر على جميع الشهادات التي تم الحصول عليها على هذا الحساب في الماضي و أي شهادات تحصل عليها حاليا أو مستقبلاً.",
"account.settings.name.change.warning.two": "لا يمكن التراجع عن هذا الإجراء دون التحقق من هويتك.",
"account.settings.name.change.id.name.label": "أدخل اسمك كما يظهر في بطاقة تعريف الطالب أو العمل أو بطاقة الهوية الصادرة عن الحكومة.",
"account.settings.name.change.id.name.placeholder": "أدخل الاسم الموجود في بطاقة تعريفك ذات الصورة.",
"account.settings.name.change.error.valid.name": "رجاءً أدخل اسما صحيحا.",
"account.settings.name.change.error.general": "حدث خطأ تقني. رجاءً حاول مجددًا.",
"account.settings.name.change.continue": "مواصلة",
"account.settings.name.change.cancel": "إلغاء",
"error.notfound.message": "الصفحة التي تبحث عنها غير متوفرة أو هناك خطأ في العنوان. رجاءً تحقق من العنوان و حاول مجددًا.",
"account.settings.editable.field.password.reset.button.confirmation.support.link": "الدعم الفني",
"account.settings.editable.field.password.reset.button.confirmation": "لقد أرسلنا رسالة إلى {email}. انقر على الرابط في الرسالة لإعادة ضبط كلمة المرور الخاصة بك. لم تصلك الرسالة؟ اتصل بـ{technicalSupportLink}.",
"account.settings.editable.field.password.reset.button": "إعادة ضبط كلمة المرور",
"account.settings.editable.field.password.reset.button.forbidden": "طلبك السابق في تقدّم، رجاءً حاول مجددًا بعد لحظات قليلة.",
"account.settings.editable.field.password.reset.label": "كلمة المرور",
"account.settings.sso.link.account": "تسجيل الدخول باستخدام {name}",
"account.settings.sso.account.connected": "موصول",
"account.settings.sso.account.disconnect.error": "حدثت مشكلة أثناء فصل هذا الحساب، اتصل بالدعم إن استمرت المشكلة.",
"account.settings.sso.unlink.account": "فصل حساب {name}",
"account.settings.sso.no.providers": "لا يمكن وصل أي حسابات في الوقت الراهن.",
"account.page.title": "الحساب | {siteName}",
"id.verification.access.blocked.denied": "لا يمكننا التحقق من هويتك في الوقت الراهن. إن لم تكن قد فعّلت حسابك بعد، فيرجى تفقد مجلد الرسائل غير المرغوب فيها بحثًا عن بريد التفعيل الإلكتروني من {email}.",
"id.verification.next": "التالي",
"id.verification.support": "الدعم",
"id.verification.example.card.alt": "مثال بطاقة تعريف صحيحة بالاسم الكامل والصورة.",
"id.verification.requirements.title": "متطلبات التحقق باستخدام الصورة",
"id.verification.requirements.description": "لإكمال التحقق بالصورة، ستحتاج ما يلي:",
"id.verification.requirements.card.device.title": "جهاز مزود بكاميرا",
"id.verification.requirements.card.device.allow": "السماح",
"id.verification.requirements.card.id.title": "بطاقة تعريف بها صورة",
"id.verification.requirements.card.id.text": "تحتاج إلى بطاقة تعريف صالحة تحتوي اسمك الكامل وصورتك، كرخصة القيادة أو جواز السفر مثلاً.",
"id.verification.privacy.title": "معلومات الخصوصية",
"id.verification.privacy.need.photo.question": "ما حاجة {siteName} لصورتي؟",
"id.verification.privacy.need.photo.answer": "نستخدم صور التحقق الخاصة بك لتأكيد هويتك ولضمان صحة شهادتك.",
"id.verification.privacy.do.with.photo.question": "ماذا يُفعَل بهذه الصورة في {siteName}؟",
"id.verification.privacy.do.with.photo.answer": "نقوم بتشفير صورتك بشكل آمن وإرسالها إلى خدمة الترخيص الخاصة بنا للمراجعة. صورتك و معلوماتك لا تُحفَظ و لا تظهر في أي مكان على {siteName} بعد اكتمال عملية التحقق.",
"id.verification.access.blocked.title": "التحقق من الهوية",
"id.verification.access.blocked.enrollment": "أنت حاليا غير ملتحق بأي مساق يتطلب التحقق من الهوية.",
"id.verification.access.blocked.pending": "لقد سلّمت من قبل معلومات التحقق الخاصة بك. ستصلك رسالة على لوحة المعلومات الخاصة بك عند اكتمال عملية التحقق (عادةً في ظرف 5 أيام).",
"id.verification.photo.take": "التقاط صورة ",
"id.verification.photo.retake": "إعادة التقاط الصورة؟",
"id.verification.photo.enable.detection": "تفعيل اكتشاف الوجوه",
"id.verification.photo.enable.detection.portrait.help.text": "في حال التأشير، سيبرز مربع حول وجهك. يمكن رؤية وجهك بوضوح إن كان المربع المحيط به أزرق اللون. أما إن كان وجهك في وضع غير جيد أو غير قابل للاكتشاف، فسيكون المربع أحمر اللون.",
"id.verification.photo.enable.detection.id.help.text": "في حال التأشير، سيظهر مربع حول صورة وجهك في بطاقة الهوية. يمكن رؤية وجهك بوضوح إن كان المربع المحيط به أزرق اللون. أما إن كان وجهك في وضع غير جيد أو غير قابل للاكتشاف، فسيكون المربع أحمر اللون.",
"id.verification.photo.feedback.correct": "موضع الوجه جيد.",
"id.verification.photo.feedback.two.faces": "تم اكتشاف أكثر من وجه واحد.",
"id.verification.photo.feedback.no.faces": "لم يتم اكتشاف أي وجه.",
"id.verification.photo.feedback.top.left": "الموضع خاطئ. أعلى اليسار.",
"id.verification.photo.feedback.top.center": "الموضع خاطئ. أعلى الوسط.",
"id.verification.photo.feedback.top.right": "الموضع خاطئ. أعلى اليمين.",
"id.verification.photo.feedback.center.left": "الموضع خاطئ. وسط اليسار.",
"id.verification.photo.feedback.center.center": "الموضع خاطئ. قريب جدًا من الكاميرا.",
"id.verification.photo.feedback.center.right": "الموضع خاطئ. وسط اليمين.",
"id.verification.photo.feedback.bottom.left": "الموضع خاطئ. أسفل اليسار.",
"id.verification.photo.feedback.bottom.center": "الموضع خاطئ. أسفل الوسط.",
"id.verification.photo.feedback.bottom.right": "الموضع خاطئ. أسفل اليمين.",
"id.verification.camera.access.title": "صلاحيات الكاميرا",
"id.verification.camera.access.title.success": "الوصول للكاميرا ممكن",
"id.verification.camera.access.title.failed": "تعذّر الوصول للكاميرا.",
"id.verification.camera.access.click.allow": "رجاءً تأكد من النقر على \"السماح\"",
"id.verification.camera.access.enable": "تفعيل الكاميرا",
"id.verification.camera.access.problems": "لديك مشاكل؟",
"id.verification.camera.access.skip": "تَخَطَّ و قم برفع ملفات صور بدلاً من ذلك.",
"id.verification.camera.access.success": "يبدو أن كاميرا جهازك جاهزة و تعمل.",
"id.verification.camera.access.failure": "يبدو أننا غير قادرين على الوصول إلى كاميرا جهازك. ستحتاج لرفع ملف صورتك و صورة بطاقة هويتك.",
"id.verification.camera.access.failure.temporary": "يبدو أننا غير قادرين على الوصول إلى الكاميرا. رجاءً تحقق من أن كاميرا جهازك متصلة ومن أنك قد سمحت للمتصفح بالوصول إليها.",
"id.verification.camera.access.failure.temporary.chrome": "لتفعيل الوصول للكاميرا في متصفح كروم:",
"id.verification.camera.access.failure.temporary.chrome.step1": "افتح كروم.",
"id.verification.camera.access.failure.temporary.chrome.step2": "توجه إلى المزيد > الإعدادات.",
"id.verification.camera.access.failure.temporary.chrome.step2.windows": "لنظام ويندوز: Alt+F أو Alt+E أو F10 متبوعاً بـ SPACEBAR",
"id.verification.camera.access.failure.temporary.chrome.step2.mac": "لنظام ماك: Command+,",
"id.verification.camera.access.failure.temporary.chrome.step3": "تحت علامة التبويب \"الخصوصية والأمان\"، اختر \"إعدادات الموقع\" ثم \"الكاميرا\".",
"id.verification.camera.access.failure.temporary.chrome.step4": "ضمن \"قائمة الحظر\"، ابحث عن \"edx.org\" وحدده.",
"id.verification.camera.access.failure.temporary.chrome.step5": "في قسم \"الصلاحيات\"، قم بتحديث صلاحيات الكاميرا إلى \"السماح\".",
"id.verification.camera.access.failure.temporary.ie11": "لتفعيل الوصول للكاميرا في متصفح انترنت إكسبلورر:",
"id.verification.camera.access.failure.temporary.ie11.step1": "افتح إدارة إعدادات Flash Player عن طريق الانتقال إلى إعدادات Windows > لوحة التحكم > Flash Player.",
"id.verification.camera.access.failure.temporary.ie11.step2": "حدد علامة التبويب \"Camera and Mic\" (الكاميرا والميكروفون)، ثم حدد الزر \"Camera and Microphone Settings by Site\" (إعدادات الكاميرا والميكروفون حسب الموقع).",
"id.verification.camera.access.failure.temporary.ie11.step3": "اختر \"edx.org\" من قائمة مواقع ويب وقم بتغيير الصلاحيات من خلال تحديد \"السماح\" في القائمة المنسدلة.",
"id.verification.camera.access.failure.temporary.firefox": "لتمكين الوصول للكاميرا في متصفّح فايرفوكس: ",
"id.verification.camera.access.failure.temporary.firefox.step1": "فتح متصفّح فايرفوكس.",
"id.verification.camera.access.failure.temporary.firefox.step2": "أدخل \"about:preferences\" في شريط عنوان الرابط.",
"id.verification.camera.access.failure.temporary.firefox.step3": "حدد علامة التبويب \"الخصوصية والأمان\"، وانتقل إلى القسم \"الصلاحيات\".",
"id.verification.camera.access.failure.temporary.firefox.step4": "بجوار \"Camera\" (الكاميرا)، حدد \"Settings…\" (الإعدادات…) الزر.",
"id.verification.camera.access.failure.temporary.firefox.step5": "في شريط البحث أدخل \"edx.org.\"",
"id.verification.camera.access.failure.temporary.firefox.step6": "في عمود الحالة لـ \"edx.org,\" حدد \"السماح\" من القائمة المنسدلة.",
"id.verification.camera.access.failure.temporary.firefox.step7": "اختر \"حفظ التغييرات.\"",
"id.verification.camera.access.failure.temporary.safari": "تمكين الوصول للكاميرا في متصفّح سفاري: ",
"id.verification.camera.access.failure.temporary.safari.step1": "فتح متصفّح سفاري.",
"id.verification.camera.access.failure.temporary.safari.step2": "انقر فوق قائمة تطبيق سفاري، ثم حدد \"Preferences\" (التفضيلات). يمكنك أيضاً استخدام الأمرCommand+, كاختصار للوحة المفاتيح",
"id.verification.camera.access.failure.temporary.safari.step3": "حدد علامة التبويب \"مواقع الويب\" ثم حدد \"كاميرا\".",
"id.verification.camera.access.failure.temporary.safari.step4": "حدد \"edx.org\" وقم بتغيير صلاحيات الكاميرا إلى \"السماح\".",
"id.verification.camera.access.failure.unsupported": "يبدو أن متصفحك لا يدعم الوصول إلى الكاميرا.",
"id.verification.camera.access.failure.unsupported.chrome.explanation": "لا يدعم متصفح Chrome حاليًا الوصول إلى الكاميرا على أجهزة iOS مثل أجهزة iPhone و iPad.",
"id.verification.camera.access.failure.unsupported.instructions": "رجاءً استخدم متصفحًا آخر لإكمال التحقق من الهوية.",
"id.verification.photo.tips.title": "نصائح مفيدة بخصوص الصورة",
"id.verification.photo.tips.description": "بعد ذلك، سنحتاج منك التقاط صورة لوجهك. يرجى مراجعة النصائح المفيدة أدناه.",
"id.verification.photo.tips.list.title": "نصائح بخصوص الصورة",
"id.verification.photo.tips.list.description": "لالتقاط صورة ناجحة، يُرجى التأكّد ممّا يلي:",
"id.verification.photo.tips.list.well.lit": "أنّ وجهك مُضاء جيدًا.",
"id.verification.photo.tips.list.inside.frame": "أنّ وجهك كلَّه داخل إطار الصورة.",
"id.verification.portrait.photo.title.camera": "التقط صورة لنفسك",
"id.verification.portrait.photo.instructions.camera": "عندما يكون وجهك في موضعه، استخدم زر 'التقاط صورة' أدناه لالتقاط صورتك.",
"id.verification.camera.help.sight.question": "ماذا إن لم أتمكن من رؤية صورة الكاميرا أو إن لم أتمكن من رؤية صورتي لتحديد أي جانب مرئي؟",
"id.verification.camera.help.sight.answer.portrait": "قد تتمكن من إكمال إجراء التقاط الصور دون مساعدة، لكن قد يتطلب الأمر بضع محاولات لضبط موضع الكاميرا بشكل صحيح.يختلف موضع الكاميرا المثالي من حاسوب ﻵخر، لكن عمومًا يكون أفضل موضع لتصوير الرأس تقريبًا على بعد 12-18 بوصة (30-45 سنتمترًا) من الكاميرا، مع وضع رأسك في المنتصف بالنسبة إلى شاشة الحاسوب. إن تم رفض الصور التي ترسلها، فحاول تغيير اتجاه الكمبيوتر أو الكاميرا لتغيير زاوية الإضاءة.",
"id.verification.camera.help.sight.answer.id": "قد تتمكن من إكمال إجراء التقاط الصور دون مساعدة، لكن قد يتطلب الأمر بضع محاولات لضبط موضع الكاميرا بشكل صحيح.يختلف موضع الكاميرا المثالي من حاسوب ﻵخر، لكن عمومًا يكون أفضل موضع لتصوير بطاقة تعريف تقريبًا على بعد 8-12 بوصة (20-30 سنتمترًا) من الكاميرا، مع وضع البطاقة في المنتصف بالنسبة للكاميرا. إن تم رفض الصور التي ترسلها، فحاول تغيير اتجاه الكمبيوتر أو الكاميرا لتغيير زاوية الإضاءة. إن أكثر سبب للرفض هو عدم القدرة على قراءة نص بطاقة التعريف.",
"id.verification.camera.help.difficulty.question.portrait": "ماذا لو واجهت صعوبة في تثبيت رأسي في الموضع المناسب للكاميرا؟",
"id.verification.camera.help.difficulty.question.id": "ماذا لو واجهت صعوبة في تثبيت بطاقة هويتي في الموضع المناسب للكاميرا؟",
"id.verification.camera.help.difficulty.answer": "إن احتجت لمساعدة في التقاط صورة لإرسالها، فاتصل بدعم {siteName} للحصول على اقتراحات إضافية.",
"id.verification.id.photo.unclear.question": "هل صورة بطاقة تعريفك غير واضحة أو ضبابية جدًا؟",
"id.verification.id.tips.title": "نصائح مفيدة بخصوص بطاقة التعريف",
"id.verification.id.tips.description": "بعد ذلك، عليك التقاط صورة لبطاقة تعريف صالحة تتضمن اسمك الكامل مع صورة، مثل رخصة القيادة أو جواز السفر.يرجى منك تجهيز بطاقة تعريفك.",
"id.verification.id.tips.list.well.lit": "أن تكون بطاقة تعريفك مضاءة جيدًا.",
"id.verification.id.tips.list.clear": "تأكد من قدرتك على رؤية صورتك وقراءة اسمك بوضوح.",
"id.verification.id.photo.title.camera": "التقط صورة لبطاقة تعريفك",
"id.verification.id.photo.title.upload": "ارفع صورة لبطاقة تعريفك",
"id.verification.id.photo.preview.alt": "معاينة صورة الهوية.",
"id.verification.id.photo.instructions.camera": "عندما تكون بطاقتك في موضعها، استخدم زر 'التقاط صورة' أدناه لالتقاط صورتك. يرجى استخدام جواز سفر أو رخصة قيادة أو بطاقة تعريف أخرى تتضمن اسمك الكامل وصورة لوجهك.",
"id.verification.id.photo.instructions.upload": "يرجى رفع صورة لبطاقة تعريفك. تأكد من أن البطاقة كاملة داخل الإطار و أنها مضاءة جيدًا. يجب أن يكون حجم الملف أقل من 10 ميجابايت. الأنساق المدعومة:",
"id.verification.id.photo.instructions.upload.error.invalidFileType": "الملف الذي حددته ليس ضمن أنواع الصورة المدعومة. اختر رجاءً من بين الأنساق التالية:",
"id.verification.id.photo.instructions.upload.error.fileTooLarge": "الملف الذي حددته كبير جداً. رجاءً أعد المحاولة باستخدام ملف ذي حجم أقل من 10 ميجابايت.",
"id.verification.name.check.title": "تحقق مرة أخرى من اسمك",
"id.verification.name.check.instructions": "هل الاسم أدناه يطابق الاسم الموجود في بطاقة تعريفك ذات الصورة. إن لم يكن كذلك، فقم بتحديث الاسم أدناه ليطابق بطاقة تعريفك.",
"id.verification.name.check.mismatch.information": "إن كان الاسم أدناه لا يطابق بطاقة تعريفك، فسيتم رفض تأكيد هويتك.",
"id.verification.name.error": "رجاءً أدخل اسمك كما يظهر في بطاقة تعريفك ذات الصورة.",
"id.verification.account.name.warning.prefix": "تُرجى الملاحظة:",
"id.verification.account.name.settings": "إعدادات الحساب",
"id.verification.name.label": "الاسم",
"id.verification.account.name.photo.alt": "صورة بطاقة هويتك التي ستُسلَّم.",
"id.verification.review.title": "مراجعة صورك",
"id.verification.review.description": "يُرجى التأكّد من أنّ الصور والمعلومات التي قدّمتها تمكّننا من التحقّق من هويّتك. ",
"id.verification.review.portrait.label": "صورتك الشخصية",
"id.verification.review.portrait.alt": "صورة وجهك التي ستُسلَّم.",
"id.verification.review.portrait.retake": "إعادة التقاط الصورة الشخصية",
"id.verification.review.id.label": "بطاقة تعريفك",
"id.verification.review.id.alt": "صورة بطاقة تعريفك التي ستُسلَّم.",
"id.verification.review.id.retake": "إعادة التقاط صورة بطاقة الهوية",
"id.verification.review.confirm": "تسليم",
"id.verification.submission.alert.error.face": "مطلوبة صورة لوجهك. رجاءً أعد التقاط صورتك الشخصية.",
"id.verification.submission.alert.error.id": "مطلوبة صورة لبطاقة تعريفك. رجاءً أعد التقاط صورة لبطاقة تعريفك.",
"id.verification.submission.alert.error.name": "مطلوب اسم حساب صحيح. يرجى تحديث اسم حسابك لمطابقة الاسم على بطاقة تعريفك.",
"id.verification.submission.alert.error.unsupported": "واحد أو أكثر من الملفات التي قمت برفعها في نَسقٍ غير مدعوم. رجاءً اختر مما يلي:",
"id.verification.review.error": "{siteName} صفحة دعم",
"id.verification.submitted.title": "التحقق من الهوية جارٍ",
"id.verification.submitted.text": "لقد تلقينا معلوماتك و نحن الآن نتحقق من هويتك. سيتم إخطارك عند اكتمال عملية التحقق (عادةً في ظرف 5 أيام). إلى ذلكم الحين، لا يزال يمكنك الوصول لجميع محتويات المساق المتاحة.",
"id.verification.return.dashboard": "العودة للوحة المعلومات",
"id.verification.return.course": "العودة للمساق",
"id.verification.return.generic": "العودة",
"id.verification.photo.upload.help.title": "قم يدلا من هذا برفع صورة",
"id.verification.photo.camera.help.title": "استخدم الكاميرا بدلاً من هذا",
"id.verification.photo.upload.help.text": "إن واجهتك مشكلة في استخدام مُلتقط الصور أعلاه، فقد ترغب بدﻷ من ذلك في رفع صورة. لرفع صورة، انقر على الزر أدناه.",
"id.verification.photo.camera.help.text": "إن واجهتك مشكلة في رفع صورة أعلاه، فقد ترغب بدلا من ذلك في استخدام كاميرا جهازك. لاستخدام الكاميرا، انقر على الزر أدناه.",
"id.verification.upload.help.button": "انتقل إلى وضع الرفع",
"id.verification.camera.help.button": "انتقل إلى وضع الكاميرا",
"id.verification.request.camera.access.instructions": "حتى تلتقط صورة باستخدام الكاميرا، قد تتلقى طلبًا من المتصفح للوصول إلى الكاميرا. {clickAllow}",
"id.verification.requirements.account.managed.alert": "إعدادات حسابك يديرها {managerTitle}. إن لم يكن الاسم في بطاقة هويتك ذات الصورة. مطابقًا للاسم الذي في حسابك، فيرجى الاتصال بالمسؤول {profileDataManager} أو ب{support} للحصول على المساعدة قبل إتمام عملية التحقق من الصورة..",
"id.verification.requirements.card.device.text": "أنت بحاجة إلى جهاز مزود بكاميرا. إذا تلقيت طلبًا من المتصفح للوصول إلى كاميرا جهازك، فتأكد رجاءً من النقر على {السماح}.",
"id.verification.account.name.summary.alert": "إعدادات حسابك يديرها {managerTitle}. إن لم يكن الاسم في بطاقة هويتك ذات الصورة. مطابقًا للاسم الذي في حسابك، فيرجى الاتصال بالمسؤول {profileDataManager} أو ب{support} للحصول على المساعدة قبل إتمام عملية التحقق من الصورة..",
"idv.submission.alert.error": "\nواجهنا خطأ فني أثناء محاولة رفع طلب التحقق من الهوية\nقد تكون هذه مشكلة مؤقتة، لذا يرجى المحاولة مجددًا بعد بضع دقائق\nإن استمرت المشكلة، فيرجى الذهاب إلى {support_link} للحصول على المساعدة.",
"id.verification.account.name.edit": "تعديل {sr}"
}

Some files were not shown because too many files have changed in this diff Show More